diff --git a/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/Hawkeye.VisionBuilder.UI.Sources.Hawkeye.csproj b/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/Hawkeye.VisionBuilder.UI.Sources.Hawkeye.csproj
new file mode 100644
index 0000000..1d21770
--- /dev/null
+++ b/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/Hawkeye.VisionBuilder.UI.Sources.Hawkeye.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/HawkeyeCameraImageSource.cs b/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/HawkeyeCameraImageSource.cs
new file mode 100644
index 0000000..7f47d32
--- /dev/null
+++ b/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/HawkeyeCameraImageSource.cs
@@ -0,0 +1,133 @@
+using Inspectron.HawkEye.Protocol;
+using OpenCvSharp;
+using Serilog;
+using System.Net;
+using System.Runtime.InteropServices;
+using VisionBuilder.UI.Common;
+using VisionBuilder.UI.Common.RecipeProcessing;
+using CameraSettings = Inspectron.HawkEye.Protocol.CameraSettings;
+
+namespace Hawkeye.VisionBuilder.UI.Sources.Hawkeye
+{
+ public class HawkeyeCameraImageSource : IImageSource, IVisionBuilderModule
+ {
+ private readonly HawkeyeSettings _settings;
+ private ImageClientTCP _client;
+ private CameraSettings _cameraSettings;
+
+
+ public HawkeyeCameraImageSource(HawkeyeSettings settings)
+ {
+ _settings = settings;
+ }
+
+ public void Open()
+ {
+
+ _client = new ImageClientTCP(new IPEndPoint(IPAddress.Parse("192.168.3.15"), 27001),
+ IPAddress.Parse(_settings.Adapter));
+ _client.ImageReceived += _client_ImageReceived;
+ _client.SettingsReceived += _client_SettingsReceived;
+ _client.Connect();
+ Log.Information("Connected to Hawkeye camera at {Adapter}", _settings.Adapter, 27001);
+
+ }
+ private void _client_SettingsReceived(CameraSettings obj)
+ {
+ ApplySettings(obj);
+ }
+ public void ApplySettings(CameraSettings obj)
+ {
+ _cameraSettings = UICameraSettings.LoadSettingsLocal(_settings.SettingsFile).ToCameraSettings();
+ _client.ApplySettings(_cameraSettings);
+ }
+
+ public Task GetImage(CancellationToken token)
+ {
+
+ _lastImage = null;
+ if (_imageAquisitionTaskSource != null && !_imageAquisitionTaskSource.Task.IsCanceled)
+ {
+ _imageAquisitionTaskSource.SetCanceled(CancellationToken.None);
+ _imageAquisitionTaskSource = null;
+
+ }
+ _imageAquisitionTaskSource= new TaskCompletionSource();
+ _client.Trigger();
+ return _imageAquisitionTaskSource.Task;
+
+ }
+
+ private TaskCompletionSource? _imageAquisitionTaskSource;
+ private Mat _lastImage;
+ byte[] _flipBuffer = new byte[2000 * 2000];
+ private void _client_ImageReceived(byte[] obj)
+ {
+ Log.Debug("Got image on {adapter}", _settings.Adapter);
+ FlipLines(obj, _flipBuffer);
+ obj = _flipBuffer;
+ var pinnedArray = GCHandle.Alloc(obj, GCHandleType.Pinned);
+ var pointer = pinnedArray.AddrOfPinnedObject();
+
+ Mat image = new Mat(_cameraSettings.ImageSettings.Lines, _cameraSettings.ImageSettings.SensorWidth,
+ MatType.CV_8UC1, pointer);
+
+ var xCrop = _cameraSettings.ImageSettings.SensorWidth - _cameraSettings.ImageWidth - _cameraSettings.OffsetX;
+
+ // crop the image with opencv
+ _lastImage = image[0, _cameraSettings.ImageSettings.Lines, xCrop,
+ xCrop + _cameraSettings.ImageWidth].Clone();
+
+ if (_cameraSettings.BayerFilter)
+ {
+ _lastImage = BayerFilter(_lastImage);
+ }
+
+ _imageAquisitionTaskSource!.SetResult(_lastImage);
+
+ pinnedArray.Free();
+ }
+
+ Mat BayerFilter(Mat image)
+ {
+ // Assumes input is a single-channel Bayer pattern image (CV_8UC1)
+ // Output is a 3-channel BGR image (CV_8UC3)
+ if (image == null || image.Empty())
+ throw new ArgumentException("Input image is null or empty.", nameof(image));
+
+ Mat bgrImage = new Mat();
+ // Use OpenCV's demosaicing function for Bayer BG pattern
+ Cv2.CvtColor(image, bgrImage, ColorConversionCodes.BayerBG2BGR);
+
+ return bgrImage;
+ }
+
+ protected void FlipLines(byte[] src, byte[] dst)
+ {
+ int srcStride = _cameraSettings.ImageSettings.SensorWidth;
+ int dstStride = _cameraSettings.ImageSettings.SensorWidth;
+ int pixelSize = 1;
+ int copySize = srcStride * pixelSize;
+ var lines = _cameraSettings.ImageSettings.Lines;
+ for (int line = 0; line < lines; line += 4)
+ {
+ for (int i = 0; i < 4; i++)
+ {
+ int srcPos = (line + i) * srcStride;
+ int dstPos = (line + (4 - i)) * dstStride;
+ for (int j = 0; j < copySize; j++)
+ {
+ dst[dstPos + j] = src[srcPos + j];
+ }
+
+
+ }
+ }
+ }
+
+ public void InitializeModule()
+ {
+
+ }
+ }
+}
diff --git a/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/HawkeyeSettings.cs b/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/HawkeyeSettings.cs
new file mode 100644
index 0000000..7989583
--- /dev/null
+++ b/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/HawkeyeSettings.cs
@@ -0,0 +1,25 @@
+using Inspectron.Settings;
+using Inspectron.Settings.Attributes;
+using VisionBuilder.UI.Common;
+
+namespace Hawkeye.VisionBuilder.UI.Sources.Hawkeye;
+
+public class HawkeyeSettings(string CameraName): ISettings
+{
+
+
+
+ public string Adapter { get; set; }="192.168.1.100";
+
+ [File("*.jcnf")]
+ public string SettingsFile { get; set; }
+
+
+ public void RegisterSettings(InspectronSettings settings)
+ {
+ settings.RegisterSimple(this, () => Adapter, $"{CameraName}/Sources/Hawkeye", nameof(Adapter));
+ settings.RegisterSimple(this, () => SettingsFile, $"{CameraName}/Sources/Hawkeye", nameof(SettingsFile));
+ }
+
+
+}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/UICameraSettings.cs b/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/UICameraSettings.cs
new file mode 100644
index 0000000..18a70ad
--- /dev/null
+++ b/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/UICameraSettings.cs
@@ -0,0 +1,119 @@
+using Inspectron.HawkEye.Protocol;
+using Newtonsoft.Json;
+
+namespace Hawkeye.VisionBuilder.UI.Sources.Hawkeye;
+
+public class UICameraSettings
+{
+ public UICameraSettings()
+ {
+ }
+
+ public UICameraSettings(CameraSettings cameraSettings)
+ {
+ Shutter = cameraSettings.ImageSettings.Shutter;
+ Gain = cameraSettings.ImageSettings.Gain;
+ OffsetX = cameraSettings.OffsetX;
+ SensorWidth = cameraSettings.ImageSettings.SensorWidth;
+ Width = cameraSettings.ImageWidth;
+ Lines = cameraSettings.ImageSettings.Lines;
+ Divider = cameraSettings.ImageSettings.Divider;
+ Light1 = cameraSettings.LightPwm1;
+ Light2 = cameraSettings.LightPwm2;
+ TriggerEnabled = cameraSettings.ImageSettings.UseExternalTrigger == 1;
+ Name = cameraSettings.Name;
+ CaptureBuffer = cameraSettings.ImageSettings.CaptureBuffer;
+ RescaleWidth = cameraSettings.RescaleWidth;
+ MinorCutoff = cameraSettings.MinorCutoff;
+ BayerFilter = cameraSettings.BayerFilter;
+ LaserTrigger = cameraSettings.LaserTrigger;
+ LaserTriggerDelay = cameraSettings.LaserTriggerDelay;
+ FlipLines = cameraSettings.FlipLines;
+ TriggerLights = cameraSettings.TriggerLights;
+ MirrorX = cameraSettings.MirrorX;
+ }
+
+
+
+ public CameraSettings ToCameraSettings()
+ {
+ return new CameraSettings()
+ {
+ ImageSettings = new ImageSettings()
+ {
+ Lines = Lines,
+ CaptureBuffer = CaptureBuffer,
+ Divider = Divider,
+ UseExternalTrigger = TriggerEnabled ? 1 : 0,
+ Gain = Gain,
+ Shutter = Shutter,
+ SensorWidth = SensorWidth
+
+
+ },
+ LightPwm1 = Light1,
+ LightPwm2 = Light2,
+ OffsetX = OffsetX,
+ ImageWidth = Width,
+ RescaleWidth = RescaleWidth,
+ MinorCutoff = MinorCutoff,
+ BayerFilter = BayerFilter,
+ LaserTrigger = LaserTrigger,
+ LaserTriggerDelay = LaserTriggerDelay,
+ FlipLines = FlipLines,
+ TriggerLights = TriggerLights,
+ MirrorX = MirrorX
+ };
+ }
+
+ public static UICameraSettings LoadSettingsLocal(string path)
+ {
+ return JsonConvert.DeserializeObject(File.ReadAllText(path));
+ }
+
+
+ public int LaserTriggerDelay { get; set; }
+
+ public bool LaserTrigger { get; set; }
+
+ public bool TriggerEnabled { get; set; }
+
+
+ public int CaptureBuffer { get; set; }
+
+ public bool FlipLines { get; set; }
+
+ public int Shutter { get; set; }
+
+ public int Gain { get; set; }
+
+ public int OffsetX { get; set; }
+
+ public int SensorWidth { get; set; }
+
+ public int Width { get; set; }
+
+ public int RescaleWidth { get; set; }
+
+ public int Lines { get; set; }
+
+ public int Divider { get; set; }
+
+ public int Light1 { get; set; }
+
+ public int Light2 { get; set; }
+
+ public bool TriggerLights { get; set; }
+
+ public string Name { get; set; }
+
+
+ public int MinorCutoff { get; set; }
+
+
+
+ public bool BayerFilter { get; set; }
+
+
+ public bool MirrorX { get; set; }
+}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.UI.Sources.IDS/IDSImageSource.cs b/Hawkeye.VisionBuilder.UI.Sources.IDS/IDSImageSource.cs
index a8ccf07..0c8b301 100644
--- a/Hawkeye.VisionBuilder.UI.Sources.IDS/IDSImageSource.cs
+++ b/Hawkeye.VisionBuilder.UI.Sources.IDS/IDSImageSource.cs
@@ -24,7 +24,6 @@ namespace Inspectron.Camera.UEye
int cameraIdx;
-
public IDSImageSource(IDSImageSourceSettings settings)
{
_settings = settings;
@@ -68,8 +67,7 @@ namespace Inspectron.Camera.UEye
Byte[] u8img;
_camera.Memory.CopyToArray(idx, out u8img);
- //Console.WriteLine($"Image size: {size.Width}x{size.Height}, channels: {channels}({mode}), data size: {u8img.Length}, pitch: {pitch}");
- //Console.ReadLine();
+
Mat mat = new Mat(size.Height,pitch, MatType.CV_8UC(channels));
mat.SetArray(u8img);
return mat;
@@ -82,11 +80,11 @@ namespace Inspectron.Camera.UEye
uEye.Defines.Status statusRet = 0;
- // Open _camera
+ // Open camera
statusRet = _camera.Init(cameraIdx);
if (statusRet != uEye.Defines.Status.Success)
{
- throw new Exception("_camera initializing failed: "+statusRet);
+ throw new Exception("Camera initialization failed: "+statusRet);
}
uEye.Types.SensorInfo info = new uEye.Types.SensorInfo();
_camera.Information.GetSensorInfo(out info);
@@ -101,7 +99,6 @@ namespace Inspectron.Camera.UEye
if (statusRet != uEye.Defines.Status.Success)
{
throw new Exception("Allocate Memory failed");
-
}
_camera.EventFrame += onFrameEvent;
try
diff --git a/Hawkeye.VisionBuilder.Workflow/.claude/settings.local.json b/Hawkeye.VisionBuilder.Workflow/.claude/settings.local.json
new file mode 100644
index 0000000..3efdcd6
--- /dev/null
+++ b/Hawkeye.VisionBuilder.Workflow/.claude/settings.local.json
@@ -0,0 +1,14 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(find:*)",
+ "Bash(for file in UnionOperation.cs HatsOperation.cs OpeningOperation.cs DilationOperation.cs ClosingOperation.cs)",
+ "Bash(do echo \"=== $file ===\")",
+ "Bash(tail:*)",
+ "Bash(done)",
+ "Bash(dotnet build:*)",
+ "Bash(ls:*)"
+ ],
+ "deny": []
+ }
+}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/BaseOperation.cs b/Hawkeye.VisionBuilder.Workflow/BaseOperation.cs
index f539355..45ffd1d 100644
--- a/Hawkeye.VisionBuilder.Workflow/BaseOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/BaseOperation.cs
@@ -90,8 +90,9 @@ namespace Hawkeye.VisionBuilder.Workflow
{
return Regex.Replace(typeName.Replace("Operation", ""), "(\\B[A-Z]+?(?=[A-Z][^A-Z])|\\B[A-Z]+?(?=[^A-Z]))", (useSpaces?" ":"")+"$1");
}
-
+ [NotForTool]
public bool Result { get; set; } = true;
+ [NotForTool]
public string ResultString
{
get
@@ -104,7 +105,12 @@ namespace Hawkeye.VisionBuilder.Workflow
[NotForTool]
public string Label { get; set; }
+ [NotForTool]
+ public bool Enabled { get; set; } = true;
+
+ [NotForTool]
public TimeSpan ExecutionTime { get; set; }
+ [NotForTool]
public string ExecutionTimeString
{
get
@@ -132,7 +138,7 @@ namespace Hawkeye.VisionBuilder.Workflow
public Dictionary GetParameters()
{
- var props = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
+ var props = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(x=>!x.HasAttribute(typeof(NotForToolAttribute)));
return props.ToDictionary(x => x.Name, x => x.GetValue(this)!);
}
@@ -142,7 +148,7 @@ namespace Hawkeye.VisionBuilder.Workflow
foreach (KeyValuePair pair in parameters)
{
this.GetType().GetProperty(pair.Key,
- BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
+ BindingFlags.Public | BindingFlags.Instance )
?.SetValue(this,pair.Value);
}
}
@@ -275,6 +281,23 @@ namespace Hawkeye.VisionBuilder.Workflow
Label = br.ReadString();
}
+ public virtual void Save(Dictionary dict)
+ {
+ dict[nameof(Id)] = Id.ToString();
+ dict[nameof(Label)] = Label;
+ dict[nameof(Enabled)] = Enabled;
+ }
+
+ public virtual void Load(Dictionary dict)
+ {
+ if (dict.ContainsKey(nameof(Id)))
+ Id = new Guid(dict[nameof(Id)].ToString());
+ if (dict.ContainsKey(nameof(Label)))
+ Label = dict[nameof(Label)].ToString();
+ if (dict.ContainsKey(nameof(Enabled)))
+ Enabled = Convert.ToBoolean(dict[nameof(Enabled)]);
+ }
+
public void SetError(string eMessage)
{
Status=eMessage;
diff --git a/Hawkeye.VisionBuilder.Workflow/BaseOperationDecorator.cs b/Hawkeye.VisionBuilder.Workflow/BaseOperationDecorator.cs
index bfbe1fa..45a2c54 100644
--- a/Hawkeye.VisionBuilder.Workflow/BaseOperationDecorator.cs
+++ b/Hawkeye.VisionBuilder.Workflow/BaseOperationDecorator.cs
@@ -9,6 +9,7 @@ public class BaseOperationDecorator
_baseOperation = baseOperation;
}
+
public bool Result => Operation.Result;
public string ResultString => Operation.ToString();
public string TypeName => Operation.TypeName;
@@ -21,4 +22,10 @@ public class BaseOperationDecorator
public string ExecutionTimeString => Operation.ExecutionTimeString;
public BaseOperation Operation => _baseOperation;
+
+ public bool IsEnabled
+ {
+ get => _baseOperation.Enabled;
+ set => _baseOperation.Enabled = value;
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/AnomalyAI.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/AnomalyAI.cs
index c84b60a..68fc5b4 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/AnomalyAI.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/AnomalyAI.cs
@@ -122,4 +122,17 @@ public class AnomalyAI: BaseOperation
_modelFilePath.Format = br.ReadString();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(ModelFilePath)] = ModelFilePath.Path;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(ModelFilePath)))
+ ModelFilePath.Path = dict[nameof(ModelFilePath)].ToString();
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/ArrayModelMatchingOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ArrayModelMatchingOperation.cs
index 8d51895..cd4b68f 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/ArrayModelMatchingOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ArrayModelMatchingOperation.cs
@@ -181,4 +181,54 @@ public class ArrayModelMatchingOperation:BaseOperation
SearchArea.Load(br);
ReloadModelInfo();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(ReferenceId)] = ReferenceId.ToString();
+ dict[nameof(Margin)] = Margin;
+ dict[nameof(Amount)] = Amount;
+ dict[nameof(ModelName)] = ModelName;
+
+ // Save ArrayHorizontalElement SearchArea properties manually
+ dict["SearchArea_Editable"] = SearchArea.Editable;
+ dict["SearchArea_LocationX"] = SearchArea.Location.X;
+ dict["SearchArea_LocationY"] = SearchArea.Location.Y;
+ dict["SearchArea_OffsetX"] = SearchArea.Offset.X;
+ dict["SearchArea_OffsetY"] = SearchArea.Offset.Y;
+ dict["SearchArea_BlockSizeX"] = SearchArea.BlockSize.X;
+ dict["SearchArea_BlockSizeY"] = SearchArea.BlockSize.Y;
+ dict["SearchArea_BlockCount"] = SearchArea.BlockCount;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(ReferenceId)))
+ ReferenceId = new Guid(dict[nameof(ReferenceId)].ToString());
+ if (dict.ContainsKey(nameof(Margin)))
+ Margin = Convert.ToInt32(dict[nameof(Margin)]);
+ if (dict.ContainsKey(nameof(Amount)))
+ Amount = Convert.ToInt32(dict[nameof(Amount)]);
+ if (dict.ContainsKey(nameof(ModelName)))
+ ModelName = dict[nameof(ModelName)].ToString();
+
+ // Load ArrayHorizontalElement SearchArea properties manually
+ if (dict.ContainsKey("SearchArea_Editable") || dict.ContainsKey("SearchArea_LocationX") || dict.ContainsKey("SearchArea_BlockSizeX"))
+ {
+ SearchArea = new ArrayHorizontalElement();
+ if (dict.ContainsKey("SearchArea_Editable"))
+ SearchArea.Editable = Convert.ToBoolean(dict["SearchArea_Editable"]);
+ if (dict.ContainsKey("SearchArea_LocationX") && dict.ContainsKey("SearchArea_LocationY"))
+ SearchArea.Location = new Vector2(Convert.ToSingle(dict["SearchArea_LocationX"]), Convert.ToSingle(dict["SearchArea_LocationY"]));
+ if (dict.ContainsKey("SearchArea_OffsetX") && dict.ContainsKey("SearchArea_OffsetY"))
+ SearchArea.Offset = new Vector2(Convert.ToSingle(dict["SearchArea_OffsetX"]), Convert.ToSingle(dict["SearchArea_OffsetY"]));
+ if (dict.ContainsKey("SearchArea_BlockSizeX") && dict.ContainsKey("SearchArea_BlockSizeY"))
+ SearchArea.BlockSize = new Vector2(Convert.ToSingle(dict["SearchArea_BlockSizeX"]), Convert.ToSingle(dict["SearchArea_BlockSizeY"]));
+ if (dict.ContainsKey("SearchArea_BlockCount"))
+ SearchArea.BlockCount = Convert.ToInt32(dict["SearchArea_BlockCount"]);
+ }
+
+ ReloadModelInfo();
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/BackgroundSeparationModelOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/BackgroundSeparationModelOperation.cs
index 420be4a..48a6688 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/BackgroundSeparationModelOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/BackgroundSeparationModelOperation.cs
@@ -120,5 +120,18 @@ public class BackgroundSeparationModelOperation:BaseOperation,IHaveOrigin
ModelName = br.ReadString();
}
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(ModelName)] = ModelName;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(ModelName)))
+ ModelName = dict[nameof(ModelName)].ToString();
+ }
+
public OriginElement Origin { get; set; }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128BinaryOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128BinaryOperation.cs
index 1d7ce47..5fb3f63 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128BinaryOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128BinaryOperation.cs
@@ -30,4 +30,20 @@ public class Color128BinaryOperation:ColorAIOperation
FilterClasses = br.ReadString();
ModelFilePath.Path = br.ReadString();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(FilterClasses)] = FilterClasses;
+ dict[nameof(ModelFilePath)] = ModelFilePath.Path;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(FilterClasses)))
+ FilterClasses = dict[nameof(FilterClasses)].ToString();
+ if (dict.ContainsKey(nameof(ModelFilePath)))
+ ModelFilePath.Path = dict[nameof(ModelFilePath)].ToString();
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128HalfOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128HalfOperation.cs
index 787056d..da796d5 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128HalfOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128HalfOperation.cs
@@ -110,4 +110,20 @@ public class Color128HalfOperation:BaseOperation
FilterClasses = br.ReadString();
ModelFilePath.Path = br.ReadString();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(FilterClasses)] = FilterClasses;
+ dict[nameof(ModelFilePath)] = ModelFilePath.Path;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(FilterClasses)))
+ FilterClasses = dict[nameof(FilterClasses)].ToString();
+ if (dict.ContainsKey(nameof(ModelFilePath)))
+ ModelFilePath.Path = dict[nameof(ModelFilePath)].ToString();
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128SimpleOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128SimpleOperation.cs
index 21f108b..eafb465 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128SimpleOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128SimpleOperation.cs
@@ -33,4 +33,20 @@ public class Color128SimpleOperation: ColorAIOperation
FilterClasses = br.ReadString();
ModelFilePath.Path = br.ReadString();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(FilterClasses)] = FilterClasses;
+ dict[nameof(ModelFilePath)] = ModelFilePath.Path;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(FilterClasses)))
+ FilterClasses = dict[nameof(FilterClasses)].ToString();
+ if (dict.ContainsKey(nameof(ModelFilePath)))
+ ModelFilePath.Path = dict[nameof(ModelFilePath)].ToString();
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/ColorModelOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ColorModelOperation.cs
index f662f56..643f58b 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/ColorModelOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ColorModelOperation.cs
@@ -39,4 +39,27 @@ public class ColorModelOperation : ColorAIOperation
var absPath = Path.GetFullPath(Path.Combine(dir, ModelFilePath.Path));
ModelFilePath.Path = absPath;
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(FilterClasses)] = FilterClasses;
+ dict[nameof(ModelFilePath)] = ModelFilePath.Path;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(FilterClasses)))
+ FilterClasses = dict[nameof(FilterClasses)].ToString();
+ if (dict.ContainsKey(nameof(ModelFilePath)))
+ {
+ ModelFilePath.Path = dict[nameof(ModelFilePath)].ToString();
+ // get current directory
+ var dir = Directory.GetCurrentDirectory();
+ // get absolute path
+ var absPath = Path.GetFullPath(Path.Combine(dir, ModelFilePath.Path));
+ ModelFilePath.Path = absPath;
+ }
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/ModelAIOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ModelAIOperation.cs
index 57b7902..5001889 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/ModelAIOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ModelAIOperation.cs
@@ -156,4 +156,23 @@ public class ModelAIOperation: BaseOperation
FilterClasses = br.ReadString();
IsCategorical = br.ReadBoolean();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(ModelFilePath)] = ModelFilePath.Path;
+ dict[nameof(FilterClasses)] = FilterClasses;
+ dict[nameof(IsCategorical)] = IsCategorical;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(ModelFilePath)))
+ ModelFilePath.Path = dict[nameof(ModelFilePath)].ToString();
+ if (dict.ContainsKey(nameof(FilterClasses)))
+ FilterClasses = dict[nameof(FilterClasses)].ToString();
+ if (dict.ContainsKey(nameof(IsCategorical)))
+ IsCategorical = Convert.ToBoolean(dict[nameof(IsCategorical)]);
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/MultichannelAI.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/MultichannelAI.cs
index babfa78..3a2f9a9 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/MultichannelAI.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/MultichannelAI.cs
@@ -145,4 +145,17 @@ public class MultichannelAI: BaseOperation
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(ModelFilePath)] = ModelFilePath.Path;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(ModelFilePath)))
+ ModelFilePath.Path = dict[nameof(ModelFilePath)].ToString();
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/RawModelAIOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/RawModelAIOperation.cs
index be8ad84..7067464 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/RawModelAIOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/RawModelAIOperation.cs
@@ -147,4 +147,23 @@ public class RawModelAIOperation: BaseOperation
FilterClasses = br.ReadString();
IsCategorical = br.ReadBoolean();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(ModelFilePath)] = ModelFilePath.Path;
+ dict[nameof(FilterClasses)] = FilterClasses;
+ dict[nameof(IsCategorical)] = IsCategorical;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(ModelFilePath)))
+ ModelFilePath.Path = dict[nameof(ModelFilePath)].ToString();
+ if (dict.ContainsKey(nameof(FilterClasses)))
+ FilterClasses = dict[nameof(FilterClasses)].ToString();
+ if (dict.ContainsKey(nameof(IsCategorical)))
+ IsCategorical = Convert.ToBoolean(dict[nameof(IsCategorical)]);
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloDetectionOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloDetectionOperation.cs
index d382089..c107ed2 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloDetectionOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloDetectionOperation.cs
@@ -139,4 +139,17 @@ public class YoloDetectionOperation:BaseOperation
base.Load(br);
ModelFilePath.Path = br.ReadString();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(ModelFilePath)] = ModelFilePath.Path;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(ModelFilePath)))
+ ModelFilePath.Path = dict[nameof(ModelFilePath)].ToString();
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloPickDetected.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloPickDetected.cs
index 05824da..d07e080 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloPickDetected.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloPickDetected.cs
@@ -112,4 +112,38 @@ public class YoloPickDetected:BaseOperation
MinArea = br.ReadInt32();
MaxArea = br.ReadInt32();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(SlotName)] = SlotName;
+ dict[nameof(TypeId)] = TypeId;
+ dict[nameof(MinWidth)] = MinWidth;
+ dict[nameof(MaxWidth)] = MaxWidth;
+ dict[nameof(MinHeight)] = MinHeight;
+ dict[nameof(MaxHeight)] = MaxHeight;
+ dict[nameof(MinArea)] = MinArea;
+ dict[nameof(MaxArea)] = MaxArea;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(SlotName)))
+ SlotName = dict[nameof(SlotName)].ToString();
+ if (dict.ContainsKey(nameof(TypeId)))
+ TypeId = Convert.ToInt32(dict[nameof(TypeId)]);
+ if (dict.ContainsKey(nameof(MinWidth)))
+ MinWidth = Convert.ToInt32(dict[nameof(MinWidth)]);
+ if (dict.ContainsKey(nameof(MaxWidth)))
+ MaxWidth = Convert.ToInt32(dict[nameof(MaxWidth)]);
+ if (dict.ContainsKey(nameof(MinHeight)))
+ MinHeight = Convert.ToInt32(dict[nameof(MinHeight)]);
+ if (dict.ContainsKey(nameof(MaxHeight)))
+ MaxHeight = Convert.ToInt32(dict[nameof(MaxHeight)]);
+ if (dict.ContainsKey(nameof(MinArea)))
+ MinArea = Convert.ToInt32(dict[nameof(MinArea)]);
+ if (dict.ContainsKey(nameof(MaxArea)))
+ MaxArea = Convert.ToInt32(dict[nameof(MaxArea)]);
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/CannyOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/CannyOperation.cs
index 110081f..68175c8 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/CannyOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/CannyOperation.cs
@@ -35,4 +35,20 @@ public class CannyOperation:BaseOperation
Threshold1 = br.ReadInt32();
Threshold2 = br.ReadInt32();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(Threshold1)] = Threshold1;
+ dict[nameof(Threshold2)] = Threshold2;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(Threshold1)))
+ Threshold1 = Convert.ToInt32(dict[nameof(Threshold1)]);
+ if (dict.ContainsKey(nameof(Threshold2)))
+ Threshold2 = Convert.ToInt32(dict[nameof(Threshold2)]);
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/DetectionPaddingOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/DetectionPaddingOperation.cs
index c8a9378..df9ec44 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/DetectionPaddingOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/DetectionPaddingOperation.cs
@@ -52,4 +52,20 @@ public class DetectionPaddingOperation:BaseOperation
HorizontalPadding = br.ReadInt32();
VerticalPadding = br.ReadInt32();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(HorizontalPadding)] = HorizontalPadding;
+ dict[nameof(VerticalPadding)] = VerticalPadding;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(HorizontalPadding)))
+ HorizontalPadding = Convert.ToInt32(dict[nameof(HorizontalPadding)]);
+ if (dict.ContainsKey(nameof(VerticalPadding)))
+ VerticalPadding = Convert.ToInt32(dict[nameof(VerticalPadding)]);
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/GaussianBlurOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/GaussianBlurOperation.cs
index fc19b14..b2d5340 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/GaussianBlurOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/GaussianBlurOperation.cs
@@ -47,4 +47,17 @@ public class GaussianBlurOperation : BaseOperation
KernelSize.Script = br.ReadString();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(KernelSize)] = KernelSize.Script;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(KernelSize)))
+ KernelSize.Script = dict[nameof(KernelSize)].ToString();
+ }
}
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/PorabollisticHoughOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/PorabollisticHoughOperation.cs
index f018414..0417b74 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/PorabollisticHoughOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/PorabollisticHoughOperation.cs
@@ -51,4 +51,29 @@ public class PorabollisticHoughOperation:BaseOperation
MinLineLength = br.ReadInt32();
MaxLineGap = br.ReadInt32();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(DistanceResolution)] = DistanceResolution;
+ dict[nameof(AngleResolution)] = AngleResolution;
+ dict[nameof(Threshold)] = Threshold;
+ dict[nameof(MinLineLength)] = MinLineLength;
+ dict[nameof(MaxLineGap)] = MaxLineGap;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(DistanceResolution)))
+ DistanceResolution = Convert.ToDouble(dict[nameof(DistanceResolution)]);
+ if (dict.ContainsKey(nameof(AngleResolution)))
+ AngleResolution = Convert.ToDouble(dict[nameof(AngleResolution)]);
+ if (dict.ContainsKey(nameof(Threshold)))
+ Threshold = Convert.ToInt32(dict[nameof(Threshold)]);
+ if (dict.ContainsKey(nameof(MinLineLength)))
+ MinLineLength = Convert.ToInt32(dict[nameof(MinLineLength)]);
+ if (dict.ContainsKey(nameof(MaxLineGap)))
+ MaxLineGap = Convert.ToInt32(dict[nameof(MaxLineGap)]);
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdMinMaxOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdMinMaxOperation.cs
index 055f1cc..c64e5b0 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdMinMaxOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdMinMaxOperation.cs
@@ -44,4 +44,20 @@ public class ThresholdMinMaxOperation : BaseOperation
ThresholdMin.Script = br.ReadString();
ThresholdMax.Script = br.ReadString();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(ThresholdMin)] = ThresholdMin.Script;
+ dict[nameof(ThresholdMax)] = ThresholdMax.Script;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(ThresholdMin)))
+ ThresholdMin.Script = dict[nameof(ThresholdMin)].ToString();
+ if (dict.ContainsKey(nameof(ThresholdMax)))
+ ThresholdMax.Script = dict[nameof(ThresholdMax)].ToString();
+ }
}
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdOperation.cs
index 02cb9f8..ae99711 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdOperation.cs
@@ -41,4 +41,17 @@ public class ThresholdOperation:BaseOperation
ThresholdMin.Script = br.ReadString();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(ThresholdMin)] = ThresholdMin.Script;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(ThresholdMin)))
+ ThresholdMin.Script = dict[nameof(ThresholdMin)].ToString();
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/ColorProfileOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/ColorProfileOperation.cs
index fb524b1..f331b0b 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/ColorProfileOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/ColorProfileOperation.cs
@@ -132,6 +132,23 @@ public class ColorProfileOperation:BaseOperation
_initialized = false;
}
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(ColorProfilePath)] = ColorProfilePath.Path;
+ dict[nameof(Category)] = Category.Value;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(ColorProfilePath)))
+ ColorProfilePath.Path = EnsureRelativePath(dict[nameof(ColorProfilePath)].ToString());
+ if (dict.ContainsKey(nameof(Category)))
+ Category.Value = dict[nameof(Category)].ToString();
+ _initialized = false;
+ }
+
string EnsureRelativePath(string path)
{
if (path.StartsWith(".."))
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/GaborFilterOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/GaborFilterOperation.cs
index 1881572..07220e2 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/GaborFilterOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/GaborFilterOperation.cs
@@ -94,4 +94,35 @@ public class GaborFilterOperation : BaseOperation
ThetaToAngle = br.ReadDouble();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(KSize)] = KSize;
+ dict[nameof(Sigma)] = Sigma;
+ dict[nameof(NumFilters)] = NumFilters;
+ dict[nameof(Lambda)] = Lambda;
+ dict[nameof(Gamma)] = Gamma;
+ dict[nameof(ThetaFromAngle)] = ThetaFromAngle;
+ dict[nameof(ThetaToAngle)] = ThetaToAngle;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(KSize)))
+ KSize = Convert.ToInt32(dict[nameof(KSize)]);
+ if (dict.ContainsKey(nameof(Sigma)))
+ Sigma = Convert.ToDouble(dict[nameof(Sigma)]);
+ if (dict.ContainsKey(nameof(NumFilters)))
+ NumFilters = Convert.ToInt32(dict[nameof(NumFilters)]);
+ if (dict.ContainsKey(nameof(Lambda)))
+ Lambda = Convert.ToDouble(dict[nameof(Lambda)]);
+ if (dict.ContainsKey(nameof(Gamma)))
+ Gamma = Convert.ToDouble(dict[nameof(Gamma)]);
+ if (dict.ContainsKey(nameof(ThetaFromAngle)))
+ ThetaFromAngle = Convert.ToDouble(dict[nameof(ThetaFromAngle)]);
+ if (dict.ContainsKey(nameof(ThetaToAngle)))
+ ThetaToAngle = Convert.ToDouble(dict[nameof(ThetaToAngle)]);
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/LUTFilterOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/LUTFilterOperation.cs
index 84b4a47..56b238f 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/LUTFilterOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/LUTFilterOperation.cs
@@ -57,4 +57,27 @@ public class LUTFilterOperation : BaseOperation
base.Load(br);
LutData.Deserialize(br);
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ // Save LutData as serialized representation
+ using var ms = new MemoryStream();
+ using var bw = new BinaryWriter(ms);
+ LutData.Serialize(bw);
+ dict[nameof(LutData)] = Convert.ToBase64String(ms.ToArray());
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(LutData)))
+ {
+ var base64Data = dict[nameof(LutData)].ToString();
+ var bytes = Convert.FromBase64String(base64Data);
+ using var ms = new MemoryStream(bytes);
+ using var br = new BinaryReader(ms);
+ LutData.Deserialize(br);
+ }
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/NoiseFilterOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/NoiseFilterOperation.cs
index 891173e..6d2b2b1 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/NoiseFilterOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/NoiseFilterOperation.cs
@@ -38,4 +38,17 @@ public class NoiseFilterOperation:BaseOperation
base.Load(br);
NoiseSize = br.ReadInt32();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(NoiseSize)] = NoiseSize;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(NoiseSize)))
+ NoiseSize = Convert.ToInt32(dict[nameof(NoiseSize)]);
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/RangeFilterOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/RangeFilterOperation.cs
index 3eaa9a8..aa08f5b 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/RangeFilterOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/RangeFilterOperation.cs
@@ -51,5 +51,33 @@ public class RangeFilterOperation: BaseOperation
C3Max = br.ReadInt32();
}
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(C1Min)] = C1Min;
+ dict[nameof(C1Max)] = C1Max;
+ dict[nameof(C2Min)] = C2Min;
+ dict[nameof(C2Max)] = C2Max;
+ dict[nameof(C3Min)] = C3Min;
+ dict[nameof(C3Max)] = C3Max;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(C1Min)))
+ C1Min = Convert.ToInt32(dict[nameof(C1Min)]);
+ if (dict.ContainsKey(nameof(C1Max)))
+ C1Max = Convert.ToInt32(dict[nameof(C1Max)]);
+ if (dict.ContainsKey(nameof(C2Min)))
+ C2Min = Convert.ToInt32(dict[nameof(C2Min)]);
+ if (dict.ContainsKey(nameof(C2Max)))
+ C2Max = Convert.ToInt32(dict[nameof(C2Max)]);
+ if (dict.ContainsKey(nameof(C3Min)))
+ C3Min = Convert.ToInt32(dict[nameof(C3Min)]);
+ if (dict.ContainsKey(nameof(C3Max)))
+ C3Max = Convert.ToInt32(dict[nameof(C3Max)]);
+ }
+
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/GetImageOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/GetImageOperation.cs
index 4203ad2..68de476 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/GetImageOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/GetImageOperation.cs
@@ -68,4 +68,22 @@ public class GetImageOperation:BaseOperation,IHaveImage
Id = new Guid(br.ReadString());
Label = br.ReadString();
}
+
+ ///
+ /// Save the operation to the dictionary
+ ///
+ ///
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ }
+
+ ///
+ /// Load the operation from the dictionary
+ ///
+ ///
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Image/GetChannelOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Image/GetChannelOperation.cs
index 61f611d..437f763 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Image/GetChannelOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Image/GetChannelOperation.cs
@@ -35,4 +35,17 @@ public class GetChannelOperation: BaseOperation
base.Load(br);
Channel = br.ReadInt32();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(Channel)] = Channel;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(Channel)))
+ Channel = Convert.ToInt32(dict[nameof(Channel)]);
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Image/ImageSizeProportionOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Image/ImageSizeProportionOperation.cs
index aa60361..e57fff1 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Image/ImageSizeProportionOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Image/ImageSizeProportionOperation.cs
@@ -41,4 +41,20 @@ public class ImageSizeProportionOperation:BaseOperation
Width = br.ReadDouble();
Height = br.ReadDouble();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(Width)] = Width;
+ dict[nameof(Height)] = Height;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(Width)))
+ Width = Convert.ToDouble(dict[nameof(Width)]);
+ if (dict.ContainsKey(nameof(Height)))
+ Height = Convert.ToDouble(dict[nameof(Height)]);
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageFromMemoryOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageFromMemoryOperation.cs
index 07afbd7..fb46680 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageFromMemoryOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageFromMemoryOperation.cs
@@ -39,4 +39,17 @@ public class ImageFromMemoryOperation:BaseOperation
base.Load(br);
SlotName = br.ReadString();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(SlotName)] = SlotName;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(SlotName)))
+ SlotName = dict[nameof(SlotName)].ToString();
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageToMemoryOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageToMemoryOperation.cs
index 97f2b1f..c6441f2 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageToMemoryOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageToMemoryOperation.cs
@@ -33,4 +33,17 @@ public class ImageToMemoryOperation:BaseOperation
base.Load(br);
SlotName = br.ReadString();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(SlotName)] = SlotName;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(SlotName)))
+ SlotName = dict[nameof(SlotName)].ToString();
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/ClosingOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/ClosingOperation.cs
index 2dada3f..1f96533 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/ClosingOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/ClosingOperation.cs
@@ -40,4 +40,17 @@ public class ClosingOperation:BaseOperation
base.Load(br);
Closing = br.ReadInt32();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(Closing)] = Closing;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(Closing)))
+ Closing = Convert.ToInt32(dict[nameof(Closing)]);
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/CutOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/CutOperation.cs
index ad1d261..419295a 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/CutOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/CutOperation.cs
@@ -80,4 +80,43 @@ public class CutOperation:BaseOperation
SearchArea.Load(br);
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(ReferencePoint)] = ReferencePoint.ToString();
+
+ // Save SearchArea properties manually
+ dict["SearchArea_Editable"] = SearchArea.Editable;
+ dict["SearchArea_IsGood"] = SearchArea.IsGood;
+ dict["SearchArea_LocationX"] = SearchArea.Location.X;
+ dict["SearchArea_LocationY"] = SearchArea.Location.Y;
+ dict["SearchArea_OffsetX"] = SearchArea.Offset.X;
+ dict["SearchArea_OffsetY"] = SearchArea.Offset.Y;
+ dict["SearchArea_SizeX"] = SearchArea.Size.X;
+ dict["SearchArea_SizeY"] = SearchArea.Size.Y;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(ReferencePoint)))
+ ReferencePoint = new Guid(dict[nameof(ReferencePoint)].ToString());
+
+ // Load SearchArea properties manually
+ if (dict.ContainsKey("SearchArea_Editable") || dict.ContainsKey("SearchArea_IsGood") || dict.ContainsKey("SearchArea_LocationX"))
+ {
+ SearchArea = new RectangleElement();
+ if (dict.ContainsKey("SearchArea_Editable"))
+ SearchArea.Editable = Convert.ToBoolean(dict["SearchArea_Editable"]);
+ if (dict.ContainsKey("SearchArea_IsGood"))
+ SearchArea.IsGood = Convert.ToBoolean(dict["SearchArea_IsGood"]);
+ if (dict.ContainsKey("SearchArea_LocationX") && dict.ContainsKey("SearchArea_LocationY"))
+ SearchArea.Location = new Vector2(Convert.ToSingle(dict["SearchArea_LocationX"]), Convert.ToSingle(dict["SearchArea_LocationY"]));
+ if (dict.ContainsKey("SearchArea_OffsetX") && dict.ContainsKey("SearchArea_OffsetY"))
+ SearchArea.Offset = new Vector2(Convert.ToSingle(dict["SearchArea_OffsetX"]), Convert.ToSingle(dict["SearchArea_OffsetY"]));
+ if (dict.ContainsKey("SearchArea_SizeX") && dict.ContainsKey("SearchArea_SizeY"))
+ SearchArea.Size = new Vector2(Convert.ToSingle(dict["SearchArea_SizeX"]), Convert.ToSingle(dict["SearchArea_SizeY"]));
+ }
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/DilationOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/DilationOperation.cs
index 438b7cd..0d9e550 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/DilationOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/DilationOperation.cs
@@ -41,4 +41,17 @@ public class DilationOperation:BaseOperation
base.Load(br);
Dilation = br.ReadInt32();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(Dilation)] = Dilation;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(Dilation)))
+ Dilation = Convert.ToInt32(dict[nameof(Dilation)]);
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/HatsOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/HatsOperation.cs
index 207a72a..576e9c7 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/HatsOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/HatsOperation.cs
@@ -40,4 +40,17 @@ public class HatsOperation : BaseOperation
base.Load(br);
HatSize = br.ReadInt32();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(HatSize)] = HatSize;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(HatSize)))
+ HatSize = Convert.ToInt32(dict[nameof(HatSize)]);
+ }
}
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/OpeningOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/OpeningOperation.cs
index f5b0c54..62803f7 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/OpeningOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/OpeningOperation.cs
@@ -40,4 +40,17 @@ public class OpeningOperation : BaseOperation
base.Load(br);
Opening = br.ReadInt32();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(Opening)] = Opening;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(Opening)))
+ Opening = Convert.ToInt32(dict[nameof(Opening)]);
+ }
}
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/SubtractOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/SubtractOperation.cs
index 42b213e..b037251 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/SubtractOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/SubtractOperation.cs
@@ -45,4 +45,14 @@ public class SubtractOperation : BaseOperation
{
base.Load(br);
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ }
}
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/TakeBiggestOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/TakeBiggestOperation.cs
index bb6727c..0073224 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/TakeBiggestOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/TakeBiggestOperation.cs
@@ -51,4 +51,14 @@ public class TakeBiggestOperation : BaseOperation
{
base.Load(br);
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ }
}
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/UnionOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/UnionOperation.cs
index c96db08..846875e 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/UnionOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/UnionOperation.cs
@@ -48,4 +48,14 @@ public class UnionOperation : BaseOperation
{
base.Load(br);
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ }
}
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindBlobOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindBlobOperation.cs
index 0ed4360..9ed52f9 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindBlobOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindBlobOperation.cs
@@ -155,4 +155,54 @@ public class FindBlobOperation : BaseOperation, IHaveOrigin, IHaveSearchArea
SearchArea.Load(br);
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(MinArea)] = MinArea;
+ dict[nameof(MaxArea)] = MaxArea;
+ dict[nameof(MinRadius)] = MinRadius;
+ dict[nameof(MaxRadius)] = MaxRadius;
+ dict[nameof(BadIfFound)] = BadIfFound;
+ dict[nameof(ReferenceId)] = ReferenceId.ToString();
+
+ // Save SearchArea properties manually
+ dict["SearchArea_Editable"] = SearchArea.Editable;
+ dict["SearchArea_IsGood"] = SearchArea.IsGood;
+ dict["SearchArea_LocationX"] = SearchArea.Location.X;
+ dict["SearchArea_LocationY"] = SearchArea.Location.Y;
+ dict["SearchArea_OffsetX"] = SearchArea.Offset.X;
+ dict["SearchArea_OffsetY"] = SearchArea.Offset.Y;
+ dict["SearchArea_SizeX"] = SearchArea.Size.X;
+ dict["SearchArea_SizeY"] = SearchArea.Size.Y;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(MinArea)))
+ MinArea = Convert.ToInt32(dict[nameof(MinArea)]);
+ if (dict.ContainsKey(nameof(MaxArea)))
+ MaxArea = Convert.ToInt32(dict[nameof(MaxArea)]);
+ if (dict.ContainsKey(nameof(MinRadius)))
+ MinRadius = Convert.ToInt32(dict[nameof(MinRadius)]);
+ if (dict.ContainsKey(nameof(MaxRadius)))
+ MaxRadius = Convert.ToInt32(dict[nameof(MaxRadius)]);
+ if (dict.ContainsKey(nameof(BadIfFound)))
+ BadIfFound = Convert.ToBoolean(dict[nameof(BadIfFound)]);
+ if (dict.ContainsKey(nameof(ReferenceId)))
+ ReferenceId = new Guid(dict[nameof(ReferenceId)].ToString());
+
+ // Load SearchArea properties manually
+ if (dict.ContainsKey("SearchArea_Editable"))
+ SearchArea.Editable = Convert.ToBoolean(dict["SearchArea_Editable"]);
+ if (dict.ContainsKey("SearchArea_IsGood"))
+ SearchArea.IsGood = Convert.ToBoolean(dict["SearchArea_IsGood"]);
+ if (dict.ContainsKey("SearchArea_LocationX") && dict.ContainsKey("SearchArea_LocationY"))
+ SearchArea.Location = new Vector2(Convert.ToSingle(dict["SearchArea_LocationX"]), Convert.ToSingle(dict["SearchArea_LocationY"]));
+ if (dict.ContainsKey("SearchArea_OffsetX") && dict.ContainsKey("SearchArea_OffsetY"))
+ SearchArea.Offset = new Vector2(Convert.ToSingle(dict["SearchArea_OffsetX"]), Convert.ToSingle(dict["SearchArea_OffsetY"]));
+ if (dict.ContainsKey("SearchArea_SizeX") && dict.ContainsKey("SearchArea_SizeY"))
+ SearchArea.Size = new Vector2(Convert.ToSingle(dict["SearchArea_SizeX"]), Convert.ToSingle(dict["SearchArea_SizeY"]));
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsExtOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsExtOperation.cs
index 92ba5e5..55bc2f6 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsExtOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsExtOperation.cs
@@ -230,4 +230,78 @@ public class FindManyBlobsExtOperation : BaseOperation
ReferenceId = new Guid(br.ReadString());
SearchArea.Load(br);
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(MinArea)] = MinArea;
+ dict[nameof(MaxArea)] = MaxArea;
+ dict[nameof(MinRadius)] = MinRadius;
+ dict[nameof(MaxRadius)] = MaxRadius;
+ dict[nameof(CheckInternalRadius)] = CheckInternalRadius;
+ dict[nameof(MinInternalRadius)] = MinInternalRadius;
+ dict[nameof(MaxInternalRadius)] = MaxInternalRadius;
+ dict[nameof(CheckWidth)] = CheckWidth;
+ dict[nameof(MinWidth)] = MinWidth;
+ dict[nameof(MaxWidth)] = MaxWidth;
+ dict[nameof(CheckHeight)] = CheckHeight;
+ dict[nameof(MinHeight)] = MinHeight;
+ dict[nameof(MaxHeight)] = MaxHeight;
+ dict[nameof(ReferenceId)] = ReferenceId.ToString();
+
+ // Save SearchArea properties manually
+ dict["SearchArea_Editable"] = SearchArea.Editable;
+ dict["SearchArea_IsGood"] = SearchArea.IsGood;
+ dict["SearchArea_LocationX"] = SearchArea.Location.X;
+ dict["SearchArea_LocationY"] = SearchArea.Location.Y;
+ dict["SearchArea_OffsetX"] = SearchArea.Offset.X;
+ dict["SearchArea_OffsetY"] = SearchArea.Offset.Y;
+ dict["SearchArea_SizeX"] = SearchArea.Size.X;
+ dict["SearchArea_SizeY"] = SearchArea.Size.Y;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(MinArea)))
+ MinArea = Convert.ToInt32(dict[nameof(MinArea)]);
+ if (dict.ContainsKey(nameof(MaxArea)))
+ MaxArea = Convert.ToInt32(dict[nameof(MaxArea)]);
+ if (dict.ContainsKey(nameof(MinRadius)))
+ MinRadius = Convert.ToInt32(dict[nameof(MinRadius)]);
+ if (dict.ContainsKey(nameof(MaxRadius)))
+ MaxRadius = Convert.ToInt32(dict[nameof(MaxRadius)]);
+ if (dict.ContainsKey(nameof(CheckInternalRadius)))
+ CheckInternalRadius = Convert.ToBoolean(dict[nameof(CheckInternalRadius)]);
+ if (dict.ContainsKey(nameof(MinInternalRadius)))
+ MinInternalRadius = Convert.ToInt32(dict[nameof(MinInternalRadius)]);
+ if (dict.ContainsKey(nameof(MaxInternalRadius)))
+ MaxInternalRadius = Convert.ToInt32(dict[nameof(MaxInternalRadius)]);
+ if (dict.ContainsKey(nameof(CheckWidth)))
+ CheckWidth = Convert.ToBoolean(dict[nameof(CheckWidth)]);
+ if (dict.ContainsKey(nameof(MinWidth)))
+ MinWidth = Convert.ToInt32(dict[nameof(MinWidth)]);
+ if (dict.ContainsKey(nameof(MaxWidth)))
+ MaxWidth = Convert.ToInt32(dict[nameof(MaxWidth)]);
+ if (dict.ContainsKey(nameof(CheckHeight)))
+ CheckHeight = Convert.ToBoolean(dict[nameof(CheckHeight)]);
+ if (dict.ContainsKey(nameof(MinHeight)))
+ MinHeight = Convert.ToInt32(dict[nameof(MinHeight)]);
+ if (dict.ContainsKey(nameof(MaxHeight)))
+ MaxHeight = Convert.ToInt32(dict[nameof(MaxHeight)]);
+ if (dict.ContainsKey(nameof(ReferenceId)))
+ ReferenceId = new Guid(dict[nameof(ReferenceId)].ToString());
+
+ // Load SearchArea properties manually
+ if (dict.ContainsKey("SearchArea_Editable"))
+ SearchArea.Editable = Convert.ToBoolean(dict["SearchArea_Editable"]);
+ if (dict.ContainsKey("SearchArea_IsGood"))
+ SearchArea.IsGood = Convert.ToBoolean(dict["SearchArea_IsGood"]);
+ if (dict.ContainsKey("SearchArea_LocationX") && dict.ContainsKey("SearchArea_LocationY"))
+ SearchArea.Location = new Vector2(Convert.ToSingle(dict["SearchArea_LocationX"]), Convert.ToSingle(dict["SearchArea_LocationY"]));
+ if (dict.ContainsKey("SearchArea_OffsetX") && dict.ContainsKey("SearchArea_OffsetY"))
+ SearchArea.Offset = new Vector2(Convert.ToSingle(dict["SearchArea_OffsetX"]), Convert.ToSingle(dict["SearchArea_OffsetY"]));
+ if (dict.ContainsKey("SearchArea_SizeX") && dict.ContainsKey("SearchArea_SizeY"))
+ SearchArea.Size = new Vector2(Convert.ToSingle(dict["SearchArea_SizeX"]), Convert.ToSingle(dict["SearchArea_SizeY"]));
+ }
}
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsOperation.cs
index a680547..eb8bfe2 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsOperation.cs
@@ -172,4 +172,51 @@ public class FindManyBlobsOperation:BaseOperation
SearchArea.Load(br);
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(MinArea)] = MinArea;
+ dict[nameof(MaxArea)] = MaxArea;
+ dict[nameof(MinRadius)] = MinRadius;
+ dict[nameof(MaxRadius)] = MaxRadius;
+ dict[nameof(ReferenceId)] = ReferenceId.ToString();
+
+ // Save SearchArea properties manually
+ dict["SearchArea_Editable"] = SearchArea.Editable;
+ dict["SearchArea_IsGood"] = SearchArea.IsGood;
+ dict["SearchArea_LocationX"] = SearchArea.Location.X;
+ dict["SearchArea_LocationY"] = SearchArea.Location.Y;
+ dict["SearchArea_OffsetX"] = SearchArea.Offset.X;
+ dict["SearchArea_OffsetY"] = SearchArea.Offset.Y;
+ dict["SearchArea_SizeX"] = SearchArea.Size.X;
+ dict["SearchArea_SizeY"] = SearchArea.Size.Y;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(MinArea)))
+ MinArea = Convert.ToInt32(dict[nameof(MinArea)]);
+ if (dict.ContainsKey(nameof(MaxArea)))
+ MaxArea = Convert.ToInt32(dict[nameof(MaxArea)]);
+ if (dict.ContainsKey(nameof(MinRadius)))
+ MinRadius = Convert.ToInt32(dict[nameof(MinRadius)]);
+ if (dict.ContainsKey(nameof(MaxRadius)))
+ MaxRadius = Convert.ToInt32(dict[nameof(MaxRadius)]);
+ if (dict.ContainsKey(nameof(ReferenceId)))
+ ReferenceId = new Guid(dict[nameof(ReferenceId)].ToString());
+
+ // Load SearchArea properties manually
+ if (dict.ContainsKey("SearchArea_Editable"))
+ SearchArea.Editable = Convert.ToBoolean(dict["SearchArea_Editable"]);
+ if (dict.ContainsKey("SearchArea_IsGood"))
+ SearchArea.IsGood = Convert.ToBoolean(dict["SearchArea_IsGood"]);
+ if (dict.ContainsKey("SearchArea_LocationX") && dict.ContainsKey("SearchArea_LocationY"))
+ SearchArea.Location = new Vector2(Convert.ToSingle(dict["SearchArea_LocationX"]), Convert.ToSingle(dict["SearchArea_LocationY"]));
+ if (dict.ContainsKey("SearchArea_OffsetX") && dict.ContainsKey("SearchArea_OffsetY"))
+ SearchArea.Offset = new Vector2(Convert.ToSingle(dict["SearchArea_OffsetX"]), Convert.ToSingle(dict["SearchArea_OffsetY"]));
+ if (dict.ContainsKey("SearchArea_SizeX") && dict.ContainsKey("SearchArea_SizeY"))
+ SearchArea.Size = new Vector2(Convert.ToSingle(dict["SearchArea_SizeX"]), Convert.ToSingle(dict["SearchArea_SizeY"]));
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindRectangleOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindRectangleOperation.cs
index 7800b7d..0da5558 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindRectangleOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindRectangleOperation.cs
@@ -109,4 +109,54 @@ public class FindRectangleOperation:BaseOperation
BadIfFound = br.ReadBoolean();
SearchArea.Load(br);
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(MinWidth)] = MinWidth;
+ dict[nameof(MaxWidth)] = MaxWidth;
+ dict[nameof(MinHeight)] = MinHeight;
+ dict[nameof(MaxHeight)] = MaxHeight;
+ dict[nameof(BadIfFound)] = BadIfFound;
+ dict[nameof(ReferenceId)] = ReferenceId.ToString();
+
+ // Save SearchArea properties manually
+ dict["SearchArea_Editable"] = SearchArea.Editable;
+ dict["SearchArea_IsGood"] = SearchArea.IsGood;
+ dict["SearchArea_LocationX"] = SearchArea.Location.X;
+ dict["SearchArea_LocationY"] = SearchArea.Location.Y;
+ dict["SearchArea_OffsetX"] = SearchArea.Offset.X;
+ dict["SearchArea_OffsetY"] = SearchArea.Offset.Y;
+ dict["SearchArea_SizeX"] = SearchArea.Size.X;
+ dict["SearchArea_SizeY"] = SearchArea.Size.Y;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(MinWidth)))
+ MinWidth = Convert.ToInt32(dict[nameof(MinWidth)]);
+ if (dict.ContainsKey(nameof(MaxWidth)))
+ MaxWidth = Convert.ToInt32(dict[nameof(MaxWidth)]);
+ if (dict.ContainsKey(nameof(MinHeight)))
+ MinHeight = Convert.ToInt32(dict[nameof(MinHeight)]);
+ if (dict.ContainsKey(nameof(MaxHeight)))
+ MaxHeight = Convert.ToInt32(dict[nameof(MaxHeight)]);
+ if (dict.ContainsKey(nameof(BadIfFound)))
+ BadIfFound = Convert.ToBoolean(dict[nameof(BadIfFound)]);
+ if (dict.ContainsKey(nameof(ReferenceId)))
+ ReferenceId = new Guid(dict[nameof(ReferenceId)].ToString());
+
+ // Load SearchArea properties manually
+ if (dict.ContainsKey("SearchArea_Editable"))
+ SearchArea.Editable = Convert.ToBoolean(dict["SearchArea_Editable"]);
+ if (dict.ContainsKey("SearchArea_IsGood"))
+ SearchArea.IsGood = Convert.ToBoolean(dict["SearchArea_IsGood"]);
+ if (dict.ContainsKey("SearchArea_LocationX") && dict.ContainsKey("SearchArea_LocationY"))
+ SearchArea.Location = new Vector2(Convert.ToSingle(dict["SearchArea_LocationX"]), Convert.ToSingle(dict["SearchArea_LocationY"]));
+ if (dict.ContainsKey("SearchArea_OffsetX") && dict.ContainsKey("SearchArea_OffsetY"))
+ SearchArea.Offset = new Vector2(Convert.ToSingle(dict["SearchArea_OffsetX"]), Convert.ToSingle(dict["SearchArea_OffsetY"]));
+ if (dict.ContainsKey("SearchArea_SizeX") && dict.ContainsKey("SearchArea_SizeY"))
+ SearchArea.Size = new Vector2(Convert.ToSingle(dict["SearchArea_SizeX"]), Convert.ToSingle(dict["SearchArea_SizeY"]));
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/TwoBlobsAlign.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/TwoBlobsAlign.cs
index 017d7ef..a17979c 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/TwoBlobsAlign.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/TwoBlobsAlign.cs
@@ -103,4 +103,23 @@ public class TwoBlobsAlign:BaseOperation,IHaveOrigin
DesiredAngle = br.ReadInt32();
}
+
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(Reference1)] = Reference1.ToString();
+ dict[nameof(Reference2)] = Reference2.ToString();
+ dict[nameof(DesiredAngle)] = DesiredAngle;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(Reference1)))
+ Reference1 = new Guid(dict[nameof(Reference1)].ToString());
+ if (dict.ContainsKey(nameof(Reference2)))
+ Reference2 = new Guid(dict[nameof(Reference2)].ToString());
+ if (dict.ContainsKey(nameof(DesiredAngle)))
+ DesiredAngle = Convert.ToInt32(dict[nameof(DesiredAngle)]);
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/EdgeIntersectionOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/EdgeIntersectionOperation.cs
index 6b77a7b..a86c452 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/EdgeIntersectionOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/EdgeIntersectionOperation.cs
@@ -100,5 +100,21 @@ public class EdgeIntersectionOperation : BaseOperation, IHaveOrigin
}
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(ReferenceEdge1)] = ReferenceEdge1.ToString();
+ dict[nameof(ReferenceEdge2)] = ReferenceEdge2.ToString();
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(ReferenceEdge1)))
+ ReferenceEdge1 = new Guid(dict[nameof(ReferenceEdge1)].ToString());
+ if (dict.ContainsKey(nameof(ReferenceEdge2)))
+ ReferenceEdge2 = new Guid(dict[nameof(ReferenceEdge2)].ToString());
+ }
+
public OriginElement Origin { get; set; } = OriginElement.Default;
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/FindEdgeOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/FindEdgeOperation.cs
index ab97113..28c3cf5 100644
--- a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/FindEdgeOperation.cs
+++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/FindEdgeOperation.cs
@@ -124,6 +124,48 @@ public class FindEdgeOperation : BaseOperation, IHaveEdge, IHaveOrigin
SearchArea.Load(br);
}
+ public override void Save(Dictionary dict)
+ {
+ base.Save(dict);
+ dict[nameof(ReferenceId)] = ReferenceId.ToString();
+
+ // Save RotatedRectangleElement SearchArea properties manually
+ dict["SearchArea_Editable"] = SearchArea.Editable;
+ dict["SearchArea_IsGood"] = SearchArea.IsGood;
+ dict["SearchArea_LocationX"] = SearchArea.Location.X;
+ dict["SearchArea_LocationY"] = SearchArea.Location.Y;
+ dict["SearchArea_OffsetX"] = SearchArea.Offset.X;
+ dict["SearchArea_OffsetY"] = SearchArea.Offset.Y;
+ dict["SearchArea_HeightX"] = SearchArea.Height.X;
+ dict["SearchArea_HeightY"] = SearchArea.Height.Y;
+ dict["SearchArea_HalfWidth"] = SearchArea.HalfWidth;
+ }
+
+ public override void Load(Dictionary dict)
+ {
+ base.Load(dict);
+ if (dict.ContainsKey(nameof(ReferenceId)))
+ ReferenceId = new Guid(dict[nameof(ReferenceId)].ToString());
+
+ // Load RotatedRectangleElement SearchArea properties manually
+ if (dict.ContainsKey("SearchArea_Editable") || dict.ContainsKey("SearchArea_LocationX") || dict.ContainsKey("SearchArea_HeightX"))
+ {
+ SearchArea = new RotatedRectangleElement();
+ if (dict.ContainsKey("SearchArea_Editable"))
+ SearchArea.Editable = Convert.ToBoolean(dict["SearchArea_Editable"]);
+ if (dict.ContainsKey("SearchArea_IsGood"))
+ SearchArea.IsGood = Convert.ToBoolean(dict["SearchArea_IsGood"]);
+ if (dict.ContainsKey("SearchArea_LocationX") && dict.ContainsKey("SearchArea_LocationY"))
+ SearchArea.Location = new Vector2(Convert.ToSingle(dict["SearchArea_LocationX"]), Convert.ToSingle(dict["SearchArea_LocationY"]));
+ if (dict.ContainsKey("SearchArea_OffsetX") && dict.ContainsKey("SearchArea_OffsetY"))
+ SearchArea.Offset = new Vector2(Convert.ToSingle(dict["SearchArea_OffsetX"]), Convert.ToSingle(dict["SearchArea_OffsetY"]));
+ if (dict.ContainsKey("SearchArea_HeightX") && dict.ContainsKey("SearchArea_HeightY"))
+ SearchArea.Height = new Vector2(Convert.ToSingle(dict["SearchArea_HeightX"]), Convert.ToSingle(dict["SearchArea_HeightY"]));
+ if (dict.ContainsKey("SearchArea_HalfWidth"))
+ SearchArea.HalfWidth = Convert.ToSingle(dict["SearchArea_HalfWidth"]);
+ }
+ }
+
public RotatedRectangleElement SearchArea { get; set; }
public OriginElement Origin { get; set; }
diff --git a/Hawkeye.VisionBuilder.Workflow/WorkflowList.cs b/Hawkeye.VisionBuilder.Workflow/WorkflowList.cs
index de14fa5..3cc2748 100644
--- a/Hawkeye.VisionBuilder.Workflow/WorkflowList.cs
+++ b/Hawkeye.VisionBuilder.Workflow/WorkflowList.cs
@@ -5,6 +5,7 @@ using System.ComponentModel.Design.Serialization;
using System.Diagnostics;
using System.Drawing;
using System.Reflection.PortableExecutable;
+using System.Text.Json;
using System.Xml;
using VisionBuilder.UI.Common.RecipeProcessing;
using Size = OpenCvSharp.Size;
@@ -32,6 +33,11 @@ public class WorkflowList
long total = 0;
foreach (BaseOperation operation in Operations)
{
+ if (!operation.Enabled)
+ {
+ operation.Result=true;
+ continue;
+ }
if (Context.CancellationToken.IsCancellationRequested) break;
operation.Interpret(Context);
}
@@ -62,6 +68,7 @@ public class WorkflowList
UpdateConfigVariables();
foreach (BaseOperation operation in Operations)
{
+ if(!operation.Enabled)continue;
if(operation == op) break;
operation.Interpret(Context);
}
@@ -275,7 +282,194 @@ public class WorkflowList
}
}
+ private Dictionary ConvertJsonElementsToNatives(Dictionary dict)
+ {
+ var result = new Dictionary();
+
+ foreach (var kvp in dict)
+ {
+ result[kvp.Key] = ConvertJsonElementToNative(kvp.Value);
+ }
+
+ return result;
+ }
+ private object ConvertJsonElementToNative(object value)
+ {
+ if (value is JsonElement element)
+ {
+ return element.ValueKind switch
+ {
+ JsonValueKind.String => element.GetString(),
+ JsonValueKind.Number => element.TryGetInt32(out var intVal) ? intVal :
+ element.TryGetDouble(out var doubleVal) ? doubleVal :
+ element.GetDecimal(),
+ JsonValueKind.True => true,
+ JsonValueKind.False => false,
+ JsonValueKind.Null => null,
+ JsonValueKind.Object => ConvertJsonElementsToNatives(
+ JsonSerializer.Deserialize>(element.GetRawText())),
+ JsonValueKind.Array => element.EnumerateArray()
+ .Select(x => ConvertJsonElementToNative(x)).ToArray(),
+ _ => value
+ };
+ }
+ else if (value is Dictionary nestedDict)
+ {
+ return ConvertJsonElementsToNatives(nestedDict);
+ }
+
+ return value;
+ }
+ public void SaveJSON(string fileName)
+ {
+ var workflowData = new Dictionary();
+
+ // Save Configuration
+ var configData = new Dictionary
+ {
+ ["RuntimeCameraType"] = Configuration.RuntimeCameraType.ToString(),
+ ["DevelopmentCameraType"] = Configuration.DevelopmentCameraType.ToString(),
+ ["Outputs"] = Configuration.Outputs.ToString(),
+ ["EmulationPath"] = Configuration.EmulationPath,
+ ["PythonPath"] = Configuration.PythonPath,
+ ["ResultPin"] = Configuration.ResultPin,
+ ["SerialPort"] = Configuration.SerialPort,
+ ["Delay"] = Configuration.Delay
+ };
+ workflowData["Configuration"] = configData;
+
+ // Save Operations
+ var operationsData = new List>();
+ foreach (BaseOperation operation in Operations)
+ {
+ var operationData = new Dictionary
+ {
+ ["Type"] = operation.GetType().AssemblyQualifiedName
+ };
+
+ var operationDict = new Dictionary();
+ operation.Save(operationDict);
+ operationData["Data"] = operationDict;
+
+ operationsData.Add(operationData);
+ }
+ workflowData["Operations"] = operationsData;
+
+ // Save Recipe Image
+ if (RecipeImage == null)
+ {
+ var mat = new Mat(128, 128, MatType.CV_8UC3, Scalar.Gray);
+ var image = mat.ToBytes();
+ workflowData["RecipeImage"] = Convert.ToBase64String(image);
+ }
+ else
+ {
+ var mat = RecipeImage;
+ var image = mat.Resize(new Size(128, 128)).ToBytes();
+ workflowData["RecipeImage"] = Convert.ToBase64String(image);
+ }
+
+ var options = new JsonSerializerOptions
+ {
+ WriteIndented = true
+ };
+ string jsonString = JsonSerializer.Serialize(workflowData, options);
+ File.WriteAllText(fileName, jsonString);
+ }
+
+ public void LoadJSON(string fileName, OperationDiscoveryService operationDiscoveryService)
+ {
+ string jsonString = File.ReadAllText(fileName);
+ var workflowData = JsonSerializer.Deserialize>(jsonString);
+
+ if (workflowData == null) return;
+
+ // Load Configuration
+ if (workflowData.ContainsKey("Configuration"))
+ {
+ var configElement = (JsonElement)workflowData["Configuration"];
+ var configData = JsonSerializer.Deserialize>(configElement.GetRawText());
+
+ if (configData != null)
+ {
+ var convertedConfigData = ConvertJsonElementsToNatives(configData);
+
+ if (convertedConfigData.ContainsKey("RuntimeCameraType"))
+ Configuration.RuntimeCameraType = Enum.Parse(convertedConfigData["RuntimeCameraType"].ToString());
+ if (convertedConfigData.ContainsKey("DevelopmentCameraType"))
+ Configuration.DevelopmentCameraType = Enum.Parse(convertedConfigData["DevelopmentCameraType"].ToString());
+ if (convertedConfigData.ContainsKey("Outputs"))
+ Configuration.Outputs = Enum.Parse(convertedConfigData["Outputs"].ToString());
+ if (convertedConfigData.ContainsKey("EmulationPath"))
+ Configuration.EmulationPath = convertedConfigData["EmulationPath"].ToString();
+ if (convertedConfigData.ContainsKey("PythonPath"))
+ Configuration.PythonPath = convertedConfigData["PythonPath"].ToString();
+ if (convertedConfigData.ContainsKey("ResultPin"))
+ Configuration.ResultPin = (int)convertedConfigData["ResultPin"];
+ if (convertedConfigData.ContainsKey("SerialPort"))
+ Configuration.SerialPort = convertedConfigData["SerialPort"].ToString();
+ if (convertedConfigData.ContainsKey("Delay"))
+ Configuration.Delay = (int)convertedConfigData["Delay"];
+ }
+ }
+
+ if (!File.Exists(Configuration.PythonPath))
+ PythonMissing();
+
+ // Load Operations
+ Operations.Clear();
+ if (workflowData.ContainsKey("Operations"))
+ {
+ var operationsElement = (JsonElement)workflowData["Operations"];
+ var operationsArray = JsonSerializer.Deserialize>>(operationsElement.GetRawText());
+
+ if (operationsArray != null)
+ {
+ foreach (var operationData in operationsArray)
+ {
+ if (operationData.ContainsKey("Type") && operationData.ContainsKey("Data"))
+ {
+ string operationType = operationData["Type"].ToString();
+ var type = Type.GetType(operationType);
+ if (type != null)
+ {
+ BaseOperation operation = operationDiscoveryService.CreateInstance(type);
+
+ var dataElement = (JsonElement)operationData["Data"];
+ var operationDict = JsonSerializer.Deserialize>(dataElement.GetRawText());
+
+ if (operationDict != null)
+ {
+ var convertedDict = ConvertJsonElementsToNatives(operationDict);
+ operation.Load(convertedDict);
+ }
+
+ Operations.Add(operation);
+ }
+ }
+ }
+ }
+ }
+
+ // Load Recipe Image
+ if (workflowData.ContainsKey("RecipeImage"))
+ {
+ string base64Image = workflowData["RecipeImage"].ToString();
+ if (!string.IsNullOrEmpty(base64Image))
+ {
+ byte[] imageData = Convert.FromBase64String(base64Image);
+ RecipeImage = Mat.FromImageData(imageData);
+ }
+ }
+ }
+
+ public static WorkflowList LoadJSONFromFile(string fileName)
+ {
+ var workflowList = new WorkflowList();
+ workflowList.LoadJSON(fileName, new OperationDiscoveryService(workflowList));
+ return workflowList;
+ }
}
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.cs b/Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.cs
index 701fe04..22c4987 100644
--- a/Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.cs
+++ b/Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.cs
@@ -43,7 +43,7 @@ namespace Hawkeye.VisionBuilder.Features.ImageStatistics
_goodNode.Nodes.Add(new TreeNode(nodeText) { Tag = filePath });
_allFiles.Add(filePath);
UpdateBranchText(_goodNode, "Good");
- treeView.ExpandAll();
+ //treeView.ExpandAll();
}
public void AddBadImage(string filePath, string[]? defects = null)
@@ -66,7 +66,7 @@ namespace Hawkeye.VisionBuilder.Features.ImageStatistics
_badNode.Nodes.Add(new TreeNode(nodeText) { Tag = filePath });
_allFiles.Add(filePath);
UpdateBranchText(_badNode, "Bad");
- treeView.ExpandAll();
+ //treeView.ExpandAll();
}
private void UpdateBranchText(TreeNode node, string name)
diff --git a/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.Designer.cs b/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.Designer.cs
index d2d1075..2395917 100644
--- a/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.Designer.cs
+++ b/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.Designer.cs
@@ -35,7 +35,7 @@
btnTest = new Button();
button1 = new Button();
Indicator = new DataGridViewTextBoxColumn();
- Link = new DataGridViewTextBoxColumn();
+ Enabled = new DataGridViewCheckBoxColumn();
UserName = new DataGridViewTextBoxColumn();
Result = new DataGridViewTextBoxColumn();
TypeName = new DataGridViewTextBoxColumn();
@@ -50,14 +50,13 @@
dataGridView1.AllowUserToAddRows = false;
dataGridView1.AllowUserToDeleteRows = false;
dataGridView1.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
- dataGridView1.Columns.AddRange(new DataGridViewColumn[] { Indicator, Link, UserName, Result, TypeName, Time });
+ dataGridView1.Columns.AddRange(new DataGridViewColumn[] { Indicator, Enabled, UserName, Result, TypeName, Time });
dataGridView1.Dock = DockStyle.Fill;
dataGridView1.Location = new Point(0, 40);
dataGridView1.MultiSelect = false;
dataGridView1.Name = "dataGridView1";
dataGridView1.ReadOnly = true;
dataGridView1.RowHeadersVisible = false;
- dataGridView1.RowTemplate.Height = 25;
dataGridView1.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dataGridView1.Size = new Size(540, 562);
dataGridView1.TabIndex = 0;
@@ -126,12 +125,12 @@
Indicator.ReadOnly = true;
Indicator.Width = 32;
//
- // Link
+ // Enabled
//
- Link.HeaderText = "";
- Link.Name = "Link";
- Link.ReadOnly = true;
- Link.Width = 32;
+ Enabled.HeaderText = "";
+ Enabled.Name = "Enabled";
+ Enabled.ReadOnly = true;
+ Enabled.Width = 32;
//
// UserName
//
@@ -182,7 +181,7 @@
private Label lblProcessingTime;
private Label label1;
private DataGridViewTextBoxColumn Indicator;
- private DataGridViewTextBoxColumn Link;
+ private DataGridViewCheckBoxColumn Enabled;
private DataGridViewTextBoxColumn UserName;
private DataGridViewTextBoxColumn Result;
private DataGridViewTextBoxColumn TypeName;
diff --git a/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.cs b/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.cs
index ceb1704..9da07ef 100644
--- a/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.cs
+++ b/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.cs
@@ -33,6 +33,7 @@ namespace Hawkeye.VisionBuilder.Features.VisionSteps
dataGridView1.DataSource = _bindingSource;
dataGridView1.Columns[0].DataPropertyName = nameof(BaseOperationDecorator.Result);
+ dataGridView1.Columns[1].DataPropertyName = nameof(BaseOperationDecorator.IsEnabled);
dataGridView1.Columns[2].DataPropertyName = nameof(BaseOperationDecorator.Label);
dataGridView1.Columns[2].ReadOnly = false;
dataGridView1.ReadOnly = false;
@@ -88,56 +89,41 @@ namespace Hawkeye.VisionBuilder.Features.VisionSteps
private void DataGridView1_Paint(object? sender, PaintEventArgs e)
{
- var g = e.Graphics;
- var c1 = dataGridView1.GetCellDisplayRectangle(1, 0, true);
- var c2 = dataGridView1.GetCellDisplayRectangle(1, 1, true);
-
- Pen start = new Pen(Color.Blue, 2);
-
- Pen body = new Pen(Color.Blue, 2);
-
- Pen end = new Pen(Color.Blue, 2);
-
- end.EndCap = LineCap.Custom;
-
- AdjustableArrowCap bigArrow = new AdjustableArrowCap(5, 5);
- end.CustomEndCap = bigArrow;
-
- g.DrawLine(start, c1.Right, (c1.Bottom - c1.Top) / 2 + c1.Top, (c1.Right - c1.Left) / 2 + c1.Left,
- (c1.Bottom - c1.Top) / 2 + c1.Top);
-
- g.DrawLine(body, (c1.Right - c1.Left) / 2 + c1.Left, (c1.Bottom - c1.Top) / 2 + c1.Top,
- (c2.Right - c2.Left) / 2 + c1.Left, (c2.Bottom - c2.Top) / 2 + c2.Top);
-
- g.DrawLine(end, (c2.Right - c2.Left) / 2 + c2.Left, (c2.Bottom - c2.Top) / 2 + c2.Top, c2.Right,
- (c2.Bottom - c2.Top) / 2 + c2.Top);
-
+
}
private void DataGridView1_CellPainting(object? sender, DataGridViewCellPaintingEventArgs e)
{
-
- if (e.ColumnIndex == 0 && e.RowIndex > -1 && e.Value != null)
+
+ if (e.RowIndex > -1)
{
- var cx = e.CellBounds.Width / 2 + e.CellBounds.Left;
- var cy = e.CellBounds.Height / 2 + e.CellBounds.Top;
- var radius = e.CellBounds.Height / 2 - 3;
var c1 = dataGridView1.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, true);
+ var row = dataGridView1.Rows[e.RowIndex];
+ var rowValue = row.DataBoundItem as BaseOperationDecorator;
e.PaintBackground(c1, true);
-
- if ((bool)e.Value)
+ if (e.ColumnIndex == 0 && e.Value != null)
{
- e.Graphics.FillEllipse(Brushes.Green, cx - radius, cy - radius, radius * 2, radius * 2);
- }
- else
- {
- e.Graphics.FillEllipse(Brushes.Red, cx - radius, cy - radius, radius * 2, radius * 2);
- }
+ var cx = e.CellBounds.Width / 2 + e.CellBounds.Left;
+ var cy = e.CellBounds.Height / 2 + e.CellBounds.Top;
+ var radius = e.CellBounds.Height / 2 - 3;
- e.Handled = true;
+
+ if ((bool) e.Value)
+ {
+ e.Graphics.FillEllipse(Brushes.Green, cx - radius, cy - radius, radius * 2, radius * 2);
+ }
+ else
+ {
+ e.Graphics.FillEllipse(Brushes.Red, cx - radius, cy - radius, radius * 2, radius * 2);
+ }
+ e.Handled = true;
+
+
+ }
+
}
}
diff --git a/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.resx b/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.resx
index bb59ef4..6e2d66b 100644
--- a/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.resx
+++ b/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.resx
@@ -1,4 +1,64 @@
-
+
+
+
@@ -60,7 +120,7 @@
True
-
+
True
diff --git a/Hawkeye.VisionBuilder/MainWindow.cs b/Hawkeye.VisionBuilder/MainWindow.cs
index 2810e36..73765a7 100644
--- a/Hawkeye.VisionBuilder/MainWindow.cs
+++ b/Hawkeye.VisionBuilder/MainWindow.cs
@@ -182,13 +182,26 @@ namespace Hawkeye.VisionBuilder
private void loadRecipeToolStripMenuItem_Click(object sender, EventArgs e)
{
OpenFileDialog dialog = new OpenFileDialog();
- dialog.Filter = "*.hrcp|*.hrcp";
+ dialog.Filter = "Suported types|*.jhrcp;*.hrcp";
PrepareDialog(dialog);
if (dialog.ShowDialog() == DialogResult.OK)
{
var path = dialog.FileName;
- LoadRecipe(path);
+ var ext = Path.GetExtension(path);
+ if (ext == ".jhrcp")
+ {
+ LoadRecipeJSON(path);
+ }
+ else if (ext == ".hrcp")
+ {
+ LoadRecipeBinary(path);
+ }
+ else
+ {
+ MessageBox.Show("Unsupported file format. Please use .hrcp or .jhrcp files.");
+ return;
+ }
}
}
@@ -210,7 +223,7 @@ namespace Hawkeye.VisionBuilder
private string _loadedPath = null;
- public void LoadRecipe(string path)
+ public void LoadRecipeBinary(string path)
{
_loadedPath = path;
var bytes = File.ReadAllBytes(path);
@@ -218,13 +231,23 @@ namespace Hawkeye.VisionBuilder
BinaryReader reader = new BinaryReader(stream);
- _workflowList.PythonMissing += _workflowList_PythonMissing;
+
_workflowList.Load(reader, _discoveryService);
ReinitCameraPanel(_workflowList);
OrganizeWindows(null, null);
_visionStepsPanel.RefreshWorkflow();
}
+ public void LoadRecipeJSON(string path)
+ {
+ _loadedPath = path;
+ _workflowList.LoadJSON(path,_discoveryService);
+ ReinitCameraPanel(_workflowList);
+ OrganizeWindows(null, null);
+ _visionStepsPanel.RefreshWorkflow();
+
+ }
+
private void _workflowList_PythonMissing()
{
SetupConfig();
@@ -234,20 +257,33 @@ namespace Hawkeye.VisionBuilder
private void saveRecipeToolStripMenuItem_Click(object sender, EventArgs e)
{
SaveFileDialog dialog = new SaveFileDialog();
- dialog.Filter = "*.hrcp|*.hrcp";
+ dialog.Filter = "*.jhrcp|*.jhrcp|*.hrcp|*.hrcp";
PrepareDialog(dialog);
if (dialog.ShowDialog() == DialogResult.OK)
{
- MemoryStream ms = new MemoryStream();
- BinaryWriter bw = new BinaryWriter(ms);
-
- _workflowList.RecipeImage = _workflowList.Context.LastCameraImage.ImageData;
- _workflowList.Save(bw);
- ms.Close();
- File.WriteAllBytes(dialog.FileName, ms.ToArray());
+ var ext = Path.GetExtension(dialog.FileName);
+ if (ext == ".jhrcp")
+ {
+ _workflowList.SaveJSON(dialog.FileName);
+ }
+ else
+ {
+ SaveBinary(dialog.FileName);
+ }
}
}
+ private void SaveBinary(string path)
+ {
+ MemoryStream ms = new MemoryStream();
+ BinaryWriter bw = new BinaryWriter(ms);
+
+ _workflowList.RecipeImage = _workflowList.Context.LastCameraImage.ImageData;
+ _workflowList.Save(bw);
+ ms.Close();
+ File.WriteAllBytes(path, ms.ToArray());
+ }
+
private void toolBtnConfiguration_Click(object sender, EventArgs e)
{
SetupConfig();
diff --git a/VisionBuilder.UI.Camera/ModuleExtensions.cs b/VisionBuilder.UI.Camera/ModuleExtensions.cs
index 794996d..9fdd331 100644
--- a/VisionBuilder.UI.Camera/ModuleExtensions.cs
+++ b/VisionBuilder.UI.Camera/ModuleExtensions.cs
@@ -1,4 +1,5 @@
using Hawkeye.VisionBuilder.UI.Sources.Emulation;
+using Hawkeye.VisionBuilder.UI.Sources.Hawkeye;
using Inspectron.Camera.UEye;
using Ninject;
using Ninject.Extensions.ChildKernel;
@@ -26,6 +27,7 @@ public static class ModuleExtensions
kernel.Bind().To().InSingletonScope();
break;
case CameraSettings.EImageSource.Hawkeye:
+ kernel.Bind().To().InSingletonScope();
break;
case CameraSettings.EImageSource.IDS:
kernel.Bind().To().InSingletonScope();
diff --git a/VisionBuilder.UI.Camera/VisionBuilder.UI.Camera.csproj b/VisionBuilder.UI.Camera/VisionBuilder.UI.Camera.csproj
index 149eabe..e458279 100644
--- a/VisionBuilder.UI.Camera/VisionBuilder.UI.Camera.csproj
+++ b/VisionBuilder.UI.Camera/VisionBuilder.UI.Camera.csproj
@@ -8,6 +8,7 @@
+
diff --git a/VisionBuilder.UI.sln b/VisionBuilder.UI.sln
index 4531492..6ac656d 100644
--- a/VisionBuilder.UI.sln
+++ b/VisionBuilder.UI.sln
@@ -75,6 +75,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "B24SiemensEmulator", "Plugi
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestPlugin", "Plugins\TestPlugin\TestPlugin.csproj", "{F7D39916-489A-3583-09A0-175AE82D08B7}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hawkeye.VisionBuilder.UI.Sources.Hawkeye", "Hawkeye.VisionBuilder.UI.Sources.Hawkeye\Hawkeye.VisionBuilder.UI.Sources.Hawkeye.csproj", "{C55BE2DA-C60B-491C-8668-9517C7F6FF2F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Inspectron.HawkEye", "framework\Inspectron.HawkEye\Inspectron.HawkEye.csproj", "{44D1BA17-FB52-40A2-9D99-E49DA56C10C2}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Starters", "Starters", "{F3414823-B70E-435E-B4EA-80ABF4371449}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -197,12 +203,22 @@ Global
{F7D39916-489A-3583-09A0-175AE82D08B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F7D39916-489A-3583-09A0-175AE82D08B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F7D39916-489A-3583-09A0-175AE82D08B7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C55BE2DA-C60B-491C-8668-9517C7F6FF2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C55BE2DA-C60B-491C-8668-9517C7F6FF2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C55BE2DA-C60B-491C-8668-9517C7F6FF2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C55BE2DA-C60B-491C-8668-9517C7F6FF2F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {44D1BA17-FB52-40A2-9D99-E49DA56C10C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {44D1BA17-FB52-40A2-9D99-E49DA56C10C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {44D1BA17-FB52-40A2-9D99-E49DA56C10C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {44D1BA17-FB52-40A2-9D99-E49DA56C10C2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
+ {B62D435C-3572-4B5B-A381-992345D343B0} = {F3414823-B70E-435E-B4EA-80ABF4371449}
{D5FD2E9D-DA4F-1343-47E0-FBC473A149BC} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1}
+ {7697A266-A721-4D74-9C5C-0D4F2F6BBF68} = {F2406FBB-DFD3-4CBE-9644-A9FFC2FCBB71}
{E286CE4C-B68A-94B6-F477-0DBF42358009} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1}
{E8868ABD-E4D0-1B7E-494E-06FB1F7D1AF5} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1}
{8B7FAFEE-4066-483C-9C9C-10D2D596206D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
@@ -224,6 +240,8 @@ Global
{DB20082B-60E8-D623-3F3A-04612A929CD6} = {583A77DF-A293-4F3E-AB96-7310BC495822}
{52D6A9AE-D0F1-4C52-B688-E0219169E179} = {583A77DF-A293-4F3E-AB96-7310BC495822}
{F7D39916-489A-3583-09A0-175AE82D08B7} = {F2406FBB-DFD3-4CBE-9644-A9FFC2FCBB71}
+ {C55BE2DA-C60B-491C-8668-9517C7F6FF2F} = {D05689E3-04C6-4E3B-ACA7-3F4507CED4CC}
+ {44D1BA17-FB52-40A2-9D99-E49DA56C10C2} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3CE42AE5-D79F-4E97-A246-AA8FD228B677}
diff --git a/framework/Inspectron.HawkEye/Camera/I2CLinux.cs b/framework/Inspectron.HawkEye/Camera/I2CLinux.cs
new file mode 100644
index 0000000..b754f1a
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Camera/I2CLinux.cs
@@ -0,0 +1,159 @@
+using System;
+using System.IO;
+using Mono.Unix.Native;
+
+namespace Inspectron.Devices.Raspberry
+{
+ public unsafe class I2CLinux
+ {
+ string device;
+ int fd = -1;
+
+ public I2CLinux(int index)
+ {
+ device = "/dev/i2c-" + index;
+ Open();
+ //Close();
+ }
+
+ public void Open()
+ {
+ fd = Syscall.open(device, OpenFlags.O_RDWR);
+ if (fd < 0)
+ throw new IOException(device);
+ }
+
+ void IoCtl(byte devAddr)
+ {
+ int ret = LunixNatives.ioctl(fd, LunixNatives.I2C_SLAVE, devAddr);
+ if (ret < 0)
+ throw new IOException(device + ": ioctl");
+ }
+
+ public byte readBytes(byte devAddr, byte regAddr, byte length, byte[] data, int offset, ushort timeout = 0)
+ {
+ if (length > 127)
+ throw new IOException(device + ": length > 127");
+
+ //Open();
+
+ IoCtl(devAddr);
+
+ //fixed(byte* p = ®Addr)
+ {
+ int ret = (int)Syscall.write(fd, ®Addr, 1);
+ if (ret != 1)
+ throw new IOException(device + ": write");
+ }
+
+ int count;
+ fixed (byte* p = &data[offset])
+ {
+ count = (int)Syscall.read(fd, p, (ulong)length);
+ if (count < 0)
+ throw new IOException(device + ": read");
+ else if (count != length)
+ throw new IOException(device + ": read short: length = " + length + " > " + count);
+ }
+
+ //Close();
+
+ return (byte)count;
+ }
+
+ public byte readBytes(byte devAddr, byte regAddr, byte length, byte[] data, ushort timeout = 0)
+ {
+ return readBytes(devAddr, regAddr, length, data, 0, timeout);
+ }
+
+ /** Write multiple bytes to an 8-bit device register.
+ * @param devAddr I2C slave device address
+ * @param regAddr First register address to write to
+ * @param length Number of bytes to write
+ * @param data Buffer to copy new data from
+ * @return Status of operation (true = success)
+ */
+ public void writeBytes(byte devAddr, byte regAddr, byte length, byte[] data)
+ {
+ if (length > 127)
+ throw new IOException(device + ": length > 127");
+
+ //Open();
+ IoCtl(devAddr);
+
+ byte[] buffer = new byte[128];
+ buffer[0] = regAddr;
+ Array.Copy(data, 0, buffer, 1, length);
+
+ int count;
+ fixed (byte* p = buffer)
+ {
+ count = (int)Syscall.write(fd, p, (ulong)(length + 1));
+ }
+
+ if (count < 0)
+ {
+ throw new IOException(device + ": write = " + count);
+ }
+ else if (count != length + 1)
+ {
+ throw new IOException(device + ": write short = " + count);
+ }
+
+ //Close();
+ }
+
+
+ /** Write multiple words to a 16-bit device register.
+ * @param devAddr I2C slave device address
+ * @param regAddr First register address to write to
+ * @param length Number of words to write
+ * @param data Buffer to copy new data from
+ * @return Status of operation (true = success)
+ */
+ public void writeWords(byte devAddr, byte regAddr, byte length, ushort[] data)
+ {
+ int count = 0;
+ byte[] buf = new byte[128];
+ int i;
+
+ // Should do potential byteswap and call writeBytes() really, but that
+ // messes with the callers buffer
+
+ if (length > 63)
+ {
+ throw new IOException(device + ": length > 63");
+ }
+
+ //Open();
+ IoCtl(devAddr);
+
+ buf[0] = regAddr;
+ for (i = 0; i < (int)length; i++)
+ {
+ buf[i * 2 + 1] = (byte)(data[i] >> 8);
+ buf[i * 2 + 2] = (byte)data[i];
+ }
+ fixed (byte* p = buf)
+ {
+ count = (int)Syscall.write(fd, p, (ulong)(length * 2 + 1));
+ }
+ if (count < 0)
+ {
+ throw new IOException(device + ": write");
+ }
+ else if (count != length * 2 + 1)
+ {
+ throw new IOException(device + ": write short");
+ }
+ //Close();
+ }
+
+ public void Close()
+ {
+ int ret = Syscall.close(fd);
+ if (ret != 0)
+ throw new IOException(device);
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Camera/InspectronCamera.cs b/framework/Inspectron.HawkEye/Camera/InspectronCamera.cs
new file mode 100644
index 0000000..0bf398d
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Camera/InspectronCamera.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Threading;
+using Inspectron.Devices.Raspberry;
+using Inspectron.HawkEye.Protocol;
+using Inspectron.HawkEye.Protocol.Interfaces;
+
+namespace Inspectron.HawkEye.Camera
+{
+ public class InspectronCamera:ICameraControl,IImageSource,ILightControl
+ {
+
+ [DllImport("libVCLibProxy.so", CallingConvention = CallingConvention.Cdecl)]
+ static extern IntPtr init(Int32 captBuf);
+ [DllImport("libVCLibProxy.so", CallingConvention = CallingConvention.Cdecl)]
+ static extern int trigger(IntPtr cpt, byte[] addr, int lines, int captBuf,ref int cancelFlag);
+ [DllImport("libVCLibProxy.so", CallingConvention = CallingConvention.Cdecl)]
+ static extern void set_parameters(IntPtr cpt, ref ImageSettings imageSettings);
+
+ [DllImport("libVCLibProxy.so", CallingConvention = CallingConvention.Cdecl)]
+ public static extern int test();
+ byte[] _buffer = new byte[2048 * 250 * 4];
+
+ public InspectronCamera()
+ {
+
+ _i2c = new I2CLinux(0);
+ _i2c.Open();
+
+ }
+
+
+
+ private bool _isRunningContiniuos = false;
+ private IntPtr _cp= IntPtr.Zero;
+ private ImageSettings _imageSettings=new ImageSettings()
+ {
+ CaptureBuffer = 25,
+ Gain = 200,
+ Lines = 1000,
+ Shutter = 200,
+ SensorWidth = 1440
+ };
+
+ private I2CLinux _i2c;
+
+ private int _lastBuffer = -1;
+ public void SetParameters(CameraSettings cameraSettings)
+
+ {
+ _cameraSettings = cameraSettings;
+ ImageSettings imageSettings = cameraSettings.ImageSettings;
+ if (_lastBuffer ==-1)
+ {
+ _lastBuffer = imageSettings.CaptureBuffer;
+ _cp = init(_lastBuffer);
+ }
+
+
+ _imageSettings = imageSettings;
+ _buffer=new byte[imageSettings.SensorWidth*(imageSettings.Lines)];
+ var cpImageSettings = imageSettings;
+ cpImageSettings.UseExternalTrigger =imageSettings.UseExternalTrigger;
+
+ if(_lastBuffer==imageSettings.CaptureBuffer)
+ set_parameters(_cp, ref cpImageSettings);
+ else
+ Console.WriteLine("Warning! Buffer size changed. Needs restart");
+ _i2c.writeBytes(4, 2, 4, BitConverter.GetBytes(imageSettings.Divider));
+ Thread.Sleep(100);
+ _i2c.writeBytes(4,3,1, new byte[] { (byte)imageSettings.UseExternalTrigger });
+ Thread.Sleep(100);
+ }
+
+
+ private int _cancelFlag = 0;
+ private CameraSettings _cameraSettings;
+
+ public byte[] GetImage()
+ {
+ _cancelFlag = 0;
+ var sw = Stopwatch.StartNew();
+
+ _i2c.writeBytes(4, 4, 1,new byte[]{ (byte)(_cameraSettings.LaserTrigger?1:0)} );
+ //Thread.Sleep(20);
+ _i2c.writeBytes(4, 5, 4, BitConverter.GetBytes(_cameraSettings.LaserTriggerDelay));
+ //Thread.Sleep(20);
+
+ trigger(_cp, _buffer, _imageSettings.Lines, _imageSettings.CaptureBuffer,ref _cancelFlag);
+ sw.Stop();
+ Console.WriteLine("Trigger time: "+sw.ElapsedMilliseconds);
+ if(_cancelFlag==1)return new byte[0];
+ return _buffer;
+ }
+
+ public void CancelTrigger()
+ {
+ _cancelFlag = 1;
+ }
+
+ public void SetLight(int pwm1, int pwm2)
+ {
+ Console.WriteLine($"setting lights to {pwm1}/{pwm2}");
+ pwm1 = (int)(pwm1 / 100.0 * 255);
+ pwm2 = (int)(pwm2 / 100.0 * 255);
+ _i2c.writeBytes(4, 1, 2, new byte[] { (byte)pwm1,(byte)pwm2 });
+
+ Thread.Sleep(10);
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Camera/LunixNatives.cs b/framework/Inspectron.HawkEye/Camera/LunixNatives.cs
new file mode 100644
index 0000000..de126bd
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Camera/LunixNatives.cs
@@ -0,0 +1,21 @@
+using System.Runtime.InteropServices;
+
+namespace Inspectron.Devices.Raspberry
+{
+ public static class LunixNatives
+ {
+ public const int O_RDWR = 2;
+
+ [DllImport("libc.so.6")]
+ extern public static int open(string file, int mode);
+
+ [DllImport("libc.so.6")]
+ extern public static int close(int fd);
+
+ [DllImport("libc.so.6")]
+ extern public static int ioctl(int fd, int request, byte x);
+
+ public const int I2C_SLAVE = 0x0703;
+
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/DefragmentedPacket.cs b/framework/Inspectron.HawkEye/DefragmentedPacket.cs
new file mode 100644
index 0000000..03f7be3
--- /dev/null
+++ b/framework/Inspectron.HawkEye/DefragmentedPacket.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+
+namespace Inspectron.HawkEye
+{
+ public class DefragmentedPacket
+ {
+ private readonly uint _packetSize;
+ private byte[] _receivedParts = null;
+ //ConcurrentDictionary _packetParts = new ConcurrentDictionary();
+ private byte[][] _packetParts;
+ public DefragmentedPacket(uint packetSize)
+ {
+ _packetSize = packetSize;
+ _packetParts=new byte[30000][];
+ }
+
+ private int _uniquePackets=0;
+ public void Defragment(byte[] data)
+ {
+ var packetStart = -1;
+
+ for (int i = 0; i < data.Length; i++)
+ {
+ if (BitConverter.ToUInt32(data, i) == 114455)
+ {
+ packetStart = i;
+ break;
+ };
+ }
+
+ if (packetStart == -1) return;
+
+ MemoryStream ms = new MemoryStream(data,packetStart, data.Length - packetStart);
+ BinaryReader br = new BinaryReader(ms);
+ br.ReadUInt32();//packetStart
+ var packetType = br.ReadUInt32();
+ var sequenceId = br.ReadUInt32();
+ var packetNumber = br.ReadUInt32();
+
+ var totalPackets = br.ReadUInt32();
+
+ if(_receivedParts==null)_receivedParts=new byte[totalPackets];
+ if (_receivedParts[packetNumber] == 1) return;
+ _receivedParts[packetNumber] = 1;
+ Interlocked.Increment(ref _uniquePackets);
+ var dataLen = br.ReadInt32();
+
+ var dataBytes = br.ReadBytes(dataLen);
+
+
+ _packetParts[packetNumber] = dataBytes;
+
+ }
+
+ public byte[] Reconstruct()
+ {
+ var parts = _receivedParts.Length;
+ byte[] res = new byte[parts*_packetSize];
+ Stopwatch sw = Stopwatch.StartNew();
+ int resSize = 0;
+ for (int i = 0; i < parts; i++)
+ {
+ //if (_packetParts.ContainsKey((uint) i))
+ if (_packetParts[i]!=null)
+ {
+ var packetData = _packetParts[(uint) i];
+
+ Array.Copy(packetData, 0,res, resSize, packetData.Length);
+ resSize += packetData.Length;
+ }
+ else
+ {
+ resSize += (int)_packetSize-20/*headerSize*/;
+ }
+ }
+
+ sw.Stop();
+
+
+ Array.Resize(ref res,resSize);
+
+ return res;
+
+ }
+
+ public bool IsComplete
+ {
+ get
+ {
+ if (_receivedParts == null) return false;
+ return _uniquePackets == _receivedParts.Length;
+ //return _receivedParts.All(x => x == 1);
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/FragmentedPacket.cs b/framework/Inspectron.HawkEye/FragmentedPacket.cs
new file mode 100644
index 0000000..1a9ec45
--- /dev/null
+++ b/framework/Inspectron.HawkEye/FragmentedPacket.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Inspectron.HawkEye
+{
+ public class FragmentedPacket
+ {
+ private readonly uint _packetSize;
+ private readonly EPacketType _packetType;
+ private readonly uint _sequnceId;
+
+ public FragmentedPacket(uint packetSize,EPacketType packetType,uint sequnceId)
+ {
+ _packetSize = packetSize;
+ _packetType = packetType;
+ _sequnceId = sequnceId;
+ }
+ public IEnumerable Fragment(byte[] packetData)
+ {
+
+
+
+ uint dataPtr = 0;
+
+ int packetNumber = 0;
+ uint headerSize = 20;
+ uint payloadSize = (_packetSize - headerSize);
+ var totalPackets = (uint)Math.Ceiling(((double)packetData.Length / payloadSize));
+ do
+ {
+ MemoryStream ms = new MemoryStream();
+ BinaryWriter bw = new BinaryWriter(ms);
+ bw.Write((uint) 114455); //packetStart //4
+ bw.Write((uint) _packetType); //8
+ bw.Write((uint) _sequnceId); //12
+ bw.Write((uint) packetNumber); //16
+ //total packets?
+
+
+ bw.Write(totalPackets); //20
+
+ byte[] data = new byte[payloadSize];
+ uint dataSize = Math.Min((uint)(packetData.Length-dataPtr), payloadSize);
+ Array.Copy(packetData, dataPtr, data, 0, dataSize);
+ dataPtr += dataSize;
+ bw.Write(dataSize);
+ bw.Write(data);
+
+ packetNumber += 1;
+ yield return ms.ToArray();
+
+ } while (packetNumber < totalPackets);
+
+
+
+
+
+
+ }
+ public IEnumerable FragmentTo(byte[] packetData,UDPSocket socket)
+ {
+
+
+
+ uint dataPtr = 0;
+
+ int packetNumber = 0;
+ uint headerSize = 20;
+ uint payloadSize = (_packetSize - headerSize);
+ var totalPackets = (uint)Math.Ceiling(((double)packetData.Length / payloadSize));
+ do
+ {
+ MemoryStream ms = new MemoryStream();
+ BinaryWriter bw = new BinaryWriter(ms);
+ bw.Write((uint)114455); //packetStart //4
+ bw.Write((uint)_packetType); //8
+ bw.Write((uint)_sequnceId); //12
+ bw.Write((uint)packetNumber); //16
+ //total packets?
+
+
+ bw.Write(totalPackets); //20
+
+ byte[] data = new byte[payloadSize];
+ uint dataSize = Math.Min((uint)(packetData.Length - dataPtr), payloadSize);
+ Array.Copy(packetData, dataPtr, data, 0, dataSize);
+ dataPtr += dataSize;
+ bw.Write(dataSize);
+ bw.Write(data);
+
+ packetNumber += 1;
+ yield return ms.ToArray();
+
+ } while (packetNumber < totalPackets);
+
+
+
+
+
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Inspectron.HawkEye.csproj b/framework/Inspectron.HawkEye/Inspectron.HawkEye.csproj
new file mode 100644
index 0000000..e73d307
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Inspectron.HawkEye.csproj
@@ -0,0 +1,21 @@
+
+
+
+ netstandard2.0
+
+
+
+ true
+
+
+
+ true
+
+
+
+
+
+
+
+
+
diff --git a/framework/Inspectron.HawkEye/Packets/EPacketType.cs b/framework/Inspectron.HawkEye/Packets/EPacketType.cs
new file mode 100644
index 0000000..f014bf7
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Packets/EPacketType.cs
@@ -0,0 +1,9 @@
+namespace Inspectron.HawkEye
+{
+ public enum EPacketType
+ {
+ Test,
+ ImageData,
+ ImageRequest
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Packets/ImageRequestPacket.cs b/framework/Inspectron.HawkEye/Packets/ImageRequestPacket.cs
new file mode 100644
index 0000000..553b5d5
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Packets/ImageRequestPacket.cs
@@ -0,0 +1,7 @@
+namespace Inspectron.HawkEye
+{
+ public class ImageRequestPacket
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Packets/Packet.cs b/framework/Inspectron.HawkEye/Packets/Packet.cs
new file mode 100644
index 0000000..b66af52
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Packets/Packet.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace Inspectron.HawkEye
+{
+ public class Packet
+ {
+ public EPacketType PacketType { get; set; }
+ public byte[] Payload { get; set; }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/Packets/PacketImage.cs b/framework/Inspectron.HawkEye/Packets/PacketImage.cs
new file mode 100644
index 0000000..7725f83
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Packets/PacketImage.cs
@@ -0,0 +1,12 @@
+namespace Inspectron.HawkEye
+{
+ public class PacketImage
+ {
+ public uint TriggerId { get; set; }
+ public uint PacketId { get; set; }
+ public uint TotalPackets { get; set; }
+
+ public uint StartIndex { get; set; }
+ public byte[] Data { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Protocol/CameraSettings.cs b/framework/Inspectron.HawkEye/Protocol/CameraSettings.cs
new file mode 100644
index 0000000..dcc557a
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Protocol/CameraSettings.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+
+namespace Inspectron.HawkEye.Protocol
+{
+ public class CameraSettings
+ {
+ public ImageSettings ImageSettings { get; set; }
+ public int LightPwm1 { get; set; }
+ public int LightPwm2 { get; set; }
+ public string Name { get; set; }
+ public int OffsetX { get; set; }
+ public int ImageWidth { get; set; }
+ public int RescaleWidth { get; set; }
+ public int MinorCutoff { get; set; }
+ public bool BayerFilter { get; set; }
+ public bool LaserTrigger { get; set; }
+ public int LaserTriggerDelay { get; set; }
+ public bool FlipLines { get; set; }
+ public bool MirrorX { get; set; }
+ public bool TriggerLights { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Protocol/Discovery/CameraInfo.cs b/framework/Inspectron.HawkEye/Protocol/Discovery/CameraInfo.cs
new file mode 100644
index 0000000..37068ef
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Protocol/Discovery/CameraInfo.cs
@@ -0,0 +1,12 @@
+using System.Net;
+using System.Net.NetworkInformation;
+
+namespace Inspectron.HawkEye.Protocol.Discovery
+{
+ public class CameraInfo
+ {
+ public string Mac { get; set; }
+ public IPAddress Address { get; set; }
+ public IPAddress AdapterAddress { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Protocol/Discovery/DiscoveryClient.cs b/framework/Inspectron.HawkEye/Protocol/Discovery/DiscoveryClient.cs
new file mode 100644
index 0000000..c66ed7a
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Protocol/Discovery/DiscoveryClient.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Inspectron.HawkEye.Protocol.Discovery
+{
+ public class DiscoveryClient
+ {
+ public ReadOnlyCollection Discovered => new ReadOnlyCollection(_discovered);
+ private readonly int _port;
+ private readonly List _discovered = new List();
+
+ private readonly object _discoveryLock = new object();
+
+ public DiscoveryClient(int port)
+ {
+ _port = port;
+ }
+
+ public event Action CameraFound = delegate { };
+
+ public void Discover()
+ {
+ var allInterfaces = NetworkInterface
+ .GetAllNetworkInterfaces()
+ .Where(nic => nic.OperationalStatus == OperationalStatus.Up);
+
+
+ Parallel.ForEach(allInterfaces, DiscoverOnInterface);
+ //foreach (NetworkInterface i in allInterfaces)
+ //{
+ // DiscoverOnInterface(i);
+ //}
+ }
+
+ private void DiscoverOnInterface(NetworkInterface iface)
+ {
+ var address = iface.GetIPProperties().UnicastAddresses
+ .First(x => x.Address.AddressFamily == AddressFamily.InterNetwork).Address;
+ UdpClient client;
+ lock (_discoveryLock)
+ {
+ client = new UdpClient(new IPEndPoint(address, 0));
+ var requestData = Encoding.ASCII.GetBytes("discovery");
+ client.Client.ReceiveTimeout = 2000;
+
+ var s = client.Client;
+ s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
+ s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DontRoute, 1);
+ client.EnableBroadcast = true;
+ client.Send(requestData, requestData.Length, new IPEndPoint(IPAddress.Broadcast, _port));
+ client.Send(requestData, requestData.Length, new IPEndPoint(IPAddress.Broadcast, _port));
+ client.Send(requestData, requestData.Length, new IPEndPoint(IPAddress.Broadcast, _port));
+ }
+
+ var serverEp = new IPEndPoint(IPAddress.Any, 0);
+
+ byte[] serverResponseData;
+ try
+ {
+ serverResponseData = client.Receive(ref serverEp);
+ var serverResponse = Encoding.ASCII.GetString(serverResponseData);
+ Console.WriteLine("Recived {0} from {1}", serverResponse, serverEp.Address);
+ var found = new CameraInfo {Mac = serverResponse, Address = serverEp.Address,AdapterAddress = address};
+ lock (_discoveryLock)
+ {
+ if (_discovered.Any(x => x.Mac == found.Mac)) return;
+ }
+
+ CameraFound(found);
+ _discovered.Add(found);
+ }
+ catch
+ {
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Protocol/Discovery/DiscoveryServer.cs b/framework/Inspectron.HawkEye/Protocol/Discovery/DiscoveryServer.cs
new file mode 100644
index 0000000..b6fbc9e
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Protocol/Discovery/DiscoveryServer.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Linq;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+
+namespace Inspectron.HawkEye.Protocol.Discovery
+{
+ public class DiscoveryServer
+ {
+ private UdpClient _server;
+ private byte[] _name;
+
+ public DiscoveryServer(int port,string name=null)
+ {
+ _server = new UdpClient(port);
+
+ if(name==null)
+ { _name = Encoding.UTF8.GetBytes( NetworkInterface
+ .GetAllNetworkInterfaces()
+ .Where(nic => nic.OperationalStatus == OperationalStatus.Up && nic.NetworkInterfaceType != NetworkInterfaceType.Loopback)
+ .Select(nic => nic.GetPhysicalAddress().ToString())
+ .FirstOrDefault());
+
+ }
+ else
+ {
+ _name = Encoding.UTF8.GetBytes(name);
+ }
+
+ }
+
+ public void Start()
+ {
+ Thread th = new Thread(DiscoveryLoop);
+ th.Start();
+ }
+ private void DiscoveryLoop()
+ {
+ while (true)
+ {
+ var clientEp = new IPEndPoint(IPAddress.Any, 0);
+ var clientRequestData = _server.Receive(ref clientEp);
+ var clientRequest = Encoding.ASCII.GetString(clientRequestData);
+
+ Console.WriteLine("Recived {0} from {1}, sending response", clientRequest, clientEp.Address.ToString());
+ _server.Send(_name, _name.Length, clientEp);
+ _server.Send(_name, _name.Length, clientEp);
+ _server.Send(_name, _name.Length, clientEp);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Protocol/ECommand.cs b/framework/Inspectron.HawkEye/Protocol/ECommand.cs
new file mode 100644
index 0000000..bd0d444
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Protocol/ECommand.cs
@@ -0,0 +1,13 @@
+namespace Inspectron.HawkEye.Protocol
+{
+ public enum ECommand
+ {
+ Connect,
+ Trigger, StartContinuous, StopContinuous,
+ Settings,SaveSettings,
+ OK,
+ SaveCalibration,
+ GetCalibration,
+ NotOK
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Protocol/EData.cs b/framework/Inspectron.HawkEye/Protocol/EData.cs
new file mode 100644
index 0000000..84e3a0d
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Protocol/EData.cs
@@ -0,0 +1,9 @@
+namespace Inspectron.HawkEye.Protocol
+{
+ public enum EData
+ {
+ Image,
+
+ OK
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Protocol/ImageClient.cs b/framework/Inspectron.HawkEye/Protocol/ImageClient.cs
new file mode 100644
index 0000000..d8c5a0b
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Protocol/ImageClient.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Net;
+using System.Text;
+using System.Threading;
+using Inspectron.HawkEye.UDPB;
+using Newtonsoft.Json;
+
+namespace Inspectron.HawkEye.Protocol
+{
+ public class ImageClient : IDisposable
+ {
+ private static readonly object connectionLock = new object();
+ private readonly IPEndPoint _endpoint;
+ private readonly IPAddress _adapter;
+
+ private readonly UDPBSocket _imageSocket = new UDPBSocket();
+ private readonly UDPBSocket _commandSocket = new UDPBSocket();
+ private bool _isConnected = true;
+
+ public ImageClient(IPEndPoint endpoint,IPAddress adapter)
+ {
+ _endpoint = endpoint;
+ _adapter = adapter;
+
+ }
+
+ public void Dispose()
+ {
+ _isConnected = false;
+ _commandSocket.Dispose();
+ }
+
+ public void Connect()
+ {
+ lock (connectionLock)
+ {
+ _commandSocket.Connect(_endpoint,_adapter);
+ var port = UDPBSocket.FindFreePort(_adapter);
+ var bytesPort = BitConverter.GetBytes(port);
+ _commandSocket.SendData(new[]
+ {(byte) ECommand.Connect, bytesPort[0], bytesPort[1], bytesPort[2], bytesPort[3]});
+ var settingsData0 = _commandSocket.Receive();
+ var b = new byte[settingsData0.Length - 1];
+ Array.Copy(settingsData0, 1, b, 0, b.Length);
+ var settingsString = Encoding.UTF8.GetString(b);
+ SettingsReceived(JsonConvert.DeserializeObject(settingsString));
+ _imageSocket.Listen(_adapter,port);
+ var th = new Thread(ReceiveLoop);
+ th.Start();
+ }
+ }
+
+ public event Action ImageReceived = delegate { };
+ public event Action SettingsReceived = delegate { };
+
+
+ public void Trigger()
+ {
+ _commandSocket.SendData(new[] {(byte) ECommand.Trigger});
+ }
+
+ public void ApplySettings(CameraSettings cameraSettings)
+ {
+ var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(cameraSettings));
+ var databytes = new byte[1000];
+ databytes[0] = (byte) ECommand.Settings;
+ Array.Copy(data, 0, databytes, 1, data.Length);
+
+ _commandSocket.SendData(databytes);
+ }
+
+ public void StartContinuous()
+ {
+ _commandSocket.SendData(new[] {(byte) ECommand.StartContinuous});
+ }
+
+ public void StopContinuous()
+ {
+ _commandSocket.SendData(new[] {(byte) ECommand.StopContinuous});
+ }
+
+ public void SaveSettingsOnCamera()
+ {
+ _commandSocket.SendData(new[] {(byte) ECommand.SaveSettings});
+ }
+
+
+ private void ReceiveLoop()
+ {
+ while (_isConnected)
+ {
+ var data = _imageSocket.Receive();
+
+ Process(data);
+
+
+ }
+ }
+
+ private void Process(byte[] data)
+ {
+ switch ((EData) data[0])
+ {
+ case EData.Image:
+ var b = new byte[data.Length - 1];
+ Array.Copy(data, 1, b, 0, b.Length);
+ ImageReceived(data);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Protocol/ImageClientTCP.cs b/framework/Inspectron.HawkEye/Protocol/ImageClientTCP.cs
new file mode 100644
index 0000000..811fd26
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Protocol/ImageClientTCP.cs
@@ -0,0 +1,192 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using Inspectron.HawkEye.UDPB;
+using Newtonsoft.Json;
+
+namespace Inspectron.HawkEye.Protocol
+{
+ public class ImageClientTCP : IDisposable
+ {
+ private static readonly object connectionLock = new object();
+ private readonly IPEndPoint _endpoint;
+ private readonly IPAddress _adapter;
+
+ private TcpListener _imageSocket;
+ private TcpClient _commandSocket;
+ private bool _isConnected = true;
+
+ public ImageClientTCP(IPEndPoint endpoint,IPAddress adapter)
+ {
+ _endpoint = endpoint;
+ _adapter = adapter;
+
+ }
+
+ public void Dispose()
+ {
+ _isConnected = false;
+ _commandSocket.Dispose();
+ }
+ byte[] _commandBuffer = new byte[1500];
+ private TcpClient _lastClient;
+ public bool SupportsCalibration { get; set; }
+ public void Connect()
+ {
+ lock (connectionLock)
+ {
+ _commandSocket=new TcpClient(new IPEndPoint(_adapter, 0));
+ Console.WriteLine($"Bind on {_adapter?.ToString()}");
+
+ _commandSocket.Connect(_endpoint);
+ var port = UDPBSocket.FindFreePort(_adapter);
+ Console.WriteLine("Connected");
+
+ _imageSocket = new TcpListener(_adapter, port);
+ _imageSocket.Start();
+
+ Console.WriteLine("TCP started");
+
+ var bytesPort = BitConverter.GetBytes(port);
+ _commandSocket.Client.SendData(new[]
+ {(byte) ECommand.Connect, bytesPort[0], bytesPort[1], bytesPort[2], bytesPort[3]});
+
+ var receivedLen = _commandSocket.Client.Receive(_commandBuffer);
+ Console.WriteLine("received answer length:"+receivedLen);
+ var b = new byte[receivedLen - 1];
+ Array.Copy(_commandBuffer, 1, b, 0, b.Length);
+ var settingsString = Encoding.UTF8.GetString(b);
+ SettingsReceived(JsonConvert.DeserializeObject(settingsString));
+ if (SupportsCalibration)
+ {
+
+ _commandSocket.Client.SendData(new[] {(byte) ECommand.GetCalibration});
+ receivedLen = _commandSocket.Client.Receive(_commandBuffer);
+ if (_commandBuffer[0] == (byte) ECommand.OK)
+ {
+ Console.WriteLine($"calibration received {receivedLen} bytes");
+ var c = new byte[receivedLen - 1];
+ Array.Copy(_commandBuffer, 1, c, 0, c.Length);
+ var calibrationString = Encoding.UTF8.GetString(c);
+ CalibrationReceived(JsonConvert.DeserializeObject>>(calibrationString));
+
+ }
+ else
+ {
+ Console.WriteLine("no calibration received");
+ }
+ }
+
+
+ var th = new Thread(ReceiveLoop);
+ th.Start();
+ }
+ }
+
+ public event Action ImageReceived = delegate { };
+ public event Action SettingsReceived = delegate { };
+ public event Action>> CalibrationReceived = delegate { };
+
+
+ public void Trigger()
+ {
+ _commandSocket.Client.SendData(new[] {(byte) ECommand.Trigger});
+ }
+
+ public void ApplySettings(CameraSettings cameraSettings)
+ {
+ var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(cameraSettings));
+ var databytes = new byte[1000];
+ databytes[0] = (byte) ECommand.Settings;
+ Array.Copy(data, 0, databytes, 1, data.Length);
+
+ _commandSocket.Client.SendData(databytes);
+ byte[] ok = new byte[1];
+ _commandSocket.Client.Receive(ok);
+ }
+
+ public void StartContinuous()
+ {
+ _commandSocket.Client.SendData(new[] {(byte) ECommand.StartContinuous});
+ }
+
+ public void StopContinuous()
+ {
+ _commandSocket.Client.SendData(new[] {(byte) ECommand.StopContinuous});
+ }
+
+ public void SaveSettingsOnCamera()
+ {
+ _commandSocket.Client.SendData(new[] {(byte) ECommand.SaveSettings});
+ }
+
+ private byte[] _imageBuffer = new byte[10*1024*1024];
+ private void ReceiveLoop()
+ {
+ while (_isConnected)
+ {
+ _lastClient = _imageSocket.AcceptTcpClient();
+ while (true)
+ {
+ int received=0;
+
+ try
+ {
+ _lastClient.Client.Receive(_imageBuffer, 0, 1,
+ SocketFlags.None);
+ Process(_imageBuffer);
+ }
+ catch
+ {
+ break;
+ }
+
+
+ }
+
+
+
+ }
+ }
+
+ public void SaveCalibration(List> calibration)
+ {
+ var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(calibration));
+ var databytes = new byte[1500];
+ databytes[0] = (byte)ECommand.SaveCalibration;
+ Array.Copy(data, 0, databytes, 1, data.Length);
+
+ _commandSocket.Client.SendData(databytes);
+ byte[] ok = new byte[1];
+ _commandSocket.Client.Receive(ok);
+ }
+
+ private void Process(byte[] data)
+ {
+ switch ((EData) data[0])
+ {
+ case EData.Image:
+ _lastClient.Client.Receive(_imageBuffer, 1, 4,
+ SocketFlags.None);
+ var imageSize = BitConverter.ToInt32(_imageBuffer,1);
+ var received = 0;
+ do
+ {
+ received += _lastClient.Client.Receive(_imageBuffer, 5+ received, imageSize- received,
+ SocketFlags.None);
+ } while (received < imageSize);
+
+ var b = new byte[imageSize];
+ Array.Copy(data, 5, b, 0, b.Length);
+ ImageReceived(b);
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Protocol/ImageServer.cs b/framework/Inspectron.HawkEye/Protocol/ImageServer.cs
new file mode 100644
index 0000000..bab6ce6
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Protocol/ImageServer.cs
@@ -0,0 +1,164 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Inspectron.HawkEye.Protocol.Interfaces;
+using Inspectron.HawkEye.UDPB;
+using Newtonsoft.Json;
+
+namespace Inspectron.HawkEye.Protocol
+{
+ public class ImageServer
+ {
+ private readonly IImageSource _imageSource;
+ private readonly ICameraControl _cameraControl;
+ private readonly ILightControl _lightControl;
+ private readonly UDPBSocket _commandSocket = new UDPBSocket();
+ private readonly UDPBSocket _imageSocket = new UDPBSocket();
+
+ private bool _autoTrigger;
+ private bool _applySettings;
+ private CameraSettings _imageSettingsToApply=new CameraSettings(){ImageSettings = new ImageSettings()};
+
+ public ImageServer(IImageSource imageSource, ICameraControl cameraControl, ILightControl lightControl)
+ {
+ _imageSource = imageSource;
+ _cameraControl = cameraControl;
+ _lightControl = lightControl;
+ _imageSocket.LossSimulation = 0;
+ }
+
+ public void Start()
+ {
+ _commandSocket.Listen(IPAddress.Any, 27001);
+ var th = new Thread(ReceiveLoop);
+ th.Start();
+ if (File.Exists("settings.json"))
+ {
+ var settings = JsonConvert.DeserializeObject(File.ReadAllText("settings.json"));
+ _imageSettingsToApply = settings;
+ ApplyParameters(settings);
+ }
+ }
+
+ public void ApplyParameters(CameraSettings cameraSettings)
+ {
+ _cameraControl.SetParameters(cameraSettings);
+
+ _lightControl.SetLight(cameraSettings.LightPwm1, cameraSettings.LightPwm2);
+ }
+
+ private void SaveSettingsLocally()
+ {
+ File.WriteAllText("settings.json", JsonConvert.SerializeObject(_imageSettingsToApply));
+ }
+
+ private void ReceiveLoop()
+ {
+ while (true)
+ {
+
+ var data = _commandSocket.Receive();
+ try
+ {
+ ProcessCommand(data);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ }
+ }
+ }
+
+ private void TriggerLoop()
+ {
+ while (_autoTrigger)
+ {
+
+ if (_applySettings)
+ {
+ ApplyParameters(_imageSettingsToApply);
+ _applySettings = false;
+ }
+
+ try
+ {
+ SendImage();
+
+ }
+ catch
+ {
+ return;
+ }
+ }
+ }
+
+ private void ProcessCommand(byte[] data)
+ {
+ Console.WriteLine(((ECommand) data[0]).ToString());
+ switch ((ECommand) data[0])
+ {
+ case ECommand.Connect:
+ {
+ var port = new byte[4];
+ Array.Copy(data, 1, port, 0, 4);
+ _imageSocket.Connect(new IPEndPoint((_commandSocket.LastConnection as IPEndPoint).Address, BitConverter.ToInt32(port,0)),null);
+ var databytes = new byte[1000];
+ var settings = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(_imageSettingsToApply));
+ Array.Copy(settings, 0, databytes, 1, settings.Length);
+ databytes[0] = (byte) ECommand.Settings;
+ _commandSocket.SendData(databytes);
+ }
+ break;
+ case ECommand.Trigger:
+ {
+ Task.Run(() => { SendImage(); });
+ }
+ break;
+ case ECommand.StartContinuous:
+ {
+ _autoTrigger = true;
+ var th = new Thread(TriggerLoop);
+ th.Start();
+ }
+ break;
+ case ECommand.StopContinuous:
+ {
+ _autoTrigger = false;
+ }
+ break;
+ case ECommand.Settings:
+ {
+ var databytes = new byte[999];
+ Array.Copy(data, 1, databytes, 0, 999);
+ var settings = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(databytes));
+ _imageSettingsToApply = settings;
+ if (_autoTrigger)
+ _applySettings = true;
+ else
+ ApplyParameters(_imageSettingsToApply);
+ }
+ break;
+ case ECommand.SaveSettings:
+ {
+ SaveSettingsLocally();
+ }
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ private void SendImage()
+ {
+ var image = _imageSource.GetImage();
+ var b = new byte[image.Length + 1];
+ b[0] = (byte) EData.Image;
+ Array.Copy(image, 0, b, 1, image.Length);
+
+ _imageSocket.SendData(b);
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Protocol/ImageServerTCP.cs b/framework/Inspectron.HawkEye/Protocol/ImageServerTCP.cs
new file mode 100644
index 0000000..b9e3560
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Protocol/ImageServerTCP.cs
@@ -0,0 +1,269 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Inspectron.HawkEye.Protocol.Interfaces;
+using Inspectron.HawkEye.UDPB;
+using Newtonsoft.Json;
+
+namespace Inspectron.HawkEye.Protocol
+{
+ public class ImageServerTCP
+ {
+ private readonly IImageSource _imageSource;
+ private readonly ICameraControl _cameraControl;
+ private readonly ILightControl _lightControl;
+ private TcpListener _commandSocket;
+ private TcpClient _imageSocket;
+
+ private bool _autoTrigger;
+ private bool _applySettings;
+
+ private CameraSettings _imageSettingsToApply=new CameraSettings(){ImageSettings = new ImageSettings()};
+ private TcpClient _lastClient;
+
+ public ImageServerTCP(IImageSource imageSource, ICameraControl cameraControl, ILightControl lightControl)
+ {
+ _imageSource = imageSource;
+ _cameraControl = cameraControl;
+ _lightControl = lightControl;
+
+ }
+
+ public void Start()
+ {
+ _commandSocket = new TcpListener(IPAddress.Any, 27001);
+ _commandSocket.Start();
+ Console.WriteLine("listen tcp");
+ var th = new Thread(ReceiveLoop);
+ th.Start();
+ if (File.Exists("settings.json"))
+ {
+ var settings = JsonConvert.DeserializeObject(File.ReadAllText("settings.json"));
+ _imageSettingsToApply = settings;
+ ApplyParameters(settings);
+ }
+ }
+
+ public void ApplyParameters(CameraSettings cameraSettings, bool setLight = false)
+ {
+ Console.WriteLine("Setting parameters:"+JsonConvert.SerializeObject(cameraSettings,Formatting.Indented));
+ _cameraControl.SetParameters(cameraSettings);
+ if(setLight&&!cameraSettings.TriggerLights) _lightControl.SetLight(cameraSettings.LightPwm1, cameraSettings.LightPwm2);
+ else _lightControl.SetLight(0, 0);
+
+ }
+
+ private void SaveSettingsLocally()
+ {
+ File.WriteAllText("settings.json", JsonConvert.SerializeObject(_imageSettingsToApply,Formatting.Indented));
+ }
+ byte[] _commandBuffer = new byte[1500];
+ private Task _lastTriggerTask=Task.CompletedTask;
+
+ private void ReceiveLoop()
+ {
+ while (true)
+ {
+
+ _lastClient=_commandSocket.AcceptTcpClient();
+
+ do
+ {
+ try
+ {
+ _lastClient.GetStream().Read(_commandBuffer, 0, 1500);
+ }
+ catch
+ {
+ break;
+ }
+
+ ProcessCommand(_commandBuffer);
+ } while (_lastClient.Connected);
+ CancelTrigger();
+ _lightControl.SetLight(0, 0);
+ }
+ }
+
+ private void TriggerLoop()
+ {
+ while (_autoTrigger)
+ {
+
+ if (_applySettings)
+ {
+ ApplyParameters(_imageSettingsToApply,true);
+ _applySettings = false;
+ }
+
+ try
+ {
+ SendImage();
+
+ }
+ catch(Exception e)
+ {
+ Console.WriteLine(e.ToString());
+ return;
+ }
+ }
+ }
+
+ private void ProcessCommand(byte[] data)
+ {
+ Console.WriteLine(((ECommand) data[0]).ToString());
+ switch ((ECommand) data[0])
+ {
+ case ECommand.Connect:
+ {
+ var port = new byte[4];
+ Array.Copy(data, 1, port, 0, 4);
+ _imageSocket=new TcpClient();
+ _imageSocket.Connect(new IPEndPoint((_lastClient.Client.RemoteEndPoint as IPEndPoint).Address, BitConverter.ToInt32(port,0)));
+ var databytes = new byte[1000];
+ var settings = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(_imageSettingsToApply));
+ Array.Copy(settings, 0, databytes, 1, settings.Length);
+ databytes[0] = (byte) ECommand.Settings;
+ _lastClient.Client.SendData(databytes);
+ Console.WriteLine("data sent");
+
+
+ _lightControl.SetLight(_imageSettingsToApply.LightPwm1, _imageSettingsToApply.LightPwm2);
+ _autoTrigger = false;
+ }
+ break;
+ case ECommand.GetCalibration:
+ {
+ var databytes = new byte[1500];
+ if (File.Exists("calibration.calib"))
+ {
+ databytes[0] = (byte)ECommand.OK;
+ Array.Copy(File.ReadAllBytes("calibration.calib"),0,databytes,1,1500-1);
+ _lastClient.Client.SendData(databytes);
+ }
+ else
+ {
+ databytes[0] = (byte)ECommand.NotOK;
+ _lastClient.Client.SendData(databytes);
+ }
+ }
+ break;
+ case ECommand.Trigger:
+ {
+ CancelTrigger();
+ _lastTriggerTask=Task.Run(() =>
+ {
+ try
+ {
+ SendImage();
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e.ToString());
+ }
+
+ });
+
+ }
+ break;
+ case ECommand.StartContinuous:
+ {
+ _autoTrigger = true;
+ var th = new Thread(TriggerLoop);
+ th.Start();
+ }
+ break;
+ case ECommand.StopContinuous:
+ {
+ _autoTrigger = false;
+ }
+ break;
+ case ECommand.Settings:
+ {
+ CancelTrigger();
+ var databytes = new byte[999];
+ Array.Copy(data, 1, databytes, 0, 999);
+ var settings = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(databytes));
+ _imageSettingsToApply = settings;
+ if (_autoTrigger)
+ _applySettings = true;
+ else
+ ApplyParameters(_imageSettingsToApply,true);
+
+ _lastClient.Client.Send(new []{(byte)EData.OK});
+
+ }
+ break;
+ case ECommand.SaveCalibration:
+ {
+
+ var databytes = new byte[1500 - 1];
+ Array.Copy(data, 1, databytes, 0, 1500 - 1);
+ File.WriteAllBytes("calibration.calib",databytes);
+
+ _lastClient.Client.Send(new[] { (byte)EData.OK });
+
+ }
+ break;
+ case ECommand.SaveSettings:
+ {
+ SaveSettingsLocally();
+ }
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ private void CancelTrigger()
+ {
+ _imageSource.CancelTrigger();
+ _lastTriggerTask.Wait();
+
+ }
+ private void SendImage()
+ {
+ if (_imageSettingsToApply.TriggerLights)
+ {
+ _lightControl.SetLight(_imageSettingsToApply.LightPwm1, _imageSettingsToApply.LightPwm2);
+ }
+
+ var image = _imageSource.GetImage();
+ if (image.Length == 0) return;
+ if (_imageSettingsToApply.TriggerLights)
+ {
+ _lightControl.SetLight(0, 0);
+ }
+
+ var b = EncodeImage(image);
+
+ _imageSocket.Client.SendData(b);
+ }
+
+ private static byte[] EncodeImage(byte[] image)
+ {
+ var b = new byte[image.Length + 5];
+ b[0] = (byte) EData.Image;
+ var byteSize = BitConverter.GetBytes(image.Length);
+ Array.Copy(byteSize, 0, b, 1, 4);
+ Array.Copy(image, 0, b, 5, image.Length);
+ return b;
+ }
+ private static byte[] EncodeChanneledImage(byte[] image,byte channels)
+ {
+ var b = new byte[image.Length + 6];
+ b[0] = (byte)EData.Image;
+ b[1] = channels;
+ var byteSize = BitConverter.GetBytes(image.Length);
+ Array.Copy(byteSize, 0, b, 2, 4);
+ Array.Copy(image, 0, b, 6, image.Length);
+ return b;
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Protocol/ImageSettings.cs b/framework/Inspectron.HawkEye/Protocol/ImageSettings.cs
new file mode 100644
index 0000000..ef94738
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Protocol/ImageSettings.cs
@@ -0,0 +1,17 @@
+using System.Runtime.InteropServices;
+
+namespace Inspectron.HawkEye.Protocol
+{
+ [StructLayout(LayoutKind.Sequential, Pack = 0)]
+ public struct ImageSettings
+ {
+ public int Shutter { get; set; }
+ public int Gain { get; set; }
+ public int SensorWidth { get; set; }
+ public int Lines { get; set; }
+ public int CaptureBuffer { get; set; }
+ public int UseExternalTrigger { get; set; }
+ public int Divider { get; set; }
+
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Protocol/Interfaces/ICameraControl.cs b/framework/Inspectron.HawkEye/Protocol/Interfaces/ICameraControl.cs
new file mode 100644
index 0000000..59ee6e2
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Protocol/Interfaces/ICameraControl.cs
@@ -0,0 +1,10 @@
+namespace Inspectron.HawkEye.Protocol.Interfaces
+{
+ public interface ICameraControl
+ {
+
+ void SetParameters(CameraSettings imageSettings);
+
+
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Protocol/Interfaces/IImageSource.cs b/framework/Inspectron.HawkEye/Protocol/Interfaces/IImageSource.cs
new file mode 100644
index 0000000..43b0928
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Protocol/Interfaces/IImageSource.cs
@@ -0,0 +1,8 @@
+namespace Inspectron.HawkEye.Protocol.Interfaces
+{
+ public interface IImageSource
+ {
+ byte[] GetImage();
+ void CancelTrigger();
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Protocol/Interfaces/ILightControl.cs b/framework/Inspectron.HawkEye/Protocol/Interfaces/ILightControl.cs
new file mode 100644
index 0000000..07d55f0
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Protocol/Interfaces/ILightControl.cs
@@ -0,0 +1,7 @@
+namespace Inspectron.HawkEye.Protocol.Interfaces
+{
+ public interface ILightControl
+ {
+ void SetLight(int pwm1, int pwm2);
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/Protocol/SocketExtensions.cs b/framework/Inspectron.HawkEye/Protocol/SocketExtensions.cs
new file mode 100644
index 0000000..cc765ad
--- /dev/null
+++ b/framework/Inspectron.HawkEye/Protocol/SocketExtensions.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Net;
+using System.Net.Sockets;
+using Inspectron.HawkEye.RTSP;
+
+namespace Inspectron.HawkEye.Protocol
+{
+ public static class SocketExtensions
+ {
+ public static void Listen(this Socket self,IPAddress adapterAddress,int port)
+ {
+ self.Bind(new IPEndPoint(adapterAddress,port));
+ }
+
+ public static void SendData(this Socket self, byte[] data)
+ {
+ Console.WriteLine("send data");
+
+
+ self.Send(data);
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/RTSP/AACPayload.cs b/framework/Inspectron.HawkEye/RTSP/AACPayload.cs
new file mode 100644
index 0000000..f3b61fc
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/AACPayload.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+
+namespace Inspectron.HawkEye.RTSP
+{
+ // This class handles the AAC-hbd (High Bitrate) Payload
+ // It has methods to process the RTP Payload
+
+ // (c) 2018 Roger Hardiman, RJH Technical Consultancy Ltd
+
+
+ /*
+ RFC 3640
+ 3.3.6. High Bit-rate AAC
+
+ This mode is signaled by mode=AAC-hbr.This mode supports the
+ transportation of variable size AAC frames.In one RTP packet,
+ either one or more complete AAC frames are carried, or a single
+ fragment of an AAC frame is carried.In this mode, the AAC frames
+ are allowed to be interleaved and hence receivers MUST support de-
+ interleaving.The maximum size of an AAC frame in this mode is 8191
+ octets.
+
+ In this mode, the RTP payload consists of the AU Header Section,
+ followed by either one AAC frame, several concatenated AAC frames or
+ one fragmented AAC frame.The Auxiliary Section MUST be empty. For
+ each AAC frame contained in the payload, there MUST be an AU-header
+ in the AU Header Section to provide:
+
+ a) the size of each AAC frame in the payload and
+
+ b) index information for computing the sequence(and hence timing) of
+ each AAC frame.
+
+ To code the maximum size of an AAC frame requires 13 bits.
+ Therefore, in this configuration 13 bits are allocated to the AU-
+ size, and 3 bits to the AU-Index(-delta) field.Thus, each AU-header
+ has a size of 2 octets.Each AU-Index field MUST be coded with the
+ value 0. In the AU Header Section, the concatenated AU-headers MUST
+ be preceded by the 16-bit AU-headers-length field, as specified in
+ section 3.2.1.
+
+ In addition to the required MIME format parameters, the following
+ parameters MUST be present: sizeLength, indexLength, and
+ indexDeltaLength.AAC frames always have a fixed duration per Access
+ Unit; when interleaving in this mode, this specific duration MUST be
+ signaled by the MIME format parameter constantDuration.In addition,
+ the parameter maxDisplacement MUST be present when interleaving.
+
+ For example:
+
+ m= audio 49230 RTP/AVP 96
+ a= rtpmap:96 mpeg4-generic/48000/6
+ a= fmtp:96 streamtype= 5; profile-level-id= 16; mode= AAC-hbr;config= 11B0; sizeLength= 13; indexLength= 3;indexDeltaLength= 3; constantDuration= 1024
+
+ The hexadecimal value of the "config" parameter is the AudioSpecificConfig(), as defined in ISO/IEC 14496-3.
+ AudioSpecificConfig() specifies a 5.1 channel AAC stream with a sampling rate of 48 kHz.For the description of MIME parameters, see
+ section 4.1.
+
+ */
+
+
+ public class AACPayload
+ {
+ public uint ObjectType = 0;
+ public uint FrequencyIndex = 0;
+ public uint ChannelConfiguration = 0;
+
+ // Constructor
+ public AACPayload(String config_string)
+ {
+ /***
+ 5 bits: object type
+ if (object type == 31)
+ 6 bits + 32: object type
+ 4 bits: frequency index
+ if (frequency index == 15)
+ 24 bits: frequency
+ 4 bits: channel configuration
+ var bits: AOT Specific Config
+ ***/
+
+ // config is a string in hex eg 1490 or 0x1210
+ // Read each ASCII character and add to a bit array
+ BitStream bs = new BitStream();
+ bs.AddHexString(config_string);
+
+ // Read 5 bits
+ ObjectType = bs.Read(5);
+
+ // Read 4 bits
+ FrequencyIndex = bs.Read(4);
+
+ // Read 4 bits
+ ChannelConfiguration = bs.Read(4);
+ }
+
+ public List Process_AAC_RTP_Packet(byte[] rtp_payload, int rtp_marker) {
+
+ // RTP Payload for MPEG4-GENERIC can consist of multple blocks.
+ // Each block has 3 parts
+ // Part 1 - Acesss Unit Header Length + Header
+ // Part 2 - Access Unit Auxiliary Data Length + Data (not used in AAC High Bitrate)
+ // Part 3 - Access Unit Audio Data
+
+ // The rest of the RTP packet is the AMR data
+ List audio_data = new List();
+
+ int ptr = 0;
+
+ while (true) {
+ if (ptr + 4 > rtp_payload.Length) break; // 2 bytes for AU Header Length, 2 bytes of AU Header payload
+
+ // Get Size of the AU Header
+ int au_headers_length_bits = (((rtp_payload[ptr] << 8) + (rtp_payload[ptr + 1] << 0))); // 16 bits
+ int au_headers_length = (int)Math.Ceiling((double)au_headers_length_bits / 8.0);
+ ptr += 2;
+
+ // Examine the AU Header. Get the size of the AAC data
+ int aac_frame_size = (((rtp_payload[ptr] << 8) + (rtp_payload[ptr+1] << 0)) >> 3); // 13 bits
+ int aac_index_delta = rtp_payload[ptr+1] & 0x03; // 3 bits
+ ptr += au_headers_length;
+
+ // extract the AAC block
+ if (ptr + aac_frame_size > rtp_payload.Length) break; // not enough data to copy
+ byte[] aac_data = new byte[aac_frame_size];
+ System.Array.Copy(rtp_payload, ptr, aac_data, 0, aac_frame_size);
+ audio_data.Add(aac_data);
+ ptr += aac_frame_size;
+ }
+
+ return audio_data;
+ }
+
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/AMRPayload.cs b/framework/Inspectron.HawkEye/RTSP/AMRPayload.cs
new file mode 100644
index 0000000..9637d6c
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/AMRPayload.cs
@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+
+namespace Inspectron.HawkEye.RTSP
+{
+ // This class handles the AMR Payload
+ // It has methods to process the RTP Payload
+
+ public class AMRPayload
+ {
+ // Constructor
+ public AMRPayload()
+ {
+ }
+
+ public List Process_AMR_RTP_Packet(byte[] rtp_payload, int rtp_marker) {
+
+ // Octet-Aligned Mode (RFC 4867 Section 4.4.1)
+
+ // First byte is the Payload Header
+ if (rtp_payload.Length < 1) return null;
+ byte payloadHeader = rtp_payload[0];
+
+ // The rest of the RTP packet is the AMR data
+ List audio_data = new List();
+
+ byte[] amr_data = new byte[rtp_payload.Length - 1];
+ System.Array.Copy(rtp_payload,1,amr_data,0,rtp_payload.Length-1);
+ audio_data.Add(amr_data);
+
+ return audio_data;
+ }
+
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Authentication.cs b/framework/Inspectron.HawkEye/RTSP/Authentication.cs
new file mode 100644
index 0000000..6981873
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Authentication.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Security.Cryptography;
+using System.Text;
+using Inspectron.HawkEye.RTSP.Messages;
+
+namespace Inspectron.HawkEye.RTSP
+{
+
+ // WWW-Authentication and Authorization Headers
+ public class Authentication
+ {
+ private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();
+
+ public enum Type {Basic, Digest};
+
+ private String username = null;
+ private String password = null;
+ private String realm = null;
+ private String nonce = null;
+ private Type authentication_type = Type.Digest;
+ private readonly MD5 md5 = System.Security.Cryptography.MD5.Create();
+
+
+ private const char quote = '\"';
+
+ // Constructor
+ public Authentication(String username, String password, String realm, Type authentication_type) {
+ this.username = username;
+ this.password = password;
+ this.realm = realm;
+ this.authentication_type = authentication_type;
+
+ this.nonce = new Random().Next(100000000,999999999).ToString(); // random 9 digit number
+ }
+
+ public String GetHeader() {
+ if (authentication_type == Type.Basic) {
+ return "Basic realm=" + quote + realm + quote;
+ }
+ if (authentication_type == Type.Digest) {
+ return "Digest realm=" + quote + realm + quote + ", nonce=" + quote + nonce + quote;
+ }
+ return null;
+ }
+
+
+ public bool IsValid(RtspMessage received_message) {
+
+ string authorization = received_message.Headers["Authorization"];
+
+
+ // Check Username and Password
+ if (authentication_type == Type.Basic && authorization.StartsWith("Basic ")) {
+ string base64_str = authorization.Substring(6); // remove 'Basic '
+ byte[] data = Convert.FromBase64String(base64_str);
+ string decoded = Encoding.UTF8.GetString(data);
+ int split_position = decoded.IndexOf(':');
+ string decoded_username = decoded.Substring(0, split_position);
+ string decoded_password = decoded.Substring(split_position + 1);
+
+ if ((decoded_username == username) && (decoded_password == password)) {
+ _logger.Debug("Basic Authorization passed");
+ return true;
+ } else {
+ _logger.Debug("Basic Authorization failed");
+ return false;
+ }
+ }
+
+ // Check Username, URI, Nonce and the MD5 hashed Response
+ if (authentication_type == Type.Digest && authorization.StartsWith("Digest ")) {
+ string value_str = authorization.Substring(7); // remove 'Digest '
+ string[] values = value_str.Split(',');
+ string auth_header_username = null;
+ string auth_header_realm = null;
+ string auth_header_nonce = null;
+ string auth_header_uri = null;
+ string auth_header_response = null;
+ string message_method = null;
+ string message_uri = null;
+ try {
+ message_method = received_message.Command.Split(' ')[0];
+ message_uri = received_message.Command.Split(' ')[1];
+ } catch {}
+
+ foreach (string value in values) {
+ string[] tuple = value.Trim().Split(new char[] {'='},2); // split on first '='
+ if (tuple.Length == 2 && tuple[0].Equals("username")) {
+ auth_header_username = tuple[1].Trim(new char[] {' ','\"'}); // trim space and quotes
+ }
+ else if (tuple.Length == 2 && tuple[0].Equals("realm")) {
+ auth_header_realm = tuple[1].Trim(new char[] {' ','\"'}); // trim space and quotes
+ }
+ else if (tuple.Length == 2 && tuple[0].Equals("nonce")) {
+ auth_header_nonce = tuple[1].Trim(new char[] {' ','\"'}); // trim space and quotes
+ }
+ else if (tuple.Length == 2 && tuple[0].Equals("uri")) {
+ auth_header_uri = tuple[1].Trim(new char[] {' ','\"'}); // trim space and quotes
+ }
+ else if (tuple.Length == 2 && tuple[0].Equals("response")) {
+ auth_header_response = tuple[1].Trim(new char[] {' ','\"'}); // trim space and quotes
+ }
+ }
+
+ // Create the MD5 Hash using all parameters passed in the Auth Header with the
+ // addition of the 'Password'
+ String hashA1 = CalculateMD5Hash(md5, auth_header_username+":"+auth_header_realm+":"+this.password);
+ String hashA2 = CalculateMD5Hash(md5, message_method + ":" + auth_header_uri);
+ String expected_response = CalculateMD5Hash(md5, hashA1 + ":" + auth_header_nonce + ":" + hashA2);
+
+ // Check if everything matches
+ // ToDo - extract paths from the URIs (ignoring SETUP's trackID)
+ if ((auth_header_username == this.username)
+ && (auth_header_realm == this.realm)
+ && (auth_header_nonce == this.nonce)
+ && (auth_header_response == expected_response)
+ ){
+ _logger.Debug("Digest Authorization passed");
+ return true;
+ } else {
+ _logger.Debug("Digest Authorization failed");
+ return false;
+ }
+ }
+ return false;
+ }
+
+
+
+ // Generate Basic or Digest Authorization
+ public string GenerateAuthorization(string username, string password,
+ string auth_type, string realm, string nonce, string url, string command) {
+
+ if (username == null || username.Length == 0) return null;
+ if (password == null || password.Length == 0) return null;
+ if (realm == null || realm.Length == 0) return null;
+ if (auth_type.Equals("Digest") && (nonce == null || nonce.Length == 0)) return null;
+
+ if (auth_type.Equals("Basic")) {
+ byte[] credentials = System.Text.Encoding.UTF8.GetBytes(username+":"+password);
+ String credentials_base64 = Convert.ToBase64String(credentials);
+ String basic_authorization = "Basic " + credentials_base64;
+ return basic_authorization;
+ }
+ else if (auth_type.Equals("Digest")) {
+
+ MD5 md5 = System.Security.Cryptography.MD5.Create();
+ String hashA1 = CalculateMD5Hash(md5, username+":"+realm+":"+password);
+ String hashA2 = CalculateMD5Hash(md5, command + ":" + url);
+ String response = CalculateMD5Hash(md5, hashA1 + ":" + nonce + ":" + hashA2);
+
+ const String quote = "\"";
+ String digest_authorization = "Digest username=" + quote + username + quote +", "
+ + "realm=" + quote + realm + quote + ", "
+ + "nonce=" + quote + nonce + quote + ", "
+ + "uri=" + quote + url + quote + ", "
+ + "response=" + quote + response + quote;
+
+ return digest_authorization;
+ }
+ else {
+ return null;
+ }
+
+ }
+
+
+
+ // MD5 (lower case)
+ private string CalculateMD5Hash(MD5 md5_session, string input)
+ {
+ byte[] inputBytes = System.Text.Encoding.UTF8.GetBytes(input);
+ byte[] hash = md5_session.ComputeHash(inputBytes);
+
+ StringBuilder output = new StringBuilder();
+ for (int i = 0; i < hash.Length; i++) {
+ output.Append(hash[i].ToString("x2"));
+ }
+
+ return output.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/RTSP/BitStream.cs b/framework/Inspectron.HawkEye/RTSP/BitStream.cs
new file mode 100644
index 0000000..bf71763
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/BitStream.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+
+// (c) 2018 Roger Hardiman, RJH Technical Consultancy Ltd
+// Simple class to Read and Write bits in a bit stream.
+// Data is written to the end of the bit stream and the bit stream can be returned as a Byte Array
+// Data can be read from the head of the bit stream
+// Example
+// bitstream.AddValue(0xA,4); // Write 4 bit value
+// bitstream.AddValue(0xB,4);
+// bitstream.AddValue(0xC,4);
+// bitstream.AddValue(0xD,4);
+// bitstream.ToArray() -> {0xAB, 0xCD} // Return Byte Array
+// bitstream.Read(8) -> 0xAB // Read 8 bit value
+
+namespace Inspectron.HawkEye.RTSP
+{
+
+ // Very simple bitstream
+ public class BitStream {
+
+ private List data = new List(); // List only stores 0 or 1 (one 'bit' per List item)
+
+ // Constructor
+ public BitStream() {
+ }
+
+ public void AddValue(int value, int num_bits) {
+ // Add each bit to the List
+ for (int i = num_bits-1; i >= 0; i--) {
+ data.Add((byte)((value>>i) & 0x01));
+ }
+ }
+
+ public void AddHexString(String hex_string) {
+ char[] hex_chars = hex_string.ToUpper().ToCharArray();
+ foreach (char c in hex_chars) {
+ if ((c.Equals('0'))) this.AddValue(0,4);
+ else if ((c.Equals('1'))) this.AddValue(1, 4);
+ else if ((c.Equals('2'))) this.AddValue(2, 4);
+ else if ((c.Equals('3'))) this.AddValue(3, 4);
+ else if ((c.Equals('4'))) this.AddValue(4, 4);
+ else if ((c.Equals('5'))) this.AddValue(5, 4);
+ else if ((c.Equals('6'))) this.AddValue(6, 4);
+ else if ((c.Equals('7'))) this.AddValue(7, 4);
+ else if ((c.Equals('8'))) this.AddValue(8, 4);
+ else if ((c.Equals('9'))) this.AddValue(9, 4);
+ else if ((c.Equals('A'))) this.AddValue(10, 4);
+ else if ((c.Equals('B'))) this.AddValue(11, 4);
+ else if ((c.Equals('C'))) this.AddValue(12, 4);
+ else if ((c.Equals('D'))) this.AddValue(13, 4);
+ else if ((c.Equals('E'))) this.AddValue(14, 4);
+ else if ((c.Equals('F'))) this.AddValue(15, 4);
+ }
+ }
+
+ public uint Read(int num_bits) {
+ // Read and remove items from the front of the list of bits
+ if (data.Count < num_bits) return 0;
+ uint result = 0;
+ for (int i = 0; i < num_bits; i++) {
+ result = result << 1;
+ result = result + data[0];
+ data.RemoveAt(0);
+ }
+ return result;
+ }
+
+ public byte[] ToArray() {
+ int num_bytes = (int)Math.Ceiling((double)data.Count/8.0);
+ byte[] array = new byte[num_bytes];
+ int ptr = 0;
+ int shift = 7;
+ for (int i = 0; i < data.Count; i++) {
+ array[ptr] += (byte)(data[i] << shift);
+ if (shift == 0) {
+ shift = 7;
+ ptr++;
+ }
+ else {
+ shift--;
+ }
+ }
+
+ return array;
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/RTSP/Client/RTSPClient.cs b/framework/Inspectron.HawkEye/RTSP/Client/RTSPClient.cs
new file mode 100644
index 0000000..958202e
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Client/RTSPClient.cs
@@ -0,0 +1,1128 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using Inspectron.HawkEye.RTSP.Messages;
+
+namespace Inspectron.HawkEye.RTSP.Client
+{
+ public class RTSPClient
+ {
+ private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();
+
+ // Events that applications can receive
+ public event Received_SPS_PPS_Delegate Received_SPS_PPS;
+ public event Received_VPS_SPS_PPS_Delegate Received_VPS_SPS_PPS;
+ public event Received_NALs_Delegate Received_NALs;
+ public event Received_G711_Delegate Received_G711;
+ public event Received_AMR_Delegate Received_AMR;
+ public event Received_AAC_Delegate Received_AAC;
+
+ // Delegated functions (essentially the function prototype)
+ public delegate void Received_SPS_PPS_Delegate (byte[] sps, byte[] pps); // H264
+ public delegate void Received_VPS_SPS_PPS_Delegate(byte[] vps, byte[] sps, byte[] pps); // H265
+ public delegate void Received_NALs_Delegate (List nal_units); // H264 or H265
+ public delegate void Received_G711_Delegate (String format, List g711);
+ public delegate void Received_AMR_Delegate (String format, List amr);
+ public delegate void Received_AAC_Delegate(String format, List aac, uint ObjectType, uint FrequencyIndex, uint ChannelConfiguration);
+
+ public enum RTP_TRANSPORT { UDP, TCP, MULTICAST, UNKNOWN };
+ public enum MEDIA_REQUEST { VIDEO_ONLY, AUDIO_ONLY, VIDEO_AND_AUDIO };
+ private enum RTSP_STATUS { WaitingToConnect, Connecting, ConnectFailed, Connected };
+
+ Inspectron.HawkEye.RTSP.RtspTcpTransport rtsp_socket = null; // RTSP connection
+ volatile RTSP_STATUS rtsp_socket_status = RTSP_STATUS.WaitingToConnect;
+ Inspectron.HawkEye.RTSP.RtspListener rtsp_client = null; // this wraps around a the RTSP tcp_socket stream
+ RTP_TRANSPORT rtp_transport = RTP_TRANSPORT.UNKNOWN; // Mode, either RTP over UDP or RTP over TCP using the RTSP socket
+ Inspectron.HawkEye.RTSP.UDPSocket video_udp_pair = null; // Pair of UDP ports used in RTP over UDP mode or in MULTICAST mode
+ Inspectron.HawkEye.RTSP.UDPSocket audio_udp_pair = null; // Pair of UDP ports used in RTP over UDP mode or in MULTICAST mode
+ String url = ""; // RTSP URL (username & password will be stripped out
+ String username = ""; // Username
+ String password = ""; // Password
+ String hostname = ""; // RTSP Server hostname or IP address
+ int port = 0; // RTSP Server TCP Port number
+ String session = ""; // RTSP Session
+ String auth_type = null; // cached from most recent WWW-Authenticate reply
+ String realm = null; // cached from most recent WWW-Authenticate reply
+ String nonce = null; // cached from most recent WWW-Authenticate reply
+ uint ssrc = 12345;
+ bool client_wants_video = false; // Client wants to receive Video
+ bool client_wants_audio = false; // Client wants to receive Audio
+ Uri video_uri = null; // URI used for the Video Track
+ int video_payload = -1; // Payload Type for the Video. (often 96 which is the first dynamic payload value. Bosch use 35)
+ int video_data_channel = -1; // RTP Channel Number used for the video RTP stream or the UDP port number
+ int video_rtcp_channel = -1; // RTP Channel Number used for the video RTCP status report messages OR the UDP port number
+ bool h264_sps_pps_fired = false; // True if the SDP included a sprop-Parameter-Set for H264 video
+ bool h265_vps_sps_pps_fired = false; // True if the SDP included a sprop-vps, sprop-sps and sprop_pps for H265 video
+ string video_codec = ""; // Codec used with Payload Types 96..127 (eg "H264")
+
+ Uri audio_uri = null; // URI used for the Audio Track
+ int audio_payload = -1; // Payload Type for the Video. (often 96 which is the first dynamic payload value)
+ int audio_data_channel = -1; // RTP Channel Number used for the audio RTP stream or the UDP port number
+ int audio_rtcp_channel = -1; // RTP Channel Number used for the audio RTCP status report messages OR the UDP port number
+ string audio_codec = ""; // Codec used with Payload Types (eg "PCMA" or "AMR")
+
+ bool server_supports_get_parameter = false; // Used with RTSP keepalive
+ bool server_supports_set_parameter = false; // Used with RTSP keepalive
+ System.Timers.Timer keepalive_timer = null; // Used with RTSP keepalive
+
+ Inspectron.HawkEye.RTSP.H264Payload h264Payload = null;
+ Inspectron.HawkEye.RTSP.H265Payload h265Payload = null;
+ Inspectron.HawkEye.RTSP.G711Payload g711Payload = new Inspectron.HawkEye.RTSP.G711Payload();
+ Inspectron.HawkEye.RTSP.AMRPayload amrPayload = new Inspectron.HawkEye.RTSP.AMRPayload();
+ Inspectron.HawkEye.RTSP.AACPayload aacPayload = null;
+
+ List setup_messages = new List(); // setup messages still to send
+
+ // Constructor
+ public RTSPClient() {
+ bool writeLogsToConsole = true;
+ if (writeLogsToConsole)
+ {
+ var config = new NLog.Config.LoggingConfiguration();
+
+ // Targets where to log to: Console
+ var logconsole = new NLog.Targets.ConsoleTarget("logconsole");
+
+ // Rules for mapping loggers to targets
+ config.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Fatal, logconsole);
+
+ // Apply config
+ NLog.LogManager.Configuration = config;
+ }
+ }
+
+
+ public void Connect(String url, RTP_TRANSPORT rtp_transport, MEDIA_REQUEST media_request = MEDIA_REQUEST.VIDEO_AND_AUDIO)
+ {
+
+ Inspectron.HawkEye.RTSP.RtspUtils.RegisterUri();
+
+ _logger.Debug("Connecting to " + url);
+ this.url = url;
+
+ // Use URI to extract username and password
+ // and to make a new URL without the username and password
+ try {
+ Uri uri = new Uri(this.url);
+ hostname = uri.Host;
+ port = uri.Port;
+
+ if (uri.UserInfo.Length > 0) {
+ username = uri.UserInfo.Split(new char[] {':'})[0];
+ password = uri.UserInfo.Split(new char[] {':'})[1];
+ this.url = uri.GetComponents((UriComponents.AbsoluteUri &~ UriComponents.UserInfo),
+ UriFormat.UriEscaped);
+ }
+ } catch {
+ username = null;
+ password = null;
+ }
+
+ // We can ask the RTSP server for Video, Audio or both. If we don't want audio we don't need to SETUP the audio channal or receive it
+ client_wants_video = false;
+ client_wants_audio = false;
+ if (media_request == MEDIA_REQUEST.VIDEO_ONLY || media_request == MEDIA_REQUEST.VIDEO_AND_AUDIO) client_wants_video = true;
+ if (media_request == MEDIA_REQUEST.AUDIO_ONLY || media_request == MEDIA_REQUEST.VIDEO_AND_AUDIO) client_wants_audio = true;
+
+ // Connect to a RTSP Server. The RTSP session is a TCP connection
+ rtsp_socket_status = RTSP_STATUS.Connecting;
+ try
+ {
+ rtsp_socket = new Inspectron.HawkEye.RTSP.RtspTcpTransport(hostname, port);
+ }
+ catch
+ {
+ rtsp_socket_status = RTSP_STATUS.ConnectFailed;
+ _logger.Warn("Error - did not connect");
+ return;
+ }
+
+ if (rtsp_socket.Connected == false)
+ {
+ rtsp_socket_status = RTSP_STATUS.ConnectFailed;
+ _logger.Warn("Error - did not connect");
+ return;
+ }
+
+ rtsp_socket_status = RTSP_STATUS.Connected;
+
+ // Connect a RTSP Listener to the RTSP Socket (or other Stream) to send RTSP messages and listen for RTSP replies
+ rtsp_client = new Inspectron.HawkEye.RTSP.RtspListener(rtsp_socket);
+
+ rtsp_client.AutoReconnect = false;
+
+ rtsp_client.MessageReceived += Rtsp_MessageReceived;
+ rtsp_client.DataReceived += Rtp_DataReceived;
+
+ rtsp_client.Start(); // start listening for messages from the server (messages fire the MessageReceived event)
+
+
+ // Check the RTP Transport
+ // If the RTP transport is TCP then we interleave the RTP packets in the RTSP stream
+ // If the RTP transport is UDP, we initialise two UDP sockets (one for video, one for RTCP status messages)
+ // If the RTP transport is MULTICAST, we have to wait for the SETUP message to get the Multicast Address from the RTSP server
+ this.rtp_transport = rtp_transport;
+ if (rtp_transport == RTP_TRANSPORT.UDP)
+ {
+ video_udp_pair = new Inspectron.HawkEye.RTSP.UDPSocket(50000, 51000); // give a range of 500 pairs (1000 addresses) to try incase some address are in use
+ video_udp_pair.DataReceived += Rtp_DataReceived;
+ video_udp_pair.Start(); // start listening for data on the UDP ports
+ audio_udp_pair = new Inspectron.HawkEye.RTSP.UDPSocket(50000, 51000); // give a range of 500 pairs (1000 addresses) to try incase some address are in use
+ audio_udp_pair.DataReceived += Rtp_DataReceived;
+ audio_udp_pair.Start(); // start listening for data on the UDP ports
+ }
+ if (rtp_transport == RTP_TRANSPORT.TCP)
+ {
+ // Nothing to do. Data will arrive in the RTSP Listener
+ }
+ if (rtp_transport == RTP_TRANSPORT.MULTICAST)
+ {
+ // Nothing to do. Will open Multicast UDP sockets after the SETUP command
+ }
+
+
+ // Send OPTIONS
+ // In the Received Message handler we will send DESCRIBE, SETUP and PLAY
+ Inspectron.HawkEye.RTSP.Messages.RtspRequest options_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestOptions();
+ options_message.RtspUri = new Uri(this.url);
+ rtsp_client.SendMessage(options_message);
+ }
+
+ // return true if this connection failed, or if it connected but is no longer connected.
+ public bool StreamingFinished() {
+ if (rtsp_socket_status == RTSP_STATUS.ConnectFailed) return true;
+ if (rtsp_socket_status == RTSP_STATUS.Connected && rtsp_socket.Connected == false) return true;
+ else return false;
+ }
+
+
+ public void Pause()
+ {
+ if (rtsp_client != null) {
+ // Send PAUSE
+ Inspectron.HawkEye.RTSP.Messages.RtspRequest pause_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestPause();
+ pause_message.RtspUri = new Uri(url);
+ pause_message.Session = session;
+ if (auth_type != null) {
+ AddAuthorization(pause_message,username,password,auth_type,realm,nonce,url);
+ }
+ rtsp_client.SendMessage(pause_message);
+ }
+ }
+
+ public void Play()
+ {
+ if (rtsp_client != null) {
+ // Send PLAY
+ Inspectron.HawkEye.RTSP.Messages.RtspRequest play_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestPlay();
+ play_message.RtspUri = new Uri(url);
+ play_message.Session = session;
+ if (auth_type != null) {
+ AddAuthorization(play_message,username,password,auth_type,realm,nonce,url);
+ }
+ rtsp_client.SendMessage(play_message);
+ }
+ }
+
+
+ public void Stop()
+ {
+ if (rtsp_client != null) {
+ // Send TEARDOWN
+ Inspectron.HawkEye.RTSP.Messages.RtspRequest teardown_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestTeardown();
+ teardown_message.RtspUri = new Uri(url);
+ teardown_message.Session = session;
+ if (auth_type != null) {
+ AddAuthorization(teardown_message,username,password,auth_type,realm,nonce,url);
+ }
+ rtsp_client.SendMessage(teardown_message);
+ }
+
+ // Stop the keepalive timer
+ if (keepalive_timer != null) keepalive_timer.Stop();
+
+ // clear up any UDP sockets
+ if (video_udp_pair != null) video_udp_pair.Stop();
+ if (audio_udp_pair != null) audio_udp_pair.Stop();
+
+ // Drop the RTSP session
+ if (rtsp_client != null) {
+ rtsp_client.Stop();
+ }
+
+ }
+
+
+ int rtp_count = 0; // used for statistics
+ // RTP packet (or RTCP packet) has been received.
+ public void Rtp_DataReceived(object sender, Inspectron.HawkEye.RTSP.RtspChunkEventArgs e)
+ {
+
+ Inspectron.HawkEye.RTSP.Messages.RtspData data_received = e.Message as Inspectron.HawkEye.RTSP.Messages.RtspData;
+
+ // Check which channel the Data was received on.
+ // eg the Video Channel, the Video Control Channel (RTCP)
+ // the Audio Channel or the Audio Control Channel (RTCP)
+
+ if (data_received.Channel == video_rtcp_channel || data_received.Channel == audio_rtcp_channel)
+ {
+ _logger.Debug("Received a RTCP message on channel " + data_received.Channel);
+
+ // RTCP Packet
+ // - Version, Padding and Receiver Report Count
+ // - Packet Type
+ // - Length
+ // - SSRC
+ // - payload
+
+ // There can be multiple RTCP packets transmitted together. Loop ever each one
+
+ long packetIndex = 0;
+ while (packetIndex < e.Message.Data.Length) {
+
+ int rtcp_version = (e.Message.Data[packetIndex+0] >> 6);
+ int rtcp_padding = (e.Message.Data[packetIndex+0] >> 5) & 0x01;
+ int rtcp_reception_report_count = (e.Message.Data[packetIndex+0] & 0x1F);
+ byte rtcp_packet_type = e.Message.Data[packetIndex+1]; // Values from 200 to 207
+ uint rtcp_length = (uint)(e.Message.Data[packetIndex+2] << 8) + (uint)(e.Message.Data[packetIndex+3]); // number of 32 bit words
+ uint rtcp_ssrc = (uint)(e.Message.Data[packetIndex+4] << 24) + (uint)(e.Message.Data[packetIndex+5] << 16)
+ + (uint)(e.Message.Data[packetIndex+6] << 8) + (uint)(e.Message.Data[packetIndex+7]);
+
+ // 200 = SR = Sender Report
+ // 201 = RR = Receiver Report
+ // 202 = SDES = Source Description
+ // 203 = Bye = Goodbye
+ // 204 = APP = Application Specific Method
+ // 207 = XR = Extended Reports
+
+ _logger.Debug("RTCP Data. PacketType=" + rtcp_packet_type
+ + " SSRC=" + rtcp_ssrc);
+
+ if (rtcp_packet_type == 200) {
+ // We have received a Sender Report
+ // Use it to convert the RTP timestamp into the UTC time
+
+ UInt32 ntp_msw_seconds = (uint)(e.Message.Data[packetIndex + 8] << 24) + (uint)(e.Message.Data[packetIndex + 9] << 16)
+ + (uint)(e.Message.Data[packetIndex + 10] << 8) + (uint)(e.Message.Data[packetIndex + 11]);
+
+ UInt32 ntp_lsw_fractions = (uint)(e.Message.Data[packetIndex + 12] << 24) + (uint)(e.Message.Data[packetIndex + 13] << 16)
+ + (uint)(e.Message.Data[packetIndex + 14] << 8) + (uint)(e.Message.Data[packetIndex + 15]);
+
+ UInt32 rtp_timestamp = (uint)(e.Message.Data[packetIndex + 16] << 24) + (uint)(e.Message.Data[packetIndex + 17] << 16)
+ + (uint)(e.Message.Data[packetIndex + 18] << 8) + (uint)(e.Message.Data[packetIndex + 19]);
+
+ double ntp = ntp_msw_seconds + (ntp_lsw_fractions / UInt32.MaxValue);
+
+ // NTP Most Signigicant Word is relative to 0h, 1 Jan 1900
+ // This will wrap around in 2036
+ DateTime time = new DateTime(1900,1,1,0,0,0,DateTimeKind.Utc);
+
+ time = time.AddSeconds((double)ntp_msw_seconds); // adds 'double' (whole&fraction)
+
+ _logger.Debug("RTCP time (UTC) for RTP timestamp " + rtp_timestamp + " is " + time);
+
+ // Send a Receiver Report
+ try
+ {
+ byte[] rtcp_receiver_report = new byte[8];
+ int version = 2;
+ int paddingBit = 0;
+ int reportCount = 0; // an empty report
+ int packetType = 201; // Receiver Report
+ int length = (rtcp_receiver_report.Length/4) - 1; // num 32 bit words minus 1
+ rtcp_receiver_report[0] = (byte)((version << 6) + (paddingBit << 5) + reportCount);
+ rtcp_receiver_report[1] = (byte)(packetType);
+ rtcp_receiver_report[2] = (byte)((length >> 8) & 0xFF);
+ rtcp_receiver_report[3] = (byte)((length >> 0) & 0XFF);
+ rtcp_receiver_report[4] = (byte)((ssrc >> 24) & 0xFF);
+ rtcp_receiver_report[5] = (byte)((ssrc >> 16) & 0xFF);
+ rtcp_receiver_report[6] = (byte)((ssrc >> 8) & 0xFF);
+ rtcp_receiver_report[7] = (byte)((ssrc >> 0) & 0xFF);
+
+ if (rtp_transport == RTP_TRANSPORT.TCP) {
+ // Send it over via the RTSP connection
+ rtsp_client.SendData(video_rtcp_channel,rtcp_receiver_report);
+ }
+ if (rtp_transport == RTP_TRANSPORT.UDP || rtp_transport == RTP_TRANSPORT.MULTICAST) {
+ // Send it via a UDP Packet
+ _logger.Debug("TODO - Need to implement RTCP over UDP");
+ }
+
+ }
+ catch
+ {
+ _logger.Debug("Error writing RTCP packet");
+ }
+ }
+
+ packetIndex = packetIndex + ((rtcp_length + 1) * 4);
+ }
+ return;
+ }
+
+ if (data_received.Channel == video_data_channel || data_received.Channel == audio_data_channel)
+ {
+ // Received some Video or Audio Data on the correct channel.
+
+ // RTP Packet Header
+ // 0 - Version, P, X, CC, M, PT and Sequence Number
+ //32 - Timestamp
+ //64 - SSRC
+ //96 - CSRCs (optional)
+ //nn - Extension ID and Length
+ //nn - Extension header
+
+ int rtp_version = (e.Message.Data[0] >> 6);
+ int rtp_padding = (e.Message.Data[0] >> 5) & 0x01;
+ int rtp_extension = (e.Message.Data[0] >> 4) & 0x01;
+ int rtp_csrc_count = (e.Message.Data[0] >> 0) & 0x0F;
+ int rtp_marker = (e.Message.Data[1] >> 7) & 0x01;
+ int rtp_payload_type = (e.Message.Data[1] >> 0) & 0x7F;
+ uint rtp_sequence_number = ((uint)e.Message.Data[2] << 8) + (uint)(e.Message.Data[3]);
+ uint rtp_timestamp = ((uint)e.Message.Data[4] << 24) + (uint)(e.Message.Data[5] << 16) + (uint)(e.Message.Data[6] << 8) + (uint)(e.Message.Data[7]);
+ uint rtp_ssrc = ((uint)e.Message.Data[8] << 24) + (uint)(e.Message.Data[9] << 16) + (uint)(e.Message.Data[10] << 8) + (uint)(e.Message.Data[11]);
+
+ int rtp_payload_start = 4 // V,P,M,SEQ
+ + 4 // time stamp
+ + 4 // ssrc
+ + (4 * rtp_csrc_count); // zero or more csrcs
+
+ uint rtp_extension_id = 0;
+ uint rtp_extension_size = 0;
+ if (rtp_extension == 1)
+ {
+ rtp_extension_id = ((uint)e.Message.Data[rtp_payload_start + 0] << 8) + (uint)(e.Message.Data[rtp_payload_start + 1] << 0);
+ rtp_extension_size = ((uint)e.Message.Data[rtp_payload_start + 2] << 8) + (uint)(e.Message.Data[rtp_payload_start + 3] << 0) * 4; // units of extension_size is 4-bytes
+ rtp_payload_start += 4 + (int)rtp_extension_size; // extension header and extension payload
+ }
+
+ _logger.Debug("RTP Data"
+ + " V=" + rtp_version
+ + " P=" + rtp_padding
+ + " X=" + rtp_extension
+ + " CC=" + rtp_csrc_count
+ + " M=" + rtp_marker
+ + " PT=" + rtp_payload_type
+ + " Seq=" + rtp_sequence_number
+ + " Time (MS)=" + rtp_timestamp / 90 // convert from 90kHZ clock to ms
+ + " SSRC=" + rtp_ssrc
+ + " Size=" + e.Message.Data.Length);
+
+
+ // Check the payload type in the RTP packet matches the Payload Type value from the SDP
+ if (data_received.Channel == video_data_channel && rtp_payload_type != video_payload)
+ {
+ _logger.Debug("Ignoring this Video RTP payload");
+ return; // ignore this data
+ }
+
+ // Check the payload type in the RTP packet matches the Payload Type value from the SDP
+ else if (data_received.Channel == audio_data_channel && rtp_payload_type != audio_payload)
+ {
+ _logger.Debug("Ignoring this Audio RTP payload");
+ return; // ignore this data
+ }
+ else if (data_received.Channel == video_data_channel
+ && rtp_payload_type == video_payload
+ && video_codec.Equals("H264")) {
+ // H264 RTP Packet
+
+ // If rtp_marker is '1' then this is the final transmission for this packet.
+ // If rtp_marker is '0' we need to accumulate data with the same timestamp
+
+ // ToDo - Check Timestamp
+ // Add the RTP packet to the tempoary_rtp list until we have a complete 'Frame'
+
+ byte[] rtp_payload = new byte[e.Message.Data.Length - rtp_payload_start]; // payload with RTP header removed
+ System.Array.Copy(e.Message.Data, rtp_payload_start, rtp_payload, 0, rtp_payload.Length); // copy payload
+
+ List nal_units = h264Payload.Process_H264_RTP_Packet(rtp_payload, rtp_marker); // this will cache the Packets until there is a Frame
+
+ if (nal_units == null) {
+ // we have not passed in enough RTP packets to make a Frame of video
+ } else {
+ // If we did not have a SPS and PPS in the SDP then search for the SPS and PPS
+ // in the NALs and fire the Received_SPS_PPS event.
+ // We assume the SPS and PPS are in the same Frame.
+ if (h264_sps_pps_fired == false) {
+
+ // Check this frame for SPS and PPS
+ byte[] sps = null;
+ byte[] pps = null;
+ foreach (byte[] nal_unit in nal_units) {
+ if (nal_unit.Length > 0)
+ {
+ int nal_ref_idc = (nal_unit[0] >> 5) & 0x03;
+ int nal_unit_type = nal_unit[0] & 0x1F;
+
+ if (nal_unit_type == 7) sps = nal_unit; // SPS
+ if (nal_unit_type == 8) pps = nal_unit; // PPS
+ }
+ }
+ if (sps != null && pps != null) {
+ // Fire the Event
+ if (Received_SPS_PPS != null)
+ {
+ Received_SPS_PPS(sps, pps);
+ }
+ h264_sps_pps_fired = true;
+ }
+ }
+
+
+
+ // we have a frame of NAL Units. Write them to the file
+ if (Received_NALs != null) {
+ Received_NALs(nal_units);
+ }
+ }
+ }
+ else if (data_received.Channel == video_data_channel
+ && rtp_payload_type == video_payload
+ && video_codec.Equals("H265"))
+ {
+ // H265 RTP Packet
+
+ // If rtp_marker is '1' then this is the final transmission for this packet.
+ // If rtp_marker is '0' we need to accumulate data with the same timestamp
+
+ // Add the RTP packet to the tempoary_rtp list until we have a complete 'Frame'
+
+ byte[] rtp_payload = new byte[e.Message.Data.Length - rtp_payload_start]; // payload with RTP header removed
+ System.Array.Copy(e.Message.Data, rtp_payload_start, rtp_payload, 0, rtp_payload.Length); // copy payload
+
+ List nal_units = h265Payload.Process_H265_RTP_Packet(rtp_payload, rtp_marker); // this will cache the Packets until there is a Frame
+
+ if (nal_units == null)
+ {
+ // we have not passed in enough RTP packets to make a Frame of video
+ }
+ else
+ {
+ // If we did not have a VPS, SPS and PPS in the SDP then search for the VPS SPS and PPS
+ // in the NALs and fire the Received_VPS_SPS_PPS event.
+ // We assume the VPS, SPS and PPS are in the same Frame.
+ if (h265_vps_sps_pps_fired == false)
+ {
+
+ // Check this frame for VPS, SPS and PPS
+ byte[] vps = null;
+ byte[] sps = null;
+ byte[] pps = null;
+ foreach (byte[] nal_unit in nal_units)
+ {
+ if (nal_unit.Length > 0)
+ {
+ int nal_unit_type = (nal_unit[0] >> 1) & 0x3F;
+
+ if (nal_unit_type == 32) vps = nal_unit; // VPS
+ if (nal_unit_type == 33) sps = nal_unit; // SPS
+ if (nal_unit_type == 34) pps = nal_unit; // PPS
+ }
+ }
+ if (vps != null && sps != null && pps != null)
+ {
+ // Fire the Event
+ if (Received_VPS_SPS_PPS != null)
+ {
+ Received_VPS_SPS_PPS(vps, sps, pps);
+ }
+ h265_vps_sps_pps_fired = true;
+ }
+ }
+
+ // we have a frame of NAL Units. Write them to the file
+ if (Received_NALs != null)
+ {
+ Received_NALs(nal_units);
+ }
+ }
+ }
+ else if (data_received.Channel == audio_data_channel && (rtp_payload_type == 0 || rtp_payload_type == 8 || audio_codec.Equals("PCMA") || audio_codec.Equals("PCMU"))) {
+ // G711 PCMA or G711 PCMU
+ byte[] rtp_payload = new byte[e.Message.Data.Length - rtp_payload_start]; // payload with RTP header removed
+ System.Array.Copy(e.Message.Data, rtp_payload_start, rtp_payload, 0, rtp_payload.Length); // copy payload
+
+ List audio_frames = g711Payload.Process_G711_RTP_Packet(rtp_payload, rtp_marker);
+
+ if (audio_frames == null) {
+ // some error
+ } else {
+ // Write the audio frames to the file
+ if (Received_G711 != null) {
+ Received_G711(audio_codec, audio_frames);
+ }
+ }
+ }
+ else if (data_received.Channel == audio_data_channel
+ && rtp_payload_type == audio_payload
+ && audio_codec.Equals("AMR")) {
+ // AMR
+ byte[] rtp_payload = new byte[e.Message.Data.Length - rtp_payload_start]; // payload with RTP header removed
+ System.Array.Copy(e.Message.Data, rtp_payload_start, rtp_payload, 0, rtp_payload.Length); // copy payload
+
+ List audio_frames = amrPayload.Process_AMR_RTP_Packet(rtp_payload, rtp_marker);
+
+ if (audio_frames == null) {
+ // some error
+ } else {
+ // Write the audio frames to the file
+ if (Received_AMR != null) {
+ Received_AMR(audio_codec, audio_frames);
+ }
+ }
+ }
+ else if (data_received.Channel == audio_data_channel
+ && rtp_payload_type == audio_payload
+ && audio_codec.Equals("MPEG4-GENERIC")
+ && aacPayload != null)
+ {
+ // AAC
+ byte[] rtp_payload = new byte[e.Message.Data.Length - rtp_payload_start]; // payload with RTP header removed
+ System.Array.Copy(e.Message.Data, rtp_payload_start, rtp_payload, 0, rtp_payload.Length); // copy payload
+
+ List audio_frames = aacPayload.Process_AAC_RTP_Packet(rtp_payload, rtp_marker);
+
+ if (audio_frames == null) {
+ // some error
+ } else {
+ // Write the audio frames to the file
+ if (Received_AAC != null) {
+ Received_AAC(audio_codec, audio_frames, aacPayload.ObjectType, aacPayload.FrequencyIndex, aacPayload.ChannelConfiguration);
+ }
+ }
+ }
+ else if (data_received.Channel == video_data_channel && rtp_payload_type == 26) {
+ _logger.Warn("No parser has been written for JPEG RTP packets. Please help write one");
+ return; // ignore this data
+ }
+ else {
+ _logger.Warn("No parser for RTP payload " + rtp_payload_type);
+ }
+ }
+ }
+
+
+ // RTSP Messages are OPTIONS, DESCRIBE, SETUP, PLAY etc
+ private void Rtsp_MessageReceived(object sender, Inspectron.HawkEye.RTSP.RtspChunkEventArgs e)
+ {
+ Inspectron.HawkEye.RTSP.Messages.RtspResponse message = e.Message as Inspectron.HawkEye.RTSP.Messages.RtspResponse;
+
+ _logger.Debug("Received RTSP Message " + message.OriginalRequest.ToString());
+
+ // If message has a 401 - Unauthorised Error, then we re-send the message with Authorization
+ // using the most recently received 'realm' and 'nonce'
+ if (message.IsOk == false) {
+ _logger.Debug("Got Error in RTSP Reply " + message.ReturnCode + " " + message.ReturnMessage);
+
+ if (message.ReturnCode == 401 && (message.OriginalRequest.Headers.ContainsKey(RtspHeaderNames.Authorization)==true)) {
+ // the authorization failed.
+ Stop();
+ return;
+ }
+
+ // Check if the Reply has an Authenticate header.
+ if (message.ReturnCode == 401 && message.Headers.ContainsKey(RtspHeaderNames.WWWAuthenticate)) {
+
+ // Process the WWW-Authenticate header
+ // EG: Basic realm="AProxy"
+ // EG: Digest realm="AXIS_WS_ACCC8E3A0A8F", nonce="000057c3Y810622bff50b36005eb5efeae118626a161bf", stale=FALSE
+ // EG: Digest realm="IP Camera(21388)", nonce="534407f373af1bdff561b7b4da295354", stale="FALSE"
+
+ String www_authenticate = message.Headers[RtspHeaderNames.WWWAuthenticate];
+ String auth_params = "";
+
+ if (www_authenticate.StartsWith("basic",StringComparison.InvariantCultureIgnoreCase)) {
+ auth_type = "Basic";
+ auth_params = www_authenticate.Substring(5);
+ }
+ if (www_authenticate.StartsWith("digest",StringComparison.InvariantCultureIgnoreCase)) {
+ auth_type = "Digest";
+ auth_params = www_authenticate.Substring(6);
+ }
+
+ string[] items = auth_params.Split(new char[] { ',' }); // NOTE, does not handle Commas in Quotes
+
+ foreach (string item in items) {
+ // Split on the = symbol and update the realm and nonce
+ string[] parts = item.Trim().Split(new char[] {'='},2); // max 2 parts in the results array
+ if (parts.Count() >= 2 && parts[0].Trim().Equals("realm")) {
+ realm = parts[1].Trim(new char[] {' ','\"'}); // trim space and quotes
+ }
+ else if (parts.Count() >= 2 && parts[0].Trim().Equals("nonce")) {
+ nonce = parts[1].Trim(new char[] {' ','\"'}); // trim space and quotes
+ }
+ }
+
+ _logger.Debug("WWW Authorize parsed for " + auth_type + " " + realm + " " + nonce);
+ }
+
+ RtspMessage resend_message = message.OriginalRequest.Clone() as RtspMessage;
+
+ if (auth_type != null) {
+ AddAuthorization(resend_message,username,password,auth_type,realm,nonce,url);
+ }
+
+ rtsp_client.SendMessage(resend_message);
+
+ return;
+
+ }
+
+
+ // If we get a reply to OPTIONS then start the Keepalive Timer and send DESCRIBE
+ if (message.OriginalRequest != null && message.OriginalRequest is Inspectron.HawkEye.RTSP.Messages.RtspRequestOptions)
+ {
+
+ // Check the capabilities returned by OPTIONS
+ // The Public: header contains the list of commands the RTSP server supports
+ // Eg DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS, ANNOUNCE, RECORD, GET_PARAMETER]}
+ if (message.Headers.ContainsKey(RtspHeaderNames.Public))
+ {
+ string[] parts = message.Headers[RtspHeaderNames.Public].Split(',');
+ foreach (String part in parts) {
+ if (part.Trim().ToUpper().Equals("GET_PARAMETER")) server_supports_get_parameter = true;
+ if (part.Trim().ToUpper().Equals("SET_PARAMETER")) server_supports_set_parameter = true;
+ }
+ }
+
+ if (keepalive_timer == null)
+ {
+ // Start a Timer to send an Keepalive RTSP command every 20 seconds
+ keepalive_timer = new System.Timers.Timer();
+ keepalive_timer.Elapsed += Timer_Elapsed;
+ keepalive_timer.Interval = 20 * 1000;
+ keepalive_timer.Enabled = true;
+
+ // Send DESCRIBE
+ Inspectron.HawkEye.RTSP.Messages.RtspRequest describe_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestDescribe();
+ describe_message.RtspUri = new Uri(url);
+ if (auth_type != null) {
+ AddAuthorization(describe_message,username,password,auth_type,realm,nonce,url);
+ }
+ rtsp_client.SendMessage(describe_message);
+ }
+ else
+ {
+ // If the Keepalive Timer was not null, the OPTIONS reply may have come from a Keepalive
+ // So no need to generate a DESCRIBE message
+ // do nothing
+ }
+ }
+
+
+ // If we get a reply to DESCRIBE (which was our second command), then prosess SDP and send the SETUP
+ if (message.OriginalRequest != null && message.OriginalRequest is Inspectron.HawkEye.RTSP.Messages.RtspRequestDescribe)
+ {
+
+ // Got a reply for DESCRIBE
+ if (message.IsOk == false) {
+ _logger.Debug("Got Error in DESCRIBE Reply " + message.ReturnCode + " " + message.ReturnMessage);
+ return;
+ }
+
+ // Examine the SDP
+
+ _logger.Debug(System.Text.Encoding.UTF8.GetString(message.Data));
+
+ Inspectron.HawkEye.RTSP.Sdp.SdpFile sdp_data;
+ using (StreamReader sdp_stream = new StreamReader(new MemoryStream(message.Data)))
+ {
+ sdp_data = Inspectron.HawkEye.RTSP.Sdp.SdpFile.Read(sdp_stream);
+ }
+
+ // RTP and RTCP 'channels' are used in TCP Interleaved mode (RTP over RTSP)
+ // These are the channels we request. The camera confirms the channel in the SETUP Reply.
+ // But, a Panasonic decides to use different channels in the reply.
+ int next_free_rtp_channel = 0;
+ int next_free_rtcp_channel = 1;
+
+ // Process each 'Media' Attribute in the SDP (each sub-stream)
+
+ for (int x = 0; x < sdp_data.Medias.Count; x++)
+ {
+ bool audio = (sdp_data.Medias[x].MediaType == Inspectron.HawkEye.RTSP.Sdp.Media.MediaTypes.audio);
+ bool video = (sdp_data.Medias[x].MediaType == Inspectron.HawkEye.RTSP.Sdp.Media.MediaTypes.video);
+
+ if (video && video_payload != -1) continue; // have already matched a video payload. don't match another
+ if (audio && audio_payload != -1) continue; // have already matched an audio payload. don't match another
+
+ if (audio && (client_wants_audio == false)) continue; // client does not want audio from the RTSP server
+ if (video && (client_wants_video == false)) continue; // client does not want video from the RTSP server
+
+ if (audio || video)
+ {
+
+ // search the attributes for control, rtpmap and fmtp
+ // (fmtp only applies to video)
+ String control = ""; // the "track" or "stream id"
+ Inspectron.HawkEye.RTSP.Sdp.AttributFmtp fmtp = null; // holds SPS and PPS in base64 (h264 video)
+ foreach (Inspectron.HawkEye.RTSP.Sdp.Attribut attrib in sdp_data.Medias[x].Attributs) {
+ if (attrib.Key.Equals("control")) {
+ String sdp_control = attrib.Value;
+ if (sdp_control.ToLower().StartsWith("rtsp://")) {
+ control = sdp_control; //absolute path
+ } else {
+ control = url + "/" + sdp_control; // relative path
+ }
+ if (video) video_uri = new Uri(control);
+ if (audio) audio_uri = new Uri(control);
+ }
+ if (attrib.Key.Equals("fmtp")) {
+ fmtp = attrib as Inspectron.HawkEye.RTSP.Sdp.AttributFmtp;
+ }
+ if (attrib.Key.Equals("rtpmap")) {
+ Inspectron.HawkEye.RTSP.Sdp.AttributRtpMap rtpmap = attrib as Inspectron.HawkEye.RTSP.Sdp.AttributRtpMap;
+
+ // Check if the Codec Used (EncodingName) is one we support
+ String[] valid_video_codecs = {"H264","H265"};
+ String[] valid_audio_codecs = {"PCMA", "PCMU", "AMR", "MPEG4-GENERIC" /* for aac */}; // Note some are "mpeg4-generic" lower case
+
+ if (video && Array.IndexOf(valid_video_codecs,rtpmap.EncodingName.ToUpper()) >= 0) {
+ // found a valid codec
+ video_codec = rtpmap.EncodingName.ToUpper();
+ video_payload = sdp_data.Medias[x].PayloadType;
+ }
+ if (audio && Array.IndexOf(valid_audio_codecs,rtpmap.EncodingName.ToUpper()) >= 0) {
+ audio_codec = rtpmap.EncodingName.ToUpper();
+ audio_payload = sdp_data.Medias[x].PayloadType;
+ }
+ }
+ }
+
+ // Create H264 RTP Parser
+ if (video && video_codec.Contains("H264"))
+ {
+ h264Payload = new Inspectron.HawkEye.RTSP.H264Payload();
+ }
+
+ // If the rtpmap contains H264 then split the fmtp to get the sprop-parameter-sets which hold the SPS and PPS in base64
+ if (video && video_codec.Contains("H264") && fmtp != null) {
+ var param = Inspectron.HawkEye.RTSP.Sdp.H264Parameters.Parse(fmtp.FormatParameter);
+ var sps_pps = param.SpropParameterSets;
+ if (sps_pps.Count() >= 2) {
+ byte[] sps = sps_pps[0];
+ byte[] pps = sps_pps[1];
+ if (Received_SPS_PPS != null) {
+ Received_SPS_PPS(sps,pps);
+ }
+ h264_sps_pps_fired = true;
+ }
+ }
+
+ // Create H265 RTP Parser
+ if (video && video_codec.Contains("H265"))
+ {
+ // TODO - check if DONL is being used
+ bool has_donl = false;
+ h265Payload = new Inspectron.HawkEye.RTSP.H265Payload(has_donl);
+ }
+
+ // If the rtpmap contains H265 then split the fmtp to get the sprop-vps, sprop-sps and sprop-pps
+ // The RFC makes the VPS, SPS and PPS OPTIONAL so they may not be present. In which we pass back NULL values
+ if (video && video_codec.Contains("H265") && fmtp != null)
+ {
+ var param = Inspectron.HawkEye.RTSP.Sdp.H265Parameters.Parse(fmtp.FormatParameter);
+ var vps_sps_pps = param.SpropParameterSets;
+ if (vps_sps_pps.Count() >= 3)
+ {
+ byte[] vps = vps_sps_pps[0];
+ byte[] sps = vps_sps_pps[1];
+ byte[] pps = vps_sps_pps[2];
+ if (Received_VPS_SPS_PPS != null)
+ {
+ Received_VPS_SPS_PPS(vps,sps, pps);
+ }
+ h265_vps_sps_pps_fired = true;
+ }
+ }
+
+ // Create AAC RTP Parser
+ // Example fmtp is "96 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1490"
+ // Example fmtp is ""96 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1210"
+ if (audio && audio_codec.Contains("MPEG4-GENERIC") && fmtp.GetParameter("mode").ToLower().Equals("aac-hbr"))
+ {
+ // Extract config (eg 0x1490 or 0x1210)
+
+ aacPayload = new Inspectron.HawkEye.RTSP.AACPayload(fmtp.GetParameter("config"));
+ }
+
+
+ // Send the SETUP RTSP command if we have a matching Payload Decoder
+ if (video && video_payload == -1) continue;
+ if (audio && audio_payload == -1) continue;
+
+ RtspTransport transport = null;
+
+ if (rtp_transport == RTP_TRANSPORT.TCP)
+ {
+ // Server interleaves the RTP packets over the RTSP connection
+ // Example for TCP mode (RTP over RTSP) Transport: RTP/AVP/TCP;interleaved=0-1
+ if (video) {
+ video_data_channel = next_free_rtp_channel;
+ video_rtcp_channel = next_free_rtcp_channel;
+ }
+ if (audio) {
+ audio_data_channel = next_free_rtp_channel;
+ audio_rtcp_channel = next_free_rtcp_channel;
+ }
+ transport = new RtspTransport()
+ {
+ LowerTransport = RtspTransport.LowerTransportType.TCP,
+ Interleaved = new PortCouple(next_free_rtp_channel, next_free_rtcp_channel), // Eg Channel 0 for RTP video data. Channel 1 for RTCP status reports
+ };
+
+ next_free_rtp_channel += 2;
+ next_free_rtcp_channel += 2;
+ }
+ if (rtp_transport == RTP_TRANSPORT.UDP)
+ {
+ int rtp_port = 0;
+ int rtcp_port = 0;
+ // Server sends the RTP packets to a Pair of UDP Ports (one for data, one for rtcp control messages)
+ // Example for UDP mode Transport: RTP/AVP;unicast;client_port=8000-8001
+ if (video) {
+ video_data_channel = video_udp_pair.data_port; // Used in DataReceived event handler
+ video_rtcp_channel = video_udp_pair.control_port; // Used in DataReceived event handler
+ rtp_port = video_udp_pair.data_port;
+ rtcp_port = video_udp_pair.control_port;
+ }
+ if (audio) {
+ audio_data_channel = audio_udp_pair.data_port; // Used in DataReceived event handler
+ audio_rtcp_channel = audio_udp_pair.control_port; // Used in DataReceived event handler
+ rtp_port = audio_udp_pair.data_port;
+ rtcp_port = audio_udp_pair.control_port;
+ }
+ transport = new RtspTransport()
+ {
+ LowerTransport = RtspTransport.LowerTransportType.UDP,
+ IsMulticast = false,
+ ClientPort = new PortCouple(rtp_port, rtcp_port), // a UDP Port for data (video or audio). a UDP Port for RTCP status reports
+ };
+ }
+ if (rtp_transport == RTP_TRANSPORT.MULTICAST)
+ {
+ // Server sends the RTP packets to a Pair of UDP ports (one for data, one for rtcp control messages)
+ // using Multicast Address and Ports that are in the reply to the SETUP message
+ // Example for MULTICAST mode Transport: RTP/AVP;multicast
+ if (video) {
+ video_data_channel = 0; // we get this information in the SETUP message reply
+ video_rtcp_channel = 0; // we get this information in the SETUP message reply
+ }
+ if (audio) {
+ audio_data_channel = 0; // we get this information in the SETUP message reply
+ audio_rtcp_channel = 0; // we get this information in the SETUP message reply
+ }
+ transport = new RtspTransport()
+ {
+ LowerTransport = RtspTransport.LowerTransportType.UDP,
+ IsMulticast = true
+ };
+ }
+
+ // Generate SETUP messages
+ Inspectron.HawkEye.RTSP.Messages.RtspRequestSetup setup_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestSetup();
+ setup_message.RtspUri = new Uri(control);
+ setup_message.AddTransport(transport);
+ if (auth_type != null) {
+ AddAuthorization(setup_message,username,password,auth_type,realm,nonce,url);
+ }
+
+ // Add SETUP message to list of mesages to send
+ setup_messages.Add(setup_message);
+
+ }
+ }
+ // Send the FIRST SETUP message and remove it from the list of Setup Messages
+ rtsp_client.SendMessage(setup_messages[0]);
+ setup_messages.RemoveAt(0);
+ }
+
+
+ // If we get a reply to SETUP (which was our third command), then we
+ // (i) check if the Interleaved Channel numbers have been modified by the camera (eg Panasonic cameras)
+ // (ii) check if we have any more SETUP commands to send out (eg if we are doing SETUP for Video and Audio)
+ // (iii) send a PLAY command if all the SETUP command have been sent
+ if (message.OriginalRequest != null && message.OriginalRequest is Inspectron.HawkEye.RTSP.Messages.RtspRequestSetup)
+ {
+ // Got Reply to SETUP
+ if (message.IsOk == false) {
+ _logger.Debug("Got Error in SETUP Reply " + message.ReturnCode + " " + message.ReturnMessage);
+ return;
+ }
+
+ _logger.Debug("Got reply from Setup. Session is " + message.Session);
+
+ session = message.Session; // Session value used with Play, Pause, Teardown and and additional Setups
+ if(message.Timeout > 0 && message.Timeout > keepalive_timer.Interval / 1000)
+ {
+ keepalive_timer.Interval = message.Timeout * 1000 / 2;
+ }
+
+ // Check the Transport header
+ if (message.Headers.ContainsKey(RtspHeaderNames.Transport))
+ {
+
+ RtspTransport transport = RtspTransport.Parse(message.Headers[RtspHeaderNames.Transport]);
+
+ // Check if Transport header includes Multicast
+ if (transport.IsMulticast)
+ {
+ String multicast_address = transport.Destination;
+ video_data_channel = transport.Port.First;
+ video_rtcp_channel = transport.Port.Second;
+
+ // Create the Pair of UDP Sockets in Multicast mode
+ video_udp_pair = new Inspectron.HawkEye.RTSP.UDPSocket(multicast_address, video_data_channel, multicast_address, video_rtcp_channel);
+ video_udp_pair.DataReceived += Rtp_DataReceived;
+ video_udp_pair.Start();
+
+ // TODO - Need to set audio_udp_pair for Multicast
+ }
+
+ // check if the requested Interleaved channels have been modified by the camera
+ // in the SETUP Reply (Panasonic have a camera that does this)
+ if (transport.LowerTransport == RtspTransport.LowerTransportType.TCP) {
+ if (message.OriginalRequest.RtspUri == video_uri) {
+ video_data_channel = transport.Interleaved.First;
+ video_rtcp_channel = transport.Interleaved.Second;
+ }
+ if (message.OriginalRequest.RtspUri == audio_uri) {
+ audio_data_channel = transport.Interleaved.First;
+ audio_rtcp_channel = transport.Interleaved.Second;
+ }
+
+ }
+ }
+
+
+ // Check if we have another SETUP command to send, then remote it from the list
+ if (setup_messages.Count > 0) {
+ // send the next SETUP message, after adding in the 'session'
+ Inspectron.HawkEye.RTSP.Messages.RtspRequestSetup next_setup = setup_messages[0];
+ next_setup.Session = session;
+ rtsp_client.SendMessage(next_setup);
+
+ setup_messages.RemoveAt(0);
+ }
+
+ else {
+ // Send PLAY
+ Inspectron.HawkEye.RTSP.Messages.RtspRequest play_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestPlay();
+ play_message.RtspUri = new Uri(url);
+ play_message.Session = session;
+ if (auth_type != null) {
+ AddAuthorization(play_message,username,password,auth_type,realm,nonce,url);
+ }
+ rtsp_client.SendMessage(play_message);
+ }
+ }
+
+ // If we get a reply to PLAY (which was our fourth command), then we should have video being received
+ if (message.OriginalRequest != null && message.OriginalRequest is Inspectron.HawkEye.RTSP.Messages.RtspRequestPlay)
+ {
+ // Got Reply to PLAY
+ if (message.IsOk == false) {
+ _logger.Debug("Got Error in PLAY Reply " + message.ReturnCode + " " + message.ReturnMessage);
+ return;
+ }
+
+ _logger.Debug("Got reply from Play " + message.Command);
+ }
+
+ }
+
+ void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
+ {
+ // Send Keepalive message
+ // The ONVIF Standard uses SET_PARAMETER as "an optional method to keep an RTSP session alive"
+ // RFC 2326 (RTSP Standard) says "GET_PARAMETER with no entity body may be used to test client or server liveness("ping")"
+
+ // This code uses GET_PARAMETER (unless OPTIONS report it is not supported, and then it sends OPTIONS as a keepalive)
+
+
+ if (server_supports_get_parameter) {
+
+ Inspectron.HawkEye.RTSP.Messages.RtspRequest getparam_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestGetParameter();
+ getparam_message.RtspUri = new Uri(url);
+ getparam_message.Session = session;
+ if (auth_type != null)
+ {
+ AddAuthorization(getparam_message, username, password, auth_type, realm, nonce, url);
+ }
+ rtsp_client.SendMessage(getparam_message);
+
+ } else {
+
+ Inspectron.HawkEye.RTSP.Messages.RtspRequest options_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestOptions();
+ options_message.RtspUri = new Uri(url);
+ if (auth_type != null) {
+ AddAuthorization(options_message,username,password,auth_type,realm,nonce,url);
+ }
+ rtsp_client.SendMessage(options_message);
+ }
+ }
+
+ // Generate Basic or Digest Authorization
+ public void AddAuthorization(RtspMessage message, string username, string password,
+ string auth_type, string realm, string nonce, string url) {
+
+ if (username == null || username.Length == 0) return;
+ if (password == null || password.Length == 0) return;
+ if (realm == null || realm.Length == 0) return;
+ if (auth_type.Equals("Digest") && (nonce == null || nonce.Length == 0)) return;
+
+ if (auth_type.Equals("Basic")) {
+ byte[] credentials = System.Text.Encoding.UTF8.GetBytes(username+":"+password);
+ String credentials_base64 = Convert.ToBase64String(credentials);
+ String basic_authorization = "Basic " + credentials_base64;
+
+ message.Headers.Add(RtspHeaderNames.Authorization, basic_authorization);
+
+ return;
+ }
+ else if (auth_type.Equals("Digest")) {
+
+ string method = message.Method; // DESCRIBE, SETUP, PLAY etc
+
+ MD5 md5 = System.Security.Cryptography.MD5.Create();
+ String hashA1 = CalculateMD5Hash(md5, username+":"+realm+":"+password);
+ String hashA2 = CalculateMD5Hash(md5, method + ":" + url);
+ String response = CalculateMD5Hash(md5, hashA1 + ":" + nonce + ":" + hashA2);
+
+ const String quote = "\"";
+ String digest_authorization = "Digest username=" + quote + username + quote +", "
+ + "realm=" + quote + realm + quote + ", "
+ + "nonce=" + quote + nonce + quote + ", "
+ + "uri=" + quote + url + quote + ", "
+ + "response=" + quote + response + quote;
+
+ message.Headers.Add(RtspHeaderNames.Authorization, digest_authorization);
+
+ return;
+ }
+ else {
+ return;
+ }
+
+ }
+
+ // MD5 (lower case)
+ public string CalculateMD5Hash(MD5 md5_session, string input)
+ {
+ byte[] inputBytes = System.Text.Encoding.UTF8.GetBytes(input);
+ byte[] hash = md5_session.ComputeHash(inputBytes);
+
+ StringBuilder output = new StringBuilder();
+ for (int i = 0; i < hash.Length; i++) {
+ output.Append(hash[i].ToString("x2"));
+ }
+
+ return output.ToString();
+ }
+
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/G711Payload.cs b/framework/Inspectron.HawkEye/RTSP/G711Payload.cs
new file mode 100644
index 0000000..5bc0f3e
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/G711Payload.cs
@@ -0,0 +1,64 @@
+using System.Collections.Generic;
+
+namespace Inspectron.HawkEye.RTSP
+{
+ // This class handles the G711 Payload
+ // It has methods to process the RTP Payload
+
+ public class G711Payload
+ {
+ // Constructor
+ public G711Payload()
+ {
+ }
+
+ public List Process_G711_RTP_Packet(byte[] rtp_payload, int rtp_marker) {
+
+ List audio_data = new List();
+ audio_data.Add(rtp_payload);
+
+ return audio_data;
+ }
+
+ /* Untested - used with G711.1 and PCMA-WB and PCMU-WB Codec Names */
+ public List Process_G711_1_RTP_Packet(byte[] rtp_payload, int rtp_marker) {
+
+ // Look at the Header. This tells us the G711 mode being used
+
+ // Mode Index (MI) is
+ // 1 - R1 40 octets containg Layer 0 data
+ // 2 - R2a 50 octets containing Layer 0 plus Layer 1 data
+ // 3 - R2b 50 octets containing Layer 0 plus Layer 2 data
+ // 4 - R3 60 octets containing Layer 0 plus Layer 1 plus Layer 2 data
+
+ byte mode_index = (byte)(rtp_payload[0] & 0x07);
+
+ int size_of_one_frame = 0; // will be in bytes
+ switch (mode_index) {
+ case 1: size_of_one_frame = 40; break;
+ case 2: size_of_one_frame = 50; break;
+ case 3: size_of_one_frame = 50; break;
+ case 4: size_of_one_frame = 60; break;
+ default: return null; // invalid Mode Index
+ }
+
+ int number_frames = (rtp_payload.Length - 1) / size_of_one_frame;
+
+
+ // Return just the basic u-Law or A-Law audio (the Layer 0 audio)
+
+ List audio_data = new List();
+
+ // Extract each audio frame and place in the audio_data List
+ int frame_start = 1; // starts just after the MI header
+ while (frame_start + size_of_one_frame < rtp_payload.Length) {
+ byte[] layer_0_audio = new byte[40];
+ System.Array.Copy(rtp_payload,frame_start,layer_0_audio,0,40); // 40 octets in Layer 0 data
+ audio_data.Add(layer_0_audio);
+
+ frame_start += size_of_one_frame;
+ }
+ return audio_data;
+ }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/H264Payload.cs b/framework/Inspectron.HawkEye/RTSP/H264Payload.cs
new file mode 100644
index 0000000..986fc36
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/H264Payload.cs
@@ -0,0 +1,184 @@
+using System.Collections.Generic;
+using System.IO;
+
+namespace Inspectron.HawkEye.RTSP
+{
+ // This class handles the H264 Payload
+ // It has methods to parse parameters in the SDP
+ // It has methods to process the RTP Payload
+
+ public class H264Payload
+ {
+ private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();
+
+ int norm, fu_a, fu_b, stap_a, stap_b, mtap16, mtap24 = 0; // used for diagnostics stats
+
+ List temporary_rtp_payloads = new List(); // used to assemble the RTP packets that form one RTP Frame
+ // Eg all the RTP Packets from M=0 through to M=1
+
+ MemoryStream fragmented_nal = new MemoryStream(); // used to concatenate fragmented H264 NALs where NALs are split over RTP packets
+
+
+ // Constructor
+ public H264Payload()
+ {
+ }
+
+ public List Process_H264_RTP_Packet(byte[] rtp_payload, int rtp_marker) {
+
+ // Add to the list of payloads for the current Frame of video
+ temporary_rtp_payloads.Add(rtp_payload); // Todo Could optimise this and go direct to Process Frame if just 1 packet in frame
+
+ if (rtp_marker == 1)
+ {
+ // End Marker is set. Process the list of RTP Packets (forming 1 RTP frame) and save the NALs to a file
+ List nal_units = Process_H264_RTP_Frame(temporary_rtp_payloads);
+ temporary_rtp_payloads.Clear();
+
+ return nal_units;
+ }
+
+ return null; // we don't have a frame yet. Keep accumulating RTP packets
+ }
+
+
+ // Process a RTP Frame. A RTP Frame can consist of several RTP Packets which have the same Timestamp
+ // Returns a list of NAL Units (with no 00 00 00 01 header and with no Size header)
+ private List Process_H264_RTP_Frame(List rtp_payloads)
+ {
+ _logger.Debug("RTP Data comprised of " + rtp_payloads.Count + " rtp packets");
+
+ List nal_units = new List(); // Stores the NAL units for a Video Frame. May be more than one NAL unit in a video frame.
+
+ for (int payload_index = 0; payload_index < rtp_payloads.Count; payload_index++)
+ {
+ // Examine the first rtp_payload and the first byte (the NAL header)
+ int nal_header_f_bit = (rtp_payloads[payload_index][0] >> 7) & 0x01;
+ int nal_header_nri = (rtp_payloads[payload_index][0] >> 5) & 0x03;
+ int nal_header_type = (rtp_payloads[payload_index][0] >> 0) & 0x1F;
+
+ // If the Nal Header Type is in the range 1..23 this is a normal NAL (not fragmented)
+ // So write the NAL to the file
+ if (nal_header_type >= 1 && nal_header_type <= 23)
+ {
+ _logger.Debug("Normal NAL");
+ norm++;
+ nal_units.Add(rtp_payloads[payload_index]);
+ }
+ // There are 4 types of Aggregation Packet (split over RTP payloads)
+ else if (nal_header_type == 24)
+ {
+ _logger.Debug("Agg STAP-A");
+ stap_a++;
+
+ // RTP packet contains multiple NALs, each with a 16 bit header
+ // Read 16 byte size
+ // Read NAL
+ try
+ {
+ int ptr = 1; // start after the nal_header_type which was '24'
+ // if we have at least 2 more bytes (the 16 bit size) then consume more data
+ while (ptr + 2 < (rtp_payloads[payload_index].Length - 1))
+ {
+ int size = (rtp_payloads[payload_index][ptr] << 8) + (rtp_payloads[payload_index][ptr + 1] << 0);
+ ptr = ptr + 2;
+ byte[] nal = new byte[size];
+ System.Array.Copy(rtp_payloads[payload_index], ptr, nal, 0, size); // copy the NAL
+ nal_units.Add(nal); // Add to list of NALs for this RTP frame. Start Codes like 00 00 00 01 get added later
+ ptr = ptr + size;
+ }
+ }
+ catch
+ {
+ _logger.Debug("H264 Aggregate Packet processing error");
+ }
+ }
+ else if (nal_header_type == 25)
+ {
+ _logger.Debug("Agg STAP-B not supported");
+ stap_b++;
+ }
+ else if (nal_header_type == 26)
+ {
+ _logger.Debug("Agg MTAP16 not supported");
+ mtap16++;
+ }
+ else if (nal_header_type == 27)
+ {
+ _logger.Debug("Agg MTAP24 not supported");
+ mtap24++;
+ }
+ else if (nal_header_type == 28)
+ {
+ _logger.Debug("Frag FU-A");
+ fu_a++;
+
+ // Parse Fragmentation Unit Header
+ int fu_header_s = (rtp_payloads[payload_index][1] >> 7) & 0x01; // start marker
+ int fu_header_e = (rtp_payloads[payload_index][1] >> 6) & 0x01; // end marker
+ int fu_header_r = (rtp_payloads[payload_index][1] >> 5) & 0x01; // reserved. should be 0
+ int fu_header_type = (rtp_payloads[payload_index][1] >> 0) & 0x1F; // Original NAL unit header
+
+ _logger.Debug("Frag FU-A s=" + fu_header_s + "e=" + fu_header_e);
+
+ // Check Start and End flags
+ if (fu_header_s == 1 && fu_header_e == 0)
+ {
+ // Start of Fragment.
+ // Initiise the fragmented_nal byte array
+ // Build the NAL header with the original F and NRI flags but use the the Type field from the fu_header_type
+ byte reconstructed_nal_type = (byte)((nal_header_f_bit << 7) + (nal_header_nri << 5) + fu_header_type);
+
+ // Empty the stream
+ fragmented_nal.SetLength(0);
+
+ // Add reconstructed_nal_type byte to the memory stream
+ fragmented_nal.WriteByte(reconstructed_nal_type);
+
+ // copy the rest of the RTP payload to the memory stream
+ fragmented_nal.Write(rtp_payloads[payload_index], 2, rtp_payloads[payload_index].Length - 2);
+ }
+
+ if (fu_header_s == 0 && fu_header_e == 0)
+ {
+ // Middle part of Fragment
+ // Append this payload to the fragmented_nal
+ // Data starts after the NAL Unit Type byte and the FU Header byte
+ fragmented_nal.Write(rtp_payloads[payload_index], 2, rtp_payloads[payload_index].Length - 2);
+ }
+
+ if (fu_header_s == 0 && fu_header_e == 1)
+ {
+ // End part of Fragment
+ // Append this payload to the fragmented_nal
+ // Data starts after the NAL Unit Type byte and the FU Header byte
+ fragmented_nal.Write(rtp_payloads[payload_index], 2, rtp_payloads[payload_index].Length - 2);
+
+ // Add the NAL to the array of NAL units
+ nal_units.Add(fragmented_nal.ToArray());
+ }
+ }
+
+ else if (nal_header_type == 29)
+ {
+ _logger.Debug("Frag FU-B not supported");
+ fu_b++;
+ }
+ else
+ {
+ _logger.Debug("Unknown NAL header " + nal_header_type + " not supported");
+ }
+
+ }
+
+ // Output some statistics
+ _logger.Debug("Norm=" + norm + " ST-A=" + stap_a + " ST-B=" + stap_b + " M16=" + mtap16 + " M24=" + mtap24 + " FU-A=" + fu_a + " FU-B=" + fu_b);
+
+ // Output all the NALs that form one RTP Frame (one frame of video)
+ return nal_units;
+
+ }
+
+
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/H265Payload.cs b/framework/Inspectron.HawkEye/RTSP/H265Payload.cs
new file mode 100644
index 0000000..9af3d89
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/H265Payload.cs
@@ -0,0 +1,224 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Inspectron.HawkEye.RTSP
+{
+ // This class handles the H265 Payload
+ // It has methods to parse parameters in the SDP
+ // It has methods to process the RTP Payload
+
+ // By Roger Hardiman, RJH Technical Consultancy Ltd
+
+ public class H265Payload
+ {
+ // H265 / HEVC structure.
+ // An 'Access Unit' is the set of NAL Units that form one Picture
+ // NAL Units have a 2 byte header comprising of
+ // F Bit, Type, Layer ID and TID
+
+
+ int single, agg, frag = 0; // used for diagnostics stats
+ bool has_donl = false;
+
+ List temporary_rtp_payloads = new List(); // used to assemble the RTP packets that form one RTP Frame
+ // Eg all the RTP Packets from M=0 through to M=1
+
+ MemoryStream fragmented_nal = new MemoryStream(); // used to concatenate fragmented H264 NALs where NALs are split over RTP packets
+
+
+ // Constructor
+ public H265Payload(bool has_donl)
+ {
+ this.has_donl = has_donl;
+ }
+
+ public List Process_H265_RTP_Packet(byte[] rtp_payload, int rtp_marker) {
+
+ // Add payload to the List of payloads for the current Frame of Video
+ // ie all the payloads with M=0 up to the final payload where M=1
+ temporary_rtp_payloads.Add(rtp_payload); // Todo Could optimise this and go direct to Process Frame if just 1 packet in frame
+
+ if (rtp_marker == 1)
+ {
+ // End Marker is set. Process the list of RTP Packets (forming 1 RTP frame) and save the NALs to a file
+ List nal_units = Process_H265_RTP_Frame(temporary_rtp_payloads);
+ temporary_rtp_payloads.Clear();
+
+ return nal_units;
+ }
+
+ return null; // we don't have a frame yet. Keep accumulating RTP packets
+ }
+
+
+ // Process a RTP Frame. A RTP Frame can consist of several RTP Packets which have the same Timestamp
+ // Returns a list of NAL Units (with no 00 00 00 01 header and with no Size header)
+ private List Process_H265_RTP_Frame(List rtp_payloads)
+ {
+ Console.WriteLine("RTP Data comprised of " + rtp_payloads.Count + " rtp packets");
+
+ List nal_units = new List(); // Stores the NAL units for a Video Frame. May be more than one NAL unit in a video frame.
+
+ for (int payload_index = 0; payload_index < rtp_payloads.Count; payload_index++)
+ {
+ // Examine the first two bytes of the RTP data, the Payload Header
+ // F (Forbidden Bit),
+ // Type of NAL Unit (or VCL NAL Unit if Type is < 32),
+ // LayerId
+ // TID (TemporalID = TID - 1)
+ /*+---------------+---------------+
+ *|0|1|2|3|4|5|6|7|0|1|2|3|4|5|6|7|
+ *+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *|F| Type | LayerId | TID |
+ *+-------------+-----------------+
+ */
+
+ int payload_header = (rtp_payloads[payload_index][0] << 8) | (rtp_payloads[payload_index][1]);
+ int payload_header_f_bit = (payload_header >> 15) & 0x01;
+ int payload_header_type = (payload_header >> 9) & 0x3F;
+ int payload_header_layer_id = (payload_header >> 3) & 0x3F;
+ int payload_header_tid = payload_header & 0x7;
+
+
+ // There are three ways to Packetize NAL units into RTP Packets
+ // Single NAL Unit Packet
+ // Aggregation Packet (payload_header_type = 48)
+ // Fragmentation Unit (payload_header_type = 49)
+
+
+ // Single NAL Unit Packet
+ // 32=VPS
+ // 33=SPS
+ // 34=PPS
+ if (payload_header_type != 48 && payload_header_type != 49)
+ {
+ Console.WriteLine("Single NAL");
+ single++;
+
+ //TODO - Handle DONL
+
+ nal_units.Add(rtp_payloads[payload_index]);
+ }
+
+ // Aggregation Packet
+ else if (payload_header_type == 48)
+ {
+ Console.WriteLine("Aggregation Packet");
+ agg++;
+
+ // RTP packet contains multiple NALs, each with a 16 bit header
+ // Read 16 byte size
+ // Read NAL
+ // Use a Try/Catch to protect from bad RTP data where block sizes exceed the
+ // available data
+ try
+ {
+ int ptr = 2; // start after 16 bit Payload Header
+
+ // loop until the ptr has moved beyond the length of the data
+ while (ptr < (rtp_payloads[payload_index].Length - 1))
+ {
+ if (has_donl) ptr = ptr + 2; // step over the DONL data
+ int size = (rtp_payloads[payload_index][ptr] << 8) + (rtp_payloads[payload_index][ptr + 1] << 0);
+ ptr = ptr + 2;
+ byte[] nal = new byte[size];
+ System.Array.Copy(rtp_payloads[payload_index], ptr, nal, 0, size); // copy the NAL
+ nal_units.Add(nal); // Add to list of NALs for this RTP frame. Start Codes like 00 00 00 01 get added later
+ ptr = ptr + size;
+ }
+ }
+ catch
+ {
+ Console.WriteLine("H265 Aggregate Packet processing error");
+ }
+ }
+
+ // Fragmentation Unit
+ else if (payload_header_type == 49)
+ {
+ Console.WriteLine("Fragmentation Unit");
+ frag++;
+
+ // Parse Fragmentation Unit Header
+ int fu_header_s = (rtp_payloads[payload_index][2] >> 7) & 0x01; // start marker
+ int fu_header_e = (rtp_payloads[payload_index][2] >> 6) & 0x01; // end marker
+ int fu_header_type = (rtp_payloads[payload_index][2] >> 0) & 0x3F; // fu type
+
+ Console.WriteLine("Frag FU-A s=" + fu_header_s + "e=" + fu_header_e);
+
+ // Check Start and End flags
+ if (fu_header_s == 1 && fu_header_e == 0)
+ {
+ // Start of Fragment.
+ // Initiise the fragmented_nal byte array
+
+ // Empty the stream
+ fragmented_nal.SetLength(0);
+
+ // Reconstrut the NAL header from the rtp_payload_header, replacing the Type with FU Type
+ int nal_header = (payload_header & 0x81FF); // strip out existing 'type'
+ nal_header = nal_header | (fu_header_type << 9);
+
+ fragmented_nal.WriteByte((byte)((nal_header >> 8) & 0xFF));
+ fragmented_nal.WriteByte((byte)((nal_header >> 0) & 0xFF));
+
+ if (has_donl)
+ {
+ // start copying after the DONL data
+ fragmented_nal.Write(rtp_payloads[payload_index], 5, rtp_payloads[payload_index].Length - 5);
+ }
+ else
+ {
+ // there is no DONL data
+ fragmented_nal.Write(rtp_payloads[payload_index], 3, rtp_payloads[payload_index].Length - 3);
+ }
+ }
+
+ if (fu_header_s == 0 && fu_header_e == 0)
+ {
+ // Middle part of Fragment
+ // Append this payload to the fragmented_nal
+
+ if (has_donl) {
+ // start copying after the DONL data
+ fragmented_nal.Write(rtp_payloads[payload_index], 5, rtp_payloads[payload_index].Length - 5);
+ } else {
+ // there is no DONL data
+ fragmented_nal.Write(rtp_payloads[payload_index], 3, rtp_payloads[payload_index].Length - 3);
+ }
+ }
+
+ if (fu_header_s == 0 && fu_header_e == 1)
+ {
+ // End part of Fragment
+ // Append this payload to the fragmented_nal
+ if (has_donl)
+ {
+ // start copying after the DONL data
+ fragmented_nal.Write(rtp_payloads[payload_index], 5, rtp_payloads[payload_index].Length - 5);
+ }
+ else
+ {
+ // there is no DONL data
+ fragmented_nal.Write(rtp_payloads[payload_index], 3, rtp_payloads[payload_index].Length - 3);
+ }
+
+ // Add the NAL to the array of NAL units
+ nal_units.Add(fragmented_nal.ToArray());
+ }
+ }
+ else {
+ Console.WriteLine("Unknown Payload Header Type = " + payload_header_type);
+ }
+ }
+
+ // Output some statistics
+ Console.WriteLine("Single=" + single + " Agg=" + agg + " Frag=" + frag);
+
+ // Output all the NALs that form one RTP Frame (one frame of video)
+ return nal_units;
+
+ }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/IRTSPTransport.cs b/framework/Inspectron.HawkEye/RTSP/IRTSPTransport.cs
new file mode 100644
index 0000000..62af36e
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/IRTSPTransport.cs
@@ -0,0 +1,41 @@
+namespace Inspectron.HawkEye.RTSP
+{
+ ///
+ /// Interface for Transport of Rtsp (TCP, TCP+SSL,..)
+ ///
+ public interface IRtspTransport
+ {
+ ///
+ /// Gets the stream of the transport.
+ ///
+ /// A stream
+ System.IO.Stream GetStream();
+
+ ///
+ /// Gets the remote address.
+ ///
+ /// The remote address.
+ string RemoteAddress
+ {
+ get;
+ }
+
+ ///
+ /// Closes this instance.
+ ///
+ void Close();
+
+ ///
+ /// Gets a value indicating whether this is connected.
+ ///
+ /// true if connected; otherwise, false.
+ bool Connected { get; }
+
+ ///
+ /// Reconnect this instance.
+ /// Must do nothing if already connected.
+ ///
+ /// Error during socket
+ void Reconnect();
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/PortCouple.cs b/framework/Inspectron.HawkEye/RTSP/Messages/PortCouple.cs
new file mode 100644
index 0000000..2791189
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Messages/PortCouple.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Diagnostics.Contracts;
+using System.Globalization;
+
+namespace Inspectron.HawkEye.RTSP.Messages
+{
+ ///
+ /// Describe a couple of port used to transfer video and command.
+ ///
+ public class PortCouple
+ {
+ ///
+ /// Gets or sets the first port number.
+ ///
+ /// The first port.
+ public int First { get; set; }
+ ///
+ /// Gets or sets the second port number.
+ ///
+ /// If not present the value is 0
+ /// The second port.
+ public int Second { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PortCouple()
+ { }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The first port.
+ public PortCouple(int first)
+ {
+ First = first;
+ Second = 0;
+ }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The first port.
+ /// The second port.
+ public PortCouple(int first, int second)
+ {
+ First = first;
+ Second = second;
+ }
+
+ ///
+ /// Gets a value indicating whether this instance has second port.
+ ///
+ ///
+ /// true if this instance has second port; otherwise, false.
+ ///
+ public bool IsSecondPortPresent
+ {
+ get { return Second != 0; }
+ }
+
+ ///
+ /// Parses the int values of port.
+ ///
+ /// A string value.
+ /// The port couple
+ public static PortCouple Parse(string stringValue)
+ {
+ if (stringValue == null)
+ throw new ArgumentNullException("stringValue");
+ Contract.Requires(!string.IsNullOrEmpty(stringValue));
+
+ string[] values = stringValue.Split('-');
+
+ int tempValue;
+
+ int.TryParse(values[0], out tempValue);
+ PortCouple result = new PortCouple(tempValue);
+
+ tempValue = 0;
+ if (values.Length > 1)
+ int.TryParse(values[1], out tempValue);
+
+ result.Second = tempValue;
+
+ return result;
+ }
+
+ ///
+ /// Returns a that represents this instance.
+ ///
+ ///
+ /// A that represents this instance.
+ ///
+ public override string ToString()
+ {
+ if (IsSecondPortPresent)
+ return First.ToString(CultureInfo.InvariantCulture) + "-" + Second.ToString(CultureInfo.InvariantCulture);
+ else
+ return First.ToString(CultureInfo.InvariantCulture);
+ }
+
+
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPChunk.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPChunk.cs
new file mode 100644
index 0000000..f29bdd2
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPChunk.cs
@@ -0,0 +1,49 @@
+using System;
+
+namespace Inspectron.HawkEye.RTSP.Messages
+{
+ ///
+ /// Class wich represent each message echanged on Rtsp socket.
+ ///
+ public abstract class RtspChunk : ICloneable
+ {
+ ///
+ /// Logs the message to debug.
+ ///
+ public void LogMessage()
+ {
+ LogMessage(NLog.LogLevel.Debug);
+ }
+
+ ///
+ /// Logs the message.
+ ///
+ /// The log level.
+ public abstract void LogMessage(NLog.LogLevel aLevel);
+
+ ///
+ /// Gets or sets the data associate with the message.
+ ///
+ /// Array of byte transmit with the message.
+ public byte[] Data
+ { get; set; }
+
+ ///
+ /// Gets or sets the source port wich receive the message.
+ ///
+ /// The source port.
+ public RtspListener SourcePort { get; set; }
+
+ #region ICloneable Membres
+
+ ///
+ /// Crée un nouvel objet qui est une copie de l'instance en cours.
+ ///
+ ///
+ /// Nouvel objet qui est une copie de cette instance.
+ ///
+ public abstract object Clone();
+
+ #endregion
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPData.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPData.cs
new file mode 100644
index 0000000..5c3d619
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPData.cs
@@ -0,0 +1,45 @@
+namespace Inspectron.HawkEye.RTSP.Messages
+{
+ ///
+ /// Message wich represent data. ($ limited message)
+ ///
+ public class RtspData : RtspChunk
+ {
+ private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();
+
+ ///
+ /// Logs the message to debug.
+ ///
+ public override void LogMessage(NLog.LogLevel aLevel)
+ {
+ // Default value to debug
+ if (aLevel == null)
+ aLevel = NLog.LogLevel.Debug;
+ // if the level is not logged directly return
+ if (!_logger.IsEnabled(aLevel))
+ return;
+ _logger.Log(aLevel, "Data message");
+ if (Data == null)
+ _logger.Log(aLevel, "Data : null");
+ else
+ _logger.Log(aLevel, "Data length :-{0}-", Data.Length);
+ }
+
+ public int Channel { get; set; }
+
+ ///
+ /// Clones this instance.
+ /// Listner is not cloned
+ ///
+ /// a clone of this instance
+ public override object Clone()
+ {
+ RtspData result = new RtspData();
+ result.Channel = this.Channel;
+ if (this.Data != null)
+ result.Data = this.Data.Clone() as byte[];
+ result.SourcePort = this.SourcePort;
+ return result;
+ }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPHeaderNames.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPHeaderNames.cs
new file mode 100644
index 0000000..c2fd567
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPHeaderNames.cs
@@ -0,0 +1,19 @@
+namespace Inspectron.HawkEye.RTSP.Messages
+{
+ ///
+ /// Class containing helper constant for general use headers.
+ ///
+ public static class RtspHeaderNames
+ {
+ public const string ContentBase = "Content-Base";
+ public const string ContentEncoding = "Content-Encoding";
+ public const string ContentType = "Content-Type";
+
+ public const string Public = "Public";
+ public const string Session = "Session";
+ public const string Transport = "Transport";
+
+ public const string WWWAuthenticate = "WWW-Authenticate";
+ public const string Authorization = "Authorization";
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPMessage.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPMessage.cs
new file mode 100644
index 0000000..a32b53b
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPMessage.cs
@@ -0,0 +1,309 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace Inspectron.HawkEye.RTSP.Messages
+{
+ public class RtspMessage : RtspChunk
+ {
+ private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();
+
+ ///
+ /// The regex to validate the Rtsp message.
+ ///
+ private static readonly Regex _rtspVersionTest = new Regex(@"^RTSP/\d\.\d", RegexOptions.Compiled);
+ ///
+ /// Create the good type of Rtsp Message from the header.
+ ///
+ /// A request line.
+ /// An Rtsp message
+ public static RtspMessage GetRtspMessage(string aRequestLine)
+ {
+ // We can't determine the message
+ if (string.IsNullOrEmpty(aRequestLine))
+ return new RtspMessage();
+ string[] requestParts = aRequestLine.Split(new char[] { ' ' }, 3);
+ RtspMessage returnValue;
+ if (requestParts.Length == 3)
+ {
+ // A request is : Method SP Request-URI SP RTSP-Version
+ // A response is : RTSP-Version SP Status-Code SP Reason-Phrase
+ // RTSP-Version = "RTSP" "/" 1*DIGIT "." 1*DIGIT
+ if (_rtspVersionTest.IsMatch(requestParts[2]))
+ returnValue = RtspRequest.GetRtspRequest(requestParts);
+ else if (_rtspVersionTest.IsMatch(requestParts[0]))
+ returnValue = new RtspResponse();
+ else
+ {
+ _logger.Warn(CultureInfo.InvariantCulture, "Got a strange message {0}", aRequestLine);
+ returnValue = new RtspMessage();
+ }
+ }
+ else
+ {
+ _logger.Warn(CultureInfo.InvariantCulture, "Got a strange message {0}", aRequestLine);
+ returnValue = new RtspMessage();
+ }
+ returnValue.Command = aRequestLine;
+ return returnValue;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public RtspMessage()
+ {
+ Data = new byte[0];
+ Creation = DateTime.Now;
+ }
+
+ private Dictionary _headers = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ internal protected string[] commandArray;
+
+ ///
+ /// Gets or sets the creation time.
+ ///
+ /// The creation time.
+ public DateTime Creation { get; private set; }
+
+ ///
+ /// Gets or sets the command of the message (first line).
+ ///
+ /// The command.
+ public string Command
+ {
+ get
+ {
+ if (commandArray == null)
+ return string.Empty;
+ return string.Join(" ", commandArray);
+ }
+ set
+ {
+ if (value == null)
+ commandArray = new string[] { String.Empty };
+ else
+ commandArray = value.Split(new char[] {' '}, 3);
+ }
+ }
+
+
+ ///
+ /// Gets the Method of the message (eg OPTIONS, DESCRIBE, SETUP, PLAY).
+ ///
+ /// The Method
+ public string Method
+ {
+ get
+ {
+ if (commandArray == null)
+ return string.Empty;
+ return commandArray[0];
+ }
+ }
+
+
+ ///
+ /// Gets the headers of the message.
+ ///
+ /// The headers.
+ public Dictionary Headers
+ {
+ get
+ {
+ return _headers;
+ }
+ }
+
+ ///
+ /// Adds one header from a string.
+ ///
+ /// The string containing header of format Header: Value.
+ /// is null
+ public void AddHeader(string line)
+ {
+ if (line == (string)null)
+ throw new ArgumentNullException("line");
+
+ //spliter
+ string[] elements = line.Split(new char[] { ':' }, 2);
+ if (elements.Length == 2)
+ {
+ _headers[elements[0].Trim()] = elements[1].TrimStart();
+ }
+ else
+ {
+ _logger.Warn(CultureInfo.InvariantCulture, "Invalid Header received : -{0}-", line);
+ }
+ }
+
+ ///
+ /// Gets or sets the Ccommande Seqquence number.
+ /// If the header is not define or not a valid number it return 0
+ ///
+ /// The sequence number.
+ public int CSeq
+ {
+ get
+ {
+ string returnStringValue;
+ int returnValue;
+ if (!(_headers.TryGetValue("CSeq", out returnStringValue) &&
+ int.TryParse(returnStringValue, out returnValue)))
+ returnValue = 0;
+
+ return returnValue;
+ }
+ set
+ {
+ _headers["CSeq"] = value.ToString(CultureInfo.InvariantCulture);
+ }
+ }
+
+ ///
+ /// Gets the session ID.
+ ///
+ /// The session ID.
+ public virtual string Session
+ {
+ get
+ {
+ if (!_headers.ContainsKey("Session"))
+ return null;
+
+ return _headers["Session"];
+ }
+ set
+ {
+ _headers["Session"] = value;
+ }
+ }
+
+ ///
+ /// Initialises the length of the data byte array from content lenth header.
+ ///
+ public void InitialiseDataFromContentLength()
+ {
+ int dataLength;
+ if (!(_headers.ContainsKey("Content-Length")
+ && int.TryParse(_headers["Content-Length"], out dataLength)))
+ {
+ dataLength = 0;
+ }
+ this.Data = new byte[dataLength];
+ }
+
+ ///
+ /// Adjusts the content length header.
+ ///
+ public void AdjustContentLength()
+ {
+ if (Data.Length > 0)
+ {
+ _headers["Content-Length"] = Data.Length.ToString(CultureInfo.InvariantCulture);
+ }
+ else
+ {
+ _headers.Remove("Content-Length");
+ }
+ }
+
+ ///
+ /// Sends to the message to a stream.
+ ///
+ /// The stream.
+ /// is empty
+ /// can't be written.
+ public void SendTo(Stream stream)
+ {
+ //
+ if (stream == null)
+ throw new ArgumentNullException("stream");
+ if (!stream.CanWrite)
+ throw
+ new ArgumentException("Stream CanWrite == false, can't send message to it", "stream");
+ //
+ Contract.EndContractBlock();
+
+ Encoding encoder = ASCIIEncoding.UTF8;
+ StringBuilder outputString = new StringBuilder();
+
+ AdjustContentLength();
+
+ // output header
+ outputString.Append(Command);
+ outputString.Append("\r\n");
+ foreach (KeyValuePair item in _headers)
+ {
+ outputString.AppendFormat("{0}: {1}\r\n", item.Key, item.Value);
+ }
+ outputString.Append("\r\n");
+ byte[] buffer = encoder.GetBytes(outputString.ToString());
+ lock(stream) {
+ stream.Write(buffer, 0, buffer.Length);
+
+ // Output data
+ if (Data.Length > 0)
+ stream.Write(Data, 0, Data.Length);
+
+ }
+ stream.Flush();
+ }
+
+
+
+ ///
+ /// Logs the message.
+ ///
+ /// A log level.
+ public override void LogMessage(NLog.LogLevel aLevel)
+ {
+ // Default value to debug
+ if (aLevel == null)
+ aLevel = NLog.LogLevel.Debug;
+ // if the level is not logged directly return
+ if (!_logger.IsEnabled(aLevel))
+ return;
+
+ _logger.Log(aLevel, "Commande : {0}", Command);
+ foreach (KeyValuePair item in _headers)
+ {
+ _logger.Log(aLevel, "Header : {0}: {1}", item.Key, item.Value);
+ }
+
+ if (Data.Length > 0)
+ {
+ _logger.Log(aLevel, "Data :-{0}-", ASCIIEncoding.ASCII.GetString(Data));
+ }
+ }
+
+ ///
+ /// Crée un nouvel objet qui est une copie de l'instance en cours.
+ ///
+ ///
+ /// Nouvel objet qui est une copie de cette instance.
+ ///
+ public override object Clone()
+ {
+ RtspMessage returnValue = GetRtspMessage(this.Command);
+
+ foreach (var item in this.Headers)
+ {
+ if (item.Value == null)
+ returnValue.Headers.Add(item.Key.Clone() as string, null);
+ else
+ returnValue.Headers.Add(item.Key.Clone() as string, item.Value.Clone() as string);
+ }
+ returnValue.Data = this.Data.Clone() as byte[];
+ returnValue.SourcePort = this.SourcePort;
+
+ return returnValue;
+ }
+
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequest.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequest.cs
new file mode 100644
index 0000000..04baa63
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequest.cs
@@ -0,0 +1,191 @@
+using System;
+using System.Diagnostics;
+
+namespace Inspectron.HawkEye.RTSP.Messages
+{
+ ///
+ /// An Rtsp Request
+ ///
+ public class RtspRequest : RtspMessage
+ {
+
+ ///
+ /// Request type.
+ ///
+ public enum RequestType
+ {
+ UNKNOWN,
+ DESCRIBE,
+ ANNOUNCE,
+ GET_PARAMETER,
+ OPTIONS,
+ PAUSE,
+ PLAY,
+ RECORD,
+ REDIRECT,
+ SETUP,
+ SET_PARAMETER,
+ TEARDOWN,
+ }
+
+ ///
+ /// Parses the request command.
+ ///
+ /// A string request command.
+ /// The typed request.
+ internal static RequestType ParseRequest(string aStringRequest)
+ {
+ RequestType returnValue;
+ if (!Enum.TryParse(aStringRequest, true, out returnValue))
+ returnValue = RequestType.UNKNOWN;
+ return returnValue;
+ }
+
+ ///
+ /// Gets the Rtsp request.
+ ///
+ /// A request parts.
+ /// the parsed request
+ internal static RtspMessage GetRtspRequest(string[] aRequestParts)
+ {
+ //
+ Debug.Assert(aRequestParts != (string[])null, "aRequestParts");
+ Debug.Assert(aRequestParts.Length != 0, "aRequestParts.Length == 0");
+ //
+ // we already know this is a Request
+ RtspRequest returnValue;
+ switch (ParseRequest(aRequestParts[0]))
+ {
+ case RequestType.OPTIONS:
+ returnValue = new RtspRequestOptions();
+ break;
+ case RequestType.DESCRIBE:
+ returnValue = new RtspRequestDescribe();
+ break;
+ case RequestType.SETUP:
+ returnValue = new RtspRequestSetup();
+ break;
+ case RequestType.PLAY:
+ returnValue = new RtspRequestPlay();
+ break;
+ case RequestType.PAUSE:
+ returnValue = new RtspRequestPause();
+ break;
+ case RequestType.TEARDOWN:
+ returnValue = new RtspRequestTeardown();
+ break;
+ case RequestType.GET_PARAMETER:
+ returnValue = new RtspRequestGetParameter();
+ break;
+ case RequestType.ANNOUNCE:
+ returnValue = new RtspRequestAnnounce();
+ break;
+ case RequestType.RECORD:
+ returnValue = new RtspRequestRecord();
+ break;
+ /*
+ case RequestType.REDIRECT:
+ break;
+
+ case RequestType.SET_PARAMETER:
+ break;
+ */
+ case RequestType.UNKNOWN:
+ default:
+ returnValue = new RtspRequest();
+ break;
+ }
+
+
+
+ return returnValue;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public RtspRequest()
+ {
+ Command = "OPTIONS * RTSP/1.0";
+ }
+
+ ///
+ /// Gets the request.
+ ///
+ /// The request in string format.
+ public string Request
+ {
+ get
+ {
+ return commandArray[0];
+ }
+ }
+
+ ///
+ /// Gets the request.
+ /// The return value is typed with if the value is not
+ /// reconise the value is sent. The string value can be get by
+ ///
+ /// The request.
+ public RequestType RequestTyped
+ {
+ get
+ {
+ return ParseRequest(commandArray[0]);
+ }
+ set
+ {
+ if (Enum.IsDefined(typeof(RequestType), value))
+ commandArray[0] = value.ToString();
+ else
+ commandArray[0] = RequestType.UNKNOWN.ToString();
+ }
+ }
+
+ private Uri _RtspUri;
+ ///
+ /// Gets or sets the Rtsp asked URI.
+ ///
+ /// The Rtsp asked URI.
+ /// The request with uri * is return with null URI
+ public Uri RtspUri
+ {
+ get
+ {
+ if (commandArray.Length < 2 || commandArray[1]=="*")
+ return null;
+ if (_RtspUri == null)
+ Uri.TryCreate(commandArray[1], UriKind.Absolute, out _RtspUri);
+ return _RtspUri;
+ }
+ set
+ {
+ _RtspUri = value;
+ if (commandArray.Length < 2)
+ {
+ Array.Resize(ref commandArray, 3);
+ }
+ commandArray[1] = (value != null ? value.ToString().TrimEnd('/') : "*");
+ }
+ }
+
+ ///
+ /// Gets the assiociate OK response with the request.
+ ///
+ /// an Rtsp response correcponding to request.
+ public virtual RtspResponse CreateResponse()
+ {
+ RtspResponse returnValue = new RtspResponse();
+ returnValue.ReturnCode = 200;
+ returnValue.CSeq = this.CSeq;
+ if (this.Headers.ContainsKey(RtspHeaderNames.Session))
+ {
+ returnValue.Headers[RtspHeaderNames.Session] = this.Headers[RtspHeaderNames.Session];
+ }
+
+ return returnValue;
+ }
+
+ public Object ContextData { get; set; }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestAnnounce.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestAnnounce.cs
new file mode 100644
index 0000000..0983c22
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestAnnounce.cs
@@ -0,0 +1,12 @@
+namespace Inspectron.HawkEye.RTSP.Messages
+{
+ public class RtspRequestAnnounce : RtspRequest
+ {
+ // constructor
+
+ public RtspRequestAnnounce()
+ {
+ Command = "ANNOUNCE * RTSP/1.0";
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestDescribe.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestDescribe.cs
new file mode 100644
index 0000000..0c6a779
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestDescribe.cs
@@ -0,0 +1,13 @@
+namespace Inspectron.HawkEye.RTSP.Messages
+{
+ public class RtspRequestDescribe : RtspRequest
+ {
+
+ // constructor
+
+ public RtspRequestDescribe()
+ {
+ Command = "DESCRIBE * RTSP/1.0";
+ }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestGetParameter.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestGetParameter.cs
new file mode 100644
index 0000000..3702362
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestGetParameter.cs
@@ -0,0 +1,12 @@
+namespace Inspectron.HawkEye.RTSP.Messages
+{
+ public class RtspRequestGetParameter : RtspRequest
+ {
+
+ // Constructor
+ public RtspRequestGetParameter()
+ {
+ Command = "GET_PARAMETER * RTSP/1.0";
+ }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestOptions.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestOptions.cs
new file mode 100644
index 0000000..25e7422
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestOptions.cs
@@ -0,0 +1,28 @@
+namespace Inspectron.HawkEye.RTSP.Messages
+{
+ public class RtspRequestOptions : RtspRequest
+ {
+
+ // Constructor
+ public RtspRequestOptions()
+ {
+ Command = "OPTIONS * RTSP/1.0";
+ }
+
+ ///
+ /// Gets the assiociate OK response with the request.
+ ///
+ ///
+ /// an Rtsp response corresponding to request.
+ ///
+ public override RtspResponse CreateResponse()
+ {
+ RtspResponse response = base.CreateResponse();
+ // Add genric suported operations.
+ response.Headers.Add(RtspHeaderNames.Public, "OPTIONS,DESCRIBE,ANNOUNCE,SETUP,PLAY,PAUSE,TEARDOWN,GET_PARAMETER,SET_PARAMETER,REDIRECT");
+
+ return response;
+ }
+
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestPause.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestPause.cs
new file mode 100644
index 0000000..6100ad7
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestPause.cs
@@ -0,0 +1,12 @@
+namespace Inspectron.HawkEye.RTSP.Messages
+{
+ public class RtspRequestPause : RtspRequest
+ {
+
+ // Constructor
+ public RtspRequestPause()
+ {
+ Command = "PAUSE * RTSP/1.0";
+ }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestPlay.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestPlay.cs
new file mode 100644
index 0000000..a8699dd
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestPlay.cs
@@ -0,0 +1,12 @@
+namespace Inspectron.HawkEye.RTSP.Messages
+{
+ public class RtspRequestPlay : RtspRequest
+ {
+
+ // Constructor
+ public RtspRequestPlay()
+ {
+ Command = "PLAY * RTSP/1.0";
+ }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestRecord.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestRecord.cs
new file mode 100644
index 0000000..2363b9e
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestRecord.cs
@@ -0,0 +1,10 @@
+namespace Inspectron.HawkEye.RTSP.Messages
+{
+ public class RtspRequestRecord : RtspRequest
+ {
+ public RtspRequestRecord()
+ {
+ Command = "RECORD * RTSP/1.0";
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestSetup.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestSetup.cs
new file mode 100644
index 0000000..40ba4c7
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestSetup.cs
@@ -0,0 +1,43 @@
+using System;
+
+namespace Inspectron.HawkEye.RTSP.Messages
+{
+ public class RtspRequestSetup : RtspRequest
+ {
+
+ // Constructor
+ public RtspRequestSetup()
+ {
+ Command = "SETUP * RTSP/1.0";
+ }
+
+
+ ///
+ /// Gets the transports associate with the request.
+ ///
+ /// The transport.
+ public RtspTransport[] GetTransports()
+ {
+
+ if (!Headers.ContainsKey(RtspHeaderNames.Transport))
+ return new RtspTransport[] { new RtspTransport() };
+
+ string[] items = Headers[RtspHeaderNames.Transport].Split(',');
+ return Array.ConvertAll(items,
+ new Converter(RtspTransport.Parse));
+
+ }
+
+ public void AddTransport(RtspTransport newTransport)
+ {
+ string actualTransport = string.Empty;
+ if(Headers.ContainsKey(RtspHeaderNames.Transport))
+ actualTransport = Headers[RtspHeaderNames.Transport] + ",";
+ Headers[RtspHeaderNames.Transport] = actualTransport + newTransport.ToString();
+
+
+
+ }
+
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestTeardown.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestTeardown.cs
new file mode 100644
index 0000000..940743a
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestTeardown.cs
@@ -0,0 +1,12 @@
+namespace Inspectron.HawkEye.RTSP.Messages
+{
+ public class RtspRequestTeardown : RtspRequest
+ {
+
+ // Constructor
+ public RtspRequestTeardown()
+ {
+ Command = "TEARDOWN * RTSP/1.0";
+ }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPResponse.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPResponse.cs
new file mode 100644
index 0000000..b54d354
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPResponse.cs
@@ -0,0 +1,228 @@
+using System;
+using System.Globalization;
+using System.Linq;
+
+namespace Inspectron.HawkEye.RTSP.Messages
+{
+ public class RtspResponse : RtspMessage
+ {
+ public const int DEFAULT_TIMEOUT = 60;
+
+ ///
+ /// Gets the default error message for an error code.
+ ///
+ /// An error code.
+ /// The default error message associate
+ private static string GetDefaultError(int aErrorCode)
+ {
+ switch (aErrorCode)
+ {
+
+ case 100: return "Continue";
+
+ case 200: return "OK";
+ case 201: return "Created";
+ case 250: return "Low on Storage Space";
+
+ case 300: return "Multiple Choices";
+ case 301: return "Moved Permanently";
+ case 302: return "Moved Temporarily";
+ case 303: return "See Other";
+ case 305: return "Use Proxy";
+
+ case 400: return "Bad Request";
+ case 401: return "Unauthorized";
+ case 402: return "Payment Required";
+ case 403: return "Forbidden";
+ case 404: return "Not Found";
+ case 405: return "Method Not Allowed";
+ case 406: return "Not Acceptable";
+ case 407: return "Proxy Authentication Required";
+ case 408: return "Request Timeout";
+ case 410: return "Gone";
+ case 411: return "Length Required";
+ case 412: return "Precondition Failed";
+ case 413: return "Request Entity Too Large";
+ case 414: return "Request-URI Too Long";
+ case 415: return "Unsupported Media Type";
+ case 451: return "Invalid parameter";
+ case 452: return "Illegal Conference Identifier";
+ case 453: return "Not Enough Bandwidth";
+ case 454: return "Session Not Found";
+ case 455: return "Method Not Valid In This State";
+ case 456: return "Header Field Not Valid";
+ case 457: return "Invalid Range";
+ case 458: return "Parameter Is Read-Only";
+ case 459: return "Aggregate Operation Not Allowed";
+ case 460: return "Only Aggregate Operation Allowed";
+ case 461: return "Unsupported Transport";
+ case 462: return "Destination Unreachable";
+
+ case 500: return "Internal Server Error";
+ case 501: return "Not Implemented";
+ case 502: return "Bad Gateway";
+ case 503: return "Service Unavailable";
+ case 504: return "Gateway Timeout";
+ case 505: return "RTSP Version Not Supported";
+ case 551: return "Option not support";
+ default:
+ return "Return: " + aErrorCode.ToString(CultureInfo.InvariantCulture);
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public RtspResponse()
+ : base()
+ {
+ // Initialise with a default result code.
+ Command = "RTSP/1.0 200 OK";
+ }
+
+ private int _returnCode;
+ ///
+ /// Gets or sets the return code of the response.
+ ///
+ /// The return code.
+ /// On change the error message is set to the default one associate with the code
+ public int ReturnCode
+ {
+ get
+ {
+ if (_returnCode == 0 && commandArray.Length >= 2)
+ {
+ int.TryParse(commandArray[1], out _returnCode);
+ }
+
+ return _returnCode;
+ }
+ set
+ {
+ if (ReturnCode != value)
+ {
+ _returnCode = value;
+ // make sure we have the room
+ if (commandArray.Length < 3)
+ {
+ Array.Resize(ref commandArray, 3);
+ }
+ commandArray[1] = value.ToString(CultureInfo.InvariantCulture);
+ commandArray[2] = GetDefaultError(value);
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the error/return message.
+ ///
+ /// The return message.
+ public string ReturnMessage
+ {
+ get
+ {
+ if (commandArray.Length < 3)
+ return String.Empty;
+ return commandArray[2];
+ }
+ set
+ {
+ // Make sure we have the room
+ if (commandArray.Length < 3)
+ {
+ Array.Resize(ref commandArray, 3);
+ }
+ commandArray[2] = value;
+
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether this instance correspond to an OK response.
+ ///
+ /// true if this instance is OK; otherwise, false.
+ public bool IsOk
+ {
+ get
+ {
+ if (ReturnCode > 0 && ReturnCode < 400)
+ return true;
+ return false;
+ }
+ }
+
+ ///
+ /// Gets the timeout in second.
+ /// The default timeout is 60.
+ ///
+ /// The timeout.
+ public int Timeout
+ {
+ get
+ {
+ int returnValue = DEFAULT_TIMEOUT;
+ if (Headers.ContainsKey(RtspHeaderNames.Session))
+ {
+ string[] parts = Headers[RtspHeaderNames.Session].Split(';');
+ if (parts.Length > 1)
+ {
+ string[] subParts = parts[1].Split('=');
+ if (subParts.Length > 1 &&
+ subParts[0].ToUpperInvariant() == "TIMEOUT")
+ if (!int.TryParse(subParts[1], out returnValue))
+ returnValue = DEFAULT_TIMEOUT;
+ }
+ }
+ return returnValue;
+ }
+ set
+ {
+ if(Headers.ContainsKey(RtspHeaderNames.Session))
+ if (value != DEFAULT_TIMEOUT)
+ {
+
+ Headers[RtspHeaderNames.Session] = Headers[RtspHeaderNames.Session].Split(';').First()
+ + ";timeout=" + value.ToString(CultureInfo.InvariantCulture);
+ }
+ else
+ {
+ //remove timeout part
+ Headers[RtspHeaderNames.Session] = Headers[RtspHeaderNames.Session].Split(';').First();
+ }
+ }
+ }
+
+ ///
+ /// Gets the session ID.
+ ///
+ /// The session ID.
+ public override string Session
+ {
+ get
+ {
+ if (!Headers.ContainsKey(RtspHeaderNames.Session))
+ return null;
+
+ return Headers[RtspHeaderNames.Session].Split(';')[0];
+ }
+ set
+ {
+ if(Timeout != DEFAULT_TIMEOUT)
+ {
+ Headers[RtspHeaderNames.Session] = value + ";timeout=" + Timeout.ToString(CultureInfo.InvariantCulture);
+ }
+ else
+ {
+ Headers[RtspHeaderNames.Session] = value;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the original request associate with the response.
+ ///
+ /// The original request.
+ public RtspRequest OriginalRequest
+ { get; set; }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPTransport.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPTransport.cs
new file mode 100644
index 0000000..a758ce8
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPTransport.cs
@@ -0,0 +1,367 @@
+using System;
+using System.Diagnostics.Contracts;
+using System.Text;
+
+namespace Inspectron.HawkEye.RTSP.Messages
+{
+ public class RtspTransport
+ {
+ public RtspTransport()
+ {
+ // Default value is true in RFC
+ IsMulticast = true;
+ LowerTransport = LowerTransportType.UDP;
+ Mode = "PLAY";
+ }
+ /*
+RFC
+Transport = "Transport" ":"
+ 1\#transport-spec
+transport-spec = transport-protocol/profile[/lower-transport]
+ *parameter
+transport-protocol = "RTP"
+profile = "AVP"
+lower-transport = "TCP" | "UDP"
+parameter = ( "unicast" | "multicast" )
+ | ";" "destination" [ "=" address ]
+ | ";" "interleaved" "=" channel [ "-" channel ]
+ | ";" "append"
+ | ";" "ttl" "=" ttl
+ | ";" "layers" "=" 1*DIGIT
+ | ";" "port" "=" port [ "-" port ]
+ | ";" "client_port" "=" port [ "-" port ]
+ | ";" "server_port" "=" port [ "-" port ]
+ | ";" "ssrc" "=" ssrc
+ | ";" "mode" = <"> 1\#mode <">
+ttl = 1*3(DIGIT)
+port = 1*5(DIGIT)
+ssrc = 8*8(HEX)
+channel = 1*3(DIGIT)
+address = host
+mode = <"> *Method <"> | Method
+
+*/
+ ///
+ /// List of transport
+ ///
+ [Serializable]
+ public enum TransportType
+ {
+ ///
+ /// RTP for now
+ ///
+ RTP,
+ }
+
+ ///
+ /// Profile type
+ ///
+ [Serializable]
+ public enum ProfileType
+ {
+ ///
+ /// RTP/AVP of now
+ ///
+ AVP,
+ }
+
+ ///
+ /// Transport type.
+ ///
+ [Serializable]
+ public enum LowerTransportType
+ {
+ ///
+ /// UDP transport.
+ ///
+ UDP,
+ ///
+ /// TCP transport.
+ ///
+ TCP,
+ }
+
+
+ ///
+ /// Gets or sets the transport.
+ ///
+ /// The transport.
+ public TransportType Transport { get; set; }
+ ///
+ /// Gets or sets the profile.
+ ///
+ /// The profile.
+ public ProfileType Profile { get; set; }
+ ///
+ /// Gets or sets the lower transport.
+ ///
+ /// The lower transport.
+ public LowerTransportType LowerTransport { get; set; }
+ ///
+ /// Gets or sets a value indicating whether this instance is multicast.
+ ///
+ ///
+ /// true if this instance is multicast; otherwise, false.
+ ///
+ public bool IsMulticast { get; set; }
+ ///
+ /// Gets or sets the destination.
+ ///
+ /// The destination.
+ public string Destination { get; set; }
+ ///
+ /// Gets or sets the source.
+ ///
+ /// The source.
+ public string Source { get; set; }
+ ///
+ /// Gets or sets the interleaved.
+ ///
+ /// The interleaved.
+ public PortCouple Interleaved { get; set; }
+ ///
+ /// Gets or sets a value indicating whether this instance is append.
+ ///
+ /// true if this instance is append; otherwise, false.
+ public bool IsAppend { get; set; }
+ ///
+ /// Gets or sets the TTL.
+ ///
+ /// The TTL.
+ public int TTL { get; set; }
+ ///
+ /// Gets or sets the layers.
+ ///
+ /// The layers.
+ public int Layers { get; set; }
+ ///
+ /// Gets or sets the port.
+ ///
+ /// The port.
+ public PortCouple Port { get; set; }
+ ///
+ /// Gets or sets the client port.
+ ///
+ /// The client port.
+ public PortCouple ClientPort { get; set; }
+ ///
+ /// Gets or sets the server port.
+ ///
+ /// The server port.
+ public PortCouple ServerPort { get; set; }
+ ///
+ /// Gets or sets the S SRC.
+ ///
+ /// The S SRC.
+ public string SSrc { get; set; }
+ ///
+ /// Gets or sets the mode.
+ ///
+ /// The mode.
+ public string Mode { get; set; }
+
+ ///
+ /// Parses the specified transport string.
+ ///
+ /// A transport string.
+ /// The transport class.
+ /// is null.
+ public static RtspTransport Parse(string aTransportString)
+ {
+ if (aTransportString == null)
+ throw new ArgumentNullException("aTransportString");
+ Contract.EndContractBlock();
+
+ RtspTransport returnValue = new RtspTransport();
+
+ string[] transportPart = aTransportString.Split(';');
+ string[] transportProtocolPart = transportPart[0].Split('/');
+
+ ReadTransport(returnValue, transportProtocolPart);
+ ReadProfile(returnValue, transportProtocolPart);
+ ReadLowerTransport(returnValue, transportProtocolPart);
+
+ foreach (string part in transportPart)
+ {
+ string[] subPart = part.Split('=');
+
+ switch (subPart[0].ToUpperInvariant())
+ {
+ case "UNICAST":
+ returnValue.IsMulticast = false;
+ break;
+ case "MULTICAST":
+ returnValue.IsMulticast = true;
+ break;
+ case "DESTINATION":
+ if (subPart.Length == 2)
+ returnValue.Destination = subPart[1];
+ break;
+ case "SOURCE":
+ if (subPart.Length == 2)
+ returnValue.Source = subPart[1];
+ break;
+ case "INTERLEAVED":
+ returnValue.IsMulticast = false;
+ if (subPart.Length < 2)
+ throw new ArgumentException("interleaved value invalid", "aTransportString");
+
+ returnValue.Interleaved = PortCouple.Parse(subPart[1]);
+ break;
+ case "APPEND":
+ returnValue.IsAppend = true;
+ break;
+ case "TTL":
+ int ttl = 0;
+ if (subPart.Length < 2 || !int.TryParse(subPart[1], out ttl))
+ throw new ArgumentException("TTL value invalid", "aTransportString");
+ returnValue.TTL = ttl;
+ break;
+ case "LAYERS":
+ int layers = 0;
+ if (subPart.Length < 2 || !int.TryParse(subPart[1], out layers))
+ throw new ArgumentException("Layers value invalid", "aTransportString");
+ returnValue.TTL = layers;
+ break;
+ case "PORT":
+ if (subPart.Length < 2)
+ throw new ArgumentException("Port value invalid", "aTransportString");
+ returnValue.Port = PortCouple.Parse(subPart[1]);
+ break;
+ case "CLIENT_PORT":
+ if (subPart.Length < 2)
+ throw new ArgumentException("client_port value invalid", "aTransportString");
+ returnValue.ClientPort = PortCouple.Parse(subPart[1]);
+ break;
+ case "SERVER_PORT":
+ if (subPart.Length < 2)
+ throw new ArgumentException("server_port value invalid", "aTransportString");
+ returnValue.ServerPort = PortCouple.Parse(subPart[1]);
+ break;
+ case "SSRC":
+ if (subPart.Length < 2)
+ throw new ArgumentException("ssrc value invalid", "aTransportString");
+ returnValue.SSrc = subPart[1];
+ break;
+ case "MODE":
+ if (subPart.Length < 2)
+ throw new ArgumentException("mode value invalid", "aTransportString");
+ returnValue.Mode = subPart[1];
+ break;
+ default:
+ // TODO log invalid part
+ break;
+ }
+ }
+ return returnValue;
+ }
+
+ private static void ReadLowerTransport(RtspTransport returnValue, string[] transportProtocolPart)
+ {
+ if (transportProtocolPart.Length == 3)
+ {
+ LowerTransportType lowerTransport;
+ if (!Enum.TryParse(transportProtocolPart[2], out lowerTransport))
+ throw new ArgumentException("Lower transport type invalid", "aTransportString");
+ returnValue.LowerTransport = lowerTransport;
+ }
+ }
+
+ private static void ReadProfile(RtspTransport returnValue, string[] transportProtocolPart)
+ {
+ ProfileType profile;
+ if (transportProtocolPart.Length < 2 || !Enum.TryParse(transportProtocolPart[1], out profile))
+ throw new ArgumentException("Transport profile type invalid", "aTransportString");
+ returnValue.Profile = profile;
+ }
+
+ private static void ReadTransport(RtspTransport returnValue, string[] transportProtocolPart)
+ {
+ TransportType transport;
+ if (!Enum.TryParse(transportProtocolPart[0], out transport))
+ throw new ArgumentException("Transport type invalid", "aTransportString");
+ returnValue.Transport = transport;
+ }
+
+ ///
+ /// Returns a that represents this instance.
+ ///
+ ///
+ /// A that represents this instance.
+ ///
+ public override string ToString()
+ {
+ StringBuilder transportString = new StringBuilder();
+ transportString.Append(Transport.ToString());
+ transportString.Append('/');
+ transportString.Append(Profile.ToString());
+ transportString.Append('/');
+ transportString.Append(LowerTransport.ToString());
+ if (LowerTransport == LowerTransportType.TCP)
+ {
+ transportString.Append(";unicast");
+ }
+ if (LowerTransport == LowerTransportType.UDP)
+ {
+ transportString.Append(';');
+ transportString.Append(IsMulticast ? "multicast" : "unicast");
+ }
+ if (Destination != null)
+ {
+ transportString.Append(";destination=");
+ transportString.Append(Destination);
+ }
+ if (Source != null)
+ {
+ transportString.Append(";source=");
+ transportString.Append(Source);
+ }
+ if (Interleaved != null)
+ {
+ transportString.Append(";interleaved=");
+ transportString.Append(Interleaved.ToString());
+ }
+ if (IsAppend)
+ {
+ transportString.Append(";append");
+ }
+ if (TTL > 0)
+ {
+ transportString.Append(";ttl=");
+ transportString.Append(TTL);
+ }
+ if (Layers > 0)
+ {
+ transportString.Append(";layers=");
+ transportString.Append(Layers);
+ }
+ if (Port != null)
+ {
+ transportString.Append(";port=");
+ transportString.Append(Port.ToString());
+ }
+ if (ClientPort != null)
+ {
+ transportString.Append(";client_port=");
+ transportString.Append(ClientPort.ToString());
+ }
+ if (ServerPort != null)
+ {
+ transportString.Append(";server_port=");
+ transportString.Append(ServerPort.ToString());
+ }
+ if (SSrc != null)
+ {
+ transportString.Append(";ssrc=");
+ transportString.Append(SSrc);
+ }
+ if (Mode != null && Mode != "PLAY")
+ {
+ transportString.Append(";mode=");
+ transportString.Append(Mode);
+ }
+ return transportString.ToString();
+ }
+
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/RTSPListener.cs b/framework/Inspectron.HawkEye/RTSP/RTSPListener.cs
new file mode 100644
index 0000000..0dc1bb7
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/RTSPListener.cs
@@ -0,0 +1,557 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Globalization;
+using System.IO;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using Inspectron.HawkEye.RTSP.Messages;
+
+namespace Inspectron.HawkEye.RTSP
+{
+ ///
+ /// Rtsp lister
+ ///
+ public class RtspListener : IDisposable
+ {
+ private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();
+
+ private IRtspTransport _transport;
+
+ private Thread _listenTread;
+ private Stream _stream;
+
+ private int _sequenceNumber;
+
+ private Dictionary _sentMessage = new Dictionary();
+
+ ///
+ /// Initializes a new instance of the class from a TCP connection.
+ ///
+ /// The connection.
+ public RtspListener(IRtspTransport connection)
+ {
+ if (connection == null)
+ throw new ArgumentNullException("connection");
+ Contract.EndContractBlock();
+
+ _transport = connection;
+ _stream = connection.GetStream();
+ }
+
+ ///
+ /// Gets the remote address.
+ ///
+ /// The remote adress.
+ public string RemoteAdress
+ {
+ get
+ {
+ return _transport.RemoteAddress;
+ }
+ }
+
+ ///
+ /// Starts this instance.
+ ///
+ public void Start()
+ {
+ _listenTread = new Thread(new ThreadStart(DoJob));
+ _listenTread.Name = "DoJob";
+ _listenTread.Start();
+ }
+
+ ///
+ /// Stops this instance.
+ ///
+ public void Stop()
+ {
+ // brutally close the TCP socket....
+ // I hope the teardown was sent elsewhere
+ _transport.Close();
+
+ }
+
+ ///
+ /// Enable auto reconnect.
+ ///
+ public bool AutoReconnect { get; set; }
+
+ ///
+ /// Occurs when message is received.
+ ///
+ public event EventHandler MessageReceived;
+
+ ///
+ /// Raises the event.
+ ///
+ /// The instance containing the event data.
+ protected void OnMessageReceived(RtspChunkEventArgs e)
+ {
+ EventHandler handler = MessageReceived;
+
+ if (handler != null)
+ handler(this, e);
+ }
+
+ ///
+ /// Occurs when Data is received.
+ ///
+ public event EventHandler DataReceived;
+
+ ///
+ /// Raises the event.
+ ///
+ /// The instance containing the event data.
+ protected void OnDataReceived(RtspChunkEventArgs rtspChunkEventArgs)
+ {
+ EventHandler handler = DataReceived;
+
+ if (handler != null)
+ handler(this, rtspChunkEventArgs);
+ }
+
+ ///
+ /// Does the reading job.
+ ///
+ ///
+ /// This method read one message from TCP connection.
+ /// If it a response it add the associate question.
+ /// The stopping is made by the closing of the TCP connection.
+ ///
+ private void DoJob()
+ {
+ try
+ {
+ _logger.Debug("Connection Open");
+ while (_transport.Connected)
+ {
+ // La lectuer est blocking sauf si la connection est coupé
+ RtspChunk currentMessage = ReadOneMessage(_stream);
+
+ if (currentMessage != null)
+ {
+ if (!(currentMessage is RtspData))
+ {
+ // on logue le tout
+ if (currentMessage.SourcePort != null)
+ _logger.Debug(CultureInfo.InvariantCulture, "Receive from {0}", currentMessage.SourcePort.RemoteAdress);
+ currentMessage.LogMessage();
+ }
+ if (currentMessage is RtspResponse)
+ {
+
+ RtspResponse response = currentMessage as RtspResponse;
+ lock (_sentMessage)
+ {
+ // add the original question to the response.
+ RtspRequest originalRequest;
+ if (_sentMessage.TryGetValue(response.CSeq, out originalRequest))
+ {
+ _sentMessage.Remove(response.CSeq);
+ response.OriginalRequest = originalRequest;
+ }
+ else
+ {
+ _logger.Warn(CultureInfo.InvariantCulture, "Receive response not asked {0}", response.CSeq);
+ }
+ }
+ OnMessageReceived(new RtspChunkEventArgs(response));
+
+ }
+ else if (currentMessage is RtspRequest)
+ {
+ OnMessageReceived(new RtspChunkEventArgs(currentMessage));
+ }
+ else if (currentMessage is RtspData)
+ {
+ OnDataReceived(new RtspChunkEventArgs(currentMessage));
+ }
+
+ }
+ else
+ {
+ _stream.Close();
+ _transport.Close();
+ }
+ }
+ }
+ catch (IOException error)
+ {
+ _logger.Warn("IO Error", error);
+ _stream.Close();
+ _transport.Close();
+ }
+ catch (SocketException error)
+ {
+ _logger.Warn("Socket Error", error);
+ _stream.Close();
+ _transport.Close();
+ }
+ catch (ObjectDisposedException error)
+ {
+ _logger.Warn("Object Disposed", error);
+ }
+ catch (Exception error)
+ {
+ _logger.Warn("Unknow Error", error);
+// throw;
+ }
+
+ _logger.Debug("Connection Close");
+ }
+
+ [Serializable]
+ private enum ReadingState
+ {
+ NewCommand,
+ Headers,
+ Data,
+ End,
+ InterleavedData,
+ MoreInterleavedData,
+ }
+
+ ///
+ /// Sends the message.
+ ///
+ /// A message.
+ /// if it is Ok, otherwise
+ public bool SendMessage(RtspMessage message)
+ {
+ if (message == null)
+ throw new ArgumentNullException("message");
+ Contract.EndContractBlock();
+
+ if (!_transport.Connected)
+ {
+ if(!AutoReconnect)
+ return false;
+
+ _logger.Warn("Reconnect to a client, strange !!");
+ try
+ {
+ Reconnect();
+ }
+ catch (SocketException)
+ {
+ // on a pas put se connecter on dit au manager de plus compter sur nous
+ return false;
+ }
+ }
+
+ // if it it a request we store the original message
+ // and we renumber it.
+ //TODO handle lost message (for example every minute cleanup old message)
+ if (message is RtspRequest)
+ {
+ RtspMessage originalMessage = message;
+ // Do not modify original message
+ message = message.Clone() as RtspMessage;
+ _sequenceNumber++;
+ message.CSeq = _sequenceNumber;
+ lock (_sentMessage)
+ {
+ _sentMessage.Add(message.CSeq, originalMessage as RtspRequest);
+ }
+ }
+
+ _logger.Debug("Send Message");
+ message.LogMessage();
+ message.SendTo(_stream);
+ return true;
+ }
+
+ ///
+ /// Reconnect this instance of RtspListener.
+ ///
+ /// Error during socket
+ public void Reconnect()
+ {
+ //if it is already connected do not reconnect
+ if (_transport.Connected)
+ return;
+
+ // If it is not connected listenthread should have die.
+ if (_listenTread != null && _listenTread.IsAlive)
+ _listenTread.Join();
+
+ if (_stream != null)
+ _stream.Dispose();
+
+ // reconnect
+ _transport.Reconnect();
+ _stream = _transport.GetStream();
+
+ // If listen thread exist restart it
+ if (_listenTread != null)
+ Start();
+ }
+
+ ///
+ /// Reads one message.
+ ///
+ /// The Rtsp stream.
+ /// Message readen
+ public RtspChunk ReadOneMessage(Stream commandStream)
+ {
+ if (commandStream == null)
+ throw new ArgumentNullException("commandStream");
+ Contract.EndContractBlock();
+
+ ReadingState currentReadingState = ReadingState.NewCommand;
+ // current decode message , create a fake new to permit compile.
+ RtspChunk currentMessage = null;
+
+ int size = 0;
+ int byteReaden = 0;
+ List buffer = new List(256);
+ string oneLine = String.Empty;
+ while (currentReadingState != ReadingState.End)
+ {
+
+ // if the system is not reading binary data.
+ if (currentReadingState != ReadingState.Data && currentReadingState != ReadingState.MoreInterleavedData)
+ {
+ oneLine = String.Empty;
+ bool needMoreChar = true;
+ // I do not know to make readline blocking
+ while (needMoreChar)
+ {
+ int currentByte = commandStream.ReadByte();
+
+ switch (currentByte)
+ {
+ case -1:
+ // the read is blocking, so if we got -1 it is because the client close;
+ currentReadingState = ReadingState.End;
+ needMoreChar = false;
+ break;
+ case '\n':
+ oneLine = ASCIIEncoding.UTF8.GetString(buffer.ToArray());
+ buffer.Clear();
+ needMoreChar = false;
+ break;
+ case '\r':
+ // simply ignore this
+ break;
+ case '$': // if first caracter of packet is $ it is an interleaved data packet
+ if (currentReadingState == ReadingState.NewCommand && buffer.Count == 0)
+ {
+ currentReadingState = ReadingState.InterleavedData;
+ needMoreChar = false;
+ }
+ else
+ goto default;
+ break;
+ default:
+ buffer.Add((byte)currentByte);
+ break;
+ }
+ }
+ }
+
+ switch (currentReadingState)
+ {
+ case ReadingState.NewCommand:
+ currentMessage = RtspMessage.GetRtspMessage(oneLine);
+ currentReadingState = ReadingState.Headers;
+ break;
+ case ReadingState.Headers:
+ string line = oneLine;
+ if (string.IsNullOrEmpty(line))
+ {
+ currentReadingState = ReadingState.Data;
+ ((RtspMessage)currentMessage).InitialiseDataFromContentLength();
+ }
+ else
+ {
+ ((RtspMessage)currentMessage).AddHeader(line);
+ }
+ break;
+ case ReadingState.Data:
+ if (currentMessage.Data.Length > 0)
+ {
+ // Read the remaning data
+ int byteCount = commandStream.Read(currentMessage.Data, byteReaden,
+ currentMessage.Data.Length - byteReaden);
+ if (byteCount <= 0) {
+ currentReadingState = ReadingState.End;
+ break;
+ }
+ byteReaden += byteCount;
+ _logger.Debug(CultureInfo.InvariantCulture, "Readen {0} byte of data", byteReaden);
+ }
+ // if we haven't read all go there again else go to end.
+ if (byteReaden >= currentMessage.Data.Length)
+ currentReadingState = ReadingState.End;
+ break;
+ case ReadingState.InterleavedData:
+ currentMessage = new RtspData();
+ int channelByte = commandStream.ReadByte();
+ if (channelByte == -1) {
+ currentReadingState = ReadingState.End;
+ break;
+ }
+ ((RtspData)currentMessage).Channel = channelByte;
+
+ int sizeByte1 = commandStream.ReadByte();
+ if (sizeByte1 == -1) {
+ currentReadingState = ReadingState.End;
+ break;
+ }
+ int sizeByte2 = commandStream.ReadByte();
+ if (sizeByte2 == -1) {
+ currentReadingState = ReadingState.End;
+ break;
+ }
+ size = (sizeByte1 << 8) + sizeByte2;
+ currentMessage.Data = new byte[size];
+ currentReadingState = ReadingState.MoreInterleavedData;
+ break;
+ case ReadingState.MoreInterleavedData:
+ // apparently non blocking
+ {
+ int byteCount = commandStream.Read(currentMessage.Data, byteReaden, size - byteReaden);
+ if (byteCount <= 0) {
+ currentReadingState = ReadingState.End;
+ break;
+ }
+ byteReaden += byteCount;
+ if (byteReaden < size)
+ currentReadingState = ReadingState.MoreInterleavedData;
+ else
+ currentReadingState = ReadingState.End;
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ if (currentMessage != null)
+ currentMessage.SourcePort = this;
+ return currentMessage;
+ }
+
+ ///
+ /// Begins the send data.
+ ///
+ /// A Rtsp data.
+ /// The async callback.
+ /// A state.
+ public IAsyncResult BeginSendData(RtspData aRtspData, AsyncCallback asyncCallback, object state)
+ {
+ if (aRtspData == null)
+ throw new ArgumentNullException("aRtspData");
+ Contract.EndContractBlock();
+
+ return BeginSendData(aRtspData.Channel, aRtspData.Data, asyncCallback, state);
+ }
+
+ ///
+ /// Begins the send data.
+ ///
+ /// The channel.
+ /// The frame.
+ /// The async callback.
+ /// A state.
+ public IAsyncResult BeginSendData(int channel, byte[] frame, AsyncCallback asyncCallback, object state)
+ {
+ if (frame == null)
+ throw new ArgumentNullException("frame");
+ if (frame.Length > 0xFFFF)
+ throw new ArgumentException("frame too large", "frame");
+ Contract.EndContractBlock();
+
+ if (!_transport.Connected)
+ {
+ if(!AutoReconnect)
+ return null; // cannot write when transport is disconnected
+
+ _logger.Warn("Reconnect to a client, strange !!");
+ Reconnect();
+ }
+
+ byte[] data = new byte[4 + frame.Length]; // add 4 bytes for the header
+ data[0] = 36; // '$' character
+ data[1] = (byte)channel;
+ data[2] = (byte)((frame.Length & 0xFF00) >> 8);
+ data[3] = (byte)((frame.Length & 0x00FF));
+ System.Array.Copy(frame,0,data,4,frame.Length);
+ return _stream.BeginWrite(data, 0, data.Length, asyncCallback, state);
+ }
+
+ ///
+ /// Ends the send data.
+ ///
+ /// The result.
+ public void EndSendData(IAsyncResult result)
+ {
+ try
+ {
+ _stream.EndWrite(result);
+ } catch (Exception e)
+ {
+ // Error, for example stream has already been Disposed
+ _logger.Debug("Error during end send (can be ignored) " + e);
+ result = null;
+ }
+ }
+
+ ///
+ /// Send data (Synchronous)
+ ///
+ /// The channel.
+ /// The frame.
+ public void SendData(int channel, byte[] frame)
+ {
+ if (frame == null)
+ throw new ArgumentNullException("frame");
+ if (frame.Length > 0xFFFF)
+ throw new ArgumentException("frame too large", "frame");
+ Contract.EndContractBlock();
+
+ if (!_transport.Connected)
+ {
+ if(!AutoReconnect)
+ throw new Exception("Connection is lost");
+
+ _logger.Warn("Reconnect to a client, strange !!");
+ Reconnect();
+ }
+
+ byte[] data = new byte[4 + frame.Length]; // add 4 bytes for the header
+ data[0] = 36; // '$' character
+ data[1] = (byte)channel;
+ data[2] = (byte)((frame.Length & 0xFF00) >> 8);
+ data[3] = (byte)((frame.Length & 0x00FF));
+ System.Array.Copy(frame, 0, data, 4, frame.Length);
+ lock (_stream) {
+ _stream.Write(data, 0, data.Length);
+ }
+ }
+
+
+ #region IDisposable Membres
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ Stop();
+ if (_stream != null)
+ _stream.Dispose();
+
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/RTSPMessageEventArgs.cs b/framework/Inspectron.HawkEye/RTSP/RTSPMessageEventArgs.cs
new file mode 100644
index 0000000..ffdeb6f
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/RTSPMessageEventArgs.cs
@@ -0,0 +1,27 @@
+using System;
+using Inspectron.HawkEye.RTSP.Messages;
+
+namespace Inspectron.HawkEye.RTSP
+{
+ ///
+ /// Event args containing information for message events.
+ ///
+ public class RtspChunkEventArgs :EventArgs
+ {
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A message.
+ public RtspChunkEventArgs(RtspChunk aMessage)
+ {
+ Message = aMessage;
+ }
+
+ ///
+ /// Gets or sets the message.
+ ///
+ /// The message.
+ public RtspChunk Message { get; set; }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/RTSPTCPTransport.cs b/framework/Inspectron.HawkEye/RTSP/RTSPTCPTransport.cs
new file mode 100644
index 0000000..4296dfe
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/RTSPTCPTransport.cs
@@ -0,0 +1,118 @@
+using System;
+using System.Diagnostics.Contracts;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+
+namespace Inspectron.HawkEye.RTSP
+{
+ ///
+ /// TCP Connection for Rtsp
+ ///
+ public class RtspTcpTransport : IRtspTransport, IDisposable
+ {
+ private IPEndPoint _currentEndPoint;
+ private TcpClient _RtspServerClient;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The underlying TCP connection.
+ public RtspTcpTransport(TcpClient tcpConnection)
+ {
+ if (tcpConnection == null)
+ throw new ArgumentNullException("tcpConnection");
+ Contract.EndContractBlock();
+
+ _currentEndPoint = (IPEndPoint)tcpConnection.Client.RemoteEndPoint;
+ _RtspServerClient = tcpConnection;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A host.
+ /// A port number.
+ public RtspTcpTransport(string aHost, int aPortNumber)
+ : this(new TcpClient(aHost, aPortNumber))
+ {
+ }
+
+
+ #region IRtspTransport Membres
+
+ ///
+ /// Gets the stream of the transport.
+ ///
+ /// A stream
+ public Stream GetStream()
+ {
+ return _RtspServerClient.GetStream();
+ }
+
+ ///
+ /// Gets the remote address.
+ ///
+ /// The remote address.
+ public string RemoteAddress
+ {
+ get
+ {
+ return string.Format(CultureInfo.InvariantCulture,"{0}:{1}", _currentEndPoint.Address, _currentEndPoint.Port);
+ }
+ }
+
+ ///
+ /// Closes this instance.
+ ///
+ public void Close()
+ {
+ Dispose(true);
+ }
+
+ ///
+ /// Gets a value indicating whether this is connected.
+ ///
+ /// true if connected; otherwise, false.
+ public bool Connected
+ {
+ get { return _RtspServerClient.Client != null && _RtspServerClient.Connected; }
+ }
+
+ ///
+ /// Reconnect this instance.
+ /// Must do nothing if already connected.
+ ///
+ /// Error during socket
+ public void Reconnect()
+ {
+ if (Connected)
+ return;
+ _RtspServerClient = new TcpClient();
+ _RtspServerClient.Connect(_currentEndPoint);
+ }
+
+ #endregion
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _RtspServerClient.Close();
+ /* // free managed resources
+ if (managedResource != null)
+ {
+ managedResource.Dispose();
+ managedResource = null;
+ }*/
+ }
+ }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/RTSPUtils.cs b/framework/Inspectron.HawkEye/RTSP/RTSPUtils.cs
new file mode 100644
index 0000000..34650e2
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/RTSPUtils.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace Inspectron.HawkEye.RTSP
+{
+ public static class RtspUtils
+ {
+ ///
+ /// Registers the URI.
+ ///
+ public static void RegisterUri()
+ {
+ if (!UriParser.IsKnownScheme("rtsp"))
+ UriParser.Register(new HttpStyleUriParser(), "rtsp", 554);
+ }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/Attribut.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/Attribut.cs
new file mode 100644
index 0000000..2b39cc2
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Sdp/Attribut.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Linq;
+
+namespace Inspectron.HawkEye.RTSP.Sdp
+{
+ public class Attribut
+ {
+ private static readonly Dictionary attributMap = new Dictionary()
+ {
+ {AttributRtpMap.NAME,typeof(AttributRtpMap)},
+ {AttributFmtp.NAME,typeof(AttributFmtp)},
+ };
+
+
+ public virtual string Key { get; private set; }
+ public virtual string Value { get; protected set; }
+
+ public static void RegisterNewAttributeType(string key, Type attributType)
+ {
+ if(!attributType.IsSubclassOf(typeof(Attribut)))
+ throw new ArgumentException("Type must be subclass of Rtsp.Sdp.Attribut","attributType");
+
+ attributMap[key] = attributType;
+ }
+
+
+
+ public Attribut()
+ {
+ }
+
+ public Attribut(string key)
+ {
+ Key = key;
+ }
+
+
+ public static Attribut ParseInvariant(string value)
+ {
+ if(value == null)
+ throw new ArgumentNullException("value");
+
+ Contract.EndContractBlock();
+
+ var listValues = value.Split(new char[] {':'}, 2);
+
+
+ Attribut returnValue;
+
+ // Call parser of child type
+ Type childType;
+ attributMap.TryGetValue(listValues[0], out childType);
+ if (childType != null)
+ {
+ var defaultContructor = childType.GetConstructor(Type.EmptyTypes);
+ returnValue = defaultContructor.Invoke(Type.EmptyTypes) as Attribut;
+ }
+ else
+ {
+ returnValue = new Attribut(listValues[0]);
+ }
+ // Parse the value. Note most attributes have a value but recvonly does not have a value
+ if (listValues.Count() > 1) returnValue.ParseValue(listValues[1]);
+
+ return returnValue;
+ }
+
+ protected virtual void ParseValue(string value)
+ {
+ Value = value;
+ }
+
+
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/AttributFmtp.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/AttributFmtp.cs
new file mode 100644
index 0000000..4af8384
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Sdp/AttributFmtp.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Inspectron.HawkEye.RTSP.Sdp
+{
+ public class AttributFmtp : Attribut
+ {
+ public const string NAME = "fmtp";
+
+ private Dictionary parameters = new Dictionary();
+
+ public AttributFmtp()
+ {
+ }
+
+ public override string Key
+ {
+ get
+ {
+ return NAME;
+ }
+ }
+
+ public override string Value
+ {
+ get
+ {
+ return string.Format("{0} {1}", PayloadNumber, FormatParameter);
+ }
+ protected set
+ {
+ ParseValue(value);
+ }
+ }
+
+ public int PayloadNumber { get; set; }
+
+ // temporary aatibute to store remaning data not parsed
+ public string FormatParameter { get; set; }
+
+
+ // Extract the Payload Number and the Format Parameters
+ protected override void ParseValue(string value)
+ {
+ var parts = value.Split(new char[] { ' ' }, 2);
+
+ int payloadNumber;
+ if(int.TryParse(parts[0], out payloadNumber))
+ {
+ this.PayloadNumber = payloadNumber;
+ }
+ if(parts.Length > 1)
+ {
+ FormatParameter = parts[1];
+
+ // Split on ';' to get a list of items.
+ // Then Trim each item and then Split on the first '='
+ // Add them to the dictionary
+ parameters.Clear();
+ foreach (var pair in parts[1].Split(';').Select(x => x.Trim().Split(new char[] { '=' }, 2))) {
+ if (!string.IsNullOrWhiteSpace(pair[0]))
+ parameters[pair[0]] = pair.Length > 1 ? pair[1] : null;
+ }
+ }
+ }
+
+ public String GetParameter(String index)
+ {
+ if (parameters.ContainsKey(index)) return parameters[index];
+ else return "";
+ }
+
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/AttributRtpMap.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/AttributRtpMap.cs
new file mode 100644
index 0000000..845d851
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Sdp/AttributRtpMap.cs
@@ -0,0 +1,76 @@
+using System;
+
+namespace Inspectron.HawkEye.RTSP.Sdp
+{
+ public class AttributRtpMap : Attribut
+ {
+ // Format
+ // rtpmap: / [/]
+ // Examples
+ // rtpmap:96 H264/90000
+ // rtpmap:8 PCMA/8000
+
+ public const string NAME = "rtpmap";
+
+ public AttributRtpMap()
+ {
+ }
+
+ public override string Key
+ {
+ get
+ {
+ return NAME;
+ }
+ }
+
+ public override string Value
+ {
+ get
+ {
+ if(string.IsNullOrEmpty(EncodingParameters))
+ {
+ return string.Format("{0} {1}/{2}", PayloadNumber, EncodingName, ClockRate);
+ } else {
+ return string.Format("{0} {1}/{2}/{3}", PayloadNumber, EncodingName, ClockRate, EncodingParameters);
+ }
+ }
+ protected set
+ {
+ ParseValue(value);
+ }
+ }
+
+ public int PayloadNumber { get; set; }
+ public String EncodingName { get; set; }
+ public String ClockRate { get; set; }
+ public String EncodingParameters { get; set; }
+
+ protected override void ParseValue(string value)
+ {
+ var parts = value.Split(new char[] { ' ', '/' });
+
+ if (parts.Length >= 1) {
+ int tmp_payloadNumber;
+ if (int.TryParse(parts[0], out tmp_payloadNumber))
+ {
+ PayloadNumber = tmp_payloadNumber;
+ }
+ }
+ if (parts.Length >= 2)
+ {
+ EncodingName = parts[1];
+ }
+ if (parts.Length >= 3)
+ {
+ ClockRate = parts[2];
+ }
+ if (parts.Length >= 4)
+ {
+ EncodingParameters = parts[3];
+ }
+
+
+ }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/Bandwidth.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/Bandwidth.cs
new file mode 100644
index 0000000..1675a7e
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Sdp/Bandwidth.cs
@@ -0,0 +1,15 @@
+namespace Inspectron.HawkEye.RTSP.Sdp
+{
+ public class Bandwidth
+ {
+ public Bandwidth()
+ {
+ }
+
+ internal static Bandwidth Parse(string value)
+ {
+ //TODO really parse.
+ return new Bandwidth();
+ }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/Connection.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/Connection.cs
new file mode 100644
index 0000000..853699f
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Sdp/Connection.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Globalization;
+
+namespace Inspectron.HawkEye.RTSP.Sdp
+{
+ public abstract class Connection
+ {
+ public Connection()
+ {
+ //Default value from spec
+ NumberOfAddress = 1;
+ }
+
+ public string Host { get; set; }
+
+ ///
+ /// Gets or sets the number of address specifed in connection.
+ ///
+ /// The number of address.
+ //TODO handle it a different way (list of adress ?)
+ public int NumberOfAddress { get; set; }
+
+ public static Connection Parse(string value)
+ {
+ if(value ==null)
+ throw new ArgumentNullException("value");
+
+ string[] parts = value.Split(' ');
+
+ if (parts.Length != 3)
+ throw new FormatException("Value do not contain 3 parts as needed.");
+
+ if (parts[0] != "IN")
+ throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "Net type {0} not suported", parts[0]));
+
+ switch (parts[1])
+ {
+ case "IP4":
+ return ConnectionIP4.Parse(parts[2]);
+ case "IP6":
+ return ConnectionIP6.Parse(parts[2]);
+ default:
+ throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "Address type {0} not suported", parts[1]));
+ }
+
+ }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/ConnectionIP4.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/ConnectionIP4.cs
new file mode 100644
index 0000000..47c237c
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Sdp/ConnectionIP4.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Globalization;
+
+namespace Inspectron.HawkEye.RTSP.Sdp
+{
+ public class ConnectionIP4 : Connection
+ {
+
+ public int Ttl { get; set; }
+
+ internal new static ConnectionIP4 Parse(string ipAddress)
+ {
+ string[] parts = ipAddress.Split('/');
+
+ if (parts.Length > 3)
+ throw new FormatException("Too much address subpart in " + ipAddress);
+
+ ConnectionIP4 result = new ConnectionIP4();
+
+ result.Host = parts[0];
+
+ int ttl;
+ if (parts.Length > 1)
+ {
+ if (!int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out ttl))
+ throw new FormatException("Invalid TTL format : " + parts[1]);
+ result.Ttl = ttl;
+ }
+ int numberOfAddress;
+ if (parts.Length > 2)
+ {
+ if (!int.TryParse(parts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out numberOfAddress))
+ throw new FormatException("Invalid number of address : " + parts[2]);
+ result.NumberOfAddress = numberOfAddress;
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/ConnectionIP6.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/ConnectionIP6.cs
new file mode 100644
index 0000000..bb09c77
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Sdp/ConnectionIP6.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Globalization;
+
+namespace Inspectron.HawkEye.RTSP.Sdp
+{
+ public class ConnectionIP6 : Connection
+ {
+ internal new static ConnectionIP6 Parse(string ipAddress)
+ {
+ string[] parts = ipAddress.Split('/');
+
+ if (parts.Length > 2)
+ throw new FormatException("Too much address subpart in " + ipAddress);
+
+ ConnectionIP6 result = new ConnectionIP6();
+
+ result.Host = parts[0];
+
+ int numberOfAddress;
+ if (parts.Length > 1)
+ {
+ if (!int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out numberOfAddress))
+ throw new FormatException("Invalid number of address : " + parts[1]);
+ result.NumberOfAddress = numberOfAddress;
+ }
+
+ return result;
+
+ }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/EncriptionKey.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/EncriptionKey.cs
new file mode 100644
index 0000000..505a24e
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Sdp/EncriptionKey.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Diagnostics.Contracts;
+
+namespace Inspectron.HawkEye.RTSP.Sdp
+{
+ public class EncriptionKey
+ {
+ public EncriptionKey(string p)
+ {
+ }
+
+ public static EncriptionKey ParseInvariant(string value)
+ {
+ if (value == null)
+ throw new ArgumentNullException("value");
+
+ Contract.EndContractBlock();
+
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/H264Parameter.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/H264Parameter.cs
new file mode 100644
index 0000000..a2e9206
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Sdp/H264Parameter.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Inspectron.HawkEye.RTSP.Sdp
+{
+ public class H264Parameters : IDictionary
+ {
+ private readonly Dictionary parameters = new Dictionary();
+
+ public List SpropParameterSets
+ {
+ get
+ {
+ List result = new List();
+
+ if (ContainsKey("sprop-parameter-sets")&& this["sprop-parameter-sets"] != null)
+ {
+ result.AddRange(this["sprop-parameter-sets"].Split(',').Select(x => Convert.FromBase64String(x)));
+ }
+
+ return result;
+ }
+ }
+
+ public static H264Parameters Parse(String parameterString)
+ {
+ var result = new H264Parameters();
+ foreach (var pair in parameterString.Split(';').Select(x => x.Trim().Split(new char[] { '=' }, 2)))
+ {
+ if(!string.IsNullOrWhiteSpace(pair[0]))
+ result[pair[0]] = pair.Length > 1 ? pair[1] : null;
+ }
+ return result;
+ }
+
+ public override string ToString()
+ {
+ return parameters.Select(p => p.Key + (p.Value != null ? "=" + p.Value : string.Empty)).Aggregate((x, y) => x + ";" + y);
+ }
+
+ public String this[String index]
+ {
+ get { return parameters[index]; }
+ set { parameters[index] = value; }
+ }
+
+ public int Count
+ {
+ get
+ {
+ return parameters.Count;
+ }
+ }
+
+ public bool IsReadOnly
+ {
+ get
+ {
+ return ((IDictionary)parameters).IsReadOnly;
+ }
+ }
+
+ public ICollection Keys
+ {
+ get
+ {
+ return ((IDictionary)parameters).Keys;
+ }
+ }
+
+ public ICollection Values
+ {
+ get
+ {
+ return ((IDictionary)parameters).Values;
+ }
+ }
+
+ public void Add(KeyValuePair item)
+ {
+ ((IDictionary)parameters).Add(item);
+ }
+
+ public void Add(string key, string value)
+ {
+ parameters.Add(key, value);
+ }
+
+ public void Clear()
+ {
+ parameters.Clear();
+ }
+
+ public bool Contains(KeyValuePair item)
+ {
+ return ((IDictionary)parameters).Contains(item);
+ }
+
+ public bool ContainsKey(string key)
+ {
+ return parameters.ContainsKey(key);
+ }
+
+ public void CopyTo(KeyValuePair[] array, int arrayIndex)
+ {
+ ((IDictionary)parameters).CopyTo(array, arrayIndex);
+ }
+
+ public IEnumerator> GetEnumerator()
+ {
+ return ((IDictionary)parameters).GetEnumerator();
+ }
+
+ public bool Remove(KeyValuePair item)
+ {
+ return ((IDictionary)parameters).Remove(item);
+ }
+
+ public bool Remove(string key)
+ {
+ return parameters.Remove(key);
+ }
+
+ public bool TryGetValue(string key, out string value)
+ {
+ return parameters.TryGetValue(key, out value);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IDictionary)parameters).GetEnumerator();
+ }
+ }
+}
diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/H265Parameter.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/H265Parameter.cs
new file mode 100644
index 0000000..23a993f
--- /dev/null
+++ b/framework/Inspectron.HawkEye/RTSP/Sdp/H265Parameter.cs
@@ -0,0 +1,149 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+// Parse 'fmtp' attribute in SDP
+// Extract H265 fields
+// By Roger Hardiman, RJH Technical Consultancy Ltd
+
+namespace Inspectron.HawkEye.RTSP.Sdp
+{
+ public class H265Parameters : IDictionary
+ {
+ private readonly Dictionary parameters = new Dictionary();
+
+ public List SpropParameterSets
+ {
+ get
+ {
+ List result = new List();
+
+ if (ContainsKey("sprop-vps")&& this["sprop-vps"] != null)
+ {
+ result.AddRange(this["sprop-vps"].Split(',').Select(x => Convert.FromBase64String(x)));
+ }
+
+ if (ContainsKey("sprop-sps") && this["sprop-sps"] != null)
+ {
+ result.AddRange(this["sprop-sps"].Split(',').Select(x => Convert.FromBase64String(x)));
+ }
+
+ if (ContainsKey("sprop-pps") && this["sprop-pps"] != null)
+ {
+ result.AddRange(this["sprop-pps"].Split(',').Select(x => Convert.FromBase64String(x)));
+ }
+ return result;
+ }
+ }
+
+ public static H265Parameters Parse(String parameterString)
+ {
+ var result = new H265Parameters();
+ foreach (var pair in parameterString.Split(';').Select(x => x.Trim().Split(new char[] { '=' }, 2)))
+ {
+ if(!string.IsNullOrWhiteSpace(pair[0]))
+ result[pair[0]] = pair.Length > 1 ? pair[1] : null;
+ }
+ return result;
+ }
+
+ public override string ToString()
+ {
+ return parameters.Select(p => p.Key + (p.Value != null ? "=" + p.Value : string.Empty)).Aggregate((x, y) => x + ";" + y);
+ }
+
+ public String this[String index]
+ {
+ get { return parameters[index]; }
+ set { parameters[index] = value; }
+ }
+
+ public int Count
+ {
+ get
+ {
+ return parameters.Count;
+ }
+ }
+
+ public bool IsReadOnly
+ {
+ get
+ {
+ return ((IDictionary)parameters).IsReadOnly;
+ }
+ }
+
+ public ICollection Keys
+ {
+ get
+ {
+ return ((IDictionary)parameters).Keys;
+ }
+ }
+
+ public ICollection Values
+ {
+ get
+ {
+ return ((IDictionary)parameters).Values;
+ }
+ }
+
+ public void Add(KeyValuePair item)
+ {
+ ((IDictionary)parameters).Add(item);
+ }
+
+ public void Add(string key, string value)
+ {
+ parameters.Add(key, value);
+ }
+
+ public void Clear()
+ {
+ parameters.Clear();
+ }
+
+ public bool Contains(KeyValuePair item)
+ {
+ return ((IDictionary)parameters).Contains(item);
+ }
+
+ public bool ContainsKey(string key)
+ {
+ return parameters.ContainsKey(key);
+ }
+
+ public void CopyTo(KeyValuePair[] array, int arrayIndex)
+ {
+ ((IDictionary)parameters).CopyTo(array, arrayIndex);
+ }
+
+ public IEnumerator> GetEnumerator()
+ {
+ return ((IDictionary)parameters).GetEnumerator();
+ }
+
+ public bool Remove(KeyValuePair