diff --git a/GenICam/GenICam.csproj b/GenICam/GenICam.csproj index c20ce05..d842100 100644 --- a/GenICam/GenICam.csproj +++ b/GenICam/GenICam.csproj @@ -4,13 +4,14 @@ net5.0;net6.0;net7.0;net8.0;net10.0 x64 true - 2.3.1 + 2.3.4-local.1 MIT - - 2.3.1: Added support for .net10.0 + + 2.3.4: Local build + 2.3.1: Added support for .net10.0 2.3: Improved XML parsing - 2.2: Version Update - 2.1.5.1: Updated Docs + 2.2: Version Update + 2.1.5.1: Updated Docs diff --git a/GenICam/GenICam_BACKUP_1879.csproj b/GenICam/GenICam_BACKUP_1879.csproj new file mode 100644 index 0000000..ca3a10f --- /dev/null +++ b/GenICam/GenICam_BACKUP_1879.csproj @@ -0,0 +1,30 @@ + + + + net5.0;net6.0;net7.0;net8.0;net10.0 + x64 + true +<<<<<<< HEAD + 2.3.1 + MIT + + 2.3.1: Added support for .net10.0 +======= + 2.3.4-local + MIT + + 2.3.4: Local build +>>>>>>> a89bb8a (Refactor StreamReceiverParallel for improved frame handling and buffer management) + 2.3: Improved XML parsing + 2.2: Version Update + 2.1.5.1: Updated Docs + + + + + + + + + + \ No newline at end of file diff --git a/GenICam/GenICam_BASE_1879.csproj b/GenICam/GenICam_BASE_1879.csproj new file mode 100644 index 0000000..22ffc0d --- /dev/null +++ b/GenICam/GenICam_BASE_1879.csproj @@ -0,0 +1,25 @@ + + + + net5.0;net6.0;net7.0;net8.0 + x64 + true + 2.3 + MIT + + 2.3: Improved XML parsing + 2.2: Version Update + 2.1.5.1: Updated Docs + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/GenICam/GenICam_LOCAL_1879.csproj b/GenICam/GenICam_LOCAL_1879.csproj new file mode 100644 index 0000000..c20ce05 --- /dev/null +++ b/GenICam/GenICam_LOCAL_1879.csproj @@ -0,0 +1,23 @@ + + + + net5.0;net6.0;net7.0;net8.0;net10.0 + x64 + true + 2.3.1 + MIT + + 2.3.1: Added support for .net10.0 + 2.3: Improved XML parsing + 2.2: Version Update + 2.1.5.1: Updated Docs + + + + + + + + + + \ No newline at end of file diff --git a/GenICam/GenICam_REMOTE_1879.csproj b/GenICam/GenICam_REMOTE_1879.csproj new file mode 100644 index 0000000..2f9df74 --- /dev/null +++ b/GenICam/GenICam_REMOTE_1879.csproj @@ -0,0 +1,26 @@ + + + + net5.0;net6.0;net7.0;net8.0 + x64 + true + 2.3.4-local + MIT + + 2.3.4: Local build + 2.3: Improved XML parsing + 2.2: Version Update + 2.1.5.1: Updated Docs + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/GenICam/Interfaces/Helpers/IDoubleValue.cs b/GenICam/Interfaces/Helpers/IDoubleValue.cs new file mode 100644 index 0000000..c585ed7 --- /dev/null +++ b/GenICam/Interfaces/Helpers/IDoubleValue.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; + +namespace GenICam +{ + /// + /// Interface for values that can be represented as doubles. + /// + public interface IDoubleValue + { + /// + /// Gets the value as a double. + /// + /// The value as a double when available. + public Task GetDoubleValueAsync(); + + /// + /// Sets the value as a double. + /// + /// The value to set. + /// The reply packet from the underlying write path. + public Task SetDoubleValueAsync(double value); + } +} \ No newline at end of file diff --git a/GenICam/Models/Converter.cs b/GenICam/Models/Converter.cs index 2c99004..8e321e6 100644 --- a/GenICam/Models/Converter.cs +++ b/GenICam/Models/Converter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Threading.Tasks; @@ -8,7 +9,7 @@ namespace GenICam /// /// Converter class. /// - public class Converter : IMathematical + public class Converter : IMathematical, IDoubleValue { private double value; @@ -72,7 +73,8 @@ public double Value /// The value as a long. public async Task GetValueAsync() { - return (long)(await ExecuteFormulaFrom()); + var result = await GetDoubleValueAsync().ConfigureAwait(false); + return result is null ? null : (long?)Math.Round(result.Value, MidpointRounding.AwayFromZero); } /// @@ -82,45 +84,61 @@ public double Value /// A representing the result of the asynchronous operation.. public async Task SetValueAsync(long value) { - var toValue = await ExecuteFormulaTo(value); - return await PValue.SetValueAsync(toValue); + return await SetDoubleValueAsync(value).ConfigureAwait(false); + } + + /// + public async Task GetDoubleValueAsync() + { + var result = await ExecuteFormulaFrom().ConfigureAwait(false); + Value = result; + return result; + } + + /// + public async Task SetDoubleValueAsync(double value) + { + var toValue = await ExecuteFormulaTo(value).ConfigureAwait(false); + Value = value; + return await PValue.SetValueAsync(toValue).ConfigureAwait(false); } private async Task ExecuteFormulaFrom() { try { - foreach (var word in FormulaFrom.Split()) + var formula = FormulaFrom; + foreach (var word in formula.Split()) { if (word.Equals("TO")) { - long? value = await PValue.GetValueAsync(); + long? value = await PValue.GetValueAsync().ConfigureAwait(false); if (value is null) { throw new GenICamException("Failed to read formula register value", new NullReferenceException()); } - FormulaFrom = FormulaFrom.Replace(word, string.Format("0x{0:X8}", value)); + formula = formula.Replace(word, value.Value.ToString(CultureInfo.InvariantCulture)); } - if (PVariables.ContainsKey(word)) + if (PVariables != null && PVariables.ContainsKey(word)) { long? value = null; - value = await PVariables[word].GetValueAsync(); + value = await PVariables[word].GetValueAsync().ConfigureAwait(false); if (value is null) { throw new GenICamException("Failed to read formula register value", new NullReferenceException()); } - FormulaFrom = FormulaFrom.Replace(word, string.Format("0x{0:X8}", value)); + formula = formula.Replace(word, value.Value.ToString(CultureInfo.InvariantCulture)); } } - FormulaFrom = MathParserHelper.FormatExpression(FormulaFrom); - return MathParserHelper.CalculateExpression(FormulaFrom); + formula = MathParserHelper.FormatExpression(formula); + return MathParserHelper.CalculateExpression(formula); } catch (Exception ex) { @@ -128,33 +146,34 @@ private async Task ExecuteFormulaFrom() } } - private async Task ExecuteFormulaTo(long value) + private async Task ExecuteFormulaTo(double value) { try { - foreach (var word in FormulaTo.Split()) + var formula = FormulaTo; + foreach (var word in formula.Split()) { if (word.Equals("FROM")) { - FormulaTo = FormulaTo.Replace(word, string.Format("0x{0:X8}", value)); + formula = formula.Replace(word, value.ToString(CultureInfo.InvariantCulture)); } - if (PVariables.ContainsKey(word)) + if (PVariables != null && PVariables.ContainsKey(word)) { long? variableValue = null; - variableValue = await PVariables[word].GetValueAsync(); + variableValue = await PVariables[word].GetValueAsync().ConfigureAwait(false); if (variableValue is null) { throw new GenICamException("Failed to read formula register value", new NullReferenceException()); } - FormulaTo = FormulaTo.Replace(word, string.Format("0x{0:X8}", variableValue)); + formula = formula.Replace(word, variableValue.Value.ToString(CultureInfo.InvariantCulture)); } } - FormulaTo = MathParserHelper.FormatExpression(FormulaTo); - return (long)MathParserHelper.CalculateExpression(FormulaTo); + formula = MathParserHelper.FormatExpression(formula); + return (long)Math.Round(MathParserHelper.CalculateExpression(formula), MidpointRounding.AwayFromZero); } catch (Exception ex) { diff --git a/GenICam/Models/GenCategories/GenFloat.cs b/GenICam/Models/GenCategories/GenFloat.cs index be28ba4..d17fa8f 100644 --- a/GenICam/Models/GenCategories/GenFloat.cs +++ b/GenICam/Models/GenCategories/GenFloat.cs @@ -176,9 +176,16 @@ public List GetListOfValidValue() /// Not yet implemented. public async Task GetMaxAsync() { + if (PMax is IDoubleValue doubleMax) + { + var result = await doubleMax.GetDoubleValueAsync().ConfigureAwait(false); + return result ?? Max; + } + if (PMax is not null) { - return (long)(await PMax.GetValueAsync()); + var result = await PMax.GetValueAsync().ConfigureAwait(false); + return result ?? Max; } return Max; @@ -188,9 +195,16 @@ public async Task GetMaxAsync() /// Not yet implemented. public async Task GetMinAsync() { + if (PMin is IDoubleValue doubleMin) + { + var result = await doubleMin.GetDoubleValueAsync().ConfigureAwait(false); + return result ?? Min; + } + if (PMin is not null) { - return (long)(await PMin.GetValueAsync()); + var result = await PMin.GetValueAsync().ConfigureAwait(false); + return result ?? Min; } return Min; @@ -206,16 +220,30 @@ public Representation GetRepresentation() /// Not yet implemented. public string GetUnit() { - throw new NotImplementedException(); + return Unit; } /// public async Task GetValueAsync() { + if (PValue is IDoubleValue doubleValue) + { + var result = await doubleValue.GetDoubleValueAsync().ConfigureAwait(false); + if (result is not null) + { + Value = result.Value; + return (long)Math.Round(result.Value, MidpointRounding.AwayFromZero); + } + } + if (PValue is not null) { - Value = (long)(await PValue.GetValueAsync()); - return (long)Value; + var result = await PValue.GetValueAsync().ConfigureAwait(false); + if (result is not null) + { + Value = result.Value; + return result.Value; + } } throw new GenICamException(message: $"Unable to set the value, missing register reference", new MissingFieldException()); @@ -224,9 +252,16 @@ public string GetUnit() /// public async Task SetValueAsync(long value) { + if (PValue is IDoubleValue doubleValue) + { + Value = value; + return await doubleValue.SetDoubleValueAsync(value).ConfigureAwait(false); + } + if (PValue is IPValue pValue) { - return await pValue.SetValueAsync(value); + Value = value; + return await pValue.SetValueAsync(value).ConfigureAwait(false); } throw new GenICamException(message: $"Unable to set the value, missing register reference", new MissingFieldException()); @@ -254,35 +289,52 @@ public void ImposeMin(double min) /// Not yet implemented. Task IFloat.GetValueAsync() { - throw new NotImplementedException(); + if (PValue is IDoubleValue doubleValue) + { + return doubleValue.GetDoubleValueAsync(); + } + + return GetValueAsDoubleAsync(); } /// - /// Not yet implemented. - public Task SetValueAsync(double value) + public async Task SetValueAsync(double value) { - throw new NotImplementedException(); + if (PValue is IDoubleValue doubleValue) + { + await doubleValue.SetDoubleValueAsync(value).ConfigureAwait(false); + Value = value; + return; + } + + if (PValue is IPValue pValue) + { + await pValue.SetValueAsync((long)Math.Round(value, MidpointRounding.AwayFromZero)).ConfigureAwait(false); + Value = value; + return; + } + + throw new GenICamException(message: $"Unable to set the value, missing register reference", new MissingFieldException()); } /// - /// Not yet implemented. long IFloat.GetDisplayPrecision() { - throw new NotImplementedException(); + return DisplayPrecision; } /// - /// Not yet implemented. public Task ImposeMinAsync(long min) { - throw new NotImplementedException(); + Min = min; + return Task.CompletedTask; } /// - /// Not yet implemented. public Task ImposeMaxAsync(long max) { - throw new NotImplementedException(); + Max = max; + return Task.CompletedTask; } private async void ExecuteSetValueCommand(object value) @@ -302,7 +354,7 @@ private async void ExecuteGetValueCommand() { try { - Value = (long)await GetValueAsync(); + Value = await GetValueAsDoubleAsync().ConfigureAwait(false) ?? Value; RaisePropertyChanged(nameof(Value)); } catch (Exception ex) @@ -310,5 +362,21 @@ private async void ExecuteGetValueCommand() //ToDo: display exception. } } + + private async Task GetValueAsDoubleAsync() + { + if (PValue is IDoubleValue doubleValue) + { + return await doubleValue.GetDoubleValueAsync().ConfigureAwait(false); + } + + if (PValue is not null) + { + var result = await PValue.GetValueAsync().ConfigureAwait(false); + return result; + } + + throw new GenICamException(message: $"Unable to get the value, missing register reference", new MissingFieldException()); + } } } \ No newline at end of file diff --git a/GenICam/Models/IntSwissKnife.cs b/GenICam/Models/IntSwissKnife.cs index 973b4f6..3cfd1d1 100644 --- a/GenICam/Models/IntSwissKnife.cs +++ b/GenICam/Models/IntSwissKnife.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -10,7 +11,7 @@ namespace GenICam /// /// this is a mathematical class for register parameter computations. /// - public class IntSwissKnife : IMathematical + public class IntSwissKnife : IMathematical, IDoubleValue { /// /// Initializes a new instance of the class. @@ -64,7 +65,8 @@ public IntSwissKnife(string formula, Dictionary pVaribles, Dict /// The result as a long. public async Task GetValueAsync() { - return (long)await ExecuteFormula(); + var result = await GetDoubleValueAsync().ConfigureAwait(false); + return result is null ? null : (long?)Math.Round(result.Value, MidpointRounding.AwayFromZero); } /// @@ -77,6 +79,20 @@ public Task SetValueAsync(long value) throw new NotImplementedException(); } + /// + public async Task GetDoubleValueAsync() + { + var result = await ExecuteFormula().ConfigureAwait(false); + Value = result; + return result; + } + + /// + public Task SetDoubleValueAsync(double value) + { + throw new NotImplementedException(); + } + /// /// Calculates the formula and returns the result. /// @@ -85,46 +101,57 @@ private async Task ExecuteFormula() { try { + var formula = Formula; + var resolvedExpressions = Expressions is null ? null : new Dictionary(Expressions); + foreach (var pVariable in PVariables) { - var value = await pVariable.Value.GetValueAsync(); - if (Expressions?.Count > 0) + var value = await pVariable.Value.GetValueAsync().ConfigureAwait(false); + if (value is null) + { + throw new GenICamException("Failed to read expression register", new NullReferenceException()); + } + + var formattedValue = value.Value.ToString(CultureInfo.InvariantCulture); + + if (resolvedExpressions?.Count > 0) { - foreach (var expression in Expressions) + foreach (var expressionKey in resolvedExpressions.Keys.ToList()) { - expression.Value.Replace(pVariable.Key, value.ToString()); + resolvedExpressions[expressionKey] = resolvedExpressions[expressionKey].Replace(pVariable.Key, formattedValue); } } - Formula = Formula.Replace(pVariable.Key, value.ToString()); + formula = formula.Replace(pVariable.Key, formattedValue); } if (Constants?.Count > 0) { foreach (var constant in Constants) { - if (Expressions?.Count > 0) + var formattedConstant = constant.Value.ToString(CultureInfo.InvariantCulture); + if (resolvedExpressions?.Count > 0) { - foreach (var expression in Expressions) + foreach (var expressionKey in resolvedExpressions.Keys.ToList()) { - expression.Value.Replace(constant.Key, constant.Value.ToString()); + resolvedExpressions[expressionKey] = resolvedExpressions[expressionKey].Replace(constant.Key, formattedConstant); } } - Formula = Formula.Replace(constant.Key, constant.Value.ToString()); + formula = formula.Replace(constant.Key, formattedConstant); } } - if (Expressions?.Count > 0) + if (resolvedExpressions?.Count > 0) { - foreach (var expression in Expressions) + foreach (var expression in resolvedExpressions) { - Formula = Formula.Replace(expression.Key, $"({MathParserHelper.CalculateExpression(expression.Value)})"); + formula = formula.Replace(expression.Key, $"({MathParserHelper.CalculateExpression(expression.Value)})"); } } - Formula = MathParserHelper.FormatExpression(Formula); - return (double)MathParserHelper.CalculateExpression(Formula); + formula = MathParserHelper.FormatExpression(formula); + return MathParserHelper.CalculateExpression(formula); } catch (Exception ex) { diff --git a/GenICam/Models/Registers/RegisterBase.cs b/GenICam/Models/Registers/RegisterBase.cs index 4d63176..7c0c768 100644 --- a/GenICam/Models/Registers/RegisterBase.cs +++ b/GenICam/Models/Registers/RegisterBase.cs @@ -100,25 +100,30 @@ public virtual async Task SetValueAsync(long value) { try { - var length = GetLength(); - byte[] pBuffer = new byte[length]; + var length = GetLength(); + byte[] pBuffer = new byte[length]; - switch (length) - { - case 2: - pBuffer = BitConverter.GetBytes((ushort)value); - break; + switch (length) + { + case 2: + pBuffer = BitConverter.GetBytes((ushort)value); + break; - case 4: - pBuffer = BitConverter.GetBytes((int)value); - break; + case 4: + pBuffer = BitConverter.GetBytes((int)value); + break; - case 8: - pBuffer = BitConverter.GetBytes(value); - break; - } + case 8: + pBuffer = BitConverter.GetBytes(value); + break; + } + + if (BitConverter.IsLittleEndian && pBuffer.Length > 1) + { + Array.Reverse(pBuffer); + } - return await SetAsync(pBuffer, length); + return await SetAsync(pBuffer, length); } catch (Exception ex) { diff --git a/GenICam/Services/XmlHelper.cs b/GenICam/Services/XmlHelper.cs index 9958699..833c0a7 100644 --- a/GenICam/Services/XmlHelper.cs +++ b/GenICam/Services/XmlHelper.cs @@ -247,7 +247,7 @@ private async Task GetGenCategory(XmlNode node) break; case nameof(CategoryType.IntConverter): - var ipValue = (IPValue) await GetConverter(node); + var ipValue = (IPValue)await GetConverter(node); genCategory = new GenCategory(null, ipValue); break; @@ -264,8 +264,9 @@ private async Task GetGenCategory(XmlNode node) break; default: var detail = await GetGenericPValue(node); - if (detail != null && detail.PValue != null) { - return (ICategory) detail; + if (detail != null && detail.PValue != null) + { + return (ICategory)detail; } break; @@ -279,7 +280,8 @@ private async Task GetGenCategory(XmlNode node) } } - private async Task GetGenericPValue(XmlNode xmlNode) { + private async Task GetGenericPValue(XmlNode xmlNode) + { try { Dictionary pVariables = new Dictionary(); @@ -333,7 +335,8 @@ private async Task GetGenericPValue(XmlNode xmlNode) { pValue ??= await GetGenCategory(pValueNode) as IPValue; var genCategory = await ReadPMaxAndPmin(pValueNode).ConfigureAwait(false); - if (genCategory != null) { + if (genCategory != null) + { pMin = genCategory.PMin; pMax = genCategory.PMax; } @@ -360,7 +363,6 @@ private async Task GetFloatCategory(XmlNode xmlNode) { var categoryPropreties = GetCategoryProperties(xmlNode); - Dictionary expressions = new Dictionary(); IPValue pValue = null; double min = 0, max = 0, value = 0; long inc = 0; @@ -412,24 +414,17 @@ private async Task GetFloatCategory(XmlNode xmlNode) pNode = ReadPNode(node.InnerText); if (pNode != null) { - if (pNode.Attributes["Name"].Value.EndsWith("Expr") || pNode.Attributes["Name"].Value.EndsWith("Conv")) - { - pValue = await GetFormula(pNode); - } - else if (pNode.Attributes["Name"].Value.EndsWith("_Float") || pNode.Attributes["Name"].Value.EndsWith("_Int") || pNode.Attributes["Name"].Value.EndsWith("_Bit")) + pValue = await PNodeToPValue(pNode).ConfigureAwait(false); + + if ((pMin is null || pMax is null) && pNode.Attributes?["Name"] is XmlAttribute pNodeName) { - var register = await GetRegisterByName(pNode.Attributes["Name"].Value).ConfigureAwait(false); + var register = await GetRegisterByName(pNodeName.Value).ConfigureAwait(false); if (register != null) { - pValue = register.PValue; - pMin = register.PMin; - pMax = register.PMax; + pMin ??= register.PMin; + pMax ??= register.PMax; } } - else if (Enum.GetNames().Contains(pNode.Name)) - { - pValue = await GetRegister(pNode); - } } break; @@ -461,30 +456,13 @@ private async Task GetBooleanCategory(XmlNode xmlNode) { var categoryPropreties = GetCategoryProperties(xmlNode); - Dictionary expressions = new Dictionary(); - IPValue pValue = null; if (SelectSingleNode(xmlNode, NodePValue) is XmlNode pValueNode) { XmlNode pNode = ReadPNode(pValueNode.InnerText); if (pNode != null) { - if (pNode.Attributes["Name"].Value.EndsWith("Expr") || pNode.Attributes["Name"].Value.EndsWith("Conv")) - { - pValue = await GetFormula(pNode); - } - else if (pNode.Attributes["Name"].Value.EndsWith("_Float") || pNode.Attributes["Name"].Value.EndsWith("_Int") || pNode.Attributes["Name"].Value.EndsWith("_Bit")) - { - var register = await GetRegisterByName(pNode.Attributes["Name"].Value).ConfigureAwait(false); - if (register != null) - { - pValue = register.PValue; - } - } - else if (Enum.GetNames().Contains(pNode.Name)) - { - pValue = await GetRegister(pNode); - } + pValue = await PNodeToPValue(pNode).ConfigureAwait(false); } } @@ -544,22 +522,7 @@ private async Task GetEnumerationCategory(XmlNode xmlNode) var pNode = ReadPNode(enumPValue.InnerText); if (pNode != null) { - if (pNode.Attributes["Name"].Value.EndsWith("Expr") || pNode.Attributes["Name"].Value.EndsWith("Conv")) - { - pValue = await GetFormula(pNode); - } - else if (pNode.Attributes["Name"].Value.EndsWith("_Float") || pNode.Attributes["Name"].Value.EndsWith("_Int") || pNode.Attributes["Name"].Value.EndsWith("_Bit")) - { - var register = await GetRegisterByName(pNode.Attributes["Name"].Value).ConfigureAwait(false); - if (register != null) - { - pValue = register.PValue; - } - } - else if (Enum.GetNames().Contains(pNode.Name)) - { - pValue = await GetRegister(pNode); - } + pValue = await PNodeToPValue(pNode).ConfigureAwait(false); } } @@ -732,21 +695,23 @@ private async Task PNodeToPValue(XmlNode pNode) IPValue pValue = null; if (pNode != null) { - if (pNode.Attributes["Name"].Value.EndsWith("Expr") || pNode.Attributes["Name"].Value.EndsWith("Conv")) + if (pNode.Name == "IntSwissKnife" || pNode.Name == "SwissKnife" || pNode.Name == "IntConverter" || pNode.Name == "Converter") { - pValue = await GetFormula(pNode); + pValue = await GetFormula(pNode).ConfigureAwait(false); } - else if (pNode.Attributes["Name"].Value.EndsWith("_Float") || pNode.Attributes["Name"].Value.EndsWith("_Int") || pNode.Attributes["Name"].Value.EndsWith("_Bit")) + else if (Enum.GetNames().Contains(pNode.Name) || pNode.ParentNode.Name == nameof(RegisterType.StructReg)) { - var register = await GetRegisterByName(pNode.Attributes["Name"].Value).ConfigureAwait(false); + pValue = await GetRegister(pNode).ConfigureAwait(false); + } + else if (pNode.Attributes?["Name"] is XmlAttribute nameAttribute) + { + var register = await GetRegisterByName(nameAttribute.Value).ConfigureAwait(false); if (register != null) { pValue = register.PValue; } - } - else if (Enum.GetNames().Contains(pNode.Name) || pNode.ParentNode.Name == nameof(RegisterType.StructReg)) - { - pValue = await GetRegister(pNode); + + pValue ??= await GetGenCategory(pNode).ConfigureAwait(false) as IPValue; } } @@ -773,22 +738,7 @@ private async Task GetCommandCategory(XmlNode xmlNode) if (pNode != null) { - if (pNode.Attributes["Name"].Value.EndsWith("Expr") || pNode.Attributes["Name"].Value.EndsWith("Conv")) - { - pValue = await GetFormula(pNode); - } - else if (pNode.Attributes["Name"].Value.EndsWith("_Float") || pNode.Attributes["Name"].Value.EndsWith("_Int") || pNode.Attributes["Name"].Value.EndsWith("_Bit")) - { - var register = await GetRegisterByName(pNode.Attributes["Name"].Value).ConfigureAwait(false); - if (register != null) - { - pValue = register.PValue; - } - } - else if (Enum.GetNames().Contains(pNode.Name)) - { - pValue = await GetRegister(pNode); - } + pValue = await PNodeToPValue(pNode).ConfigureAwait(false); } return new GenCommand(categoryProperties, commandValue, pValue); @@ -977,26 +927,7 @@ private async Task GetGenInteger(XmlNode xmlNode) var pNode = ReadPNode(node.InnerText); if (pNode != null) { - if (pNode.Attributes["Name"].Value.EndsWith("Expr")) - { - pValue = await GetIntSwissKnife(pNode); - } - else if (pNode.Attributes["Name"].Value.EndsWith("Conv")) - { - pValue = await GetConverter(pNode); - } - else if (pNode.Attributes["Name"].Value.EndsWith("_Float") || pNode.Attributes["Name"].Value.EndsWith("_Int") || pNode.Attributes["Name"].Value.EndsWith("_Bit")) - { - var register = await GetRegisterByName(pNode.Attributes["Name"].Value).ConfigureAwait(false); - if (register != null) - { - pValue = register.PValue; - } - } - else if (Enum.GetNames().Contains(pNode.Name)) - { - pValue = await GetRegister(pNode); - } + pValue = await PNodeToPValue(pNode).ConfigureAwait(false); } break; @@ -1314,8 +1245,21 @@ private XmlNode GetNodeByAttirbuteValue(string elementName = "*", string attirbu xpath += attirbute; } - var node = xmlDocument.SelectSingleNode(xpath, xmlNamespaceManager); - return node; + var nodes = xmlDocument.SelectNodes(xpath, xmlNamespaceManager); + if (nodes == null || nodes.Count == 0) + { + return null; + } + + foreach (XmlNode node in nodes) + { + if (node.Name != "EnumEntry") + { + return node; + } + } + + return nodes[0]; } catch (Exception ex) { diff --git a/GenICam_example.md b/GenICam_example.md new file mode 100644 index 0000000..17ed8b5 --- /dev/null +++ b/GenICam_example.md @@ -0,0 +1,960 @@ +# GenICam and GigE Vision End-to-End Example + +This document is a repository-accurate example for working with a compliant GenICam GigE camera through this project. + +It covers: + +1. Discovering cameras on the network +2. Connecting to a camera +3. Grabbing a single frame +4. Streaming continuously +5. Reading and setting GenICam features +6. Converting raw frame bytes to a displayable image +7. Handling the common GVSP pixel format cases discussed for this repository + +## Important Notes About This Repository + +The root README is slightly out of date for direct camera construction. In the current codebase you do this: + +```csharp +var camera = new Camera(); +camera.IP = "192.168.10.224"; +``` + +not this: + +```csharp +var camera = new Camera("192.168.10.224"); +``` + +Also note: + +1. `StartStreamAsync` already loads the camera XML, synchronizes width, height, offset, and pixel format, takes control, configures the GVSP destination, and starts acquisition. +2. `FrameReady` gives you one complete frame, not a packet. +3. The receiver reuses buffers internally, so copy the frame bytes inside the callback if you need to keep them. +4. On machines with multiple NICs, set `camera.RxIP` to the host NIC that is on the same subnet as the camera. + +## Namespaces Used In The Examples + +```csharp +using GigeVision.Core; +using GigeVision.Core.Enums; +using GigeVision.Core.Services; +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +``` + +If you want Bayer and YUV conversion examples, also add Emgu CV, which is already used elsewhere in this repository: + +```csharp +using Emgu.CV; +using Emgu.CV.CvEnum; +using System.Runtime.InteropServices; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using GvPixelFormat = GigeVision.Core.Enums.PixelFormat; +``` + +## 1. Discover Cameras + +This is the standard entry point. + +```csharp +NetworkService.AllowAppThroughFirewall(); + +var camera = new Camera(); +var devices = await camera.Gvcp.GetAllGigeDevicesInNetworkAsnyc(); + +foreach (var device in devices) +{ + Console.WriteLine($"Camera IP: {device.IP}, Host NIC: {device.NetworkIP}, Model: {device.Model}"); +} +``` + +If a device is found, the `CameraInformation.NetworkIP` value is the host interface this library discovered for that device. That is usually the safest value to assign to `camera.RxIP`. + +## 2. Connect To The First Camera + +```csharp +var camera = new Camera(); +var devices = await camera.Gvcp.GetAllGigeDevicesInNetworkAsnyc(); + +var device = devices.FirstOrDefault() + ?? throw new InvalidOperationException("No GigE camera found."); + +camera.IP = device.IP; +camera.RxIP = device.NetworkIP; + +await camera.SyncParameters(); + +Console.WriteLine($"Connected to {camera.IP}"); +Console.WriteLine($"Width: {camera.Width}"); +Console.WriteLine($"Height: {camera.Height}"); +Console.WriteLine($"PixelFormat: {camera.PixelFormat}"); +``` + +## 3. Grab A Single Frame + +This is the practical "single frame" pattern for this repository. There is no separate one-shot public grab API on `ICamera`; you start the stream, wait for the first completed frame, then stop. + +```csharp +public static async Task GrabSingleFrameAsync(Camera camera, TimeSpan timeout) +{ + var frameTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + void OnFrameReady(object? sender, byte[] frameBytes) + { + var copy = new byte[frameBytes.Length]; + Buffer.BlockCopy(frameBytes, 0, copy, 0, frameBytes.Length); + frameTcs.TrySetResult(copy); + } + + camera.FrameReady += OnFrameReady; + + try + { + bool started = await camera.StartStreamAsync(); + if (!started) + { + throw new InvalidOperationException("StartStreamAsync returned false."); + } + + Task completed = await Task.WhenAny(frameTcs.Task, Task.Delay(timeout)); + if (completed != frameTcs.Task) + { + throw new TimeoutException("Timed out waiting for the first frame."); + } + + return await frameTcs.Task; + } + finally + { + camera.FrameReady -= OnFrameReady; + + if (camera.IsStreaming) + { + await camera.StopStream(); + } + } +} +``` + +Usage: + +```csharp +var camera = new Camera(); +var devices = await camera.Gvcp.GetAllGigeDevicesInNetworkAsnyc(); +var device = devices.First(); + +camera.IP = device.IP; +camera.RxIP = device.NetworkIP; + +byte[] firstFrame = await GrabSingleFrameAsync(camera, TimeSpan.FromSeconds(3)); + +Console.WriteLine($"Bytes received: {firstFrame.Length}"); +Console.WriteLine($"Width: {camera.Width}"); +Console.WriteLine($"Height: {camera.Height}"); +Console.WriteLine($"PixelFormat: {camera.PixelFormat}"); +``` + +## 4. Stream Continuously + +This is the minimal continuous-stream pattern. + +```csharp +var camera = new Camera(); +var devices = await camera.Gvcp.GetAllGigeDevicesInNetworkAsnyc(); +var device = devices.First(); + +camera.IP = device.IP; +camera.RxIP = device.NetworkIP; + +camera.FrameReady += (sender, frameBytes) => +{ + var copy = new byte[frameBytes.Length]; + Buffer.BlockCopy(frameBytes, 0, copy, 0, frameBytes.Length); + + Console.WriteLine($"Frame bytes: {copy.Length}"); +}; + +bool started = await camera.StartStreamAsync(); +Console.WriteLine($"Stream started: {started}"); + +Console.ReadLine(); +await camera.StopStream(); +``` + +## 5. Read And Set GenICam Features + +The `Camera` class provides higher-level helpers that sit on top of the XML-backed GenICam node map. + +### Read Common Features + +```csharp +await camera.SyncParameters(); + +Console.WriteLine(camera.Width); +Console.WriteLine(camera.Height); +Console.WriteLine(camera.OffsetX); +Console.WriteLine(camera.OffsetY); +Console.WriteLine(camera.PixelFormat); +``` + +### Use Dedicated Helpers For Width, Height, And Offset + +These are safer than generic writes because this repository already wraps the control flow for them. + +```csharp +bool resolutionSet = await camera.SetResolutionAsync(1280, 1024); +bool offsetSet = await camera.SetOffsetAsync(0, 0); + +await camera.SyncParameters(); +``` + +### Generic Feature Read And Write + +```csharp +long? widthValue = await camera.GetParameterValue(nameof(RegisterName.Width)); +long widthMin = await camera.GetParameterMinValue(nameof(RegisterName.Width)); +long widthMax = await camera.GetParameterMaxValue(nameof(RegisterName.Width)); + +Console.WriteLine($"Width value: {widthValue}"); +Console.WriteLine($"Width range: {widthMin} - {widthMax}"); +``` + +For Lucid and other cameras that expose engineering values through `Converter` nodes, use the typed overloads instead of forcing the feature through the integer path. For example, in the attached Lucid XML `AcquisitionFrameRate` is a `Float` whose `pValue` points at converter `N108`, and that converter writes the integer `AcquisitionFrameTime` register using `1e6 / FROM`. + +```csharp +bool frameRateEnableSet = await camera.SetCameraParameter("AcquisitionFrameRateEnable", true); +bool frameRateSet = await camera.SetCameraParameter("AcquisitionFrameRate", 1.0); + +bool? frameRateEnabled = await camera.GetParameterValue("AcquisitionFrameRateEnable"); +double? frameRate = await camera.GetParameterValue("AcquisitionFrameRate"); + +Console.WriteLine($"AcquisitionFrameRateEnable: {frameRateEnabled}"); +Console.WriteLine($"AcquisitionFrameRate: {frameRate} Hz"); +``` + +If you drop down to `camera.Gvcp.GetRegister(...)`, treat converter-backed features as `IDoubleValue` or `IFloat` feature categories, not as plain integer registers. + +### Set Pixel Format Through The Generic Helper + +```csharp +bool pixelFormatSet = await camera.SetCameraParameter( + nameof(RegisterName.PixelFormat), + (long)PixelFormat.Mono8); + +await camera.SyncParameters(); +``` + +### Probe Vendor-Specific Features Such As Exposure And Gain + +Feature names like `ExposureTime`, `ExposureTimeAbs`, `ExposureTimeRaw`, `Gain`, and `GainRaw` depend on the camera XML. + +```csharp +static async Task FindFeature(Camera camera, params string[] names) +{ + foreach (string name in names) + { + if (await camera.GetParameter(name) != null) + { + return name; + } + } + + return null; +} + +string? exposureName = await FindFeature(camera, "ExposureTime", "ExposureTimeAbs", "ExposureTimeRaw"); +if (exposureName != null) +{ + long min = await camera.GetParameterMinValue(exposureName); + long max = await camera.GetParameterMaxValue(exposureName); + long target = Math.Clamp(5000, min, max); + + bool ok = await camera.SetCameraParameter(exposureName, target); + long? actual = await camera.GetParameterValue(exposureName); + + Console.WriteLine($"{exposureName}: set={ok}, value={actual}, range=[{min}, {max}]"); +} + +string? gainName = await FindFeature(camera, "Gain", "GainRaw", "AnalogGain", "DigitalGainAll"); +if (gainName != null) +{ + long min = await camera.GetParameterMinValue(gainName); + long max = await camera.GetParameterMaxValue(gainName); + long target = Math.Clamp(10, min, max); + + bool ok = await camera.SetCameraParameter(gainName, target); + long? actual = await camera.GetParameterValue(gainName); + + Console.WriteLine($"{gainName}: set={ok}, value={actual}, range=[{min}, {max}]"); +} +``` + +## 6. Convert Raw Bytes To A Displayable Image + +The conversion path depends on `camera.PixelFormat`. + +### Practical Format Categories + +1. `Mono8`: bytes already represent a grayscale image +2. `RGB8Packed` or `BGR8Packed`: bytes already represent a 24-bit color image +3. `BayerGR8`, `BayerRG8`, `BayerGB8`, `BayerBG8`: raw sensor mosaic, must be demosaiced +4. `Mono10Packed`, `Mono12Packed`: packed mono data, must be unpacked and scaled for display +5. `BayerGR10Packed`, `BayerRG10Packed`, `BayerGB10Packed`, `BayerBG10Packed`, and the 12-bit packed Bayer variants: unpack, scale, then demosaic +6. `Mono10`, `Mono12`, `Mono14`, `Mono16`: unpacked high bit-depth mono, scale to 8-bit for display +7. `BayerGR10`, `BayerRG10`, `BayerGB10`, `BayerBG10`, `BayerGR12`, `BayerRG12`, `BayerGB12`, `BayerBG12`, `BayerGR16`, `BayerRG16`, `BayerGB16`, `BayerBG16`: unpacked high bit-depth Bayer, scale to 8-bit for display, then demosaic +8. `YUV422Packed` and `YUYVPacked`: convert through OpenCV + +### Full WPF Bitmap Helper + +This helper returns a `BitmapSource` suitable for display in WPF. It keeps the output simple: Gray8 or Bgr24. + +```csharp +using Emgu.CV; +using Emgu.CV.CvEnum; +using GvPixelFormat = GigeVision.Core.Enums.PixelFormat; +using System; +using System.Runtime.InteropServices; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +public static class GenICamFrameConverter +{ + public static BitmapSource ToBitmapSource(byte[] frameBytes, int width, int height, GvPixelFormat pixelFormat) + { + if (frameBytes == null) + { + throw new ArgumentNullException(nameof(frameBytes)); + } + + return pixelFormat switch + { + GvPixelFormat.Mono8 => CreateGray8Bitmap(frameBytes, width, height), + GvPixelFormat.Mono8Signed => CreateGray8Bitmap(OffsetSignedToUnsigned(frameBytes), width, height), + + GvPixelFormat.RGB8Packed => CreateRgb24Bitmap(frameBytes, width, height), + GvPixelFormat.BGR8Packed => CreateBgr24Bitmap(frameBytes, width, height), + + GvPixelFormat.BayerRG8 => CreateBayer8Bitmap(frameBytes, width, height, ColorConversion.BayerRg2Bgr), + GvPixelFormat.BayerBG8 => CreateBayer8Bitmap(frameBytes, width, height, ColorConversion.BayerBg2Bgr), + GvPixelFormat.BayerGR8 => CreateBayer8Bitmap(frameBytes, width, height, ColorConversion.BayerGr2Bgr), + GvPixelFormat.BayerGB8 => CreateBayer8Bitmap(frameBytes, width, height, ColorConversion.BayerGb2Bgr), + + GvPixelFormat.Mono10Packed => CreateGray8Bitmap(ScaleTo8Bit(UnpackToUShorts(frameBytes, 10, width * height), 10), width, height), + GvPixelFormat.Mono12Packed => CreateGray8Bitmap(ScaleTo8Bit(UnpackToUShorts(frameBytes, 12, width * height), 12), width, height), + + GvPixelFormat.Mono10 => CreateGray8Bitmap(ScaleTo8Bit(BytesToUShorts(frameBytes), 10), width, height), + GvPixelFormat.Mono12 => CreateGray8Bitmap(ScaleTo8Bit(BytesToUShorts(frameBytes), 12), width, height), + GvPixelFormat.Mono14 => CreateGray8Bitmap(ScaleTo8Bit(BytesToUShorts(frameBytes), 14), width, height), + GvPixelFormat.Mono16 => CreateGray8Bitmap(ScaleTo8Bit(BytesToUShorts(frameBytes), 16), width, height), + + GvPixelFormat.BayerRG10Packed => CreateBayerHighBitBitmap(UnpackToUShorts(frameBytes, 10, width * height), width, height, 10, ColorConversion.BayerRg2Bgr), + GvPixelFormat.BayerBG10Packed => CreateBayerHighBitBitmap(UnpackToUShorts(frameBytes, 10, width * height), width, height, 10, ColorConversion.BayerBg2Bgr), + GvPixelFormat.BayerGR10Packed => CreateBayerHighBitBitmap(UnpackToUShorts(frameBytes, 10, width * height), width, height, 10, ColorConversion.BayerGr2Bgr), + GvPixelFormat.BayerGB10Packed => CreateBayerHighBitBitmap(UnpackToUShorts(frameBytes, 10, width * height), width, height, 10, ColorConversion.BayerGb2Bgr), + + GvPixelFormat.BayerRG12Packed => CreateBayerHighBitBitmap(UnpackToUShorts(frameBytes, 12, width * height), width, height, 12, ColorConversion.BayerRg2Bgr), + GvPixelFormat.BayerBG12Packed => CreateBayerHighBitBitmap(UnpackToUShorts(frameBytes, 12, width * height), width, height, 12, ColorConversion.BayerBg2Bgr), + GvPixelFormat.BayerGR12Packed => CreateBayerHighBitBitmap(UnpackToUShorts(frameBytes, 12, width * height), width, height, 12, ColorConversion.BayerGr2Bgr), + GvPixelFormat.BayerGB12Packed => CreateBayerHighBitBitmap(UnpackToUShorts(frameBytes, 12, width * height), width, height, 12, ColorConversion.BayerGb2Bgr), + + GvPixelFormat.BayerRG10 => CreateBayerHighBitBitmap(BytesToUShorts(frameBytes), width, height, 10, ColorConversion.BayerRg2Bgr), + GvPixelFormat.BayerBG10 => CreateBayerHighBitBitmap(BytesToUShorts(frameBytes), width, height, 10, ColorConversion.BayerBg2Bgr), + GvPixelFormat.BayerGR10 => CreateBayerHighBitBitmap(BytesToUShorts(frameBytes), width, height, 10, ColorConversion.BayerGr2Bgr), + GvPixelFormat.BayerGB10 => CreateBayerHighBitBitmap(BytesToUShorts(frameBytes), width, height, 10, ColorConversion.BayerGb2Bgr), + + GvPixelFormat.BayerRG12 => CreateBayerHighBitBitmap(BytesToUShorts(frameBytes), width, height, 12, ColorConversion.BayerRg2Bgr), + GvPixelFormat.BayerBG12 => CreateBayerHighBitBitmap(BytesToUShorts(frameBytes), width, height, 12, ColorConversion.BayerBg2Bgr), + GvPixelFormat.BayerGR12 => CreateBayerHighBitBitmap(BytesToUShorts(frameBytes), width, height, 12, ColorConversion.BayerGr2Bgr), + GvPixelFormat.BayerGB12 => CreateBayerHighBitBitmap(BytesToUShorts(frameBytes), width, height, 12, ColorConversion.BayerGb2Bgr), + + GvPixelFormat.BayerRG16 => CreateBayerHighBitBitmap(BytesToUShorts(frameBytes), width, height, 16, ColorConversion.BayerRg2Bgr), + GvPixelFormat.BayerBG16 => CreateBayerHighBitBitmap(BytesToUShorts(frameBytes), width, height, 16, ColorConversion.BayerBg2Bgr), + GvPixelFormat.BayerGR16 => CreateBayerHighBitBitmap(BytesToUShorts(frameBytes), width, height, 16, ColorConversion.BayerGr2Bgr), + GvPixelFormat.BayerGB16 => CreateBayerHighBitBitmap(BytesToUShorts(frameBytes), width, height, 16, ColorConversion.BayerGb2Bgr), + + GvPixelFormat.YUV422Packed => CreateYuvBitmap(frameBytes, width, height, ColorConversion.Yuv2BgrUyvy), + GvPixelFormat.YUYVPacked => CreateYuvBitmap(frameBytes, width, height, ColorConversion.Yuv2BgrYuy2), + + _ => throw new NotSupportedException($"Display conversion is not implemented for {pixelFormat}.") + }; + } + + private static BitmapSource CreateGray8Bitmap(byte[] grayBytes, int width, int height) + { + return BitmapSource.Create( + width, + height, + 96, + 96, + PixelFormats.Gray8, + null, + grayBytes, + width); + } + + private static BitmapSource CreateRgb24Bitmap(byte[] rgbBytes, int width, int height) + { + return BitmapSource.Create( + width, + height, + 96, + 96, + PixelFormats.Rgb24, + null, + rgbBytes, + width * 3); + } + + private static BitmapSource CreateBgr24Bitmap(byte[] bgrBytes, int width, int height) + { + return BitmapSource.Create( + width, + height, + 96, + 96, + PixelFormats.Bgr24, + null, + bgrBytes, + width * 3); + } + + private static BitmapSource CreateBayer8Bitmap(byte[] raw8, int width, int height, ColorConversion conversion) + { + using Mat raw = new Mat(height, width, DepthType.Cv8U, 1); + using Mat color = new Mat(height, width, DepthType.Cv8U, 3); + + Marshal.Copy(raw8, 0, raw.DataPointer, raw8.Length); + CvInvoke.CvtColor(raw, color, conversion); + + byte[] bgrBytes = ExtractBytes(color, width * height * 3); + return CreateBgr24Bitmap(bgrBytes, width, height); + } + + private static BitmapSource CreateBayerHighBitBitmap(ushort[] rawValues, int width, int height, int bitDepth, ColorConversion conversion) + { + byte[] normalized = ScaleTo8Bit(rawValues, bitDepth); + return CreateBayer8Bitmap(normalized, width, height, conversion); + } + + private static BitmapSource CreateYuvBitmap(byte[] yuvBytes, int width, int height, ColorConversion conversion) + { + using Mat raw = new Mat(height, width, DepthType.Cv8U, 2); + using Mat color = new Mat(height, width, DepthType.Cv8U, 3); + + Marshal.Copy(yuvBytes, 0, raw.DataPointer, yuvBytes.Length); + CvInvoke.CvtColor(raw, color, conversion); + + byte[] bgrBytes = ExtractBytes(color, width * height * 3); + return CreateBgr24Bitmap(bgrBytes, width, height); + } + + private static byte[] OffsetSignedToUnsigned(byte[] signedBytes) + { + byte[] result = new byte[signedBytes.Length]; + + for (int i = 0; i < signedBytes.Length; i++) + { + result[i] = unchecked((byte)(signedBytes[i] + 128)); + } + + return result; + } + + private static ushort[] BytesToUShorts(byte[] bytes) + { + if ((bytes.Length & 1) != 0) + { + throw new ArgumentException("Expected an even number of bytes for unpacked 16-bit data.", nameof(bytes)); + } + + ushort[] values = new ushort[bytes.Length / 2]; + Buffer.BlockCopy(bytes, 0, values, 0, bytes.Length); + return values; + } + + private static ushort[] UnpackToUShorts(byte[] packedBytes, int bitsPerPixel, int pixelCount) + { + ushort[] values = new ushort[pixelCount]; + int bitOffset = 0; + uint mask = (uint)((1 << bitsPerPixel) - 1); + + for (int i = 0; i < pixelCount; i++) + { + int byteIndex = bitOffset >> 3; + int intraByteBitOffset = bitOffset & 7; + + uint chunk = 0; + if (byteIndex < packedBytes.Length) chunk |= packedBytes[byteIndex]; + if (byteIndex + 1 < packedBytes.Length) chunk |= (uint)packedBytes[byteIndex + 1] << 8; + if (byteIndex + 2 < packedBytes.Length) chunk |= (uint)packedBytes[byteIndex + 2] << 16; + if (byteIndex + 3 < packedBytes.Length) chunk |= (uint)packedBytes[byteIndex + 3] << 24; + + values[i] = (ushort)((chunk >> intraByteBitOffset) & mask); + bitOffset += bitsPerPixel; + } + + return values; + } + + private static byte[] ScaleTo8Bit(ushort[] values, int sourceBitDepth) + { + if (sourceBitDepth < 1 || sourceBitDepth > 16) + { + throw new ArgumentOutOfRangeException(nameof(sourceBitDepth)); + } + + int maxValue = (1 << sourceBitDepth) - 1; + byte[] result = new byte[values.Length]; + + for (int i = 0; i < values.Length; i++) + { + result[i] = (byte)((values[i] * 255 + (maxValue / 2)) / maxValue); + } + + return result; + } + + private static byte[] ExtractBytes(Mat mat, int expectedLength) + { + byte[] bytes = new byte[expectedLength]; + Marshal.Copy(mat.DataPointer, bytes, 0, expectedLength); + return bytes; + } +} +``` + +## 7. Use The Helper In A Single-Frame Workflow + +```csharp +var camera = new Camera(); +var devices = await camera.Gvcp.GetAllGigeDevicesInNetworkAsnyc(); +var device = devices.First(); + +camera.IP = device.IP; +camera.RxIP = device.NetworkIP; + +byte[] firstFrame = await GrabSingleFrameAsync(camera, TimeSpan.FromSeconds(3)); + +BitmapSource bitmap = GenICamFrameConverter.ToBitmapSource( + firstFrame, + (int)camera.Width, + (int)camera.Height, + camera.PixelFormat); +``` + +## 8. Use The Helper In A WPF Streaming Workflow + +```csharp +camera.FrameReady += (sender, frameBytes) => +{ + var copy = new byte[frameBytes.Length]; + Buffer.BlockCopy(frameBytes, 0, copy, 0, frameBytes.Length); + + BitmapSource bitmap = GenICamFrameConverter.ToBitmapSource( + copy, + (int)camera.Width, + (int)camera.Height, + camera.PixelFormat); + + Dispatcher.Invoke(() => imageControl.Source = bitmap); +}; + +await camera.StartStreamAsync(); +``` + +## 9. When To Use The OpenCV Stream Receiver Instead + +The repository already contains an OpenCV-based receiver in `GigeVision.OpenCV`. + +That path is useful when: + +1. You want `Mat` buffers directly instead of `byte[]` +2. You want to process frames with OpenCV immediately +3. You want the same general approach used by the Avalonia sample +4. You want a high-throughput processing loop that avoids allocating a new `byte[]` per frame + +### What The OpenCV Receiver Actually Gives You + +`StreamReceiverParallelOpencv` allocates a rotating array of OpenCV Mats: + +1. `streamReceiver.image[i]` is a ring buffer of raw image Mats +2. The Mats are created as single-channel `Cv8U` or `Cv16U` +3. `waitHandleFrame` is released whenever a full frame has been assembled +4. `frameInCounter` tracks how many complete frames have been published into the ring buffer +5. `imageIndex` tracks total frames seen +6. `lossCount` tracks frames dropped because packet loss exceeded tolerance + +In other words, the OpenCV receiver does not expose a `FrameReady` callback. You consume frames by waiting on `waitHandleFrame` and reading from `streamReceiver.image[bufferIndex]`. + +### Important Limitation Of The OpenCV Receiver + +The current OpenCV receiver creates raw Mats as either 8-bit single-channel or 16-bit single-channel depending on `GvspInfo.BytesPerPixel`. + +That makes it a good fit for: + +1. `Mono8` +2. `BayerRG8`, `BayerBG8`, `BayerGR8`, `BayerGB8` +3. Unpacked high-bit-depth formats like `Mono16` or unpacked Bayer 10, 12, and 16-bit formats that arrive as 16-bit samples + +It is not by itself a complete solution for packed 10-bit and 12-bit formats such as: + +1. `Mono10Packed` +2. `Mono12Packed` +3. `BayerRG10Packed` +4. `BayerBG10Packed` +5. `BayerGR10Packed` +6. `BayerGB10Packed` +7. The packed 12-bit Bayer variants + +For those packed formats, prefer the `byte[]` conversion path from section 6 or add an unpack step before wrapping the data in a Mat. + +### Project Setup For OpenCV + +If you are using the OpenCV path in your own application project, make sure your app references Emgu CV and the Windows runtime package, not only `GigeVision.OpenCV`. + +Typical package references for a Windows app look like this: + +```xml + + + + +``` + +Example setup: + +```csharp +var streamReceiver = new GigeVision.OpenCV.StreamReceiverParallelOpencv(2); + +var camera = new Camera +{ + StreamReceiver = streamReceiver +}; +``` + +### OpenCV Setup With Camera Discovery + +```csharp +using Emgu.CV; +using Emgu.CV.CvEnum; +using GigeVision.Core; +using GigeVision.Core.Enums; +using GigeVision.Core.Services; +using GigeVision.OpenCV; + +NetworkService.AllowAppThroughFirewall(); + +var streamReceiver = new StreamReceiverParallelOpencv(totalBuffers: 3); +var camera = new Camera +{ + StreamReceiver = streamReceiver +}; + +var devices = await camera.Gvcp.GetAllGigeDevicesInNetworkAsnyc(); +var device = devices.FirstOrDefault() + ?? throw new InvalidOperationException("No camera found."); + +camera.IP = device.IP; +camera.RxIP = device.NetworkIP; + +await camera.SyncParameters(); + +Console.WriteLine($"Width: {camera.Width}"); +Console.WriteLine($"Height: {camera.Height}"); +Console.WriteLine($"PixelFormat: {camera.PixelFormat}"); +``` + +### Single-Frame Example Using The OpenCV Receiver + +This pattern is the OpenCV equivalent of the one-shot byte-array example. + +```csharp +public static async Task GrabSingleRawMatAsync(Camera camera, StreamReceiverParallelOpencv streamReceiver, TimeSpan timeout) +{ + int localBufferIndex = 0; + + bool started = await camera.StartStreamAsync(); + if (!started) + { + throw new InvalidOperationException("StartStreamAsync returned false."); + } + + try + { + using var cts = new CancellationTokenSource(timeout); + await streamReceiver.waitHandleFrame.WaitAsync(cts.Token); + + return streamReceiver.image[localBufferIndex].Clone(); + } + finally + { + if (camera.IsStreaming) + { + await camera.StopStream(); + } + } +} +``` + +If the camera is Bayer and you want a color image immediately: + +```csharp +Mat raw = await GrabSingleRawMatAsync(camera, streamReceiver, TimeSpan.FromSeconds(3)); +Mat display = ConvertRawMatToBgr(raw, camera.PixelFormat); + +display.Save("frame.png"); + +raw.Dispose(); +display.Dispose(); +``` + +### Continuous OpenCV Processing Loop + +This is the most important OpenCV pattern in this repository. + +The receiver writes incoming frames into a ring buffer. Your processing loop should: + +1. Keep its own `localBufferIndex` +2. Keep its own `frameOutCounter` +3. Wait on `waitHandleFrame` +4. Process frames until `frameOutCounter == frameInCounter` +5. Advance `localBufferIndex` modulo `TotalBuffers` + +That is the same model used by the Avalonia sample. + +```csharp +int localBufferIndex = 0; +long frameOutCounter = 0; + +bool started = await camera.StartStreamAsync(); +if (!started) +{ + throw new InvalidOperationException("Failed to start stream."); +} + +try +{ + while (streamReceiver.IsReceiving) + { + await streamReceiver.waitHandleFrame.WaitAsync(); + + while (frameOutCounter < streamReceiver.frameInCounter) + { + Mat raw = streamReceiver.image[localBufferIndex]; + + using Mat display = ConvertRawMatToBgr(raw, camera.PixelFormat); + + Console.WriteLine($"Frame {frameOutCounter}, size={display.Width}x{display.Height}, loss={streamReceiver.lossCount}"); + + localBufferIndex++; + if (localBufferIndex == streamReceiver.TotalBuffers) + { + localBufferIndex = 0; + } + + frameOutCounter++; + } + } +} +finally +{ + if (camera.IsStreaming) + { + await camera.StopStream(); + } +} +``` + +### Detailed Raw-Mat To Display-Mat Conversion + +This helper is the OpenCV-side equivalent of the byte-array bitmap helper. + +It converts the raw single-channel Mat produced by `StreamReceiverParallelOpencv` into a BGR Mat suitable for display, saving, or further CV processing. + +```csharp +using Emgu.CV; +using Emgu.CV.CvEnum; +using GvPixelFormat = GigeVision.Core.Enums.PixelFormat; + +public static Mat ConvertRawMatToBgr(Mat raw, GvPixelFormat pixelFormat) +{ + if (raw == null) + { + throw new ArgumentNullException(nameof(raw)); + } + + return pixelFormat switch + { + GvPixelFormat.Mono8 => Gray8ToBgr(raw), + GvPixelFormat.Mono10 => Gray16ToBgr(raw, 10), + GvPixelFormat.Mono12 => Gray16ToBgr(raw, 12), + GvPixelFormat.Mono14 => Gray16ToBgr(raw, 14), + GvPixelFormat.Mono16 => Gray16ToBgr(raw, 16), + + GvPixelFormat.BayerRG8 => Bayer8ToBgr(raw, ColorConversion.BayerRg2Bgr), + GvPixelFormat.BayerBG8 => Bayer8ToBgr(raw, ColorConversion.BayerBg2Bgr), + GvPixelFormat.BayerGR8 => Bayer8ToBgr(raw, ColorConversion.BayerGr2Bgr), + GvPixelFormat.BayerGB8 => Bayer8ToBgr(raw, ColorConversion.BayerGb2Bgr), + + GvPixelFormat.BayerRG10 => Bayer16ToBgr(raw, 10, ColorConversion.BayerRg2Bgr), + GvPixelFormat.BayerBG10 => Bayer16ToBgr(raw, 10, ColorConversion.BayerBg2Bgr), + GvPixelFormat.BayerGR10 => Bayer16ToBgr(raw, 10, ColorConversion.BayerGr2Bgr), + GvPixelFormat.BayerGB10 => Bayer16ToBgr(raw, 10, ColorConversion.BayerGb2Bgr), + + GvPixelFormat.BayerRG12 => Bayer16ToBgr(raw, 12, ColorConversion.BayerRg2Bgr), + GvPixelFormat.BayerBG12 => Bayer16ToBgr(raw, 12, ColorConversion.BayerBg2Bgr), + GvPixelFormat.BayerGR12 => Bayer16ToBgr(raw, 12, ColorConversion.BayerGr2Bgr), + GvPixelFormat.BayerGB12 => Bayer16ToBgr(raw, 12, ColorConversion.BayerGb2Bgr), + + GvPixelFormat.BayerRG16 => Bayer16ToBgr(raw, 16, ColorConversion.BayerRg2Bgr), + GvPixelFormat.BayerBG16 => Bayer16ToBgr(raw, 16, ColorConversion.BayerBg2Bgr), + GvPixelFormat.BayerGR16 => Bayer16ToBgr(raw, 16, ColorConversion.BayerGr2Bgr), + GvPixelFormat.BayerGB16 => Bayer16ToBgr(raw, 16, ColorConversion.BayerGb2Bgr), + + _ => throw new NotSupportedException( + $"OpenCV Mat conversion is not implemented for {pixelFormat}. Packed 10/12-bit formats require an unpack step before Mat conversion.") + }; +} + +private static Mat Gray8ToBgr(Mat raw) +{ + Mat color = new Mat(); + CvInvoke.CvtColor(raw, color, ColorConversion.Gray2Bgr); + return color; +} + +private static Mat Gray16ToBgr(Mat raw, int bitDepth) +{ + using Mat normalized8 = Normalize16To8(raw, bitDepth); + return Gray8ToBgr(normalized8); +} + +private static Mat Bayer8ToBgr(Mat raw, ColorConversion conversion) +{ + Mat color = new Mat(); + CvInvoke.CvtColor(raw, color, conversion); + return color; +} + +private static Mat Bayer16ToBgr(Mat raw, int bitDepth, ColorConversion conversion) +{ + using Mat normalized8 = Normalize16To8(raw, bitDepth); + return Bayer8ToBgr(normalized8, conversion); +} + +private static Mat Normalize16To8(Mat raw16, int bitDepth) +{ + double scale = 255.0 / ((1 << bitDepth) - 1); + Mat normalized8 = new Mat(); + raw16.ConvertTo(normalized8, DepthType.Cv8U, scale); + return normalized8; +} +``` + +### Save Every Nth Frame To Disk + +```csharp +int localBufferIndex = 0; +long frameOutCounter = 0; +long saveEvery = 30; + +await camera.StartStreamAsync(); + +try +{ + while (streamReceiver.IsReceiving) + { + await streamReceiver.waitHandleFrame.WaitAsync(); + + while (frameOutCounter < streamReceiver.frameInCounter) + { + Mat raw = streamReceiver.image[localBufferIndex]; + + if (frameOutCounter % saveEvery == 0) + { + using Mat display = ConvertRawMatToBgr(raw, camera.PixelFormat); + display.Save($"frame_{frameOutCounter:D6}.png"); + } + + localBufferIndex = (localBufferIndex + 1) % streamReceiver.TotalBuffers; + frameOutCounter++; + } + } +} +finally +{ + await camera.StopStream(); +} +``` + +### Display In A UI Without Extra Byte Copies + +The Avalonia sample demonstrates the intended pattern: + +1. Receive raw Mats into `streamReceiver.image` +2. Demosaic or normalize into a display Mat +3. Copy the final BGR bytes into the UI bitmap buffer + +That is usually better than converting each frame back into managed `byte[]` unless you specifically need a managed buffer for other reasons. + +### When To Choose The Byte Array Path Instead Of OpenCV + +Prefer the `byte[]` path from section 6 when: + +1. You need a framework-agnostic helper that returns `BitmapSource` +2. You are handling packed 10-bit or 12-bit formats +3. You do not want to take a dependency on Emgu CV in your application +4. You only need a single frame occasionally and do not need CV processing + +## 10. Troubleshooting + +### `StartStreamAsync` Returns `false` + +Check these first: + +1. The camera IP and host NIC are on the same subnet +2. `camera.RxIP` is set correctly on multi-NIC machines +3. Firewall is not blocking GVSP traffic +4. Another process does not already control the camera + +### The Frame Size Looks Wrong + +Call: + +```csharp +await camera.SyncParameters(); +``` + +before converting the bytes, and make sure you are using `camera.Width`, `camera.Height`, and `camera.PixelFormat` from the active camera state. + +### The Image Is Garbled + +Most often this means one of these: + +1. The pixel format dispatch is wrong +2. The format is Bayer and you displayed it as Mono8 +3. The format is packed 10-bit or 12-bit and you treated it as 8-bit or 16-bit data +4. The stride passed to the bitmap is wrong + +### The First Frame Looks Fine But Later Frames Corrupt + +Copy the frame bytes in `FrameReady`. Do not store the original callback array reference for later use. + +## 11. Recommended Safe Workflow + +If you want the least surprising end-to-end path in this repository: + +1. Discover with `GetAllGigeDevicesInNetworkAsnyc` +2. Set `camera.IP` and `camera.RxIP` +3. Call `SyncParameters` +4. Grab one frame with `GrabSingleFrameAsync` +5. Convert using `GenICamFrameConverter.ToBitmapSource` +6. If needed, switch to continuous streaming after the single-frame path is working + +That isolates connection issues from conversion issues and makes debugging much simpler. \ No newline at end of file diff --git a/GevSCPD.md b/GevSCPD.md new file mode 100644 index 0000000..0630343 --- /dev/null +++ b/GevSCPD.md @@ -0,0 +1,69 @@ +The value is not arbitrary — it's derived from the physical properties of the camera's stream and where Windows drops packets. + +The "cold burst" problem at 1 Hz + +At 33 Hz there is ~30 ms between frames. The Windows NIC driver, NDIS interrupt DPC, and socket receive path are all continuously active ("warm"). Packets arrive at a pace the kernel is already scheduled to handle. + +When you switch to 1 Hz there is a ~1-second gap of silence. During that gap: + +The NIC's interrupt coalescing timer resets to its maximum holdoff (typically 100–200 µs on Intel NICs) +The kernel NDIS receive DPC de-schedules +The socket application thread blocks on Receive() +Then the camera sends all ~540 packets (≈780 KB) in a burst. With GevSCPD = 0 the camera transmits at full line rate: each 1472-byte packet takes ~11.8 µs at 1 Gbps, so the entire frame arrives in ~6.4 ms. The NIC interrupt coalescing fires once, batching potentially hundreds of packets into a single DPC. The NIC on-chip ring is small (often 256–512 descriptors), so it overflows before the kernel can drain it — those drops are invisible to the socket receive buffer. + +Why 100 000 works + +On a Lucid camera the timestamp counter runs at 1 GHz (1 tick = 1 ns), so: + +$$GevSCPD=100000⇒100μs per packet$$ + +$$540 packets × 100μs = 54 ms total spread$$ + +At 100 µs spacing the kernel processes each packet (or small batch) before the next arrives. The NIC ring never overflows. At 1 Hz this 54 ms is only 5.4% of the 1000 ms frame interval, so it costs nothing in practice. + +Why 10 000 was marginal + +$$10000⇒10μs/packet,540×10μs=5.4ms$$ + +That is barely wider than the raw line-rate burst (~6.4 ms), barely enough to beat the NIC coalescing holdoff. Under light system load it works; under any CPU contention it reverts to drops — which is why it was unstable. + +Rule of thumb for the value + +$$GevSCPD≥\frac{NIC coalescing holdoff}{packets per frame}×clock frequency$$ + +For a 200 µs NIC holdoff and 540 packets: $200μs/540≈0.37μs$ minimum spacing. But with Windows scheduling jitter you need headroom; empirically 100 µs/packet (100 000 ticks) is a safe working point. If you want the smallest delay that reliably works, you can bisect between 10 000 and 100 000 — the threshold is likely around 30 000–50 000 for a typical Intel NIC at 1 Gbps. + +**The derivable formula** + +There are two constraints on GevSCPD: + +1. **Floor** — inter-packet spacing must be large enough for Windows to service the NIC even when cold (coming out of a long idle gap). Empirically this is ~100 µs. For a 1 GHz camera clock: `d_min = 100_000 ticks`. + +2. **Ceiling** — you cannot delay packets longer than the inter-frame period divided by packet count, or the next frame's burst starts before the current one finishes: + +$$d_{\max} = \left\lfloor \frac{F_c}{f \cdot N} \right\rfloor$$ + +where $F_c$ = camera clock frequency (ticks/s), $f$ = frame rate (Hz), $N$ = `GvspInfo.FinalPacketID` (packets per frame). + +Taking the minimum of both: + +$$\boxed{\text{GevSCPD}(f, N, F_c) = \min\!\left(100{,}000,\ \left\lfloor \frac{F_c}{f \cdot N} \right\rfloor\right)}$$ + +**Validation against your observations** + +| f (Hz) | N | $F_c/(f \cdot N)$ | GevSCPD | Total burst | +|---|---|---|---|---| +| 1 | 540 | 1,851,852 | **100,000** | 54 ms | +| 10 | 540 | 185,185 | **100,000** | 54 ms | +| 18.5 | 540 | ~100,000 | **100,000** | ~54 ms (crossover) | +| 33 | 540 | 56,166 | **56,166** | 30 ms (= full frame period → continuous) | +| 100 | 540 | 18,519 | **18,519** | 10 ms | + +The formula recovers exactly 100,000 at your 1 Hz case. At ≥33 Hz the NIC is warm and the reduced delay is safe; the ceiling also ensures you never delay the stream into the next frame. + +**Two unknowns needed at runtime** + +- $N$ = `camera.StreamReceiver.GvspInfo.FinalPacketID` — available after `DetectGvspType()`, so it can only be used for the *next* `StartStreamAsync` call, not the first. For the first call you can estimate it from `Width`, `Height`, `BytesPerPixel`, and `Payload`. +- $F_c$ — read from `GevTimestampTickFrequency` via GVCP, or assume `1_000_000_000` for Lucid (1 GHz). + +If you want, I can implement a `CalculateSuggestedStreamPacketDelay(double frameRateHz)` method on `Camera` using the estimated $N$ from current camera geometry. \ No newline at end of file diff --git a/GigeVision.Core/Enums/GevRegisters.cs b/GigeVision.Core/Enums/GevRegisters.cs index 1be103e..6ad3bc0 100644 --- a/GigeVision.Core/Enums/GevRegisters.cs +++ b/GigeVision.Core/Enums/GevRegisters.cs @@ -20,7 +20,7 @@ public enum GvcpRegister CurrentIPAddress = 0x0024, CurrentSubnetMask = 0x0034, CurrentDefaultGateway = 0x0044, - GevSCSP = 0x01C, + GevSCSP = 0x0D1C, FirstURL = 0x0200, SecondURL = 0x0400, NumberOfInterfaces = 0x0600, diff --git a/GigeVision.Core/Enums/PixelFormat.cs b/GigeVision.Core/Enums/PixelFormat.cs index 82a62ed..21cf015 100644 --- a/GigeVision.Core/Enums/PixelFormat.cs +++ b/GigeVision.Core/Enums/PixelFormat.cs @@ -3,7 +3,7 @@ /// /// Pixel format of GVSP stream /// - public enum PixelFormat + public enum PixelFormat : uint { /// /// GVSP_PIX_MONO8 diff --git a/GigeVision.Core/GigeVision.Core.csproj b/GigeVision.Core/GigeVision.Core.csproj index 3c18dbe..bc4725b 100644 --- a/GigeVision.Core/GigeVision.Core.csproj +++ b/GigeVision.Core/GigeVision.Core.csproj @@ -11,12 +11,14 @@ GigeVision, GeniCam, GVSP, GVCP true true - 2.7.3 + 2.7.6-local License.txt gige.png x64 + 2.7.6: Local build + 2.7.5: Local build 2.7.3: .net10.0 support added 2.7.1: Add Firewall bypass routine and with improved XML parsing for GenICam 2.6: Stream Receiver Parallel @@ -25,7 +27,8 @@ 2.5: Add firewall exception and Custom Stream Receiver option enabled 2.4: Fix Get all registers, Force IP command, Documentation Added and .net5.0 support added 2.1.1: Upgraded to Netcore 6.0 with Multicast reception fix - 1.8.0: Pushes the block id (frame number) in the frame event and Gives notification on frameloss + 1.8.0: Pushes the block id (frame number) in the frame event and Gives notification on + frameloss Fix Receiver IP Detection Fix Discovery for all cameras in network @@ -46,12 +49,12 @@ - + - + \ No newline at end of file diff --git a/GigeVision.Core/Interfaces/ICamera.cs b/GigeVision.Core/Interfaces/ICamera.cs index 5459f2c..d415c73 100644 --- a/GigeVision.Core/Interfaces/ICamera.cs +++ b/GigeVision.Core/Interfaces/ICamera.cs @@ -53,6 +53,12 @@ public interface ICamera /// EventHandler FrameReady { get; set; } + /// + /// Fired alongside with per-frame metadata + /// (hardware timestamp and frame ID) from the GVSP image leader. + /// + EventHandler FrameReadyWithInfo { get; set; } + /// /// Event for general updates /// @@ -63,10 +69,15 @@ public interface ICamera /// uint Payload { get; set; } + /// + /// Optional inter-packet delay written to GevSCPD before starting the stream. + /// + uint StreamPacketDelay { get; set; } + /// /// Tolerance for missing packet /// - public int MissingPacketTolerance { get; set; } + public int MissingPacketTolerance { get; set; } /// /// Camera width @@ -93,6 +104,13 @@ public interface ICamera /// PixelFormat PixelFormat { get; set; } + /// + /// Human-readable pixel format name. Returns the registry name for vendor-specific + /// formats (e.g. "QOI_BayerRG8"), the enum name for standard PFNC formats, + /// or a hex string (e.g. "0x81080B99") for completely unknown values. + /// + string PixelFormatName { get; } + /// /// Device IP /// @@ -121,7 +139,7 @@ public interface ICamera /// If rxIP is not provided, method will detect system IP and use it /// It will set randomly when not provided /// - Task StartStreamAsync(string rxIP = null, int rxPort = 0); + Task StartStreamAsync(string? rxIP = null, int rxPort = 0); /// /// Stops the camera stream and leave camera control @@ -129,6 +147,107 @@ public interface ICamera /// Is streaming status Task StopStream(); + /// + /// Gets the value of an integer-backed parameter. + /// + /// The parameter name. + /// The parameter value when available. + Task GetParameterValue(string parameterName); + + /// + /// Gets the minimum of an integer-backed parameter. + /// + /// The parameter name. + /// The minimum value when available. + Task GetParameterMinValue(string parameterName); + + /// + /// Gets the maximum of an integer-backed parameter. + /// + /// The parameter name. + /// The maximum value when available. + Task GetParameterMaxValue(string parameterName); + + /// + /// Sets an integer-backed parameter. + /// + /// The parameter name. + /// The value to set. + /// True when the write succeeds. + Task SetCameraParameter(string parameterName, long value); + + /// + /// Sets a floating-point or converter-backed parameter. + /// + /// The parameter name. + /// The value to set. + /// True when the write succeeds. + Task SetCameraParameter(string parameterName, double value); + + /// + /// Sets a boolean parameter. + /// + /// The parameter name. + /// The value to set. + /// True when the write succeeds. + Task SetCameraParameter(string parameterName, bool value); + + /// + /// Gets the value of a parameter using the requested CLR type. + /// + /// The requested value type. + /// The parameter name. + /// The parameter value when available. + Task GetParameterValue(string parameterName) where T : struct; + + /// + /// Gets the value of a floating-point or converter-backed parameter. + /// + /// The parameter name. + /// The parameter value when available. + [Obsolete("Use GetParameterValue(parameterName) instead.")] + Task GetFloatParameterValue(string parameterName); + + /// + /// Gets the minimum of a floating-point or converter-backed parameter. + /// + /// The parameter name. + /// The minimum value when available. + Task GetFloatParameterMinValue(string parameterName); + + /// + /// Gets the maximum of a floating-point or converter-backed parameter. + /// + /// The parameter name. + /// The maximum value when available. + Task GetFloatParameterMaxValue(string parameterName); + + /// + /// Sets a floating-point or converter-backed parameter. + /// + /// The parameter name. + /// The value to set. + /// True when the write succeeds. + [Obsolete("Use SetCameraParameter(parameterName, value) instead.")] + Task SetFloatParameter(string parameterName, double value); + + /// + /// Gets the value of a boolean parameter. + /// + /// The parameter name. + /// The boolean value when available. + [Obsolete("Use GetParameterValue(parameterName) instead.")] + Task GetBooleanParameterValue(string parameterName); + + /// + /// Sets a boolean parameter. + /// + /// The parameter name. + /// The value to set. + /// True when the write succeeds. + [Obsolete("Use SetCameraParameter(parameterName, value) instead.")] + Task SetBooleanParameter(string parameterName, bool value); + /// /// Sets the Resolution /// diff --git a/GigeVision.Core/Interfaces/IStreamReceiver.cs b/GigeVision.Core/Interfaces/IStreamReceiver.cs index adaf07a..88c50a0 100644 --- a/GigeVision.Core/Interfaces/IStreamReceiver.cs +++ b/GigeVision.Core/Interfaces/IStreamReceiver.cs @@ -12,12 +12,18 @@ public interface IStreamReceiver /// Event for frame ready /// EventHandler FrameReady { get; set; } - + + /// + /// Fired alongside and carries per-frame metadata + /// (hardware timestamp and frame ID) extracted from the GVSP image leader packet. + /// + EventHandler FrameReadyWithInfo { get; set; } + /// /// The camera source traffic port. Required for firewall traversal traffic /// int CameraSourcePort { get; set; } - + /// /// Camera ip, required for firewall traversal traffic /// @@ -57,7 +63,7 @@ public interface IStreamReceiver /// RX IP, required for multicast group /// string RxIP { get; set; } - + /// /// The socket receive timeout in milliseconds. Set -1 to infinite timeout /// @@ -67,7 +73,7 @@ public interface IStreamReceiver /// General update event /// EventHandler Updates { get; set; } - + /// /// Time interval from a package to another for firewall traversal. Set value <= 0 to disable it /// diff --git a/GigeVision.Core/Models/GvcpReply.cs b/GigeVision.Core/Models/GvcpReply.cs index cd21fe5..1c795a1 100644 --- a/GigeVision.Core/Models/GvcpReply.cs +++ b/GigeVision.Core/Models/GvcpReply.cs @@ -118,8 +118,7 @@ public void DetectCommand(byte[] buffer) if (buffer?.Length > 7) { Status = (GvcpStatus)((buffer[0] << 8) | (buffer[1])); - if (Status != GvcpStatus.GEV_STATUS_SUCCESS) - return; + AcknowledgementID = (ushort)((buffer[6] << 8) | (buffer[7])); IsSentAndReplyReceived = true; GvcpCommandType commandType = (GvcpCommandType)((buffer[2] << 8) | (buffer[3])); if (Enum.IsDefined(typeof(GvcpCommandType), commandType)) @@ -134,6 +133,11 @@ public void DetectCommand(byte[] buffer) return; } + if (Status != GvcpStatus.GEV_STATUS_SUCCESS) + { + return; + } + switch (Type) { case GvcpCommandType.Discovery: @@ -143,11 +147,11 @@ public void DetectCommand(byte[] buffer) break; case GvcpCommandType.ReadRegAck: - if (buffer?.Length < 13)//Single Register reply + if (buffer.Length == 12)//Single Register reply { RegisterValue = (uint)((buffer[8] << 24) | (buffer[9] << 16) | (buffer[10] << 8) | (buffer[11])); } - else //Multiple register reply + else if (buffer.Length > 12 && (buffer.Length - 8) % 4 == 0) //Multiple register reply { int totalRegisters = (buffer.Length - 8) / 4; RegisterValues = new List(); @@ -160,6 +164,10 @@ public void DetectCommand(byte[] buffer) (buffer[11 + (4 * i)]))); } } + else + { + IsValid = false; + } break; case GvcpCommandType.WriteReg: @@ -177,7 +185,6 @@ public void DetectCommand(byte[] buffer) case GvcpCommandType.Invalid: break; } - AcknowledgementID = (ushort)((buffer[6] << 8) | (buffer[7])); } } } diff --git a/GigeVision.Core/Models/GvspFrameInfo.cs b/GigeVision.Core/Models/GvspFrameInfo.cs new file mode 100644 index 0000000..394e278 --- /dev/null +++ b/GigeVision.Core/Models/GvspFrameInfo.cs @@ -0,0 +1,27 @@ +namespace GigeVision.Core.Models +{ + /// + /// Per-frame metadata extracted from the GVSP image leader packet. + /// Available in . + /// + public readonly struct GvspFrameInfo + { + /// + /// Camera block ID (frame counter). Monotonically increasing; gaps indicate dropped frames. + /// + public ulong FrameId { get; } + + /// + /// 64-bit hardware timestamp from the camera's free-running clock, in camera clock ticks. + /// For Lucid cameras the clock runs at 1 GHz (1 tick = 1 ns). + /// Convert to seconds: Timestamp / 1e9. + /// + public ulong Timestamp { get; } + + public GvspFrameInfo(ulong frameId, ulong timestamp) + { + FrameId = frameId; + Timestamp = timestamp; + } + } +} diff --git a/GigeVision.Core/Services/Camera.cs b/GigeVision.Core/Services/Camera.cs index 18ca838..20f4ff6 100644 --- a/GigeVision.Core/Services/Camera.cs +++ b/GigeVision.Core/Services/Camera.cs @@ -1,10 +1,10 @@ using GigeVision.Core.Enums; using GigeVision.Core.Interfaces; using System; -using System.Collections.Generic; -using System.Globalization; +using System.Collections.Concurrent; using System.Net; using System.Net.Sockets; +using System.Threading; using System.Threading.Tasks; using GenICam; using GigeVision.Core.Models; @@ -35,10 +35,12 @@ public class Camera : BaseNotifyPropertyChanged, ICamera private bool isMulticast; private bool isStreaming; private uint payload = 0; + private uint streamPacketDelay; private int portRx; private string rxIP; private uint width, height, offsetX, offsetY, bytesPerPixel; - private Dictionary cameraParametersCache; + private readonly ConcurrentDictionary cameraParametersCache; + private readonly SemaphoreSlim syncParametersSemaphore = new(1, 1); /// /// Camera constructor with initialized Gvcp Controller @@ -47,8 +49,7 @@ public class Camera : BaseNotifyPropertyChanged, ICamera public Camera(IGvcp gvcp) { Gvcp = gvcp; - cameraParametersCache = new Dictionary(); - Task.Run(async () => await SyncParameters().ConfigureAwait(false)); + cameraParametersCache = new ConcurrentDictionary(); Init(); } @@ -61,7 +62,7 @@ public Camera(IGvcp gvcp) public Camera() { Gvcp = new Gvcp(); - cameraParametersCache = new Dictionary(); + cameraParametersCache = new ConcurrentDictionary(); Init(); } @@ -70,6 +71,12 @@ public Camera() /// public EventHandler FrameReady { get; set; } + /// + /// Fired alongside with per-frame metadata + /// (hardware timestamp and frame ID) from the GVSP image leader. + /// + public EventHandler FrameReadyWithInfo { get; set; } + /// /// GVCP controller /// @@ -83,7 +90,7 @@ public int SCSPPort get; private set; } - + /// /// Camera height /// @@ -124,14 +131,14 @@ public int ReceiveTimeoutInMilliseconds Gvcp.ReceiveTimeoutInMilliseconds = value; if (StreamReceiver != null) { - StreamReceiver.ReceiveTimeoutInMilliseconds = value; + StreamReceiver.ReceiveTimeoutInMilliseconds = value; } - + OnPropertyChanged(nameof(ReceiveTimeoutInMilliseconds)); } } - + /// /// Multi-Cast Option /// @@ -233,11 +240,34 @@ public uint Payload } } + /// + /// Optional inter-packet delay written to GevSCPD before starting the stream. + /// + public uint StreamPacketDelay + { + get => streamPacketDelay; + set + { + if (streamPacketDelay != value) + { + streamPacketDelay = value; + OnPropertyChanged(nameof(StreamPacketDelay)); + } + } + } + /// /// Camera Pixel Format /// public PixelFormat PixelFormat { get; set; } + /// + /// Human-readable pixel format name. Returns the registry name for vendor-specific + /// formats (e.g. "QOI_BayerRG8"), the enum name for standard PFNC formats, + /// or a hex string (e.g. "0x81080B99") for completely unknown values. + /// + public string PixelFormatName => PixelFormatHelper.GetName((uint)PixelFormat); + /// /// Rx port /// @@ -293,12 +323,6 @@ public uint Width } } - public bool IsBitSet(T t, int pos) where T : struct, IConvertible - { - var value = t.ToInt64(CultureInfo.CurrentCulture); - return (value & (1 << pos)) != 0; - } - /// /// Bridge Command for motor controller, controls focus/zoom/iris operation /// @@ -366,28 +390,41 @@ public async Task SetOffsetAsync() /// Command Status public async Task SetOffsetAsync(uint offsetX, uint offsetY) { - if (!IsStreaming) - { - await Gvcp.TakeControl().ConfigureAwait(false); - } - var offsetXRegister = (await Gvcp.GetRegister(nameof(RegisterName.OffsetX))).register; - var offsetYRegister = (await Gvcp.GetRegister(nameof(RegisterName.OffsetY))).register; - string[] registers = new string[2]; - registers[0] = string.Format("0x{0:X8}", (await offsetXRegister.GetAddressAsync())); - registers[1] = string.Format("0x{0:X8}", (await offsetYRegister.GetAddressAsync())); - uint[] valueToWrite = new uint[] { offsetX, offsetY }; - bool status = (await Gvcp.WriteRegisterAsync(registers, valueToWrite).ConfigureAwait(false)).Status == GvcpStatus.GEV_STATUS_SUCCESS; - GvcpReply reply = await Gvcp.ReadRegisterAsync(registers).ConfigureAwait(false); - if (reply.Status == GvcpStatus.GEV_STATUS_SUCCESS) + bool controlTaken = false; + try { - OffsetX = reply.RegisterValues[0]; - OffsetY = reply.RegisterValues[1]; + if (!IsStreaming) + { + controlTaken = await Gvcp.TakeControl().ConfigureAwait(false); + if (!controlTaken) + { + return false; + } + } + + var offsetXRegister = (await Gvcp.GetRegister(nameof(RegisterName.OffsetX))).register; + var offsetYRegister = (await Gvcp.GetRegister(nameof(RegisterName.OffsetY))).register; + string[] registers = new string[2]; + registers[0] = string.Format("0x{0:X8}", (await offsetXRegister.GetAddressAsync())); + registers[1] = string.Format("0x{0:X8}", (await offsetYRegister.GetAddressAsync())); + uint[] valueToWrite = new uint[] { offsetX, offsetY }; + bool status = (await Gvcp.WriteRegisterAsync(registers, valueToWrite).ConfigureAwait(false)).Status == GvcpStatus.GEV_STATUS_SUCCESS; + GvcpReply reply = await Gvcp.ReadRegisterAsync(registers).ConfigureAwait(false); + if (reply.Status == GvcpStatus.GEV_STATUS_SUCCESS) + { + OffsetX = reply.RegisterValues[0]; + OffsetY = reply.RegisterValues[1]; + } + + return status; } - if (!IsStreaming) + finally { - await Gvcp.LeaveControl().ConfigureAwait(false); + if (controlTaken && !IsStreaming) + { + await Gvcp.LeaveControl().ConfigureAwait(false); + } } - return status; } /// @@ -398,9 +435,15 @@ public async Task SetOffsetAsync(uint offsetX, uint offsetY) /// Command Status public async Task SetResolutionAsync(uint width, uint height) { + bool controlTaken = false; try { - await Gvcp.TakeControl().ConfigureAwait(false); + controlTaken = await Gvcp.TakeControl().ConfigureAwait(false); + if (!controlTaken) + { + return false; + } + var widthPValue = (await Gvcp.GetRegister(nameof(RegisterName.Width))).pValue; var heightPValue = (await Gvcp.GetRegister(nameof(RegisterName.Height))).pValue; GvcpReply widthWriteReply = (await widthPValue.SetValueAsync(width).ConfigureAwait(false)) as GvcpReply; @@ -414,14 +457,19 @@ public async Task SetResolutionAsync(uint width, uint height) Width = (uint)newWidth; Height = (uint)newHeight; } - - await Gvcp.LeaveControl().ConfigureAwait(false); return status; } catch (Exception) { return false; } + finally + { + if (controlTaken) + { + await Gvcp.LeaveControl().ConfigureAwait(false); + } + } } /// @@ -451,15 +499,18 @@ public async Task SetResolutionAsync() /// It will set randomly when not provided /// If not null this event will be raised /// - public async Task StartStreamAsync(string rxIP = null, int rxPort = 0) + public async Task StartStreamAsync(string? rxIP = null, int rxPort = 0) { string ip2Send; + bool controlTaken = false; + bool receiverStarted = false; // If the custom stream receiver is not set then it will set the default one if (string.IsNullOrEmpty(rxIP)) { if (string.IsNullOrEmpty(RxIP) && !SetRxIP()) { + Updates?.Invoke(this, "StartStreamAsync failed: no valid receiver IP was available."); return false; } } @@ -477,25 +528,26 @@ public async Task StartStreamAsync(string rxIP = null, int rxPort = 0) { var status = await SyncParameters().ConfigureAwait(false); if (!status) + { + Updates?.Invoke(this, "StartStreamAsync failed: SyncParameters returned false."); return status; + } } - catch + catch (Exception ex) { + Updates?.Invoke(this, $"StartStreamAsync failed during SyncParameters: {ex.Message}"); return false; } - if (rxPort == 0) + + if (rxPort != 0) { - if (PortRx == 0) - { - UdpClient udpClient = new(0); - PortRx = ((IPEndPoint)(udpClient.Client.LocalEndPoint)).Port; - udpClient.Dispose(); - } + PortRx = rxPort; } - else + else if (!IsMulticast) { - PortRx = rxPort; + PortRx = 0; } + if (Payload == 0) { CalculateSingleRowPayload(); @@ -506,52 +558,123 @@ public async Task StartStreamAsync(string rxIP = null, int rxPort = 0) SetRxBuffer(); } - - var acquisitionStart = (await Gvcp.GetRegister(nameof(RegisterName.AcquisitionStart))).pValue; - if (acquisitionStart != null) + if (acquisitionStart == null) { - if (await Gvcp.TakeControl(true).ConfigureAwait(false)) + Updates?.Invoke(this, "StartStreamAsync failed: AcquisitionStart command was not found in the XML/register map."); + return false; + } + + try + { + SetupReceiver(); + if (!IsMulticast && PortRx == 0) + { + SetupRxThread(); + receiverStarted = true; + PortRx = StreamReceiver.PortRx; + } + + controlTaken = await Gvcp.TakeControl(true).ConfigureAwait(false); + if (!controlTaken) { - var gevSCPHostPort = (await Gvcp.GetRegister(nameof(GvcpRegister.GevSCPHostPort))).pValue; - if ((await gevSCPHostPort.SetValueAsync((uint)PortRx).ConfigureAwait(false) as GvcpReply).Status == GvcpStatus.GEV_STATUS_SUCCESS) + Updates?.Invoke(this, "StartStreamAsync failed: camera control could not be acquired."); + if (IsMulticast) { - var gevSCDA = (await Gvcp.GetRegister(nameof(GvcpRegister.GevSCDA))).pValue; - if ((await gevSCDA.SetValueAsync(Converter.IpToNumber(ip2Send)).ConfigureAwait(false) as GvcpReply).Status == GvcpStatus.GEV_STATUS_SUCCESS) + if (!receiverStarted) { - var gevSCSP = (await Gvcp.GetRegister(nameof(GvcpRegister.GevSCSP))).pValue; - var getSCSPPortValue = await gevSCSP.GetValueAsync().ConfigureAwait((false)); - if (getSCSPPortValue.HasValue) - { - SCSPPort = (int)getSCSPPortValue.Value; - } - - var gevSCPSPacketSize = (await Gvcp.GetRegister(nameof(GvcpRegister.GevSCPSPacketSize))).pValue; - var reply = await gevSCPSPacketSize.SetValueAsync(Payload).ConfigureAwait(false); - SetupReceiver(); SetupRxThread(); - - if (((await acquisitionStart.SetValueAsync(1).ConfigureAwait(false)) as GvcpReply).Status == GvcpStatus.GEV_STATUS_SUCCESS) - { - IsStreaming = true; - } - else - { - await StopStream().ConfigureAwait(false); - } + receiverStarted = true; } + + IsStreaming = true; } + + return IsStreaming; + } + + SetupReceiver(); + var gevSCPHostPort = (await Gvcp.GetRegister(nameof(GvcpRegister.GevSCPHostPort))).pValue; + var gevSCPHostPortReply = (await gevSCPHostPort.SetValueAsync((uint)PortRx).ConfigureAwait(false) as GvcpReply); + if (gevSCPHostPortReply?.Status != GvcpStatus.GEV_STATUS_SUCCESS) + { + Updates?.Invoke(this, $"StartStreamAsync failed: GevSCPHostPort write returned {gevSCPHostPortReply?.Status} for port {PortRx}."); + return false; + } + + var gevSCDA = (await Gvcp.GetRegister(nameof(GvcpRegister.GevSCDA))).pValue; + var gevSCDAReply = (await gevSCDA.SetValueAsync(Converter.IpToNumber(ip2Send)).ConfigureAwait(false) as GvcpReply); + if (gevSCDAReply?.Status != GvcpStatus.GEV_STATUS_SUCCESS) + { + Updates?.Invoke(this, $"StartStreamAsync failed: GevSCDA write returned {gevSCDAReply?.Status} for receiver IP {ip2Send}."); + return false; + } + + var gevSCSP = (await Gvcp.GetRegister(nameof(GvcpRegister.GevSCSP))).pValue; + var getSCSPPortValue = await gevSCSP.GetValueAsync().ConfigureAwait(false); + if (getSCSPPortValue.HasValue) + { + SCSPPort = (int)getSCSPPortValue.Value; + } + + StreamReceiver.CameraSourcePort = SCSPPort; + + var gevSCPSPacketSize = (await Gvcp.GetRegister(nameof(GvcpRegister.GevSCPSPacketSize))).pValue; + var gevSCPSPacketSizeReply = (await gevSCPSPacketSize.SetValueAsync(Payload).ConfigureAwait(false) as GvcpReply); + if (gevSCPSPacketSizeReply?.Status != GvcpStatus.GEV_STATUS_SUCCESS) + { + Updates?.Invoke(this, $"StartStreamAsync failed: GevSCPSPacketSize write returned {gevSCPSPacketSizeReply?.Status} for payload {Payload}."); + return false; + } + + var gevSCPDReply = await Gvcp.WriteRegisterAsync(GvcpRegister.GevSCPD, StreamPacketDelay).ConfigureAwait(false); + if (gevSCPDReply?.Status != GvcpStatus.GEV_STATUS_SUCCESS) + { + Updates?.Invoke(this, $"StartStreamAsync failed: GevSCPD write returned {gevSCPDReply?.Status} for delay {StreamPacketDelay}."); + return false; + } + + if (!receiverStarted) + { + SetupReceiver(); + SetupRxThread(); + receiverStarted = true; + PortRx = StreamReceiver.PortRx; + } + + var acquisitionStartReply = (await acquisitionStart.SetValueAsync(1).ConfigureAwait(false)) as GvcpReply; + if (acquisitionStartReply?.Status == GvcpStatus.GEV_STATUS_SUCCESS) + { + IsStreaming = true; } else { - if (IsMulticast) + Updates?.Invoke(this, $"StartStreamAsync failed: AcquisitionStart returned {acquisitionStartReply?.Status}."); + return false; + } + } + catch (Exception ex) + { + Updates?.Invoke(this, $"StartStreamAsync failed with exception: {ex.Message}"); + return false; + } + finally + { + if (!IsStreaming) + { + if (receiverStarted) { - SetupRxThread(); - IsStreaming = true; + StreamReceiver?.StopReception(); + } + + if (controlTaken) + { + await Gvcp.LeaveControl().ConfigureAwait(false); } } } + return IsStreaming; } @@ -572,19 +695,124 @@ public async Task StopStream() public async Task GetParameterValue(string parameterName) { - if (cameraParametersCache == null) + ICategory parameter = await GetParameter(parameterName).ConfigureAwait(false); + if (parameter == null) + { + return null; + } + + return await parameter.PValue.GetValueAsync().ConfigureAwait(false); + } + + public async Task GetParameterValue(string parameterName) where T : struct + { + Type requestedType = typeof(T); + + if (requestedType == typeof(bool)) + { + var result = await GetBooleanParameterValueCore(parameterName).ConfigureAwait(false); + return result is null ? null : (T?)(object)result.Value; + } + + if (requestedType == typeof(double)) + { + var result = await GetFloatParameterValueCore(parameterName).ConfigureAwait(false); + return result is null ? null : (T?)(object)result.Value; + } + + if (requestedType == typeof(float)) + { + var result = await GetFloatParameterValueCore(parameterName).ConfigureAwait(false); + return result is null ? null : (T?)(object)(float)result.Value; + } + + var integerValue = await GetParameterValue(parameterName).ConfigureAwait(false); + if (integerValue is null) { - cameraParametersCache = new Dictionary(); + return null; } + + object convertedValue = requestedType switch + { + _ when requestedType == typeof(long) => integerValue.Value, + _ when requestedType == typeof(int) => checked((int)integerValue.Value), + _ when requestedType == typeof(uint) => checked((uint)integerValue.Value), + _ when requestedType == typeof(short) => checked((short)integerValue.Value), + _ when requestedType == typeof(ushort) => checked((ushort)integerValue.Value), + _ when requestedType == typeof(byte) => checked((byte)integerValue.Value), + _ when requestedType == typeof(sbyte) => checked((sbyte)integerValue.Value), + _ => throw new NotSupportedException($"GetParameterValue<{requestedType.Name}> is not supported."), + }; + + return (T?)convertedValue; + } + + [Obsolete("Use GetParameterValue(parameterName) instead.")] + public async Task GetFloatParameterValue(string parameterName) + { + return await GetFloatParameterValueCore(parameterName).ConfigureAwait(false); + } + + private async Task GetFloatParameterValueCore(string parameterName) + { ICategory parameter = await GetParameter(parameterName).ConfigureAwait(false); if (parameter == null) { return null; } - - return await parameter.PValue.GetValueAsync().ConfigureAwait(false); + + if (parameter is IFloat floatParameter) + { + return await floatParameter.GetValueAsync().ConfigureAwait(false); + } + + if (parameter.PValue is IDoubleValue doubleValue) + { + return await doubleValue.GetDoubleValueAsync().ConfigureAwait(false); + } + + if (parameter.PValue is not null) + { + var result = await parameter.PValue.GetValueAsync().ConfigureAwait(false); + return result; + } + + return null; + } + + [Obsolete("Use GetParameterValue(parameterName) instead.")] + public async Task GetBooleanParameterValue(string parameterName) + { + return await GetBooleanParameterValueCore(parameterName).ConfigureAwait(false); } - + + private async Task GetBooleanParameterValueCore(string parameterName) + { + ICategory parameter = await GetParameter(parameterName).ConfigureAwait(false); + if (parameter == null) + { + return null; + } + + if (parameter is IBoolean boolParameter) + { + return await boolParameter.GetValueAsync().ConfigureAwait(false); + } + + if (parameter.PValue is null) + { + return null; + } + + var result = await parameter.PValue.GetValueAsync().ConfigureAwait(false); + return result switch + { + null => null, + 0 => false, + _ => true, + }; + } + /// /// Load a camera parameter /// @@ -617,7 +845,7 @@ public async Task GetParameterProperties(string parameterNam return parameter.CategoryProperties; } - + /// /// Obtain the minimum value allowed for the parameter. 0 if the parameter does not support it. /// @@ -626,7 +854,7 @@ public async Task GetParameterProperties(string parameterNam public async Task GetParameterMinValue(string parameterName) { ICategory parameter = await GetParameter(parameterName).ConfigureAwait(false); - + if (parameter == null) { return 0; @@ -636,11 +864,39 @@ public async Task GetParameterMinValue(string parameterName) { return 0; } - + var result = await parameter.PMin.GetValueAsync().ConfigureAwait(false); return result ?? 0; } - + + public async Task GetFloatParameterMinValue(string parameterName) + { + ICategory parameter = await GetParameter(parameterName).ConfigureAwait(false); + if (parameter == null) + { + return 0; + } + + if (parameter is IFloat floatParameter) + { + return await floatParameter.GetMinAsync().ConfigureAwait(false); + } + + if (parameter.PMin is IDoubleValue doubleMin) + { + var result = await doubleMin.GetDoubleValueAsync().ConfigureAwait(false); + return result ?? 0; + } + + if (parameter.PMin == null) + { + return 0; + } + + var fallback = await parameter.PMin.GetValueAsync().ConfigureAwait(false); + return fallback ?? 0; + } + /// /// Obtain the maximum value allowed for the parameter. 0 if the parameter does not support it. /// @@ -649,7 +905,7 @@ public async Task GetParameterMinValue(string parameterName) public async Task GetParameterMaxValue(string parameterName) { ICategory parameter = await GetParameter(parameterName).ConfigureAwait(false); - + if (parameter == null) { return 0; @@ -659,11 +915,39 @@ public async Task GetParameterMaxValue(string parameterName) { return 0; } - + var result = await parameter.PMax.GetValueAsync().ConfigureAwait(false); return result ?? 0; } - + + public async Task GetFloatParameterMaxValue(string parameterName) + { + ICategory parameter = await GetParameter(parameterName).ConfigureAwait(false); + if (parameter == null) + { + return 0; + } + + if (parameter is IFloat floatParameter) + { + return await floatParameter.GetMaxAsync().ConfigureAwait(false); + } + + if (parameter.PMax is IDoubleValue doubleMax) + { + var result = await doubleMax.GetDoubleValueAsync().ConfigureAwait(false); + return result ?? 0; + } + + if (parameter.PMax == null) + { + return 0; + } + + var fallback = await parameter.PMax.GetValueAsync().ConfigureAwait(false); + return fallback ?? 0; + } + /// /// Get the description of the parameter /// @@ -671,12 +955,14 @@ public async Task GetParameterMaxValue(string parameterName) /// public async Task GetParameter(string parameterName) { - if (!cameraParametersCache.ContainsKey(parameterName) && ! await LoadParameter(parameterName).ConfigureAwait(false)) + if (!cameraParametersCache.TryGetValue(parameterName, out ICategory parameter) && + !await LoadParameter(parameterName).ConfigureAwait(false)) { return null; } - return cameraParametersCache[parameterName]; + cameraParametersCache.TryGetValue(parameterName, out parameter); + return parameter; } /// @@ -687,13 +973,106 @@ public async Task GetParameter(string parameterName) /// public async Task SetCameraParameter(string parameterName, long value) { - if (!cameraParametersCache.ContainsKey(parameterName) && ! await LoadParameter(parameterName).ConfigureAwait(false)) + ICategory parameter = await GetParameter(parameterName).ConfigureAwait(false); + if (parameter == null) + { + return false; + } + + if (parameter is IBoolean) + { + return await SetCameraParameter(parameterName, value != 0).ConfigureAwait(false); + } + + if (parameter is IFloat || parameter.PValue is IDoubleValue) + { + return await SetCameraParameter(parameterName, (double)value).ConfigureAwait(false); + } + + if (parameter.PValue is null) + { + return false; + } + + return IsSuccessfulReply(await parameter.PValue.SetValueAsync(value).ConfigureAwait(false)); + } + + public Task SetCameraParameter(string parameterName, double value) + { + return SetFloatParameterCore(parameterName, value); + } + + public Task SetCameraParameter(string parameterName, bool value) + { + return SetBooleanParameterCore(parameterName, value); + } + + [Obsolete("Use SetCameraParameter(parameterName, value) instead.")] + public async Task SetFloatParameter(string parameterName, double value) + { + return await SetFloatParameterCore(parameterName, value).ConfigureAwait(false); + } + + private async Task SetFloatParameterCore(string parameterName, double value) + { + ICategory parameter = await GetParameter(parameterName).ConfigureAwait(false); + if (parameter == null) { return false; } - - var result = await cameraParametersCache[parameterName].PValue.SetValueAsync(value).ConfigureAwait(false) as GvcpReply; - return result.Status == GvcpStatus.GEV_STATUS_SUCCESS; + + if (parameter.PValue is IDoubleValue doubleValue) + { + return IsSuccessfulReply(await doubleValue.SetDoubleValueAsync(value).ConfigureAwait(false)); + } + + if (parameter is IFloat floatParameter) + { + try + { + await floatParameter.SetValueAsync(value).ConfigureAwait(false); + return true; + } + catch (Exception ex) + { + Updates?.Invoke(this, ex.Message); + return false; + } + } + + if (parameter.PValue is not null) + { + return IsSuccessfulReply(await parameter.PValue.SetValueAsync((long)Math.Round(value, MidpointRounding.AwayFromZero)).ConfigureAwait(false)); + } + + return false; + } + + [Obsolete("Use SetCameraParameter(parameterName, value) instead.")] + public async Task SetBooleanParameter(string parameterName, bool value) + { + return await SetBooleanParameterCore(parameterName, value).ConfigureAwait(false); + } + + private async Task SetBooleanParameterCore(string parameterName, bool value) + { + ICategory parameter = await GetParameter(parameterName).ConfigureAwait(false); + if (parameter == null) + { + return false; + } + + if (parameter is IBoolean boolParameter) + { + return IsSuccessfulReply(await boolParameter.SetValueAsync(value).ConfigureAwait(false)); + } + + if (parameter.PValue is null) + { + return false; + } + + return IsSuccessfulReply(await parameter.PValue.SetValueAsync(value ? 1 : 0).ConfigureAwait(false)); } /// @@ -702,10 +1081,15 @@ public async Task SetCameraParameter(string parameterName, long value) /// public async Task SyncParameters(int syncAttempts = 1) { + await syncParametersSemaphore.WaitAsync().ConfigureAwait(false); try { if (!await Gvcp.ReadXmlFileAsync(IP)) + { return false; + } + + cameraParametersCache.Clear(); Width = (uint)await GetParameterValue(nameof(RegisterName.Width)).ConfigureAwait(false); Height = (uint)await GetParameterValue(nameof(RegisterName.Height)).ConfigureAwait(false); @@ -713,19 +1097,24 @@ public async Task SyncParameters(int syncAttempts = 1) OffsetY = (uint)await GetParameterValue(nameof(RegisterName.OffsetY)).ConfigureAwait(false); PixelFormat = (PixelFormat)(uint)await GetParameterValue(nameof(RegisterName.PixelFormat)).ConfigureAwait(false); bytesPerPixel = (uint)PixelFormatToBytesPerPixel(PixelFormat); - + return true; } catch (Exception ex) { Updates?.Invoke(this, ex.Message); } + finally + { + syncParametersSemaphore.Release(); + } + return false; } private void CalculateSingleRowPayload() { - Payload = 8 + 28 + (Width * bytesPerPixel); + Payload = (uint)(8 + 28 + PixelFormatHelper.GetLineSize((int)Width, (uint)PixelFormat)); } private async void CameraIpChanged(object sender, EventArgs e) @@ -742,9 +1131,7 @@ private void Init() private int PixelFormatToBytesPerPixel(PixelFormat pixelFormat) { - var rawValue = (int)pixelFormat; - var isBitHigh = IsBitSet(rawValue, 20); - return isBitHigh ? 2 : 1; + return PixelFormatHelper.GetBytesPerPixelRoundedUp((uint)pixelFormat); } private void SetRxBuffer() @@ -754,13 +1141,13 @@ private void SetRxBuffer() Array.Clear(rawBytes, 0, rawBytes.Length); } - if (!IsRawFrame && PixelFormat.ToString().Contains("Bayer")) + if (!IsRawFrame && PixelFormatHelper.IsBayerFormat((uint)PixelFormat)) { rawBytes = new byte[Width * Height * 3]; } else { - rawBytes = new byte[Width * Height * bytesPerPixel]; + rawBytes = new byte[PixelFormatHelper.GetFrameSize((int)Width, (int)Height, (uint)PixelFormat)]; } } @@ -796,11 +1183,17 @@ private void SetupReceiver() StreamReceiver.MissingPacketTolerance = MissingPacketTolerance; StreamReceiver.Updates = Updates; StreamReceiver.FrameReady = FrameReady; + StreamReceiver.FrameReadyWithInfo = FrameReadyWithInfo; } private void SetupRxThread() { StreamReceiver.StartRxThread(); } + + private static bool IsSuccessfulReply(IReplyPacket reply) + { + return reply is GvcpReply gvcpReply && gvcpReply.Status == GvcpStatus.GEV_STATUS_SUCCESS; + } } } \ No newline at end of file diff --git a/GigeVision.Core/Services/CustomPixelFormatInfo.cs b/GigeVision.Core/Services/CustomPixelFormatInfo.cs new file mode 100644 index 0000000..6576600 --- /dev/null +++ b/GigeVision.Core/Services/CustomPixelFormatInfo.cs @@ -0,0 +1,32 @@ +namespace GigeVision.Core.Services +{ + /// + /// Describes a vendor-specific pixel format that is not part of the standard PFNC table. + /// Register instances with . + /// + public sealed class CustomPixelFormatInfo + { + /// Human-readable name, e.g. "QOI_BayerRG8". + public string Name { get; } + + /// + /// Effective bits consumed per pixel for buffer-size calculation. + /// For losslessly-compressed formats transmit as a worst-case uncompressed size + /// (e.g. 8 for QOI_BayerRG8). + /// + public int EffectiveBitsPerPixel { get; } + + /// + /// True when the format carries a Bayer colour-filter-array pattern and the + /// library should allocate a 3-channel (RGB) debayer output buffer. + /// + public bool IsBayer { get; } + + public CustomPixelFormatInfo(string name, int effectiveBitsPerPixel, bool isBayer) + { + Name = name; + EffectiveBitsPerPixel = effectiveBitsPerPixel; + IsBayer = isBayer; + } + } +} diff --git a/GigeVision.Core/Services/GenPort.cs b/GigeVision.Core/Services/GenPort.cs index ea0164a..f74b9de 100644 --- a/GigeVision.Core/Services/GenPort.cs +++ b/GigeVision.Core/Services/GenPort.cs @@ -1,4 +1,5 @@ using GenICam; +using GigeVision.Core.Enums; using GigeVision.Core.Interfaces; using GigeVision.Core.Models; using System; @@ -25,16 +26,16 @@ public async Task ReadAsync(long? address, long length) try { GvcpReply reply; - var addressBytes = GetAddressBytes((long)address, length); + var addressBytes = GetAddressBytes((long)address); Array.Reverse(addressBytes); - if (length >= 8) + if (length == 4 && ((long)address % 4) == 0) { - reply = await Gvcp.ReadMemoryAsync(Gvcp.CameraIp, addressBytes, (ushort)length).ConfigureAwait(false); + reply = await Gvcp.ReadRegisterAsync(addressBytes).ConfigureAwait(false); } else { - reply = await Gvcp.ReadRegisterAsync(addressBytes).ConfigureAwait(false); + reply = await Gvcp.ReadMemoryAsync(Gvcp.CameraIp, addressBytes, (ushort)length).ConfigureAwait(false); } return reply; @@ -53,14 +54,21 @@ public async Task WriteAsync(byte[] pBuffer, long? address, long l } try { - var addressBytes = GetAddressBytes((long)address, length); - Array.Reverse(addressBytes); - //await Gvcp.TakeControl(false); - return await Gvcp.WriteRegisterAsync(addressBytes, BitConverter.ToUInt32(pBuffer)).ConfigureAwait(false); + if (pBuffer == null) + { + throw new GvcpException(message: "missing write buffer.", new NullReferenceException()); + } + + if (length <= 0 || pBuffer.Length < length) + { + throw new GvcpException(message: "invalid write length.", new ArgumentOutOfRangeException(nameof(length))); + } + + return await WriteBufferAsync((long)address, pBuffer, (int)length).ConfigureAwait(false); } catch (Exception ex) { - throw new GvcpException(message: "failed to read register.", ex); + throw new GvcpException(message: "failed to write register.", ex); } finally { @@ -68,21 +76,53 @@ public async Task WriteAsync(byte[] pBuffer, long? address, long l } } - private byte[] GetAddressBytes(Int64 address, Int64 length) + private async Task WriteBufferAsync(long address, byte[] buffer, int length) { - try + GvcpReply lastReply = new(); + + for (int offset = 0; offset < length;) { - switch (length) + long chunkAddress = address + offset; + long alignedAddress = chunkAddress & ~0x3L; + int byteOffset = (int)(chunkAddress - alignedAddress); + int bytesToWrite = Math.Min(4 - byteOffset, length - offset); + byte[] wordBuffer = new byte[4]; + + if (byteOffset != 0 || bytesToWrite != 4) { - case 2: - return BitConverter.GetBytes((Int16)address); + GvcpReply currentWord = await Gvcp.ReadRegisterAsync(FormatAddress(alignedAddress)).ConfigureAwait(false); + if (currentWord.Status != GvcpStatus.GEV_STATUS_SUCCESS) + { + return currentWord; + } + + byte[] currentBytes = ToBigEndianBytes(currentWord.RegisterValue); + Array.Copy(currentBytes, wordBuffer, wordBuffer.Length); + } - case 4: - return BitConverter.GetBytes((Int32)address); + Array.Copy(buffer, offset, wordBuffer, byteOffset, bytesToWrite); + uint valueToWrite = ToUInt32BigEndian(wordBuffer, 0); + string targetAddress = FormatAddress(alignedAddress); + lastReply = alignedAddress == chunkAddress && bytesToWrite == 4 + ? await Gvcp.WriteRegisterAsync(targetAddress, valueToWrite).ConfigureAwait(false) + : await Gvcp.WriteMemoryAsync(targetAddress, valueToWrite).ConfigureAwait(false); - default: - return BitConverter.GetBytes((Int32)address); + if (lastReply.Status != GvcpStatus.GEV_STATUS_SUCCESS) + { + return lastReply; } + + offset += bytesToWrite; + } + + return lastReply; + } + + private byte[] GetAddressBytes(Int64 address) + { + try + { + return BitConverter.GetBytes((Int32)address); } catch (InvalidCastException ex) { @@ -93,5 +133,29 @@ private byte[] GetAddressBytes(Int64 address, Int64 length) throw new GvcpException(message: "failed to address value.", ex); } } + + private static string FormatAddress(long address) + { + return $"0x{address:X8}"; + } + + private static byte[] ToBigEndianBytes(uint value) + { + return new[] + { + (byte)((value >> 24) & 0xFF), + (byte)((value >> 16) & 0xFF), + (byte)((value >> 8) & 0xFF), + (byte)(value & 0xFF), + }; + } + + private static uint ToUInt32BigEndian(byte[] buffer, int offset) + { + return (uint)((buffer[offset] << 24) | + (buffer[offset + 1] << 16) | + (buffer[offset + 2] << 8) | + buffer[offset + 3]); + } } } \ No newline at end of file diff --git a/GigeVision.Core/Services/Gvcp.cs b/GigeVision.Core/Services/Gvcp.cs index 597d18b..e7ac4e1 100644 --- a/GigeVision.Core/Services/Gvcp.cs +++ b/GigeVision.Core/Services/Gvcp.cs @@ -10,6 +10,7 @@ using System.Net; using System.Net.Sockets; using System.Text; +using System.Threading; using System.Threading.Tasks; using System.Xml; using GigeVision.Core.Models; @@ -25,10 +26,12 @@ public class Gvcp : IGvcp private List cameraInfoList; private string cameraIP = ""; - private ushort gvcpRequestID = 1; + private int gvcpRequestID; private bool isHeartBeatThreadRunning; + private readonly SemaphoreSlim controlSocketLock = new(1, 1); + private XmlHelper xmlHelper; /// @@ -39,7 +42,7 @@ public Gvcp(string ip) { CameraIp = ip; } - + /// /// Gvcp constructor, initializes camera IP and socket timeout, and try to get register values /// @@ -58,7 +61,7 @@ public Gvcp() { ReceiveTimeoutInMilliseconds = 1000; } - + /// /// The socket read timeout in milliseconds. Set -1 for infinite timeout /// @@ -251,19 +254,27 @@ public async Task> GetAllGigeDevicesInNetworkAsnyc(strin return tuple; } - if (category.PValue is IPValue pValue) + if (category is IPValue categoryValue) + { + tuple.pValue = categoryValue; + } + else if (category.PValue is IPValue pValue) { tuple.pValue = pValue; } - - if (category.PValue is IRegister register) + + if (category is IRegister categoryRegister) + { + tuple.register = categoryRegister; + } + else if (category.PValue is IRegister register) { tuple.register = register; } return tuple; } - + public async Task GetRegisterCategory(string name) { return await xmlHelper.GetRegisterByName(name); @@ -321,11 +332,12 @@ public async Task ReadAllRegisterAddressFromCameraAsync(string cameraIp) RegistersDictionary = new Dictionary(); await ReadAllRegisters(CategoryDictionary); } - }; + } + ; } catch (Exception ex) { - throw ex; + throw; } finally { @@ -351,10 +363,8 @@ public async Task ReadMemoryAsync(string ip, byte[] memoryAddress, us { if (ValidateIp(ip)) { - GvcpCommand command = new(memoryAddress, GvcpCommandType.ReadMem, requestID: gvcpRequestID++, count: count); - using UdpClient socket = new(); - socket.Client.ReceiveTimeout = ReceiveTimeoutInMilliseconds; - socket.Connect(ip, 3956); + GvcpCommand command = new(memoryAddress, GvcpCommandType.ReadMem, requestID: GetNextRequestId(), count: count); + using UdpClient socket = CreateConnectedSocket(ip); return await SendGvcpCommand(socket, command).ConfigureAwait(false); } else @@ -378,11 +388,8 @@ public async Task ReadRegisterAsync(string ip, byte[] registerAddress { if (ValidateIp(ip)) { - GvcpCommand command = new(registerAddress, GvcpCommandType.ReadReg, requestID: gvcpRequestID++); - using UdpClient socket = new(); - socket.Client.ReceiveTimeout = 1000; - socket.Connect(ip, 3956); - return await SendGvcpCommand(socket, command).ConfigureAwait(false); + using UdpClient socket = CreateConnectedSocket(ip); + return await ReadRegisterAsync(socket, registerAddress).ConfigureAwait(false); } else { @@ -526,7 +533,7 @@ public async Task TakeControl(bool KeepAlive = true) /// Command Status public async Task WriteMemoryAsync(string memoryAddress, uint valueToWrite) { - GvcpCommand gvcpCommand = new GvcpCommand(Converter.RegisterStringToByteArray(memoryAddress), GvcpCommandType.WriteMem, valueToWrite, gvcpRequestID++); + GvcpCommand gvcpCommand = new GvcpCommand(Converter.RegisterStringToByteArray(memoryAddress), GvcpCommandType.WriteMem, valueToWrite, GetNextRequestId()); return await WriteMemory(ControlSocket, gvcpCommand).ConfigureAwait(false); } @@ -536,7 +543,7 @@ public async Task WriteMemoryAsync(string memoryAddress, uint valueTo /// Command Status public async Task WriteRegisterAsync(UdpClient socket, byte[] registerAddress, uint valueToWrite) { - GvcpCommand gvcpCommand = new GvcpCommand(registerAddress, GvcpCommandType.WriteReg, valueToWrite, gvcpRequestID++); + GvcpCommand gvcpCommand = new GvcpCommand(registerAddress, GvcpCommandType.WriteReg, valueToWrite, GetNextRequestId()); return await WriteRegister(socket, gvcpCommand).ConfigureAwait(false); } @@ -546,7 +553,7 @@ public async Task WriteRegisterAsync(UdpClient socket, byte[] registe /// Command Status public async Task WriteRegisterAsync(UdpClient socket, string registerAddress, uint valueToWrite) { - GvcpCommand gvcpCommand = new GvcpCommand(Converter.RegisterStringToByteArray(registerAddress), GvcpCommandType.WriteReg, valueToWrite, gvcpRequestID++); + GvcpCommand gvcpCommand = new GvcpCommand(Converter.RegisterStringToByteArray(registerAddress), GvcpCommandType.WriteReg, valueToWrite, GetNextRequestId()); return await WriteRegister(socket, gvcpCommand).ConfigureAwait(false); } @@ -556,7 +563,7 @@ public async Task WriteRegisterAsync(UdpClient socket, string registe /// Command Status public async Task WriteRegisterAsync(UdpClient socket, string[] registerAddress, uint[] valuesToWrite) { - GvcpCommand gvcpCommand = new GvcpCommand(registerAddress, valuesToWrite, gvcpRequestID++); + GvcpCommand gvcpCommand = new GvcpCommand(registerAddress, valuesToWrite, GetNextRequestId()); return await WriteRegister(socket, gvcpCommand).ConfigureAwait(false); } @@ -566,7 +573,7 @@ public async Task WriteRegisterAsync(UdpClient socket, string[] regis /// Command Status public async Task WriteRegisterAsync(UdpClient socket, GvcpRegister register, uint valueToWrite) { - GvcpCommand gvcpCommand = new GvcpCommand(Converter.RegisterStringToByteArray(register.ToString("X")), GvcpCommandType.WriteReg, valueToWrite, gvcpRequestID++); + GvcpCommand gvcpCommand = new GvcpCommand(Converter.RegisterStringToByteArray(register.ToString("X")), GvcpCommandType.WriteReg, valueToWrite, GetNextRequestId()); return await WriteRegister(socket, gvcpCommand).ConfigureAwait(false); } @@ -576,17 +583,14 @@ public async Task WriteRegisterAsync(UdpClient socket, GvcpRegister r /// Command Status public async Task WriteRegisterAsync(string Ip, byte[] registerAddress, uint valueToWrite) { - UdpClient socket = new UdpClient(Ip, PortGvcp); - socket.Client.ReceiveTimeout = 1000; + using UdpClient socket = CreateConnectedSocket(Ip); if (await GetControlAsync(socket).ConfigureAwait(false)) { - GvcpCommand gvcpCommand = new GvcpCommand(registerAddress, GvcpCommandType.WriteReg, valueToWrite, gvcpRequestID++); + GvcpCommand gvcpCommand = new GvcpCommand(registerAddress, GvcpCommandType.WriteReg, valueToWrite, GetNextRequestId()); return await WriteRegister(socket, gvcpCommand).ConfigureAwait(false); } - else - { - return new GvcpReply() { Status = GvcpStatus.GEV_STATUS_ACCESS_DENIED }; - } + + return new GvcpReply() { Status = GvcpStatus.GEV_STATUS_ACCESS_DENIED }; } /// @@ -595,17 +599,14 @@ public async Task WriteRegisterAsync(string Ip, byte[] registerAddres /// Command Status public async Task WriteRegisterAsync(string Ip, string registerAddress, uint valueToWrite) { - UdpClient socket = new UdpClient(Ip, PortGvcp); - socket.Client.ReceiveTimeout = 1000; + using UdpClient socket = CreateConnectedSocket(Ip); if (await GetControlAsync(socket).ConfigureAwait(false)) { - GvcpCommand gvcpCommand = new GvcpCommand(Converter.RegisterStringToByteArray(registerAddress), GvcpCommandType.WriteReg, valueToWrite, gvcpRequestID++); + GvcpCommand gvcpCommand = new GvcpCommand(Converter.RegisterStringToByteArray(registerAddress), GvcpCommandType.WriteReg, valueToWrite, GetNextRequestId()); return await WriteRegister(socket, gvcpCommand).ConfigureAwait(false); } - else - { - return new GvcpReply() { Status = GvcpStatus.GEV_STATUS_ACCESS_DENIED }; - } + + return new GvcpReply() { Status = GvcpStatus.GEV_STATUS_ACCESS_DENIED }; } /// @@ -614,18 +615,14 @@ public async Task WriteRegisterAsync(string Ip, string registerAddres /// Command Status public async Task WriteRegisterAsync(string Ip, string[] registerAddress, uint[] valuesToWrite) { - // await Task.Delay(100).ConfigureAwait(false); - UdpClient socket = new UdpClient(Ip, PortGvcp); - socket.Client.ReceiveTimeout = 1000; + using UdpClient socket = CreateConnectedSocket(Ip); if (await GetControlAsync(socket).ConfigureAwait(false)) { - GvcpCommand gvcpCommand = new GvcpCommand(registerAddress, valuesToWrite, gvcpRequestID++); + GvcpCommand gvcpCommand = new GvcpCommand(registerAddress, valuesToWrite, GetNextRequestId()); return await WriteRegister(socket, gvcpCommand).ConfigureAwait(false); } - else - { - return new GvcpReply() { Status = GvcpStatus.GEV_STATUS_ACCESS_DENIED }; - } + + return new GvcpReply() { Status = GvcpStatus.GEV_STATUS_ACCESS_DENIED }; } /// @@ -634,17 +631,14 @@ public async Task WriteRegisterAsync(string Ip, string[] registerAddr /// Command Status public async Task WriteRegisterAsync(string Ip, GvcpRegister register, uint valueToWrite) { - UdpClient socket = new UdpClient(Ip, PortGvcp); - socket.Client.ReceiveTimeout = 1000; + using UdpClient socket = CreateConnectedSocket(Ip); if (await GetControlAsync(socket).ConfigureAwait(false)) { - GvcpCommand gvcpCommand = new GvcpCommand(Converter.RegisterStringToByteArray(register.ToString("X")), GvcpCommandType.WriteReg, valueToWrite, gvcpRequestID++); + GvcpCommand gvcpCommand = new GvcpCommand(Converter.RegisterStringToByteArray(register.ToString("X")), GvcpCommandType.WriteReg, valueToWrite, GetNextRequestId()); return await WriteRegister(socket, gvcpCommand).ConfigureAwait(false); } - else - { - return new GvcpReply() { Status = GvcpStatus.GEV_STATUS_ACCESS_DENIED }; - } + + return new GvcpReply() { Status = GvcpStatus.GEV_STATUS_ACCESS_DENIED }; } /// @@ -653,7 +647,7 @@ public async Task WriteRegisterAsync(string Ip, GvcpRegister register /// Command Status public async Task WriteRegisterAsync(byte[] registerAddressOrKey, uint valueToWrite) { - GvcpCommand gvcpCommand = new GvcpCommand(registerAddressOrKey, GvcpCommandType.WriteReg, valueToWrite, gvcpRequestID++); + GvcpCommand gvcpCommand = new GvcpCommand(registerAddressOrKey, GvcpCommandType.WriteReg, valueToWrite, GetNextRequestId()); return await WriteRegister(ControlSocket, gvcpCommand).ConfigureAwait(false); } @@ -663,7 +657,7 @@ public async Task WriteRegisterAsync(byte[] registerAddressOrKey, uin /// Command Status public async Task WriteRegisterAsync(string registerAddress, uint valueToWrite) { - GvcpCommand gvcpCommand = new GvcpCommand(Converter.RegisterStringToByteArray(registerAddress), GvcpCommandType.WriteReg, valueToWrite, gvcpRequestID++); + GvcpCommand gvcpCommand = new GvcpCommand(Converter.RegisterStringToByteArray(registerAddress), GvcpCommandType.WriteReg, valueToWrite, GetNextRequestId()); return await WriteRegister(ControlSocket, gvcpCommand).ConfigureAwait(false); } @@ -673,7 +667,7 @@ public async Task WriteRegisterAsync(string registerAddress, uint val /// Command Status public async Task WriteRegisterAsync(string[] registerAddress, uint[] valueToWrite) { - GvcpCommand gvcpCommand = new GvcpCommand(registerAddress, valueToWrite, gvcpRequestID++); + GvcpCommand gvcpCommand = new GvcpCommand(registerAddress, valueToWrite, GetNextRequestId()); return await WriteRegister(ControlSocket, gvcpCommand).ConfigureAwait(false); } @@ -683,23 +677,68 @@ public async Task WriteRegisterAsync(string[] registerAddress, uint[] /// Command Status public async Task WriteRegisterAsync(GvcpRegister register, uint valueToWrite) { - GvcpCommand gvcpCommand = new GvcpCommand(Converter.RegisterStringToByteArray(register.ToString("X")), GvcpCommandType.WriteReg, valueToWrite, gvcpRequestID++); + GvcpCommand gvcpCommand = new GvcpCommand(Converter.RegisterStringToByteArray(register.ToString("X")), GvcpCommandType.WriteReg, valueToWrite, GetNextRequestId()); return await WriteRegister(ControlSocket, gvcpCommand).ConfigureAwait(false); } - private static async Task SendGvcpCommand(UdpClient socketTx, GvcpCommand command) + private async Task SendGvcpCommand(UdpClient socketTx, GvcpCommand command) { GvcpReply gvcpReply = new(); - await socketTx.SendAsync(command.CommandBytes, command.Length).ConfigureAwait(false); - gvcpReply.IsSent = true; - Task reply = socketTx.ReceiveAsync(); - if (await Task.WhenAny(reply, Task.Delay(socketTx.Client.ReceiveTimeout)).ConfigureAwait(false) == reply) + SemaphoreSlim commandLock = ReferenceEquals(socketTx, ControlSocket) ? controlSocketLock : null; + if (commandLock != null) { - gvcpReply.DetectCommand(reply.Result.Buffer); - gvcpReply.IPSender = reply.Result.RemoteEndPoint.Address.ToString(); - gvcpReply.PortSender = reply.Result.RemoteEndPoint.Port; + await commandLock.WaitAsync().ConfigureAwait(false); + } + + try + { + await socketTx.SendAsync(command.CommandBytes, command.Length).ConfigureAwait(false); + gvcpReply.IsSent = true; + var receiveTimeout = GetSocketReceiveTimeout(socketTx); + var deadline = receiveTimeout == Timeout.Infinite ? DateTime.MaxValue : DateTime.UtcNow.AddMilliseconds(receiveTimeout); + + while (true) + { + Task replyTask = socketTx.ReceiveAsync(); + if (receiveTimeout != Timeout.Infinite) + { + TimeSpan remaining = deadline - DateTime.UtcNow; + if (remaining <= TimeSpan.Zero) + { + ObserveFault(replyTask); + gvcpReply.Error = $"Timed out waiting for {command.Type} acknowledgement."; + ResetSharedControlSocket(socketTx); + return gvcpReply; + } + + if (await Task.WhenAny(replyTask, Task.Delay(remaining)).ConfigureAwait(false) != replyTask) + { + ObserveFault(replyTask); + gvcpReply.Error = $"Timed out waiting for {command.Type} acknowledgement."; + ResetSharedControlSocket(socketTx); + return gvcpReply; + } + } + + UdpReceiveResult reply = await replyTask.ConfigureAwait(false); + gvcpReply = new GvcpReply(); + gvcpReply.IsSent = true; + gvcpReply.DetectCommand(reply.Buffer); + gvcpReply.IPSender = reply.RemoteEndPoint.Address.ToString(); + gvcpReply.PortSender = reply.RemoteEndPoint.Port; + + if (gvcpReply.AcknowledgementID != command.RequestId) + { + continue; + } + + return gvcpReply; + } + } + finally + { + commandLock?.Release(); } - return gvcpReply; } /// @@ -759,6 +798,11 @@ private static Stream UnZipEncodedZipFile(byte[] encodedZipFile) private CameraInformation DecodeDiscoveryPacket(byte[] discoveryPacket) { + if (discoveryPacket == null || discoveryPacket.Length < 256) + { + return null; + } + var cameraInfo = new CameraInformation() { IP = discoveryPacket[44].ToString() + "." + discoveryPacket[45].ToString() + "." + discoveryPacket[46].ToString() + "." + discoveryPacket[47].ToString() @@ -815,21 +859,32 @@ private CameraInformation DecodeDiscoveryPacket(byte[] discoveryPacket) private void DiscoveryReception(IPEndPoint localEndpoint, byte[] data) { var camera = DecodeDiscoveryPacket(data); + if (camera == null) + { + return; + } + camera.NetworkIP = localEndpoint.Address.ToString(); cameraInfoList.Add(camera); } #region Read All Registers Address XML + private async Task ReadRegisterAsync(UdpClient socket, byte[] registerAddress) + { + GvcpCommand command = new(registerAddress, GvcpCommandType.ReadReg, requestID: GetNextRequestId()); + return await SendGvcpCommand(socket, command).ConfigureAwait(false); + } + private async Task GetControlAsync(UdpClient socket) { - var currentStatus = await ReadRegisterAsync(Converter.RegisterStringToByteArray(GvcpRegister.GevCCP.ToString("X"))).ConfigureAwait(false); + var currentStatus = await ReadRegisterAsync(socket, Converter.RegisterStringToByteArray(GvcpRegister.GevCCP.ToString("X"))).ConfigureAwait(false); if (currentStatus.IsValid) { if (currentStatus.RegisterValue == 0)//Its free and can be controlled { GvcpCommand controlCommand = new(Converter.RegisterStringToByteArray(GvcpRegister.GevCCP.ToString("X")), - GvcpCommandType.WriteReg, 2, gvcpRequestID++); + GvcpCommandType.WriteReg, 2, GetNextRequestId()); var reply = await SendGvcpCommand(socket, controlCommand).ConfigureAwait(false); return reply.Status == GvcpStatus.GEV_STATUS_SUCCESS; } @@ -851,23 +906,12 @@ private async Task GetControlAsync(UdpClient socket) { return await Task.Run(() => { - UdpClient client = new UdpClient(); - client.Client.ReceiveTimeout = 1000; - //connecting to the server - client.Connect(IP, PortGvcp); - - byte[] commandCCP = new byte[] { 0x42, 0x00, 0x00, 0x82, 0x00, 0x08, 0x10, 0x01, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x02 }; - - //sending the packet - // client.Send(commandCCP, commandCCP.Length); - Task.Delay(100); - //preparing the header for sending - - byte[] gvcpHeader = GetReadMessageHeader(0x0200); //GevFirstURL = 0x0200 + using UdpClient client = CreateConnectedSocket(IP); + Thread.Sleep(100); + ushort headerRequestId = GetNextRequestId(); + byte[] gvcpHeader = GetReadMessageHeader(0x0200, headerRequestId); - //sending the packet client.Send(gvcpHeader, gvcpHeader.Length); - gvcpRequestID++; int packetSize = 512 + 24; //512 for the original payload size and 24 for the header byte[] count = { 0x02, 0x18 }; //Number of bytes to read from device memory it must be multiple of 4 bytes @@ -897,7 +941,7 @@ private async Task GetControlAsync(UdpClient socket) { count = i == (fileLength - lastPacket) ? BitConverter.GetBytes(lastPacket) : BitConverter.GetBytes(packetSize); - byte[] requestID = BitConverter.GetBytes(gvcpRequestID); + byte[] requestID = BitConverter.GetBytes(GetNextRequestId()); byte[] tempFileAddress = BitConverter.GetBytes(fileAddress + i); if (BitConverter.IsLittleEndian) @@ -918,7 +962,6 @@ private async Task GetControlAsync(UdpClient socket) if (recivedData[0] == (byte)0x00) { Array.Copy(recivedData, 12, encodedZipFile, i, recivedData.Length - 12); - gvcpRequestID++; } else break; @@ -928,11 +971,11 @@ private async Task GetControlAsync(UdpClient socket) }).ConfigureAwait(false); } - private byte[] GetReadMessageHeader(int register) + private byte[] GetReadMessageHeader(int register, ushort requestId) { //converting register and requestID into bytes byte[] tempRegister = BitConverter.GetBytes(register); - byte[] requestID = BitConverter.GetBytes((short)gvcpRequestID); + byte[] requestID = BitConverter.GetBytes(requestId); if (BitConverter.IsLittleEndian) { @@ -941,7 +984,7 @@ private byte[] GetReadMessageHeader(int register) } //preparing the header for sending - byte[] gvcpHeader = { 0x42, 0x01, 0x00, 0x84, 0x00, 0x08, requestID[0], requestID[1], 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00 }; + byte[] gvcpHeader = { 0x42, 0x01, 0x00, 0x84, 0x00, 0x08, requestID[0], requestID[1], tempRegister[0], tempRegister[1], tempRegister[2], tempRegister[3], 0x00, 0x00, 0x02, 0x00 }; return gvcpHeader; } @@ -971,8 +1014,6 @@ private async Task GetXmlFileFromCamera(string ip) break; } } - - gvcpRequestID++; return xmlFile; } @@ -1050,7 +1091,7 @@ private void Reconnect() try { ControlSocket = new UdpClient(cameraIP, PortGvcp); - ControlSocket.Client.ReceiveTimeout = 1000; + ControlSocket.Client.ReceiveTimeout = ReceiveTimeoutInMilliseconds > 0 ? ReceiveTimeoutInMilliseconds : 0; ControlSocket.Client.SendTimeout = 500; } catch (Exception) @@ -1068,12 +1109,12 @@ private void RunHeartbeatThread() Task.Run(async () => { isHeartBeatThreadRunning = true; - GvcpCommand command = new(Converter.RegisterStringToByteArray(GvcpRegister.GevCCP.ToString("X")), GvcpCommandType.ReadReg); while (IsKeepingAlive) { try { - _ = SendGvcpCommand(ControlSocket, command); + GvcpCommand command = new(Converter.RegisterStringToByteArray(GvcpRegister.GevCCP.ToString("X")), GvcpCommandType.ReadReg, requestID: GetNextRequestId()); + await SendGvcpCommand(ControlSocket, command).ConfigureAwait(false); if (!IsKeepingAlive) break; await Task.Delay(500).ConfigureAwait(false); if (!IsKeepingAlive) break; @@ -1121,7 +1162,7 @@ private async Task SendBroadCastPacket(byte[] packet, Action } foreach (var ipNetwork in ips) { - var broadcastClientLocal = new UdpClient() + using var broadcastClientLocal = new UdpClient() { EnableBroadcast = true, }; @@ -1172,16 +1213,7 @@ private bool ValidateIp(string ipString) /// private async Task WriteMemory(UdpClient socket, GvcpCommand gvcpCommand) { - await socket.SendAsync(gvcpCommand.CommandBytes, gvcpCommand.Length).ConfigureAwait(false); - var reply = await socket.ReceiveAsync().ConfigureAwait(false); - if (reply.Buffer?.Length > 0) - { - return new GvcpReply(reply.Buffer); - } - else - { - return new GvcpReply() { Error = "Couldn't Get Reply" }; - } + return await SendGvcpCommand(socket, gvcpCommand).ConfigureAwait(false); } /// @@ -1193,6 +1225,44 @@ private async Task WriteRegister(UdpClient socket, GvcpCommand gvcpCo return await SendGvcpCommand(socket, gvcpCommand).ConfigureAwait(false); } + private UdpClient CreateConnectedSocket(string ip) + { + UdpClient socket = new(); + socket.Client.ReceiveTimeout = ReceiveTimeoutInMilliseconds > 0 ? ReceiveTimeoutInMilliseconds : 0; + socket.Client.SendTimeout = 500; + socket.Connect(ip, PortGvcp); + return socket; + } + + private ushort GetNextRequestId() + { + return unchecked((ushort)Interlocked.Increment(ref gvcpRequestID)); + } + + private int GetSocketReceiveTimeout(UdpClient socket) + { + int receiveTimeout = socket.Client.ReceiveTimeout; + if (receiveTimeout > 0) + { + return receiveTimeout; + } + + return ReceiveTimeoutInMilliseconds > 0 ? ReceiveTimeoutInMilliseconds : Timeout.Infinite; + } + + private void ResetSharedControlSocket(UdpClient socket) + { + if (ReferenceEquals(socket, ControlSocket) && ValidateIp(cameraIP)) + { + Reconnect(); + } + } + + private static void ObserveFault(Task task) + { + _ = task.ContinueWith(t => _ = t.Exception, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously); + } + #endregion Write Register } } \ No newline at end of file diff --git a/GigeVision.Core/Services/PixelFormatHelper.cs b/GigeVision.Core/Services/PixelFormatHelper.cs new file mode 100644 index 0000000..76fba2d --- /dev/null +++ b/GigeVision.Core/Services/PixelFormatHelper.cs @@ -0,0 +1,90 @@ +using System; +using GigeVision.Core.Enums; + +namespace GigeVision.Core.Services +{ + public static class PixelFormatHelper + { + /// + /// Returns the human-readable name for a pixel format value. + /// Checks first, then the standard + /// enum, then falls back to hex. + /// + public static string GetName(uint pixelFormat) + { + if (PixelFormatRegistry.TryGet(pixelFormat, out var info)) + return info.Name; + + if (Enum.IsDefined(typeof(PixelFormat), pixelFormat)) + return ((PixelFormat)pixelFormat).ToString(); + + return $"0x{pixelFormat:X8}"; + } + + public static int GetBytesPerPixelRoundedUp(uint pixelFormat) + { + return DivideRoundUp(GetEffectiveBitsPerPixel(pixelFormat), 8); + } + + public static int GetFrameSize(int width, int height, uint pixelFormat) + { + if (width <= 0 || height <= 0) + { + return 0; + } + + long totalBits = (long)width * height * GetEffectiveBitsPerPixel(pixelFormat); + return (int)DivideRoundUp(totalBits, 8); + } + + public static int GetLineSize(int width, uint pixelFormat) + { + if (width <= 0) + { + return 0; + } + + long totalBits = (long)width * GetEffectiveBitsPerPixel(pixelFormat); + return (int)DivideRoundUp(totalBits, 8); + } + + /// + /// Returns true when the pixel format carries a Bayer CFA pattern. + /// Checks the first so that vendor-specific + /// Bayer formats (e.g. Lucid QOI_BayerRG8) are recognised correctly. + /// Falls back to testing whether the standard PFNC enum name contains "Bayer". + /// + public static bool IsBayerFormat(uint pixelFormat) + { + if (PixelFormatRegistry.TryGet(pixelFormat, out var info)) + { + return info.IsBayer; + } + + if (Enum.IsDefined(typeof(PixelFormat), pixelFormat)) + { + return ((PixelFormat)pixelFormat).ToString().Contains("Bayer"); + } + + return false; + } + + private static int GetEffectiveBitsPerPixel(uint pixelFormat) + { + // Registry entries take precedence — useful for compressed formats where + // the PFNC bits-per-pixel field does not reflect worst-case buffer requirements. + if (PixelFormatRegistry.TryGet(pixelFormat, out var info)) + { + return info.EffectiveBitsPerPixel > 0 ? info.EffectiveBitsPerPixel : 8; + } + + int effectiveBits = (int)((pixelFormat >> 16) & 0xFF); + return effectiveBits > 0 ? effectiveBits : 8; + } + + private static int DivideRoundUp(long value, int divisor) + { + return (int)((value + divisor - 1) / divisor); + } + } +} \ No newline at end of file diff --git a/GigeVision.Core/Services/PixelFormatRegistry.cs b/GigeVision.Core/Services/PixelFormatRegistry.cs new file mode 100644 index 0000000..6630452 --- /dev/null +++ b/GigeVision.Core/Services/PixelFormatRegistry.cs @@ -0,0 +1,42 @@ +using System.Collections.Concurrent; + +namespace GigeVision.Core.Services +{ + /// + /// Application-level registry for vendor-specific pixel formats that are not defined in + /// the standard PFNC/GigE Vision pixel-format table. + /// + /// Register a custom format once at startup before opening any camera stream: + /// + /// // Lucid QOI-compressed Bayer RG 8-bit + /// PixelFormatRegistry.Register(0x0108_0100, new CustomPixelFormatInfo("QOI_BayerRG8", 8, isBayer: true)); + /// + /// The registered effective-bits-per-pixel value is used for receive-buffer sizing. + /// The isBayer flag drives the 3-channel debayer output buffer when IsRawFrame is false. + /// + /// + public static class PixelFormatRegistry + { + private static readonly ConcurrentDictionary _formats = + new ConcurrentDictionary(); + + /// + /// Registers a vendor-specific pixel format. + /// Overwrites any previous registration for the same . + /// + /// 32-bit PFNC-style pixel-format code reported by the camera. + /// Metadata describing the format. + public static void Register(uint value, CustomPixelFormatInfo info) + { + _formats[value] = info; + } + + /// + /// Attempts to look up a previously registered custom format. + /// + public static bool TryGet(uint value, out CustomPixelFormatInfo info) + { + return _formats.TryGetValue(value, out info); + } + } +} diff --git a/GigeVision.Core/Services/StreamReceiverBase.cs b/GigeVision.Core/Services/StreamReceiverBase.cs index 41640c1..3b41c49 100644 --- a/GigeVision.Core/Services/StreamReceiverBase.cs +++ b/GigeVision.Core/Services/StreamReceiverBase.cs @@ -14,8 +14,12 @@ namespace GigeVision.Core.Services /// public abstract class StreamReceiverBase : BaseNotifyPropertyChanged, IStreamReceiver { + private const int MinimumReceiveBufferBytes = 8 * 1024 * 1024; protected Socket socketRxRaw; private DateTime lastFirewallPunchKeepAliveSent; + private const byte GvspLeaderPacketType = 0x01; + private const byte GvspDataPacketType = 0x03; + private const byte GvspDataEndPacketType = 0x02; /// /// Receives the GigeStream @@ -43,6 +47,12 @@ public StreamReceiverBase() /// public EventHandler FrameReady { get; set; } + /// + /// Fired alongside and carries per-frame metadata + /// (hardware timestamp and frame ID) extracted from the GVSP image leader packet. + /// + public EventHandler FrameReadyWithInfo { get; set; } + /// /// GVSP info for image info /// @@ -116,7 +126,7 @@ public void StopReception() { IsReceiving = false; socketRxRaw?.Close(); - socketRxRaw.Dispose(); + socketRxRaw?.Dispose(); } /// @@ -128,6 +138,7 @@ protected void DetectGvspType() socketRxRaw.Receive(singlePacket); GvspInfo.IsDecodingAsVersion2 = ((singlePacket[4] & 0xF0) >> 4) == 8; GvspInfo.SetDecodingTypeParameter(); + uint pixelFormat = 0; var packetID = (singlePacket[GvspInfo.PacketIDIndex] << 8) | singlePacket[GvspInfo.PacketIDIndex + 1]; if (packetID == 0) @@ -135,32 +146,58 @@ protected void DetectGvspType() GvspInfo.IsImageData = ((singlePacket[10] << 8) | singlePacket[11]) == 1; if (GvspInfo.IsImageData) { - GvspInfo.BytesPerPixel = (int)Math.Ceiling((double)(singlePacket[21] / 8)); + pixelFormat = (uint)((singlePacket[20] << 24) | (singlePacket[21] << 16) | (singlePacket[22] << 8) | singlePacket[23]); + GvspInfo.BytesPerPixel = PixelFormatHelper.GetBytesPerPixelRoundedUp(pixelFormat); GvspInfo.Width = (singlePacket[24] << 24) | (singlePacket[25] << 16) | (singlePacket[26] << 8) | (singlePacket[27]); GvspInfo.Height = (singlePacket[28] << 24) | (singlePacket[29] << 16) | (singlePacket[30] << 8) | (singlePacket[31]); } } - //Optimizing the array length for receive buffer - int length = socketRxRaw.Receive(singlePacket); - packetID = (singlePacket[GvspInfo.PacketIDIndex] << 8) | singlePacket[GvspInfo.PacketIDIndex + 1]; - if (packetID > 0) + // Probe a small packet window and keep the largest data-packet length. + // Startup can land on a short final packet, which would otherwise inflate FinalPacketID + // and produce false packet-loss reports on every frame. + int length = 0; + int maxPacketLength = 0; + int dataPacketSamples = 0; + const int packetLengthProbeCount = 64; + for (int probe = 0; probe < packetLengthProbeCount; probe++) + { + length = socketRxRaw.Receive(singlePacket); + packetID = (singlePacket[GvspInfo.PacketIDIndex] << 8) | singlePacket[GvspInfo.PacketIDIndex + 1]; + if (packetID > 0 && IsDataPacket(singlePacket[4])) + { + dataPacketSamples++; + if (length > maxPacketLength) + { + maxPacketLength = length; + } + + if (dataPacketSamples >= 2 && length == maxPacketLength) + { + break; + } + } + } + + if (maxPacketLength > 0) { - GvspInfo.PacketLength = length; + GvspInfo.PacketLength = maxPacketLength; } + IsReceiving = length > 10; GvspInfo.PayloadSize = GvspInfo.PacketLength - GvspInfo.PayloadOffset; if (GvspInfo.Width > 0 && GvspInfo.Height > 0) //Now we can calculate the final packet ID { - var totalBytesExpectedForOneFrame = GvspInfo.Width * GvspInfo.Height * GvspInfo.BytesPerPixel; + var totalBytesExpectedForOneFrame = PixelFormatHelper.GetFrameSize(GvspInfo.Width, GvspInfo.Height, pixelFormat); GvspInfo.FinalPacketID = totalBytesExpectedForOneFrame / GvspInfo.PayloadSize; if (totalBytesExpectedForOneFrame % GvspInfo.PayloadSize != 0) { GvspInfo.FinalPacketID++; } - socketRxRaw.ReceiveBufferSize = (GvspInfo.PacketLength * GvspInfo.FinalPacketID); //Single frame with GVSP header - GvspInfo.RawImageSize = GvspInfo.Width * GvspInfo.Height * GvspInfo.BytesPerPixel; + int recommendedReceiveBuffer = GvspInfo.PacketLength * GvspInfo.FinalPacketID * 4; + socketRxRaw.ReceiveBufferSize = Math.Max(MinimumReceiveBufferBytes, recommendedReceiveBuffer); + GvspInfo.RawImageSize = totalBytesExpectedForOneFrame; } } @@ -169,11 +206,13 @@ protected void DetectGvspType() /// protected virtual void Receiver() { - int packetID = 0, bufferIndex = 0, bufferLength = 0, bufferStart = 0, length = 0, packetRxCount = 1, packetRxCountClone, bufferIndexClone; + int packetID = 0, bufferIndex = 0, bufferLength = 0, bufferStart = 0, length = 0, packetRxCount = 0, packetRxCountClone, bufferIndexClone; ulong imageID, lastImageID = 0, lastImageIDClone, deltaImageID; + ulong currentFrameTimestamp = 0; byte[] blockID; byte[][] buffer = new byte[2][]; int frameCounter = 0; + bool skipUntilNextFrameBoundary = true; try { DetectGvspType(); @@ -184,8 +223,31 @@ protected virtual void Receiver() while (IsReceiving) { length = socketRxRaw.Receive(singlePacket); - if (singlePacket[4] == GvspInfo.DataIdentifier) //Packet + if (IsLeaderPacket(singlePacket[4])) { + // Extract 64-bit big-endian timestamp from the image leader. + // TimeStampIndex is already set correctly for both GVSP v1 and v2. + if (length > GvspInfo.TimeStampIndex + 7) + { + currentFrameTimestamp = + ((ulong)singlePacket[GvspInfo.TimeStampIndex] << 56) | + ((ulong)singlePacket[GvspInfo.TimeStampIndex + 1] << 48) | + ((ulong)singlePacket[GvspInfo.TimeStampIndex + 2] << 40) | + ((ulong)singlePacket[GvspInfo.TimeStampIndex + 3] << 32) | + ((ulong)singlePacket[GvspInfo.TimeStampIndex + 4] << 24) | + ((ulong)singlePacket[GvspInfo.TimeStampIndex + 5] << 16) | + ((ulong)singlePacket[GvspInfo.TimeStampIndex + 6] << 8) | + (ulong)singlePacket[GvspInfo.TimeStampIndex + 7]; + } + continue; + } + if (IsDataPacket(singlePacket[4])) //Packet + { + if (skipUntilNextFrameBoundary) + { + continue; + } + packetRxCount++; packetID = (singlePacket[GvspInfo.PacketIDIndex] << 8) | singlePacket[GvspInfo.PacketIDIndex + 1]; bufferStart = (packetID - 1) * GvspInfo.PayloadSize; //This use buffer length of regular packet @@ -193,18 +255,35 @@ protected virtual void Receiver() singlePacket.Slice(GvspInfo.PayloadOffset, bufferLength).CopyTo(buffer[bufferIndex].AsSpan().Slice(bufferStart, bufferLength)); continue; } - if (singlePacket[4] == GvspInfo.DataEndIdentifier) + if (IsDataEndPacket(singlePacket[4])) { + // Always read the per-frame actual packet count from the DataEnd packet. + // For fixed-size uncompressed formats this equals GvspInfo.FinalPacketID; for + // variable-length compressed formats (e.g. QOI_BayerRG8) it varies per frame + // and must not be compared against the pre-calculated static value, which + // assumes uncompressed size and would cause every compressed frame to be + // discarded as "missing packets". + int dataEndPacketID = (singlePacket[GvspInfo.PacketIDIndex] << 8) | singlePacket[GvspInfo.PacketIDIndex + 1]; + int thisFrameFinalPacketID = dataEndPacketID - 1; + if (GvspInfo.FinalPacketID == 0) { - packetID = (singlePacket[GvspInfo.PacketIDIndex] << 8) | singlePacket[GvspInfo.PacketIDIndex + 1]; - GvspInfo.FinalPacketID = packetID - 1; + GvspInfo.FinalPacketID = thisFrameFinalPacketID; } blockID = singlePacket.Slice(GvspInfo.BlockIDIndex, GvspInfo.BlockIDLength).ToArray(); Array.Reverse(blockID); Array.Resize(ref blockID, 8); imageID = BitConverter.ToUInt64(blockID); + + if (skipUntilNextFrameBoundary) + { + lastImageID = imageID; + packetRxCount = 0; + skipUntilNextFrameBoundary = false; + continue; + } + packetRxCountClone = packetRxCount; lastImageIDClone = lastImageID; bufferIndexClone = bufferIndex; @@ -212,32 +291,34 @@ protected virtual void Receiver() packetRxCount = 0; lastImageID = imageID; - if (DateTime.Now.Subtract(lastFirewallPunchKeepAliveSent).Seconds >= FirewallPunchKeepAliveIntervalInSeconds && FirewallPunchKeepAliveIntervalInSeconds > 0) + if (DateTime.Now.Subtract(lastFirewallPunchKeepAliveSent).Seconds >= FirewallPunchKeepAliveIntervalInSeconds && FirewallPunchKeepAliveIntervalInSeconds > 0 && CameraSourcePort > 0 && IPAddress.TryParse(CameraIP, out IPAddress cameraAddress)) { lastFirewallPunchKeepAliveSent = DateTime.Now; - socketRxRaw.SendTo(new byte[8], new IPEndPoint(IPAddress.Parse(CameraIP), CameraSourcePort)); + socketRxRaw.SendTo(new byte[8], new IPEndPoint(cameraAddress, CameraSourcePort)); } - Task.Run(() => + //Checking if we receive all packets + int packetCountDelta = packetRxCountClone - thisFrameFinalPacketID; + if (Math.Abs(packetCountDelta) <= MissingPacketTolerance) { - //Checking if we receive all packets - if (Math.Abs(packetRxCountClone - GvspInfo.FinalPacketID) <= MissingPacketTolerance) - { - ++frameCounter; - FrameReady?.Invoke(imageID, buffer[bufferIndex]); - } - else - { - Updates?.Invoke(UpdateType.FrameLoss, $"Image tx skipped because of {packetRxCountClone - GvspInfo.FinalPacketID} packet loss"); - } - - deltaImageID = imageID - lastImageIDClone; - //This <10000 is just to skip the overflow value when the counter (2 or 8 bytes) will complete it should not show false missing images - if (deltaImageID != 1 && deltaImageID < 10000) - { - Updates?.Invoke(UpdateType.FrameLoss, $"{imageID - lastImageIDClone - 1} Image missed between {lastImageIDClone}-{imageID}"); - } - }); + ++frameCounter; + FrameReady?.Invoke(imageID, buffer[bufferIndexClone]); + FrameReadyWithInfo?.Invoke(this, new GvspFrameInfo(imageID, currentFrameTimestamp)); + } + else + { + string frameLossMessage = packetCountDelta < 0 + ? $"Frame skipped because {Math.Abs(packetCountDelta)} packets were missing." + : $"Frame skipped because {packetCountDelta} unexpected extra packets were received."; + Updates?.Invoke(UpdateType.FrameLoss, frameLossMessage); + } + + deltaImageID = imageID - lastImageIDClone; + //This <10000 is just to skip the overflow value when the counter (2 or 8 bytes) will complete it should not show false missing images + if (deltaImageID != 1 && deltaImageID < 10000) + { + Updates?.Invoke(UpdateType.FrameLoss, $"{imageID - lastImageIDClone - 1} Image missed between {lastImageIDClone}-{imageID}"); + } } } } @@ -251,6 +332,21 @@ protected virtual void Receiver() } } + private static bool IsLeaderPacket(byte packetHeaderType) + { + return (packetHeaderType & 0x0F) == GvspLeaderPacketType; + } + + private static bool IsDataPacket(byte packetHeaderType) + { + return (packetHeaderType & 0x0F) == GvspDataPacketType; + } + + private static bool IsDataEndPacket(byte packetHeaderType) + { + return (packetHeaderType & 0x0F) == GvspDataEndPacketType; + } + /// /// Sets up socket parameters /// @@ -265,18 +361,23 @@ private void SetupSocketRxRaw() } socketRxRaw = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); socketRxRaw.Bind(new IPEndPoint(IPAddress.Any, PortRx)); - socketRxRaw.ReceiveTimeout = ReceiveTimeoutInMilliseconds; + PortRx = ((IPEndPoint)socketRxRaw.LocalEndPoint).Port; + socketRxRaw.ReceiveTimeout = ReceiveTimeoutInMilliseconds > 0 ? ReceiveTimeoutInMilliseconds : 0; - socketRxRaw.SendTo(new byte[8], new IPEndPoint(IPAddress.Parse(CameraIP), CameraSourcePort)); - lastFirewallPunchKeepAliveSent = DateTime.Now; + if (CameraSourcePort > 0 && IPAddress.TryParse(CameraIP, out IPAddress cameraAddress)) + { + socketRxRaw.SendTo(new byte[8], new IPEndPoint(cameraAddress, CameraSourcePort)); + lastFirewallPunchKeepAliveSent = DateTime.Now; + } if (IsMulticast) { MulticastOption mcastOption = new(IPAddress.Parse(MulticastIP), IPAddress.Parse(RxIP)); socketRxRaw.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, mcastOption); } - //One full hd image with GVSP2.0 Header as default, it will be updated for image type - socketRxRaw.ReceiveBufferSize = (int)(1920 * 1100); + // Use a generous default receive buffer so bursty UDP frame delivery does not overflow + // before the stream metadata is available to calculate a frame-sized buffer. + socketRxRaw.ReceiveBufferSize = MinimumReceiveBufferBytes; } catch (Exception ex) { diff --git a/GigeVision.Core/Services/StreamReceiverParallel.cs b/GigeVision.Core/Services/StreamReceiverParallel.cs index 08a05b0..27fcf2f 100644 --- a/GigeVision.Core/Services/StreamReceiverParallel.cs +++ b/GigeVision.Core/Services/StreamReceiverParallel.cs @@ -53,7 +53,7 @@ protected override async void Receiver() image = new byte[TotalBuffers][]; for (int i = 0; i < TotalBuffers; i++) { - image[i] = new byte[GvspInfo.Height * GvspInfo.Width * GvspInfo.BytesPerPixel]; + image[i] = new byte[GvspInfo.RawImageSize]; } indexMemoryWriter = 0; _ = Task.Run(DecodePackets); @@ -92,6 +92,7 @@ protected override async void Receiver() private void DecodePackets() { int indexMemoryReader, imageBufferIndex = 0, packetRxCount = 0, packetID, bufferStart, bufferLength; + bool skipUntilNextFrameBoundary = true; imageIndex = 0; lossCount = 0; var imageSpan = new Span(image[imageBufferIndex]); @@ -103,6 +104,10 @@ private void DecodePackets() } int finalPacketLength = image[imageBufferIndex].Length % GvspInfo.PayloadSize; + if (finalPacketLength == 0) + { + finalPacketLength = GvspInfo.PayloadSize; + } var length = GvspInfo.PacketLength; indexMemoryReader = 0; while (IsReceiving) @@ -118,6 +123,11 @@ private void DecodePackets() switch (packet[4] & 0x0F)//it unifies extended ID and normal ID { case 3: //Data + if (skipUntilNextFrameBoundary) + { + break; + } + packetRxCount++; packetID = (packet[GvspInfo.PacketIDIndex] << 8) | packet[GvspInfo.PacketIDIndex + 1]; bufferStart = (packetID - 1) * GvspInfo.PayloadSize; @@ -131,13 +141,27 @@ private void DecodePackets() case 2: //Data End imageIndex++; + if (skipUntilNextFrameBoundary) + { + packetRxCount = 0; + skipUntilNextFrameBoundary = false; + break; + } + //Checking if we receive all packets - if (Math.Abs(packetRxCount - GvspInfo.FinalPacketID) > MissingPacketTolerance) + int packetCountDelta = packetRxCount - GvspInfo.FinalPacketID; + if (Math.Abs(packetCountDelta) > MissingPacketTolerance) { lossCount++; + string frameLossMessage = packetCountDelta < 0 + ? $"Frame skipped because {Math.Abs(packetCountDelta)} packets were missing." + : $"Frame skipped because {packetCountDelta} unexpected extra packets were received."; + Updates?.Invoke(UpdateType.FrameLoss, frameLossMessage); packetRxCount = 0; break; } + + int completedBufferIndex = imageBufferIndex; packetRxCount = 0; frameInCounter++; waitHandleFrame.Release(); @@ -147,6 +171,7 @@ private void DecodePackets() imageBufferIndex = 0; } imageSpan = new Span(image[imageBufferIndex]); //Next Frame + FrameReady?.Invoke(imageIndex, image[completedBufferIndex]); break; } } diff --git a/GigeVision.OpenCV/GigeVision.OpenCV.csproj b/GigeVision.OpenCV/GigeVision.OpenCV.csproj index 1eb94e7..e9ead23 100644 --- a/GigeVision.OpenCV/GigeVision.OpenCV.csproj +++ b/GigeVision.OpenCV/GigeVision.OpenCV.csproj @@ -8,7 +8,7 @@ GigE Vision Implementation With OpenCV/EMGU support - 1.1 + 1.1-local GigeVisionLibraryOpenCV 1.0: GigE Vision Implementation With OpenCV/EMGU support @@ -17,7 +17,7 @@ - + diff --git a/README.md b/README.md index 65d82cd..7fa3b24 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,122 @@ # GigeVision -Simple GigeVision implementation, GVSP, GVCP protocol implemented -## How to use: +Simple GigE Vision implementation with GVSP and GVCP support. -### Get all cameras in the network +## Basic Usage - var camera = new Camera(); - var listOfDevices = await camera.Gvcp.GetAllGigeDevicesInNetworkAsnyc().ConfigureAwait(); +### Discover Cameras + +```csharp +var camera = new Camera(); +var listOfDevices = await camera.Gvcp.GetAllGigeDevicesInNetworkAsnyc().ConfigureAwait(false); +``` + +### Read Register + +```csharp +var gvcp = new Gvcp +{ + CameraIp = "192.168.10.99" +}; + +var reply = await gvcp.ReadRegisterAsync("0x0D04").ConfigureAwait(false); +``` -### Read Register - - var gvcp = new Gvcp(); - gvcp.CameraIP = "192.168.10.99"; - var reply = await gvcp.ReadRegisterAsync("0x0D04"); - ### Write Register -To write register you must take the control by Writing 0x02 to CCP register, you can write it manually or use the TakeControl Method from library. -If succeeded it will give you control for the time that is set in Heartbeat Register, send True as a parameter to keep the control status alive - - var gvcp = new Gvcp - { - CameraIp = "192.168.10.99" - }; - if (await gvcp.TakeControl()) //Send True as a parameter to keep alive - { - var reply = await gvcp.WriteRegisterAsync("0x0D04", 1000); - } - else - { - throw new Exception("Camera is already in control"); - } - + +To write a register you must take control by writing `0x02` to the CCP register. You can do that manually or use `TakeControl()` from the library. + +```csharp +var gvcp = new Gvcp +{ + CameraIp = "192.168.10.99" +}; + +if (await gvcp.TakeControl().ConfigureAwait(false)) +{ + var reply = await gvcp.WriteRegisterAsync("0x0D04", 1000).ConfigureAwait(false); +} +else +{ + throw new Exception("Camera is already in control"); +} +``` + ### Read Memory - var gvcp = new Gvcp(); - gvcp.CameraIP = "192.168.10.99"; - var reply = await gvcp.ReadMemoryAsync("0x0D04", 500); //To read 500 bytes from memory address 0x0D04 +```csharp +var gvcp = new Gvcp +{ + CameraIp = "192.168.10.99" +}; + +var reply = await gvcp.ReadMemoryAsync("0x0D04", 500).ConfigureAwait(false); +``` + +The `Gvcp` class has many overloads for register and memory operations. The implementation is in `GigeVision.Core/Services/Gvcp.cs`. + +## Streaming + +The current camera setup flow is property-based. + +```csharp +var camera = new Camera(); +camera.IP = "192.168.10.224"; +camera.RxIP = "192.168.10.221"; // Optional, but recommended on multi-NIC systems -Above mentioned samples are very basic, and in the Gvcp class there are too many method overloads for each function. Link here: https://github.com/Touseefelahi/GigeVision/blob/master/GigeVision.Core/Services/Gvcp.cs +bool synced = await camera.SyncParameters().ConfigureAwait(false); +if (!synced) +{ + throw new InvalidOperationException("Failed to load camera parameters."); +} +``` +Subscribe to the frame callback before starting the stream: +```csharp +camera.FrameReady += (sender, frame) => +{ + // frame contains the raw image bytes +}; +``` +Start the stream: -## Direct Use for Streaming, For a camera of ip 192.168.10.224 we need to initialize the camera objects as below: +```csharp +bool isStarted = await camera.StartStreamAsync(camera.RxIP).ConfigureAwait(false); +``` - var camera = new Camera("192.168.10.224"); - -To Receive the stream raw bytes we need to subscribe to the frame ready event, Make sure to have the same interface as the camera, if you have multiple interfaces you need to define it explicitly when starting the stream +## Unified Parameter API - camera.FrameReady += FrameReady; +The preferred public API is now: -To start the stream we need to call the method StartStream as: +```csharp +long? width = await camera.GetParameterValue(nameof(RegisterName.Width)).ConfigureAwait(false); +bool? reverseX = await camera.GetParameterValue("ReverseX").ConfigureAwait(false); +double? exposure = await camera.GetParameterValue("ExposureTime").ConfigureAwait(false); - //For multiple interfaces you can define the Rx IP as: (In this example 192.168.10.221 is the PC IP) - bool isStarted = await camera.StartStreamAsync("192.168.10.221").ConfigureAwait(false); +await camera.SetCameraParameter(nameof(RegisterName.Width), 1280L).ConfigureAwait(false); +await camera.SetCameraParameter("ReverseX", true).ConfigureAwait(false); +await camera.SetCameraParameter("ExposureTime", 5000.0).ConfigureAwait(false); +``` -Once the frame is fully received the FrameReady event will be invoked +The older typed helpers like `GetFloatParameterValue`, `SetFloatParameter`, `GetBooleanParameterValue`, and `SetBooleanParameter` are still present for compatibility, but new code should prefer the generic getter and the overloaded `SetCameraParameter` methods. - private void FrameReady(object sender, byte[] e) - { - // e contains the raw data in bytes - } - +## Force IP - ### Force IP - In this example we are setting the IP for first detected camera to fix IP to 192.168.10.243 +This example sets the IP of the first detected camera to `192.168.10.243`. - var camera = new Camera(); - var listOfDevices = await camera.Gvcp.GetAllGigeDevicesInNetworkAsnyc().ConfigureAwait(true); - if (listOfDevices.Count > 0) - { - camera.Gvcp.ForceIPAsync(listOfDevices[0].MacAddress, "192.168.10.243"); - } +```csharp +var camera = new Camera(); +var listOfDevices = await camera.Gvcp.GetAllGigeDevicesInNetworkAsnyc().ConfigureAwait(false); +if (listOfDevices.Count > 0) +{ + await camera.Gvcp.ForceIPAsync(listOfDevices[0].MacAddress, "192.168.10.243").ConfigureAwait(false); +} +``` -Simple WPF app example is here https://github.com/Touseefelahi/GigeVision/blob/master/GigeVisionLibrary.Test.Wpf/MainWindow.xaml.cs +## Examples -There are some issues with the namespaces it is conflicting Services and Models, it will be fixed someday +- Minimal working samples are in `examples.md`. +- A WPF sample app is in `GigeVisionLibrary.Test.Wpf/MainWindow.xaml.cs`. diff --git a/changes.md b/changes.md new file mode 100644 index 0000000..2e872a2 --- /dev/null +++ b/changes.md @@ -0,0 +1,79 @@ +# Implemented Changes + +Date: 2026-04-21 + +This file summarizes the fixes implemented from the `GigeVision.Core` audit. + +## GVCP Control Path + +- Serialized shared `ControlSocket` traffic with a `SemaphoreSlim` to prevent concurrent send/receive races. +- Replaced mutable `ushort` request ID usage with atomic `Interlocked.Increment` generation. +- Correlated replies using `AcknowledgementID` so commands only accept their own response. +- Reconnected the shared control socket after timeouts to avoid stale pending receives poisoning later commands. +- Made heartbeat traffic use the same awaited GVCP command path instead of fire-and-forget socket usage. +- Fixed `GetControlAsync()` so it reads control state through the same socket/target being written to. +- Disposed temporary GVCP sockets consistently and propagated the configured timeout into helper-created sockets. +- Fixed XML read-header generation so the requested register address is actually encoded in the packet. +- Added minimum-length validation for discovery packets before parsing fixed offsets. + +## Stream Reception + +- Fixed the default receiver buffer handoff so completed frames publish the finished buffer, not the live write buffer. +- Removed the asynchronous callback race in the default receiver by invoking frame delivery on the completed buffer directly. +- Changed startup frame handling to skip the first partial frame boundary after GVSP type detection. +- Made `StopReception()` null-safe when the receive socket was never created. +- Captured the real bound receive port from the socket after binding, enabling safe port-0 receiver startup. +- Guarded firewall keepalive traffic so it only sends when camera IP and source port are valid. +- Fixed `StreamReceiverParallel` image allocation to use computed raw image size. +- Fixed `StreamReceiverParallel` final-packet handling for exact-multiple payload sizes. + +## Pixel Format And Payload Sizing + +- Added `PixelFormatHelper` to derive byte and frame sizes from PFNC effective bits instead of the old bit-20 heuristic. +- Updated GVSP leader parsing to compute bytes-per-pixel and raw image size from PFNC metadata. +- Updated camera payload calculation to use per-line PFNC size. +- Updated receive-buffer allocation to use exact PFNC-based frame size. + +## Camera Control And Stream Startup + +- Wrapped `SetOffsetAsync()` control ownership in `try/finally` so control is always released on failure. +- Wrapped `SetResolutionAsync()` control ownership in `try/finally` so control is always released on failure. +- Reworked `StartStreamAsync()` rollback behavior so receiver startup and control ownership are cleaned up on failed setup. +- Removed the temporary free-port probe race by allowing the receiver socket to bind and then reading back the assigned port. +- Ensured receiver setup occurs before multicast fallback startup. +- Propagated the discovered `SCSPPort` into the receiver before reception. + +## GenICam Port Bridge + +- Fixed `GenPort` to always encode GVCP register addresses as 32-bit addresses. +- Restricted direct register reads to aligned 4-byte accesses and used memory reads otherwise. +- Replaced the old `BitConverter.ToUInt32(pBuffer)` write path with chunked read-modify-write handling for short or unaligned writes. +- Preserved surrounding bytes correctly when writing sub-register values. + +## GenICam Type Handling + +- Made `Converter` evaluation stateless so repeated reads and writes do not corrupt stored formula templates. +- Made `IntSwissKnife` evaluation stateless so repeated formula reads remain deterministic across calls. +- Added `IDoubleValue` and implemented it on converter-backed values, enabling engineering-unit reads and writes without forcing everything through `long`. +- Completed the usable `GenFloat` path so float categories can read current values, min/max, units, and writes through converter-backed or integer-backed `PValue` implementations. +- Reworked XML `pValue` resolution to dispatch by node type instead of only naming suffixes, making converter-backed and SwissKnife-backed features more robust across different vendor XMLs. + +## Camera Parameter APIs + +- Added typed camera helpers for float and boolean parameters alongside the existing integer helpers. +- Added float-aware min/max accessors so engineering-unit features can be queried without truncation. +- Added `SetCameraParameter` overloads for `double` and `bool`, so callers can use a single write method name across integer, converter-backed, and boolean features. +- Added `GetParameterValue` so callers can read `long`, `double`, and `bool` features through one typed entry point instead of separate getter names. +- Updated the `long` overload of `SetCameraParameter` to auto-dispatch to boolean or floating-point handling when the underlying feature is not an integer register. +- Kept the existing `long`-based parameter APIs intact for backward compatibility. + +## Protocol Constants And Reply Parsing + +- Corrected `GevSCSP` from `0x01C` to `0x0D1C`. +- Updated `GvcpReply` to capture `AcknowledgementID` before success short-circuiting. +- Tightened `ReadRegAck` parsing to distinguish valid single-register, multi-register, and malformed replies. + +## Validation + +- `dotnet build .\GigeVision.Core\GigeVision.Core.csproj -c Debug` succeeded. +- `GigeVision.CoreTests` was intentionally skipped per request. \ No newline at end of file diff --git a/chunkMode.md b/chunkMode.md new file mode 100644 index 0000000..042158a --- /dev/null +++ b/chunkMode.md @@ -0,0 +1,211 @@ +# GenICam Chunk Data Mode — Implementation Notes + +## What It Is + +When `ChunkModeActive = true` is set on the camera, each GVSP frame is extended with +a **chunk data region** appended after the last pixel byte. This region contains named +records (timestamp, exposure time, gain, white balance, ROI, etc.) defined per-camera +in the GenICam XML under the `ChunkDataControl` category. + +The GVSP trailer still arrives as a normal data-end packet. The chunk region sits inside +the data packets of the frame — it is additional payload that comes after the image bytes. + +--- + +## Wire Format (GigE Vision 2.0, Section 4.8) + +Each frame's chunk area is a sequence of records followed by a terminator: + +``` +[ChunkData_0] + 4 bytes ChunkID (big-endian uint32, e.g. 0x00000001 = Timestamp) + 4 bytes ChunkLength (big-endian uint32, number of data bytes following) + N bytes ChunkData + +[ChunkData_1] + ... + +[Terminator] + 4 bytes 0xD0000001 (end-of-chunks marker) + 4 bytes 0x00000000 +``` + +The chunk area begins at byte offset `RawImageSize` within the assembled frame buffer. +The terminator is always the last 8 bytes of the extended buffer. + +Standard ChunkIDs are defined in the SFNC; vendor-specific ones are listed in the +camera's GenICam XML under `ChunkID` enum entries. + +Lucid TRI032S standard chunks (from XML `ChunkDataControl`): +- `ChunkTimestamp` — `ChunkID = 0x00000001` — 8 bytes, uint64, camera ticks +- `ChunkExposureTime` — `ChunkID = 0x00010006` — 8 bytes, double, microseconds +- `ChunkGain` — `ChunkID = 0x00010007` — 8 bytes, double, dB +- `ChunkWidth` / `ChunkHeight` / `ChunkOffsetX` / `ChunkOffsetY` — 4 bytes each, uint32 +- `ChunkPixelFormat` — `ChunkID = 0x00010002` — 4 bytes, uint32 (PFNC value) + +> Actual ChunkIDs and lengths must be verified from the live camera XML at runtime; +> they can differ between firmware versions. + +--- + +## Camera Configuration (GenICam) + +Call these before `StartStreamAsync()`: + +```csharp +await camera.SetCameraParameter("ChunkModeActive", true); + +// Enable timestamp chunk +await camera.SetCameraParameter("ChunkSelector", "Timestamp"); +await camera.SetCameraParameter("ChunkEnable", true); + +// Enable exposure chunk (optional) +await camera.SetCameraParameter("ChunkSelector", "ExposureTime"); +await camera.SetCameraParameter("ChunkEnable", true); +``` + +To disable: +```csharp +await camera.SetCameraParameter("ChunkModeActive", false); +``` + +--- + +## Required Library Changes + +### 1 — Detect chunk mode and extended frame size + +After syncing parameters, read `ChunkModeActive` from the camera and, if true, read +`PayloadSize` (GevSCSP / the actual GVSP payload size register) to determine the full +per-frame byte count including chunk data. + +`Camera.SyncParameters()` should set a new property `ChunkAreaSize`: + +```csharp +if ((bool)await GetParameterValue("ChunkModeActive")) +{ + ulong fullPayload = (ulong)await GetParameterValue("PayloadSize"); + ChunkAreaSize = (int)(fullPayload - (ulong)PixelFormatHelper.GetFrameSize(...)); +} +``` + +### 2 — Grow the receive buffer + +`SetRxBuffer()` must allocate `RawImageSize + ChunkAreaSize` when chunk mode is active. +`GvspInfo.RawImageSize` should remain the image-only size so existing pixel consumers +are unaffected. + +```csharp +int totalFrameSize = GvspInfo.RawImageSize + ChunkAreaSize; +rawBytes = new byte[totalFrameSize]; +``` + +The `StreamReceiverBase` buffers in `Receiver()` also need the larger size: +```csharp +buffer[0] = new byte[GvspInfo.RawImageSize + chunkAreaSize]; +buffer[1] = new byte[GvspInfo.RawImageSize + chunkAreaSize]; +``` + +Either pass `chunkAreaSize` into the receiver or expose it through `GvspInfo`. + +### 3 — Chunk parser + +Add `GigeVision.Core.Services.ChunkParser`: + +```csharp +public static class ChunkParser +{ + private const uint ChunkTerminator = 0xD0000001; + + /// + /// Parses the chunk region that starts at bytes + /// into . + /// + public static IReadOnlyList Parse(byte[] frameBuffer, int imageSize) + { + var result = new List(); + int offset = imageSize; + + while (offset + 8 <= frameBuffer.Length) + { + uint chunkId = ReadUInt32BE(frameBuffer, offset); + if (chunkId == ChunkTerminator) break; + + uint chunkLength = ReadUInt32BE(frameBuffer, offset + 4); + offset += 8; + + if (offset + (int)chunkLength > frameBuffer.Length) break; + + result.Add(new ChunkRecord(chunkId, frameBuffer, offset, (int)chunkLength)); + offset += (int)chunkLength; + } + + return result; + } + + private static uint ReadUInt32BE(byte[] buf, int pos) => + ((uint)buf[pos] << 24) | ((uint)buf[pos+1] << 16) | + ((uint)buf[pos+2] << 8) | (uint)buf[pos+3]; +} + +public readonly struct ChunkRecord +{ + public uint ChunkId { get; } + private readonly byte[] _buffer; + private readonly int _offset; + public int Length { get; } + + public ChunkRecord(uint chunkId, byte[] buffer, int offset, int length) + { + ChunkId = chunkId; _buffer = buffer; _offset = offset; Length = length; + } + + public ulong ReadUInt64() => ... // big-endian helpers + public double ReadDouble() => BitConverter.ToDouble(...) // verify endianness + public uint ReadUInt32() => ... +} +``` + +### 4 — Surface chunk data to callers + +Two options (pick one): + +**Option A** — extend `GvspFrameInfo`: +```csharp +public readonly struct GvspFrameInfo +{ + public ulong FrameId { get; } + public ulong Timestamp { get; } + public IReadOnlyList Chunks { get; } // null when chunk mode off +} +``` + +**Option B** — fire a separate event `ChunkDataReady` with the parsed chunk list. + +Option A is simpler. When chunk mode is off, `Chunks` is null and existing code is unchanged. + +--- + +## Endianness Note + +GVSP chunk data bytes are big-endian (network order). The `double` type for +`ExposureTime` and `Gain` is IEEE 754 big-endian. On Windows (little-endian), +`BitConverter.ToDouble` needs the bytes reversed before use: + +```csharp +var bytes = chunkRecord.GetBytes(); +if (BitConverter.IsLittleEndian) Array.Reverse(bytes); +double value = BitConverter.ToDouble(bytes, 0); +``` + +--- + +## Testing Checklist + +- [ ] `ChunkModeActive` is readable and writable via GenICam on Lucid TRI032S +- [ ] `PayloadSize` reflects the enlarged per-frame size when chunk mode is on +- [ ] Frame buffer large enough — no `IndexOutOfRangeException` at high frame rates +- [ ] `FinalPacketID` is still computed from image pixels only, not total payload +- [ ] Chunk terminator `0xD0000001` is present at expected offset +- [ ] Timestamps from chunk data match timestamps from GVSP leader (same clock) +- [ ] Disabling chunk mode restores original buffer sizing without restart diff --git a/examples.md b/examples.md new file mode 100644 index 0000000..8227cc4 --- /dev/null +++ b/examples.md @@ -0,0 +1,228 @@ +# Minimal Working Examples + +Date: 2026-04-21 + +This file contains small examples that match the current APIs in this repository. + +Important notes: + +- Feature names are camera-XML dependent. The examples below use names that exist in `GenICam.Tests/Hikrobot.xml`, such as `Width`, `ReverseX`, and `AcquisitionFrameRate`. +- Some cameras expose engineering-unit features through `Converter` or `IntConverter` nodes instead of binding the feature directly to a register. The preferred public path is now `GetParameterValue(...)` plus overloaded `SetCameraParameter(...)` calls. +- `Camera.FrameReady` reuses internal buffers. If you want to keep a frame after the callback returns, copy the `byte[]`. +- Float and converter-backed features now resolve through typed camera helpers without forcing callers down to the raw `PValue` path, but normal callers can stay on the unified generic read and overloaded write API. + +## Namespaces + +```csharp +using System; +using System.Linq; +using System.Threading.Tasks; +using GenICam; +using GigeVision.Core; +using GigeVision.Core.Enums; +using GigeVision.Core.Models; +using GigeVision.Core.Services; +``` + +## 1. Open A Camera + +This example discovers the first camera, sets the camera IP and host NIC, then loads the XML-backed parameters. + +```csharp +public static async Task OpenFirstCameraAsync() +{ + NetworkService.AllowAppThroughFirewall(); + + var camera = new Camera(); + var devices = await camera.Gvcp.GetAllGigeDevicesInNetworkAsnyc().ConfigureAwait(false); + + var device = devices.FirstOrDefault() + ?? throw new InvalidOperationException("No GigE camera found."); + + camera.IP = device.IP; + camera.RxIP = device.NetworkIP; + + bool synced = await camera.SyncParameters().ConfigureAwait(false); + if (!synced) + { + throw new InvalidOperationException("Failed to load camera parameters."); + } + + Console.WriteLine($"Connected to {camera.IP}"); + Console.WriteLine($"Width={camera.Width}, Height={camera.Height}, PixelFormat={camera.PixelFormat}"); + + return camera; +} +``` + +## 2. Read And Set An Integer Parameter + +`Width` is a standard integer feature and is exposed directly by the high-level camera helpers. + +```csharp +public static async Task ReadAndSetIntegerAsync(Camera camera) +{ + long? currentWidth = await camera.GetParameterValue(nameof(RegisterName.Width)).ConfigureAwait(false); + long minWidth = await camera.GetParameterMinValue(nameof(RegisterName.Width)).ConfigureAwait(false); + long maxWidth = await camera.GetParameterMaxValue(nameof(RegisterName.Width)).ConfigureAwait(false); + + Console.WriteLine($"Width: current={currentWidth}, min={minWidth}, max={maxWidth}"); + + long targetWidth = Math.Clamp(1280, minWidth, maxWidth); + bool ok = await camera.SetCameraParameter(nameof(RegisterName.Width), targetWidth).ConfigureAwait(false); + long? updatedWidth = await camera.GetParameterValue(nameof(RegisterName.Width)).ConfigureAwait(false); + + Console.WriteLine($"Width write status={ok}, new value={updatedWidth}"); +} +``` + +If you want to change both width and height together, the camera class already provides a dedicated helper: + +```csharp +bool resized = await camera.SetResolutionAsync(1280, 1024).ConfigureAwait(false); +Console.WriteLine($"Resolution write status={resized}"); +``` + +## 3. Read And Set A Boolean Parameter + +`ReverseX` is a boolean feature in the sample Hikrobot XML. + +```csharp +public static async Task ReadAndSetBooleanAsync(Camera camera) +{ + bool? current = await camera.GetParameterValue("ReverseX").ConfigureAwait(false); + if (current is null) + { + throw new InvalidOperationException("ReverseX is not available on this camera."); + } + + Console.WriteLine($"ReverseX current value={current}"); + + bool ok = await camera.SetCameraParameter("ReverseX", !current.Value).ConfigureAwait(false); + bool? updated = await camera.GetParameterValue("ReverseX").ConfigureAwait(false); + + Console.WriteLine($"ReverseX write status={ok}, new value={updated}"); +} +``` + +## 4. Read And Set A Float Feature + +In this repository, float features and converter-backed engineering values can now be accessed through the generic getter and the overloaded `SetCameraParameter` method. + +For example, the attached Lucid XML maps features like `ExposureTime` and `Gain` through converters such as: + +- `ExposureTime -> Converter -> ExposureTimeRaw` +- `Gain -> Converter -> GainRaw` + +That means the feature should be accessed through the typed parameter API, not by assuming the feature itself is a plain register node. + +`ExposureTime` is a good converter-backed example because the Lucid XML converts microseconds to a raw register with `FROM * 125` and `TO / 125`. + +```csharp +public static async Task ReadAndSetConverterBackedFeatureAsync(Camera camera) +{ + double? current = await camera.GetParameterValue("ExposureTime").ConfigureAwait(false); + Console.WriteLine($"ExposureTime current value={current} us"); + + bool ok = await camera.SetCameraParameter("ExposureTime", 5000.0).ConfigureAwait(false); + double? updated = await camera.GetParameterValue("ExposureTime").ConfigureAwait(false); + + Console.WriteLine($"ExposureTime write status={ok}, new value={updated} us"); +} +``` + +For plain integer-backed features like `Width`, the high-level helper is still fine: + +```csharp +bool widthOk = await camera.SetCameraParameter(nameof(RegisterName.Width), 1280).ConfigureAwait(false); +long? width = await camera.GetParameterValue(nameof(RegisterName.Width)).ConfigureAwait(false); + +Console.WriteLine($"Width write status={widthOk}, new value={width}"); +``` + +For another converter-backed feature, such as Lucid `Gain`, use the same typed float path: + +```csharp +double? gain = await camera.GetParameterValue("Gain").ConfigureAwait(false); +bool gainOk = await camera.SetCameraParameter("Gain", 12.5).ConfigureAwait(false); + +Console.WriteLine($"Gain old={gain}, write status={gainOk}, new={await camera.GetParameterValue("Gain").ConfigureAwait(false)}"); +``` + +## 5. Receive Frames + +This example opens the stream, waits for the first frame, copies it, then stops the stream. + +```csharp +public static async Task ReceiveOneFrameAsync(Camera camera) +{ + var frameTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + void OnFrameReady(object sender, byte[] frame) + { + var copy = new byte[frame.Length]; + Buffer.BlockCopy(frame, 0, copy, 0, frame.Length); + frameTcs.TrySetResult(copy); + } + + camera.FrameReady += OnFrameReady; + + try + { + bool started = await camera.StartStreamAsync().ConfigureAwait(false); + if (!started) + { + throw new InvalidOperationException("Failed to start the stream."); + } + + byte[] firstFrame = await frameTcs.Task.ConfigureAwait(false); + Console.WriteLine($"Received frame with {firstFrame.Length} bytes."); + return firstFrame; + } + finally + { + camera.FrameReady -= OnFrameReady; + + if (camera.IsStreaming) + { + await camera.StopStream().ConfigureAwait(false); + } + } +} +``` + +For continuous reception, subscribe once and keep the stream open: + +```csharp +camera.FrameReady += (sender, frame) => +{ + var copy = new byte[frame.Length]; + Buffer.BlockCopy(frame, 0, copy, 0, frame.Length); + + Console.WriteLine($"Frame received: {copy.Length} bytes"); +}; + +bool started = await camera.StartStreamAsync().ConfigureAwait(false); +Console.WriteLine($"Stream started={started}"); + +// Keep the process alive here until you want to stop. +// await Task.Delay(...); + +if (camera.IsStreaming) +{ + await camera.StopStream().ConfigureAwait(false); +} +``` + +## 6. Full Minimal Flow + +```csharp +Camera camera = await OpenFirstCameraAsync().ConfigureAwait(false); + +await ReadAndSetIntegerAsync(camera).ConfigureAwait(false); +await ReadAndSetBooleanAsync(camera).ConfigureAwait(false); +await ReadAndSetConverterBackedFeatureAsync(camera).ConfigureAwait(false); + +byte[] frame = await ReceiveOneFrameAsync(camera).ConfigureAwait(false); +Console.WriteLine($"Done. Final frame size={frame.Length} bytes"); +``` \ No newline at end of file diff --git a/improvements.md b/improvements.md new file mode 100644 index 0000000..8048fba --- /dev/null +++ b/improvements.md @@ -0,0 +1,411 @@ +# GigeVision.Core Audit + +Date: 2026-04-21 + +## Scope + +This review focused on the `GigeVision.Core` library, especially the GVCP control path, stream-receiver implementations, camera startup/shutdown flow, register-map definitions, and the `GenPort` bridge used by the GenICam layer. + +I also did two external cross-checks where the code looked spec-sensitive: + +- PFNC / pixel-format handling was checked against the structure of the existing `PixelFormat` enum values. +- The `GevSCSP` register address was cross-checked against the public Aravis GigE Vision implementation, which maps the source stream port register to `0x0D1C`. + +Build status: + +- `GigeVision.Core` builds successfully. +- The wider solution still emits a very large warning set (mostly nullability/style debt in `GenICam` plus a smaller set in `GigeVision.Core`), so compile success should not be treated as proof of runtime correctness. + +## Highest-Risk Areas + +- `GigeVision.Core/Services/Gvcp.cs`: shared control socket, request IDs, XML download, discovery and broadcast helpers. +- `GigeVision.Core/Services/StreamReceiverBase.cs`: this is the effective default streaming path because `StreamReceiverBufferswap` is only an empty subclass. +- `GigeVision.Core/Services/Camera.cs`: stream setup, control ownership, payload selection, and event wiring. +- `GigeVision.Core/GenPort.cs`: the GenICam port bridge, where address width and byte-order rules must be exact. + +## Findings + +### 1. Critical - GVCP control traffic is not serialized, request IDs are shared, and replies are not correlated back to the command that sent them + +Affected code: + +- `GigeVision.Core/Services/Gvcp.cs:28` +- `GigeVision.Core/Services/Gvcp.cs:539` +- `GigeVision.Core/Services/Gvcp.cs:656` +- `GigeVision.Core/Services/Gvcp.cs:690-696` +- `GigeVision.Core/Services/Gvcp.cs:1071-1076` +- `GigeVision.Core/Models/GvcpReply.cs:180` + +Problem: + +- `gvcpRequestID` is mutable shared state and is incremented from many public methods without any synchronization. +- `RunHeartbeatThread()` sends GVCP traffic on the same shared `ControlSocket` used by normal read/write/control operations. +- `SendGvcpCommand()` accepts the first received datagram and never checks that `AcknowledgementID` matches `command.RequestId`. +- If `Task.WhenAny(...)` times out, the pending `ReceiveAsync()` is left alive; later packets can complete the stale receive instead of the command that is actually waiting for them. + +Why this matters: + +- Two overlapping control operations can consume each other's replies. +- Heartbeat traffic can race with normal register reads/writes. +- Under timeouts or retries, replies can be silently mis-associated instead of being rejected. + +Suggested improvement: + +- Make the control channel single-flight with a `SemaphoreSlim` or equivalent lock around every `ControlSocket` send/receive pair. +- Generate request IDs atomically (`Interlocked.Increment` on an `int` backing field, then cast to `ushort`). +- Reject replies whose `AcknowledgementID` does not match the sent command. +- Do not fire-and-forget GVCP commands on the control socket. +- Consider using a dedicated socket for heartbeat if control traffic must remain concurrent. + +### 2. Critical - `GetControlAsync()` checks `CameraIp`, not the IP/socket passed by the caller + +Affected code: + +- `GigeVision.Core/Services/Gvcp.cs:579-641` +- `GigeVision.Core/Services/Gvcp.cs:824-833` + +Problem: + +- The `WriteRegisterAsync(string Ip, ...)` overloads create a socket that targets the caller-provided `Ip`. +- `GetControlAsync(UdpClient socket)` then reads the control register with `ReadRegisterAsync(Converter.RegisterStringToByteArray(...))`, which resolves through `CameraIp`, not through the target socket or the `Ip` parameter. + +Why this matters: + +- If `CameraIp` is empty or points to a different device, the control check runs against the wrong camera. +- The string-IP overloads can fail even when the target camera is valid, or worse, check one device and then send the write to another. + +Suggested improvement: + +- Change `GetControlAsync()` to read via the same target that will be written to. +- Add a `ReadRegisterAsync(UdpClient socket, ...)` or `ReadRegisterAsync(string ip, ...)` path and use it consistently. + +### 3. Critical - The default receiver path can publish the wrong frame buffer while the next frame is already being written + +Affected code: + +- `GigeVision.Core/Services/StreamReceiverBufferswap.cs:5` +- `GigeVision.Core/Services/StreamReceiverBase.cs:210-227` + +Problem: + +- `StreamReceiverBufferswap` inherits `StreamReceiverBase` unchanged, so the base implementation is the default stream path. +- On trailer reception, the code stores `bufferIndexClone = bufferIndex`, then swaps `bufferIndex`, but the callback uses `buffer[bufferIndex]` instead of `buffer[bufferIndexClone]`. +- The callback is also inside `Task.Run(...)`, so it captures the mutable `bufferIndex` field and can observe a later value. + +Why this matters: + +- The consumer can receive the next buffer instead of the completed one. +- That next buffer is already being filled by the receiver thread, so the frame exposed to the application can be corrupted and nondeterministic. + +Suggested improvement: + +- Publish the cloned buffer index, not the mutable live one. +- Avoid `Task.Run(...)` here unless there is an actual queue or immutable frame handoff. +- If asynchronous delivery is required, queue immutable frame descriptors or copy the completed buffer into a consumer-owned pool. + +### 4. Critical - Pixel-format and bytes-per-pixel logic is wrong for packed and color formats + +Affected code: + +- `GigeVision.Core/Services/StreamReceiverBase.cs:138,163` +- `GigeVision.Core/Services/Camera.cs:715,743-747` +- `GigeVision.Core/Enums/PixelFormat.cs:41,91,196,216` + +Problem: + +- `DetectGvspType()` computes bytes per pixel with `Math.Ceiling((double)(singlePacket[21] / 8))`. Because the division happens before the cast, `10/8`, `12/8`, `14/8` all collapse to `1` before `Ceiling(...)` is applied. +- `PixelFormatToBytesPerPixel()` reduces all formats to `1` or `2` bytes solely by checking bit 20. +- The existing `PixelFormat` enum already shows formats that require very different occupied sizes: + - `Mono10Packed = 0x010C0004` (`0x0C` occupied bits -> 12 bits per pixel) + - `Mono10 = 0x01100003` (`0x10` occupied bits -> 16 bits per pixel) + - `RGB8Packed = 0x02180014` (`0x18` occupied bits -> 24 bits per pixel) + - `RGBA8Packed = 0x02200016` (`0x20` occupied bits -> 32 bits per pixel) + +Why this matters: + +- Raw image buffers can be undersized or oversized. +- Payload calculations become wrong. +- Any non-trivial PFNC format is at risk, especially packed monochrome/Bayer formats and color formats. + +Suggested improvement: + +- Derive occupied bits from the PFNC value instead of a one-bit heuristic. +- For example, `occupiedBits = ((int)pixelFormat >> 16) & 0xFF` is a far better starting point than `IsBitSet(..., 20)`. +- Distinguish packed formats from unpacked formats when allocating buffers and when deciding whether the consumer receives transport-packed bytes or unpacked pixels. + +### 5. High - `GenPort` builds malformed addresses for short registers and blindly converts every write buffer to `UInt32` + +Affected code: + +- `GigeVision.Core/Services/GenPort.cs:25-35` +- `GigeVision.Core/Services/GenPort.cs:48-59` +- `GigeVision.Core/Services/GenPort.cs:75-84` + +Problem: + +- `GetAddressBytes()` returns a 2-byte address for `length == 2`, even though GVCP register addresses are 32-bit addresses on the wire. +- `ReadAsync()` decides between register-read and memory-read using `length >= 8`, which is too coarse for GenICam port semantics. +- `WriteAsync()` always does `BitConverter.ToUInt32(pBuffer)` regardless of the actual requested write length. + +Why this matters: + +- A 1-byte or 2-byte write can throw if `pBuffer.Length < 4`. +- A longer write silently truncates to the first 4 bytes. +- Short-register accesses can send an invalid 2-byte address instead of a 4-byte register address. + +Suggested improvement: + +- Always encode GVCP register addresses as 4 bytes. +- Respect the requested `length` when reading/writing the GenICam port. +- For non-4-byte operations, prefer memory transactions or a proper stacked read/write strategy instead of assuming "anything under 8 bytes is a register read/write". + +### 6. High - Camera control ownership is not released reliably on failure or exception + +Affected code: + +- `GigeVision.Core/Services/Camera.cs:367-389` +- `GigeVision.Core/Services/Camera.cs:399-423` +- `GigeVision.Core/Services/Camera.cs:514-535` +- `GigeVision.Core/Services/Camera.cs:550-566` + +Problem: + +- `SetOffsetAsync()` acquires control and releases it only on the success path; any exception between those awaits leaves control held. +- `SetResolutionAsync()` catches and returns `false`, but the `catch` block does not release control. +- `StartStreamAsync()` acquires control with keep-alive enabled, but several non-success paths fall out of the nested `if` chain without calling `StopStream()` or `LeaveControl()`. + +Why this matters: + +- A failed setup can leave the heartbeat thread running and keep the camera locked until the device heartbeat times out. +- That can block the same process on the next call, or block a different client entirely. + +Suggested improvement: + +- Treat control ownership as a scoped resource. +- Use `try/finally` around every path that calls `TakeControl()`. +- Only start heartbeat after the rest of the stream setup has succeeded, or guarantee rollback on every early return. + +### 7. High - `StartStreamAsync()` has a port-selection race and a multicast fallback null-path + +Affected code: + +- `GigeVision.Core/Services/Camera.cs:490-492` +- `GigeVision.Core/Services/Camera.cs:532-533` +- `GigeVision.Core/Services/Camera.cs:550` + +Problem: + +- The code opens a temporary `UdpClient(0)` to discover a free port, closes it, then later asks the stream receiver to bind the same port. +- In the multicast fallback branch, `SetupRxThread()` is called without first calling `SetupReceiver()`. + +Why this matters: + +- The selected port can be claimed by another process between discovery and the real bind. +- The multicast fallback can dereference a null `StreamReceiver`. + +Suggested improvement: + +- Let the actual receive socket bind to port `0`, then read back the assigned port and program the camera with that real port. +- Ensure `SetupReceiver()` runs before every `SetupRxThread()` path. + +### 8. High - Several GVCP helpers leak sockets, and some overloads ignore the configured timeout + +Affected code: + +- `GigeVision.Core/Services/Gvcp.cs:579-641` +- `GigeVision.Core/Services/Gvcp.cs:854-921` +- `GigeVision.Core/Services/Gvcp.cs:1124-1144` +- `GigeVision.Core/Services/Gvcp.cs:356,383,580,599,619,638,1053` + +Problem: + +- Multiple `WriteRegisterAsync(string Ip, ...)` overloads allocate `UdpClient` instances and never dispose them. +- `GetRawXmlFileFromCamera()` creates its own `UdpClient` and never disposes it. +- `SendBroadCastPacket()` allocates a `UdpClient` per interface and never disposes it. +- Some methods honor `ReceiveTimeoutInMilliseconds`; others hard-code `1000` ms. + +Why this matters: + +- Repeated control calls can leak sockets and exhaust ephemeral ports or OS handles. +- Timeout behavior becomes inconsistent across API surfaces, which is especially bad for discovery and XML download. + +Suggested improvement: + +- Dispose temporary clients with `using`/`await using`. +- Propagate the configured timeout to every code path that creates a socket. + +### 9. High - GVSP type detection consumes live packets and can corrupt or partially accept the first frame + +Affected code: + +- `GigeVision.Core/Services/StreamReceiverBase.cs:128-145` +- `GigeVision.Core/Services/StreamReceiverBase.cs:172` +- `GigeVision.Core/Services/StreamReceiverBase.cs:186-193` + +Problem: + +- `DetectGvspType()` performs two real `Receive(...)` calls before the main receive loop starts. +- The main receiver initializes `packetRxCount = 1`, apparently trying to compensate for the already-consumed packet. +- The consumed packet is not copied into the frame buffer. + +Why this matters: + +- The first frame can be emitted with one packet missing but still pass the default missing-packet tolerance. +- At best the first frame is dropped; at worst it is accepted but contains zeroed/garbled data where the consumed packet belonged. + +Suggested improvement: + +- Capture the packets consumed during type detection and feed them into the normal frame assembly path. +- Alternatively, do not start accepting frames until the next full frame boundary after detection. + +### 10. High - `StreamReceiverParallel` can drop the last packet of exact-multiple frames and can overwrite unread packet chunks + +Affected code: + +- `GigeVision.Core/Services/StreamReceiverParallel.cs:58-75` +- `GigeVision.Core/Services/StreamReceiverParallel.cs:105-129` +- `GigeVision.Core/Services/StreamReceiverParallel.cs:156` + +Problem: + +- `finalPacketLength` is calculated as `image.Length % GvspInfo.PayloadSize`. +- If the image size is an exact multiple of the payload size, that remainder is `0`, so the final packet copies zero bytes. +- The writer rotates through a fixed array of packet buffers and releases semaphores, but there is no ownership/backpressure mechanism that prevents the writer from wrapping around and reusing a packet buffer before the decoder has finished reading it. + +Why this matters: + +- Exact-multiple payload cases lose the final packet of every frame. +- Under load, decoded frames can be built from overwritten packet chunks. + +Suggested improvement: + +- Use `finalPacketLength = remainder == 0 ? GvspInfo.PayloadSize : remainder`. +- Replace the current wraparound scheme with a proper producer/consumer queue or buffer-pool lease model. + +### 11. Medium - Camera events are copied into the receiver once instead of being forwarded dynamically + +Affected code: + +- `GigeVision.Core/Services/Camera.cs:71,278,797-798` +- `GigeVision.Core/Services/StreamReceiverBase.cs:44,94` + +Problem: + +- `SetupReceiver()` copies the current values of `Camera.FrameReady` and `Camera.Updates` into the receiver. +- Later subscriptions or unsubscriptions on `Camera` are not propagated. + +Why this matters: + +- If a consumer subscribes after `StartStreamAsync()`, it can miss all stream events. +- This is an API behavior trap because the `Camera` object appears to own the events. + +Suggested improvement: + +- Forward events instead of copying delegates. +- For example, keep the receiver private and relay its events through `Camera`, or subscribe the receiver once to internal handlers that then raise the camera-level events. + +### 12. Medium - The pipeline-based receivers are incomplete and unsafe to expose as production options + +Affected code: + +- `GigeVision.Core/Services/StreamReceiverPipeLine.cs:33,54,60,78` +- `GigeVision.Core/Services/CameraStreamReceiverPipeline.cs:27,53,78,86,111` + +Problem: + +- `StreamReceiverPipeLine` has the actual payload copy and `FrameReady` invocation commented out, so it does not complete frames. +- `CameraStreamReceiverPipeline` allocates `FinalPacketID * packetSize` instead of raw image size, copies payloads into slices sized with `packetLength`, and swallows exceptions silently. + +Why this matters: + +- These types are effectively experimental/incomplete and can mis-size buffers or fail silently. + +Suggested improvement: + +- Either remove them from the supported surface until they are finished, or make it explicit in code/docs that they are not production-ready. +- Add tests before shipping any pipeline receiver as a selectable default. + +### 13. Medium - Discovery parsing trusts fixed offsets without validating packet length + +Affected code: + +- `GigeVision.Core/Services/Gvcp.cs:764-804` + +Problem: + +- `DecodeDiscoveryPacket()` indexes fixed offsets for IP, MAC, manufacturer, model, version, serial number and user-defined name without checking `discoveryPacket.Length` first. +- The current offsets require at least 256 bytes of payload to be safe. + +Why this matters: + +- A truncated or malformed discovery reply can throw and abort discovery. + +Suggested improvement: + +- Validate minimum packet size up front. +- Prefer explicit parsing with named constants and reject malformed discovery packets gracefully. + +### 14. Medium - ForceIP/XML helper code contains hard-coded protocol values that are either wrong or too rigid + +Affected code: + +- `GigeVision.Core/Services/Gvcp.cs:193` +- `GigeVision.Core/Services/Gvcp.cs:863` +- `GigeVision.Core/Services/Gvcp.cs:931-944` + +Problem: + +- `ForceIPAsync()` derives the gateway by copying the target IP and forcing the last octet to `1`. +- `GetReadMessageHeader(int register)` computes `tempRegister` but never uses the `register` parameter; the payload is hard-coded to `0x0200`. +- `Task.Delay(100);` in `GetRawXmlFileFromCamera()` is not awaited and has no effect. + +Why this matters: + +- The ForceIP implementation is wrong on networks where the default gateway is not `x.x.x.1`. +- The read-header helper only works accidentally for the one register it is currently used for. +- The unawaited delay makes the code look timing-sensitive without actually enforcing any timing. + +Suggested improvement: + +- Accept an explicit gateway parameter (or write `0.0.0.0` when gateway is unknown). +- Use the `register` parameter when building the read-memory header. +- Remove the dead delay or `await` it if the timing is genuinely required. + +### 15. Medium - The `GevSCSP` register constant looks like a typo and points at the wrong address + +Affected code: + +- `GigeVision.Core/Enums/GevRegisters.cs:23` + +Problem: + +- `GevSCSP` is currently defined as `0x01C`. +- That does not fit the surrounding stream-channel register block (`0x0D00`, `0x0D04`, `0x0D08`, `0x0D18`). +- Cross-checking against the Aravis GigE Vision implementation places the source stream port register at `0x0D1C`. + +Why this matters: + +- Any code that uses the enum value directly will target the wrong address. +- The current `Camera.StartStreamAsync()` path only avoids this because it looks the feature up by name through the XML helper instead of using the enum value. + +Suggested improvement: + +- Change the constant to `0x0D1C` after confirming against the target GigE Vision register map / standard used by this library. + +## Additional Cleanup Items + +- `GigeVision.Core/Services/Gvcp.cs:227`: `GetAllGigeDevicesInNetworkAsnyc(Action<...>)` is `async void`; it should return `Task` so failures can be observed. +- `GigeVision.Core/Services/Gvcp.cs:328`: `throw ex;` destroys the original stack trace; use `throw;`. +- `GigeVision.Core/Services/Gvcp.cs:477,1047-1060,1144`: several `catch (Exception)` blocks silently swallow failures and make network/debug problems much harder to diagnose. +- `GigeVision.Core/Models/GvcpReply.cs:146-153`: `ReadRegAck` parsing should validate buffer length before indexing `buffer[8]..buffer[11]`; truncated replies can still reach that code path. +- `GigeVision.Core/Models/GvcpCommand.cs:113,163,264`: per-call `Random` allocation and generic `Exception` throws are not correctness bugs by themselves, but they add avoidable nondeterminism and weak diagnostics. +- `GigeVision.Core/Services/StreamReceiverBase.cs:115-119`: `StopReception()` calls `socketRxRaw.Dispose()` without a null-conditional; calling it before the socket is created will throw. + +## Suggested Fix Order + +1. Fix the GVCP control path first: single-flight access, atomic request IDs, and reply/request correlation. +2. Fix the default receiver handoff bug in `StreamReceiverBase` because it affects the shipping stream path immediately. +3. Rework pixel-format and payload sizing so buffers are correct for packed and color formats. +4. Make control acquisition/release exception-safe in `Camera`. +5. Repair `GenPort` so address width, read/write size and byte order follow the transport rules exactly. +6. Only then revisit the experimental pipeline/parallel receivers. \ No newline at end of file