From 6ef6aced8dd8e224ed60121311ee696349b34d8c Mon Sep 17 00:00:00 2001 From: meelstorm <8780022+TesAnti@users.noreply.github.com> Date: Tue, 19 Aug 2025 12:45:50 +0200 Subject: [PATCH] hawkeye camera support(not tested) --- ...ye.VisionBuilder.UI.Sources.Hawkeye.csproj | 14 + .../HawkeyeCameraImageSource.cs | 133 + .../HawkeyeSettings.cs | 25 + .../UICameraSettings.cs | 119 + .../IDSImageSource.cs | 9 +- .../.claude/settings.local.json | 14 + .../BaseOperation.cs | 29 +- .../BaseOperationDecorator.cs | 7 + .../Operations/AI/AnomalyAI.cs | 13 + .../AI/ArrayModelMatchingOperation.cs | 50 + .../AI/BackgroundSeparationModelOperation.cs | 13 + .../Operations/AI/Color128BinaryOperation.cs | 16 + .../Operations/AI/Color128HalfOperation.cs | 16 + .../Operations/AI/Color128SimpleOperation.cs | 16 + .../Operations/AI/ColorModelOperation.cs | 23 + .../Operations/AI/ModelAIOperation.cs | 19 + .../Operations/AI/MultichannelAI.cs | 13 + .../Operations/AI/RawModelAIOperation.cs | 19 + .../Operations/AI/YoloDetectionOperation.cs | 13 + .../Operations/AI/YoloPickDetected.cs | 34 + .../Operations/Basic/CannyOperation.cs | 16 + .../Basic/DetectionPaddingOperation.cs | 16 + .../Operations/Basic/GaussianBlurOperation.cs | 13 + .../Basic/PorabollisticHoughOperation.cs | 25 + .../Basic/ThresholdMinMaxOperation.cs | 16 + .../Operations/Basic/ThresholdOperation.cs | 13 + .../Filters/ColorProfileOperation.cs | 17 + .../Filters/GaborFilterOperation.cs | 31 + .../Operations/Filters/LUTFilterOperation.cs | 23 + .../Filters/NoiseFilterOperation.cs | 13 + .../Filters/RangeFilterOperation.cs | 28 + .../Operations/GetImageOperation.cs | 18 + .../Operations/Image/GetChannelOperation.cs | 13 + .../Image/ImageSizeProportionOperation.cs | 16 + .../Memory/ImageFromMemoryOperation.cs | 13 + .../Memory/ImageToMemoryOperation.cs | 13 + .../Operations/Morphology/ClosingOperation.cs | 13 + .../Operations/Morphology/CutOperation.cs | 39 + .../Morphology/DilationOperation.cs | 13 + .../Operations/Morphology/HatsOperation.cs | 13 + .../Operations/Morphology/OpeningOperation.cs | 13 + .../Morphology/SubtractOperation.cs | 10 + .../Morphology/TakeBiggestOperation.cs | 10 + .../Operations/Morphology/UnionOperation.cs | 10 + .../Simple/Blobs/FindBlobOperation.cs | 50 + .../Simple/Blobs/FindManyBlobsExtOperation.cs | 74 + .../Simple/Blobs/FindManyBlobsOperation.cs | 47 + .../Simple/Blobs/FindRectangleOperation.cs | 50 + .../Operations/Simple/Blobs/TwoBlobsAlign.cs | 19 + .../Simple/Edges/EdgeIntersectionOperation.cs | 16 + .../Simple/Edges/FindEdgeOperation.cs | 42 + .../WorkflowList.cs | 194 ++ .../ImageStatistics/ImageStatisticsDialog.cs | 4 +- .../VisionSteps/VisionStepsPanel.Designer.cs | 17 +- .../Features/VisionSteps/VisionStepsPanel.cs | 62 +- .../VisionSteps/VisionStepsPanel.resx | 64 +- Hawkeye.VisionBuilder/MainWindow.cs | 60 +- VisionBuilder.UI.Camera/ModuleExtensions.cs | 2 + .../VisionBuilder.UI.Camera.csproj | 1 + VisionBuilder.UI.sln | 18 + .../Inspectron.HawkEye/Camera/I2CLinux.cs | 159 ++ .../Camera/InspectronCamera.cs | 112 + .../Inspectron.HawkEye/Camera/LunixNatives.cs | 21 + .../Inspectron.HawkEye/DefragmentedPacket.cs | 104 + .../Inspectron.HawkEye/FragmentedPacket.cs | 105 + .../Inspectron.HawkEye.csproj | 21 + .../Inspectron.HawkEye/Packets/EPacketType.cs | 9 + .../Packets/ImageRequestPacket.cs | 7 + .../Inspectron.HawkEye/Packets/Packet.cs | 10 + .../Inspectron.HawkEye/Packets/PacketImage.cs | 12 + .../Protocol/CameraSettings.cs | 22 + .../Protocol/Discovery/CameraInfo.cs | 12 + .../Protocol/Discovery/DiscoveryClient.cs | 84 + .../Protocol/Discovery/DiscoveryServer.cs | 55 + .../Inspectron.HawkEye/Protocol/ECommand.cs | 13 + .../Inspectron.HawkEye/Protocol/EData.cs | 9 + .../Protocol/ImageClient.cs | 114 + .../Protocol/ImageClientTCP.cs | 192 ++ .../Protocol/ImageServer.cs | 164 ++ .../Protocol/ImageServerTCP.cs | 269 ++ .../Protocol/ImageSettings.cs | 17 + .../Protocol/Interfaces/ICameraControl.cs | 10 + .../Protocol/Interfaces/IImageSource.cs | 8 + .../Protocol/Interfaces/ILightControl.cs | 7 + .../Protocol/SocketExtensions.cs | 23 + .../Inspectron.HawkEye/RTSP/AACPayload.cs | 136 + .../Inspectron.HawkEye/RTSP/AMRPayload.cs | 34 + .../Inspectron.HawkEye/RTSP/Authentication.cs | 183 ++ .../Inspectron.HawkEye/RTSP/BitStream.cs | 88 + .../RTSP/Client/RTSPClient.cs | 1128 ++++++++ .../Inspectron.HawkEye/RTSP/G711Payload.cs | 64 + .../Inspectron.HawkEye/RTSP/H264Payload.cs | 184 ++ .../Inspectron.HawkEye/RTSP/H265Payload.cs | 224 ++ .../Inspectron.HawkEye/RTSP/IRTSPTransport.cs | 41 + .../RTSP/Messages/PortCouple.cs | 103 + .../RTSP/Messages/RTSPChunk.cs | 49 + .../RTSP/Messages/RTSPData.cs | 45 + .../RTSP/Messages/RTSPHeaderNames.cs | 19 + .../RTSP/Messages/RTSPMessage.cs | 309 ++ .../RTSP/Messages/RTSPRequest.cs | 191 ++ .../RTSP/Messages/RTSPRequestAnnounce.cs | 12 + .../RTSP/Messages/RTSPRequestDescribe.cs | 13 + .../RTSP/Messages/RTSPRequestGetParameter.cs | 12 + .../RTSP/Messages/RTSPRequestOptions.cs | 28 + .../RTSP/Messages/RTSPRequestPause.cs | 12 + .../RTSP/Messages/RTSPRequestPlay.cs | 12 + .../RTSP/Messages/RTSPRequestRecord.cs | 10 + .../RTSP/Messages/RTSPRequestSetup.cs | 43 + .../RTSP/Messages/RTSPRequestTeardown.cs | 12 + .../RTSP/Messages/RTSPResponse.cs | 228 ++ .../RTSP/Messages/RTSPTransport.cs | 367 +++ .../Inspectron.HawkEye/RTSP/RTSPListener.cs | 557 ++++ .../RTSP/RTSPMessageEventArgs.cs | 27 + .../RTSP/RTSPTCPTransport.cs | 118 + .../Inspectron.HawkEye/RTSP/RTSPUtils.cs | 16 + .../Inspectron.HawkEye/RTSP/Sdp/Attribut.cs | 77 + .../RTSP/Sdp/AttributFmtp.cs | 75 + .../RTSP/Sdp/AttributRtpMap.cs | 76 + .../Inspectron.HawkEye/RTSP/Sdp/Bandwidth.cs | 15 + .../Inspectron.HawkEye/RTSP/Sdp/Connection.cs | 48 + .../RTSP/Sdp/ConnectionIP4.cs | 40 + .../RTSP/Sdp/ConnectionIP6.cs | 31 + .../RTSP/Sdp/EncriptionKey.cs | 22 + .../RTSP/Sdp/H264Parameter.cs | 136 + .../RTSP/Sdp/H265Parameter.cs | 149 + .../Inspectron.HawkEye/RTSP/Sdp/Media.cs | 60 + .../Inspectron.HawkEye/RTSP/Sdp/Origin.cs | 101 + .../Inspectron.HawkEye/RTSP/Sdp/SdpFile.cs | 267 ++ .../RTSP/Sdp/SdpTimeZone.cs | 26 + .../Inspectron.HawkEye/RTSP/Sdp/Timing.cs | 15 + .../RTSP/Server/CJOCh264bitstream.cs | 401 +++ .../RTSP/Server/CJOCh264encoder.cs | 498 ++++ .../RTSP/Server/RTPPacketUtil.cs | 34 + .../RTSP/Server/RtspServer.cs | 800 ++++++ .../RTSP/Server/SimpleH264Encoder.cs | 109 + .../RTSP/Server/TestCard.cs | 209 ++ .../RTSP/Server/TinyH264Encoder.cs | 166 ++ .../Inspectron.HawkEye/RTSP/UdpSocket.cs | 231 ++ framework/Inspectron.HawkEye/TCPSocket.cs | 106 + framework/Inspectron.HawkEye/UDPB/Block.cs | 9 + .../Inspectron.HawkEye/UDPB/UDPBPacket.cs | 91 + .../Inspectron.HawkEye/UDPB/UDPBQueue.cs | 77 + .../UDPB/UDPBReceiveBuffer.cs | 127 + .../Inspectron.HawkEye/UDPB/UDPBSequence.cs | 142 + .../Inspectron.HawkEye/UDPB/UDPBSocket.cs | 220 ++ framework/Inspectron.HawkEye/UDPServer.cs | 17 + framework/Inspectron.HawkEye/UDPSocket.cs | 106 + framework/Inspectron.HawkEye/UDT/AckNumber.cs | 14 + framework/Inspectron.HawkEye/UDT/Buffer.cs | 476 ++++ framework/Inspectron.HawkEye/UDT/Channel.cs | 217 ++ .../UDT/CongestionControl.cs | 362 +++ .../UDT/CongestionControlFactory.cs | 22 + framework/Inspectron.HawkEye/UDT/Core.cs | 2530 +++++++++++++++++ .../Inspectron.HawkEye/UDT/CoreExtensions.cs | 19 + .../Inspectron.HawkEye/UDT/ERequestType.cs | 7 + framework/Inspectron.HawkEye/UDT/InfoBlock.cs | 111 + framework/Inspectron.HawkEye/UDT/LossList.cs | 672 +++++ .../Inspectron.HawkEye/UDT/MessageNumber.cs | 38 + framework/Inspectron.HawkEye/UDT/Packet.cs | 764 +++++ framework/Inspectron.HawkEye/UDT/Queue.cs | 903 ++++++ .../Inspectron.HawkEye/UDT/SequenceNumber.cs | 55 + framework/Inspectron.HawkEye/UDT/Timer.cs | 115 + .../Inspectron.HawkEye/UDT/UdtException.cs | 291 ++ .../UDT/UdtNetworkStream.cs | 49 + framework/Inspectron.HawkEye/UDT/UdtSocket.cs | 156 + framework/Inspectron.HawkEye/UDT/UdtUnited.cs | 960 +++++++ framework/Inspectron.HawkEye/UDT/Utilities.cs | 51 + framework/Inspectron.HawkEye/UDT/Window.cs | 309 ++ framework/Inspectron.HawkEye/UDTClient.cs | 58 + framework/Inspectron.HawkEye/UDTServer.cs | 58 + 170 files changed, 20190 insertions(+), 72 deletions(-) create mode 100644 Hawkeye.VisionBuilder.UI.Sources.Hawkeye/Hawkeye.VisionBuilder.UI.Sources.Hawkeye.csproj create mode 100644 Hawkeye.VisionBuilder.UI.Sources.Hawkeye/HawkeyeCameraImageSource.cs create mode 100644 Hawkeye.VisionBuilder.UI.Sources.Hawkeye/HawkeyeSettings.cs create mode 100644 Hawkeye.VisionBuilder.UI.Sources.Hawkeye/UICameraSettings.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/.claude/settings.local.json create mode 100644 framework/Inspectron.HawkEye/Camera/I2CLinux.cs create mode 100644 framework/Inspectron.HawkEye/Camera/InspectronCamera.cs create mode 100644 framework/Inspectron.HawkEye/Camera/LunixNatives.cs create mode 100644 framework/Inspectron.HawkEye/DefragmentedPacket.cs create mode 100644 framework/Inspectron.HawkEye/FragmentedPacket.cs create mode 100644 framework/Inspectron.HawkEye/Inspectron.HawkEye.csproj create mode 100644 framework/Inspectron.HawkEye/Packets/EPacketType.cs create mode 100644 framework/Inspectron.HawkEye/Packets/ImageRequestPacket.cs create mode 100644 framework/Inspectron.HawkEye/Packets/Packet.cs create mode 100644 framework/Inspectron.HawkEye/Packets/PacketImage.cs create mode 100644 framework/Inspectron.HawkEye/Protocol/CameraSettings.cs create mode 100644 framework/Inspectron.HawkEye/Protocol/Discovery/CameraInfo.cs create mode 100644 framework/Inspectron.HawkEye/Protocol/Discovery/DiscoveryClient.cs create mode 100644 framework/Inspectron.HawkEye/Protocol/Discovery/DiscoveryServer.cs create mode 100644 framework/Inspectron.HawkEye/Protocol/ECommand.cs create mode 100644 framework/Inspectron.HawkEye/Protocol/EData.cs create mode 100644 framework/Inspectron.HawkEye/Protocol/ImageClient.cs create mode 100644 framework/Inspectron.HawkEye/Protocol/ImageClientTCP.cs create mode 100644 framework/Inspectron.HawkEye/Protocol/ImageServer.cs create mode 100644 framework/Inspectron.HawkEye/Protocol/ImageServerTCP.cs create mode 100644 framework/Inspectron.HawkEye/Protocol/ImageSettings.cs create mode 100644 framework/Inspectron.HawkEye/Protocol/Interfaces/ICameraControl.cs create mode 100644 framework/Inspectron.HawkEye/Protocol/Interfaces/IImageSource.cs create mode 100644 framework/Inspectron.HawkEye/Protocol/Interfaces/ILightControl.cs create mode 100644 framework/Inspectron.HawkEye/Protocol/SocketExtensions.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/AACPayload.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/AMRPayload.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Authentication.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/BitStream.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Client/RTSPClient.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/G711Payload.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/H264Payload.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/H265Payload.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/IRTSPTransport.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Messages/PortCouple.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Messages/RTSPChunk.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Messages/RTSPData.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Messages/RTSPHeaderNames.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Messages/RTSPMessage.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequest.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestAnnounce.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestDescribe.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestGetParameter.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestOptions.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestPause.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestPlay.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestRecord.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestSetup.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestTeardown.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Messages/RTSPResponse.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Messages/RTSPTransport.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/RTSPListener.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/RTSPMessageEventArgs.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/RTSPTCPTransport.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/RTSPUtils.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Sdp/Attribut.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Sdp/AttributFmtp.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Sdp/AttributRtpMap.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Sdp/Bandwidth.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Sdp/Connection.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Sdp/ConnectionIP4.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Sdp/ConnectionIP6.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Sdp/EncriptionKey.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Sdp/H264Parameter.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Sdp/H265Parameter.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Sdp/Media.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Sdp/Origin.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Sdp/SdpFile.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Sdp/SdpTimeZone.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Sdp/Timing.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Server/CJOCh264bitstream.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Server/CJOCh264encoder.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Server/RTPPacketUtil.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Server/RtspServer.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Server/SimpleH264Encoder.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Server/TestCard.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/Server/TinyH264Encoder.cs create mode 100644 framework/Inspectron.HawkEye/RTSP/UdpSocket.cs create mode 100644 framework/Inspectron.HawkEye/TCPSocket.cs create mode 100644 framework/Inspectron.HawkEye/UDPB/Block.cs create mode 100644 framework/Inspectron.HawkEye/UDPB/UDPBPacket.cs create mode 100644 framework/Inspectron.HawkEye/UDPB/UDPBQueue.cs create mode 100644 framework/Inspectron.HawkEye/UDPB/UDPBReceiveBuffer.cs create mode 100644 framework/Inspectron.HawkEye/UDPB/UDPBSequence.cs create mode 100644 framework/Inspectron.HawkEye/UDPB/UDPBSocket.cs create mode 100644 framework/Inspectron.HawkEye/UDPServer.cs create mode 100644 framework/Inspectron.HawkEye/UDPSocket.cs create mode 100644 framework/Inspectron.HawkEye/UDT/AckNumber.cs create mode 100644 framework/Inspectron.HawkEye/UDT/Buffer.cs create mode 100644 framework/Inspectron.HawkEye/UDT/Channel.cs create mode 100644 framework/Inspectron.HawkEye/UDT/CongestionControl.cs create mode 100644 framework/Inspectron.HawkEye/UDT/CongestionControlFactory.cs create mode 100644 framework/Inspectron.HawkEye/UDT/Core.cs create mode 100644 framework/Inspectron.HawkEye/UDT/CoreExtensions.cs create mode 100644 framework/Inspectron.HawkEye/UDT/ERequestType.cs create mode 100644 framework/Inspectron.HawkEye/UDT/InfoBlock.cs create mode 100644 framework/Inspectron.HawkEye/UDT/LossList.cs create mode 100644 framework/Inspectron.HawkEye/UDT/MessageNumber.cs create mode 100644 framework/Inspectron.HawkEye/UDT/Packet.cs create mode 100644 framework/Inspectron.HawkEye/UDT/Queue.cs create mode 100644 framework/Inspectron.HawkEye/UDT/SequenceNumber.cs create mode 100644 framework/Inspectron.HawkEye/UDT/Timer.cs create mode 100644 framework/Inspectron.HawkEye/UDT/UdtException.cs create mode 100644 framework/Inspectron.HawkEye/UDT/UdtNetworkStream.cs create mode 100644 framework/Inspectron.HawkEye/UDT/UdtSocket.cs create mode 100644 framework/Inspectron.HawkEye/UDT/UdtUnited.cs create mode 100644 framework/Inspectron.HawkEye/UDT/Utilities.cs create mode 100644 framework/Inspectron.HawkEye/UDT/Window.cs create mode 100644 framework/Inspectron.HawkEye/UDTClient.cs create mode 100644 framework/Inspectron.HawkEye/UDTServer.cs diff --git a/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/Hawkeye.VisionBuilder.UI.Sources.Hawkeye.csproj b/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/Hawkeye.VisionBuilder.UI.Sources.Hawkeye.csproj new file mode 100644 index 0000000..1d21770 --- /dev/null +++ b/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/Hawkeye.VisionBuilder.UI.Sources.Hawkeye.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/HawkeyeCameraImageSource.cs b/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/HawkeyeCameraImageSource.cs new file mode 100644 index 0000000..7f47d32 --- /dev/null +++ b/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/HawkeyeCameraImageSource.cs @@ -0,0 +1,133 @@ +using Inspectron.HawkEye.Protocol; +using OpenCvSharp; +using Serilog; +using System.Net; +using System.Runtime.InteropServices; +using VisionBuilder.UI.Common; +using VisionBuilder.UI.Common.RecipeProcessing; +using CameraSettings = Inspectron.HawkEye.Protocol.CameraSettings; + +namespace Hawkeye.VisionBuilder.UI.Sources.Hawkeye +{ + public class HawkeyeCameraImageSource : IImageSource, IVisionBuilderModule + { + private readonly HawkeyeSettings _settings; + private ImageClientTCP _client; + private CameraSettings _cameraSettings; + + + public HawkeyeCameraImageSource(HawkeyeSettings settings) + { + _settings = settings; + } + + public void Open() + { + + _client = new ImageClientTCP(new IPEndPoint(IPAddress.Parse("192.168.3.15"), 27001), + IPAddress.Parse(_settings.Adapter)); + _client.ImageReceived += _client_ImageReceived; + _client.SettingsReceived += _client_SettingsReceived; + _client.Connect(); + Log.Information("Connected to Hawkeye camera at {Adapter}", _settings.Adapter, 27001); + + } + private void _client_SettingsReceived(CameraSettings obj) + { + ApplySettings(obj); + } + public void ApplySettings(CameraSettings obj) + { + _cameraSettings = UICameraSettings.LoadSettingsLocal(_settings.SettingsFile).ToCameraSettings(); + _client.ApplySettings(_cameraSettings); + } + + public Task GetImage(CancellationToken token) + { + + _lastImage = null; + if (_imageAquisitionTaskSource != null && !_imageAquisitionTaskSource.Task.IsCanceled) + { + _imageAquisitionTaskSource.SetCanceled(CancellationToken.None); + _imageAquisitionTaskSource = null; + + } + _imageAquisitionTaskSource= new TaskCompletionSource(); + _client.Trigger(); + return _imageAquisitionTaskSource.Task; + + } + + private TaskCompletionSource? _imageAquisitionTaskSource; + private Mat _lastImage; + byte[] _flipBuffer = new byte[2000 * 2000]; + private void _client_ImageReceived(byte[] obj) + { + Log.Debug("Got image on {adapter}", _settings.Adapter); + FlipLines(obj, _flipBuffer); + obj = _flipBuffer; + var pinnedArray = GCHandle.Alloc(obj, GCHandleType.Pinned); + var pointer = pinnedArray.AddrOfPinnedObject(); + + Mat image = new Mat(_cameraSettings.ImageSettings.Lines, _cameraSettings.ImageSettings.SensorWidth, + MatType.CV_8UC1, pointer); + + var xCrop = _cameraSettings.ImageSettings.SensorWidth - _cameraSettings.ImageWidth - _cameraSettings.OffsetX; + + // crop the image with opencv + _lastImage = image[0, _cameraSettings.ImageSettings.Lines, xCrop, + xCrop + _cameraSettings.ImageWidth].Clone(); + + if (_cameraSettings.BayerFilter) + { + _lastImage = BayerFilter(_lastImage); + } + + _imageAquisitionTaskSource!.SetResult(_lastImage); + + pinnedArray.Free(); + } + + Mat BayerFilter(Mat image) + { + // Assumes input is a single-channel Bayer pattern image (CV_8UC1) + // Output is a 3-channel BGR image (CV_8UC3) + if (image == null || image.Empty()) + throw new ArgumentException("Input image is null or empty.", nameof(image)); + + Mat bgrImage = new Mat(); + // Use OpenCV's demosaicing function for Bayer BG pattern + Cv2.CvtColor(image, bgrImage, ColorConversionCodes.BayerBG2BGR); + + return bgrImage; + } + + protected void FlipLines(byte[] src, byte[] dst) + { + int srcStride = _cameraSettings.ImageSettings.SensorWidth; + int dstStride = _cameraSettings.ImageSettings.SensorWidth; + int pixelSize = 1; + int copySize = srcStride * pixelSize; + var lines = _cameraSettings.ImageSettings.Lines; + for (int line = 0; line < lines; line += 4) + { + for (int i = 0; i < 4; i++) + { + int srcPos = (line + i) * srcStride; + int dstPos = (line + (4 - i)) * dstStride; + for (int j = 0; j < copySize; j++) + { + dst[dstPos + j] = src[srcPos + j]; + } + + + } + } + } + + public void InitializeModule() + { + + } + } +} diff --git a/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/HawkeyeSettings.cs b/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/HawkeyeSettings.cs new file mode 100644 index 0000000..7989583 --- /dev/null +++ b/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/HawkeyeSettings.cs @@ -0,0 +1,25 @@ +using Inspectron.Settings; +using Inspectron.Settings.Attributes; +using VisionBuilder.UI.Common; + +namespace Hawkeye.VisionBuilder.UI.Sources.Hawkeye; + +public class HawkeyeSettings(string CameraName): ISettings +{ + + + + public string Adapter { get; set; }="192.168.1.100"; + + [File("*.jcnf")] + public string SettingsFile { get; set; } + + + public void RegisterSettings(InspectronSettings settings) + { + settings.RegisterSimple(this, () => Adapter, $"{CameraName}/Sources/Hawkeye", nameof(Adapter)); + settings.RegisterSimple(this, () => SettingsFile, $"{CameraName}/Sources/Hawkeye", nameof(SettingsFile)); + } + + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/UICameraSettings.cs b/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/UICameraSettings.cs new file mode 100644 index 0000000..18a70ad --- /dev/null +++ b/Hawkeye.VisionBuilder.UI.Sources.Hawkeye/UICameraSettings.cs @@ -0,0 +1,119 @@ +using Inspectron.HawkEye.Protocol; +using Newtonsoft.Json; + +namespace Hawkeye.VisionBuilder.UI.Sources.Hawkeye; + +public class UICameraSettings +{ + public UICameraSettings() + { + } + + public UICameraSettings(CameraSettings cameraSettings) + { + Shutter = cameraSettings.ImageSettings.Shutter; + Gain = cameraSettings.ImageSettings.Gain; + OffsetX = cameraSettings.OffsetX; + SensorWidth = cameraSettings.ImageSettings.SensorWidth; + Width = cameraSettings.ImageWidth; + Lines = cameraSettings.ImageSettings.Lines; + Divider = cameraSettings.ImageSettings.Divider; + Light1 = cameraSettings.LightPwm1; + Light2 = cameraSettings.LightPwm2; + TriggerEnabled = cameraSettings.ImageSettings.UseExternalTrigger == 1; + Name = cameraSettings.Name; + CaptureBuffer = cameraSettings.ImageSettings.CaptureBuffer; + RescaleWidth = cameraSettings.RescaleWidth; + MinorCutoff = cameraSettings.MinorCutoff; + BayerFilter = cameraSettings.BayerFilter; + LaserTrigger = cameraSettings.LaserTrigger; + LaserTriggerDelay = cameraSettings.LaserTriggerDelay; + FlipLines = cameraSettings.FlipLines; + TriggerLights = cameraSettings.TriggerLights; + MirrorX = cameraSettings.MirrorX; + } + + + + public CameraSettings ToCameraSettings() + { + return new CameraSettings() + { + ImageSettings = new ImageSettings() + { + Lines = Lines, + CaptureBuffer = CaptureBuffer, + Divider = Divider, + UseExternalTrigger = TriggerEnabled ? 1 : 0, + Gain = Gain, + Shutter = Shutter, + SensorWidth = SensorWidth + + + }, + LightPwm1 = Light1, + LightPwm2 = Light2, + OffsetX = OffsetX, + ImageWidth = Width, + RescaleWidth = RescaleWidth, + MinorCutoff = MinorCutoff, + BayerFilter = BayerFilter, + LaserTrigger = LaserTrigger, + LaserTriggerDelay = LaserTriggerDelay, + FlipLines = FlipLines, + TriggerLights = TriggerLights, + MirrorX = MirrorX + }; + } + + public static UICameraSettings LoadSettingsLocal(string path) + { + return JsonConvert.DeserializeObject(File.ReadAllText(path)); + } + + + public int LaserTriggerDelay { get; set; } + + public bool LaserTrigger { get; set; } + + public bool TriggerEnabled { get; set; } + + + public int CaptureBuffer { get; set; } + + public bool FlipLines { get; set; } + + public int Shutter { get; set; } + + public int Gain { get; set; } + + public int OffsetX { get; set; } + + public int SensorWidth { get; set; } + + public int Width { get; set; } + + public int RescaleWidth { get; set; } + + public int Lines { get; set; } + + public int Divider { get; set; } + + public int Light1 { get; set; } + + public int Light2 { get; set; } + + public bool TriggerLights { get; set; } + + public string Name { get; set; } + + + public int MinorCutoff { get; set; } + + + + public bool BayerFilter { get; set; } + + + public bool MirrorX { get; set; } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.UI.Sources.IDS/IDSImageSource.cs b/Hawkeye.VisionBuilder.UI.Sources.IDS/IDSImageSource.cs index a8ccf07..0c8b301 100644 --- a/Hawkeye.VisionBuilder.UI.Sources.IDS/IDSImageSource.cs +++ b/Hawkeye.VisionBuilder.UI.Sources.IDS/IDSImageSource.cs @@ -24,7 +24,6 @@ namespace Inspectron.Camera.UEye int cameraIdx; - public IDSImageSource(IDSImageSourceSettings settings) { _settings = settings; @@ -68,8 +67,7 @@ namespace Inspectron.Camera.UEye Byte[] u8img; _camera.Memory.CopyToArray(idx, out u8img); - //Console.WriteLine($"Image size: {size.Width}x{size.Height}, channels: {channels}({mode}), data size: {u8img.Length}, pitch: {pitch}"); - //Console.ReadLine(); + Mat mat = new Mat(size.Height,pitch, MatType.CV_8UC(channels)); mat.SetArray(u8img); return mat; @@ -82,11 +80,11 @@ namespace Inspectron.Camera.UEye uEye.Defines.Status statusRet = 0; - // Open _camera + // Open camera statusRet = _camera.Init(cameraIdx); if (statusRet != uEye.Defines.Status.Success) { - throw new Exception("_camera initializing failed: "+statusRet); + throw new Exception("Camera initialization failed: "+statusRet); } uEye.Types.SensorInfo info = new uEye.Types.SensorInfo(); _camera.Information.GetSensorInfo(out info); @@ -101,7 +99,6 @@ namespace Inspectron.Camera.UEye if (statusRet != uEye.Defines.Status.Success) { throw new Exception("Allocate Memory failed"); - } _camera.EventFrame += onFrameEvent; try diff --git a/Hawkeye.VisionBuilder.Workflow/.claude/settings.local.json b/Hawkeye.VisionBuilder.Workflow/.claude/settings.local.json new file mode 100644 index 0000000..3efdcd6 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/.claude/settings.local.json @@ -0,0 +1,14 @@ +{ + "permissions": { + "allow": [ + "Bash(find:*)", + "Bash(for file in UnionOperation.cs HatsOperation.cs OpeningOperation.cs DilationOperation.cs ClosingOperation.cs)", + "Bash(do echo \"=== $file ===\")", + "Bash(tail:*)", + "Bash(done)", + "Bash(dotnet build:*)", + "Bash(ls:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/BaseOperation.cs b/Hawkeye.VisionBuilder.Workflow/BaseOperation.cs index f539355..45ffd1d 100644 --- a/Hawkeye.VisionBuilder.Workflow/BaseOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/BaseOperation.cs @@ -90,8 +90,9 @@ namespace Hawkeye.VisionBuilder.Workflow { return Regex.Replace(typeName.Replace("Operation", ""), "(\\B[A-Z]+?(?=[A-Z][^A-Z])|\\B[A-Z]+?(?=[^A-Z]))", (useSpaces?" ":"")+"$1"); } - + [NotForTool] public bool Result { get; set; } = true; + [NotForTool] public string ResultString { get @@ -104,7 +105,12 @@ namespace Hawkeye.VisionBuilder.Workflow [NotForTool] public string Label { get; set; } + [NotForTool] + public bool Enabled { get; set; } = true; + + [NotForTool] public TimeSpan ExecutionTime { get; set; } + [NotForTool] public string ExecutionTimeString { get @@ -132,7 +138,7 @@ namespace Hawkeye.VisionBuilder.Workflow public Dictionary GetParameters() { - var props = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + var props = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(x=>!x.HasAttribute(typeof(NotForToolAttribute))); return props.ToDictionary(x => x.Name, x => x.GetValue(this)!); } @@ -142,7 +148,7 @@ namespace Hawkeye.VisionBuilder.Workflow foreach (KeyValuePair pair in parameters) { this.GetType().GetProperty(pair.Key, - BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + BindingFlags.Public | BindingFlags.Instance ) ?.SetValue(this,pair.Value); } } @@ -275,6 +281,23 @@ namespace Hawkeye.VisionBuilder.Workflow Label = br.ReadString(); } + public virtual void Save(Dictionary dict) + { + dict[nameof(Id)] = Id.ToString(); + dict[nameof(Label)] = Label; + dict[nameof(Enabled)] = Enabled; + } + + public virtual void Load(Dictionary dict) + { + if (dict.ContainsKey(nameof(Id))) + Id = new Guid(dict[nameof(Id)].ToString()); + if (dict.ContainsKey(nameof(Label))) + Label = dict[nameof(Label)].ToString(); + if (dict.ContainsKey(nameof(Enabled))) + Enabled = Convert.ToBoolean(dict[nameof(Enabled)]); + } + public void SetError(string eMessage) { Status=eMessage; diff --git a/Hawkeye.VisionBuilder.Workflow/BaseOperationDecorator.cs b/Hawkeye.VisionBuilder.Workflow/BaseOperationDecorator.cs index bfbe1fa..45a2c54 100644 --- a/Hawkeye.VisionBuilder.Workflow/BaseOperationDecorator.cs +++ b/Hawkeye.VisionBuilder.Workflow/BaseOperationDecorator.cs @@ -9,6 +9,7 @@ public class BaseOperationDecorator _baseOperation = baseOperation; } + public bool Result => Operation.Result; public string ResultString => Operation.ToString(); public string TypeName => Operation.TypeName; @@ -21,4 +22,10 @@ public class BaseOperationDecorator public string ExecutionTimeString => Operation.ExecutionTimeString; public BaseOperation Operation => _baseOperation; + + public bool IsEnabled + { + get => _baseOperation.Enabled; + set => _baseOperation.Enabled = value; + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/AnomalyAI.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/AnomalyAI.cs index c84b60a..68fc5b4 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/AnomalyAI.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/AnomalyAI.cs @@ -122,4 +122,17 @@ public class AnomalyAI: BaseOperation _modelFilePath.Format = br.ReadString(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(ModelFilePath)] = ModelFilePath.Path; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(ModelFilePath))) + ModelFilePath.Path = dict[nameof(ModelFilePath)].ToString(); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/ArrayModelMatchingOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ArrayModelMatchingOperation.cs index 8d51895..cd4b68f 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/ArrayModelMatchingOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ArrayModelMatchingOperation.cs @@ -181,4 +181,54 @@ public class ArrayModelMatchingOperation:BaseOperation SearchArea.Load(br); ReloadModelInfo(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(ReferenceId)] = ReferenceId.ToString(); + dict[nameof(Margin)] = Margin; + dict[nameof(Amount)] = Amount; + dict[nameof(ModelName)] = ModelName; + + // Save ArrayHorizontalElement SearchArea properties manually + dict["SearchArea_Editable"] = SearchArea.Editable; + dict["SearchArea_LocationX"] = SearchArea.Location.X; + dict["SearchArea_LocationY"] = SearchArea.Location.Y; + dict["SearchArea_OffsetX"] = SearchArea.Offset.X; + dict["SearchArea_OffsetY"] = SearchArea.Offset.Y; + dict["SearchArea_BlockSizeX"] = SearchArea.BlockSize.X; + dict["SearchArea_BlockSizeY"] = SearchArea.BlockSize.Y; + dict["SearchArea_BlockCount"] = SearchArea.BlockCount; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(ReferenceId))) + ReferenceId = new Guid(dict[nameof(ReferenceId)].ToString()); + if (dict.ContainsKey(nameof(Margin))) + Margin = Convert.ToInt32(dict[nameof(Margin)]); + if (dict.ContainsKey(nameof(Amount))) + Amount = Convert.ToInt32(dict[nameof(Amount)]); + if (dict.ContainsKey(nameof(ModelName))) + ModelName = dict[nameof(ModelName)].ToString(); + + // Load ArrayHorizontalElement SearchArea properties manually + if (dict.ContainsKey("SearchArea_Editable") || dict.ContainsKey("SearchArea_LocationX") || dict.ContainsKey("SearchArea_BlockSizeX")) + { + SearchArea = new ArrayHorizontalElement(); + if (dict.ContainsKey("SearchArea_Editable")) + SearchArea.Editable = Convert.ToBoolean(dict["SearchArea_Editable"]); + if (dict.ContainsKey("SearchArea_LocationX") && dict.ContainsKey("SearchArea_LocationY")) + SearchArea.Location = new Vector2(Convert.ToSingle(dict["SearchArea_LocationX"]), Convert.ToSingle(dict["SearchArea_LocationY"])); + if (dict.ContainsKey("SearchArea_OffsetX") && dict.ContainsKey("SearchArea_OffsetY")) + SearchArea.Offset = new Vector2(Convert.ToSingle(dict["SearchArea_OffsetX"]), Convert.ToSingle(dict["SearchArea_OffsetY"])); + if (dict.ContainsKey("SearchArea_BlockSizeX") && dict.ContainsKey("SearchArea_BlockSizeY")) + SearchArea.BlockSize = new Vector2(Convert.ToSingle(dict["SearchArea_BlockSizeX"]), Convert.ToSingle(dict["SearchArea_BlockSizeY"])); + if (dict.ContainsKey("SearchArea_BlockCount")) + SearchArea.BlockCount = Convert.ToInt32(dict["SearchArea_BlockCount"]); + } + + ReloadModelInfo(); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/BackgroundSeparationModelOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/BackgroundSeparationModelOperation.cs index 420be4a..48a6688 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/BackgroundSeparationModelOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/BackgroundSeparationModelOperation.cs @@ -120,5 +120,18 @@ public class BackgroundSeparationModelOperation:BaseOperation,IHaveOrigin ModelName = br.ReadString(); } + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(ModelName)] = ModelName; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(ModelName))) + ModelName = dict[nameof(ModelName)].ToString(); + } + public OriginElement Origin { get; set; } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128BinaryOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128BinaryOperation.cs index 1d7ce47..5fb3f63 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128BinaryOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128BinaryOperation.cs @@ -30,4 +30,20 @@ public class Color128BinaryOperation:ColorAIOperation FilterClasses = br.ReadString(); ModelFilePath.Path = br.ReadString(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(FilterClasses)] = FilterClasses; + dict[nameof(ModelFilePath)] = ModelFilePath.Path; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(FilterClasses))) + FilterClasses = dict[nameof(FilterClasses)].ToString(); + if (dict.ContainsKey(nameof(ModelFilePath))) + ModelFilePath.Path = dict[nameof(ModelFilePath)].ToString(); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128HalfOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128HalfOperation.cs index 787056d..da796d5 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128HalfOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128HalfOperation.cs @@ -110,4 +110,20 @@ public class Color128HalfOperation:BaseOperation FilterClasses = br.ReadString(); ModelFilePath.Path = br.ReadString(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(FilterClasses)] = FilterClasses; + dict[nameof(ModelFilePath)] = ModelFilePath.Path; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(FilterClasses))) + FilterClasses = dict[nameof(FilterClasses)].ToString(); + if (dict.ContainsKey(nameof(ModelFilePath))) + ModelFilePath.Path = dict[nameof(ModelFilePath)].ToString(); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128SimpleOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128SimpleOperation.cs index 21f108b..eafb465 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128SimpleOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128SimpleOperation.cs @@ -33,4 +33,20 @@ public class Color128SimpleOperation: ColorAIOperation FilterClasses = br.ReadString(); ModelFilePath.Path = br.ReadString(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(FilterClasses)] = FilterClasses; + dict[nameof(ModelFilePath)] = ModelFilePath.Path; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(FilterClasses))) + FilterClasses = dict[nameof(FilterClasses)].ToString(); + if (dict.ContainsKey(nameof(ModelFilePath))) + ModelFilePath.Path = dict[nameof(ModelFilePath)].ToString(); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/ColorModelOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ColorModelOperation.cs index f662f56..643f58b 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/ColorModelOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ColorModelOperation.cs @@ -39,4 +39,27 @@ public class ColorModelOperation : ColorAIOperation var absPath = Path.GetFullPath(Path.Combine(dir, ModelFilePath.Path)); ModelFilePath.Path = absPath; } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(FilterClasses)] = FilterClasses; + dict[nameof(ModelFilePath)] = ModelFilePath.Path; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(FilterClasses))) + FilterClasses = dict[nameof(FilterClasses)].ToString(); + if (dict.ContainsKey(nameof(ModelFilePath))) + { + ModelFilePath.Path = dict[nameof(ModelFilePath)].ToString(); + // get current directory + var dir = Directory.GetCurrentDirectory(); + // get absolute path + var absPath = Path.GetFullPath(Path.Combine(dir, ModelFilePath.Path)); + ModelFilePath.Path = absPath; + } + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/ModelAIOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ModelAIOperation.cs index 57b7902..5001889 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/ModelAIOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ModelAIOperation.cs @@ -156,4 +156,23 @@ public class ModelAIOperation: BaseOperation FilterClasses = br.ReadString(); IsCategorical = br.ReadBoolean(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(ModelFilePath)] = ModelFilePath.Path; + dict[nameof(FilterClasses)] = FilterClasses; + dict[nameof(IsCategorical)] = IsCategorical; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(ModelFilePath))) + ModelFilePath.Path = dict[nameof(ModelFilePath)].ToString(); + if (dict.ContainsKey(nameof(FilterClasses))) + FilterClasses = dict[nameof(FilterClasses)].ToString(); + if (dict.ContainsKey(nameof(IsCategorical))) + IsCategorical = Convert.ToBoolean(dict[nameof(IsCategorical)]); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/MultichannelAI.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/MultichannelAI.cs index babfa78..3a2f9a9 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/MultichannelAI.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/MultichannelAI.cs @@ -145,4 +145,17 @@ public class MultichannelAI: BaseOperation } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(ModelFilePath)] = ModelFilePath.Path; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(ModelFilePath))) + ModelFilePath.Path = dict[nameof(ModelFilePath)].ToString(); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/RawModelAIOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/RawModelAIOperation.cs index be8ad84..7067464 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/RawModelAIOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/RawModelAIOperation.cs @@ -147,4 +147,23 @@ public class RawModelAIOperation: BaseOperation FilterClasses = br.ReadString(); IsCategorical = br.ReadBoolean(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(ModelFilePath)] = ModelFilePath.Path; + dict[nameof(FilterClasses)] = FilterClasses; + dict[nameof(IsCategorical)] = IsCategorical; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(ModelFilePath))) + ModelFilePath.Path = dict[nameof(ModelFilePath)].ToString(); + if (dict.ContainsKey(nameof(FilterClasses))) + FilterClasses = dict[nameof(FilterClasses)].ToString(); + if (dict.ContainsKey(nameof(IsCategorical))) + IsCategorical = Convert.ToBoolean(dict[nameof(IsCategorical)]); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloDetectionOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloDetectionOperation.cs index d382089..c107ed2 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloDetectionOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloDetectionOperation.cs @@ -139,4 +139,17 @@ public class YoloDetectionOperation:BaseOperation base.Load(br); ModelFilePath.Path = br.ReadString(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(ModelFilePath)] = ModelFilePath.Path; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(ModelFilePath))) + ModelFilePath.Path = dict[nameof(ModelFilePath)].ToString(); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloPickDetected.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloPickDetected.cs index 05824da..d07e080 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloPickDetected.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloPickDetected.cs @@ -112,4 +112,38 @@ public class YoloPickDetected:BaseOperation MinArea = br.ReadInt32(); MaxArea = br.ReadInt32(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(SlotName)] = SlotName; + dict[nameof(TypeId)] = TypeId; + dict[nameof(MinWidth)] = MinWidth; + dict[nameof(MaxWidth)] = MaxWidth; + dict[nameof(MinHeight)] = MinHeight; + dict[nameof(MaxHeight)] = MaxHeight; + dict[nameof(MinArea)] = MinArea; + dict[nameof(MaxArea)] = MaxArea; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(SlotName))) + SlotName = dict[nameof(SlotName)].ToString(); + if (dict.ContainsKey(nameof(TypeId))) + TypeId = Convert.ToInt32(dict[nameof(TypeId)]); + if (dict.ContainsKey(nameof(MinWidth))) + MinWidth = Convert.ToInt32(dict[nameof(MinWidth)]); + if (dict.ContainsKey(nameof(MaxWidth))) + MaxWidth = Convert.ToInt32(dict[nameof(MaxWidth)]); + if (dict.ContainsKey(nameof(MinHeight))) + MinHeight = Convert.ToInt32(dict[nameof(MinHeight)]); + if (dict.ContainsKey(nameof(MaxHeight))) + MaxHeight = Convert.ToInt32(dict[nameof(MaxHeight)]); + if (dict.ContainsKey(nameof(MinArea))) + MinArea = Convert.ToInt32(dict[nameof(MinArea)]); + if (dict.ContainsKey(nameof(MaxArea))) + MaxArea = Convert.ToInt32(dict[nameof(MaxArea)]); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/CannyOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/CannyOperation.cs index 110081f..68175c8 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/CannyOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/CannyOperation.cs @@ -35,4 +35,20 @@ public class CannyOperation:BaseOperation Threshold1 = br.ReadInt32(); Threshold2 = br.ReadInt32(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(Threshold1)] = Threshold1; + dict[nameof(Threshold2)] = Threshold2; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(Threshold1))) + Threshold1 = Convert.ToInt32(dict[nameof(Threshold1)]); + if (dict.ContainsKey(nameof(Threshold2))) + Threshold2 = Convert.ToInt32(dict[nameof(Threshold2)]); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/DetectionPaddingOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/DetectionPaddingOperation.cs index c8a9378..df9ec44 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/DetectionPaddingOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/DetectionPaddingOperation.cs @@ -52,4 +52,20 @@ public class DetectionPaddingOperation:BaseOperation HorizontalPadding = br.ReadInt32(); VerticalPadding = br.ReadInt32(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(HorizontalPadding)] = HorizontalPadding; + dict[nameof(VerticalPadding)] = VerticalPadding; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(HorizontalPadding))) + HorizontalPadding = Convert.ToInt32(dict[nameof(HorizontalPadding)]); + if (dict.ContainsKey(nameof(VerticalPadding))) + VerticalPadding = Convert.ToInt32(dict[nameof(VerticalPadding)]); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/GaussianBlurOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/GaussianBlurOperation.cs index fc19b14..b2d5340 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/GaussianBlurOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/GaussianBlurOperation.cs @@ -47,4 +47,17 @@ public class GaussianBlurOperation : BaseOperation KernelSize.Script = br.ReadString(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(KernelSize)] = KernelSize.Script; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(KernelSize))) + KernelSize.Script = dict[nameof(KernelSize)].ToString(); + } } diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/PorabollisticHoughOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/PorabollisticHoughOperation.cs index f018414..0417b74 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/PorabollisticHoughOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/PorabollisticHoughOperation.cs @@ -51,4 +51,29 @@ public class PorabollisticHoughOperation:BaseOperation MinLineLength = br.ReadInt32(); MaxLineGap = br.ReadInt32(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(DistanceResolution)] = DistanceResolution; + dict[nameof(AngleResolution)] = AngleResolution; + dict[nameof(Threshold)] = Threshold; + dict[nameof(MinLineLength)] = MinLineLength; + dict[nameof(MaxLineGap)] = MaxLineGap; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(DistanceResolution))) + DistanceResolution = Convert.ToDouble(dict[nameof(DistanceResolution)]); + if (dict.ContainsKey(nameof(AngleResolution))) + AngleResolution = Convert.ToDouble(dict[nameof(AngleResolution)]); + if (dict.ContainsKey(nameof(Threshold))) + Threshold = Convert.ToInt32(dict[nameof(Threshold)]); + if (dict.ContainsKey(nameof(MinLineLength))) + MinLineLength = Convert.ToInt32(dict[nameof(MinLineLength)]); + if (dict.ContainsKey(nameof(MaxLineGap))) + MaxLineGap = Convert.ToInt32(dict[nameof(MaxLineGap)]); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdMinMaxOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdMinMaxOperation.cs index 055f1cc..c64e5b0 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdMinMaxOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdMinMaxOperation.cs @@ -44,4 +44,20 @@ public class ThresholdMinMaxOperation : BaseOperation ThresholdMin.Script = br.ReadString(); ThresholdMax.Script = br.ReadString(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(ThresholdMin)] = ThresholdMin.Script; + dict[nameof(ThresholdMax)] = ThresholdMax.Script; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(ThresholdMin))) + ThresholdMin.Script = dict[nameof(ThresholdMin)].ToString(); + if (dict.ContainsKey(nameof(ThresholdMax))) + ThresholdMax.Script = dict[nameof(ThresholdMax)].ToString(); + } } diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdOperation.cs index 02cb9f8..ae99711 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdOperation.cs @@ -41,4 +41,17 @@ public class ThresholdOperation:BaseOperation ThresholdMin.Script = br.ReadString(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(ThresholdMin)] = ThresholdMin.Script; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(ThresholdMin))) + ThresholdMin.Script = dict[nameof(ThresholdMin)].ToString(); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/ColorProfileOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/ColorProfileOperation.cs index fb524b1..f331b0b 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/ColorProfileOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/ColorProfileOperation.cs @@ -132,6 +132,23 @@ public class ColorProfileOperation:BaseOperation _initialized = false; } + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(ColorProfilePath)] = ColorProfilePath.Path; + dict[nameof(Category)] = Category.Value; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(ColorProfilePath))) + ColorProfilePath.Path = EnsureRelativePath(dict[nameof(ColorProfilePath)].ToString()); + if (dict.ContainsKey(nameof(Category))) + Category.Value = dict[nameof(Category)].ToString(); + _initialized = false; + } + string EnsureRelativePath(string path) { if (path.StartsWith("..")) diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/GaborFilterOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/GaborFilterOperation.cs index 1881572..07220e2 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/GaborFilterOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/GaborFilterOperation.cs @@ -94,4 +94,35 @@ public class GaborFilterOperation : BaseOperation ThetaToAngle = br.ReadDouble(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(KSize)] = KSize; + dict[nameof(Sigma)] = Sigma; + dict[nameof(NumFilters)] = NumFilters; + dict[nameof(Lambda)] = Lambda; + dict[nameof(Gamma)] = Gamma; + dict[nameof(ThetaFromAngle)] = ThetaFromAngle; + dict[nameof(ThetaToAngle)] = ThetaToAngle; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(KSize))) + KSize = Convert.ToInt32(dict[nameof(KSize)]); + if (dict.ContainsKey(nameof(Sigma))) + Sigma = Convert.ToDouble(dict[nameof(Sigma)]); + if (dict.ContainsKey(nameof(NumFilters))) + NumFilters = Convert.ToInt32(dict[nameof(NumFilters)]); + if (dict.ContainsKey(nameof(Lambda))) + Lambda = Convert.ToDouble(dict[nameof(Lambda)]); + if (dict.ContainsKey(nameof(Gamma))) + Gamma = Convert.ToDouble(dict[nameof(Gamma)]); + if (dict.ContainsKey(nameof(ThetaFromAngle))) + ThetaFromAngle = Convert.ToDouble(dict[nameof(ThetaFromAngle)]); + if (dict.ContainsKey(nameof(ThetaToAngle))) + ThetaToAngle = Convert.ToDouble(dict[nameof(ThetaToAngle)]); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/LUTFilterOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/LUTFilterOperation.cs index 84b4a47..56b238f 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/LUTFilterOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/LUTFilterOperation.cs @@ -57,4 +57,27 @@ public class LUTFilterOperation : BaseOperation base.Load(br); LutData.Deserialize(br); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + // Save LutData as serialized representation + using var ms = new MemoryStream(); + using var bw = new BinaryWriter(ms); + LutData.Serialize(bw); + dict[nameof(LutData)] = Convert.ToBase64String(ms.ToArray()); + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(LutData))) + { + var base64Data = dict[nameof(LutData)].ToString(); + var bytes = Convert.FromBase64String(base64Data); + using var ms = new MemoryStream(bytes); + using var br = new BinaryReader(ms); + LutData.Deserialize(br); + } + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/NoiseFilterOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/NoiseFilterOperation.cs index 891173e..6d2b2b1 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/NoiseFilterOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/NoiseFilterOperation.cs @@ -38,4 +38,17 @@ public class NoiseFilterOperation:BaseOperation base.Load(br); NoiseSize = br.ReadInt32(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(NoiseSize)] = NoiseSize; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(NoiseSize))) + NoiseSize = Convert.ToInt32(dict[nameof(NoiseSize)]); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/RangeFilterOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/RangeFilterOperation.cs index 3eaa9a8..aa08f5b 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/RangeFilterOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/RangeFilterOperation.cs @@ -51,5 +51,33 @@ public class RangeFilterOperation: BaseOperation C3Max = br.ReadInt32(); } + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(C1Min)] = C1Min; + dict[nameof(C1Max)] = C1Max; + dict[nameof(C2Min)] = C2Min; + dict[nameof(C2Max)] = C2Max; + dict[nameof(C3Min)] = C3Min; + dict[nameof(C3Max)] = C3Max; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(C1Min))) + C1Min = Convert.ToInt32(dict[nameof(C1Min)]); + if (dict.ContainsKey(nameof(C1Max))) + C1Max = Convert.ToInt32(dict[nameof(C1Max)]); + if (dict.ContainsKey(nameof(C2Min))) + C2Min = Convert.ToInt32(dict[nameof(C2Min)]); + if (dict.ContainsKey(nameof(C2Max))) + C2Max = Convert.ToInt32(dict[nameof(C2Max)]); + if (dict.ContainsKey(nameof(C3Min))) + C3Min = Convert.ToInt32(dict[nameof(C3Min)]); + if (dict.ContainsKey(nameof(C3Max))) + C3Max = Convert.ToInt32(dict[nameof(C3Max)]); + } + } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/GetImageOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/GetImageOperation.cs index 4203ad2..68de476 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/GetImageOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/GetImageOperation.cs @@ -68,4 +68,22 @@ public class GetImageOperation:BaseOperation,IHaveImage Id = new Guid(br.ReadString()); Label = br.ReadString(); } + + /// + /// Save the operation to the dictionary + /// + /// + public override void Save(Dictionary dict) + { + base.Save(dict); + } + + /// + /// Load the operation from the dictionary + /// + /// + public override void Load(Dictionary dict) + { + base.Load(dict); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Image/GetChannelOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Image/GetChannelOperation.cs index 61f611d..437f763 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Image/GetChannelOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Image/GetChannelOperation.cs @@ -35,4 +35,17 @@ public class GetChannelOperation: BaseOperation base.Load(br); Channel = br.ReadInt32(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(Channel)] = Channel; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(Channel))) + Channel = Convert.ToInt32(dict[nameof(Channel)]); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Image/ImageSizeProportionOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Image/ImageSizeProportionOperation.cs index aa60361..e57fff1 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Image/ImageSizeProportionOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Image/ImageSizeProportionOperation.cs @@ -41,4 +41,20 @@ public class ImageSizeProportionOperation:BaseOperation Width = br.ReadDouble(); Height = br.ReadDouble(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(Width)] = Width; + dict[nameof(Height)] = Height; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(Width))) + Width = Convert.ToDouble(dict[nameof(Width)]); + if (dict.ContainsKey(nameof(Height))) + Height = Convert.ToDouble(dict[nameof(Height)]); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageFromMemoryOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageFromMemoryOperation.cs index 07afbd7..fb46680 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageFromMemoryOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageFromMemoryOperation.cs @@ -39,4 +39,17 @@ public class ImageFromMemoryOperation:BaseOperation base.Load(br); SlotName = br.ReadString(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(SlotName)] = SlotName; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(SlotName))) + SlotName = dict[nameof(SlotName)].ToString(); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageToMemoryOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageToMemoryOperation.cs index 97f2b1f..c6441f2 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageToMemoryOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageToMemoryOperation.cs @@ -33,4 +33,17 @@ public class ImageToMemoryOperation:BaseOperation base.Load(br); SlotName = br.ReadString(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(SlotName)] = SlotName; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(SlotName))) + SlotName = dict[nameof(SlotName)].ToString(); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/ClosingOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/ClosingOperation.cs index 2dada3f..1f96533 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/ClosingOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/ClosingOperation.cs @@ -40,4 +40,17 @@ public class ClosingOperation:BaseOperation base.Load(br); Closing = br.ReadInt32(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(Closing)] = Closing; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(Closing))) + Closing = Convert.ToInt32(dict[nameof(Closing)]); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/CutOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/CutOperation.cs index ad1d261..419295a 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/CutOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/CutOperation.cs @@ -80,4 +80,43 @@ public class CutOperation:BaseOperation SearchArea.Load(br); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(ReferencePoint)] = ReferencePoint.ToString(); + + // Save SearchArea properties manually + dict["SearchArea_Editable"] = SearchArea.Editable; + dict["SearchArea_IsGood"] = SearchArea.IsGood; + dict["SearchArea_LocationX"] = SearchArea.Location.X; + dict["SearchArea_LocationY"] = SearchArea.Location.Y; + dict["SearchArea_OffsetX"] = SearchArea.Offset.X; + dict["SearchArea_OffsetY"] = SearchArea.Offset.Y; + dict["SearchArea_SizeX"] = SearchArea.Size.X; + dict["SearchArea_SizeY"] = SearchArea.Size.Y; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(ReferencePoint))) + ReferencePoint = new Guid(dict[nameof(ReferencePoint)].ToString()); + + // Load SearchArea properties manually + if (dict.ContainsKey("SearchArea_Editable") || dict.ContainsKey("SearchArea_IsGood") || dict.ContainsKey("SearchArea_LocationX")) + { + SearchArea = new RectangleElement(); + if (dict.ContainsKey("SearchArea_Editable")) + SearchArea.Editable = Convert.ToBoolean(dict["SearchArea_Editable"]); + if (dict.ContainsKey("SearchArea_IsGood")) + SearchArea.IsGood = Convert.ToBoolean(dict["SearchArea_IsGood"]); + if (dict.ContainsKey("SearchArea_LocationX") && dict.ContainsKey("SearchArea_LocationY")) + SearchArea.Location = new Vector2(Convert.ToSingle(dict["SearchArea_LocationX"]), Convert.ToSingle(dict["SearchArea_LocationY"])); + if (dict.ContainsKey("SearchArea_OffsetX") && dict.ContainsKey("SearchArea_OffsetY")) + SearchArea.Offset = new Vector2(Convert.ToSingle(dict["SearchArea_OffsetX"]), Convert.ToSingle(dict["SearchArea_OffsetY"])); + if (dict.ContainsKey("SearchArea_SizeX") && dict.ContainsKey("SearchArea_SizeY")) + SearchArea.Size = new Vector2(Convert.ToSingle(dict["SearchArea_SizeX"]), Convert.ToSingle(dict["SearchArea_SizeY"])); + } + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/DilationOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/DilationOperation.cs index 438b7cd..0d9e550 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/DilationOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/DilationOperation.cs @@ -41,4 +41,17 @@ public class DilationOperation:BaseOperation base.Load(br); Dilation = br.ReadInt32(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(Dilation)] = Dilation; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(Dilation))) + Dilation = Convert.ToInt32(dict[nameof(Dilation)]); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/HatsOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/HatsOperation.cs index 207a72a..576e9c7 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/HatsOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/HatsOperation.cs @@ -40,4 +40,17 @@ public class HatsOperation : BaseOperation base.Load(br); HatSize = br.ReadInt32(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(HatSize)] = HatSize; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(HatSize))) + HatSize = Convert.ToInt32(dict[nameof(HatSize)]); + } } diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/OpeningOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/OpeningOperation.cs index f5b0c54..62803f7 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/OpeningOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/OpeningOperation.cs @@ -40,4 +40,17 @@ public class OpeningOperation : BaseOperation base.Load(br); Opening = br.ReadInt32(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(Opening)] = Opening; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(Opening))) + Opening = Convert.ToInt32(dict[nameof(Opening)]); + } } diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/SubtractOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/SubtractOperation.cs index 42b213e..b037251 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/SubtractOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/SubtractOperation.cs @@ -45,4 +45,14 @@ public class SubtractOperation : BaseOperation { base.Load(br); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + } } diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/TakeBiggestOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/TakeBiggestOperation.cs index bb6727c..0073224 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/TakeBiggestOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/TakeBiggestOperation.cs @@ -51,4 +51,14 @@ public class TakeBiggestOperation : BaseOperation { base.Load(br); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + } } diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/UnionOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/UnionOperation.cs index c96db08..846875e 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/UnionOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/UnionOperation.cs @@ -48,4 +48,14 @@ public class UnionOperation : BaseOperation { base.Load(br); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + } } diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindBlobOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindBlobOperation.cs index 0ed4360..9ed52f9 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindBlobOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindBlobOperation.cs @@ -155,4 +155,54 @@ public class FindBlobOperation : BaseOperation, IHaveOrigin, IHaveSearchArea SearchArea.Load(br); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(MinArea)] = MinArea; + dict[nameof(MaxArea)] = MaxArea; + dict[nameof(MinRadius)] = MinRadius; + dict[nameof(MaxRadius)] = MaxRadius; + dict[nameof(BadIfFound)] = BadIfFound; + dict[nameof(ReferenceId)] = ReferenceId.ToString(); + + // Save SearchArea properties manually + dict["SearchArea_Editable"] = SearchArea.Editable; + dict["SearchArea_IsGood"] = SearchArea.IsGood; + dict["SearchArea_LocationX"] = SearchArea.Location.X; + dict["SearchArea_LocationY"] = SearchArea.Location.Y; + dict["SearchArea_OffsetX"] = SearchArea.Offset.X; + dict["SearchArea_OffsetY"] = SearchArea.Offset.Y; + dict["SearchArea_SizeX"] = SearchArea.Size.X; + dict["SearchArea_SizeY"] = SearchArea.Size.Y; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(MinArea))) + MinArea = Convert.ToInt32(dict[nameof(MinArea)]); + if (dict.ContainsKey(nameof(MaxArea))) + MaxArea = Convert.ToInt32(dict[nameof(MaxArea)]); + if (dict.ContainsKey(nameof(MinRadius))) + MinRadius = Convert.ToInt32(dict[nameof(MinRadius)]); + if (dict.ContainsKey(nameof(MaxRadius))) + MaxRadius = Convert.ToInt32(dict[nameof(MaxRadius)]); + if (dict.ContainsKey(nameof(BadIfFound))) + BadIfFound = Convert.ToBoolean(dict[nameof(BadIfFound)]); + if (dict.ContainsKey(nameof(ReferenceId))) + ReferenceId = new Guid(dict[nameof(ReferenceId)].ToString()); + + // Load SearchArea properties manually + if (dict.ContainsKey("SearchArea_Editable")) + SearchArea.Editable = Convert.ToBoolean(dict["SearchArea_Editable"]); + if (dict.ContainsKey("SearchArea_IsGood")) + SearchArea.IsGood = Convert.ToBoolean(dict["SearchArea_IsGood"]); + if (dict.ContainsKey("SearchArea_LocationX") && dict.ContainsKey("SearchArea_LocationY")) + SearchArea.Location = new Vector2(Convert.ToSingle(dict["SearchArea_LocationX"]), Convert.ToSingle(dict["SearchArea_LocationY"])); + if (dict.ContainsKey("SearchArea_OffsetX") && dict.ContainsKey("SearchArea_OffsetY")) + SearchArea.Offset = new Vector2(Convert.ToSingle(dict["SearchArea_OffsetX"]), Convert.ToSingle(dict["SearchArea_OffsetY"])); + if (dict.ContainsKey("SearchArea_SizeX") && dict.ContainsKey("SearchArea_SizeY")) + SearchArea.Size = new Vector2(Convert.ToSingle(dict["SearchArea_SizeX"]), Convert.ToSingle(dict["SearchArea_SizeY"])); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsExtOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsExtOperation.cs index 92ba5e5..55bc2f6 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsExtOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsExtOperation.cs @@ -230,4 +230,78 @@ public class FindManyBlobsExtOperation : BaseOperation ReferenceId = new Guid(br.ReadString()); SearchArea.Load(br); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(MinArea)] = MinArea; + dict[nameof(MaxArea)] = MaxArea; + dict[nameof(MinRadius)] = MinRadius; + dict[nameof(MaxRadius)] = MaxRadius; + dict[nameof(CheckInternalRadius)] = CheckInternalRadius; + dict[nameof(MinInternalRadius)] = MinInternalRadius; + dict[nameof(MaxInternalRadius)] = MaxInternalRadius; + dict[nameof(CheckWidth)] = CheckWidth; + dict[nameof(MinWidth)] = MinWidth; + dict[nameof(MaxWidth)] = MaxWidth; + dict[nameof(CheckHeight)] = CheckHeight; + dict[nameof(MinHeight)] = MinHeight; + dict[nameof(MaxHeight)] = MaxHeight; + dict[nameof(ReferenceId)] = ReferenceId.ToString(); + + // Save SearchArea properties manually + dict["SearchArea_Editable"] = SearchArea.Editable; + dict["SearchArea_IsGood"] = SearchArea.IsGood; + dict["SearchArea_LocationX"] = SearchArea.Location.X; + dict["SearchArea_LocationY"] = SearchArea.Location.Y; + dict["SearchArea_OffsetX"] = SearchArea.Offset.X; + dict["SearchArea_OffsetY"] = SearchArea.Offset.Y; + dict["SearchArea_SizeX"] = SearchArea.Size.X; + dict["SearchArea_SizeY"] = SearchArea.Size.Y; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(MinArea))) + MinArea = Convert.ToInt32(dict[nameof(MinArea)]); + if (dict.ContainsKey(nameof(MaxArea))) + MaxArea = Convert.ToInt32(dict[nameof(MaxArea)]); + if (dict.ContainsKey(nameof(MinRadius))) + MinRadius = Convert.ToInt32(dict[nameof(MinRadius)]); + if (dict.ContainsKey(nameof(MaxRadius))) + MaxRadius = Convert.ToInt32(dict[nameof(MaxRadius)]); + if (dict.ContainsKey(nameof(CheckInternalRadius))) + CheckInternalRadius = Convert.ToBoolean(dict[nameof(CheckInternalRadius)]); + if (dict.ContainsKey(nameof(MinInternalRadius))) + MinInternalRadius = Convert.ToInt32(dict[nameof(MinInternalRadius)]); + if (dict.ContainsKey(nameof(MaxInternalRadius))) + MaxInternalRadius = Convert.ToInt32(dict[nameof(MaxInternalRadius)]); + if (dict.ContainsKey(nameof(CheckWidth))) + CheckWidth = Convert.ToBoolean(dict[nameof(CheckWidth)]); + if (dict.ContainsKey(nameof(MinWidth))) + MinWidth = Convert.ToInt32(dict[nameof(MinWidth)]); + if (dict.ContainsKey(nameof(MaxWidth))) + MaxWidth = Convert.ToInt32(dict[nameof(MaxWidth)]); + if (dict.ContainsKey(nameof(CheckHeight))) + CheckHeight = Convert.ToBoolean(dict[nameof(CheckHeight)]); + if (dict.ContainsKey(nameof(MinHeight))) + MinHeight = Convert.ToInt32(dict[nameof(MinHeight)]); + if (dict.ContainsKey(nameof(MaxHeight))) + MaxHeight = Convert.ToInt32(dict[nameof(MaxHeight)]); + if (dict.ContainsKey(nameof(ReferenceId))) + ReferenceId = new Guid(dict[nameof(ReferenceId)].ToString()); + + // Load SearchArea properties manually + if (dict.ContainsKey("SearchArea_Editable")) + SearchArea.Editable = Convert.ToBoolean(dict["SearchArea_Editable"]); + if (dict.ContainsKey("SearchArea_IsGood")) + SearchArea.IsGood = Convert.ToBoolean(dict["SearchArea_IsGood"]); + if (dict.ContainsKey("SearchArea_LocationX") && dict.ContainsKey("SearchArea_LocationY")) + SearchArea.Location = new Vector2(Convert.ToSingle(dict["SearchArea_LocationX"]), Convert.ToSingle(dict["SearchArea_LocationY"])); + if (dict.ContainsKey("SearchArea_OffsetX") && dict.ContainsKey("SearchArea_OffsetY")) + SearchArea.Offset = new Vector2(Convert.ToSingle(dict["SearchArea_OffsetX"]), Convert.ToSingle(dict["SearchArea_OffsetY"])); + if (dict.ContainsKey("SearchArea_SizeX") && dict.ContainsKey("SearchArea_SizeY")) + SearchArea.Size = new Vector2(Convert.ToSingle(dict["SearchArea_SizeX"]), Convert.ToSingle(dict["SearchArea_SizeY"])); + } } diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsOperation.cs index a680547..eb8bfe2 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsOperation.cs @@ -172,4 +172,51 @@ public class FindManyBlobsOperation:BaseOperation SearchArea.Load(br); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(MinArea)] = MinArea; + dict[nameof(MaxArea)] = MaxArea; + dict[nameof(MinRadius)] = MinRadius; + dict[nameof(MaxRadius)] = MaxRadius; + dict[nameof(ReferenceId)] = ReferenceId.ToString(); + + // Save SearchArea properties manually + dict["SearchArea_Editable"] = SearchArea.Editable; + dict["SearchArea_IsGood"] = SearchArea.IsGood; + dict["SearchArea_LocationX"] = SearchArea.Location.X; + dict["SearchArea_LocationY"] = SearchArea.Location.Y; + dict["SearchArea_OffsetX"] = SearchArea.Offset.X; + dict["SearchArea_OffsetY"] = SearchArea.Offset.Y; + dict["SearchArea_SizeX"] = SearchArea.Size.X; + dict["SearchArea_SizeY"] = SearchArea.Size.Y; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(MinArea))) + MinArea = Convert.ToInt32(dict[nameof(MinArea)]); + if (dict.ContainsKey(nameof(MaxArea))) + MaxArea = Convert.ToInt32(dict[nameof(MaxArea)]); + if (dict.ContainsKey(nameof(MinRadius))) + MinRadius = Convert.ToInt32(dict[nameof(MinRadius)]); + if (dict.ContainsKey(nameof(MaxRadius))) + MaxRadius = Convert.ToInt32(dict[nameof(MaxRadius)]); + if (dict.ContainsKey(nameof(ReferenceId))) + ReferenceId = new Guid(dict[nameof(ReferenceId)].ToString()); + + // Load SearchArea properties manually + if (dict.ContainsKey("SearchArea_Editable")) + SearchArea.Editable = Convert.ToBoolean(dict["SearchArea_Editable"]); + if (dict.ContainsKey("SearchArea_IsGood")) + SearchArea.IsGood = Convert.ToBoolean(dict["SearchArea_IsGood"]); + if (dict.ContainsKey("SearchArea_LocationX") && dict.ContainsKey("SearchArea_LocationY")) + SearchArea.Location = new Vector2(Convert.ToSingle(dict["SearchArea_LocationX"]), Convert.ToSingle(dict["SearchArea_LocationY"])); + if (dict.ContainsKey("SearchArea_OffsetX") && dict.ContainsKey("SearchArea_OffsetY")) + SearchArea.Offset = new Vector2(Convert.ToSingle(dict["SearchArea_OffsetX"]), Convert.ToSingle(dict["SearchArea_OffsetY"])); + if (dict.ContainsKey("SearchArea_SizeX") && dict.ContainsKey("SearchArea_SizeY")) + SearchArea.Size = new Vector2(Convert.ToSingle(dict["SearchArea_SizeX"]), Convert.ToSingle(dict["SearchArea_SizeY"])); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindRectangleOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindRectangleOperation.cs index 7800b7d..0da5558 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindRectangleOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindRectangleOperation.cs @@ -109,4 +109,54 @@ public class FindRectangleOperation:BaseOperation BadIfFound = br.ReadBoolean(); SearchArea.Load(br); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(MinWidth)] = MinWidth; + dict[nameof(MaxWidth)] = MaxWidth; + dict[nameof(MinHeight)] = MinHeight; + dict[nameof(MaxHeight)] = MaxHeight; + dict[nameof(BadIfFound)] = BadIfFound; + dict[nameof(ReferenceId)] = ReferenceId.ToString(); + + // Save SearchArea properties manually + dict["SearchArea_Editable"] = SearchArea.Editable; + dict["SearchArea_IsGood"] = SearchArea.IsGood; + dict["SearchArea_LocationX"] = SearchArea.Location.X; + dict["SearchArea_LocationY"] = SearchArea.Location.Y; + dict["SearchArea_OffsetX"] = SearchArea.Offset.X; + dict["SearchArea_OffsetY"] = SearchArea.Offset.Y; + dict["SearchArea_SizeX"] = SearchArea.Size.X; + dict["SearchArea_SizeY"] = SearchArea.Size.Y; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(MinWidth))) + MinWidth = Convert.ToInt32(dict[nameof(MinWidth)]); + if (dict.ContainsKey(nameof(MaxWidth))) + MaxWidth = Convert.ToInt32(dict[nameof(MaxWidth)]); + if (dict.ContainsKey(nameof(MinHeight))) + MinHeight = Convert.ToInt32(dict[nameof(MinHeight)]); + if (dict.ContainsKey(nameof(MaxHeight))) + MaxHeight = Convert.ToInt32(dict[nameof(MaxHeight)]); + if (dict.ContainsKey(nameof(BadIfFound))) + BadIfFound = Convert.ToBoolean(dict[nameof(BadIfFound)]); + if (dict.ContainsKey(nameof(ReferenceId))) + ReferenceId = new Guid(dict[nameof(ReferenceId)].ToString()); + + // Load SearchArea properties manually + if (dict.ContainsKey("SearchArea_Editable")) + SearchArea.Editable = Convert.ToBoolean(dict["SearchArea_Editable"]); + if (dict.ContainsKey("SearchArea_IsGood")) + SearchArea.IsGood = Convert.ToBoolean(dict["SearchArea_IsGood"]); + if (dict.ContainsKey("SearchArea_LocationX") && dict.ContainsKey("SearchArea_LocationY")) + SearchArea.Location = new Vector2(Convert.ToSingle(dict["SearchArea_LocationX"]), Convert.ToSingle(dict["SearchArea_LocationY"])); + if (dict.ContainsKey("SearchArea_OffsetX") && dict.ContainsKey("SearchArea_OffsetY")) + SearchArea.Offset = new Vector2(Convert.ToSingle(dict["SearchArea_OffsetX"]), Convert.ToSingle(dict["SearchArea_OffsetY"])); + if (dict.ContainsKey("SearchArea_SizeX") && dict.ContainsKey("SearchArea_SizeY")) + SearchArea.Size = new Vector2(Convert.ToSingle(dict["SearchArea_SizeX"]), Convert.ToSingle(dict["SearchArea_SizeY"])); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/TwoBlobsAlign.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/TwoBlobsAlign.cs index 017d7ef..a17979c 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/TwoBlobsAlign.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/TwoBlobsAlign.cs @@ -103,4 +103,23 @@ public class TwoBlobsAlign:BaseOperation,IHaveOrigin DesiredAngle = br.ReadInt32(); } + + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(Reference1)] = Reference1.ToString(); + dict[nameof(Reference2)] = Reference2.ToString(); + dict[nameof(DesiredAngle)] = DesiredAngle; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(Reference1))) + Reference1 = new Guid(dict[nameof(Reference1)].ToString()); + if (dict.ContainsKey(nameof(Reference2))) + Reference2 = new Guid(dict[nameof(Reference2)].ToString()); + if (dict.ContainsKey(nameof(DesiredAngle))) + DesiredAngle = Convert.ToInt32(dict[nameof(DesiredAngle)]); + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/EdgeIntersectionOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/EdgeIntersectionOperation.cs index 6b77a7b..a86c452 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/EdgeIntersectionOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/EdgeIntersectionOperation.cs @@ -100,5 +100,21 @@ public class EdgeIntersectionOperation : BaseOperation, IHaveOrigin } + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(ReferenceEdge1)] = ReferenceEdge1.ToString(); + dict[nameof(ReferenceEdge2)] = ReferenceEdge2.ToString(); + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(ReferenceEdge1))) + ReferenceEdge1 = new Guid(dict[nameof(ReferenceEdge1)].ToString()); + if (dict.ContainsKey(nameof(ReferenceEdge2))) + ReferenceEdge2 = new Guid(dict[nameof(ReferenceEdge2)].ToString()); + } + public OriginElement Origin { get; set; } = OriginElement.Default; } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/FindEdgeOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/FindEdgeOperation.cs index ab97113..28c3cf5 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/FindEdgeOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/FindEdgeOperation.cs @@ -124,6 +124,48 @@ public class FindEdgeOperation : BaseOperation, IHaveEdge, IHaveOrigin SearchArea.Load(br); } + public override void Save(Dictionary dict) + { + base.Save(dict); + dict[nameof(ReferenceId)] = ReferenceId.ToString(); + + // Save RotatedRectangleElement SearchArea properties manually + dict["SearchArea_Editable"] = SearchArea.Editable; + dict["SearchArea_IsGood"] = SearchArea.IsGood; + dict["SearchArea_LocationX"] = SearchArea.Location.X; + dict["SearchArea_LocationY"] = SearchArea.Location.Y; + dict["SearchArea_OffsetX"] = SearchArea.Offset.X; + dict["SearchArea_OffsetY"] = SearchArea.Offset.Y; + dict["SearchArea_HeightX"] = SearchArea.Height.X; + dict["SearchArea_HeightY"] = SearchArea.Height.Y; + dict["SearchArea_HalfWidth"] = SearchArea.HalfWidth; + } + + public override void Load(Dictionary dict) + { + base.Load(dict); + if (dict.ContainsKey(nameof(ReferenceId))) + ReferenceId = new Guid(dict[nameof(ReferenceId)].ToString()); + + // Load RotatedRectangleElement SearchArea properties manually + if (dict.ContainsKey("SearchArea_Editable") || dict.ContainsKey("SearchArea_LocationX") || dict.ContainsKey("SearchArea_HeightX")) + { + SearchArea = new RotatedRectangleElement(); + if (dict.ContainsKey("SearchArea_Editable")) + SearchArea.Editable = Convert.ToBoolean(dict["SearchArea_Editable"]); + if (dict.ContainsKey("SearchArea_IsGood")) + SearchArea.IsGood = Convert.ToBoolean(dict["SearchArea_IsGood"]); + if (dict.ContainsKey("SearchArea_LocationX") && dict.ContainsKey("SearchArea_LocationY")) + SearchArea.Location = new Vector2(Convert.ToSingle(dict["SearchArea_LocationX"]), Convert.ToSingle(dict["SearchArea_LocationY"])); + if (dict.ContainsKey("SearchArea_OffsetX") && dict.ContainsKey("SearchArea_OffsetY")) + SearchArea.Offset = new Vector2(Convert.ToSingle(dict["SearchArea_OffsetX"]), Convert.ToSingle(dict["SearchArea_OffsetY"])); + if (dict.ContainsKey("SearchArea_HeightX") && dict.ContainsKey("SearchArea_HeightY")) + SearchArea.Height = new Vector2(Convert.ToSingle(dict["SearchArea_HeightX"]), Convert.ToSingle(dict["SearchArea_HeightY"])); + if (dict.ContainsKey("SearchArea_HalfWidth")) + SearchArea.HalfWidth = Convert.ToSingle(dict["SearchArea_HalfWidth"]); + } + } + public RotatedRectangleElement SearchArea { get; set; } public OriginElement Origin { get; set; } diff --git a/Hawkeye.VisionBuilder.Workflow/WorkflowList.cs b/Hawkeye.VisionBuilder.Workflow/WorkflowList.cs index de14fa5..3cc2748 100644 --- a/Hawkeye.VisionBuilder.Workflow/WorkflowList.cs +++ b/Hawkeye.VisionBuilder.Workflow/WorkflowList.cs @@ -5,6 +5,7 @@ using System.ComponentModel.Design.Serialization; using System.Diagnostics; using System.Drawing; using System.Reflection.PortableExecutable; +using System.Text.Json; using System.Xml; using VisionBuilder.UI.Common.RecipeProcessing; using Size = OpenCvSharp.Size; @@ -32,6 +33,11 @@ public class WorkflowList long total = 0; foreach (BaseOperation operation in Operations) { + if (!operation.Enabled) + { + operation.Result=true; + continue; + } if (Context.CancellationToken.IsCancellationRequested) break; operation.Interpret(Context); } @@ -62,6 +68,7 @@ public class WorkflowList UpdateConfigVariables(); foreach (BaseOperation operation in Operations) { + if(!operation.Enabled)continue; if(operation == op) break; operation.Interpret(Context); } @@ -275,7 +282,194 @@ public class WorkflowList } } + private Dictionary ConvertJsonElementsToNatives(Dictionary dict) + { + var result = new Dictionary(); + + foreach (var kvp in dict) + { + result[kvp.Key] = ConvertJsonElementToNative(kvp.Value); + } + + return result; + } + private object ConvertJsonElementToNative(object value) + { + if (value is JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.String => element.GetString(), + JsonValueKind.Number => element.TryGetInt32(out var intVal) ? intVal : + element.TryGetDouble(out var doubleVal) ? doubleVal : + element.GetDecimal(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + JsonValueKind.Object => ConvertJsonElementsToNatives( + JsonSerializer.Deserialize>(element.GetRawText())), + JsonValueKind.Array => element.EnumerateArray() + .Select(x => ConvertJsonElementToNative(x)).ToArray(), + _ => value + }; + } + else if (value is Dictionary nestedDict) + { + return ConvertJsonElementsToNatives(nestedDict); + } + + return value; + } + public void SaveJSON(string fileName) + { + var workflowData = new Dictionary(); + + // Save Configuration + var configData = new Dictionary + { + ["RuntimeCameraType"] = Configuration.RuntimeCameraType.ToString(), + ["DevelopmentCameraType"] = Configuration.DevelopmentCameraType.ToString(), + ["Outputs"] = Configuration.Outputs.ToString(), + ["EmulationPath"] = Configuration.EmulationPath, + ["PythonPath"] = Configuration.PythonPath, + ["ResultPin"] = Configuration.ResultPin, + ["SerialPort"] = Configuration.SerialPort, + ["Delay"] = Configuration.Delay + }; + workflowData["Configuration"] = configData; + + // Save Operations + var operationsData = new List>(); + foreach (BaseOperation operation in Operations) + { + var operationData = new Dictionary + { + ["Type"] = operation.GetType().AssemblyQualifiedName + }; + + var operationDict = new Dictionary(); + operation.Save(operationDict); + operationData["Data"] = operationDict; + + operationsData.Add(operationData); + } + workflowData["Operations"] = operationsData; + + // Save Recipe Image + if (RecipeImage == null) + { + var mat = new Mat(128, 128, MatType.CV_8UC3, Scalar.Gray); + var image = mat.ToBytes(); + workflowData["RecipeImage"] = Convert.ToBase64String(image); + } + else + { + var mat = RecipeImage; + var image = mat.Resize(new Size(128, 128)).ToBytes(); + workflowData["RecipeImage"] = Convert.ToBase64String(image); + } + + var options = new JsonSerializerOptions + { + WriteIndented = true + }; + string jsonString = JsonSerializer.Serialize(workflowData, options); + File.WriteAllText(fileName, jsonString); + } + + public void LoadJSON(string fileName, OperationDiscoveryService operationDiscoveryService) + { + string jsonString = File.ReadAllText(fileName); + var workflowData = JsonSerializer.Deserialize>(jsonString); + + if (workflowData == null) return; + + // Load Configuration + if (workflowData.ContainsKey("Configuration")) + { + var configElement = (JsonElement)workflowData["Configuration"]; + var configData = JsonSerializer.Deserialize>(configElement.GetRawText()); + + if (configData != null) + { + var convertedConfigData = ConvertJsonElementsToNatives(configData); + + if (convertedConfigData.ContainsKey("RuntimeCameraType")) + Configuration.RuntimeCameraType = Enum.Parse(convertedConfigData["RuntimeCameraType"].ToString()); + if (convertedConfigData.ContainsKey("DevelopmentCameraType")) + Configuration.DevelopmentCameraType = Enum.Parse(convertedConfigData["DevelopmentCameraType"].ToString()); + if (convertedConfigData.ContainsKey("Outputs")) + Configuration.Outputs = Enum.Parse(convertedConfigData["Outputs"].ToString()); + if (convertedConfigData.ContainsKey("EmulationPath")) + Configuration.EmulationPath = convertedConfigData["EmulationPath"].ToString(); + if (convertedConfigData.ContainsKey("PythonPath")) + Configuration.PythonPath = convertedConfigData["PythonPath"].ToString(); + if (convertedConfigData.ContainsKey("ResultPin")) + Configuration.ResultPin = (int)convertedConfigData["ResultPin"]; + if (convertedConfigData.ContainsKey("SerialPort")) + Configuration.SerialPort = convertedConfigData["SerialPort"].ToString(); + if (convertedConfigData.ContainsKey("Delay")) + Configuration.Delay = (int)convertedConfigData["Delay"]; + } + } + + if (!File.Exists(Configuration.PythonPath)) + PythonMissing(); + + // Load Operations + Operations.Clear(); + if (workflowData.ContainsKey("Operations")) + { + var operationsElement = (JsonElement)workflowData["Operations"]; + var operationsArray = JsonSerializer.Deserialize>>(operationsElement.GetRawText()); + + if (operationsArray != null) + { + foreach (var operationData in operationsArray) + { + if (operationData.ContainsKey("Type") && operationData.ContainsKey("Data")) + { + string operationType = operationData["Type"].ToString(); + var type = Type.GetType(operationType); + if (type != null) + { + BaseOperation operation = operationDiscoveryService.CreateInstance(type); + + var dataElement = (JsonElement)operationData["Data"]; + var operationDict = JsonSerializer.Deserialize>(dataElement.GetRawText()); + + if (operationDict != null) + { + var convertedDict = ConvertJsonElementsToNatives(operationDict); + operation.Load(convertedDict); + } + + Operations.Add(operation); + } + } + } + } + } + + // Load Recipe Image + if (workflowData.ContainsKey("RecipeImage")) + { + string base64Image = workflowData["RecipeImage"].ToString(); + if (!string.IsNullOrEmpty(base64Image)) + { + byte[] imageData = Convert.FromBase64String(base64Image); + RecipeImage = Mat.FromImageData(imageData); + } + } + } + + public static WorkflowList LoadJSONFromFile(string fileName) + { + var workflowList = new WorkflowList(); + workflowList.LoadJSON(fileName, new OperationDiscoveryService(workflowList)); + return workflowList; + } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.cs b/Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.cs index 701fe04..22c4987 100644 --- a/Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.cs +++ b/Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.cs @@ -43,7 +43,7 @@ namespace Hawkeye.VisionBuilder.Features.ImageStatistics _goodNode.Nodes.Add(new TreeNode(nodeText) { Tag = filePath }); _allFiles.Add(filePath); UpdateBranchText(_goodNode, "Good"); - treeView.ExpandAll(); + //treeView.ExpandAll(); } public void AddBadImage(string filePath, string[]? defects = null) @@ -66,7 +66,7 @@ namespace Hawkeye.VisionBuilder.Features.ImageStatistics _badNode.Nodes.Add(new TreeNode(nodeText) { Tag = filePath }); _allFiles.Add(filePath); UpdateBranchText(_badNode, "Bad"); - treeView.ExpandAll(); + //treeView.ExpandAll(); } private void UpdateBranchText(TreeNode node, string name) diff --git a/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.Designer.cs b/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.Designer.cs index d2d1075..2395917 100644 --- a/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.Designer.cs +++ b/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.Designer.cs @@ -35,7 +35,7 @@ btnTest = new Button(); button1 = new Button(); Indicator = new DataGridViewTextBoxColumn(); - Link = new DataGridViewTextBoxColumn(); + Enabled = new DataGridViewCheckBoxColumn(); UserName = new DataGridViewTextBoxColumn(); Result = new DataGridViewTextBoxColumn(); TypeName = new DataGridViewTextBoxColumn(); @@ -50,14 +50,13 @@ dataGridView1.AllowUserToAddRows = false; dataGridView1.AllowUserToDeleteRows = false; dataGridView1.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize; - dataGridView1.Columns.AddRange(new DataGridViewColumn[] { Indicator, Link, UserName, Result, TypeName, Time }); + dataGridView1.Columns.AddRange(new DataGridViewColumn[] { Indicator, Enabled, UserName, Result, TypeName, Time }); dataGridView1.Dock = DockStyle.Fill; dataGridView1.Location = new Point(0, 40); dataGridView1.MultiSelect = false; dataGridView1.Name = "dataGridView1"; dataGridView1.ReadOnly = true; dataGridView1.RowHeadersVisible = false; - dataGridView1.RowTemplate.Height = 25; dataGridView1.SelectionMode = DataGridViewSelectionMode.FullRowSelect; dataGridView1.Size = new Size(540, 562); dataGridView1.TabIndex = 0; @@ -126,12 +125,12 @@ Indicator.ReadOnly = true; Indicator.Width = 32; // - // Link + // Enabled // - Link.HeaderText = ""; - Link.Name = "Link"; - Link.ReadOnly = true; - Link.Width = 32; + Enabled.HeaderText = ""; + Enabled.Name = "Enabled"; + Enabled.ReadOnly = true; + Enabled.Width = 32; // // UserName // @@ -182,7 +181,7 @@ private Label lblProcessingTime; private Label label1; private DataGridViewTextBoxColumn Indicator; - private DataGridViewTextBoxColumn Link; + private DataGridViewCheckBoxColumn Enabled; private DataGridViewTextBoxColumn UserName; private DataGridViewTextBoxColumn Result; private DataGridViewTextBoxColumn TypeName; diff --git a/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.cs b/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.cs index ceb1704..9da07ef 100644 --- a/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.cs +++ b/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.cs @@ -33,6 +33,7 @@ namespace Hawkeye.VisionBuilder.Features.VisionSteps dataGridView1.DataSource = _bindingSource; dataGridView1.Columns[0].DataPropertyName = nameof(BaseOperationDecorator.Result); + dataGridView1.Columns[1].DataPropertyName = nameof(BaseOperationDecorator.IsEnabled); dataGridView1.Columns[2].DataPropertyName = nameof(BaseOperationDecorator.Label); dataGridView1.Columns[2].ReadOnly = false; dataGridView1.ReadOnly = false; @@ -88,56 +89,41 @@ namespace Hawkeye.VisionBuilder.Features.VisionSteps private void DataGridView1_Paint(object? sender, PaintEventArgs e) { - var g = e.Graphics; - var c1 = dataGridView1.GetCellDisplayRectangle(1, 0, true); - var c2 = dataGridView1.GetCellDisplayRectangle(1, 1, true); - - Pen start = new Pen(Color.Blue, 2); - - Pen body = new Pen(Color.Blue, 2); - - Pen end = new Pen(Color.Blue, 2); - - end.EndCap = LineCap.Custom; - - AdjustableArrowCap bigArrow = new AdjustableArrowCap(5, 5); - end.CustomEndCap = bigArrow; - - g.DrawLine(start, c1.Right, (c1.Bottom - c1.Top) / 2 + c1.Top, (c1.Right - c1.Left) / 2 + c1.Left, - (c1.Bottom - c1.Top) / 2 + c1.Top); - - g.DrawLine(body, (c1.Right - c1.Left) / 2 + c1.Left, (c1.Bottom - c1.Top) / 2 + c1.Top, - (c2.Right - c2.Left) / 2 + c1.Left, (c2.Bottom - c2.Top) / 2 + c2.Top); - - g.DrawLine(end, (c2.Right - c2.Left) / 2 + c2.Left, (c2.Bottom - c2.Top) / 2 + c2.Top, c2.Right, - (c2.Bottom - c2.Top) / 2 + c2.Top); - + } private void DataGridView1_CellPainting(object? sender, DataGridViewCellPaintingEventArgs e) { - - if (e.ColumnIndex == 0 && e.RowIndex > -1 && e.Value != null) + + if (e.RowIndex > -1) { - var cx = e.CellBounds.Width / 2 + e.CellBounds.Left; - var cy = e.CellBounds.Height / 2 + e.CellBounds.Top; - var radius = e.CellBounds.Height / 2 - 3; var c1 = dataGridView1.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, true); + var row = dataGridView1.Rows[e.RowIndex]; + var rowValue = row.DataBoundItem as BaseOperationDecorator; e.PaintBackground(c1, true); - - if ((bool)e.Value) + if (e.ColumnIndex == 0 && e.Value != null) { - e.Graphics.FillEllipse(Brushes.Green, cx - radius, cy - radius, radius * 2, radius * 2); - } - else - { - e.Graphics.FillEllipse(Brushes.Red, cx - radius, cy - radius, radius * 2, radius * 2); - } + var cx = e.CellBounds.Width / 2 + e.CellBounds.Left; + var cy = e.CellBounds.Height / 2 + e.CellBounds.Top; + var radius = e.CellBounds.Height / 2 - 3; - e.Handled = true; + + if ((bool) e.Value) + { + e.Graphics.FillEllipse(Brushes.Green, cx - radius, cy - radius, radius * 2, radius * 2); + } + else + { + e.Graphics.FillEllipse(Brushes.Red, cx - radius, cy - radius, radius * 2, radius * 2); + } + e.Handled = true; + + + } + } } diff --git a/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.resx b/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.resx index bb59ef4..6e2d66b 100644 --- a/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.resx +++ b/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.resx @@ -1,4 +1,64 @@ - + + + @@ -60,7 +120,7 @@ True - + True diff --git a/Hawkeye.VisionBuilder/MainWindow.cs b/Hawkeye.VisionBuilder/MainWindow.cs index 2810e36..73765a7 100644 --- a/Hawkeye.VisionBuilder/MainWindow.cs +++ b/Hawkeye.VisionBuilder/MainWindow.cs @@ -182,13 +182,26 @@ namespace Hawkeye.VisionBuilder private void loadRecipeToolStripMenuItem_Click(object sender, EventArgs e) { OpenFileDialog dialog = new OpenFileDialog(); - dialog.Filter = "*.hrcp|*.hrcp"; + dialog.Filter = "Suported types|*.jhrcp;*.hrcp"; PrepareDialog(dialog); if (dialog.ShowDialog() == DialogResult.OK) { var path = dialog.FileName; - LoadRecipe(path); + var ext = Path.GetExtension(path); + if (ext == ".jhrcp") + { + LoadRecipeJSON(path); + } + else if (ext == ".hrcp") + { + LoadRecipeBinary(path); + } + else + { + MessageBox.Show("Unsupported file format. Please use .hrcp or .jhrcp files."); + return; + } } } @@ -210,7 +223,7 @@ namespace Hawkeye.VisionBuilder private string _loadedPath = null; - public void LoadRecipe(string path) + public void LoadRecipeBinary(string path) { _loadedPath = path; var bytes = File.ReadAllBytes(path); @@ -218,13 +231,23 @@ namespace Hawkeye.VisionBuilder BinaryReader reader = new BinaryReader(stream); - _workflowList.PythonMissing += _workflowList_PythonMissing; + _workflowList.Load(reader, _discoveryService); ReinitCameraPanel(_workflowList); OrganizeWindows(null, null); _visionStepsPanel.RefreshWorkflow(); } + public void LoadRecipeJSON(string path) + { + _loadedPath = path; + _workflowList.LoadJSON(path,_discoveryService); + ReinitCameraPanel(_workflowList); + OrganizeWindows(null, null); + _visionStepsPanel.RefreshWorkflow(); + + } + private void _workflowList_PythonMissing() { SetupConfig(); @@ -234,20 +257,33 @@ namespace Hawkeye.VisionBuilder private void saveRecipeToolStripMenuItem_Click(object sender, EventArgs e) { SaveFileDialog dialog = new SaveFileDialog(); - dialog.Filter = "*.hrcp|*.hrcp"; + dialog.Filter = "*.jhrcp|*.jhrcp|*.hrcp|*.hrcp"; PrepareDialog(dialog); if (dialog.ShowDialog() == DialogResult.OK) { - MemoryStream ms = new MemoryStream(); - BinaryWriter bw = new BinaryWriter(ms); - - _workflowList.RecipeImage = _workflowList.Context.LastCameraImage.ImageData; - _workflowList.Save(bw); - ms.Close(); - File.WriteAllBytes(dialog.FileName, ms.ToArray()); + var ext = Path.GetExtension(dialog.FileName); + if (ext == ".jhrcp") + { + _workflowList.SaveJSON(dialog.FileName); + } + else + { + SaveBinary(dialog.FileName); + } } } + private void SaveBinary(string path) + { + MemoryStream ms = new MemoryStream(); + BinaryWriter bw = new BinaryWriter(ms); + + _workflowList.RecipeImage = _workflowList.Context.LastCameraImage.ImageData; + _workflowList.Save(bw); + ms.Close(); + File.WriteAllBytes(path, ms.ToArray()); + } + private void toolBtnConfiguration_Click(object sender, EventArgs e) { SetupConfig(); diff --git a/VisionBuilder.UI.Camera/ModuleExtensions.cs b/VisionBuilder.UI.Camera/ModuleExtensions.cs index 794996d..9fdd331 100644 --- a/VisionBuilder.UI.Camera/ModuleExtensions.cs +++ b/VisionBuilder.UI.Camera/ModuleExtensions.cs @@ -1,4 +1,5 @@ using Hawkeye.VisionBuilder.UI.Sources.Emulation; +using Hawkeye.VisionBuilder.UI.Sources.Hawkeye; using Inspectron.Camera.UEye; using Ninject; using Ninject.Extensions.ChildKernel; @@ -26,6 +27,7 @@ public static class ModuleExtensions kernel.Bind().To().InSingletonScope(); break; case CameraSettings.EImageSource.Hawkeye: + kernel.Bind().To().InSingletonScope(); break; case CameraSettings.EImageSource.IDS: kernel.Bind().To().InSingletonScope(); diff --git a/VisionBuilder.UI.Camera/VisionBuilder.UI.Camera.csproj b/VisionBuilder.UI.Camera/VisionBuilder.UI.Camera.csproj index 149eabe..e458279 100644 --- a/VisionBuilder.UI.Camera/VisionBuilder.UI.Camera.csproj +++ b/VisionBuilder.UI.Camera/VisionBuilder.UI.Camera.csproj @@ -8,6 +8,7 @@ + diff --git a/VisionBuilder.UI.sln b/VisionBuilder.UI.sln index 4531492..6ac656d 100644 --- a/VisionBuilder.UI.sln +++ b/VisionBuilder.UI.sln @@ -75,6 +75,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "B24SiemensEmulator", "Plugi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestPlugin", "Plugins\TestPlugin\TestPlugin.csproj", "{F7D39916-489A-3583-09A0-175AE82D08B7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hawkeye.VisionBuilder.UI.Sources.Hawkeye", "Hawkeye.VisionBuilder.UI.Sources.Hawkeye\Hawkeye.VisionBuilder.UI.Sources.Hawkeye.csproj", "{C55BE2DA-C60B-491C-8668-9517C7F6FF2F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Inspectron.HawkEye", "framework\Inspectron.HawkEye\Inspectron.HawkEye.csproj", "{44D1BA17-FB52-40A2-9D99-E49DA56C10C2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Starters", "Starters", "{F3414823-B70E-435E-B4EA-80ABF4371449}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -197,12 +203,22 @@ Global {F7D39916-489A-3583-09A0-175AE82D08B7}.Debug|Any CPU.Build.0 = Debug|Any CPU {F7D39916-489A-3583-09A0-175AE82D08B7}.Release|Any CPU.ActiveCfg = Release|Any CPU {F7D39916-489A-3583-09A0-175AE82D08B7}.Release|Any CPU.Build.0 = Release|Any CPU + {C55BE2DA-C60B-491C-8668-9517C7F6FF2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C55BE2DA-C60B-491C-8668-9517C7F6FF2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C55BE2DA-C60B-491C-8668-9517C7F6FF2F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C55BE2DA-C60B-491C-8668-9517C7F6FF2F}.Release|Any CPU.Build.0 = Release|Any CPU + {44D1BA17-FB52-40A2-9D99-E49DA56C10C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44D1BA17-FB52-40A2-9D99-E49DA56C10C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44D1BA17-FB52-40A2-9D99-E49DA56C10C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44D1BA17-FB52-40A2-9D99-E49DA56C10C2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {B62D435C-3572-4B5B-A381-992345D343B0} = {F3414823-B70E-435E-B4EA-80ABF4371449} {D5FD2E9D-DA4F-1343-47E0-FBC473A149BC} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1} + {7697A266-A721-4D74-9C5C-0D4F2F6BBF68} = {F2406FBB-DFD3-4CBE-9644-A9FFC2FCBB71} {E286CE4C-B68A-94B6-F477-0DBF42358009} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1} {E8868ABD-E4D0-1B7E-494E-06FB1F7D1AF5} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1} {8B7FAFEE-4066-483C-9C9C-10D2D596206D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} @@ -224,6 +240,8 @@ Global {DB20082B-60E8-D623-3F3A-04612A929CD6} = {583A77DF-A293-4F3E-AB96-7310BC495822} {52D6A9AE-D0F1-4C52-B688-E0219169E179} = {583A77DF-A293-4F3E-AB96-7310BC495822} {F7D39916-489A-3583-09A0-175AE82D08B7} = {F2406FBB-DFD3-4CBE-9644-A9FFC2FCBB71} + {C55BE2DA-C60B-491C-8668-9517C7F6FF2F} = {D05689E3-04C6-4E3B-ACA7-3F4507CED4CC} + {44D1BA17-FB52-40A2-9D99-E49DA56C10C2} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3CE42AE5-D79F-4E97-A246-AA8FD228B677} diff --git a/framework/Inspectron.HawkEye/Camera/I2CLinux.cs b/framework/Inspectron.HawkEye/Camera/I2CLinux.cs new file mode 100644 index 0000000..b754f1a --- /dev/null +++ b/framework/Inspectron.HawkEye/Camera/I2CLinux.cs @@ -0,0 +1,159 @@ +using System; +using System.IO; +using Mono.Unix.Native; + +namespace Inspectron.Devices.Raspberry +{ + public unsafe class I2CLinux + { + string device; + int fd = -1; + + public I2CLinux(int index) + { + device = "/dev/i2c-" + index; + Open(); + //Close(); + } + + public void Open() + { + fd = Syscall.open(device, OpenFlags.O_RDWR); + if (fd < 0) + throw new IOException(device); + } + + void IoCtl(byte devAddr) + { + int ret = LunixNatives.ioctl(fd, LunixNatives.I2C_SLAVE, devAddr); + if (ret < 0) + throw new IOException(device + ": ioctl"); + } + + public byte readBytes(byte devAddr, byte regAddr, byte length, byte[] data, int offset, ushort timeout = 0) + { + if (length > 127) + throw new IOException(device + ": length > 127"); + + //Open(); + + IoCtl(devAddr); + + //fixed(byte* p = ®Addr) + { + int ret = (int)Syscall.write(fd, ®Addr, 1); + if (ret != 1) + throw new IOException(device + ": write"); + } + + int count; + fixed (byte* p = &data[offset]) + { + count = (int)Syscall.read(fd, p, (ulong)length); + if (count < 0) + throw new IOException(device + ": read"); + else if (count != length) + throw new IOException(device + ": read short: length = " + length + " > " + count); + } + + //Close(); + + return (byte)count; + } + + public byte readBytes(byte devAddr, byte regAddr, byte length, byte[] data, ushort timeout = 0) + { + return readBytes(devAddr, regAddr, length, data, 0, timeout); + } + + /** Write multiple bytes to an 8-bit device register. + * @param devAddr I2C slave device address + * @param regAddr First register address to write to + * @param length Number of bytes to write + * @param data Buffer to copy new data from + * @return Status of operation (true = success) + */ + public void writeBytes(byte devAddr, byte regAddr, byte length, byte[] data) + { + if (length > 127) + throw new IOException(device + ": length > 127"); + + //Open(); + IoCtl(devAddr); + + byte[] buffer = new byte[128]; + buffer[0] = regAddr; + Array.Copy(data, 0, buffer, 1, length); + + int count; + fixed (byte* p = buffer) + { + count = (int)Syscall.write(fd, p, (ulong)(length + 1)); + } + + if (count < 0) + { + throw new IOException(device + ": write = " + count); + } + else if (count != length + 1) + { + throw new IOException(device + ": write short = " + count); + } + + //Close(); + } + + + /** Write multiple words to a 16-bit device register. + * @param devAddr I2C slave device address + * @param regAddr First register address to write to + * @param length Number of words to write + * @param data Buffer to copy new data from + * @return Status of operation (true = success) + */ + public void writeWords(byte devAddr, byte regAddr, byte length, ushort[] data) + { + int count = 0; + byte[] buf = new byte[128]; + int i; + + // Should do potential byteswap and call writeBytes() really, but that + // messes with the callers buffer + + if (length > 63) + { + throw new IOException(device + ": length > 63"); + } + + //Open(); + IoCtl(devAddr); + + buf[0] = regAddr; + for (i = 0; i < (int)length; i++) + { + buf[i * 2 + 1] = (byte)(data[i] >> 8); + buf[i * 2 + 2] = (byte)data[i]; + } + fixed (byte* p = buf) + { + count = (int)Syscall.write(fd, p, (ulong)(length * 2 + 1)); + } + if (count < 0) + { + throw new IOException(device + ": write"); + } + else if (count != length * 2 + 1) + { + throw new IOException(device + ": write short"); + } + //Close(); + } + + public void Close() + { + int ret = Syscall.close(fd); + if (ret != 0) + throw new IOException(device); + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Camera/InspectronCamera.cs b/framework/Inspectron.HawkEye/Camera/InspectronCamera.cs new file mode 100644 index 0000000..0bf398d --- /dev/null +++ b/framework/Inspectron.HawkEye/Camera/InspectronCamera.cs @@ -0,0 +1,112 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using Inspectron.Devices.Raspberry; +using Inspectron.HawkEye.Protocol; +using Inspectron.HawkEye.Protocol.Interfaces; + +namespace Inspectron.HawkEye.Camera +{ + public class InspectronCamera:ICameraControl,IImageSource,ILightControl + { + + [DllImport("libVCLibProxy.so", CallingConvention = CallingConvention.Cdecl)] + static extern IntPtr init(Int32 captBuf); + [DllImport("libVCLibProxy.so", CallingConvention = CallingConvention.Cdecl)] + static extern int trigger(IntPtr cpt, byte[] addr, int lines, int captBuf,ref int cancelFlag); + [DllImport("libVCLibProxy.so", CallingConvention = CallingConvention.Cdecl)] + static extern void set_parameters(IntPtr cpt, ref ImageSettings imageSettings); + + [DllImport("libVCLibProxy.so", CallingConvention = CallingConvention.Cdecl)] + public static extern int test(); + byte[] _buffer = new byte[2048 * 250 * 4]; + + public InspectronCamera() + { + + _i2c = new I2CLinux(0); + _i2c.Open(); + + } + + + + private bool _isRunningContiniuos = false; + private IntPtr _cp= IntPtr.Zero; + private ImageSettings _imageSettings=new ImageSettings() + { + CaptureBuffer = 25, + Gain = 200, + Lines = 1000, + Shutter = 200, + SensorWidth = 1440 + }; + + private I2CLinux _i2c; + + private int _lastBuffer = -1; + public void SetParameters(CameraSettings cameraSettings) + + { + _cameraSettings = cameraSettings; + ImageSettings imageSettings = cameraSettings.ImageSettings; + if (_lastBuffer ==-1) + { + _lastBuffer = imageSettings.CaptureBuffer; + _cp = init(_lastBuffer); + } + + + _imageSettings = imageSettings; + _buffer=new byte[imageSettings.SensorWidth*(imageSettings.Lines)]; + var cpImageSettings = imageSettings; + cpImageSettings.UseExternalTrigger =imageSettings.UseExternalTrigger; + + if(_lastBuffer==imageSettings.CaptureBuffer) + set_parameters(_cp, ref cpImageSettings); + else + Console.WriteLine("Warning! Buffer size changed. Needs restart"); + _i2c.writeBytes(4, 2, 4, BitConverter.GetBytes(imageSettings.Divider)); + Thread.Sleep(100); + _i2c.writeBytes(4,3,1, new byte[] { (byte)imageSettings.UseExternalTrigger }); + Thread.Sleep(100); + } + + + private int _cancelFlag = 0; + private CameraSettings _cameraSettings; + + public byte[] GetImage() + { + _cancelFlag = 0; + var sw = Stopwatch.StartNew(); + + _i2c.writeBytes(4, 4, 1,new byte[]{ (byte)(_cameraSettings.LaserTrigger?1:0)} ); + //Thread.Sleep(20); + _i2c.writeBytes(4, 5, 4, BitConverter.GetBytes(_cameraSettings.LaserTriggerDelay)); + //Thread.Sleep(20); + + trigger(_cp, _buffer, _imageSettings.Lines, _imageSettings.CaptureBuffer,ref _cancelFlag); + sw.Stop(); + Console.WriteLine("Trigger time: "+sw.ElapsedMilliseconds); + if(_cancelFlag==1)return new byte[0]; + return _buffer; + } + + public void CancelTrigger() + { + _cancelFlag = 1; + } + + public void SetLight(int pwm1, int pwm2) + { + Console.WriteLine($"setting lights to {pwm1}/{pwm2}"); + pwm1 = (int)(pwm1 / 100.0 * 255); + pwm2 = (int)(pwm2 / 100.0 * 255); + _i2c.writeBytes(4, 1, 2, new byte[] { (byte)pwm1,(byte)pwm2 }); + + Thread.Sleep(10); + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Camera/LunixNatives.cs b/framework/Inspectron.HawkEye/Camera/LunixNatives.cs new file mode 100644 index 0000000..de126bd --- /dev/null +++ b/framework/Inspectron.HawkEye/Camera/LunixNatives.cs @@ -0,0 +1,21 @@ +using System.Runtime.InteropServices; + +namespace Inspectron.Devices.Raspberry +{ + public static class LunixNatives + { + public const int O_RDWR = 2; + + [DllImport("libc.so.6")] + extern public static int open(string file, int mode); + + [DllImport("libc.so.6")] + extern public static int close(int fd); + + [DllImport("libc.so.6")] + extern public static int ioctl(int fd, int request, byte x); + + public const int I2C_SLAVE = 0x0703; + + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/DefragmentedPacket.cs b/framework/Inspectron.HawkEye/DefragmentedPacket.cs new file mode 100644 index 0000000..03f7be3 --- /dev/null +++ b/framework/Inspectron.HawkEye/DefragmentedPacket.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; + +namespace Inspectron.HawkEye +{ + public class DefragmentedPacket + { + private readonly uint _packetSize; + private byte[] _receivedParts = null; + //ConcurrentDictionary _packetParts = new ConcurrentDictionary(); + private byte[][] _packetParts; + public DefragmentedPacket(uint packetSize) + { + _packetSize = packetSize; + _packetParts=new byte[30000][]; + } + + private int _uniquePackets=0; + public void Defragment(byte[] data) + { + var packetStart = -1; + + for (int i = 0; i < data.Length; i++) + { + if (BitConverter.ToUInt32(data, i) == 114455) + { + packetStart = i; + break; + }; + } + + if (packetStart == -1) return; + + MemoryStream ms = new MemoryStream(data,packetStart, data.Length - packetStart); + BinaryReader br = new BinaryReader(ms); + br.ReadUInt32();//packetStart + var packetType = br.ReadUInt32(); + var sequenceId = br.ReadUInt32(); + var packetNumber = br.ReadUInt32(); + + var totalPackets = br.ReadUInt32(); + + if(_receivedParts==null)_receivedParts=new byte[totalPackets]; + if (_receivedParts[packetNumber] == 1) return; + _receivedParts[packetNumber] = 1; + Interlocked.Increment(ref _uniquePackets); + var dataLen = br.ReadInt32(); + + var dataBytes = br.ReadBytes(dataLen); + + + _packetParts[packetNumber] = dataBytes; + + } + + public byte[] Reconstruct() + { + var parts = _receivedParts.Length; + byte[] res = new byte[parts*_packetSize]; + Stopwatch sw = Stopwatch.StartNew(); + int resSize = 0; + for (int i = 0; i < parts; i++) + { + //if (_packetParts.ContainsKey((uint) i)) + if (_packetParts[i]!=null) + { + var packetData = _packetParts[(uint) i]; + + Array.Copy(packetData, 0,res, resSize, packetData.Length); + resSize += packetData.Length; + } + else + { + resSize += (int)_packetSize-20/*headerSize*/; + } + } + + sw.Stop(); + + + Array.Resize(ref res,resSize); + + return res; + + } + + public bool IsComplete + { + get + { + if (_receivedParts == null) return false; + return _uniquePackets == _receivedParts.Length; + //return _receivedParts.All(x => x == 1); + } + } + + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/FragmentedPacket.cs b/framework/Inspectron.HawkEye/FragmentedPacket.cs new file mode 100644 index 0000000..1a9ec45 --- /dev/null +++ b/framework/Inspectron.HawkEye/FragmentedPacket.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; + +namespace Inspectron.HawkEye +{ + public class FragmentedPacket + { + private readonly uint _packetSize; + private readonly EPacketType _packetType; + private readonly uint _sequnceId; + + public FragmentedPacket(uint packetSize,EPacketType packetType,uint sequnceId) + { + _packetSize = packetSize; + _packetType = packetType; + _sequnceId = sequnceId; + } + public IEnumerable Fragment(byte[] packetData) + { + + + + uint dataPtr = 0; + + int packetNumber = 0; + uint headerSize = 20; + uint payloadSize = (_packetSize - headerSize); + var totalPackets = (uint)Math.Ceiling(((double)packetData.Length / payloadSize)); + do + { + MemoryStream ms = new MemoryStream(); + BinaryWriter bw = new BinaryWriter(ms); + bw.Write((uint) 114455); //packetStart //4 + bw.Write((uint) _packetType); //8 + bw.Write((uint) _sequnceId); //12 + bw.Write((uint) packetNumber); //16 + //total packets? + + + bw.Write(totalPackets); //20 + + byte[] data = new byte[payloadSize]; + uint dataSize = Math.Min((uint)(packetData.Length-dataPtr), payloadSize); + Array.Copy(packetData, dataPtr, data, 0, dataSize); + dataPtr += dataSize; + bw.Write(dataSize); + bw.Write(data); + + packetNumber += 1; + yield return ms.ToArray(); + + } while (packetNumber < totalPackets); + + + + + + + } + public IEnumerable FragmentTo(byte[] packetData,UDPSocket socket) + { + + + + uint dataPtr = 0; + + int packetNumber = 0; + uint headerSize = 20; + uint payloadSize = (_packetSize - headerSize); + var totalPackets = (uint)Math.Ceiling(((double)packetData.Length / payloadSize)); + do + { + MemoryStream ms = new MemoryStream(); + BinaryWriter bw = new BinaryWriter(ms); + bw.Write((uint)114455); //packetStart //4 + bw.Write((uint)_packetType); //8 + bw.Write((uint)_sequnceId); //12 + bw.Write((uint)packetNumber); //16 + //total packets? + + + bw.Write(totalPackets); //20 + + byte[] data = new byte[payloadSize]; + uint dataSize = Math.Min((uint)(packetData.Length - dataPtr), payloadSize); + Array.Copy(packetData, dataPtr, data, 0, dataSize); + dataPtr += dataSize; + bw.Write(dataSize); + bw.Write(data); + + packetNumber += 1; + yield return ms.ToArray(); + + } while (packetNumber < totalPackets); + + + + + + + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Inspectron.HawkEye.csproj b/framework/Inspectron.HawkEye/Inspectron.HawkEye.csproj new file mode 100644 index 0000000..e73d307 --- /dev/null +++ b/framework/Inspectron.HawkEye/Inspectron.HawkEye.csproj @@ -0,0 +1,21 @@ + + + + netstandard2.0 + + + + true + + + + true + + + + + + + + + diff --git a/framework/Inspectron.HawkEye/Packets/EPacketType.cs b/framework/Inspectron.HawkEye/Packets/EPacketType.cs new file mode 100644 index 0000000..f014bf7 --- /dev/null +++ b/framework/Inspectron.HawkEye/Packets/EPacketType.cs @@ -0,0 +1,9 @@ +namespace Inspectron.HawkEye +{ + public enum EPacketType + { + Test, + ImageData, + ImageRequest + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Packets/ImageRequestPacket.cs b/framework/Inspectron.HawkEye/Packets/ImageRequestPacket.cs new file mode 100644 index 0000000..553b5d5 --- /dev/null +++ b/framework/Inspectron.HawkEye/Packets/ImageRequestPacket.cs @@ -0,0 +1,7 @@ +namespace Inspectron.HawkEye +{ + public class ImageRequestPacket + { + + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Packets/Packet.cs b/framework/Inspectron.HawkEye/Packets/Packet.cs new file mode 100644 index 0000000..b66af52 --- /dev/null +++ b/framework/Inspectron.HawkEye/Packets/Packet.cs @@ -0,0 +1,10 @@ +using System; + +namespace Inspectron.HawkEye +{ + public class Packet + { + public EPacketType PacketType { get; set; } + public byte[] Payload { get; set; } + } +} diff --git a/framework/Inspectron.HawkEye/Packets/PacketImage.cs b/framework/Inspectron.HawkEye/Packets/PacketImage.cs new file mode 100644 index 0000000..7725f83 --- /dev/null +++ b/framework/Inspectron.HawkEye/Packets/PacketImage.cs @@ -0,0 +1,12 @@ +namespace Inspectron.HawkEye +{ + public class PacketImage + { + public uint TriggerId { get; set; } + public uint PacketId { get; set; } + public uint TotalPackets { get; set; } + + public uint StartIndex { get; set; } + public byte[] Data { get; set; } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Protocol/CameraSettings.cs b/framework/Inspectron.HawkEye/Protocol/CameraSettings.cs new file mode 100644 index 0000000..dcc557a --- /dev/null +++ b/framework/Inspectron.HawkEye/Protocol/CameraSettings.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace Inspectron.HawkEye.Protocol +{ + public class CameraSettings + { + public ImageSettings ImageSettings { get; set; } + public int LightPwm1 { get; set; } + public int LightPwm2 { get; set; } + public string Name { get; set; } + public int OffsetX { get; set; } + public int ImageWidth { get; set; } + public int RescaleWidth { get; set; } + public int MinorCutoff { get; set; } + public bool BayerFilter { get; set; } + public bool LaserTrigger { get; set; } + public int LaserTriggerDelay { get; set; } + public bool FlipLines { get; set; } + public bool MirrorX { get; set; } + public bool TriggerLights { get; set; } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Protocol/Discovery/CameraInfo.cs b/framework/Inspectron.HawkEye/Protocol/Discovery/CameraInfo.cs new file mode 100644 index 0000000..37068ef --- /dev/null +++ b/framework/Inspectron.HawkEye/Protocol/Discovery/CameraInfo.cs @@ -0,0 +1,12 @@ +using System.Net; +using System.Net.NetworkInformation; + +namespace Inspectron.HawkEye.Protocol.Discovery +{ + public class CameraInfo + { + public string Mac { get; set; } + public IPAddress Address { get; set; } + public IPAddress AdapterAddress { get; set; } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Protocol/Discovery/DiscoveryClient.cs b/framework/Inspectron.HawkEye/Protocol/Discovery/DiscoveryClient.cs new file mode 100644 index 0000000..c66ed7a --- /dev/null +++ b/framework/Inspectron.HawkEye/Protocol/Discovery/DiscoveryClient.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace Inspectron.HawkEye.Protocol.Discovery +{ + public class DiscoveryClient + { + public ReadOnlyCollection Discovered => new ReadOnlyCollection(_discovered); + private readonly int _port; + private readonly List _discovered = new List(); + + private readonly object _discoveryLock = new object(); + + public DiscoveryClient(int port) + { + _port = port; + } + + public event Action CameraFound = delegate { }; + + public void Discover() + { + var allInterfaces = NetworkInterface + .GetAllNetworkInterfaces() + .Where(nic => nic.OperationalStatus == OperationalStatus.Up); + + + Parallel.ForEach(allInterfaces, DiscoverOnInterface); + //foreach (NetworkInterface i in allInterfaces) + //{ + // DiscoverOnInterface(i); + //} + } + + private void DiscoverOnInterface(NetworkInterface iface) + { + var address = iface.GetIPProperties().UnicastAddresses + .First(x => x.Address.AddressFamily == AddressFamily.InterNetwork).Address; + UdpClient client; + lock (_discoveryLock) + { + client = new UdpClient(new IPEndPoint(address, 0)); + var requestData = Encoding.ASCII.GetBytes("discovery"); + client.Client.ReceiveTimeout = 2000; + + var s = client.Client; + s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1); + s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DontRoute, 1); + client.EnableBroadcast = true; + client.Send(requestData, requestData.Length, new IPEndPoint(IPAddress.Broadcast, _port)); + client.Send(requestData, requestData.Length, new IPEndPoint(IPAddress.Broadcast, _port)); + client.Send(requestData, requestData.Length, new IPEndPoint(IPAddress.Broadcast, _port)); + } + + var serverEp = new IPEndPoint(IPAddress.Any, 0); + + byte[] serverResponseData; + try + { + serverResponseData = client.Receive(ref serverEp); + var serverResponse = Encoding.ASCII.GetString(serverResponseData); + Console.WriteLine("Recived {0} from {1}", serverResponse, serverEp.Address); + var found = new CameraInfo {Mac = serverResponse, Address = serverEp.Address,AdapterAddress = address}; + lock (_discoveryLock) + { + if (_discovered.Any(x => x.Mac == found.Mac)) return; + } + + CameraFound(found); + _discovered.Add(found); + } + catch + { + } + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Protocol/Discovery/DiscoveryServer.cs b/framework/Inspectron.HawkEye/Protocol/Discovery/DiscoveryServer.cs new file mode 100644 index 0000000..b6fbc9e --- /dev/null +++ b/framework/Inspectron.HawkEye/Protocol/Discovery/DiscoveryServer.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text; +using System.Threading; + +namespace Inspectron.HawkEye.Protocol.Discovery +{ + public class DiscoveryServer + { + private UdpClient _server; + private byte[] _name; + + public DiscoveryServer(int port,string name=null) + { + _server = new UdpClient(port); + + if(name==null) + { _name = Encoding.UTF8.GetBytes( NetworkInterface + .GetAllNetworkInterfaces() + .Where(nic => nic.OperationalStatus == OperationalStatus.Up && nic.NetworkInterfaceType != NetworkInterfaceType.Loopback) + .Select(nic => nic.GetPhysicalAddress().ToString()) + .FirstOrDefault()); + + } + else + { + _name = Encoding.UTF8.GetBytes(name); + } + + } + + public void Start() + { + Thread th = new Thread(DiscoveryLoop); + th.Start(); + } + private void DiscoveryLoop() + { + while (true) + { + var clientEp = new IPEndPoint(IPAddress.Any, 0); + var clientRequestData = _server.Receive(ref clientEp); + var clientRequest = Encoding.ASCII.GetString(clientRequestData); + + Console.WriteLine("Recived {0} from {1}, sending response", clientRequest, clientEp.Address.ToString()); + _server.Send(_name, _name.Length, clientEp); + _server.Send(_name, _name.Length, clientEp); + _server.Send(_name, _name.Length, clientEp); + } + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Protocol/ECommand.cs b/framework/Inspectron.HawkEye/Protocol/ECommand.cs new file mode 100644 index 0000000..bd0d444 --- /dev/null +++ b/framework/Inspectron.HawkEye/Protocol/ECommand.cs @@ -0,0 +1,13 @@ +namespace Inspectron.HawkEye.Protocol +{ + public enum ECommand + { + Connect, + Trigger, StartContinuous, StopContinuous, + Settings,SaveSettings, + OK, + SaveCalibration, + GetCalibration, + NotOK + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Protocol/EData.cs b/framework/Inspectron.HawkEye/Protocol/EData.cs new file mode 100644 index 0000000..84e3a0d --- /dev/null +++ b/framework/Inspectron.HawkEye/Protocol/EData.cs @@ -0,0 +1,9 @@ +namespace Inspectron.HawkEye.Protocol +{ + public enum EData + { + Image, + + OK + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Protocol/ImageClient.cs b/framework/Inspectron.HawkEye/Protocol/ImageClient.cs new file mode 100644 index 0000000..d8c5a0b --- /dev/null +++ b/framework/Inspectron.HawkEye/Protocol/ImageClient.cs @@ -0,0 +1,114 @@ +using System; +using System.Net; +using System.Text; +using System.Threading; +using Inspectron.HawkEye.UDPB; +using Newtonsoft.Json; + +namespace Inspectron.HawkEye.Protocol +{ + public class ImageClient : IDisposable + { + private static readonly object connectionLock = new object(); + private readonly IPEndPoint _endpoint; + private readonly IPAddress _adapter; + + private readonly UDPBSocket _imageSocket = new UDPBSocket(); + private readonly UDPBSocket _commandSocket = new UDPBSocket(); + private bool _isConnected = true; + + public ImageClient(IPEndPoint endpoint,IPAddress adapter) + { + _endpoint = endpoint; + _adapter = adapter; + + } + + public void Dispose() + { + _isConnected = false; + _commandSocket.Dispose(); + } + + public void Connect() + { + lock (connectionLock) + { + _commandSocket.Connect(_endpoint,_adapter); + var port = UDPBSocket.FindFreePort(_adapter); + var bytesPort = BitConverter.GetBytes(port); + _commandSocket.SendData(new[] + {(byte) ECommand.Connect, bytesPort[0], bytesPort[1], bytesPort[2], bytesPort[3]}); + var settingsData0 = _commandSocket.Receive(); + var b = new byte[settingsData0.Length - 1]; + Array.Copy(settingsData0, 1, b, 0, b.Length); + var settingsString = Encoding.UTF8.GetString(b); + SettingsReceived(JsonConvert.DeserializeObject(settingsString)); + _imageSocket.Listen(_adapter,port); + var th = new Thread(ReceiveLoop); + th.Start(); + } + } + + public event Action ImageReceived = delegate { }; + public event Action SettingsReceived = delegate { }; + + + public void Trigger() + { + _commandSocket.SendData(new[] {(byte) ECommand.Trigger}); + } + + public void ApplySettings(CameraSettings cameraSettings) + { + var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(cameraSettings)); + var databytes = new byte[1000]; + databytes[0] = (byte) ECommand.Settings; + Array.Copy(data, 0, databytes, 1, data.Length); + + _commandSocket.SendData(databytes); + } + + public void StartContinuous() + { + _commandSocket.SendData(new[] {(byte) ECommand.StartContinuous}); + } + + public void StopContinuous() + { + _commandSocket.SendData(new[] {(byte) ECommand.StopContinuous}); + } + + public void SaveSettingsOnCamera() + { + _commandSocket.SendData(new[] {(byte) ECommand.SaveSettings}); + } + + + private void ReceiveLoop() + { + while (_isConnected) + { + var data = _imageSocket.Receive(); + + Process(data); + + + } + } + + private void Process(byte[] data) + { + switch ((EData) data[0]) + { + case EData.Image: + var b = new byte[data.Length - 1]; + Array.Copy(data, 1, b, 0, b.Length); + ImageReceived(data); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Protocol/ImageClientTCP.cs b/framework/Inspectron.HawkEye/Protocol/ImageClientTCP.cs new file mode 100644 index 0000000..811fd26 --- /dev/null +++ b/framework/Inspectron.HawkEye/Protocol/ImageClientTCP.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using Inspectron.HawkEye.UDPB; +using Newtonsoft.Json; + +namespace Inspectron.HawkEye.Protocol +{ + public class ImageClientTCP : IDisposable + { + private static readonly object connectionLock = new object(); + private readonly IPEndPoint _endpoint; + private readonly IPAddress _adapter; + + private TcpListener _imageSocket; + private TcpClient _commandSocket; + private bool _isConnected = true; + + public ImageClientTCP(IPEndPoint endpoint,IPAddress adapter) + { + _endpoint = endpoint; + _adapter = adapter; + + } + + public void Dispose() + { + _isConnected = false; + _commandSocket.Dispose(); + } + byte[] _commandBuffer = new byte[1500]; + private TcpClient _lastClient; + public bool SupportsCalibration { get; set; } + public void Connect() + { + lock (connectionLock) + { + _commandSocket=new TcpClient(new IPEndPoint(_adapter, 0)); + Console.WriteLine($"Bind on {_adapter?.ToString()}"); + + _commandSocket.Connect(_endpoint); + var port = UDPBSocket.FindFreePort(_adapter); + Console.WriteLine("Connected"); + + _imageSocket = new TcpListener(_adapter, port); + _imageSocket.Start(); + + Console.WriteLine("TCP started"); + + var bytesPort = BitConverter.GetBytes(port); + _commandSocket.Client.SendData(new[] + {(byte) ECommand.Connect, bytesPort[0], bytesPort[1], bytesPort[2], bytesPort[3]}); + + var receivedLen = _commandSocket.Client.Receive(_commandBuffer); + Console.WriteLine("received answer length:"+receivedLen); + var b = new byte[receivedLen - 1]; + Array.Copy(_commandBuffer, 1, b, 0, b.Length); + var settingsString = Encoding.UTF8.GetString(b); + SettingsReceived(JsonConvert.DeserializeObject(settingsString)); + if (SupportsCalibration) + { + + _commandSocket.Client.SendData(new[] {(byte) ECommand.GetCalibration}); + receivedLen = _commandSocket.Client.Receive(_commandBuffer); + if (_commandBuffer[0] == (byte) ECommand.OK) + { + Console.WriteLine($"calibration received {receivedLen} bytes"); + var c = new byte[receivedLen - 1]; + Array.Copy(_commandBuffer, 1, c, 0, c.Length); + var calibrationString = Encoding.UTF8.GetString(c); + CalibrationReceived(JsonConvert.DeserializeObject>>(calibrationString)); + + } + else + { + Console.WriteLine("no calibration received"); + } + } + + + var th = new Thread(ReceiveLoop); + th.Start(); + } + } + + public event Action ImageReceived = delegate { }; + public event Action SettingsReceived = delegate { }; + public event Action>> CalibrationReceived = delegate { }; + + + public void Trigger() + { + _commandSocket.Client.SendData(new[] {(byte) ECommand.Trigger}); + } + + public void ApplySettings(CameraSettings cameraSettings) + { + var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(cameraSettings)); + var databytes = new byte[1000]; + databytes[0] = (byte) ECommand.Settings; + Array.Copy(data, 0, databytes, 1, data.Length); + + _commandSocket.Client.SendData(databytes); + byte[] ok = new byte[1]; + _commandSocket.Client.Receive(ok); + } + + public void StartContinuous() + { + _commandSocket.Client.SendData(new[] {(byte) ECommand.StartContinuous}); + } + + public void StopContinuous() + { + _commandSocket.Client.SendData(new[] {(byte) ECommand.StopContinuous}); + } + + public void SaveSettingsOnCamera() + { + _commandSocket.Client.SendData(new[] {(byte) ECommand.SaveSettings}); + } + + private byte[] _imageBuffer = new byte[10*1024*1024]; + private void ReceiveLoop() + { + while (_isConnected) + { + _lastClient = _imageSocket.AcceptTcpClient(); + while (true) + { + int received=0; + + try + { + _lastClient.Client.Receive(_imageBuffer, 0, 1, + SocketFlags.None); + Process(_imageBuffer); + } + catch + { + break; + } + + + } + + + + } + } + + public void SaveCalibration(List> calibration) + { + var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(calibration)); + var databytes = new byte[1500]; + databytes[0] = (byte)ECommand.SaveCalibration; + Array.Copy(data, 0, databytes, 1, data.Length); + + _commandSocket.Client.SendData(databytes); + byte[] ok = new byte[1]; + _commandSocket.Client.Receive(ok); + } + + private void Process(byte[] data) + { + switch ((EData) data[0]) + { + case EData.Image: + _lastClient.Client.Receive(_imageBuffer, 1, 4, + SocketFlags.None); + var imageSize = BitConverter.ToInt32(_imageBuffer,1); + var received = 0; + do + { + received += _lastClient.Client.Receive(_imageBuffer, 5+ received, imageSize- received, + SocketFlags.None); + } while (received < imageSize); + + var b = new byte[imageSize]; + Array.Copy(data, 5, b, 0, b.Length); + ImageReceived(b); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Protocol/ImageServer.cs b/framework/Inspectron.HawkEye/Protocol/ImageServer.cs new file mode 100644 index 0000000..bab6ce6 --- /dev/null +++ b/framework/Inspectron.HawkEye/Protocol/ImageServer.cs @@ -0,0 +1,164 @@ +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Inspectron.HawkEye.Protocol.Interfaces; +using Inspectron.HawkEye.UDPB; +using Newtonsoft.Json; + +namespace Inspectron.HawkEye.Protocol +{ + public class ImageServer + { + private readonly IImageSource _imageSource; + private readonly ICameraControl _cameraControl; + private readonly ILightControl _lightControl; + private readonly UDPBSocket _commandSocket = new UDPBSocket(); + private readonly UDPBSocket _imageSocket = new UDPBSocket(); + + private bool _autoTrigger; + private bool _applySettings; + private CameraSettings _imageSettingsToApply=new CameraSettings(){ImageSettings = new ImageSettings()}; + + public ImageServer(IImageSource imageSource, ICameraControl cameraControl, ILightControl lightControl) + { + _imageSource = imageSource; + _cameraControl = cameraControl; + _lightControl = lightControl; + _imageSocket.LossSimulation = 0; + } + + public void Start() + { + _commandSocket.Listen(IPAddress.Any, 27001); + var th = new Thread(ReceiveLoop); + th.Start(); + if (File.Exists("settings.json")) + { + var settings = JsonConvert.DeserializeObject(File.ReadAllText("settings.json")); + _imageSettingsToApply = settings; + ApplyParameters(settings); + } + } + + public void ApplyParameters(CameraSettings cameraSettings) + { + _cameraControl.SetParameters(cameraSettings); + + _lightControl.SetLight(cameraSettings.LightPwm1, cameraSettings.LightPwm2); + } + + private void SaveSettingsLocally() + { + File.WriteAllText("settings.json", JsonConvert.SerializeObject(_imageSettingsToApply)); + } + + private void ReceiveLoop() + { + while (true) + { + + var data = _commandSocket.Receive(); + try + { + ProcessCommand(data); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + } + + private void TriggerLoop() + { + while (_autoTrigger) + { + + if (_applySettings) + { + ApplyParameters(_imageSettingsToApply); + _applySettings = false; + } + + try + { + SendImage(); + + } + catch + { + return; + } + } + } + + private void ProcessCommand(byte[] data) + { + Console.WriteLine(((ECommand) data[0]).ToString()); + switch ((ECommand) data[0]) + { + case ECommand.Connect: + { + var port = new byte[4]; + Array.Copy(data, 1, port, 0, 4); + _imageSocket.Connect(new IPEndPoint((_commandSocket.LastConnection as IPEndPoint).Address, BitConverter.ToInt32(port,0)),null); + var databytes = new byte[1000]; + var settings = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(_imageSettingsToApply)); + Array.Copy(settings, 0, databytes, 1, settings.Length); + databytes[0] = (byte) ECommand.Settings; + _commandSocket.SendData(databytes); + } + break; + case ECommand.Trigger: + { + Task.Run(() => { SendImage(); }); + } + break; + case ECommand.StartContinuous: + { + _autoTrigger = true; + var th = new Thread(TriggerLoop); + th.Start(); + } + break; + case ECommand.StopContinuous: + { + _autoTrigger = false; + } + break; + case ECommand.Settings: + { + var databytes = new byte[999]; + Array.Copy(data, 1, databytes, 0, 999); + var settings = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(databytes)); + _imageSettingsToApply = settings; + if (_autoTrigger) + _applySettings = true; + else + ApplyParameters(_imageSettingsToApply); + } + break; + case ECommand.SaveSettings: + { + SaveSettingsLocally(); + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + private void SendImage() + { + var image = _imageSource.GetImage(); + var b = new byte[image.Length + 1]; + b[0] = (byte) EData.Image; + Array.Copy(image, 0, b, 1, image.Length); + + _imageSocket.SendData(b); + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Protocol/ImageServerTCP.cs b/framework/Inspectron.HawkEye/Protocol/ImageServerTCP.cs new file mode 100644 index 0000000..b9e3560 --- /dev/null +++ b/framework/Inspectron.HawkEye/Protocol/ImageServerTCP.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Inspectron.HawkEye.Protocol.Interfaces; +using Inspectron.HawkEye.UDPB; +using Newtonsoft.Json; + +namespace Inspectron.HawkEye.Protocol +{ + public class ImageServerTCP + { + private readonly IImageSource _imageSource; + private readonly ICameraControl _cameraControl; + private readonly ILightControl _lightControl; + private TcpListener _commandSocket; + private TcpClient _imageSocket; + + private bool _autoTrigger; + private bool _applySettings; + + private CameraSettings _imageSettingsToApply=new CameraSettings(){ImageSettings = new ImageSettings()}; + private TcpClient _lastClient; + + public ImageServerTCP(IImageSource imageSource, ICameraControl cameraControl, ILightControl lightControl) + { + _imageSource = imageSource; + _cameraControl = cameraControl; + _lightControl = lightControl; + + } + + public void Start() + { + _commandSocket = new TcpListener(IPAddress.Any, 27001); + _commandSocket.Start(); + Console.WriteLine("listen tcp"); + var th = new Thread(ReceiveLoop); + th.Start(); + if (File.Exists("settings.json")) + { + var settings = JsonConvert.DeserializeObject(File.ReadAllText("settings.json")); + _imageSettingsToApply = settings; + ApplyParameters(settings); + } + } + + public void ApplyParameters(CameraSettings cameraSettings, bool setLight = false) + { + Console.WriteLine("Setting parameters:"+JsonConvert.SerializeObject(cameraSettings,Formatting.Indented)); + _cameraControl.SetParameters(cameraSettings); + if(setLight&&!cameraSettings.TriggerLights) _lightControl.SetLight(cameraSettings.LightPwm1, cameraSettings.LightPwm2); + else _lightControl.SetLight(0, 0); + + } + + private void SaveSettingsLocally() + { + File.WriteAllText("settings.json", JsonConvert.SerializeObject(_imageSettingsToApply,Formatting.Indented)); + } + byte[] _commandBuffer = new byte[1500]; + private Task _lastTriggerTask=Task.CompletedTask; + + private void ReceiveLoop() + { + while (true) + { + + _lastClient=_commandSocket.AcceptTcpClient(); + + do + { + try + { + _lastClient.GetStream().Read(_commandBuffer, 0, 1500); + } + catch + { + break; + } + + ProcessCommand(_commandBuffer); + } while (_lastClient.Connected); + CancelTrigger(); + _lightControl.SetLight(0, 0); + } + } + + private void TriggerLoop() + { + while (_autoTrigger) + { + + if (_applySettings) + { + ApplyParameters(_imageSettingsToApply,true); + _applySettings = false; + } + + try + { + SendImage(); + + } + catch(Exception e) + { + Console.WriteLine(e.ToString()); + return; + } + } + } + + private void ProcessCommand(byte[] data) + { + Console.WriteLine(((ECommand) data[0]).ToString()); + switch ((ECommand) data[0]) + { + case ECommand.Connect: + { + var port = new byte[4]; + Array.Copy(data, 1, port, 0, 4); + _imageSocket=new TcpClient(); + _imageSocket.Connect(new IPEndPoint((_lastClient.Client.RemoteEndPoint as IPEndPoint).Address, BitConverter.ToInt32(port,0))); + var databytes = new byte[1000]; + var settings = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(_imageSettingsToApply)); + Array.Copy(settings, 0, databytes, 1, settings.Length); + databytes[0] = (byte) ECommand.Settings; + _lastClient.Client.SendData(databytes); + Console.WriteLine("data sent"); + + + _lightControl.SetLight(_imageSettingsToApply.LightPwm1, _imageSettingsToApply.LightPwm2); + _autoTrigger = false; + } + break; + case ECommand.GetCalibration: + { + var databytes = new byte[1500]; + if (File.Exists("calibration.calib")) + { + databytes[0] = (byte)ECommand.OK; + Array.Copy(File.ReadAllBytes("calibration.calib"),0,databytes,1,1500-1); + _lastClient.Client.SendData(databytes); + } + else + { + databytes[0] = (byte)ECommand.NotOK; + _lastClient.Client.SendData(databytes); + } + } + break; + case ECommand.Trigger: + { + CancelTrigger(); + _lastTriggerTask=Task.Run(() => + { + try + { + SendImage(); + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + } + + }); + + } + break; + case ECommand.StartContinuous: + { + _autoTrigger = true; + var th = new Thread(TriggerLoop); + th.Start(); + } + break; + case ECommand.StopContinuous: + { + _autoTrigger = false; + } + break; + case ECommand.Settings: + { + CancelTrigger(); + var databytes = new byte[999]; + Array.Copy(data, 1, databytes, 0, 999); + var settings = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(databytes)); + _imageSettingsToApply = settings; + if (_autoTrigger) + _applySettings = true; + else + ApplyParameters(_imageSettingsToApply,true); + + _lastClient.Client.Send(new []{(byte)EData.OK}); + + } + break; + case ECommand.SaveCalibration: + { + + var databytes = new byte[1500 - 1]; + Array.Copy(data, 1, databytes, 0, 1500 - 1); + File.WriteAllBytes("calibration.calib",databytes); + + _lastClient.Client.Send(new[] { (byte)EData.OK }); + + } + break; + case ECommand.SaveSettings: + { + SaveSettingsLocally(); + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + private void CancelTrigger() + { + _imageSource.CancelTrigger(); + _lastTriggerTask.Wait(); + + } + private void SendImage() + { + if (_imageSettingsToApply.TriggerLights) + { + _lightControl.SetLight(_imageSettingsToApply.LightPwm1, _imageSettingsToApply.LightPwm2); + } + + var image = _imageSource.GetImage(); + if (image.Length == 0) return; + if (_imageSettingsToApply.TriggerLights) + { + _lightControl.SetLight(0, 0); + } + + var b = EncodeImage(image); + + _imageSocket.Client.SendData(b); + } + + private static byte[] EncodeImage(byte[] image) + { + var b = new byte[image.Length + 5]; + b[0] = (byte) EData.Image; + var byteSize = BitConverter.GetBytes(image.Length); + Array.Copy(byteSize, 0, b, 1, 4); + Array.Copy(image, 0, b, 5, image.Length); + return b; + } + private static byte[] EncodeChanneledImage(byte[] image,byte channels) + { + var b = new byte[image.Length + 6]; + b[0] = (byte)EData.Image; + b[1] = channels; + var byteSize = BitConverter.GetBytes(image.Length); + Array.Copy(byteSize, 0, b, 2, 4); + Array.Copy(image, 0, b, 6, image.Length); + return b; + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Protocol/ImageSettings.cs b/framework/Inspectron.HawkEye/Protocol/ImageSettings.cs new file mode 100644 index 0000000..ef94738 --- /dev/null +++ b/framework/Inspectron.HawkEye/Protocol/ImageSettings.cs @@ -0,0 +1,17 @@ +using System.Runtime.InteropServices; + +namespace Inspectron.HawkEye.Protocol +{ + [StructLayout(LayoutKind.Sequential, Pack = 0)] + public struct ImageSettings + { + public int Shutter { get; set; } + public int Gain { get; set; } + public int SensorWidth { get; set; } + public int Lines { get; set; } + public int CaptureBuffer { get; set; } + public int UseExternalTrigger { get; set; } + public int Divider { get; set; } + + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Protocol/Interfaces/ICameraControl.cs b/framework/Inspectron.HawkEye/Protocol/Interfaces/ICameraControl.cs new file mode 100644 index 0000000..59ee6e2 --- /dev/null +++ b/framework/Inspectron.HawkEye/Protocol/Interfaces/ICameraControl.cs @@ -0,0 +1,10 @@ +namespace Inspectron.HawkEye.Protocol.Interfaces +{ + public interface ICameraControl + { + + void SetParameters(CameraSettings imageSettings); + + + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Protocol/Interfaces/IImageSource.cs b/framework/Inspectron.HawkEye/Protocol/Interfaces/IImageSource.cs new file mode 100644 index 0000000..43b0928 --- /dev/null +++ b/framework/Inspectron.HawkEye/Protocol/Interfaces/IImageSource.cs @@ -0,0 +1,8 @@ +namespace Inspectron.HawkEye.Protocol.Interfaces +{ + public interface IImageSource + { + byte[] GetImage(); + void CancelTrigger(); + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Protocol/Interfaces/ILightControl.cs b/framework/Inspectron.HawkEye/Protocol/Interfaces/ILightControl.cs new file mode 100644 index 0000000..07d55f0 --- /dev/null +++ b/framework/Inspectron.HawkEye/Protocol/Interfaces/ILightControl.cs @@ -0,0 +1,7 @@ +namespace Inspectron.HawkEye.Protocol.Interfaces +{ + public interface ILightControl + { + void SetLight(int pwm1, int pwm2); + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/Protocol/SocketExtensions.cs b/framework/Inspectron.HawkEye/Protocol/SocketExtensions.cs new file mode 100644 index 0000000..cc765ad --- /dev/null +++ b/framework/Inspectron.HawkEye/Protocol/SocketExtensions.cs @@ -0,0 +1,23 @@ +using System; +using System.Net; +using System.Net.Sockets; +using Inspectron.HawkEye.RTSP; + +namespace Inspectron.HawkEye.Protocol +{ + public static class SocketExtensions + { + public static void Listen(this Socket self,IPAddress adapterAddress,int port) + { + self.Bind(new IPEndPoint(adapterAddress,port)); + } + + public static void SendData(this Socket self, byte[] data) + { + Console.WriteLine("send data"); + + + self.Send(data); + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/RTSP/AACPayload.cs b/framework/Inspectron.HawkEye/RTSP/AACPayload.cs new file mode 100644 index 0000000..f3b61fc --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/AACPayload.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; + +namespace Inspectron.HawkEye.RTSP +{ + // This class handles the AAC-hbd (High Bitrate) Payload + // It has methods to process the RTP Payload + + // (c) 2018 Roger Hardiman, RJH Technical Consultancy Ltd + + + /* + RFC 3640 + 3.3.6. High Bit-rate AAC + + This mode is signaled by mode=AAC-hbr.This mode supports the + transportation of variable size AAC frames.In one RTP packet, + either one or more complete AAC frames are carried, or a single + fragment of an AAC frame is carried.In this mode, the AAC frames + are allowed to be interleaved and hence receivers MUST support de- + interleaving.The maximum size of an AAC frame in this mode is 8191 + octets. + + In this mode, the RTP payload consists of the AU Header Section, + followed by either one AAC frame, several concatenated AAC frames or + one fragmented AAC frame.The Auxiliary Section MUST be empty. For + each AAC frame contained in the payload, there MUST be an AU-header + in the AU Header Section to provide: + + a) the size of each AAC frame in the payload and + + b) index information for computing the sequence(and hence timing) of + each AAC frame. + + To code the maximum size of an AAC frame requires 13 bits. + Therefore, in this configuration 13 bits are allocated to the AU- + size, and 3 bits to the AU-Index(-delta) field.Thus, each AU-header + has a size of 2 octets.Each AU-Index field MUST be coded with the + value 0. In the AU Header Section, the concatenated AU-headers MUST + be preceded by the 16-bit AU-headers-length field, as specified in + section 3.2.1. + + In addition to the required MIME format parameters, the following + parameters MUST be present: sizeLength, indexLength, and + indexDeltaLength.AAC frames always have a fixed duration per Access + Unit; when interleaving in this mode, this specific duration MUST be + signaled by the MIME format parameter constantDuration.In addition, + the parameter maxDisplacement MUST be present when interleaving. + + For example: + + m= audio 49230 RTP/AVP 96 + a= rtpmap:96 mpeg4-generic/48000/6 + a= fmtp:96 streamtype= 5; profile-level-id= 16; mode= AAC-hbr;config= 11B0; sizeLength= 13; indexLength= 3;indexDeltaLength= 3; constantDuration= 1024 + + The hexadecimal value of the "config" parameter is the AudioSpecificConfig(), as defined in ISO/IEC 14496-3. + AudioSpecificConfig() specifies a 5.1 channel AAC stream with a sampling rate of 48 kHz.For the description of MIME parameters, see + section 4.1. + + */ + + + public class AACPayload + { + public uint ObjectType = 0; + public uint FrequencyIndex = 0; + public uint ChannelConfiguration = 0; + + // Constructor + public AACPayload(String config_string) + { + /*** + 5 bits: object type + if (object type == 31) + 6 bits + 32: object type + 4 bits: frequency index + if (frequency index == 15) + 24 bits: frequency + 4 bits: channel configuration + var bits: AOT Specific Config + ***/ + + // config is a string in hex eg 1490 or 0x1210 + // Read each ASCII character and add to a bit array + BitStream bs = new BitStream(); + bs.AddHexString(config_string); + + // Read 5 bits + ObjectType = bs.Read(5); + + // Read 4 bits + FrequencyIndex = bs.Read(4); + + // Read 4 bits + ChannelConfiguration = bs.Read(4); + } + + public List Process_AAC_RTP_Packet(byte[] rtp_payload, int rtp_marker) { + + // RTP Payload for MPEG4-GENERIC can consist of multple blocks. + // Each block has 3 parts + // Part 1 - Acesss Unit Header Length + Header + // Part 2 - Access Unit Auxiliary Data Length + Data (not used in AAC High Bitrate) + // Part 3 - Access Unit Audio Data + + // The rest of the RTP packet is the AMR data + List audio_data = new List(); + + int ptr = 0; + + while (true) { + if (ptr + 4 > rtp_payload.Length) break; // 2 bytes for AU Header Length, 2 bytes of AU Header payload + + // Get Size of the AU Header + int au_headers_length_bits = (((rtp_payload[ptr] << 8) + (rtp_payload[ptr + 1] << 0))); // 16 bits + int au_headers_length = (int)Math.Ceiling((double)au_headers_length_bits / 8.0); + ptr += 2; + + // Examine the AU Header. Get the size of the AAC data + int aac_frame_size = (((rtp_payload[ptr] << 8) + (rtp_payload[ptr+1] << 0)) >> 3); // 13 bits + int aac_index_delta = rtp_payload[ptr+1] & 0x03; // 3 bits + ptr += au_headers_length; + + // extract the AAC block + if (ptr + aac_frame_size > rtp_payload.Length) break; // not enough data to copy + byte[] aac_data = new byte[aac_frame_size]; + System.Array.Copy(rtp_payload, ptr, aac_data, 0, aac_frame_size); + audio_data.Add(aac_data); + ptr += aac_frame_size; + } + + return audio_data; + } + + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/AMRPayload.cs b/framework/Inspectron.HawkEye/RTSP/AMRPayload.cs new file mode 100644 index 0000000..9637d6c --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/AMRPayload.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace Inspectron.HawkEye.RTSP +{ + // This class handles the AMR Payload + // It has methods to process the RTP Payload + + public class AMRPayload + { + // Constructor + public AMRPayload() + { + } + + public List Process_AMR_RTP_Packet(byte[] rtp_payload, int rtp_marker) { + + // Octet-Aligned Mode (RFC 4867 Section 4.4.1) + + // First byte is the Payload Header + if (rtp_payload.Length < 1) return null; + byte payloadHeader = rtp_payload[0]; + + // The rest of the RTP packet is the AMR data + List audio_data = new List(); + + byte[] amr_data = new byte[rtp_payload.Length - 1]; + System.Array.Copy(rtp_payload,1,amr_data,0,rtp_payload.Length-1); + audio_data.Add(amr_data); + + return audio_data; + } + + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Authentication.cs b/framework/Inspectron.HawkEye/RTSP/Authentication.cs new file mode 100644 index 0000000..6981873 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Authentication.cs @@ -0,0 +1,183 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using Inspectron.HawkEye.RTSP.Messages; + +namespace Inspectron.HawkEye.RTSP +{ + + // WWW-Authentication and Authorization Headers + public class Authentication + { + private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); + + public enum Type {Basic, Digest}; + + private String username = null; + private String password = null; + private String realm = null; + private String nonce = null; + private Type authentication_type = Type.Digest; + private readonly MD5 md5 = System.Security.Cryptography.MD5.Create(); + + + private const char quote = '\"'; + + // Constructor + public Authentication(String username, String password, String realm, Type authentication_type) { + this.username = username; + this.password = password; + this.realm = realm; + this.authentication_type = authentication_type; + + this.nonce = new Random().Next(100000000,999999999).ToString(); // random 9 digit number + } + + public String GetHeader() { + if (authentication_type == Type.Basic) { + return "Basic realm=" + quote + realm + quote; + } + if (authentication_type == Type.Digest) { + return "Digest realm=" + quote + realm + quote + ", nonce=" + quote + nonce + quote; + } + return null; + } + + + public bool IsValid(RtspMessage received_message) { + + string authorization = received_message.Headers["Authorization"]; + + + // Check Username and Password + if (authentication_type == Type.Basic && authorization.StartsWith("Basic ")) { + string base64_str = authorization.Substring(6); // remove 'Basic ' + byte[] data = Convert.FromBase64String(base64_str); + string decoded = Encoding.UTF8.GetString(data); + int split_position = decoded.IndexOf(':'); + string decoded_username = decoded.Substring(0, split_position); + string decoded_password = decoded.Substring(split_position + 1); + + if ((decoded_username == username) && (decoded_password == password)) { + _logger.Debug("Basic Authorization passed"); + return true; + } else { + _logger.Debug("Basic Authorization failed"); + return false; + } + } + + // Check Username, URI, Nonce and the MD5 hashed Response + if (authentication_type == Type.Digest && authorization.StartsWith("Digest ")) { + string value_str = authorization.Substring(7); // remove 'Digest ' + string[] values = value_str.Split(','); + string auth_header_username = null; + string auth_header_realm = null; + string auth_header_nonce = null; + string auth_header_uri = null; + string auth_header_response = null; + string message_method = null; + string message_uri = null; + try { + message_method = received_message.Command.Split(' ')[0]; + message_uri = received_message.Command.Split(' ')[1]; + } catch {} + + foreach (string value in values) { + string[] tuple = value.Trim().Split(new char[] {'='},2); // split on first '=' + if (tuple.Length == 2 && tuple[0].Equals("username")) { + auth_header_username = tuple[1].Trim(new char[] {' ','\"'}); // trim space and quotes + } + else if (tuple.Length == 2 && tuple[0].Equals("realm")) { + auth_header_realm = tuple[1].Trim(new char[] {' ','\"'}); // trim space and quotes + } + else if (tuple.Length == 2 && tuple[0].Equals("nonce")) { + auth_header_nonce = tuple[1].Trim(new char[] {' ','\"'}); // trim space and quotes + } + else if (tuple.Length == 2 && tuple[0].Equals("uri")) { + auth_header_uri = tuple[1].Trim(new char[] {' ','\"'}); // trim space and quotes + } + else if (tuple.Length == 2 && tuple[0].Equals("response")) { + auth_header_response = tuple[1].Trim(new char[] {' ','\"'}); // trim space and quotes + } + } + + // Create the MD5 Hash using all parameters passed in the Auth Header with the + // addition of the 'Password' + String hashA1 = CalculateMD5Hash(md5, auth_header_username+":"+auth_header_realm+":"+this.password); + String hashA2 = CalculateMD5Hash(md5, message_method + ":" + auth_header_uri); + String expected_response = CalculateMD5Hash(md5, hashA1 + ":" + auth_header_nonce + ":" + hashA2); + + // Check if everything matches + // ToDo - extract paths from the URIs (ignoring SETUP's trackID) + if ((auth_header_username == this.username) + && (auth_header_realm == this.realm) + && (auth_header_nonce == this.nonce) + && (auth_header_response == expected_response) + ){ + _logger.Debug("Digest Authorization passed"); + return true; + } else { + _logger.Debug("Digest Authorization failed"); + return false; + } + } + return false; + } + + + + // Generate Basic or Digest Authorization + public string GenerateAuthorization(string username, string password, + string auth_type, string realm, string nonce, string url, string command) { + + if (username == null || username.Length == 0) return null; + if (password == null || password.Length == 0) return null; + if (realm == null || realm.Length == 0) return null; + if (auth_type.Equals("Digest") && (nonce == null || nonce.Length == 0)) return null; + + if (auth_type.Equals("Basic")) { + byte[] credentials = System.Text.Encoding.UTF8.GetBytes(username+":"+password); + String credentials_base64 = Convert.ToBase64String(credentials); + String basic_authorization = "Basic " + credentials_base64; + return basic_authorization; + } + else if (auth_type.Equals("Digest")) { + + MD5 md5 = System.Security.Cryptography.MD5.Create(); + String hashA1 = CalculateMD5Hash(md5, username+":"+realm+":"+password); + String hashA2 = CalculateMD5Hash(md5, command + ":" + url); + String response = CalculateMD5Hash(md5, hashA1 + ":" + nonce + ":" + hashA2); + + const String quote = "\""; + String digest_authorization = "Digest username=" + quote + username + quote +", " + + "realm=" + quote + realm + quote + ", " + + "nonce=" + quote + nonce + quote + ", " + + "uri=" + quote + url + quote + ", " + + "response=" + quote + response + quote; + + return digest_authorization; + } + else { + return null; + } + + } + + + + // MD5 (lower case) + private string CalculateMD5Hash(MD5 md5_session, string input) + { + byte[] inputBytes = System.Text.Encoding.UTF8.GetBytes(input); + byte[] hash = md5_session.ComputeHash(inputBytes); + + StringBuilder output = new StringBuilder(); + for (int i = 0; i < hash.Length; i++) { + output.Append(hash[i].ToString("x2")); + } + + return output.ToString(); + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/RTSP/BitStream.cs b/framework/Inspectron.HawkEye/RTSP/BitStream.cs new file mode 100644 index 0000000..bf71763 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/BitStream.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; + +// (c) 2018 Roger Hardiman, RJH Technical Consultancy Ltd +// Simple class to Read and Write bits in a bit stream. +// Data is written to the end of the bit stream and the bit stream can be returned as a Byte Array +// Data can be read from the head of the bit stream +// Example +// bitstream.AddValue(0xA,4); // Write 4 bit value +// bitstream.AddValue(0xB,4); +// bitstream.AddValue(0xC,4); +// bitstream.AddValue(0xD,4); +// bitstream.ToArray() -> {0xAB, 0xCD} // Return Byte Array +// bitstream.Read(8) -> 0xAB // Read 8 bit value + +namespace Inspectron.HawkEye.RTSP +{ + + // Very simple bitstream + public class BitStream { + + private List data = new List(); // List only stores 0 or 1 (one 'bit' per List item) + + // Constructor + public BitStream() { + } + + public void AddValue(int value, int num_bits) { + // Add each bit to the List + for (int i = num_bits-1; i >= 0; i--) { + data.Add((byte)((value>>i) & 0x01)); + } + } + + public void AddHexString(String hex_string) { + char[] hex_chars = hex_string.ToUpper().ToCharArray(); + foreach (char c in hex_chars) { + if ((c.Equals('0'))) this.AddValue(0,4); + else if ((c.Equals('1'))) this.AddValue(1, 4); + else if ((c.Equals('2'))) this.AddValue(2, 4); + else if ((c.Equals('3'))) this.AddValue(3, 4); + else if ((c.Equals('4'))) this.AddValue(4, 4); + else if ((c.Equals('5'))) this.AddValue(5, 4); + else if ((c.Equals('6'))) this.AddValue(6, 4); + else if ((c.Equals('7'))) this.AddValue(7, 4); + else if ((c.Equals('8'))) this.AddValue(8, 4); + else if ((c.Equals('9'))) this.AddValue(9, 4); + else if ((c.Equals('A'))) this.AddValue(10, 4); + else if ((c.Equals('B'))) this.AddValue(11, 4); + else if ((c.Equals('C'))) this.AddValue(12, 4); + else if ((c.Equals('D'))) this.AddValue(13, 4); + else if ((c.Equals('E'))) this.AddValue(14, 4); + else if ((c.Equals('F'))) this.AddValue(15, 4); + } + } + + public uint Read(int num_bits) { + // Read and remove items from the front of the list of bits + if (data.Count < num_bits) return 0; + uint result = 0; + for (int i = 0; i < num_bits; i++) { + result = result << 1; + result = result + data[0]; + data.RemoveAt(0); + } + return result; + } + + public byte[] ToArray() { + int num_bytes = (int)Math.Ceiling((double)data.Count/8.0); + byte[] array = new byte[num_bytes]; + int ptr = 0; + int shift = 7; + for (int i = 0; i < data.Count; i++) { + array[ptr] += (byte)(data[i] << shift); + if (shift == 0) { + shift = 7; + ptr++; + } + else { + shift--; + } + } + + return array; + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/RTSP/Client/RTSPClient.cs b/framework/Inspectron.HawkEye/RTSP/Client/RTSPClient.cs new file mode 100644 index 0000000..958202e --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Client/RTSPClient.cs @@ -0,0 +1,1128 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Inspectron.HawkEye.RTSP.Messages; + +namespace Inspectron.HawkEye.RTSP.Client +{ + public class RTSPClient + { + private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); + + // Events that applications can receive + public event Received_SPS_PPS_Delegate Received_SPS_PPS; + public event Received_VPS_SPS_PPS_Delegate Received_VPS_SPS_PPS; + public event Received_NALs_Delegate Received_NALs; + public event Received_G711_Delegate Received_G711; + public event Received_AMR_Delegate Received_AMR; + public event Received_AAC_Delegate Received_AAC; + + // Delegated functions (essentially the function prototype) + public delegate void Received_SPS_PPS_Delegate (byte[] sps, byte[] pps); // H264 + public delegate void Received_VPS_SPS_PPS_Delegate(byte[] vps, byte[] sps, byte[] pps); // H265 + public delegate void Received_NALs_Delegate (List nal_units); // H264 or H265 + public delegate void Received_G711_Delegate (String format, List g711); + public delegate void Received_AMR_Delegate (String format, List amr); + public delegate void Received_AAC_Delegate(String format, List aac, uint ObjectType, uint FrequencyIndex, uint ChannelConfiguration); + + public enum RTP_TRANSPORT { UDP, TCP, MULTICAST, UNKNOWN }; + public enum MEDIA_REQUEST { VIDEO_ONLY, AUDIO_ONLY, VIDEO_AND_AUDIO }; + private enum RTSP_STATUS { WaitingToConnect, Connecting, ConnectFailed, Connected }; + + Inspectron.HawkEye.RTSP.RtspTcpTransport rtsp_socket = null; // RTSP connection + volatile RTSP_STATUS rtsp_socket_status = RTSP_STATUS.WaitingToConnect; + Inspectron.HawkEye.RTSP.RtspListener rtsp_client = null; // this wraps around a the RTSP tcp_socket stream + RTP_TRANSPORT rtp_transport = RTP_TRANSPORT.UNKNOWN; // Mode, either RTP over UDP or RTP over TCP using the RTSP socket + Inspectron.HawkEye.RTSP.UDPSocket video_udp_pair = null; // Pair of UDP ports used in RTP over UDP mode or in MULTICAST mode + Inspectron.HawkEye.RTSP.UDPSocket audio_udp_pair = null; // Pair of UDP ports used in RTP over UDP mode or in MULTICAST mode + String url = ""; // RTSP URL (username & password will be stripped out + String username = ""; // Username + String password = ""; // Password + String hostname = ""; // RTSP Server hostname or IP address + int port = 0; // RTSP Server TCP Port number + String session = ""; // RTSP Session + String auth_type = null; // cached from most recent WWW-Authenticate reply + String realm = null; // cached from most recent WWW-Authenticate reply + String nonce = null; // cached from most recent WWW-Authenticate reply + uint ssrc = 12345; + bool client_wants_video = false; // Client wants to receive Video + bool client_wants_audio = false; // Client wants to receive Audio + Uri video_uri = null; // URI used for the Video Track + int video_payload = -1; // Payload Type for the Video. (often 96 which is the first dynamic payload value. Bosch use 35) + int video_data_channel = -1; // RTP Channel Number used for the video RTP stream or the UDP port number + int video_rtcp_channel = -1; // RTP Channel Number used for the video RTCP status report messages OR the UDP port number + bool h264_sps_pps_fired = false; // True if the SDP included a sprop-Parameter-Set for H264 video + bool h265_vps_sps_pps_fired = false; // True if the SDP included a sprop-vps, sprop-sps and sprop_pps for H265 video + string video_codec = ""; // Codec used with Payload Types 96..127 (eg "H264") + + Uri audio_uri = null; // URI used for the Audio Track + int audio_payload = -1; // Payload Type for the Video. (often 96 which is the first dynamic payload value) + int audio_data_channel = -1; // RTP Channel Number used for the audio RTP stream or the UDP port number + int audio_rtcp_channel = -1; // RTP Channel Number used for the audio RTCP status report messages OR the UDP port number + string audio_codec = ""; // Codec used with Payload Types (eg "PCMA" or "AMR") + + bool server_supports_get_parameter = false; // Used with RTSP keepalive + bool server_supports_set_parameter = false; // Used with RTSP keepalive + System.Timers.Timer keepalive_timer = null; // Used with RTSP keepalive + + Inspectron.HawkEye.RTSP.H264Payload h264Payload = null; + Inspectron.HawkEye.RTSP.H265Payload h265Payload = null; + Inspectron.HawkEye.RTSP.G711Payload g711Payload = new Inspectron.HawkEye.RTSP.G711Payload(); + Inspectron.HawkEye.RTSP.AMRPayload amrPayload = new Inspectron.HawkEye.RTSP.AMRPayload(); + Inspectron.HawkEye.RTSP.AACPayload aacPayload = null; + + List setup_messages = new List(); // setup messages still to send + + // Constructor + public RTSPClient() { + bool writeLogsToConsole = true; + if (writeLogsToConsole) + { + var config = new NLog.Config.LoggingConfiguration(); + + // Targets where to log to: Console + var logconsole = new NLog.Targets.ConsoleTarget("logconsole"); + + // Rules for mapping loggers to targets + config.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Fatal, logconsole); + + // Apply config + NLog.LogManager.Configuration = config; + } + } + + + public void Connect(String url, RTP_TRANSPORT rtp_transport, MEDIA_REQUEST media_request = MEDIA_REQUEST.VIDEO_AND_AUDIO) + { + + Inspectron.HawkEye.RTSP.RtspUtils.RegisterUri(); + + _logger.Debug("Connecting to " + url); + this.url = url; + + // Use URI to extract username and password + // and to make a new URL without the username and password + try { + Uri uri = new Uri(this.url); + hostname = uri.Host; + port = uri.Port; + + if (uri.UserInfo.Length > 0) { + username = uri.UserInfo.Split(new char[] {':'})[0]; + password = uri.UserInfo.Split(new char[] {':'})[1]; + this.url = uri.GetComponents((UriComponents.AbsoluteUri &~ UriComponents.UserInfo), + UriFormat.UriEscaped); + } + } catch { + username = null; + password = null; + } + + // We can ask the RTSP server for Video, Audio or both. If we don't want audio we don't need to SETUP the audio channal or receive it + client_wants_video = false; + client_wants_audio = false; + if (media_request == MEDIA_REQUEST.VIDEO_ONLY || media_request == MEDIA_REQUEST.VIDEO_AND_AUDIO) client_wants_video = true; + if (media_request == MEDIA_REQUEST.AUDIO_ONLY || media_request == MEDIA_REQUEST.VIDEO_AND_AUDIO) client_wants_audio = true; + + // Connect to a RTSP Server. The RTSP session is a TCP connection + rtsp_socket_status = RTSP_STATUS.Connecting; + try + { + rtsp_socket = new Inspectron.HawkEye.RTSP.RtspTcpTransport(hostname, port); + } + catch + { + rtsp_socket_status = RTSP_STATUS.ConnectFailed; + _logger.Warn("Error - did not connect"); + return; + } + + if (rtsp_socket.Connected == false) + { + rtsp_socket_status = RTSP_STATUS.ConnectFailed; + _logger.Warn("Error - did not connect"); + return; + } + + rtsp_socket_status = RTSP_STATUS.Connected; + + // Connect a RTSP Listener to the RTSP Socket (or other Stream) to send RTSP messages and listen for RTSP replies + rtsp_client = new Inspectron.HawkEye.RTSP.RtspListener(rtsp_socket); + + rtsp_client.AutoReconnect = false; + + rtsp_client.MessageReceived += Rtsp_MessageReceived; + rtsp_client.DataReceived += Rtp_DataReceived; + + rtsp_client.Start(); // start listening for messages from the server (messages fire the MessageReceived event) + + + // Check the RTP Transport + // If the RTP transport is TCP then we interleave the RTP packets in the RTSP stream + // If the RTP transport is UDP, we initialise two UDP sockets (one for video, one for RTCP status messages) + // If the RTP transport is MULTICAST, we have to wait for the SETUP message to get the Multicast Address from the RTSP server + this.rtp_transport = rtp_transport; + if (rtp_transport == RTP_TRANSPORT.UDP) + { + video_udp_pair = new Inspectron.HawkEye.RTSP.UDPSocket(50000, 51000); // give a range of 500 pairs (1000 addresses) to try incase some address are in use + video_udp_pair.DataReceived += Rtp_DataReceived; + video_udp_pair.Start(); // start listening for data on the UDP ports + audio_udp_pair = new Inspectron.HawkEye.RTSP.UDPSocket(50000, 51000); // give a range of 500 pairs (1000 addresses) to try incase some address are in use + audio_udp_pair.DataReceived += Rtp_DataReceived; + audio_udp_pair.Start(); // start listening for data on the UDP ports + } + if (rtp_transport == RTP_TRANSPORT.TCP) + { + // Nothing to do. Data will arrive in the RTSP Listener + } + if (rtp_transport == RTP_TRANSPORT.MULTICAST) + { + // Nothing to do. Will open Multicast UDP sockets after the SETUP command + } + + + // Send OPTIONS + // In the Received Message handler we will send DESCRIBE, SETUP and PLAY + Inspectron.HawkEye.RTSP.Messages.RtspRequest options_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestOptions(); + options_message.RtspUri = new Uri(this.url); + rtsp_client.SendMessage(options_message); + } + + // return true if this connection failed, or if it connected but is no longer connected. + public bool StreamingFinished() { + if (rtsp_socket_status == RTSP_STATUS.ConnectFailed) return true; + if (rtsp_socket_status == RTSP_STATUS.Connected && rtsp_socket.Connected == false) return true; + else return false; + } + + + public void Pause() + { + if (rtsp_client != null) { + // Send PAUSE + Inspectron.HawkEye.RTSP.Messages.RtspRequest pause_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestPause(); + pause_message.RtspUri = new Uri(url); + pause_message.Session = session; + if (auth_type != null) { + AddAuthorization(pause_message,username,password,auth_type,realm,nonce,url); + } + rtsp_client.SendMessage(pause_message); + } + } + + public void Play() + { + if (rtsp_client != null) { + // Send PLAY + Inspectron.HawkEye.RTSP.Messages.RtspRequest play_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestPlay(); + play_message.RtspUri = new Uri(url); + play_message.Session = session; + if (auth_type != null) { + AddAuthorization(play_message,username,password,auth_type,realm,nonce,url); + } + rtsp_client.SendMessage(play_message); + } + } + + + public void Stop() + { + if (rtsp_client != null) { + // Send TEARDOWN + Inspectron.HawkEye.RTSP.Messages.RtspRequest teardown_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestTeardown(); + teardown_message.RtspUri = new Uri(url); + teardown_message.Session = session; + if (auth_type != null) { + AddAuthorization(teardown_message,username,password,auth_type,realm,nonce,url); + } + rtsp_client.SendMessage(teardown_message); + } + + // Stop the keepalive timer + if (keepalive_timer != null) keepalive_timer.Stop(); + + // clear up any UDP sockets + if (video_udp_pair != null) video_udp_pair.Stop(); + if (audio_udp_pair != null) audio_udp_pair.Stop(); + + // Drop the RTSP session + if (rtsp_client != null) { + rtsp_client.Stop(); + } + + } + + + int rtp_count = 0; // used for statistics + // RTP packet (or RTCP packet) has been received. + public void Rtp_DataReceived(object sender, Inspectron.HawkEye.RTSP.RtspChunkEventArgs e) + { + + Inspectron.HawkEye.RTSP.Messages.RtspData data_received = e.Message as Inspectron.HawkEye.RTSP.Messages.RtspData; + + // Check which channel the Data was received on. + // eg the Video Channel, the Video Control Channel (RTCP) + // the Audio Channel or the Audio Control Channel (RTCP) + + if (data_received.Channel == video_rtcp_channel || data_received.Channel == audio_rtcp_channel) + { + _logger.Debug("Received a RTCP message on channel " + data_received.Channel); + + // RTCP Packet + // - Version, Padding and Receiver Report Count + // - Packet Type + // - Length + // - SSRC + // - payload + + // There can be multiple RTCP packets transmitted together. Loop ever each one + + long packetIndex = 0; + while (packetIndex < e.Message.Data.Length) { + + int rtcp_version = (e.Message.Data[packetIndex+0] >> 6); + int rtcp_padding = (e.Message.Data[packetIndex+0] >> 5) & 0x01; + int rtcp_reception_report_count = (e.Message.Data[packetIndex+0] & 0x1F); + byte rtcp_packet_type = e.Message.Data[packetIndex+1]; // Values from 200 to 207 + uint rtcp_length = (uint)(e.Message.Data[packetIndex+2] << 8) + (uint)(e.Message.Data[packetIndex+3]); // number of 32 bit words + uint rtcp_ssrc = (uint)(e.Message.Data[packetIndex+4] << 24) + (uint)(e.Message.Data[packetIndex+5] << 16) + + (uint)(e.Message.Data[packetIndex+6] << 8) + (uint)(e.Message.Data[packetIndex+7]); + + // 200 = SR = Sender Report + // 201 = RR = Receiver Report + // 202 = SDES = Source Description + // 203 = Bye = Goodbye + // 204 = APP = Application Specific Method + // 207 = XR = Extended Reports + + _logger.Debug("RTCP Data. PacketType=" + rtcp_packet_type + + " SSRC=" + rtcp_ssrc); + + if (rtcp_packet_type == 200) { + // We have received a Sender Report + // Use it to convert the RTP timestamp into the UTC time + + UInt32 ntp_msw_seconds = (uint)(e.Message.Data[packetIndex + 8] << 24) + (uint)(e.Message.Data[packetIndex + 9] << 16) + + (uint)(e.Message.Data[packetIndex + 10] << 8) + (uint)(e.Message.Data[packetIndex + 11]); + + UInt32 ntp_lsw_fractions = (uint)(e.Message.Data[packetIndex + 12] << 24) + (uint)(e.Message.Data[packetIndex + 13] << 16) + + (uint)(e.Message.Data[packetIndex + 14] << 8) + (uint)(e.Message.Data[packetIndex + 15]); + + UInt32 rtp_timestamp = (uint)(e.Message.Data[packetIndex + 16] << 24) + (uint)(e.Message.Data[packetIndex + 17] << 16) + + (uint)(e.Message.Data[packetIndex + 18] << 8) + (uint)(e.Message.Data[packetIndex + 19]); + + double ntp = ntp_msw_seconds + (ntp_lsw_fractions / UInt32.MaxValue); + + // NTP Most Signigicant Word is relative to 0h, 1 Jan 1900 + // This will wrap around in 2036 + DateTime time = new DateTime(1900,1,1,0,0,0,DateTimeKind.Utc); + + time = time.AddSeconds((double)ntp_msw_seconds); // adds 'double' (whole&fraction) + + _logger.Debug("RTCP time (UTC) for RTP timestamp " + rtp_timestamp + " is " + time); + + // Send a Receiver Report + try + { + byte[] rtcp_receiver_report = new byte[8]; + int version = 2; + int paddingBit = 0; + int reportCount = 0; // an empty report + int packetType = 201; // Receiver Report + int length = (rtcp_receiver_report.Length/4) - 1; // num 32 bit words minus 1 + rtcp_receiver_report[0] = (byte)((version << 6) + (paddingBit << 5) + reportCount); + rtcp_receiver_report[1] = (byte)(packetType); + rtcp_receiver_report[2] = (byte)((length >> 8) & 0xFF); + rtcp_receiver_report[3] = (byte)((length >> 0) & 0XFF); + rtcp_receiver_report[4] = (byte)((ssrc >> 24) & 0xFF); + rtcp_receiver_report[5] = (byte)((ssrc >> 16) & 0xFF); + rtcp_receiver_report[6] = (byte)((ssrc >> 8) & 0xFF); + rtcp_receiver_report[7] = (byte)((ssrc >> 0) & 0xFF); + + if (rtp_transport == RTP_TRANSPORT.TCP) { + // Send it over via the RTSP connection + rtsp_client.SendData(video_rtcp_channel,rtcp_receiver_report); + } + if (rtp_transport == RTP_TRANSPORT.UDP || rtp_transport == RTP_TRANSPORT.MULTICAST) { + // Send it via a UDP Packet + _logger.Debug("TODO - Need to implement RTCP over UDP"); + } + + } + catch + { + _logger.Debug("Error writing RTCP packet"); + } + } + + packetIndex = packetIndex + ((rtcp_length + 1) * 4); + } + return; + } + + if (data_received.Channel == video_data_channel || data_received.Channel == audio_data_channel) + { + // Received some Video or Audio Data on the correct channel. + + // RTP Packet Header + // 0 - Version, P, X, CC, M, PT and Sequence Number + //32 - Timestamp + //64 - SSRC + //96 - CSRCs (optional) + //nn - Extension ID and Length + //nn - Extension header + + int rtp_version = (e.Message.Data[0] >> 6); + int rtp_padding = (e.Message.Data[0] >> 5) & 0x01; + int rtp_extension = (e.Message.Data[0] >> 4) & 0x01; + int rtp_csrc_count = (e.Message.Data[0] >> 0) & 0x0F; + int rtp_marker = (e.Message.Data[1] >> 7) & 0x01; + int rtp_payload_type = (e.Message.Data[1] >> 0) & 0x7F; + uint rtp_sequence_number = ((uint)e.Message.Data[2] << 8) + (uint)(e.Message.Data[3]); + uint rtp_timestamp = ((uint)e.Message.Data[4] << 24) + (uint)(e.Message.Data[5] << 16) + (uint)(e.Message.Data[6] << 8) + (uint)(e.Message.Data[7]); + uint rtp_ssrc = ((uint)e.Message.Data[8] << 24) + (uint)(e.Message.Data[9] << 16) + (uint)(e.Message.Data[10] << 8) + (uint)(e.Message.Data[11]); + + int rtp_payload_start = 4 // V,P,M,SEQ + + 4 // time stamp + + 4 // ssrc + + (4 * rtp_csrc_count); // zero or more csrcs + + uint rtp_extension_id = 0; + uint rtp_extension_size = 0; + if (rtp_extension == 1) + { + rtp_extension_id = ((uint)e.Message.Data[rtp_payload_start + 0] << 8) + (uint)(e.Message.Data[rtp_payload_start + 1] << 0); + rtp_extension_size = ((uint)e.Message.Data[rtp_payload_start + 2] << 8) + (uint)(e.Message.Data[rtp_payload_start + 3] << 0) * 4; // units of extension_size is 4-bytes + rtp_payload_start += 4 + (int)rtp_extension_size; // extension header and extension payload + } + + _logger.Debug("RTP Data" + + " V=" + rtp_version + + " P=" + rtp_padding + + " X=" + rtp_extension + + " CC=" + rtp_csrc_count + + " M=" + rtp_marker + + " PT=" + rtp_payload_type + + " Seq=" + rtp_sequence_number + + " Time (MS)=" + rtp_timestamp / 90 // convert from 90kHZ clock to ms + + " SSRC=" + rtp_ssrc + + " Size=" + e.Message.Data.Length); + + + // Check the payload type in the RTP packet matches the Payload Type value from the SDP + if (data_received.Channel == video_data_channel && rtp_payload_type != video_payload) + { + _logger.Debug("Ignoring this Video RTP payload"); + return; // ignore this data + } + + // Check the payload type in the RTP packet matches the Payload Type value from the SDP + else if (data_received.Channel == audio_data_channel && rtp_payload_type != audio_payload) + { + _logger.Debug("Ignoring this Audio RTP payload"); + return; // ignore this data + } + else if (data_received.Channel == video_data_channel + && rtp_payload_type == video_payload + && video_codec.Equals("H264")) { + // H264 RTP Packet + + // If rtp_marker is '1' then this is the final transmission for this packet. + // If rtp_marker is '0' we need to accumulate data with the same timestamp + + // ToDo - Check Timestamp + // Add the RTP packet to the tempoary_rtp list until we have a complete 'Frame' + + byte[] rtp_payload = new byte[e.Message.Data.Length - rtp_payload_start]; // payload with RTP header removed + System.Array.Copy(e.Message.Data, rtp_payload_start, rtp_payload, 0, rtp_payload.Length); // copy payload + + List nal_units = h264Payload.Process_H264_RTP_Packet(rtp_payload, rtp_marker); // this will cache the Packets until there is a Frame + + if (nal_units == null) { + // we have not passed in enough RTP packets to make a Frame of video + } else { + // If we did not have a SPS and PPS in the SDP then search for the SPS and PPS + // in the NALs and fire the Received_SPS_PPS event. + // We assume the SPS and PPS are in the same Frame. + if (h264_sps_pps_fired == false) { + + // Check this frame for SPS and PPS + byte[] sps = null; + byte[] pps = null; + foreach (byte[] nal_unit in nal_units) { + if (nal_unit.Length > 0) + { + int nal_ref_idc = (nal_unit[0] >> 5) & 0x03; + int nal_unit_type = nal_unit[0] & 0x1F; + + if (nal_unit_type == 7) sps = nal_unit; // SPS + if (nal_unit_type == 8) pps = nal_unit; // PPS + } + } + if (sps != null && pps != null) { + // Fire the Event + if (Received_SPS_PPS != null) + { + Received_SPS_PPS(sps, pps); + } + h264_sps_pps_fired = true; + } + } + + + + // we have a frame of NAL Units. Write them to the file + if (Received_NALs != null) { + Received_NALs(nal_units); + } + } + } + else if (data_received.Channel == video_data_channel + && rtp_payload_type == video_payload + && video_codec.Equals("H265")) + { + // H265 RTP Packet + + // If rtp_marker is '1' then this is the final transmission for this packet. + // If rtp_marker is '0' we need to accumulate data with the same timestamp + + // Add the RTP packet to the tempoary_rtp list until we have a complete 'Frame' + + byte[] rtp_payload = new byte[e.Message.Data.Length - rtp_payload_start]; // payload with RTP header removed + System.Array.Copy(e.Message.Data, rtp_payload_start, rtp_payload, 0, rtp_payload.Length); // copy payload + + List nal_units = h265Payload.Process_H265_RTP_Packet(rtp_payload, rtp_marker); // this will cache the Packets until there is a Frame + + if (nal_units == null) + { + // we have not passed in enough RTP packets to make a Frame of video + } + else + { + // If we did not have a VPS, SPS and PPS in the SDP then search for the VPS SPS and PPS + // in the NALs and fire the Received_VPS_SPS_PPS event. + // We assume the VPS, SPS and PPS are in the same Frame. + if (h265_vps_sps_pps_fired == false) + { + + // Check this frame for VPS, SPS and PPS + byte[] vps = null; + byte[] sps = null; + byte[] pps = null; + foreach (byte[] nal_unit in nal_units) + { + if (nal_unit.Length > 0) + { + int nal_unit_type = (nal_unit[0] >> 1) & 0x3F; + + if (nal_unit_type == 32) vps = nal_unit; // VPS + if (nal_unit_type == 33) sps = nal_unit; // SPS + if (nal_unit_type == 34) pps = nal_unit; // PPS + } + } + if (vps != null && sps != null && pps != null) + { + // Fire the Event + if (Received_VPS_SPS_PPS != null) + { + Received_VPS_SPS_PPS(vps, sps, pps); + } + h265_vps_sps_pps_fired = true; + } + } + + // we have a frame of NAL Units. Write them to the file + if (Received_NALs != null) + { + Received_NALs(nal_units); + } + } + } + else if (data_received.Channel == audio_data_channel && (rtp_payload_type == 0 || rtp_payload_type == 8 || audio_codec.Equals("PCMA") || audio_codec.Equals("PCMU"))) { + // G711 PCMA or G711 PCMU + byte[] rtp_payload = new byte[e.Message.Data.Length - rtp_payload_start]; // payload with RTP header removed + System.Array.Copy(e.Message.Data, rtp_payload_start, rtp_payload, 0, rtp_payload.Length); // copy payload + + List audio_frames = g711Payload.Process_G711_RTP_Packet(rtp_payload, rtp_marker); + + if (audio_frames == null) { + // some error + } else { + // Write the audio frames to the file + if (Received_G711 != null) { + Received_G711(audio_codec, audio_frames); + } + } + } + else if (data_received.Channel == audio_data_channel + && rtp_payload_type == audio_payload + && audio_codec.Equals("AMR")) { + // AMR + byte[] rtp_payload = new byte[e.Message.Data.Length - rtp_payload_start]; // payload with RTP header removed + System.Array.Copy(e.Message.Data, rtp_payload_start, rtp_payload, 0, rtp_payload.Length); // copy payload + + List audio_frames = amrPayload.Process_AMR_RTP_Packet(rtp_payload, rtp_marker); + + if (audio_frames == null) { + // some error + } else { + // Write the audio frames to the file + if (Received_AMR != null) { + Received_AMR(audio_codec, audio_frames); + } + } + } + else if (data_received.Channel == audio_data_channel + && rtp_payload_type == audio_payload + && audio_codec.Equals("MPEG4-GENERIC") + && aacPayload != null) + { + // AAC + byte[] rtp_payload = new byte[e.Message.Data.Length - rtp_payload_start]; // payload with RTP header removed + System.Array.Copy(e.Message.Data, rtp_payload_start, rtp_payload, 0, rtp_payload.Length); // copy payload + + List audio_frames = aacPayload.Process_AAC_RTP_Packet(rtp_payload, rtp_marker); + + if (audio_frames == null) { + // some error + } else { + // Write the audio frames to the file + if (Received_AAC != null) { + Received_AAC(audio_codec, audio_frames, aacPayload.ObjectType, aacPayload.FrequencyIndex, aacPayload.ChannelConfiguration); + } + } + } + else if (data_received.Channel == video_data_channel && rtp_payload_type == 26) { + _logger.Warn("No parser has been written for JPEG RTP packets. Please help write one"); + return; // ignore this data + } + else { + _logger.Warn("No parser for RTP payload " + rtp_payload_type); + } + } + } + + + // RTSP Messages are OPTIONS, DESCRIBE, SETUP, PLAY etc + private void Rtsp_MessageReceived(object sender, Inspectron.HawkEye.RTSP.RtspChunkEventArgs e) + { + Inspectron.HawkEye.RTSP.Messages.RtspResponse message = e.Message as Inspectron.HawkEye.RTSP.Messages.RtspResponse; + + _logger.Debug("Received RTSP Message " + message.OriginalRequest.ToString()); + + // If message has a 401 - Unauthorised Error, then we re-send the message with Authorization + // using the most recently received 'realm' and 'nonce' + if (message.IsOk == false) { + _logger.Debug("Got Error in RTSP Reply " + message.ReturnCode + " " + message.ReturnMessage); + + if (message.ReturnCode == 401 && (message.OriginalRequest.Headers.ContainsKey(RtspHeaderNames.Authorization)==true)) { + // the authorization failed. + Stop(); + return; + } + + // Check if the Reply has an Authenticate header. + if (message.ReturnCode == 401 && message.Headers.ContainsKey(RtspHeaderNames.WWWAuthenticate)) { + + // Process the WWW-Authenticate header + // EG: Basic realm="AProxy" + // EG: Digest realm="AXIS_WS_ACCC8E3A0A8F", nonce="000057c3Y810622bff50b36005eb5efeae118626a161bf", stale=FALSE + // EG: Digest realm="IP Camera(21388)", nonce="534407f373af1bdff561b7b4da295354", stale="FALSE" + + String www_authenticate = message.Headers[RtspHeaderNames.WWWAuthenticate]; + String auth_params = ""; + + if (www_authenticate.StartsWith("basic",StringComparison.InvariantCultureIgnoreCase)) { + auth_type = "Basic"; + auth_params = www_authenticate.Substring(5); + } + if (www_authenticate.StartsWith("digest",StringComparison.InvariantCultureIgnoreCase)) { + auth_type = "Digest"; + auth_params = www_authenticate.Substring(6); + } + + string[] items = auth_params.Split(new char[] { ',' }); // NOTE, does not handle Commas in Quotes + + foreach (string item in items) { + // Split on the = symbol and update the realm and nonce + string[] parts = item.Trim().Split(new char[] {'='},2); // max 2 parts in the results array + if (parts.Count() >= 2 && parts[0].Trim().Equals("realm")) { + realm = parts[1].Trim(new char[] {' ','\"'}); // trim space and quotes + } + else if (parts.Count() >= 2 && parts[0].Trim().Equals("nonce")) { + nonce = parts[1].Trim(new char[] {' ','\"'}); // trim space and quotes + } + } + + _logger.Debug("WWW Authorize parsed for " + auth_type + " " + realm + " " + nonce); + } + + RtspMessage resend_message = message.OriginalRequest.Clone() as RtspMessage; + + if (auth_type != null) { + AddAuthorization(resend_message,username,password,auth_type,realm,nonce,url); + } + + rtsp_client.SendMessage(resend_message); + + return; + + } + + + // If we get a reply to OPTIONS then start the Keepalive Timer and send DESCRIBE + if (message.OriginalRequest != null && message.OriginalRequest is Inspectron.HawkEye.RTSP.Messages.RtspRequestOptions) + { + + // Check the capabilities returned by OPTIONS + // The Public: header contains the list of commands the RTSP server supports + // Eg DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS, ANNOUNCE, RECORD, GET_PARAMETER]} + if (message.Headers.ContainsKey(RtspHeaderNames.Public)) + { + string[] parts = message.Headers[RtspHeaderNames.Public].Split(','); + foreach (String part in parts) { + if (part.Trim().ToUpper().Equals("GET_PARAMETER")) server_supports_get_parameter = true; + if (part.Trim().ToUpper().Equals("SET_PARAMETER")) server_supports_set_parameter = true; + } + } + + if (keepalive_timer == null) + { + // Start a Timer to send an Keepalive RTSP command every 20 seconds + keepalive_timer = new System.Timers.Timer(); + keepalive_timer.Elapsed += Timer_Elapsed; + keepalive_timer.Interval = 20 * 1000; + keepalive_timer.Enabled = true; + + // Send DESCRIBE + Inspectron.HawkEye.RTSP.Messages.RtspRequest describe_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestDescribe(); + describe_message.RtspUri = new Uri(url); + if (auth_type != null) { + AddAuthorization(describe_message,username,password,auth_type,realm,nonce,url); + } + rtsp_client.SendMessage(describe_message); + } + else + { + // If the Keepalive Timer was not null, the OPTIONS reply may have come from a Keepalive + // So no need to generate a DESCRIBE message + // do nothing + } + } + + + // If we get a reply to DESCRIBE (which was our second command), then prosess SDP and send the SETUP + if (message.OriginalRequest != null && message.OriginalRequest is Inspectron.HawkEye.RTSP.Messages.RtspRequestDescribe) + { + + // Got a reply for DESCRIBE + if (message.IsOk == false) { + _logger.Debug("Got Error in DESCRIBE Reply " + message.ReturnCode + " " + message.ReturnMessage); + return; + } + + // Examine the SDP + + _logger.Debug(System.Text.Encoding.UTF8.GetString(message.Data)); + + Inspectron.HawkEye.RTSP.Sdp.SdpFile sdp_data; + using (StreamReader sdp_stream = new StreamReader(new MemoryStream(message.Data))) + { + sdp_data = Inspectron.HawkEye.RTSP.Sdp.SdpFile.Read(sdp_stream); + } + + // RTP and RTCP 'channels' are used in TCP Interleaved mode (RTP over RTSP) + // These are the channels we request. The camera confirms the channel in the SETUP Reply. + // But, a Panasonic decides to use different channels in the reply. + int next_free_rtp_channel = 0; + int next_free_rtcp_channel = 1; + + // Process each 'Media' Attribute in the SDP (each sub-stream) + + for (int x = 0; x < sdp_data.Medias.Count; x++) + { + bool audio = (sdp_data.Medias[x].MediaType == Inspectron.HawkEye.RTSP.Sdp.Media.MediaTypes.audio); + bool video = (sdp_data.Medias[x].MediaType == Inspectron.HawkEye.RTSP.Sdp.Media.MediaTypes.video); + + if (video && video_payload != -1) continue; // have already matched a video payload. don't match another + if (audio && audio_payload != -1) continue; // have already matched an audio payload. don't match another + + if (audio && (client_wants_audio == false)) continue; // client does not want audio from the RTSP server + if (video && (client_wants_video == false)) continue; // client does not want video from the RTSP server + + if (audio || video) + { + + // search the attributes for control, rtpmap and fmtp + // (fmtp only applies to video) + String control = ""; // the "track" or "stream id" + Inspectron.HawkEye.RTSP.Sdp.AttributFmtp fmtp = null; // holds SPS and PPS in base64 (h264 video) + foreach (Inspectron.HawkEye.RTSP.Sdp.Attribut attrib in sdp_data.Medias[x].Attributs) { + if (attrib.Key.Equals("control")) { + String sdp_control = attrib.Value; + if (sdp_control.ToLower().StartsWith("rtsp://")) { + control = sdp_control; //absolute path + } else { + control = url + "/" + sdp_control; // relative path + } + if (video) video_uri = new Uri(control); + if (audio) audio_uri = new Uri(control); + } + if (attrib.Key.Equals("fmtp")) { + fmtp = attrib as Inspectron.HawkEye.RTSP.Sdp.AttributFmtp; + } + if (attrib.Key.Equals("rtpmap")) { + Inspectron.HawkEye.RTSP.Sdp.AttributRtpMap rtpmap = attrib as Inspectron.HawkEye.RTSP.Sdp.AttributRtpMap; + + // Check if the Codec Used (EncodingName) is one we support + String[] valid_video_codecs = {"H264","H265"}; + String[] valid_audio_codecs = {"PCMA", "PCMU", "AMR", "MPEG4-GENERIC" /* for aac */}; // Note some are "mpeg4-generic" lower case + + if (video && Array.IndexOf(valid_video_codecs,rtpmap.EncodingName.ToUpper()) >= 0) { + // found a valid codec + video_codec = rtpmap.EncodingName.ToUpper(); + video_payload = sdp_data.Medias[x].PayloadType; + } + if (audio && Array.IndexOf(valid_audio_codecs,rtpmap.EncodingName.ToUpper()) >= 0) { + audio_codec = rtpmap.EncodingName.ToUpper(); + audio_payload = sdp_data.Medias[x].PayloadType; + } + } + } + + // Create H264 RTP Parser + if (video && video_codec.Contains("H264")) + { + h264Payload = new Inspectron.HawkEye.RTSP.H264Payload(); + } + + // If the rtpmap contains H264 then split the fmtp to get the sprop-parameter-sets which hold the SPS and PPS in base64 + if (video && video_codec.Contains("H264") && fmtp != null) { + var param = Inspectron.HawkEye.RTSP.Sdp.H264Parameters.Parse(fmtp.FormatParameter); + var sps_pps = param.SpropParameterSets; + if (sps_pps.Count() >= 2) { + byte[] sps = sps_pps[0]; + byte[] pps = sps_pps[1]; + if (Received_SPS_PPS != null) { + Received_SPS_PPS(sps,pps); + } + h264_sps_pps_fired = true; + } + } + + // Create H265 RTP Parser + if (video && video_codec.Contains("H265")) + { + // TODO - check if DONL is being used + bool has_donl = false; + h265Payload = new Inspectron.HawkEye.RTSP.H265Payload(has_donl); + } + + // If the rtpmap contains H265 then split the fmtp to get the sprop-vps, sprop-sps and sprop-pps + // The RFC makes the VPS, SPS and PPS OPTIONAL so they may not be present. In which we pass back NULL values + if (video && video_codec.Contains("H265") && fmtp != null) + { + var param = Inspectron.HawkEye.RTSP.Sdp.H265Parameters.Parse(fmtp.FormatParameter); + var vps_sps_pps = param.SpropParameterSets; + if (vps_sps_pps.Count() >= 3) + { + byte[] vps = vps_sps_pps[0]; + byte[] sps = vps_sps_pps[1]; + byte[] pps = vps_sps_pps[2]; + if (Received_VPS_SPS_PPS != null) + { + Received_VPS_SPS_PPS(vps,sps, pps); + } + h265_vps_sps_pps_fired = true; + } + } + + // Create AAC RTP Parser + // Example fmtp is "96 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1490" + // Example fmtp is ""96 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1210" + if (audio && audio_codec.Contains("MPEG4-GENERIC") && fmtp.GetParameter("mode").ToLower().Equals("aac-hbr")) + { + // Extract config (eg 0x1490 or 0x1210) + + aacPayload = new Inspectron.HawkEye.RTSP.AACPayload(fmtp.GetParameter("config")); + } + + + // Send the SETUP RTSP command if we have a matching Payload Decoder + if (video && video_payload == -1) continue; + if (audio && audio_payload == -1) continue; + + RtspTransport transport = null; + + if (rtp_transport == RTP_TRANSPORT.TCP) + { + // Server interleaves the RTP packets over the RTSP connection + // Example for TCP mode (RTP over RTSP) Transport: RTP/AVP/TCP;interleaved=0-1 + if (video) { + video_data_channel = next_free_rtp_channel; + video_rtcp_channel = next_free_rtcp_channel; + } + if (audio) { + audio_data_channel = next_free_rtp_channel; + audio_rtcp_channel = next_free_rtcp_channel; + } + transport = new RtspTransport() + { + LowerTransport = RtspTransport.LowerTransportType.TCP, + Interleaved = new PortCouple(next_free_rtp_channel, next_free_rtcp_channel), // Eg Channel 0 for RTP video data. Channel 1 for RTCP status reports + }; + + next_free_rtp_channel += 2; + next_free_rtcp_channel += 2; + } + if (rtp_transport == RTP_TRANSPORT.UDP) + { + int rtp_port = 0; + int rtcp_port = 0; + // Server sends the RTP packets to a Pair of UDP Ports (one for data, one for rtcp control messages) + // Example for UDP mode Transport: RTP/AVP;unicast;client_port=8000-8001 + if (video) { + video_data_channel = video_udp_pair.data_port; // Used in DataReceived event handler + video_rtcp_channel = video_udp_pair.control_port; // Used in DataReceived event handler + rtp_port = video_udp_pair.data_port; + rtcp_port = video_udp_pair.control_port; + } + if (audio) { + audio_data_channel = audio_udp_pair.data_port; // Used in DataReceived event handler + audio_rtcp_channel = audio_udp_pair.control_port; // Used in DataReceived event handler + rtp_port = audio_udp_pair.data_port; + rtcp_port = audio_udp_pair.control_port; + } + transport = new RtspTransport() + { + LowerTransport = RtspTransport.LowerTransportType.UDP, + IsMulticast = false, + ClientPort = new PortCouple(rtp_port, rtcp_port), // a UDP Port for data (video or audio). a UDP Port for RTCP status reports + }; + } + if (rtp_transport == RTP_TRANSPORT.MULTICAST) + { + // Server sends the RTP packets to a Pair of UDP ports (one for data, one for rtcp control messages) + // using Multicast Address and Ports that are in the reply to the SETUP message + // Example for MULTICAST mode Transport: RTP/AVP;multicast + if (video) { + video_data_channel = 0; // we get this information in the SETUP message reply + video_rtcp_channel = 0; // we get this information in the SETUP message reply + } + if (audio) { + audio_data_channel = 0; // we get this information in the SETUP message reply + audio_rtcp_channel = 0; // we get this information in the SETUP message reply + } + transport = new RtspTransport() + { + LowerTransport = RtspTransport.LowerTransportType.UDP, + IsMulticast = true + }; + } + + // Generate SETUP messages + Inspectron.HawkEye.RTSP.Messages.RtspRequestSetup setup_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestSetup(); + setup_message.RtspUri = new Uri(control); + setup_message.AddTransport(transport); + if (auth_type != null) { + AddAuthorization(setup_message,username,password,auth_type,realm,nonce,url); + } + + // Add SETUP message to list of mesages to send + setup_messages.Add(setup_message); + + } + } + // Send the FIRST SETUP message and remove it from the list of Setup Messages + rtsp_client.SendMessage(setup_messages[0]); + setup_messages.RemoveAt(0); + } + + + // If we get a reply to SETUP (which was our third command), then we + // (i) check if the Interleaved Channel numbers have been modified by the camera (eg Panasonic cameras) + // (ii) check if we have any more SETUP commands to send out (eg if we are doing SETUP for Video and Audio) + // (iii) send a PLAY command if all the SETUP command have been sent + if (message.OriginalRequest != null && message.OriginalRequest is Inspectron.HawkEye.RTSP.Messages.RtspRequestSetup) + { + // Got Reply to SETUP + if (message.IsOk == false) { + _logger.Debug("Got Error in SETUP Reply " + message.ReturnCode + " " + message.ReturnMessage); + return; + } + + _logger.Debug("Got reply from Setup. Session is " + message.Session); + + session = message.Session; // Session value used with Play, Pause, Teardown and and additional Setups + if(message.Timeout > 0 && message.Timeout > keepalive_timer.Interval / 1000) + { + keepalive_timer.Interval = message.Timeout * 1000 / 2; + } + + // Check the Transport header + if (message.Headers.ContainsKey(RtspHeaderNames.Transport)) + { + + RtspTransport transport = RtspTransport.Parse(message.Headers[RtspHeaderNames.Transport]); + + // Check if Transport header includes Multicast + if (transport.IsMulticast) + { + String multicast_address = transport.Destination; + video_data_channel = transport.Port.First; + video_rtcp_channel = transport.Port.Second; + + // Create the Pair of UDP Sockets in Multicast mode + video_udp_pair = new Inspectron.HawkEye.RTSP.UDPSocket(multicast_address, video_data_channel, multicast_address, video_rtcp_channel); + video_udp_pair.DataReceived += Rtp_DataReceived; + video_udp_pair.Start(); + + // TODO - Need to set audio_udp_pair for Multicast + } + + // check if the requested Interleaved channels have been modified by the camera + // in the SETUP Reply (Panasonic have a camera that does this) + if (transport.LowerTransport == RtspTransport.LowerTransportType.TCP) { + if (message.OriginalRequest.RtspUri == video_uri) { + video_data_channel = transport.Interleaved.First; + video_rtcp_channel = transport.Interleaved.Second; + } + if (message.OriginalRequest.RtspUri == audio_uri) { + audio_data_channel = transport.Interleaved.First; + audio_rtcp_channel = transport.Interleaved.Second; + } + + } + } + + + // Check if we have another SETUP command to send, then remote it from the list + if (setup_messages.Count > 0) { + // send the next SETUP message, after adding in the 'session' + Inspectron.HawkEye.RTSP.Messages.RtspRequestSetup next_setup = setup_messages[0]; + next_setup.Session = session; + rtsp_client.SendMessage(next_setup); + + setup_messages.RemoveAt(0); + } + + else { + // Send PLAY + Inspectron.HawkEye.RTSP.Messages.RtspRequest play_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestPlay(); + play_message.RtspUri = new Uri(url); + play_message.Session = session; + if (auth_type != null) { + AddAuthorization(play_message,username,password,auth_type,realm,nonce,url); + } + rtsp_client.SendMessage(play_message); + } + } + + // If we get a reply to PLAY (which was our fourth command), then we should have video being received + if (message.OriginalRequest != null && message.OriginalRequest is Inspectron.HawkEye.RTSP.Messages.RtspRequestPlay) + { + // Got Reply to PLAY + if (message.IsOk == false) { + _logger.Debug("Got Error in PLAY Reply " + message.ReturnCode + " " + message.ReturnMessage); + return; + } + + _logger.Debug("Got reply from Play " + message.Command); + } + + } + + void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + { + // Send Keepalive message + // The ONVIF Standard uses SET_PARAMETER as "an optional method to keep an RTSP session alive" + // RFC 2326 (RTSP Standard) says "GET_PARAMETER with no entity body may be used to test client or server liveness("ping")" + + // This code uses GET_PARAMETER (unless OPTIONS report it is not supported, and then it sends OPTIONS as a keepalive) + + + if (server_supports_get_parameter) { + + Inspectron.HawkEye.RTSP.Messages.RtspRequest getparam_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestGetParameter(); + getparam_message.RtspUri = new Uri(url); + getparam_message.Session = session; + if (auth_type != null) + { + AddAuthorization(getparam_message, username, password, auth_type, realm, nonce, url); + } + rtsp_client.SendMessage(getparam_message); + + } else { + + Inspectron.HawkEye.RTSP.Messages.RtspRequest options_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestOptions(); + options_message.RtspUri = new Uri(url); + if (auth_type != null) { + AddAuthorization(options_message,username,password,auth_type,realm,nonce,url); + } + rtsp_client.SendMessage(options_message); + } + } + + // Generate Basic or Digest Authorization + public void AddAuthorization(RtspMessage message, string username, string password, + string auth_type, string realm, string nonce, string url) { + + if (username == null || username.Length == 0) return; + if (password == null || password.Length == 0) return; + if (realm == null || realm.Length == 0) return; + if (auth_type.Equals("Digest") && (nonce == null || nonce.Length == 0)) return; + + if (auth_type.Equals("Basic")) { + byte[] credentials = System.Text.Encoding.UTF8.GetBytes(username+":"+password); + String credentials_base64 = Convert.ToBase64String(credentials); + String basic_authorization = "Basic " + credentials_base64; + + message.Headers.Add(RtspHeaderNames.Authorization, basic_authorization); + + return; + } + else if (auth_type.Equals("Digest")) { + + string method = message.Method; // DESCRIBE, SETUP, PLAY etc + + MD5 md5 = System.Security.Cryptography.MD5.Create(); + String hashA1 = CalculateMD5Hash(md5, username+":"+realm+":"+password); + String hashA2 = CalculateMD5Hash(md5, method + ":" + url); + String response = CalculateMD5Hash(md5, hashA1 + ":" + nonce + ":" + hashA2); + + const String quote = "\""; + String digest_authorization = "Digest username=" + quote + username + quote +", " + + "realm=" + quote + realm + quote + ", " + + "nonce=" + quote + nonce + quote + ", " + + "uri=" + quote + url + quote + ", " + + "response=" + quote + response + quote; + + message.Headers.Add(RtspHeaderNames.Authorization, digest_authorization); + + return; + } + else { + return; + } + + } + + // MD5 (lower case) + public string CalculateMD5Hash(MD5 md5_session, string input) + { + byte[] inputBytes = System.Text.Encoding.UTF8.GetBytes(input); + byte[] hash = md5_session.ComputeHash(inputBytes); + + StringBuilder output = new StringBuilder(); + for (int i = 0; i < hash.Length; i++) { + output.Append(hash[i].ToString("x2")); + } + + return output.ToString(); + } + + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/G711Payload.cs b/framework/Inspectron.HawkEye/RTSP/G711Payload.cs new file mode 100644 index 0000000..5bc0f3e --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/G711Payload.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; + +namespace Inspectron.HawkEye.RTSP +{ + // This class handles the G711 Payload + // It has methods to process the RTP Payload + + public class G711Payload + { + // Constructor + public G711Payload() + { + } + + public List Process_G711_RTP_Packet(byte[] rtp_payload, int rtp_marker) { + + List audio_data = new List(); + audio_data.Add(rtp_payload); + + return audio_data; + } + + /* Untested - used with G711.1 and PCMA-WB and PCMU-WB Codec Names */ + public List Process_G711_1_RTP_Packet(byte[] rtp_payload, int rtp_marker) { + + // Look at the Header. This tells us the G711 mode being used + + // Mode Index (MI) is + // 1 - R1 40 octets containg Layer 0 data + // 2 - R2a 50 octets containing Layer 0 plus Layer 1 data + // 3 - R2b 50 octets containing Layer 0 plus Layer 2 data + // 4 - R3 60 octets containing Layer 0 plus Layer 1 plus Layer 2 data + + byte mode_index = (byte)(rtp_payload[0] & 0x07); + + int size_of_one_frame = 0; // will be in bytes + switch (mode_index) { + case 1: size_of_one_frame = 40; break; + case 2: size_of_one_frame = 50; break; + case 3: size_of_one_frame = 50; break; + case 4: size_of_one_frame = 60; break; + default: return null; // invalid Mode Index + } + + int number_frames = (rtp_payload.Length - 1) / size_of_one_frame; + + + // Return just the basic u-Law or A-Law audio (the Layer 0 audio) + + List audio_data = new List(); + + // Extract each audio frame and place in the audio_data List + int frame_start = 1; // starts just after the MI header + while (frame_start + size_of_one_frame < rtp_payload.Length) { + byte[] layer_0_audio = new byte[40]; + System.Array.Copy(rtp_payload,frame_start,layer_0_audio,0,40); // 40 octets in Layer 0 data + audio_data.Add(layer_0_audio); + + frame_start += size_of_one_frame; + } + return audio_data; + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/H264Payload.cs b/framework/Inspectron.HawkEye/RTSP/H264Payload.cs new file mode 100644 index 0000000..986fc36 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/H264Payload.cs @@ -0,0 +1,184 @@ +using System.Collections.Generic; +using System.IO; + +namespace Inspectron.HawkEye.RTSP +{ + // This class handles the H264 Payload + // It has methods to parse parameters in the SDP + // It has methods to process the RTP Payload + + public class H264Payload + { + private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); + + int norm, fu_a, fu_b, stap_a, stap_b, mtap16, mtap24 = 0; // used for diagnostics stats + + List temporary_rtp_payloads = new List(); // used to assemble the RTP packets that form one RTP Frame + // Eg all the RTP Packets from M=0 through to M=1 + + MemoryStream fragmented_nal = new MemoryStream(); // used to concatenate fragmented H264 NALs where NALs are split over RTP packets + + + // Constructor + public H264Payload() + { + } + + public List Process_H264_RTP_Packet(byte[] rtp_payload, int rtp_marker) { + + // Add to the list of payloads for the current Frame of video + temporary_rtp_payloads.Add(rtp_payload); // Todo Could optimise this and go direct to Process Frame if just 1 packet in frame + + if (rtp_marker == 1) + { + // End Marker is set. Process the list of RTP Packets (forming 1 RTP frame) and save the NALs to a file + List nal_units = Process_H264_RTP_Frame(temporary_rtp_payloads); + temporary_rtp_payloads.Clear(); + + return nal_units; + } + + return null; // we don't have a frame yet. Keep accumulating RTP packets + } + + + // Process a RTP Frame. A RTP Frame can consist of several RTP Packets which have the same Timestamp + // Returns a list of NAL Units (with no 00 00 00 01 header and with no Size header) + private List Process_H264_RTP_Frame(List rtp_payloads) + { + _logger.Debug("RTP Data comprised of " + rtp_payloads.Count + " rtp packets"); + + List nal_units = new List(); // Stores the NAL units for a Video Frame. May be more than one NAL unit in a video frame. + + for (int payload_index = 0; payload_index < rtp_payloads.Count; payload_index++) + { + // Examine the first rtp_payload and the first byte (the NAL header) + int nal_header_f_bit = (rtp_payloads[payload_index][0] >> 7) & 0x01; + int nal_header_nri = (rtp_payloads[payload_index][0] >> 5) & 0x03; + int nal_header_type = (rtp_payloads[payload_index][0] >> 0) & 0x1F; + + // If the Nal Header Type is in the range 1..23 this is a normal NAL (not fragmented) + // So write the NAL to the file + if (nal_header_type >= 1 && nal_header_type <= 23) + { + _logger.Debug("Normal NAL"); + norm++; + nal_units.Add(rtp_payloads[payload_index]); + } + // There are 4 types of Aggregation Packet (split over RTP payloads) + else if (nal_header_type == 24) + { + _logger.Debug("Agg STAP-A"); + stap_a++; + + // RTP packet contains multiple NALs, each with a 16 bit header + // Read 16 byte size + // Read NAL + try + { + int ptr = 1; // start after the nal_header_type which was '24' + // if we have at least 2 more bytes (the 16 bit size) then consume more data + while (ptr + 2 < (rtp_payloads[payload_index].Length - 1)) + { + int size = (rtp_payloads[payload_index][ptr] << 8) + (rtp_payloads[payload_index][ptr + 1] << 0); + ptr = ptr + 2; + byte[] nal = new byte[size]; + System.Array.Copy(rtp_payloads[payload_index], ptr, nal, 0, size); // copy the NAL + nal_units.Add(nal); // Add to list of NALs for this RTP frame. Start Codes like 00 00 00 01 get added later + ptr = ptr + size; + } + } + catch + { + _logger.Debug("H264 Aggregate Packet processing error"); + } + } + else if (nal_header_type == 25) + { + _logger.Debug("Agg STAP-B not supported"); + stap_b++; + } + else if (nal_header_type == 26) + { + _logger.Debug("Agg MTAP16 not supported"); + mtap16++; + } + else if (nal_header_type == 27) + { + _logger.Debug("Agg MTAP24 not supported"); + mtap24++; + } + else if (nal_header_type == 28) + { + _logger.Debug("Frag FU-A"); + fu_a++; + + // Parse Fragmentation Unit Header + int fu_header_s = (rtp_payloads[payload_index][1] >> 7) & 0x01; // start marker + int fu_header_e = (rtp_payloads[payload_index][1] >> 6) & 0x01; // end marker + int fu_header_r = (rtp_payloads[payload_index][1] >> 5) & 0x01; // reserved. should be 0 + int fu_header_type = (rtp_payloads[payload_index][1] >> 0) & 0x1F; // Original NAL unit header + + _logger.Debug("Frag FU-A s=" + fu_header_s + "e=" + fu_header_e); + + // Check Start and End flags + if (fu_header_s == 1 && fu_header_e == 0) + { + // Start of Fragment. + // Initiise the fragmented_nal byte array + // Build the NAL header with the original F and NRI flags but use the the Type field from the fu_header_type + byte reconstructed_nal_type = (byte)((nal_header_f_bit << 7) + (nal_header_nri << 5) + fu_header_type); + + // Empty the stream + fragmented_nal.SetLength(0); + + // Add reconstructed_nal_type byte to the memory stream + fragmented_nal.WriteByte(reconstructed_nal_type); + + // copy the rest of the RTP payload to the memory stream + fragmented_nal.Write(rtp_payloads[payload_index], 2, rtp_payloads[payload_index].Length - 2); + } + + if (fu_header_s == 0 && fu_header_e == 0) + { + // Middle part of Fragment + // Append this payload to the fragmented_nal + // Data starts after the NAL Unit Type byte and the FU Header byte + fragmented_nal.Write(rtp_payloads[payload_index], 2, rtp_payloads[payload_index].Length - 2); + } + + if (fu_header_s == 0 && fu_header_e == 1) + { + // End part of Fragment + // Append this payload to the fragmented_nal + // Data starts after the NAL Unit Type byte and the FU Header byte + fragmented_nal.Write(rtp_payloads[payload_index], 2, rtp_payloads[payload_index].Length - 2); + + // Add the NAL to the array of NAL units + nal_units.Add(fragmented_nal.ToArray()); + } + } + + else if (nal_header_type == 29) + { + _logger.Debug("Frag FU-B not supported"); + fu_b++; + } + else + { + _logger.Debug("Unknown NAL header " + nal_header_type + " not supported"); + } + + } + + // Output some statistics + _logger.Debug("Norm=" + norm + " ST-A=" + stap_a + " ST-B=" + stap_b + " M16=" + mtap16 + " M24=" + mtap24 + " FU-A=" + fu_a + " FU-B=" + fu_b); + + // Output all the NALs that form one RTP Frame (one frame of video) + return nal_units; + + } + + + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/H265Payload.cs b/framework/Inspectron.HawkEye/RTSP/H265Payload.cs new file mode 100644 index 0000000..9af3d89 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/H265Payload.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Inspectron.HawkEye.RTSP +{ + // This class handles the H265 Payload + // It has methods to parse parameters in the SDP + // It has methods to process the RTP Payload + + // By Roger Hardiman, RJH Technical Consultancy Ltd + + public class H265Payload + { + // H265 / HEVC structure. + // An 'Access Unit' is the set of NAL Units that form one Picture + // NAL Units have a 2 byte header comprising of + // F Bit, Type, Layer ID and TID + + + int single, agg, frag = 0; // used for diagnostics stats + bool has_donl = false; + + List temporary_rtp_payloads = new List(); // used to assemble the RTP packets that form one RTP Frame + // Eg all the RTP Packets from M=0 through to M=1 + + MemoryStream fragmented_nal = new MemoryStream(); // used to concatenate fragmented H264 NALs where NALs are split over RTP packets + + + // Constructor + public H265Payload(bool has_donl) + { + this.has_donl = has_donl; + } + + public List Process_H265_RTP_Packet(byte[] rtp_payload, int rtp_marker) { + + // Add payload to the List of payloads for the current Frame of Video + // ie all the payloads with M=0 up to the final payload where M=1 + temporary_rtp_payloads.Add(rtp_payload); // Todo Could optimise this and go direct to Process Frame if just 1 packet in frame + + if (rtp_marker == 1) + { + // End Marker is set. Process the list of RTP Packets (forming 1 RTP frame) and save the NALs to a file + List nal_units = Process_H265_RTP_Frame(temporary_rtp_payloads); + temporary_rtp_payloads.Clear(); + + return nal_units; + } + + return null; // we don't have a frame yet. Keep accumulating RTP packets + } + + + // Process a RTP Frame. A RTP Frame can consist of several RTP Packets which have the same Timestamp + // Returns a list of NAL Units (with no 00 00 00 01 header and with no Size header) + private List Process_H265_RTP_Frame(List rtp_payloads) + { + Console.WriteLine("RTP Data comprised of " + rtp_payloads.Count + " rtp packets"); + + List nal_units = new List(); // Stores the NAL units for a Video Frame. May be more than one NAL unit in a video frame. + + for (int payload_index = 0; payload_index < rtp_payloads.Count; payload_index++) + { + // Examine the first two bytes of the RTP data, the Payload Header + // F (Forbidden Bit), + // Type of NAL Unit (or VCL NAL Unit if Type is < 32), + // LayerId + // TID (TemporalID = TID - 1) + /*+---------------+---------------+ + *|0|1|2|3|4|5|6|7|0|1|2|3|4|5|6|7| + *+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + *|F| Type | LayerId | TID | + *+-------------+-----------------+ + */ + + int payload_header = (rtp_payloads[payload_index][0] << 8) | (rtp_payloads[payload_index][1]); + int payload_header_f_bit = (payload_header >> 15) & 0x01; + int payload_header_type = (payload_header >> 9) & 0x3F; + int payload_header_layer_id = (payload_header >> 3) & 0x3F; + int payload_header_tid = payload_header & 0x7; + + + // There are three ways to Packetize NAL units into RTP Packets + // Single NAL Unit Packet + // Aggregation Packet (payload_header_type = 48) + // Fragmentation Unit (payload_header_type = 49) + + + // Single NAL Unit Packet + // 32=VPS + // 33=SPS + // 34=PPS + if (payload_header_type != 48 && payload_header_type != 49) + { + Console.WriteLine("Single NAL"); + single++; + + //TODO - Handle DONL + + nal_units.Add(rtp_payloads[payload_index]); + } + + // Aggregation Packet + else if (payload_header_type == 48) + { + Console.WriteLine("Aggregation Packet"); + agg++; + + // RTP packet contains multiple NALs, each with a 16 bit header + // Read 16 byte size + // Read NAL + // Use a Try/Catch to protect from bad RTP data where block sizes exceed the + // available data + try + { + int ptr = 2; // start after 16 bit Payload Header + + // loop until the ptr has moved beyond the length of the data + while (ptr < (rtp_payloads[payload_index].Length - 1)) + { + if (has_donl) ptr = ptr + 2; // step over the DONL data + int size = (rtp_payloads[payload_index][ptr] << 8) + (rtp_payloads[payload_index][ptr + 1] << 0); + ptr = ptr + 2; + byte[] nal = new byte[size]; + System.Array.Copy(rtp_payloads[payload_index], ptr, nal, 0, size); // copy the NAL + nal_units.Add(nal); // Add to list of NALs for this RTP frame. Start Codes like 00 00 00 01 get added later + ptr = ptr + size; + } + } + catch + { + Console.WriteLine("H265 Aggregate Packet processing error"); + } + } + + // Fragmentation Unit + else if (payload_header_type == 49) + { + Console.WriteLine("Fragmentation Unit"); + frag++; + + // Parse Fragmentation Unit Header + int fu_header_s = (rtp_payloads[payload_index][2] >> 7) & 0x01; // start marker + int fu_header_e = (rtp_payloads[payload_index][2] >> 6) & 0x01; // end marker + int fu_header_type = (rtp_payloads[payload_index][2] >> 0) & 0x3F; // fu type + + Console.WriteLine("Frag FU-A s=" + fu_header_s + "e=" + fu_header_e); + + // Check Start and End flags + if (fu_header_s == 1 && fu_header_e == 0) + { + // Start of Fragment. + // Initiise the fragmented_nal byte array + + // Empty the stream + fragmented_nal.SetLength(0); + + // Reconstrut the NAL header from the rtp_payload_header, replacing the Type with FU Type + int nal_header = (payload_header & 0x81FF); // strip out existing 'type' + nal_header = nal_header | (fu_header_type << 9); + + fragmented_nal.WriteByte((byte)((nal_header >> 8) & 0xFF)); + fragmented_nal.WriteByte((byte)((nal_header >> 0) & 0xFF)); + + if (has_donl) + { + // start copying after the DONL data + fragmented_nal.Write(rtp_payloads[payload_index], 5, rtp_payloads[payload_index].Length - 5); + } + else + { + // there is no DONL data + fragmented_nal.Write(rtp_payloads[payload_index], 3, rtp_payloads[payload_index].Length - 3); + } + } + + if (fu_header_s == 0 && fu_header_e == 0) + { + // Middle part of Fragment + // Append this payload to the fragmented_nal + + if (has_donl) { + // start copying after the DONL data + fragmented_nal.Write(rtp_payloads[payload_index], 5, rtp_payloads[payload_index].Length - 5); + } else { + // there is no DONL data + fragmented_nal.Write(rtp_payloads[payload_index], 3, rtp_payloads[payload_index].Length - 3); + } + } + + if (fu_header_s == 0 && fu_header_e == 1) + { + // End part of Fragment + // Append this payload to the fragmented_nal + if (has_donl) + { + // start copying after the DONL data + fragmented_nal.Write(rtp_payloads[payload_index], 5, rtp_payloads[payload_index].Length - 5); + } + else + { + // there is no DONL data + fragmented_nal.Write(rtp_payloads[payload_index], 3, rtp_payloads[payload_index].Length - 3); + } + + // Add the NAL to the array of NAL units + nal_units.Add(fragmented_nal.ToArray()); + } + } + else { + Console.WriteLine("Unknown Payload Header Type = " + payload_header_type); + } + } + + // Output some statistics + Console.WriteLine("Single=" + single + " Agg=" + agg + " Frag=" + frag); + + // Output all the NALs that form one RTP Frame (one frame of video) + return nal_units; + + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/IRTSPTransport.cs b/framework/Inspectron.HawkEye/RTSP/IRTSPTransport.cs new file mode 100644 index 0000000..62af36e --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/IRTSPTransport.cs @@ -0,0 +1,41 @@ +namespace Inspectron.HawkEye.RTSP +{ + /// + /// Interface for Transport of Rtsp (TCP, TCP+SSL,..) + /// + public interface IRtspTransport + { + /// + /// Gets the stream of the transport. + /// + /// A stream + System.IO.Stream GetStream(); + + /// + /// Gets the remote address. + /// + /// The remote address. + string RemoteAddress + { + get; + } + + /// + /// Closes this instance. + /// + void Close(); + + /// + /// Gets a value indicating whether this is connected. + /// + /// true if connected; otherwise, false. + bool Connected { get; } + + /// + /// Reconnect this instance. + /// Must do nothing if already connected. + /// + /// Error during socket + void Reconnect(); + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/PortCouple.cs b/framework/Inspectron.HawkEye/RTSP/Messages/PortCouple.cs new file mode 100644 index 0000000..2791189 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Messages/PortCouple.cs @@ -0,0 +1,103 @@ +using System; +using System.Diagnostics.Contracts; +using System.Globalization; + +namespace Inspectron.HawkEye.RTSP.Messages +{ + /// + /// Describe a couple of port used to transfer video and command. + /// + public class PortCouple + { + /// + /// Gets or sets the first port number. + /// + /// The first port. + public int First { get; set; } + /// + /// Gets or sets the second port number. + /// + /// If not present the value is 0 + /// The second port. + public int Second { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public PortCouple() + { } + /// + /// Initializes a new instance of the class. + /// + /// The first port. + public PortCouple(int first) + { + First = first; + Second = 0; + } + /// + /// Initializes a new instance of the class. + /// + /// The first port. + /// The second port. + public PortCouple(int first, int second) + { + First = first; + Second = second; + } + + /// + /// Gets a value indicating whether this instance has second port. + /// + /// + /// true if this instance has second port; otherwise, false. + /// + public bool IsSecondPortPresent + { + get { return Second != 0; } + } + + /// + /// Parses the int values of port. + /// + /// A string value. + /// The port couple + public static PortCouple Parse(string stringValue) + { + if (stringValue == null) + throw new ArgumentNullException("stringValue"); + Contract.Requires(!string.IsNullOrEmpty(stringValue)); + + string[] values = stringValue.Split('-'); + + int tempValue; + + int.TryParse(values[0], out tempValue); + PortCouple result = new PortCouple(tempValue); + + tempValue = 0; + if (values.Length > 1) + int.TryParse(values[1], out tempValue); + + result.Second = tempValue; + + return result; + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + if (IsSecondPortPresent) + return First.ToString(CultureInfo.InvariantCulture) + "-" + Second.ToString(CultureInfo.InvariantCulture); + else + return First.ToString(CultureInfo.InvariantCulture); + } + + + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPChunk.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPChunk.cs new file mode 100644 index 0000000..f29bdd2 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPChunk.cs @@ -0,0 +1,49 @@ +using System; + +namespace Inspectron.HawkEye.RTSP.Messages +{ + /// + /// Class wich represent each message echanged on Rtsp socket. + /// + public abstract class RtspChunk : ICloneable + { + /// + /// Logs the message to debug. + /// + public void LogMessage() + { + LogMessage(NLog.LogLevel.Debug); + } + + /// + /// Logs the message. + /// + /// The log level. + public abstract void LogMessage(NLog.LogLevel aLevel); + + /// + /// Gets or sets the data associate with the message. + /// + /// Array of byte transmit with the message. + public byte[] Data + { get; set; } + + /// + /// Gets or sets the source port wich receive the message. + /// + /// The source port. + public RtspListener SourcePort { get; set; } + + #region ICloneable Membres + + /// + /// Crée un nouvel objet qui est une copie de l'instance en cours. + /// + /// + /// Nouvel objet qui est une copie de cette instance. + /// + public abstract object Clone(); + + #endregion + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPData.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPData.cs new file mode 100644 index 0000000..5c3d619 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPData.cs @@ -0,0 +1,45 @@ +namespace Inspectron.HawkEye.RTSP.Messages +{ + /// + /// Message wich represent data. ($ limited message) + /// + public class RtspData : RtspChunk + { + private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); + + /// + /// Logs the message to debug. + /// + public override void LogMessage(NLog.LogLevel aLevel) + { + // Default value to debug + if (aLevel == null) + aLevel = NLog.LogLevel.Debug; + // if the level is not logged directly return + if (!_logger.IsEnabled(aLevel)) + return; + _logger.Log(aLevel, "Data message"); + if (Data == null) + _logger.Log(aLevel, "Data : null"); + else + _logger.Log(aLevel, "Data length :-{0}-", Data.Length); + } + + public int Channel { get; set; } + + /// + /// Clones this instance. + /// Listner is not cloned + /// + /// a clone of this instance + public override object Clone() + { + RtspData result = new RtspData(); + result.Channel = this.Channel; + if (this.Data != null) + result.Data = this.Data.Clone() as byte[]; + result.SourcePort = this.SourcePort; + return result; + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPHeaderNames.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPHeaderNames.cs new file mode 100644 index 0000000..c2fd567 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPHeaderNames.cs @@ -0,0 +1,19 @@ +namespace Inspectron.HawkEye.RTSP.Messages +{ + /// + /// Class containing helper constant for general use headers. + /// + public static class RtspHeaderNames + { + public const string ContentBase = "Content-Base"; + public const string ContentEncoding = "Content-Encoding"; + public const string ContentType = "Content-Type"; + + public const string Public = "Public"; + public const string Session = "Session"; + public const string Transport = "Transport"; + + public const string WWWAuthenticate = "WWW-Authenticate"; + public const string Authorization = "Authorization"; + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPMessage.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPMessage.cs new file mode 100644 index 0000000..a32b53b --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPMessage.cs @@ -0,0 +1,309 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; + +namespace Inspectron.HawkEye.RTSP.Messages +{ + public class RtspMessage : RtspChunk + { + private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); + + /// + /// The regex to validate the Rtsp message. + /// + private static readonly Regex _rtspVersionTest = new Regex(@"^RTSP/\d\.\d", RegexOptions.Compiled); + /// + /// Create the good type of Rtsp Message from the header. + /// + /// A request line. + /// An Rtsp message + public static RtspMessage GetRtspMessage(string aRequestLine) + { + // We can't determine the message + if (string.IsNullOrEmpty(aRequestLine)) + return new RtspMessage(); + string[] requestParts = aRequestLine.Split(new char[] { ' ' }, 3); + RtspMessage returnValue; + if (requestParts.Length == 3) + { + // A request is : Method SP Request-URI SP RTSP-Version + // A response is : RTSP-Version SP Status-Code SP Reason-Phrase + // RTSP-Version = "RTSP" "/" 1*DIGIT "." 1*DIGIT + if (_rtspVersionTest.IsMatch(requestParts[2])) + returnValue = RtspRequest.GetRtspRequest(requestParts); + else if (_rtspVersionTest.IsMatch(requestParts[0])) + returnValue = new RtspResponse(); + else + { + _logger.Warn(CultureInfo.InvariantCulture, "Got a strange message {0}", aRequestLine); + returnValue = new RtspMessage(); + } + } + else + { + _logger.Warn(CultureInfo.InvariantCulture, "Got a strange message {0}", aRequestLine); + returnValue = new RtspMessage(); + } + returnValue.Command = aRequestLine; + return returnValue; + } + + /// + /// Initializes a new instance of the class. + /// + public RtspMessage() + { + Data = new byte[0]; + Creation = DateTime.Now; + } + + private Dictionary _headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + + internal protected string[] commandArray; + + /// + /// Gets or sets the creation time. + /// + /// The creation time. + public DateTime Creation { get; private set; } + + /// + /// Gets or sets the command of the message (first line). + /// + /// The command. + public string Command + { + get + { + if (commandArray == null) + return string.Empty; + return string.Join(" ", commandArray); + } + set + { + if (value == null) + commandArray = new string[] { String.Empty }; + else + commandArray = value.Split(new char[] {' '}, 3); + } + } + + + /// + /// Gets the Method of the message (eg OPTIONS, DESCRIBE, SETUP, PLAY). + /// + /// The Method + public string Method + { + get + { + if (commandArray == null) + return string.Empty; + return commandArray[0]; + } + } + + + /// + /// Gets the headers of the message. + /// + /// The headers. + public Dictionary Headers + { + get + { + return _headers; + } + } + + /// + /// Adds one header from a string. + /// + /// The string containing header of format Header: Value. + /// is null + public void AddHeader(string line) + { + if (line == (string)null) + throw new ArgumentNullException("line"); + + //spliter + string[] elements = line.Split(new char[] { ':' }, 2); + if (elements.Length == 2) + { + _headers[elements[0].Trim()] = elements[1].TrimStart(); + } + else + { + _logger.Warn(CultureInfo.InvariantCulture, "Invalid Header received : -{0}-", line); + } + } + + /// + /// Gets or sets the Ccommande Seqquence number. + /// If the header is not define or not a valid number it return 0 + /// + /// The sequence number. + public int CSeq + { + get + { + string returnStringValue; + int returnValue; + if (!(_headers.TryGetValue("CSeq", out returnStringValue) && + int.TryParse(returnStringValue, out returnValue))) + returnValue = 0; + + return returnValue; + } + set + { + _headers["CSeq"] = value.ToString(CultureInfo.InvariantCulture); + } + } + + /// + /// Gets the session ID. + /// + /// The session ID. + public virtual string Session + { + get + { + if (!_headers.ContainsKey("Session")) + return null; + + return _headers["Session"]; + } + set + { + _headers["Session"] = value; + } + } + + /// + /// Initialises the length of the data byte array from content lenth header. + /// + public void InitialiseDataFromContentLength() + { + int dataLength; + if (!(_headers.ContainsKey("Content-Length") + && int.TryParse(_headers["Content-Length"], out dataLength))) + { + dataLength = 0; + } + this.Data = new byte[dataLength]; + } + + /// + /// Adjusts the content length header. + /// + public void AdjustContentLength() + { + if (Data.Length > 0) + { + _headers["Content-Length"] = Data.Length.ToString(CultureInfo.InvariantCulture); + } + else + { + _headers.Remove("Content-Length"); + } + } + + /// + /// Sends to the message to a stream. + /// + /// The stream. + /// is empty + /// can't be written. + public void SendTo(Stream stream) + { + // + if (stream == null) + throw new ArgumentNullException("stream"); + if (!stream.CanWrite) + throw + new ArgumentException("Stream CanWrite == false, can't send message to it", "stream"); + // + Contract.EndContractBlock(); + + Encoding encoder = ASCIIEncoding.UTF8; + StringBuilder outputString = new StringBuilder(); + + AdjustContentLength(); + + // output header + outputString.Append(Command); + outputString.Append("\r\n"); + foreach (KeyValuePair item in _headers) + { + outputString.AppendFormat("{0}: {1}\r\n", item.Key, item.Value); + } + outputString.Append("\r\n"); + byte[] buffer = encoder.GetBytes(outputString.ToString()); + lock(stream) { + stream.Write(buffer, 0, buffer.Length); + + // Output data + if (Data.Length > 0) + stream.Write(Data, 0, Data.Length); + + } + stream.Flush(); + } + + + + /// + /// Logs the message. + /// + /// A log level. + public override void LogMessage(NLog.LogLevel aLevel) + { + // Default value to debug + if (aLevel == null) + aLevel = NLog.LogLevel.Debug; + // if the level is not logged directly return + if (!_logger.IsEnabled(aLevel)) + return; + + _logger.Log(aLevel, "Commande : {0}", Command); + foreach (KeyValuePair item in _headers) + { + _logger.Log(aLevel, "Header : {0}: {1}", item.Key, item.Value); + } + + if (Data.Length > 0) + { + _logger.Log(aLevel, "Data :-{0}-", ASCIIEncoding.ASCII.GetString(Data)); + } + } + + /// + /// Crée un nouvel objet qui est une copie de l'instance en cours. + /// + /// + /// Nouvel objet qui est une copie de cette instance. + /// + public override object Clone() + { + RtspMessage returnValue = GetRtspMessage(this.Command); + + foreach (var item in this.Headers) + { + if (item.Value == null) + returnValue.Headers.Add(item.Key.Clone() as string, null); + else + returnValue.Headers.Add(item.Key.Clone() as string, item.Value.Clone() as string); + } + returnValue.Data = this.Data.Clone() as byte[]; + returnValue.SourcePort = this.SourcePort; + + return returnValue; + } + + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequest.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequest.cs new file mode 100644 index 0000000..04baa63 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequest.cs @@ -0,0 +1,191 @@ +using System; +using System.Diagnostics; + +namespace Inspectron.HawkEye.RTSP.Messages +{ + /// + /// An Rtsp Request + /// + public class RtspRequest : RtspMessage + { + + /// + /// Request type. + /// + public enum RequestType + { + UNKNOWN, + DESCRIBE, + ANNOUNCE, + GET_PARAMETER, + OPTIONS, + PAUSE, + PLAY, + RECORD, + REDIRECT, + SETUP, + SET_PARAMETER, + TEARDOWN, + } + + /// + /// Parses the request command. + /// + /// A string request command. + /// The typed request. + internal static RequestType ParseRequest(string aStringRequest) + { + RequestType returnValue; + if (!Enum.TryParse(aStringRequest, true, out returnValue)) + returnValue = RequestType.UNKNOWN; + return returnValue; + } + + /// + /// Gets the Rtsp request. + /// + /// A request parts. + /// the parsed request + internal static RtspMessage GetRtspRequest(string[] aRequestParts) + { + // + Debug.Assert(aRequestParts != (string[])null, "aRequestParts"); + Debug.Assert(aRequestParts.Length != 0, "aRequestParts.Length == 0"); + // + // we already know this is a Request + RtspRequest returnValue; + switch (ParseRequest(aRequestParts[0])) + { + case RequestType.OPTIONS: + returnValue = new RtspRequestOptions(); + break; + case RequestType.DESCRIBE: + returnValue = new RtspRequestDescribe(); + break; + case RequestType.SETUP: + returnValue = new RtspRequestSetup(); + break; + case RequestType.PLAY: + returnValue = new RtspRequestPlay(); + break; + case RequestType.PAUSE: + returnValue = new RtspRequestPause(); + break; + case RequestType.TEARDOWN: + returnValue = new RtspRequestTeardown(); + break; + case RequestType.GET_PARAMETER: + returnValue = new RtspRequestGetParameter(); + break; + case RequestType.ANNOUNCE: + returnValue = new RtspRequestAnnounce(); + break; + case RequestType.RECORD: + returnValue = new RtspRequestRecord(); + break; + /* + case RequestType.REDIRECT: + break; + + case RequestType.SET_PARAMETER: + break; + */ + case RequestType.UNKNOWN: + default: + returnValue = new RtspRequest(); + break; + } + + + + return returnValue; + } + + /// + /// Initializes a new instance of the class. + /// + public RtspRequest() + { + Command = "OPTIONS * RTSP/1.0"; + } + + /// + /// Gets the request. + /// + /// The request in string format. + public string Request + { + get + { + return commandArray[0]; + } + } + + /// + /// Gets the request. + /// The return value is typed with if the value is not + /// reconise the value is sent. The string value can be get by + /// + /// The request. + public RequestType RequestTyped + { + get + { + return ParseRequest(commandArray[0]); + } + set + { + if (Enum.IsDefined(typeof(RequestType), value)) + commandArray[0] = value.ToString(); + else + commandArray[0] = RequestType.UNKNOWN.ToString(); + } + } + + private Uri _RtspUri; + /// + /// Gets or sets the Rtsp asked URI. + /// + /// The Rtsp asked URI. + /// The request with uri * is return with null URI + public Uri RtspUri + { + get + { + if (commandArray.Length < 2 || commandArray[1]=="*") + return null; + if (_RtspUri == null) + Uri.TryCreate(commandArray[1], UriKind.Absolute, out _RtspUri); + return _RtspUri; + } + set + { + _RtspUri = value; + if (commandArray.Length < 2) + { + Array.Resize(ref commandArray, 3); + } + commandArray[1] = (value != null ? value.ToString().TrimEnd('/') : "*"); + } + } + + /// + /// Gets the assiociate OK response with the request. + /// + /// an Rtsp response correcponding to request. + public virtual RtspResponse CreateResponse() + { + RtspResponse returnValue = new RtspResponse(); + returnValue.ReturnCode = 200; + returnValue.CSeq = this.CSeq; + if (this.Headers.ContainsKey(RtspHeaderNames.Session)) + { + returnValue.Headers[RtspHeaderNames.Session] = this.Headers[RtspHeaderNames.Session]; + } + + return returnValue; + } + + public Object ContextData { get; set; } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestAnnounce.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestAnnounce.cs new file mode 100644 index 0000000..0983c22 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestAnnounce.cs @@ -0,0 +1,12 @@ +namespace Inspectron.HawkEye.RTSP.Messages +{ + public class RtspRequestAnnounce : RtspRequest + { + // constructor + + public RtspRequestAnnounce() + { + Command = "ANNOUNCE * RTSP/1.0"; + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestDescribe.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestDescribe.cs new file mode 100644 index 0000000..0c6a779 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestDescribe.cs @@ -0,0 +1,13 @@ +namespace Inspectron.HawkEye.RTSP.Messages +{ + public class RtspRequestDescribe : RtspRequest + { + + // constructor + + public RtspRequestDescribe() + { + Command = "DESCRIBE * RTSP/1.0"; + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestGetParameter.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestGetParameter.cs new file mode 100644 index 0000000..3702362 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestGetParameter.cs @@ -0,0 +1,12 @@ +namespace Inspectron.HawkEye.RTSP.Messages +{ + public class RtspRequestGetParameter : RtspRequest + { + + // Constructor + public RtspRequestGetParameter() + { + Command = "GET_PARAMETER * RTSP/1.0"; + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestOptions.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestOptions.cs new file mode 100644 index 0000000..25e7422 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestOptions.cs @@ -0,0 +1,28 @@ +namespace Inspectron.HawkEye.RTSP.Messages +{ + public class RtspRequestOptions : RtspRequest + { + + // Constructor + public RtspRequestOptions() + { + Command = "OPTIONS * RTSP/1.0"; + } + + /// + /// Gets the assiociate OK response with the request. + /// + /// + /// an Rtsp response corresponding to request. + /// + public override RtspResponse CreateResponse() + { + RtspResponse response = base.CreateResponse(); + // Add genric suported operations. + response.Headers.Add(RtspHeaderNames.Public, "OPTIONS,DESCRIBE,ANNOUNCE,SETUP,PLAY,PAUSE,TEARDOWN,GET_PARAMETER,SET_PARAMETER,REDIRECT"); + + return response; + } + + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestPause.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestPause.cs new file mode 100644 index 0000000..6100ad7 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestPause.cs @@ -0,0 +1,12 @@ +namespace Inspectron.HawkEye.RTSP.Messages +{ + public class RtspRequestPause : RtspRequest + { + + // Constructor + public RtspRequestPause() + { + Command = "PAUSE * RTSP/1.0"; + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestPlay.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestPlay.cs new file mode 100644 index 0000000..a8699dd --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestPlay.cs @@ -0,0 +1,12 @@ +namespace Inspectron.HawkEye.RTSP.Messages +{ + public class RtspRequestPlay : RtspRequest + { + + // Constructor + public RtspRequestPlay() + { + Command = "PLAY * RTSP/1.0"; + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestRecord.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestRecord.cs new file mode 100644 index 0000000..2363b9e --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestRecord.cs @@ -0,0 +1,10 @@ +namespace Inspectron.HawkEye.RTSP.Messages +{ + public class RtspRequestRecord : RtspRequest + { + public RtspRequestRecord() + { + Command = "RECORD * RTSP/1.0"; + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestSetup.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestSetup.cs new file mode 100644 index 0000000..40ba4c7 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestSetup.cs @@ -0,0 +1,43 @@ +using System; + +namespace Inspectron.HawkEye.RTSP.Messages +{ + public class RtspRequestSetup : RtspRequest + { + + // Constructor + public RtspRequestSetup() + { + Command = "SETUP * RTSP/1.0"; + } + + + /// + /// Gets the transports associate with the request. + /// + /// The transport. + public RtspTransport[] GetTransports() + { + + if (!Headers.ContainsKey(RtspHeaderNames.Transport)) + return new RtspTransport[] { new RtspTransport() }; + + string[] items = Headers[RtspHeaderNames.Transport].Split(','); + return Array.ConvertAll(items, + new Converter(RtspTransport.Parse)); + + } + + public void AddTransport(RtspTransport newTransport) + { + string actualTransport = string.Empty; + if(Headers.ContainsKey(RtspHeaderNames.Transport)) + actualTransport = Headers[RtspHeaderNames.Transport] + ","; + Headers[RtspHeaderNames.Transport] = actualTransport + newTransport.ToString(); + + + + } + + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestTeardown.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestTeardown.cs new file mode 100644 index 0000000..940743a --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPRequestTeardown.cs @@ -0,0 +1,12 @@ +namespace Inspectron.HawkEye.RTSP.Messages +{ + public class RtspRequestTeardown : RtspRequest + { + + // Constructor + public RtspRequestTeardown() + { + Command = "TEARDOWN * RTSP/1.0"; + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPResponse.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPResponse.cs new file mode 100644 index 0000000..b54d354 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPResponse.cs @@ -0,0 +1,228 @@ +using System; +using System.Globalization; +using System.Linq; + +namespace Inspectron.HawkEye.RTSP.Messages +{ + public class RtspResponse : RtspMessage + { + public const int DEFAULT_TIMEOUT = 60; + + /// + /// Gets the default error message for an error code. + /// + /// An error code. + /// The default error message associate + private static string GetDefaultError(int aErrorCode) + { + switch (aErrorCode) + { + + case 100: return "Continue"; + + case 200: return "OK"; + case 201: return "Created"; + case 250: return "Low on Storage Space"; + + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Moved Temporarily"; + case 303: return "See Other"; + case 305: return "Use Proxy"; + + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Timeout"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Request Entity Too Large"; + case 414: return "Request-URI Too Long"; + case 415: return "Unsupported Media Type"; + case 451: return "Invalid parameter"; + case 452: return "Illegal Conference Identifier"; + case 453: return "Not Enough Bandwidth"; + case 454: return "Session Not Found"; + case 455: return "Method Not Valid In This State"; + case 456: return "Header Field Not Valid"; + case 457: return "Invalid Range"; + case 458: return "Parameter Is Read-Only"; + case 459: return "Aggregate Operation Not Allowed"; + case 460: return "Only Aggregate Operation Allowed"; + case 461: return "Unsupported Transport"; + case 462: return "Destination Unreachable"; + + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "RTSP Version Not Supported"; + case 551: return "Option not support"; + default: + return "Return: " + aErrorCode.ToString(CultureInfo.InvariantCulture); + } + } + + /// + /// Initializes a new instance of the class. + /// + public RtspResponse() + : base() + { + // Initialise with a default result code. + Command = "RTSP/1.0 200 OK"; + } + + private int _returnCode; + /// + /// Gets or sets the return code of the response. + /// + /// The return code. + /// On change the error message is set to the default one associate with the code + public int ReturnCode + { + get + { + if (_returnCode == 0 && commandArray.Length >= 2) + { + int.TryParse(commandArray[1], out _returnCode); + } + + return _returnCode; + } + set + { + if (ReturnCode != value) + { + _returnCode = value; + // make sure we have the room + if (commandArray.Length < 3) + { + Array.Resize(ref commandArray, 3); + } + commandArray[1] = value.ToString(CultureInfo.InvariantCulture); + commandArray[2] = GetDefaultError(value); + } + } + } + + /// + /// Gets or sets the error/return message. + /// + /// The return message. + public string ReturnMessage + { + get + { + if (commandArray.Length < 3) + return String.Empty; + return commandArray[2]; + } + set + { + // Make sure we have the room + if (commandArray.Length < 3) + { + Array.Resize(ref commandArray, 3); + } + commandArray[2] = value; + + } + } + + /// + /// Gets a value indicating whether this instance correspond to an OK response. + /// + /// true if this instance is OK; otherwise, false. + public bool IsOk + { + get + { + if (ReturnCode > 0 && ReturnCode < 400) + return true; + return false; + } + } + + /// + /// Gets the timeout in second. + /// The default timeout is 60. + /// + /// The timeout. + public int Timeout + { + get + { + int returnValue = DEFAULT_TIMEOUT; + if (Headers.ContainsKey(RtspHeaderNames.Session)) + { + string[] parts = Headers[RtspHeaderNames.Session].Split(';'); + if (parts.Length > 1) + { + string[] subParts = parts[1].Split('='); + if (subParts.Length > 1 && + subParts[0].ToUpperInvariant() == "TIMEOUT") + if (!int.TryParse(subParts[1], out returnValue)) + returnValue = DEFAULT_TIMEOUT; + } + } + return returnValue; + } + set + { + if(Headers.ContainsKey(RtspHeaderNames.Session)) + if (value != DEFAULT_TIMEOUT) + { + + Headers[RtspHeaderNames.Session] = Headers[RtspHeaderNames.Session].Split(';').First() + + ";timeout=" + value.ToString(CultureInfo.InvariantCulture); + } + else + { + //remove timeout part + Headers[RtspHeaderNames.Session] = Headers[RtspHeaderNames.Session].Split(';').First(); + } + } + } + + /// + /// Gets the session ID. + /// + /// The session ID. + public override string Session + { + get + { + if (!Headers.ContainsKey(RtspHeaderNames.Session)) + return null; + + return Headers[RtspHeaderNames.Session].Split(';')[0]; + } + set + { + if(Timeout != DEFAULT_TIMEOUT) + { + Headers[RtspHeaderNames.Session] = value + ";timeout=" + Timeout.ToString(CultureInfo.InvariantCulture); + } + else + { + Headers[RtspHeaderNames.Session] = value; + } + } + } + + /// + /// Gets or sets the original request associate with the response. + /// + /// The original request. + public RtspRequest OriginalRequest + { get; set; } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Messages/RTSPTransport.cs b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPTransport.cs new file mode 100644 index 0000000..a758ce8 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Messages/RTSPTransport.cs @@ -0,0 +1,367 @@ +using System; +using System.Diagnostics.Contracts; +using System.Text; + +namespace Inspectron.HawkEye.RTSP.Messages +{ + public class RtspTransport + { + public RtspTransport() + { + // Default value is true in RFC + IsMulticast = true; + LowerTransport = LowerTransportType.UDP; + Mode = "PLAY"; + } + /* +RFC +Transport = "Transport" ":" + 1\#transport-spec +transport-spec = transport-protocol/profile[/lower-transport] + *parameter +transport-protocol = "RTP" +profile = "AVP" +lower-transport = "TCP" | "UDP" +parameter = ( "unicast" | "multicast" ) + | ";" "destination" [ "=" address ] + | ";" "interleaved" "=" channel [ "-" channel ] + | ";" "append" + | ";" "ttl" "=" ttl + | ";" "layers" "=" 1*DIGIT + | ";" "port" "=" port [ "-" port ] + | ";" "client_port" "=" port [ "-" port ] + | ";" "server_port" "=" port [ "-" port ] + | ";" "ssrc" "=" ssrc + | ";" "mode" = <"> 1\#mode <"> +ttl = 1*3(DIGIT) +port = 1*5(DIGIT) +ssrc = 8*8(HEX) +channel = 1*3(DIGIT) +address = host +mode = <"> *Method <"> | Method + +*/ + /// + /// List of transport + /// + [Serializable] + public enum TransportType + { + /// + /// RTP for now + /// + RTP, + } + + /// + /// Profile type + /// + [Serializable] + public enum ProfileType + { + /// + /// RTP/AVP of now + /// + AVP, + } + + /// + /// Transport type. + /// + [Serializable] + public enum LowerTransportType + { + /// + /// UDP transport. + /// + UDP, + /// + /// TCP transport. + /// + TCP, + } + + + /// + /// Gets or sets the transport. + /// + /// The transport. + public TransportType Transport { get; set; } + /// + /// Gets or sets the profile. + /// + /// The profile. + public ProfileType Profile { get; set; } + /// + /// Gets or sets the lower transport. + /// + /// The lower transport. + public LowerTransportType LowerTransport { get; set; } + /// + /// Gets or sets a value indicating whether this instance is multicast. + /// + /// + /// true if this instance is multicast; otherwise, false. + /// + public bool IsMulticast { get; set; } + /// + /// Gets or sets the destination. + /// + /// The destination. + public string Destination { get; set; } + /// + /// Gets or sets the source. + /// + /// The source. + public string Source { get; set; } + /// + /// Gets or sets the interleaved. + /// + /// The interleaved. + public PortCouple Interleaved { get; set; } + /// + /// Gets or sets a value indicating whether this instance is append. + /// + /// true if this instance is append; otherwise, false. + public bool IsAppend { get; set; } + /// + /// Gets or sets the TTL. + /// + /// The TTL. + public int TTL { get; set; } + /// + /// Gets or sets the layers. + /// + /// The layers. + public int Layers { get; set; } + /// + /// Gets or sets the port. + /// + /// The port. + public PortCouple Port { get; set; } + /// + /// Gets or sets the client port. + /// + /// The client port. + public PortCouple ClientPort { get; set; } + /// + /// Gets or sets the server port. + /// + /// The server port. + public PortCouple ServerPort { get; set; } + /// + /// Gets or sets the S SRC. + /// + /// The S SRC. + public string SSrc { get; set; } + /// + /// Gets or sets the mode. + /// + /// The mode. + public string Mode { get; set; } + + /// + /// Parses the specified transport string. + /// + /// A transport string. + /// The transport class. + /// is null. + public static RtspTransport Parse(string aTransportString) + { + if (aTransportString == null) + throw new ArgumentNullException("aTransportString"); + Contract.EndContractBlock(); + + RtspTransport returnValue = new RtspTransport(); + + string[] transportPart = aTransportString.Split(';'); + string[] transportProtocolPart = transportPart[0].Split('/'); + + ReadTransport(returnValue, transportProtocolPart); + ReadProfile(returnValue, transportProtocolPart); + ReadLowerTransport(returnValue, transportProtocolPart); + + foreach (string part in transportPart) + { + string[] subPart = part.Split('='); + + switch (subPart[0].ToUpperInvariant()) + { + case "UNICAST": + returnValue.IsMulticast = false; + break; + case "MULTICAST": + returnValue.IsMulticast = true; + break; + case "DESTINATION": + if (subPart.Length == 2) + returnValue.Destination = subPart[1]; + break; + case "SOURCE": + if (subPart.Length == 2) + returnValue.Source = subPart[1]; + break; + case "INTERLEAVED": + returnValue.IsMulticast = false; + if (subPart.Length < 2) + throw new ArgumentException("interleaved value invalid", "aTransportString"); + + returnValue.Interleaved = PortCouple.Parse(subPart[1]); + break; + case "APPEND": + returnValue.IsAppend = true; + break; + case "TTL": + int ttl = 0; + if (subPart.Length < 2 || !int.TryParse(subPart[1], out ttl)) + throw new ArgumentException("TTL value invalid", "aTransportString"); + returnValue.TTL = ttl; + break; + case "LAYERS": + int layers = 0; + if (subPart.Length < 2 || !int.TryParse(subPart[1], out layers)) + throw new ArgumentException("Layers value invalid", "aTransportString"); + returnValue.TTL = layers; + break; + case "PORT": + if (subPart.Length < 2) + throw new ArgumentException("Port value invalid", "aTransportString"); + returnValue.Port = PortCouple.Parse(subPart[1]); + break; + case "CLIENT_PORT": + if (subPart.Length < 2) + throw new ArgumentException("client_port value invalid", "aTransportString"); + returnValue.ClientPort = PortCouple.Parse(subPart[1]); + break; + case "SERVER_PORT": + if (subPart.Length < 2) + throw new ArgumentException("server_port value invalid", "aTransportString"); + returnValue.ServerPort = PortCouple.Parse(subPart[1]); + break; + case "SSRC": + if (subPart.Length < 2) + throw new ArgumentException("ssrc value invalid", "aTransportString"); + returnValue.SSrc = subPart[1]; + break; + case "MODE": + if (subPart.Length < 2) + throw new ArgumentException("mode value invalid", "aTransportString"); + returnValue.Mode = subPart[1]; + break; + default: + // TODO log invalid part + break; + } + } + return returnValue; + } + + private static void ReadLowerTransport(RtspTransport returnValue, string[] transportProtocolPart) + { + if (transportProtocolPart.Length == 3) + { + LowerTransportType lowerTransport; + if (!Enum.TryParse(transportProtocolPart[2], out lowerTransport)) + throw new ArgumentException("Lower transport type invalid", "aTransportString"); + returnValue.LowerTransport = lowerTransport; + } + } + + private static void ReadProfile(RtspTransport returnValue, string[] transportProtocolPart) + { + ProfileType profile; + if (transportProtocolPart.Length < 2 || !Enum.TryParse(transportProtocolPart[1], out profile)) + throw new ArgumentException("Transport profile type invalid", "aTransportString"); + returnValue.Profile = profile; + } + + private static void ReadTransport(RtspTransport returnValue, string[] transportProtocolPart) + { + TransportType transport; + if (!Enum.TryParse(transportProtocolPart[0], out transport)) + throw new ArgumentException("Transport type invalid", "aTransportString"); + returnValue.Transport = transport; + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + StringBuilder transportString = new StringBuilder(); + transportString.Append(Transport.ToString()); + transportString.Append('/'); + transportString.Append(Profile.ToString()); + transportString.Append('/'); + transportString.Append(LowerTransport.ToString()); + if (LowerTransport == LowerTransportType.TCP) + { + transportString.Append(";unicast"); + } + if (LowerTransport == LowerTransportType.UDP) + { + transportString.Append(';'); + transportString.Append(IsMulticast ? "multicast" : "unicast"); + } + if (Destination != null) + { + transportString.Append(";destination="); + transportString.Append(Destination); + } + if (Source != null) + { + transportString.Append(";source="); + transportString.Append(Source); + } + if (Interleaved != null) + { + transportString.Append(";interleaved="); + transportString.Append(Interleaved.ToString()); + } + if (IsAppend) + { + transportString.Append(";append"); + } + if (TTL > 0) + { + transportString.Append(";ttl="); + transportString.Append(TTL); + } + if (Layers > 0) + { + transportString.Append(";layers="); + transportString.Append(Layers); + } + if (Port != null) + { + transportString.Append(";port="); + transportString.Append(Port.ToString()); + } + if (ClientPort != null) + { + transportString.Append(";client_port="); + transportString.Append(ClientPort.ToString()); + } + if (ServerPort != null) + { + transportString.Append(";server_port="); + transportString.Append(ServerPort.ToString()); + } + if (SSrc != null) + { + transportString.Append(";ssrc="); + transportString.Append(SSrc); + } + if (Mode != null && Mode != "PLAY") + { + transportString.Append(";mode="); + transportString.Append(Mode); + } + return transportString.ToString(); + } + + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/RTSPListener.cs b/framework/Inspectron.HawkEye/RTSP/RTSPListener.cs new file mode 100644 index 0000000..0dc1bb7 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/RTSPListener.cs @@ -0,0 +1,557 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.IO; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using Inspectron.HawkEye.RTSP.Messages; + +namespace Inspectron.HawkEye.RTSP +{ + /// + /// Rtsp lister + /// + public class RtspListener : IDisposable + { + private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); + + private IRtspTransport _transport; + + private Thread _listenTread; + private Stream _stream; + + private int _sequenceNumber; + + private Dictionary _sentMessage = new Dictionary(); + + /// + /// Initializes a new instance of the class from a TCP connection. + /// + /// The connection. + public RtspListener(IRtspTransport connection) + { + if (connection == null) + throw new ArgumentNullException("connection"); + Contract.EndContractBlock(); + + _transport = connection; + _stream = connection.GetStream(); + } + + /// + /// Gets the remote address. + /// + /// The remote adress. + public string RemoteAdress + { + get + { + return _transport.RemoteAddress; + } + } + + /// + /// Starts this instance. + /// + public void Start() + { + _listenTread = new Thread(new ThreadStart(DoJob)); + _listenTread.Name = "DoJob"; + _listenTread.Start(); + } + + /// + /// Stops this instance. + /// + public void Stop() + { + // brutally close the TCP socket.... + // I hope the teardown was sent elsewhere + _transport.Close(); + + } + + /// + /// Enable auto reconnect. + /// + public bool AutoReconnect { get; set; } + + /// + /// Occurs when message is received. + /// + public event EventHandler MessageReceived; + + /// + /// Raises the event. + /// + /// The instance containing the event data. + protected void OnMessageReceived(RtspChunkEventArgs e) + { + EventHandler handler = MessageReceived; + + if (handler != null) + handler(this, e); + } + + /// + /// Occurs when Data is received. + /// + public event EventHandler DataReceived; + + /// + /// Raises the event. + /// + /// The instance containing the event data. + protected void OnDataReceived(RtspChunkEventArgs rtspChunkEventArgs) + { + EventHandler handler = DataReceived; + + if (handler != null) + handler(this, rtspChunkEventArgs); + } + + /// + /// Does the reading job. + /// + /// + /// This method read one message from TCP connection. + /// If it a response it add the associate question. + /// The stopping is made by the closing of the TCP connection. + /// + private void DoJob() + { + try + { + _logger.Debug("Connection Open"); + while (_transport.Connected) + { + // La lectuer est blocking sauf si la connection est coupé + RtspChunk currentMessage = ReadOneMessage(_stream); + + if (currentMessage != null) + { + if (!(currentMessage is RtspData)) + { + // on logue le tout + if (currentMessage.SourcePort != null) + _logger.Debug(CultureInfo.InvariantCulture, "Receive from {0}", currentMessage.SourcePort.RemoteAdress); + currentMessage.LogMessage(); + } + if (currentMessage is RtspResponse) + { + + RtspResponse response = currentMessage as RtspResponse; + lock (_sentMessage) + { + // add the original question to the response. + RtspRequest originalRequest; + if (_sentMessage.TryGetValue(response.CSeq, out originalRequest)) + { + _sentMessage.Remove(response.CSeq); + response.OriginalRequest = originalRequest; + } + else + { + _logger.Warn(CultureInfo.InvariantCulture, "Receive response not asked {0}", response.CSeq); + } + } + OnMessageReceived(new RtspChunkEventArgs(response)); + + } + else if (currentMessage is RtspRequest) + { + OnMessageReceived(new RtspChunkEventArgs(currentMessage)); + } + else if (currentMessage is RtspData) + { + OnDataReceived(new RtspChunkEventArgs(currentMessage)); + } + + } + else + { + _stream.Close(); + _transport.Close(); + } + } + } + catch (IOException error) + { + _logger.Warn("IO Error", error); + _stream.Close(); + _transport.Close(); + } + catch (SocketException error) + { + _logger.Warn("Socket Error", error); + _stream.Close(); + _transport.Close(); + } + catch (ObjectDisposedException error) + { + _logger.Warn("Object Disposed", error); + } + catch (Exception error) + { + _logger.Warn("Unknow Error", error); +// throw; + } + + _logger.Debug("Connection Close"); + } + + [Serializable] + private enum ReadingState + { + NewCommand, + Headers, + Data, + End, + InterleavedData, + MoreInterleavedData, + } + + /// + /// Sends the message. + /// + /// A message. + /// if it is Ok, otherwise + public bool SendMessage(RtspMessage message) + { + if (message == null) + throw new ArgumentNullException("message"); + Contract.EndContractBlock(); + + if (!_transport.Connected) + { + if(!AutoReconnect) + return false; + + _logger.Warn("Reconnect to a client, strange !!"); + try + { + Reconnect(); + } + catch (SocketException) + { + // on a pas put se connecter on dit au manager de plus compter sur nous + return false; + } + } + + // if it it a request we store the original message + // and we renumber it. + //TODO handle lost message (for example every minute cleanup old message) + if (message is RtspRequest) + { + RtspMessage originalMessage = message; + // Do not modify original message + message = message.Clone() as RtspMessage; + _sequenceNumber++; + message.CSeq = _sequenceNumber; + lock (_sentMessage) + { + _sentMessage.Add(message.CSeq, originalMessage as RtspRequest); + } + } + + _logger.Debug("Send Message"); + message.LogMessage(); + message.SendTo(_stream); + return true; + } + + /// + /// Reconnect this instance of RtspListener. + /// + /// Error during socket + public void Reconnect() + { + //if it is already connected do not reconnect + if (_transport.Connected) + return; + + // If it is not connected listenthread should have die. + if (_listenTread != null && _listenTread.IsAlive) + _listenTread.Join(); + + if (_stream != null) + _stream.Dispose(); + + // reconnect + _transport.Reconnect(); + _stream = _transport.GetStream(); + + // If listen thread exist restart it + if (_listenTread != null) + Start(); + } + + /// + /// Reads one message. + /// + /// The Rtsp stream. + /// Message readen + public RtspChunk ReadOneMessage(Stream commandStream) + { + if (commandStream == null) + throw new ArgumentNullException("commandStream"); + Contract.EndContractBlock(); + + ReadingState currentReadingState = ReadingState.NewCommand; + // current decode message , create a fake new to permit compile. + RtspChunk currentMessage = null; + + int size = 0; + int byteReaden = 0; + List buffer = new List(256); + string oneLine = String.Empty; + while (currentReadingState != ReadingState.End) + { + + // if the system is not reading binary data. + if (currentReadingState != ReadingState.Data && currentReadingState != ReadingState.MoreInterleavedData) + { + oneLine = String.Empty; + bool needMoreChar = true; + // I do not know to make readline blocking + while (needMoreChar) + { + int currentByte = commandStream.ReadByte(); + + switch (currentByte) + { + case -1: + // the read is blocking, so if we got -1 it is because the client close; + currentReadingState = ReadingState.End; + needMoreChar = false; + break; + case '\n': + oneLine = ASCIIEncoding.UTF8.GetString(buffer.ToArray()); + buffer.Clear(); + needMoreChar = false; + break; + case '\r': + // simply ignore this + break; + case '$': // if first caracter of packet is $ it is an interleaved data packet + if (currentReadingState == ReadingState.NewCommand && buffer.Count == 0) + { + currentReadingState = ReadingState.InterleavedData; + needMoreChar = false; + } + else + goto default; + break; + default: + buffer.Add((byte)currentByte); + break; + } + } + } + + switch (currentReadingState) + { + case ReadingState.NewCommand: + currentMessage = RtspMessage.GetRtspMessage(oneLine); + currentReadingState = ReadingState.Headers; + break; + case ReadingState.Headers: + string line = oneLine; + if (string.IsNullOrEmpty(line)) + { + currentReadingState = ReadingState.Data; + ((RtspMessage)currentMessage).InitialiseDataFromContentLength(); + } + else + { + ((RtspMessage)currentMessage).AddHeader(line); + } + break; + case ReadingState.Data: + if (currentMessage.Data.Length > 0) + { + // Read the remaning data + int byteCount = commandStream.Read(currentMessage.Data, byteReaden, + currentMessage.Data.Length - byteReaden); + if (byteCount <= 0) { + currentReadingState = ReadingState.End; + break; + } + byteReaden += byteCount; + _logger.Debug(CultureInfo.InvariantCulture, "Readen {0} byte of data", byteReaden); + } + // if we haven't read all go there again else go to end. + if (byteReaden >= currentMessage.Data.Length) + currentReadingState = ReadingState.End; + break; + case ReadingState.InterleavedData: + currentMessage = new RtspData(); + int channelByte = commandStream.ReadByte(); + if (channelByte == -1) { + currentReadingState = ReadingState.End; + break; + } + ((RtspData)currentMessage).Channel = channelByte; + + int sizeByte1 = commandStream.ReadByte(); + if (sizeByte1 == -1) { + currentReadingState = ReadingState.End; + break; + } + int sizeByte2 = commandStream.ReadByte(); + if (sizeByte2 == -1) { + currentReadingState = ReadingState.End; + break; + } + size = (sizeByte1 << 8) + sizeByte2; + currentMessage.Data = new byte[size]; + currentReadingState = ReadingState.MoreInterleavedData; + break; + case ReadingState.MoreInterleavedData: + // apparently non blocking + { + int byteCount = commandStream.Read(currentMessage.Data, byteReaden, size - byteReaden); + if (byteCount <= 0) { + currentReadingState = ReadingState.End; + break; + } + byteReaden += byteCount; + if (byteReaden < size) + currentReadingState = ReadingState.MoreInterleavedData; + else + currentReadingState = ReadingState.End; + break; + } + default: + break; + } + } + if (currentMessage != null) + currentMessage.SourcePort = this; + return currentMessage; + } + + /// + /// Begins the send data. + /// + /// A Rtsp data. + /// The async callback. + /// A state. + public IAsyncResult BeginSendData(RtspData aRtspData, AsyncCallback asyncCallback, object state) + { + if (aRtspData == null) + throw new ArgumentNullException("aRtspData"); + Contract.EndContractBlock(); + + return BeginSendData(aRtspData.Channel, aRtspData.Data, asyncCallback, state); + } + + /// + /// Begins the send data. + /// + /// The channel. + /// The frame. + /// The async callback. + /// A state. + public IAsyncResult BeginSendData(int channel, byte[] frame, AsyncCallback asyncCallback, object state) + { + if (frame == null) + throw new ArgumentNullException("frame"); + if (frame.Length > 0xFFFF) + throw new ArgumentException("frame too large", "frame"); + Contract.EndContractBlock(); + + if (!_transport.Connected) + { + if(!AutoReconnect) + return null; // cannot write when transport is disconnected + + _logger.Warn("Reconnect to a client, strange !!"); + Reconnect(); + } + + byte[] data = new byte[4 + frame.Length]; // add 4 bytes for the header + data[0] = 36; // '$' character + data[1] = (byte)channel; + data[2] = (byte)((frame.Length & 0xFF00) >> 8); + data[3] = (byte)((frame.Length & 0x00FF)); + System.Array.Copy(frame,0,data,4,frame.Length); + return _stream.BeginWrite(data, 0, data.Length, asyncCallback, state); + } + + /// + /// Ends the send data. + /// + /// The result. + public void EndSendData(IAsyncResult result) + { + try + { + _stream.EndWrite(result); + } catch (Exception e) + { + // Error, for example stream has already been Disposed + _logger.Debug("Error during end send (can be ignored) " + e); + result = null; + } + } + + /// + /// Send data (Synchronous) + /// + /// The channel. + /// The frame. + public void SendData(int channel, byte[] frame) + { + if (frame == null) + throw new ArgumentNullException("frame"); + if (frame.Length > 0xFFFF) + throw new ArgumentException("frame too large", "frame"); + Contract.EndContractBlock(); + + if (!_transport.Connected) + { + if(!AutoReconnect) + throw new Exception("Connection is lost"); + + _logger.Warn("Reconnect to a client, strange !!"); + Reconnect(); + } + + byte[] data = new byte[4 + frame.Length]; // add 4 bytes for the header + data[0] = 36; // '$' character + data[1] = (byte)channel; + data[2] = (byte)((frame.Length & 0xFF00) >> 8); + data[3] = (byte)((frame.Length & 0x00FF)); + System.Array.Copy(frame, 0, data, 4, frame.Length); + lock (_stream) { + _stream.Write(data, 0, data.Length); + } + } + + + #region IDisposable Membres + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Stop(); + if (_stream != null) + _stream.Dispose(); + + } + } + + #endregion + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/RTSPMessageEventArgs.cs b/framework/Inspectron.HawkEye/RTSP/RTSPMessageEventArgs.cs new file mode 100644 index 0000000..ffdeb6f --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/RTSPMessageEventArgs.cs @@ -0,0 +1,27 @@ +using System; +using Inspectron.HawkEye.RTSP.Messages; + +namespace Inspectron.HawkEye.RTSP +{ + /// + /// Event args containing information for message events. + /// + public class RtspChunkEventArgs :EventArgs + { + + /// + /// Initializes a new instance of the class. + /// + /// A message. + public RtspChunkEventArgs(RtspChunk aMessage) + { + Message = aMessage; + } + + /// + /// Gets or sets the message. + /// + /// The message. + public RtspChunk Message { get; set; } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/RTSPTCPTransport.cs b/framework/Inspectron.HawkEye/RTSP/RTSPTCPTransport.cs new file mode 100644 index 0000000..4296dfe --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/RTSPTCPTransport.cs @@ -0,0 +1,118 @@ +using System; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.Sockets; + +namespace Inspectron.HawkEye.RTSP +{ + /// + /// TCP Connection for Rtsp + /// + public class RtspTcpTransport : IRtspTransport, IDisposable + { + private IPEndPoint _currentEndPoint; + private TcpClient _RtspServerClient; + + /// + /// Initializes a new instance of the class. + /// + /// The underlying TCP connection. + public RtspTcpTransport(TcpClient tcpConnection) + { + if (tcpConnection == null) + throw new ArgumentNullException("tcpConnection"); + Contract.EndContractBlock(); + + _currentEndPoint = (IPEndPoint)tcpConnection.Client.RemoteEndPoint; + _RtspServerClient = tcpConnection; + } + + /// + /// Initializes a new instance of the class. + /// + /// A host. + /// A port number. + public RtspTcpTransport(string aHost, int aPortNumber) + : this(new TcpClient(aHost, aPortNumber)) + { + } + + + #region IRtspTransport Membres + + /// + /// Gets the stream of the transport. + /// + /// A stream + public Stream GetStream() + { + return _RtspServerClient.GetStream(); + } + + /// + /// Gets the remote address. + /// + /// The remote address. + public string RemoteAddress + { + get + { + return string.Format(CultureInfo.InvariantCulture,"{0}:{1}", _currentEndPoint.Address, _currentEndPoint.Port); + } + } + + /// + /// Closes this instance. + /// + public void Close() + { + Dispose(true); + } + + /// + /// Gets a value indicating whether this is connected. + /// + /// true if connected; otherwise, false. + public bool Connected + { + get { return _RtspServerClient.Client != null && _RtspServerClient.Connected; } + } + + /// + /// Reconnect this instance. + /// Must do nothing if already connected. + /// + /// Error during socket + public void Reconnect() + { + if (Connected) + return; + _RtspServerClient = new TcpClient(); + _RtspServerClient.Connect(_currentEndPoint); + } + + #endregion + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _RtspServerClient.Close(); + /* // free managed resources + if (managedResource != null) + { + managedResource.Dispose(); + managedResource = null; + }*/ + } + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/RTSPUtils.cs b/framework/Inspectron.HawkEye/RTSP/RTSPUtils.cs new file mode 100644 index 0000000..34650e2 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/RTSPUtils.cs @@ -0,0 +1,16 @@ +using System; + +namespace Inspectron.HawkEye.RTSP +{ + public static class RtspUtils + { + /// + /// Registers the URI. + /// + public static void RegisterUri() + { + if (!UriParser.IsKnownScheme("rtsp")) + UriParser.Register(new HttpStyleUriParser(), "rtsp", 554); + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/Attribut.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/Attribut.cs new file mode 100644 index 0000000..2b39cc2 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Sdp/Attribut.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; + +namespace Inspectron.HawkEye.RTSP.Sdp +{ + public class Attribut + { + private static readonly Dictionary attributMap = new Dictionary() + { + {AttributRtpMap.NAME,typeof(AttributRtpMap)}, + {AttributFmtp.NAME,typeof(AttributFmtp)}, + }; + + + public virtual string Key { get; private set; } + public virtual string Value { get; protected set; } + + public static void RegisterNewAttributeType(string key, Type attributType) + { + if(!attributType.IsSubclassOf(typeof(Attribut))) + throw new ArgumentException("Type must be subclass of Rtsp.Sdp.Attribut","attributType"); + + attributMap[key] = attributType; + } + + + + public Attribut() + { + } + + public Attribut(string key) + { + Key = key; + } + + + public static Attribut ParseInvariant(string value) + { + if(value == null) + throw new ArgumentNullException("value"); + + Contract.EndContractBlock(); + + var listValues = value.Split(new char[] {':'}, 2); + + + Attribut returnValue; + + // Call parser of child type + Type childType; + attributMap.TryGetValue(listValues[0], out childType); + if (childType != null) + { + var defaultContructor = childType.GetConstructor(Type.EmptyTypes); + returnValue = defaultContructor.Invoke(Type.EmptyTypes) as Attribut; + } + else + { + returnValue = new Attribut(listValues[0]); + } + // Parse the value. Note most attributes have a value but recvonly does not have a value + if (listValues.Count() > 1) returnValue.ParseValue(listValues[1]); + + return returnValue; + } + + protected virtual void ParseValue(string value) + { + Value = value; + } + + + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/AttributFmtp.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/AttributFmtp.cs new file mode 100644 index 0000000..4af8384 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Sdp/AttributFmtp.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Inspectron.HawkEye.RTSP.Sdp +{ + public class AttributFmtp : Attribut + { + public const string NAME = "fmtp"; + + private Dictionary parameters = new Dictionary(); + + public AttributFmtp() + { + } + + public override string Key + { + get + { + return NAME; + } + } + + public override string Value + { + get + { + return string.Format("{0} {1}", PayloadNumber, FormatParameter); + } + protected set + { + ParseValue(value); + } + } + + public int PayloadNumber { get; set; } + + // temporary aatibute to store remaning data not parsed + public string FormatParameter { get; set; } + + + // Extract the Payload Number and the Format Parameters + protected override void ParseValue(string value) + { + var parts = value.Split(new char[] { ' ' }, 2); + + int payloadNumber; + if(int.TryParse(parts[0], out payloadNumber)) + { + this.PayloadNumber = payloadNumber; + } + if(parts.Length > 1) + { + FormatParameter = parts[1]; + + // Split on ';' to get a list of items. + // Then Trim each item and then Split on the first '=' + // Add them to the dictionary + parameters.Clear(); + foreach (var pair in parts[1].Split(';').Select(x => x.Trim().Split(new char[] { '=' }, 2))) { + if (!string.IsNullOrWhiteSpace(pair[0])) + parameters[pair[0]] = pair.Length > 1 ? pair[1] : null; + } + } + } + + public String GetParameter(String index) + { + if (parameters.ContainsKey(index)) return parameters[index]; + else return ""; + } + + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/AttributRtpMap.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/AttributRtpMap.cs new file mode 100644 index 0000000..845d851 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Sdp/AttributRtpMap.cs @@ -0,0 +1,76 @@ +using System; + +namespace Inspectron.HawkEye.RTSP.Sdp +{ + public class AttributRtpMap : Attribut + { + // Format + // rtpmap: / [/] + // Examples + // rtpmap:96 H264/90000 + // rtpmap:8 PCMA/8000 + + public const string NAME = "rtpmap"; + + public AttributRtpMap() + { + } + + public override string Key + { + get + { + return NAME; + } + } + + public override string Value + { + get + { + if(string.IsNullOrEmpty(EncodingParameters)) + { + return string.Format("{0} {1}/{2}", PayloadNumber, EncodingName, ClockRate); + } else { + return string.Format("{0} {1}/{2}/{3}", PayloadNumber, EncodingName, ClockRate, EncodingParameters); + } + } + protected set + { + ParseValue(value); + } + } + + public int PayloadNumber { get; set; } + public String EncodingName { get; set; } + public String ClockRate { get; set; } + public String EncodingParameters { get; set; } + + protected override void ParseValue(string value) + { + var parts = value.Split(new char[] { ' ', '/' }); + + if (parts.Length >= 1) { + int tmp_payloadNumber; + if (int.TryParse(parts[0], out tmp_payloadNumber)) + { + PayloadNumber = tmp_payloadNumber; + } + } + if (parts.Length >= 2) + { + EncodingName = parts[1]; + } + if (parts.Length >= 3) + { + ClockRate = parts[2]; + } + if (parts.Length >= 4) + { + EncodingParameters = parts[3]; + } + + + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/Bandwidth.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/Bandwidth.cs new file mode 100644 index 0000000..1675a7e --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Sdp/Bandwidth.cs @@ -0,0 +1,15 @@ +namespace Inspectron.HawkEye.RTSP.Sdp +{ + public class Bandwidth + { + public Bandwidth() + { + } + + internal static Bandwidth Parse(string value) + { + //TODO really parse. + return new Bandwidth(); + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/Connection.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/Connection.cs new file mode 100644 index 0000000..853699f --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Sdp/Connection.cs @@ -0,0 +1,48 @@ +using System; +using System.Globalization; + +namespace Inspectron.HawkEye.RTSP.Sdp +{ + public abstract class Connection + { + public Connection() + { + //Default value from spec + NumberOfAddress = 1; + } + + public string Host { get; set; } + + /// + /// Gets or sets the number of address specifed in connection. + /// + /// The number of address. + //TODO handle it a different way (list of adress ?) + public int NumberOfAddress { get; set; } + + public static Connection Parse(string value) + { + if(value ==null) + throw new ArgumentNullException("value"); + + string[] parts = value.Split(' '); + + if (parts.Length != 3) + throw new FormatException("Value do not contain 3 parts as needed."); + + if (parts[0] != "IN") + throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "Net type {0} not suported", parts[0])); + + switch (parts[1]) + { + case "IP4": + return ConnectionIP4.Parse(parts[2]); + case "IP6": + return ConnectionIP6.Parse(parts[2]); + default: + throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "Address type {0} not suported", parts[1])); + } + + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/ConnectionIP4.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/ConnectionIP4.cs new file mode 100644 index 0000000..47c237c --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Sdp/ConnectionIP4.cs @@ -0,0 +1,40 @@ +using System; +using System.Globalization; + +namespace Inspectron.HawkEye.RTSP.Sdp +{ + public class ConnectionIP4 : Connection + { + + public int Ttl { get; set; } + + internal new static ConnectionIP4 Parse(string ipAddress) + { + string[] parts = ipAddress.Split('/'); + + if (parts.Length > 3) + throw new FormatException("Too much address subpart in " + ipAddress); + + ConnectionIP4 result = new ConnectionIP4(); + + result.Host = parts[0]; + + int ttl; + if (parts.Length > 1) + { + if (!int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out ttl)) + throw new FormatException("Invalid TTL format : " + parts[1]); + result.Ttl = ttl; + } + int numberOfAddress; + if (parts.Length > 2) + { + if (!int.TryParse(parts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out numberOfAddress)) + throw new FormatException("Invalid number of address : " + parts[2]); + result.NumberOfAddress = numberOfAddress; + } + + return result; + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/ConnectionIP6.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/ConnectionIP6.cs new file mode 100644 index 0000000..bb09c77 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Sdp/ConnectionIP6.cs @@ -0,0 +1,31 @@ +using System; +using System.Globalization; + +namespace Inspectron.HawkEye.RTSP.Sdp +{ + public class ConnectionIP6 : Connection + { + internal new static ConnectionIP6 Parse(string ipAddress) + { + string[] parts = ipAddress.Split('/'); + + if (parts.Length > 2) + throw new FormatException("Too much address subpart in " + ipAddress); + + ConnectionIP6 result = new ConnectionIP6(); + + result.Host = parts[0]; + + int numberOfAddress; + if (parts.Length > 1) + { + if (!int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out numberOfAddress)) + throw new FormatException("Invalid number of address : " + parts[1]); + result.NumberOfAddress = numberOfAddress; + } + + return result; + + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/EncriptionKey.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/EncriptionKey.cs new file mode 100644 index 0000000..505a24e --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Sdp/EncriptionKey.cs @@ -0,0 +1,22 @@ +using System; +using System.Diagnostics.Contracts; + +namespace Inspectron.HawkEye.RTSP.Sdp +{ + public class EncriptionKey + { + public EncriptionKey(string p) + { + } + + public static EncriptionKey ParseInvariant(string value) + { + if (value == null) + throw new ArgumentNullException("value"); + + Contract.EndContractBlock(); + + throw new NotImplementedException(); + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/H264Parameter.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/H264Parameter.cs new file mode 100644 index 0000000..a2e9206 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Sdp/H264Parameter.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Inspectron.HawkEye.RTSP.Sdp +{ + public class H264Parameters : IDictionary + { + private readonly Dictionary parameters = new Dictionary(); + + public List SpropParameterSets + { + get + { + List result = new List(); + + if (ContainsKey("sprop-parameter-sets")&& this["sprop-parameter-sets"] != null) + { + result.AddRange(this["sprop-parameter-sets"].Split(',').Select(x => Convert.FromBase64String(x))); + } + + return result; + } + } + + public static H264Parameters Parse(String parameterString) + { + var result = new H264Parameters(); + foreach (var pair in parameterString.Split(';').Select(x => x.Trim().Split(new char[] { '=' }, 2))) + { + if(!string.IsNullOrWhiteSpace(pair[0])) + result[pair[0]] = pair.Length > 1 ? pair[1] : null; + } + return result; + } + + public override string ToString() + { + return parameters.Select(p => p.Key + (p.Value != null ? "=" + p.Value : string.Empty)).Aggregate((x, y) => x + ";" + y); + } + + public String this[String index] + { + get { return parameters[index]; } + set { parameters[index] = value; } + } + + public int Count + { + get + { + return parameters.Count; + } + } + + public bool IsReadOnly + { + get + { + return ((IDictionary)parameters).IsReadOnly; + } + } + + public ICollection Keys + { + get + { + return ((IDictionary)parameters).Keys; + } + } + + public ICollection Values + { + get + { + return ((IDictionary)parameters).Values; + } + } + + public void Add(KeyValuePair item) + { + ((IDictionary)parameters).Add(item); + } + + public void Add(string key, string value) + { + parameters.Add(key, value); + } + + public void Clear() + { + parameters.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return ((IDictionary)parameters).Contains(item); + } + + public bool ContainsKey(string key) + { + return parameters.ContainsKey(key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((IDictionary)parameters).CopyTo(array, arrayIndex); + } + + public IEnumerator> GetEnumerator() + { + return ((IDictionary)parameters).GetEnumerator(); + } + + public bool Remove(KeyValuePair item) + { + return ((IDictionary)parameters).Remove(item); + } + + public bool Remove(string key) + { + return parameters.Remove(key); + } + + public bool TryGetValue(string key, out string value) + { + return parameters.TryGetValue(key, out value); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IDictionary)parameters).GetEnumerator(); + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/H265Parameter.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/H265Parameter.cs new file mode 100644 index 0000000..23a993f --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Sdp/H265Parameter.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +// Parse 'fmtp' attribute in SDP +// Extract H265 fields +// By Roger Hardiman, RJH Technical Consultancy Ltd + +namespace Inspectron.HawkEye.RTSP.Sdp +{ + public class H265Parameters : IDictionary + { + private readonly Dictionary parameters = new Dictionary(); + + public List SpropParameterSets + { + get + { + List result = new List(); + + if (ContainsKey("sprop-vps")&& this["sprop-vps"] != null) + { + result.AddRange(this["sprop-vps"].Split(',').Select(x => Convert.FromBase64String(x))); + } + + if (ContainsKey("sprop-sps") && this["sprop-sps"] != null) + { + result.AddRange(this["sprop-sps"].Split(',').Select(x => Convert.FromBase64String(x))); + } + + if (ContainsKey("sprop-pps") && this["sprop-pps"] != null) + { + result.AddRange(this["sprop-pps"].Split(',').Select(x => Convert.FromBase64String(x))); + } + return result; + } + } + + public static H265Parameters Parse(String parameterString) + { + var result = new H265Parameters(); + foreach (var pair in parameterString.Split(';').Select(x => x.Trim().Split(new char[] { '=' }, 2))) + { + if(!string.IsNullOrWhiteSpace(pair[0])) + result[pair[0]] = pair.Length > 1 ? pair[1] : null; + } + return result; + } + + public override string ToString() + { + return parameters.Select(p => p.Key + (p.Value != null ? "=" + p.Value : string.Empty)).Aggregate((x, y) => x + ";" + y); + } + + public String this[String index] + { + get { return parameters[index]; } + set { parameters[index] = value; } + } + + public int Count + { + get + { + return parameters.Count; + } + } + + public bool IsReadOnly + { + get + { + return ((IDictionary)parameters).IsReadOnly; + } + } + + public ICollection Keys + { + get + { + return ((IDictionary)parameters).Keys; + } + } + + public ICollection Values + { + get + { + return ((IDictionary)parameters).Values; + } + } + + public void Add(KeyValuePair item) + { + ((IDictionary)parameters).Add(item); + } + + public void Add(string key, string value) + { + parameters.Add(key, value); + } + + public void Clear() + { + parameters.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return ((IDictionary)parameters).Contains(item); + } + + public bool ContainsKey(string key) + { + return parameters.ContainsKey(key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((IDictionary)parameters).CopyTo(array, arrayIndex); + } + + public IEnumerator> GetEnumerator() + { + return ((IDictionary)parameters).GetEnumerator(); + } + + public bool Remove(KeyValuePair item) + { + return ((IDictionary)parameters).Remove(item); + } + + public bool Remove(string key) + { + return parameters.Remove(key); + } + + public bool TryGetValue(string key, out string value) + { + return parameters.TryGetValue(key, out value); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IDictionary)parameters).GetEnumerator(); + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/Media.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/Media.cs new file mode 100644 index 0000000..fd67c5d --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Sdp/Media.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Inspectron.HawkEye.RTSP.Sdp +{ + public class Media + { + private string mediaString; + + public Media(string mediaString) + { + // Example is 'video 0 RTP/AVP 26; + this.mediaString = mediaString; + + var parts = mediaString.Split(new char[] { ' ' } , 4); + + if (parts.Count() >= 1) { + if (parts[0].Equals("video")) MediaType = MediaTypes.video; + else if (parts[0].Equals("audio")) MediaType = MediaTypes.audio; + else if (parts[0].Equals("text")) MediaType = MediaTypes.text; + else if (parts[0].Equals("application")) MediaType = MediaTypes.application; + else if (parts[0].Equals("message")) MediaType = MediaTypes.message; + else MediaType = MediaTypes.unknown; // standard does allow for future types to be defined + } + + int pt; + if (parts.Count() >= 4) { + if(int.TryParse(parts[3], out pt)) + { + PayloadType = pt; + } else { + PayloadType = 0; + } + } + } + + // RFC4566 Media Types + public enum MediaTypes { video, audio, text, application, message, unknown }; + + public Connection Connection { get; set; } + + public Bandwidth Bandwidth { get; set; } + + public EncriptionKey EncriptionKey { get; set; } + + public MediaTypes MediaType { get; set; } + + public int PayloadType { get; set; } + + private readonly List attributs = new List(); + + public IList Attributs + { + get + { + return attributs; + } + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/Origin.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/Origin.cs new file mode 100644 index 0000000..92f171a --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Sdp/Origin.cs @@ -0,0 +1,101 @@ +using System; +using System.Globalization; + +namespace Inspectron.HawkEye.RTSP.Sdp +{ + /// + /// Object ot represent orgin in an Session Description Protocol + /// + public class Origin + { + public Origin() + { + } + + /// + /// Parses the specified origin string. + /// + /// The string to convert to origin object. + /// + public static Origin Parse(string originString) + { + if (originString == null) + throw new ArgumentNullException("originString"); + + string[] parts = originString.Split(' '); + + if (parts.Length != 6) + throw new FormatException("Number of element invalid in origin string."); + + Origin result = new Origin(); + result.Username = parts[0]; + result.SessionId = parts[1]; + result.SessionVersion = parts[2]; + result.NetType = parts[3]; + result.AddressType = parts[4]; + result.UnicastAddress = parts[5]; + + return result; + } + + /// + /// Gets or sets the username. + /// + /// It is the user's login on the originating host, or it is "-" + /// if the originating host does not support the concept of user IDs. + /// This MUST NOT contain spaces + /// The username. + public string Username { get; set; } + + /// + /// Gets or sets the session id. + /// + /// It is a numeric string such that the tuple of , + /// , , , and forms a + /// globally unique identifier for the session. The method of + /// allocation is up to the creating tool, but it has been + /// suggested that a Network Time Protocol (NTP) format timestamp be + /// used to ensure uniqueness + /// The session id. + public string SessionId { get; set; } + + /// + /// Gets or sets the session version. + /// + /// The session version. + public string SessionVersion { get; set; } + + /// + /// Gets or sets the type of the net. + /// + /// The type of the net. + public string NetType { get; set; } + + /// + /// Gets or sets the type of the address. + /// + /// The type of the address. + public string AddressType { get; set; } + + /// + /// Gets or sets the unicast address (IP or FDQN). + /// + /// The unicast address. + public string UnicastAddress { get; set; } + + public override string ToString() + { + return String.Join(" ", + new string[] + { + Username, + SessionId, + SessionVersion.ToString(CultureInfo.InvariantCulture), + NetType, + AddressType, + UnicastAddress, + } + ); + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/SdpFile.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/SdpFile.cs new file mode 100644 index 0000000..3192013 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Sdp/SdpFile.cs @@ -0,0 +1,267 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; + +namespace Inspectron.HawkEye.RTSP.Sdp +{ + public class SdpFile + { + private static KeyValuePair GetKeyValue(TextReader sdpStream) + { + string line = sdpStream.ReadLine(); + + // end of file ? + if(string.IsNullOrEmpty(line)) + return new KeyValuePair(null, null); + + + string[] parts = line.Split(new char[] { '=' }, 2); + if (parts.Length != 2) + throw new InvalidDataException(); + if (parts[0].Length != 1) + throw new InvalidDataException(); + + KeyValuePair value = new KeyValuePair(parts[0], parts[1]); + return value; + } + + /// + /// Reads the specified SDP stream. + /// As define in RFC 4566 + /// + /// The SDP stream. + /// + public static SdpFile Read(TextReader sdpStream) + { + SdpFile returnValue = new SdpFile(); + KeyValuePair value = GetKeyValue(sdpStream); + + // Version mandatory + if (value.Key == "v") + { + returnValue.Version = int.Parse(value.Value, CultureInfo.InvariantCulture); + value = GetKeyValue(sdpStream); + } + else { + throw new InvalidDataException(); + } + + // Origin mandatory + if (value.Key == "o") + { + returnValue.Origin = Origin.Parse(value.Value); + value = GetKeyValue(sdpStream); + } + else { + throw new InvalidDataException(); + } + + // Session mandatory. + // However the MuxLab HDMI Encoder (TX-500762) Firmware 1.0.6 + // does not include the 'Session' so supress InvalidDatarException + if (value.Key == "s") + { + returnValue.Session = value.Value; + value = GetKeyValue(sdpStream); + } + else { + // throw new InvalidDataException(); // we should throw, but instead we just ignore the error + } + + // Session Information optional + if (value.Key == "i") + { + returnValue.SessionInformation = value.Value; + value = GetKeyValue(sdpStream); + } + + // Uri optional + if (value.Key == "u") + { + returnValue.Url = new Uri(value.Value); + value = GetKeyValue(sdpStream); + } + + // Email optional + if (value.Key == "e") + { + returnValue.Email = value.Value; + value = GetKeyValue(sdpStream); + } + + // Phone optional + if (value.Key == "p") + { + returnValue.Phone = value.Value; + value = GetKeyValue(sdpStream); + } + + // Connection optional + if (value.Key == "c") + { + returnValue.Connection = Connection.Parse(value.Value); + value = GetKeyValue(sdpStream); + } + + // bandwidth optional + if (value.Key == "b") + { + returnValue.Bandwidth = Bandwidth.Parse(value.Value); + value = GetKeyValue(sdpStream); + } + + // Timing mandatory + while (value.Key == "t") + { + string timing = value.Value; + string repeat = string.Empty; + value = GetKeyValue(sdpStream); + if (value.Key == "r") + { + repeat = value.Value; + value = GetKeyValue(sdpStream); + } + returnValue.Timings.Add(new Timing(timing, repeat)); + } + + // timezone optional + if (value.Key == "z") + { + + returnValue.TimeZone = SdpTimeZone.ParseInvariant(value.Value); + value = GetKeyValue(sdpStream); + } + + // encryption key optional + if (value.Key == "k") + { + + returnValue.EncriptionKey = EncriptionKey.ParseInvariant(value.Value); + value = GetKeyValue(sdpStream); + } + + //Attribute optional multiple + while (value.Key == "a") + { + returnValue.Attributs.Add(Attribut.ParseInvariant(value.Value)); + value = GetKeyValue(sdpStream); + } + + // Hack for MuxLab HDMI Encoder (TX-500762) Firmware 1.0.6 + // Skip over all other Key/Value pairs until the 'm=' key + while (value.Key != "m") { + value = GetKeyValue(sdpStream); + } + + // Media + while (value.Key == "m") + { + Media newMedia = ReadMedia(sdpStream, ref value); + returnValue.Medias.Add(newMedia); + } + + + return returnValue; + } + + private static Media ReadMedia(TextReader sdpStream, ref KeyValuePair value) + { + Media returnValue = new Media(value.Value); + value = GetKeyValue(sdpStream); + + // Media title + if (value.Key == "i") + { + value = GetKeyValue(sdpStream); + } + + // Connexion optional + if (value.Key == "c") + { + returnValue.Connection = Connection.Parse(value.Value); + value = GetKeyValue(sdpStream); + } + + // bandwidth optional + if (value.Key == "b") + { + returnValue.Bandwidth = Bandwidth.Parse(value.Value); + value = GetKeyValue(sdpStream); + } + + // enkription key optional + if (value.Key == "k") + { + + returnValue.EncriptionKey = EncriptionKey.ParseInvariant(value.Value); + value = GetKeyValue(sdpStream); + } + + //Attribut optional multiple + while (value.Key == "a") + { + returnValue.Attributs.Add(Attribut.ParseInvariant(value.Value)); + value = GetKeyValue(sdpStream); + } + + return returnValue; + } + + + public int Version { get; set; } + + + public Origin Origin { get; set; } + + public string Session { get; set; } + + public string SessionInformation { get; set; } + + public Uri Url { get; set; } + + public string Email { get; set; } + + public string Phone { get; set; } + + public Connection Connection { get; set; } + + public Bandwidth Bandwidth { get; set; } + + private readonly List timingList = new List(); + + public IList Timings + { + get + { + return timingList; + } + } + + public SdpTimeZone TimeZone { get; set; } + + public EncriptionKey EncriptionKey { get; set; } + + private readonly List attributs = new List(); + + public IList Attributs + { + get + { + return attributs; + } + } + + private readonly List medias = new List(); + + public IList Medias + { + get + { + return medias; + } + + } + + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/SdpTimeZone.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/SdpTimeZone.cs new file mode 100644 index 0000000..2c20787 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Sdp/SdpTimeZone.cs @@ -0,0 +1,26 @@ +using System; +using System.Diagnostics.Contracts; + +namespace Inspectron.HawkEye.RTSP.Sdp +{ + public class SdpTimeZone + { + public SdpTimeZone() + { + } + + public static SdpTimeZone ParseInvariant(string value) + { + if (value == null) + throw new ArgumentNullException("value"); + Contract.EndContractBlock(); + + SdpTimeZone returnValue = new SdpTimeZone(); + + throw new NotImplementedException(); + + + return returnValue; + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Sdp/Timing.cs b/framework/Inspectron.HawkEye/RTSP/Sdp/Timing.cs new file mode 100644 index 0000000..883cab6 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Sdp/Timing.cs @@ -0,0 +1,15 @@ +namespace Inspectron.HawkEye.RTSP.Sdp +{ + public class Timing + { + private string timing; + private string repeat; + + public Timing(string timing, string repeat) + { + // TODO: Complete member initialization + this.timing = timing; + this.repeat = repeat; + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Server/CJOCh264bitstream.cs b/framework/Inspectron.HawkEye/RTSP/Server/CJOCh264bitstream.cs new file mode 100644 index 0000000..df8dece --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Server/CJOCh264bitstream.cs @@ -0,0 +1,401 @@ +using System; +using System.Collections.Generic; + +/* + * CJOCh264bitstream.cpp + * + * Created on: Aug 23, 2014 + * Author: Jordi Cenzano (www.jordicenzano.name) + */ + +/* + * CJOCh264bitstream.h + * + * Created on: Aug 23, 2014 + * Author: Jordi Cenzano (www.jordicenzano.name) + */ + + + +//! h264 bitstream class +/*! + It is used to create the h264 bit oriented stream, it contains different functions that helps you to create the h264 compliant stream (bit oriented, exp golomb coder) + */ +namespace Inspectron.HawkEye.RTSP.Server +{ + public class CJOCh264bitstream : System.IDisposable + { + private const int BUFFER_SIZE_BITS = 24; //! Buffer size in bits used for emulation prevention +//C++ TO C# CONVERTER NOTE: The following #define macro was replaced in-line: +//ORIGINAL LINE: #define BUFFER_SIZE_BYTES (24/8) + + private const int H264_EMULATION_PREVENTION_BYTE = 0x03; //! Emulation prevention byte + + + /*! Buffer */ + private byte[] m_buffer = new byte[BUFFER_SIZE_BITS]; + + /*! Bit buffer index */ + private int m_nLastbitinbuffer; + + /*! Starting byte indicator */ + private int m_nStartingbyte; + + /*! Pointer to output file */ + //private FILE m_pOutFile; + //Byte Array used for output + private List m_pOutFile; + + //! Clears the buffer + private void clearbuffer() + { + //C++ TO C# CONVERTER TODO TASK: The memory management function 'memset' has no equivalent in C#: + //memset(m_buffer, 0, sizeof(byte) * BUFFER_SIZE_BITS); + System.Array.Clear(m_buffer, 0, BUFFER_SIZE_BITS); + m_nLastbitinbuffer = 0; + m_nStartingbyte = 0; + } + + //! Returns the nNumbit value (1 or 0) of lval + /*! + \param lval number to extract the nNumbit value + \param nNumbit Bit position that we want to know if its 1 or 0 (from 0 to 63) + \return bit value (1 or 0) + */ + private static int getbitnum(uint lval, int nNumbit) + { + int lrc = 0; + + uint lmask = (uint) Math.Pow((uint)2,(uint)nNumbit); + if ((lval & lmask) > 0) + { + lrc = 1; + } + + return lrc; + } + + //! Adds 1 bit to the end of h264 bitstream + /*! + \param nVal bit to add at the end of h264 bitstream + */ + private void addbittostream(int nVal) + { + if (m_nLastbitinbuffer >= BUFFER_SIZE_BITS) + { + //Must be aligned, no need to do dobytealign(); + savebufferbyte(); + } + + //Use circular buffer of BUFFER_SIZE_BYTES + int nBytePos = (m_nStartingbyte + (m_nLastbitinbuffer / 8)) % (24 / 8); + //The first bit to add is on the left + int nBitPosInByte = 7 - m_nLastbitinbuffer % 8; + + //Get the byte value from buffer + int nValTmp = m_buffer[nBytePos]; + + //Change the bit + if (nVal > 0) + { + nValTmp = (nValTmp | (int) Math.Pow(2,nBitPosInByte)); + } + else + { + nValTmp = (nValTmp & ~((int) Math.Pow(2,nBitPosInByte))); + } + + //Save the new byte value to the buffer + m_buffer[nBytePos] = (byte) nValTmp; + + m_nLastbitinbuffer++; + } + + //! Adds 8 bit to the end of h264 bitstream (it is optimized for byte aligned situations) + /*! + \param nVal byte to add at the end of h264 bitstream (from 0 to 255) + */ + private void addbytetostream(int nVal) + { + if (m_nLastbitinbuffer >= BUFFER_SIZE_BITS) + { + //Must be aligned, no need to do dobytealign(); + savebufferbyte(); + } + + //Used circular buffer of BUFFER_SIZE_BYTES + int nBytePos = (m_nStartingbyte + (m_nLastbitinbuffer / 8)) % (24 / 8); + //The first bit to add is on the left + int nBitPosInByte = 7 - m_nLastbitinbuffer % 8; + + //Check if it is byte aligned + if (nBitPosInByte != 7) + { + throw new System.Exception("Error: inserting not aligment byte"); + } + + //Add all byte to buffer + m_buffer[nBytePos] = (byte) nVal; + + m_nLastbitinbuffer = m_nLastbitinbuffer + 8; + } + + //! Save all buffer to file + /*! + \param bemulationprevention Indicates if it will insert the emulation prevention byte or not (when it is needed) + */ + private void savebufferbyte(bool bemulationprevention = true) + { + bool bemulationpreventionexecuted = false; + + if (m_pOutFile == null) + { + throw new System.Exception("Error: out file is NULL"); + } + + //Check if the last bit in buffer is multiple of 8 + if ((m_nLastbitinbuffer % 8) != 0) + { + throw new System.Exception("Error: Save to file must be byte aligned"); + } + + if ((m_nLastbitinbuffer / 8) <= 0) + { + throw new System.Exception("Error: NO bytes to save"); + } + + if (bemulationprevention == true) + { + //Emulation prevention will be used: + /*As per h.264 spec, + rbsp_data shouldn't contain + - 0x 00 00 00 + - 0x 00 00 01 + - 0x 00 00 02 + - 0x 00 00 03 + + rbsp_data shall be in the following way + - 0x 00 00 03 00 + - 0x 00 00 03 01 + - 0x 00 00 03 02 + - 0x 00 00 03 03 + */ + + //Check if emulation prevention is needed (emulation prevention is byte align defined) + if ( (m_buffer[((m_nStartingbyte + 0) % (24 / 8))] == 0x00) + && (m_buffer[((m_nStartingbyte + 1) % (24 / 8))] == 0x00) + && ((m_buffer[((m_nStartingbyte + 2) % (24 / 8))] == 0x00) + || (m_buffer[((m_nStartingbyte + 2) % (24 / 8))] == 0x01) + || (m_buffer[((m_nStartingbyte + 2) % (24 / 8))] == 0x02) + || (m_buffer[((m_nStartingbyte + 2) % (24 / 8))] == 0x03))) + { + int nbuffersaved = 0; + byte cEmulationPreventionByte = H264_EMULATION_PREVENTION_BYTE; + + //Save 1st byte + fwrite(m_buffer[((m_nStartingbyte + nbuffersaved) % (24 / 8))], 1, 1, m_pOutFile); + nbuffersaved++; + + //Save 2st byte + fwrite(m_buffer[((m_nStartingbyte + nbuffersaved) % (24 / 8))], 1, 1, m_pOutFile); + nbuffersaved++; + + //Save emulation prevention byte + fwrite(cEmulationPreventionByte, 1, 1, m_pOutFile); + + //Save the rest of bytes (usually 1) + while (nbuffersaved < (24 / 8)) + { + fwrite(m_buffer[((m_nStartingbyte + nbuffersaved) % (24 / 8))], 1, 1, m_pOutFile); + nbuffersaved++; + } + + //All bytes in buffer are saved, so clear the buffer + clearbuffer(); + + bemulationpreventionexecuted = true; + } + } + + if (bemulationpreventionexecuted == false) + { + //No emulation prevention was used + + //Save the oldest byte in buffer + fwrite(m_buffer[m_nStartingbyte], 1, 1, m_pOutFile); + + //Move the index + m_buffer[m_nStartingbyte] = 0; + m_nStartingbyte++; + m_nStartingbyte = m_nStartingbyte % (24 / 8); + m_nLastbitinbuffer = m_nLastbitinbuffer - 8; + } + } + + //! Constructor + /*! + \param pOutBinaryFile The output file pointer + */ + public CJOCh264bitstream(List pOutBinaryFile) + { + clearbuffer(); + +//C++ TO C# CONVERTER TODO TASK: C# does not have an equivalent to pointers to variables (in C#, the variable no longer points to the original when the original variable is re-assigned): +//ORIGINAL LINE: m_pOutFile = pOutBinaryFile; + m_pOutFile = pOutBinaryFile; + } + + //! Destructor + public virtual void Dispose() + { + close(); + } + + //! Add 4 bytes to h264 bistream without taking into acount the emulation prevention. Used to add the NAL header to the h264 bistream + /*! + \param nVal The 32b value to add + \param bDoAlign Indicates if the function will insert 0 in order to create a byte aligned stream before adding nVal 4 bytes to stream. If you try to call this function and the stream is not byte aligned an exception will be thrown + */ + public void add4bytesnoemulationprevention(uint nVal, bool bDoAlign = false) + { + //Used to add NAL header stream + //Remember: NAL header is byte oriented + + if (bDoAlign == true) + { + dobytealign(); + } + + if ((m_nLastbitinbuffer % 8) != 0) + { + throw new System.Exception("Error: Save to file must be byte aligned"); + } + + while (m_nLastbitinbuffer != 0) + { + savebufferbyte(); + } + + byte cbyte = (byte)((nVal & 0xFF000000) >> 24); + fwrite(cbyte, 1, 1, m_pOutFile); + + cbyte = (byte)((nVal & 0x00FF0000) >> 16); + fwrite(cbyte, 1, 1, m_pOutFile); + + cbyte = (byte)((nVal & 0x0000FF00) >> 8); + fwrite(cbyte, 1, 1, m_pOutFile); + + cbyte = (byte)(nVal & 0x000000FF); + fwrite(cbyte, 1, 1, m_pOutFile); + } + + //! Adds nNumbits of lval to the end of h264 bitstream + /*! + \param nVal value to add at the end of the h264 stream (only the LAST nNumbits will be added) + \param nNumbits number of bits of lval that will be added to h264 stream (counting from left) + */ + + //Public functions + + public void addbits(uint lval, int nNumbits) + { + if ((nNumbits <= 0) || (nNumbits > 64)) + { + throw new System.Exception("Error: numbits must be between 1 ... 64"); + } + + int nBit = 0; + int n = nNumbits - 1; + while (n >= 0) + { + nBit = getbitnum(lval, n); + n--; + + addbittostream(nBit); + } + } + + //! Adds lval to the end of h264 bitstream using exp golomb coding for unsigned values + /*! + \param nVal value to add at the end of the h264 stream + */ + public void addexpgolombunsigned(uint lval) + { + //it implements unsigned exp golomb coding + + uint lvalint = lval + 1; + int nnumbits = (int)(Math.Log(lvalint,2) + 1); + + for (int n = 0; n < (nnumbits - 1); n++) + { + addbits(0, 1); + } + + addbits(lvalint, nnumbits); + } + + //! Adds lval to the end of h264 bitstream using exp golomb coding for signed values + /*! + \param nVal value to add at the end of the h264 stream + */ + public void addexpgolombsigned(int lval) + { + //it implements a signed exp golomb coding + + uint lvalint = (uint)(Math.Abs(lval) * 2 - 1); + if (lval <= 0) + { + lvalint = (uint)(2 * Math.Abs(lval)); + } + + addexpgolombunsigned(lvalint); + } + + //! Adds 0 to the end of h264 bistream in order to leave a byte aligned stream (It will insert seven 0 maximum) + public void dobytealign() + { + //Check if the last bit in buffer is multiple of 8 + int nr = m_nLastbitinbuffer % 8; + if ((nr % 8) != 0) + { + m_nLastbitinbuffer = m_nLastbitinbuffer + (8 - nr); + } + } + + //! Adds cByte (8 bits) to the end of h264 bitstream. This function it is optimized in byte aligned streams. + /*! + \param cByte value to add at the end of the h264 stream (from 0 to 255) + */ + public void addbyte(byte cByte) + { + //Byte alignment optimization + if ((m_nLastbitinbuffer % 8) == 0) + { + addbytetostream(cByte); + } + else + { + addbits(cByte, 8); + } + } + + //! Close the h264 stream saving to disk the last remaing bits in buffer + public void close() + { + //Flush the data in stream buffer + + dobytealign(); + + while (m_nLastbitinbuffer != 0) + { + savebufferbyte(); + } + } + + // 'writing' to memory + private void fwrite(byte b, int x, int y, Listdata) + { + data.Add(b); + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Server/CJOCh264encoder.cs b/framework/Inspectron.HawkEye/RTSP/Server/CJOCh264encoder.cs new file mode 100644 index 0000000..f584f64 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Server/CJOCh264encoder.cs @@ -0,0 +1,498 @@ +/* * CJOCh264encoder.cpp + * + * Created on: Aug 17, 2014 + * Author: Jordi Cenzano (www.jordicenzano.name) + */ + +/* + * CJOCh264encoder.h + * + * Created on: Aug 17, 2014 + * Author: Jordi Cenzano (www.jordicenzano.name) + */ + + + +//C++ TO C# CONVERTER NOTE: The following #define macro was replaced in-line: +//ORIGINAL LINE: #define BUFFER_SIZE_BYTES (24/8) + +//! h264 encoder class +/*! + It is used to create the h264 compliant stream + */ + +using System.Collections.Generic; + +namespace Inspectron.HawkEye.RTSP.Server +{ + public class CJOCh264encoder : CJOCh264bitstream + { + + /** + * Allowed sample formats + */ + public enum enSampleFormat + { + SAMPLE_FORMAT_YUV420p //!< SAMPLE_FORMAT_YUV420p + } + + + public List m_pOutFile = null; + public byte[] sps = null; + public byte[] pps = null; + public byte[] nal = null; + + /*!Set the used Y macroblock size for I PCM in YUV420p */ + private const int MACROBLOCK_Y_WIDTH = 16; + private const int MACROBLOCK_Y_HEIGHT = 16; + + /*!Set time base in Hz */ + private const int TIME_SCALE_IN_HZ = 27000000; + + /*!Pointer to pixels */ + private class YUV420p_frame_t + { + public byte[] pYCbCr; + } + + /*! Frame */ + private class frame_t + { + public enSampleFormat sampleformat; //!< Sample format + public uint nYwidth; //!< Y (luminance) block width in pixels + public uint nYheight; //!< Y (luminance) block height in pixels + public uint nCwidth; //!< C (Crominance) block width in pixels + public uint nCheight; //!< C (Crominance) block height in pixels + + public uint nYmbwidth; //!< Y (luminance) macroblock width in pixels + public uint nYmbheight; //!< Y (luminance) macroblock height in pixels + public uint nCmbwidth; //!< Y (Crominance) macroblock width in pixels + public uint nCmbheight; //!< Y (Crominance) macroblock height in pixels + + public YUV420p_frame_t yuv420pframe = new YUV420p_frame_t(); //!< Pointer to current frame data + public uint nyuv420pframesize; //!< Size in bytes of yuv420pframe + } + + /*! The frame var*/ + private frame_t m_frame = new frame_t(); + + /*! The frames per second var*/ + private uint m_nFps; + + /*! Number of frames sent to the output */ + private uint m_lNumFramesAdded; + + + + //! Frees the frame yuv420pframe allocated memory + + //Free the allocated video frame mem + private void free_video_src_frame() + { + if (m_frame.yuv420pframe.pYCbCr != null) + { +//C++ TO C# CONVERTER TODO TASK: The memory management function 'free' has no equivalent in C#: +// free(m_frame.yuv420pframe.pYCbCr); + } + +//C++ TO C# CONVERTER TODO TASK: The memory management function 'memset' has no equivalent in C#: +// memset(m_frame, 0, sizeof(frame_t)); + } + + //! Allocs the frame yuv420pframe memory according to the frame properties + + //Alloc mem to store a video frame + private void alloc_video_src_frame() + { + if (m_frame.yuv420pframe.pYCbCr != null) + { + throw new System.Exception("Error: null values in frame"); + } + + uint nYsize = m_frame.nYwidth * m_frame.nYheight; + uint nCsize = m_frame.nCwidth * m_frame.nCheight; + m_frame.nyuv420pframesize = nYsize + nCsize + nCsize; + + m_frame.yuv420pframe.pYCbCr = new byte[m_frame.nyuv420pframesize]; + + if (m_frame.yuv420pframe.pYCbCr == null) + { + throw new System.Exception("Error: memory alloc"); + } + } + + //! Creates SPS NAL and add it to the output + /*! + \param nImW Frame width in pixels + \param nImH Frame height in pixels + \param nMbW macroblock width in pixels + \param nMbH macroblock height in pixels + \param nFps frames x second (tipical values are: 25, 30, 50, etc) + \param nSARw Indicates the horizontal size of the sample aspect ratio (tipical values are:1, 4, 16, etc) + \param nSARh Indicates the vertical size of the sample aspect ratio (tipical values are:1, 3, 9, etc) + */ + + //Creates and saves the NAL SPS (including VUI) (one per file) + private void create_sps(uint nImW, uint nImH, uint nMbW, uint nMbH, uint nFps, uint nSARw, uint nSARh) + { + add4bytesnoemulationprevention(0x000001); // NAL header + addbits(0x0, 1); // forbidden_bit + addbits(0x3, 2); // nal_ref_idc + addbits(0x7, 5); // nal_unit_type : 7 ( SPS ) + addbits(0x42, 8); // profile_idc = baseline ( 0x42 ) + addbits(0x0, 1); // constraint_set0_flag + addbits(0x0, 1); // constraint_set1_flag + addbits(0x0, 1); // constraint_set2_flag + addbits(0x0, 1); // constraint_set3_flag + addbits(0x0, 1); // constraint_set4_flag + addbits(0x0, 1); // constraint_set5_flag + addbits(0x0, 2); // reserved_zero_2bits /* equal to 0 */ + addbits(0x0a, 8); // level_idc: 3.1 (0x0a) + addexpgolombunsigned(0); // seq_parameter_set_id + addexpgolombunsigned(0); // log2_max_frame_num_minus4 + addexpgolombunsigned(0); // pic_order_cnt_type + addexpgolombunsigned(0); // log2_max_pic_order_cnt_lsb_minus4 + addexpgolombunsigned(0); // max_num_refs_frames + addbits(0x0, 1); // gaps_in_frame_num_value_allowed_flag + + uint nWinMbs = nImW / nMbW; + addexpgolombunsigned(nWinMbs - 1); // pic_width_in_mbs_minus_1 + uint nHinMbs = nImH / nMbH; + addexpgolombunsigned(nHinMbs - 1); // pic_height_in_map_units_minus_1 + + addbits(0x1, 1); // frame_mbs_only_flag + addbits(0x0, 1); // direct_8x8_interfernce + addbits(0x0, 1); // frame_cropping_flag +// addbits(0x1, 1); // vui_parameter_present + addbits(0x0, 1); // vui_parameter_present + + //VUI parameters (AR, timming) +// addbits(0x1, 1); //aspect_ratio_info_present_flag +// addbits(0xFF, 8); //aspect_ratio_idc = Extended_SAR + + //AR +// addbits(nSARw, 16); //sar_width +// addbits(nSARh, 16); //sar_height + +// addbits(0x0, 1); //overscan_info_present_flag +// addbits(0x0, 1); //video_signal_type_present_flag +// addbits(0x0, 1); //chroma_loc_info_present_flag +// addbits(0x1, 1); //timing_info_present_flag + +// uint nnum_units_in_tick = TIME_SCALE_IN_HZ / (2 * nFps); +// addbits(nnum_units_in_tick, 32); //num_units_in_tick +// addbits(TIME_SCALE_IN_HZ, 32); //time_scale +// addbits(0x1, 1); //fixed_frame_rate_flag + +// addbits(0x0, 1); //nal_hrd_parameters_present_flag +// addbits(0x0, 1); //vcl_hrd_parameters_present_flag +// addbits(0x0, 1); //pic_struct_present_flag +// addbits(0x0, 1); //bitstream_restriction_flag + //END VUI + +//BUG? addbits(0x0, 1); // frame_mbs_only_flag + addbits(0x1, 1); // rbsp stop bit + + dobytealign(); + } + + //! Creates PPS NAL and add it to the output + + //Creates and saves the NAL PPS (one per file) + private void create_pps() + { + add4bytesnoemulationprevention(0x000001); // NAL header + addbits(0x0, 1); // forbidden_bit + addbits(0x3, 2); // nal_ref_idc + addbits(0x8, 5); // nal_unit_type : 8 ( PPS ) + addexpgolombunsigned(0); // pic_parameter_set_id + addexpgolombunsigned(0); // seq_parameter_set_id + addbits(0x0, 1); // entropy_coding_mode_flag + addbits(0x0, 1); // bottom_field_pic_order_in frame_present_flag + addexpgolombunsigned(0); // nun_slices_groups_minus1 + addexpgolombunsigned(0); // num_ref_idx10_default_active_minus + addexpgolombunsigned(0); // num_ref_idx11_default_active_minus + addbits(0x0, 1); // weighted_pred_flag + addbits(0x0, 2); // weighted_bipred_idc + addexpgolombsigned(0); // pic_init_qp_minus26 + addexpgolombsigned(0); // pic_init_qs_minus26 + addexpgolombsigned(0); // chroma_qp_index_offset + addbits(0x0, 1); //deblocking_filter_present_flag + addbits(0x0, 1); // constrained_intra_pred_flag + addbits(0x0, 1); //redundant_pic_ent_present_flag + addbits(0x1, 1); // rbsp stop bit + + dobytealign(); + } + + //! Creates Slice NAL and add it to the output + /*! + \param lFrameNum number of frame + */ + + //Creates and saves the NAL SLICE (one per frame) + //H264 Spec Section 7.3.3 Slice Header Syntax + private void create_slice_header(uint lFrameNum) + { + add4bytesnoemulationprevention(0x000001); // NAL header + addbits(0x0, 1); // forbidden_bit + addbits(0x3, 2); // nal_ref_idc + addbits(0x5, 5); // nal_unit_type : 5 ( Coded slice of an IDR picture ) + addexpgolombunsigned(0); // first_mb_in_slice + addexpgolombunsigned(7); // slice_type + addexpgolombunsigned(0); // pic_param_set_id + + byte cFrameNum = 0; // (byte)(lFrameNum % 16); // H264 Spec says "If the current picture is an IDR picture, frame_num shall be equal to 0. " + // Also any maths here must relate to the value of log2_max_frame_num_minus4 in the SPS + + addbits(cFrameNum, 4); // frame_num ( numbits = v = log2_max_frame_num_minus4 + 4) + + // idr_pic_id range is 0..65535. All slices in the same IDR must have the same pic_id. Spec says if there are two + // IDRs back to back they must have different idr_pic_id values + uint lidr_pic_id = lFrameNum % 65536; + + addexpgolombunsigned(lidr_pic_id); // idr_pic_id + + addbits(0x0, 4); // pic_order_cnt_lsb (numbits = v = log2_max_fpic_order_cnt_lsb_minus4 + 4) + // nal_ref_idc != 0. Insert dec_ref_pic_marking + addbits(0x0, 1); // no_output_of_prior_pics_flag + addbits(0x0, 1); // long_term_reference_flag + + addexpgolombsigned(0); //slice_qp_delta + + //Probably NOT byte aligned!!! + } + + //! Creates macroblock header and add it to the output + + //Creates and saves the macroblock header(one per macroblock) + private void create_macroblock_header() + { + addexpgolombunsigned(25); // mb_type (I_PCM) + } + + //! Creates the slice footer and add it to the output + + //Creates and saves the SLICE footer (one per SLICE) + private void create_slice_footer() + { + addbits(0x1, 1); // rbsp stop bit + } + + //! Creates SPS NAL and add it to the output + /*! + \param nYpos First vertical macroblock pixel inside the frame + \param nYpos nXpos horizontal macroblock pixel inside the frame + */ + + //Creates & saves a macroblock (coded INTRA 16x16) + private void create_macroblock(uint nYpos, uint nXpos) + { + uint x; + uint y; + + create_macroblock_header(); + + dobytealign(); + + //Y + uint nYsize = m_frame.nYwidth * m_frame.nYheight; + for (y = nYpos * m_frame.nYmbheight; y < (nYpos + 1) * m_frame.nYmbheight; y++) + { + for (x = nXpos * m_frame.nYmbwidth; x < (nXpos + 1) * m_frame.nYmbwidth; x++) + { + addbyte(m_frame.yuv420pframe.pYCbCr[(y * m_frame.nYwidth + x)]); + } + } + + //Cb + uint nCsize = m_frame.nCwidth * m_frame.nCheight; + for (y = nYpos * m_frame.nCmbheight; y < (nYpos + 1) * m_frame.nCmbheight; y++) + { + for (x = nXpos * m_frame.nCmbwidth; x < (nXpos + 1) * m_frame.nCmbwidth; x++) + { + addbyte(m_frame.yuv420pframe.pYCbCr[nYsize + (y * m_frame.nCwidth + x)]); + } + } + + //Cr + for (y = nYpos * m_frame.nCmbheight; y < (nYpos + 1) * m_frame.nCmbheight; y++) + { + for (x = nXpos * m_frame.nCmbwidth; x < (nXpos + 1) * m_frame.nCmbwidth; x++) + { + addbyte(m_frame.yuv420pframe.pYCbCr[nYsize + nCsize + (y * m_frame.nCwidth + x)]); + } + } + } + + //! Constructor + /*! + \param pOutFile The output file pointer + */ + + //Private functions + + //Contructor + public CJOCh264encoder(List pOutFile) : base(pOutFile) + { + m_lNumFramesAdded = 0; + +//C++ TO C# CONVERTER TODO TASK: The memory management function 'memset' has no equivalent in C#: + //memset(m_frame, 0, sizeof(frame_t)); + m_nFps = 25; + + m_pOutFile = pOutFile; + } + + //! Destructor + + //Destructor + public override void Dispose() + { + free_video_src_frame(); + base.Dispose(); + } + + //! Initializes the coder + /*! + \param nImW Frame width in pixels + \param nImH Frame height in pixels + \param nFps Desired frames per second of the output file (typical values are: 25, 30, 50, etc) + \param SampleFormat Sample format if the input file. In this implementation only SAMPLE_FORMAT_YUV420p is allowed + \param nSARw Indicates the horizontal size of the sample aspect ratio (typical values are:1, 4, 16, etc) + \param nSARh Indicates the vertical size of the sample aspect ratio (typical values are:1, 3, 9, etc) + */ + + //public functions + + //Initilizes the h264 coder (mini-coder) + public void IniCoder(uint nImW, uint nImH, uint nImFps, CJOCh264encoder.enSampleFormat SampleFormat, uint nSARw = 1, uint nSARh = 1) + { + m_lNumFramesAdded = 0; + + if (SampleFormat != enSampleFormat.SAMPLE_FORMAT_YUV420p) + { + throw new System.Exception("Error: SAMPLE FORMAT not allowed. Only yuv420p is allowed in this version"); + } + + free_video_src_frame(); + + //Ini vars + m_frame.sampleformat = SampleFormat; + m_frame.nYwidth = nImW; + m_frame.nYheight = nImH; + if (SampleFormat == enSampleFormat.SAMPLE_FORMAT_YUV420p) + { + //Set macroblock Y size + m_frame.nYmbwidth = MACROBLOCK_Y_WIDTH; + m_frame.nYmbheight = MACROBLOCK_Y_HEIGHT; + + //Set macroblock C size (in YUV420 is 1/2 of Y) + m_frame.nCmbwidth = MACROBLOCK_Y_WIDTH / 2; + m_frame.nCmbheight = MACROBLOCK_Y_HEIGHT / 2; + + //Set C size + m_frame.nCwidth = m_frame.nYwidth / 2; + m_frame.nCheight = m_frame.nYheight / 2; + + //In this implementation only picture sizes multiples of macroblock size (16x16) are allowed + if (((nImW % MACROBLOCK_Y_WIDTH) != 0) || ((nImH % MACROBLOCK_Y_HEIGHT) != 0)) + { + throw new System.Exception("Error: size not allowed. Only multiples of macroblock are allowed (macroblock size is: 16x16)"); + } + } + m_nFps = nImFps; + + //Alloc mem for 1 frame + alloc_video_src_frame(); + + //Create h264 SPS & PPS + create_sps(m_frame.nYwidth, m_frame.nYheight, m_frame.nYmbwidth, m_frame.nYmbheight, nImFps, nSARw, nSARh); + close(); // Flush data to the List + sps = m_pOutFile.ToArray(); + m_pOutFile.Clear(); + + create_pps(); + close(); // Flush data to the List + pps = m_pOutFile.ToArray(); + m_pOutFile.Clear(); + } + + //! Returns the frame pointer + /*! + \return Frame pointer ready to fill with frame pixels data (the format to fill the data is indicated by SampleFormat parameter when the coder is initialized + */ + + //Returns the frame pointer to load the video frame + public byte[] GetFramePtr() + { + if (m_frame.yuv420pframe.pYCbCr == null) + { + throw new System.Exception("Error: video frame is null (not initialized)"); + } + + return m_frame.yuv420pframe.pYCbCr; + } + + //! Returns the allocated frame memory in bytes + /*! + \return The allocated memory to store the frame data + */ + + //Returns the the allocated size for video frame + public uint GetFrameSize() + { + return m_frame.nyuv420pframesize; + } + + //! It codes the frame that is in frame memory a it saves the coded data to disc + + //Codifies & save the video frame (it only uses 16x16 intra PCM -> NO COMPRESSION!) + public void CodeAndSaveFrame() + { + m_pOutFile.Clear(); + + //The slice header is not byte aligned, so the first macroblock header is not byte aligned + create_slice_header(m_lNumFramesAdded); + + //Loop over macroblock size + uint y; + uint x; + for (y = 0; y < m_frame.nYheight / m_frame.nYmbheight; y++) + { + for (x = 0; x < m_frame.nYwidth / m_frame.nYmbwidth; x++) + { + create_macroblock(y, x); + } + } + + create_slice_footer(); + dobytealign(); + + m_lNumFramesAdded++; + + // flush + close(); + nal = m_pOutFile.ToArray(); + } + + //! Returns number of coded frames + /*! + \return The number of coded frames + */ + + //Returns the number of codified frames + public uint GetSavedFrames() + { + return m_lNumFramesAdded; + } + + //! Flush all data and save the trailing bits + + //Closes the h264 coder saving the last bits in the buffer + public void CloseCoder() + { + close(); + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Server/RTPPacketUtil.cs b/framework/Inspectron.HawkEye/RTSP/Server/RTPPacketUtil.cs new file mode 100644 index 0000000..368a2e7 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Server/RTPPacketUtil.cs @@ -0,0 +1,34 @@ +namespace Inspectron.HawkEye.RTSP.Server +{ + public static class RTPPacketUtil + { + + public static void WriteHeader(byte[] rtp_packet, int rtp_version, int rtp_padding, int rtp_extension, int rtp_csrc_count, int rtp_marker, int rtp_payload_type) + { + rtp_packet[0] = (byte)((rtp_version << 6) | (rtp_padding << 5) | (rtp_extension << 4) | rtp_csrc_count); + rtp_packet[1] = (byte)((rtp_marker << 7) | (rtp_payload_type & 0x7F)); + } + + public static void WriteSequenceNumber(byte[] rtp_packet, uint empty_sequence_id) + { + rtp_packet[2] = ((byte)((empty_sequence_id >> 8) & 0xFF)); + rtp_packet[3] = ((byte)((empty_sequence_id >> 0) & 0xFF)); + } + + public static void WriteTS(byte[] rtp_packet, uint ts) + { + rtp_packet[4] = ((byte)((ts >> 24) & 0xFF)); + rtp_packet[5] = ((byte)((ts >> 16) & 0xFF)); + rtp_packet[6] = ((byte)((ts >> 8) & 0xFF)); + rtp_packet[7] = ((byte)((ts >> 0) & 0xFF)); + } + + public static void WriteSSRC(byte[] rtp_packet, uint ssrc) + { + rtp_packet[8] = ((byte)((ssrc >> 24) & 0xFF)); + rtp_packet[9] = ((byte)((ssrc >> 16) & 0xFF)); + rtp_packet[10] = ((byte)((ssrc >> 8) & 0xFF)); + rtp_packet[11] = ((byte)((ssrc >> 0) & 0xFF)); + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Server/RtspServer.cs b/framework/Inspectron.HawkEye/RTSP/Server/RtspServer.cs new file mode 100644 index 0000000..a41e912 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Server/RtspServer.cs @@ -0,0 +1,800 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using Inspectron.HawkEye.RTSP.Messages; + +// RTSP Server Example (c) Roger Hardiman, 2016, 2018 +// Released uder the MIT Open Source Licence +// +// Re-uses some code from the Multiplexer example of SharpRTSP +// +// This example simulates a live RTSP video stream, for example a CCTV Camera +// It creates a Video Source (a test card) that creates a YUV Image +// The image is then encoded as H264 data using a very basic H264 Encoder +// The H264 data (the NALs) are sent to the RTSP clients +// Video is sent in UDP Mode or TCP Mode (ie RTP over RTSP mode) + +// The Tiny H264 Encoder is a 100% .NET encoder which is lossless and creates large bitstreams as +// there is no compression. It is limited to 128x96 resolution. However it makes it easy to write a quick +// demo without needing native APIs or cross compiled C libraries for H264 + +namespace Inspectron.HawkEye.RTSP.Server +{ + public class RtspServer : IDisposable + { + const int h264_width = 192; // Tiny needs 128x96 + const int h264_height = 128; + const int h264_fps = 25; + + const uint global_ssrc = 0x4321FADE; // 8 hex digits + + private TcpListener _RTSPServerListener; + private ManualResetEvent _Stopping; + private Thread _ListenTread; + + private TestCard video_source = null; + private SimpleH264Encoder h264_encoder = null; + //private TinyH264Encoder h264_encoder = null; + + byte[] raw_sps = null; + byte[] raw_pps = null; + + List rtsp_list = new List(); // list of RTSP Listeners + + Random rnd = new Random(); + int session_handle = 1; + + Authentication auth = null; + + /// + /// Initializes a new instance of the class. + /// + /// A numero port. + /// username. + /// password. + public RtspServer(int portNumber, String username, String password) + { + if (portNumber < System.Net.IPEndPoint.MinPort || portNumber > System.Net.IPEndPoint.MaxPort) + throw new ArgumentOutOfRangeException("aPortNumber", portNumber, "Port number must be between System.Net.IPEndPoint.MinPort and System.Net.IPEndPoint.MaxPort"); + Contract.EndContractBlock(); + + if (String.IsNullOrEmpty(username) == false + && String.IsNullOrEmpty(password) == false) { + String realm = "SharpRTSPServer"; + auth = new Authentication(username,password,realm,Authentication.Type.Digest); + } else { + auth = null; + } + + RtspUtils.RegisterUri(); + _RTSPServerListener = new TcpListener(IPAddress.Any, portNumber); + } + + /// + /// Starts the listen. + /// + public void StartListen() + { + _RTSPServerListener.Start(); + + _Stopping = new ManualResetEvent(false); + _ListenTread = new Thread(new ThreadStart(AcceptConnection)); + _ListenTread.Start(); + + // Initialise the H264 encoder + h264_encoder = new SimpleH264Encoder(h264_width, h264_height, h264_fps); + //h264_encoder = new TinyH264Encoder(); // hard coded to 192x128 + + // Start the VideoSource + video_source = new TestCard(h264_width, h264_height, h264_fps); + video_source.ReceivedYUVFrame += video_source_ReceivedYUVFrame; + } + + + /// + /// Accepts the connection. + /// + private void AcceptConnection() + { + try + { + while (!_Stopping.WaitOne(0)) + { + // Wait for an incoming TCP Connection + TcpClient oneClient = _RTSPServerListener.AcceptTcpClient(); + Console.WriteLine("Connection from " + oneClient.Client.RemoteEndPoint.ToString()); + + // Hand the incoming TCP connection over to the RTSP classes + var rtsp_socket = new RtspTcpTransport(oneClient); + RtspListener newListener = new RtspListener(rtsp_socket); + newListener.MessageReceived += RTSP_Message_Received; + //RTSPDispatcher.Instance.AddListener(newListener); + + // Add the RtspListener to the RTSPConnections List + lock (rtsp_list) { + RTSPConnection new_connection = new RTSPConnection(); + new_connection.listener = newListener; + new_connection.client_hostname = newListener.RemoteAdress.Split(':')[0]; + new_connection.ssrc = global_ssrc; + + new_connection.time_since_last_rtsp_keepalive = DateTime.UtcNow; + new_connection.video_time_since_last_rtcp_keepalive = DateTime.UtcNow; + + rtsp_list.Add(new_connection); + } + + newListener.Start(); + } + } + catch (SocketException error) + { + // _logger.Warn("Got an error listening, I have to handle the stopping which also throw an error", error); + } + catch (Exception error) + { + // _logger.Error("Got an error listening...", error); + throw; + } + + + } + + + public void StopListen() + { + _RTSPServerListener.Stop(); + _Stopping.Set(); + _ListenTread.Join(); + } + + #region IDisposable Membres + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + StopListen(); + _Stopping.Dispose(); + } + } + + #endregion + + // Process each RTSP message that is received + private void RTSP_Message_Received(object sender, RtspChunkEventArgs e) + { + // Cast the 'sender' and 'e' into the RTSP Listener (the Socket) and the RTSP Message + RtspListener listener = sender as RtspListener; + RtspMessage message = e.Message as RtspMessage; + + Console.WriteLine("RTSP message received " + message); + + + // Check if the RTSP Message has valid authentication (validating against username,password,realm and nonce) + if (auth != null) { + bool authorized = false; + if (message.Headers.ContainsKey("Authorization") == true ) { + // The Header contained Authorization + // Check the message has the correct Authorization + // If it does not have the correct Authorization then close the RTSP connection + authorized = auth.IsValid(message); + + if (authorized == false) { + // Send a 401 Authentication Failed reply, then close the RTSP Socket + RtspResponse authorization_response = (e.Message as RtspRequest).CreateResponse(); + authorization_response.AddHeader("WWW-Authenticate: " + auth.GetHeader()); + authorization_response.ReturnCode = 401; + listener.SendMessage(authorization_response); + + lock (rtsp_list) { + foreach (RTSPConnection connection in rtsp_list.ToArray()){ + if (connection.listener == listener) { + rtsp_list.Remove(connection); + } + } + } + listener.Dispose(); + return; + + } + } + if ((message.Headers.ContainsKey("Authorization") == false)){ + // Send a 401 Authentication Failed with extra info in WWW-Authenticate + // to tell the Client if we are using Basic or Digest Authentication + RtspResponse authorization_response = (e.Message as RtspRequest).CreateResponse(); + authorization_response.AddHeader("WWW-Authenticate: " + auth.GetHeader()); // 'Basic' or 'Digest' + authorization_response.ReturnCode = 401; + listener.SendMessage(authorization_response); + return; + } + } + + // Update the RTSP Keepalive Timeout + // We could check that the message is GET_PARAMETER or OPTIONS for a keepalive but instead we will update the timer on any message + lock (rtsp_list) + { + foreach (RTSPConnection connection in rtsp_list) + { + if (connection.listener.RemoteAdress.Equals(listener.RemoteAdress)) + { + // found the connection + connection.time_since_last_rtsp_keepalive = DateTime.UtcNow; + break; + + } + } + } + + + // Handle OPTIONS message + if (message is RtspRequestOptions) + { + // Create the reponse to OPTIONS + RtspResponse options_response = (e.Message as RtspRequestOptions).CreateResponse(); + listener.SendMessage(options_response); + } + + // Handle DESCRIBE message + if (message is RtspRequestDescribe) + { + String requested_url = (message as RtspRequestDescribe).RtspUri.ToString(); + Console.WriteLine("Request for " + requested_url); + + // TODO. Check the requsted_url is valid. In this example we accept any RTSP URL + + // Make the Base64 SPS and PPS + raw_sps = h264_encoder.GetRawSPS(); // no 0x00 0x00 0x00 0x01 or 32 bit size header + raw_pps = h264_encoder.GetRawPPS(); // no 0x00 0x00 0x00 0x01 or 32 bit size header + String sps_str = Convert.ToBase64String(raw_sps); + String pps_str = Convert.ToBase64String(raw_pps); + + StringBuilder sdp = new StringBuilder(); + + // Generate the SDP + // The sprop-parameter-sets provide the SPS and PPS for H264 video + // The packetization-mode defines the H264 over RTP payloads used but is Optional + sdp.Append("v=0\n"); + sdp.Append("o=user 123 0 IN IP4 0.0.0.0\n"); + sdp.Append("s=SharpRTSP Test Camera\n"); + sdp.Append("m=video 0 RTP/AVP 96\n"); + sdp.Append("c=IN IP4 0.0.0.0\n"); + sdp.Append("a=control:trackID=0\n"); + sdp.Append("a=rtpmap:96 H264/90000\n"); + sdp.Append("a=fmtp:96 profile-level-id=42A01E; sprop-parameter-sets=" + sps_str + "," + pps_str + ";\n"); + + byte[] sdp_bytes = Encoding.ASCII.GetBytes(sdp.ToString()); + + // Create the reponse to DESCRIBE + // This must include the Session Description Protocol (SDP) + RtspResponse describe_response = (e.Message as RtspRequestDescribe).CreateResponse(); + + describe_response.AddHeader("Content-Base: " + requested_url); + describe_response.AddHeader("Content-Type: application/sdp"); + describe_response.Data = sdp_bytes; + describe_response.AdjustContentLength(); + listener.SendMessage(describe_response); + } + + // Handle SETUP message + if (message is RtspRequestSetup) + { + + // + var setupMessage = message as RtspRequestSetup; + + // Check the RTSP transport + // If it is UDP or Multicast, create the sockets + // If it is RTP over RTSP we send data via the RTSP Listener + + // FIXME client may send more than one possible transport. + // very rare + RtspTransport transport = setupMessage.GetTransports()[0]; + + + // Construct the Transport: reply from the Server to the client + RtspTransport transport_reply = new RtspTransport(); + transport_reply.SSrc = global_ssrc.ToString("X8"); // Convert to Hex, padded to 8 characters + + if (transport.LowerTransport == RtspTransport.LowerTransportType.TCP) + { + // RTP over RTSP mode} + transport_reply.LowerTransport = RtspTransport.LowerTransportType.TCP; + transport_reply.Interleaved = new PortCouple(transport.Interleaved.First, transport.Interleaved.Second); + } + + UDPSocket udp_pair = null; + if (transport.LowerTransport == RtspTransport.LowerTransportType.UDP + && transport.IsMulticast == false) + { + Boolean udp_supported = true; + if (udp_supported) { + // RTP over UDP mode + // Create a pair of UDP sockets - One is for the Video, one is for the RTCP + udp_pair = new UDPSocket(50000, 51000); // give a range of 500 pairs (1000 addresses) to try incase some address are in use + udp_pair.DataReceived += (object local_sender, RtspChunkEventArgs local_e) => { + // RTCP data received + Console.WriteLine("RTCP data received " + local_sender.ToString() + " " + local_e.ToString()); + }; + udp_pair.Start(); // start listening for data on the UDP ports + + // Pass the Port of the two sockets back in the reply + transport_reply.LowerTransport = RtspTransport.LowerTransportType.UDP; + transport_reply.IsMulticast = false; + transport_reply.ClientPort = new PortCouple(udp_pair.data_port,udp_pair.control_port); + } else { + transport_reply = null; + } + } + + if (transport.LowerTransport == RtspTransport.LowerTransportType.UDP + && transport.IsMulticast == true) + { + // RTP over Multicast UDP mode} + // Create a pair of UDP sockets in Multicast Mode + // Pass the Ports of the two sockets back in the reply + transport_reply.LowerTransport = RtspTransport.LowerTransportType.UDP; + transport_reply.IsMulticast = true; + transport_reply.Port = new PortCouple(7000, 7001); // FIX + + // for now until implemented + transport_reply = null; + } + + + if (transport_reply != null) + { + + // Update the session with transport information + String copy_of_session_id = ""; + lock (rtsp_list) + { + foreach (RTSPConnection connection in rtsp_list) + { + if (connection.listener.RemoteAdress.Equals(listener.RemoteAdress)) { + // ToDo - Check the Track ID to determine if this is a SETUP for the Video Stream + // or a SETUP for an Audio Stream. + // In the SDP the H264 video track is TrackID 0 + + + // found the connection + // Add the transports to the connection + connection.video_client_transport = transport; + connection.video_transport_reply = transport_reply; + + // If we are sending in UDP mode, add the UDP Socket pair and the Client Hostname + connection.video_udp_pair = udp_pair; + + + connection.video_session_id = session_handle.ToString(); + session_handle++; + + + // Copy the Session ID + copy_of_session_id = connection.video_session_id; + break; + } + } + } + + RtspResponse setup_response = setupMessage.CreateResponse(); + setup_response.Headers[RtspHeaderNames.Transport] = transport_reply.ToString(); + setup_response.Session = copy_of_session_id; + listener.SendMessage(setup_response); + } + else + { + RtspResponse setup_response = setupMessage.CreateResponse(); + // unsuported transport + setup_response.ReturnCode = 461; + listener.SendMessage(setup_response); + } + + } + + // Handle PLAY message (Sent with a Session ID) + if (message is RtspRequestPlay) + { + lock (rtsp_list) + { + // Search for the Session in the Sessions List. Change the state to "PLAY" + bool session_found = false; + foreach (RTSPConnection connection in rtsp_list) + { + if (message.Session == connection.video_session_id) /* OR AUDIO_SESSION_ID */ + { + // found the session + session_found = true; + connection.play = true; // ACTUALLY YOU COULD PAUSE JUST THE VIDEO (or JUST THE AUDIO) + + string range = "npt=0-"; // Playing the 'video' from 0 seconds until the end + string rtp_info = "url="+((RtspRequestPlay)message).RtspUri+";seq=" + connection.video_sequence_number; // TODO Add rtptime +";rtptime="+session.rtp_initial_timestamp; + + // Send the reply + RtspResponse play_response = (e.Message as RtspRequestPlay).CreateResponse(); + play_response.AddHeader("Range: " + range); + play_response.AddHeader("RTP-Info: " + rtp_info); + listener.SendMessage(play_response); + + break; + } + } + + if (session_found == false) { + // Session ID was not found in the list of Sessions. Send a 454 error + RtspResponse play_failed_response = (e.Message as RtspRequestPlay).CreateResponse(); + play_failed_response.ReturnCode = 454; // Session Not Found + listener.SendMessage(play_failed_response); + } + + } + + } + + // Handle PAUSE message (Sent with a Session ID) + if (message is RtspRequestPause) + { + lock (rtsp_list) + { + // Search for the Session in the Sessions List. Change the state of "PLAY" + foreach (RTSPConnection connection in rtsp_list) + { + if (message.Session == connection.video_session_id /* OR AUDIO SESSION ID */) + { + // found the session + connection.play = false; // COULD HAVE PLAY/PAUSE FOR VIDEO AND AUDIO + break; + } + } + } + + // ToDo - only send back the OK response if the Session in the RTSP message was found + RtspResponse pause_response = (e.Message as RtspRequestPause).CreateResponse(); + listener.SendMessage(pause_response); + } + + + // Handle GET_PARAMETER message, often used as a Keep Alive + if (message is RtspRequestGetParameter) + { + // Create the reponse to GET_PARAMETER + RtspResponse getparameter_response = (e.Message as RtspRequestGetParameter).CreateResponse(); + listener.SendMessage(getparameter_response); + } + + + // Handle TEARDOWN (sent with a Session ID) + if (message is RtspRequestTeardown) + { + lock (rtsp_list) + { + // Search for the Session in the Sessions List. + foreach (RTSPConnection connection in rtsp_list.ToArray()) // Convert to ToArray so we can delete from the rtp_list + { + if (message.Session == connection.video_session_id) // SHOULD HAVE AN AUDIO TEARDOWN AS WELL + { + // If this is UDP, close the transport + // For TCP there is no transport to close (as RTP packets were interleaved into the RTSP connection) + if (connection.video_udp_pair != null) { + connection.video_udp_pair.Stop(); + connection.video_udp_pair = null; + } + + rtsp_list.Remove(connection); + + // Close the RTSP socket + listener.Dispose(); + } + } + } + } + + + } + + // The 'Camera' (YUV TestCard) has generated a YUV image. + // If there are RTSP clients connected then Compress the Video Frame (with H264) and send it to the client + void video_source_ReceivedYUVFrame(uint timestamp_ms, int width, int height, byte[] yuv_data) + { + DateTime now = DateTime.UtcNow; + int current_rtp_play_count = 0; + int current_rtp_count = 0; + int timeout_in_seconds = 70; // must have a RTSP message every 70 seconds or we will close the connection + lock (rtsp_list) { + current_rtp_count = rtsp_list.Count; + foreach (RTSPConnection connection in rtsp_list.ToArray()) { // Convert to Array to allow us to delete from rtsp_list + // RTSP Timeout (clients receiving RTP video over the RTSP session + // do not need to send a keepalive (so we check for Socket write errors) + Boolean sending_rtp_via_tcp = false; + if ((connection.video_client_transport != null) && + (connection.video_client_transport.LowerTransport == RtspTransport.LowerTransportType.TCP)) + { + sending_rtp_via_tcp = true; + } + + if (sending_rtp_via_tcp == false && ((now - connection.time_since_last_rtsp_keepalive).TotalSeconds > timeout_in_seconds)) { + + Console.WriteLine("Removing session " + connection.video_session_id + " due to TIMEOUT"); + connection.play = false; // stop sending data + if (connection.video_udp_pair != null) + { + connection.video_udp_pair.Stop(); + connection.video_udp_pair = null; + } + connection.listener.Dispose(); + + rtsp_list.Remove(connection); + continue; + } + else if (connection.play) current_rtp_play_count++; + } + } + + // Take the YUV image and encode it into a H264 NAL + // This returns a NAL with no headers (no 00 00 00 01 header and no 32 bit sizes) + Console.WriteLine(current_rtp_count + " RTSP clients connected. " + current_rtp_play_count + " RTSP clients in PLAY mode"); + + if (current_rtp_play_count == 0) return; + + // Compress the video (YUV to H264) + byte[] raw_video_nal = h264_encoder.CompressFrame(yuv_data); + Boolean isKeyframe = true; // SimpleH264encoder and TinyH24encoder only emit keyframes + + + List nal_array = new List(); + + // We may want to add the SPS and PPS to the H264 stream as in-band data. + // This may be of use if the client did not parse the SPS/PPS in the SDP + // or if the H264 encoder changes properties (eg a new resolution or framerate which + // gives a new SPS or PPS). + // Also looking towards H265, the VPS/SPS/PPS do not need to be in the SDP so would be added here. + + Boolean add_sps_pps_to_keyframe = true; + + if (add_sps_pps_to_keyframe && isKeyframe) { + nal_array.Add(raw_sps); + nal_array.Add(raw_pps); + } + + // add the rest of the NALs + nal_array.Add(raw_video_nal); + + + + UInt32 rtp_timestamp = timestamp_ms * 90; // 90kHz clock + + // Build a list of 1 or more RTP packets + // The last packet will have the M bit set to '1' + List rtp_packets = new List(); + + for(int x = 0; x < nal_array.Count; x++) { + + byte[] raw_nal = nal_array[x]; + Boolean last_nal = false; + if (x == nal_array.Count - 1) { + last_nal = true; // last NAL in our nal_array + } + + // The H264 Payload could be sent as one large RTP packet (assuming the receiver can handle it) + // or as a Fragmented Data, split over several RTP packets with the same Timestamp. + bool fragmenting = false; + int packetMTU = 65500; + if (raw_nal.Length > packetMTU) fragmenting = true; + + + if (fragmenting == false) + { + // Put the whole NAL into one RTP packet. + // Note some receivers will have maximum buffers and be unable to handle large RTP packets. + // Also with RTP over RTSP there is a limit of 65535 bytes for the RTP packet. + + byte[] rtp_packet = new byte[12 + raw_nal.Length]; // 12 is header size when there are no CSRCs or extensions + // Create an single RTP fragment + + // RTP Packet Header + // 0 - Version, P, X, CC, M, PT and Sequence Number + //32 - Timestamp. H264 uses a 90kHz clock + //64 - SSRC + //96 - CSRCs (optional) + //nn - Extension ID and Length + //nn - Extension header + + int rtp_version = 2; + int rtp_padding = 0; + int rtp_extension = 0; + int rtp_csrc_count = 0; + int rtp_marker = (last_nal == true ? 1 : 0); // set to 1 if the last NAL in the array + int rtp_payload_type = 96; + + RTPPacketUtil.WriteHeader(rtp_packet, rtp_version, rtp_padding, rtp_extension, rtp_csrc_count, rtp_marker, rtp_payload_type); + + UInt32 empty_sequence_id = 0; + RTPPacketUtil.WriteSequenceNumber(rtp_packet, empty_sequence_id); + + RTPPacketUtil.WriteTS(rtp_packet, rtp_timestamp); + + UInt32 empty_ssrc = 0; + RTPPacketUtil.WriteSSRC(rtp_packet, empty_ssrc); + + // Now append the raw NAL + System.Array.Copy(raw_nal, 0, rtp_packet, 12, raw_nal.Length); + + rtp_packets.Add(rtp_packet); + } + else + { + int data_remaining = raw_nal.Length; + int nal_pointer = 0; + int start_bit = 1; + int end_bit = 0; + + // consume first byte of the raw_nal. It is used in the FU header + byte first_byte = raw_nal[0]; + nal_pointer++; + data_remaining--; + + while (data_remaining > 0) + { + int payload_size = Math.Min(packetMTU, data_remaining); + if (data_remaining - payload_size == 0) end_bit = 1; + + byte[] rtp_packet = new byte[12 + 2 + payload_size]; // 12 is header size. 2 bytes for FU-A header. Then payload + + // RTP Packet Header + // 0 - Version, P, X, CC, M, PT and Sequence Number + //32 - Timestamp. H264 uses a 90kHz clock + //64 - SSRC + //96 - CSRCs (optional) + //nn - Extension ID and Length + //nn - Extension header + + int rtp_version = 2; + int rtp_padding = 0; + int rtp_extension = 0; + int rtp_csrc_count = 0; + int rtp_marker = (last_nal == true ? 1 : 0); // Marker set to 1 on last packet + int rtp_payload_type = 96; + + RTPPacketUtil.WriteHeader(rtp_packet, rtp_version, rtp_padding, rtp_extension, rtp_csrc_count, rtp_marker, rtp_payload_type); + + UInt32 empty_sequence_id = 0; + RTPPacketUtil.WriteSequenceNumber(rtp_packet, empty_sequence_id); + + RTPPacketUtil.WriteTS(rtp_packet, rtp_timestamp); + + UInt32 empty_ssrc = 0; + RTPPacketUtil.WriteSSRC(rtp_packet, empty_ssrc); + + // Now append the Fragmentation Header (with Start and End marker) and part of the raw_nal + byte f_bit = 0; + byte nri = (byte)((first_byte >> 5) & 0x03); // Part of the 1st byte of the Raw NAL (NAL Reference ID) + byte type = 28; // FU-A Fragmentation + + rtp_packet[12] = (byte)((f_bit << 7) + (nri << 5) + type); + rtp_packet[13] = (byte)((start_bit << 7) + (end_bit << 6) + (0 << 5) + (first_byte & 0x1F)); + + System.Array.Copy(raw_nal, nal_pointer, rtp_packet, 14, payload_size); + nal_pointer = nal_pointer + payload_size; + data_remaining = data_remaining - payload_size; + + rtp_packets.Add(rtp_packet); + + start_bit = 0; + } + } + } + + lock (rtsp_list) + { + + // Go through each RTSP connection and output the NAL on the Video Session + foreach (RTSPConnection connection in rtsp_list.ToArray()) // ToArray makes a temp copy of the list. + // This lets us delete items in the foreach + // eg when there is Write Error + { + // Only process Sessions in Play Mode + if (connection.play == false) continue; + + String connection_type = ""; + if (connection.video_client_transport.LowerTransport == RtspTransport.LowerTransportType.TCP) connection_type = "TCP"; + if (connection.video_client_transport.LowerTransport == RtspTransport.LowerTransportType.UDP + && connection.video_client_transport.IsMulticast == false) connection_type = "UDP"; + if (connection.video_client_transport.LowerTransport == RtspTransport.LowerTransportType.UDP + && connection.video_client_transport.IsMulticast == true) connection_type = "Multicast"; + Console.WriteLine("Sending video session " + connection.video_session_id + " " + connection_type + " Timestamp(ms)=" + timestamp_ms + ". RTP timestamp=" + rtp_timestamp + ". Sequence="+ connection.video_sequence_number); + + // There could be more than 1 RTP packet (if the data is fragmented) + Boolean write_error = false; + foreach (byte[] rtp_packet in rtp_packets) + { + // Add the specific data for each transmission + RTPPacketUtil.WriteSequenceNumber(rtp_packet, connection.video_sequence_number); + connection.video_sequence_number++; + + // Add the specific SSRC for each transmission + RTPPacketUtil.WriteSSRC(rtp_packet, connection.ssrc); + + + // Send as RTP over RTSP (Interleaved) + if (connection.video_transport_reply.LowerTransport == RtspTransport.LowerTransportType.TCP) + { + int video_channel = connection.video_transport_reply.Interleaved.First; // second is for RTCP status messages) + object state = new object(); + try + { + // send the whole NAL. With RTP over RTSP we do not need to Fragment the NAL (as we do with UDP packets or Multicast) + //session.listener.BeginSendData(video_channel, rtp_packet, new AsyncCallback(session.listener.EndSendData), state); + connection.listener.SendData(video_channel, rtp_packet); + } + catch + { + Console.WriteLine("Error writing to listener " + connection.listener.RemoteAdress); + write_error = true; + break; // exit out of foreach loop + } + } + + // Send as RTP over UDP + if (connection.video_transport_reply.LowerTransport == RtspTransport.LowerTransportType.UDP && connection.video_transport_reply.IsMulticast == false) + { + try + { + // send the whole NAL. ** We could fragment the RTP packet into smaller chuncks that fit within the MTU + // Send to the IP address of the Client + // Send to the UDP Port the Client gave us in the SETUP command + connection.video_udp_pair.Write_To_Data_Port(rtp_packet,connection.client_hostname,connection.video_client_transport.ClientPort.First); + } + catch (Exception e) + { + Console.WriteLine("UDP Write Exception " + e.ToString()); + Console.WriteLine("Error writing to listener " + connection.listener.RemoteAdress); + write_error = true; + break; // exit out of foreach loop + } + } + + // TODO. Add Multicast + } + if (write_error) + { + Console.WriteLine("Removing session " + connection.video_session_id + " due to write error"); + connection.play = false; // stop sending data + if (connection.video_udp_pair != null) { + connection.video_udp_pair.Stop(); + connection.video_udp_pair = null; + } + connection.listener.Dispose(); + rtsp_list.Remove(connection); // remove the session. It is dead + } + } + } + } + + public class RTSPConnection + { + public RtspListener listener = null; // The RTSP client connection + public bool play = false; // set to true when Session is in Play mode + public DateTime time_since_last_rtsp_keepalive = DateTime.UtcNow; // Time since last RTSP message received - used to spot dead UDP clients + public UInt32 ssrc = 0x12345678; // SSRC value used with this client connection + public String client_hostname = ""; // Client Hostname/IP Address + + public String video_session_id = ""; // RTSP Session ID used with this client connection + public UInt16 video_sequence_number = 1; // 16 bit RTP packet sequence number used with this client connection + public RtspTransport video_client_transport; // Transport: string from the client to the server + public RtspTransport video_transport_reply; // Transport: reply from the server to the client + public UDPSocket video_udp_pair = null; // Pair of UDP sockets (data and control) used when sending via UDP + public DateTime video_time_since_last_rtcp_keepalive = DateTime.UtcNow; // Time since last RTCP message received - used to spot dead UDP clients + + // TODO - Add Audio + } + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Server/SimpleH264Encoder.cs b/framework/Inspectron.HawkEye/RTSP/Server/SimpleH264Encoder.cs new file mode 100644 index 0000000..fbc7b01 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Server/SimpleH264Encoder.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; + + +// Simple H264 Encoder +// Written by Jordi Cenzano (www.jordicenzano.name) +// +// Ported to C# by Roger Hardiman www.rjh.org.uk + +// This is a very simple lossless H264 encoder. No compression is used and so the output NAL data is as +// large as the input YUV data. +// It is used for a quick example of H264 encoding in pure .Net without needing OS specific APIs +// or cross compiled C libraries. +// +// SimpleH264Encoder can use any image Width or Height + + +namespace Inspectron.HawkEye.RTSP.Server +{ + public class SimpleH264Encoder + { + CJOCh264encoder h264encoder = null; + + uint width = 0; + uint height = 0; + + List nal = new List(); + + // Constuctor + public SimpleH264Encoder(uint width, uint height, uint fps) + { + // We have the ability to set the aspect ratio (SAR). + // For now we set to 1:1 + uint SARw = 1; + uint SARh = 1; + + // Initialise H264 encoder. The original C++ code writes to a file. In this port it writes to a List + h264encoder = new CJOCh264encoder(nal); + h264encoder.IniCoder(width, height, fps, CJOCh264encoder.enSampleFormat.SAMPLE_FORMAT_YUV420p, SARw, SARh); + + this.width = width; + this.height = height; + + // NAL array will contain SPS and PPS + + } + + // Raw SPS with no Size Header and no 00 00 00 01 headers + public byte[] GetRawSPS() + { + byte[] sps_with_header = h264encoder.sps; + byte[] sps = new byte[sps_with_header.Length - 4]; + System.Array.Copy(sps_with_header, 4, sps, 0, sps.Length); + return sps; + } + + public byte[] GetRawPPS() + { + byte[] pps_with_header = h264encoder.pps; + byte[] pps = new byte[pps_with_header.Length - 4]; + System.Array.Copy(pps_with_header, 4, pps, 0, pps.Length); + return pps; + } + + public byte[] CompressFrame(byte[] yuv_data) + { + byte[] image = h264encoder.GetFramePtr(); + // copy over the YUV image + System.Array.Copy(yuv_data, image, image.Length); + +// // HACK. Set the YUV pixels all to 127 +// for (int hack = 0; hack < image.Length; hack++) image[hack] = 127; + + h264encoder.CodeAndSaveFrame(); + + // Get the NAL (which has the 00 00 00 01 header) + byte[] nal_with_header = h264encoder.nal; + byte[] nal = new byte[nal_with_header.Length - 4]; + System.Array.Copy(nal_with_header, 4, nal, 0, nal.Length); + return nal; + } + + + public void ChangeAnnexBto32BitSize(byte[] data) + { + + if (data.Length < 4) return; + + // change data from 0x00 0x00 0x00 0x01 format to 32 bit size + int len = data.Length - 4;// subtract Annex B header size + + if (BitConverter.IsLittleEndian) + { + data[0] = (byte)((len >> 24) & 0xFF); + data[1] = (byte)((len >> 16) & 0xFF); + data[2] = (byte)((len >> 8) & 0xFF); + data[3] = (byte)((len << 0) & 0xFF); + } + else + { + data[0] = (byte)((len >> 0) & 0xFF); + data[1] = (byte)((len >> 8) & 0xFF); + data[2] = (byte)((len >> 16) & 0xFF); + data[3] = (byte)((len >> 24) & 0xFF); + } + } + } +} + diff --git a/framework/Inspectron.HawkEye/RTSP/Server/TestCard.cs b/framework/Inspectron.HawkEye/RTSP/Server/TestCard.cs new file mode 100644 index 0000000..baa0dca --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Server/TestCard.cs @@ -0,0 +1,209 @@ +using System; +using System.Diagnostics; +using System.Globalization; + +// (c) Roger Hardiman 2016 + +// This class uses a System Timer to generate a YUV image at regular intervals +// The ReceivedYUVFrame event is fired for each new YUV image + +namespace Inspectron.HawkEye.RTSP.Server +{ + public class TestCard + { + + // Events that applications can receive + public event ReceivedYUVFrameHandler ReceivedYUVFrame; + + // Delegated functions (essentially the function prototype) + public delegate void ReceivedYUVFrameHandler(uint timestamp, int width, int height, byte[] data); + + + // Local variables + private System.Timers.Timer frame_timer; + private int fps = 0; + private Stopwatch stopwatch; + private byte[] yuv_frame = null; + private int x_position = 0; + private int y_position = 0; + private int width = 0; + private int height = 0; + private Object generate_lock = new Object(); + private long count = 0; + + // ASCII Font + // Created by Roger Hardiman using an online generation tool + // http://www.riyas.org/2013/12/online-led-matrix-font-generator-with.html + + byte[] ascii_0 = { 0x00, 0x3c, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3c }; + byte[] ascii_1 = { 0x00, 0x08, 0x18, 0x28, 0x08, 0x08, 0x08, 0x3e }; + byte[] ascii_2 = { 0x00, 0x3e, 0x42, 0x02, 0x0c, 0x30, 0x40, 0x7e }; + byte[] ascii_3 = { 0x00, 0x7c, 0x02, 0x02, 0x3c, 0x02, 0x02, 0x7c }; + byte[] ascii_4 = { 0x00, 0x0c, 0x14, 0x24, 0x44, 0x7e, 0x04, 0x04 }; + byte[] ascii_5 = { 0x00, 0x7e, 0x40, 0x40, 0x7c, 0x02, 0x02, 0x7c }; + byte[] ascii_6 = { 0x00, 0x3e, 0x40, 0x40, 0x7c, 0x42, 0x42, 0x3c }; + byte[] ascii_7 = { 0x00, 0x7e, 0x02, 0x02, 0x04, 0x08, 0x10, 0x20 }; + byte[] ascii_8 = { 0x00, 0x3c, 0x42, 0x42, 0x3c, 0x42, 0x42, 0x3c }; + byte[] ascii_9 = { 0x00, 0x3c, 0x42, 0x42, 0x3c, 0x02, 0x02, 0x3e }; + byte[] ascii_colon = { 0x00, 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00 }; + byte[] ascii_space = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + byte[] ascii_dot = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00 }; + + // Constructor + public TestCard(int width, int height, int fps) + { + this.width = width; + this.height = height; + this.fps = fps; + + // YUV size + int y_size = width * height; + int u_size = (width >> 1) * (height >> 1); + int v_size = (width >> 1) * (height >> 1); + yuv_frame = new byte[y_size + u_size + v_size]; + + // Set all values to 127 + for (int x = 0; x < yuv_frame.Length; x++) + { + yuv_frame[x] = 127; + } + + stopwatch = new Stopwatch(); + stopwatch.Start(); + + // Start timer. The Timer will generate each YUV frame + frame_timer = new System.Timers.Timer(); + frame_timer.Interval = 1; // on first pass timer will fire straight away (cannot have zero interval) + frame_timer.AutoReset = false; // do not restart timer after the time has elapsed + frame_timer.Elapsed += (object sender, System.Timers.ElapsedEventArgs e) => + { + // send a frame + Send_YUV_Frame(); + count++; + + // Some CPU cycles will have been used in Sending the YUV Frame. + // Compute the delay required (the Timer Interval) before sending the next YUV frame + long time_for_next_tick_ms = (count * 1000) / fps; + long time_to_wait = time_for_next_tick_ms - stopwatch.ElapsedMilliseconds; + if (time_to_wait <= 0) time_to_wait = 1; // cannot have negative or zero intervals + frame_timer.Interval = time_to_wait; + frame_timer.Start(); + }; + frame_timer.Start(); + + } + + // Dispose + public void Disconnect() + { + // Stop the frame timer + frame_timer.Stop(); + frame_timer.Dispose(); + } + + + private void Send_YUV_Frame() + { + lock (generate_lock) + { + // Get the current time + DateTime now_utc = DateTime.UtcNow; + DateTime now_local = now_utc.ToLocalTime(); + + + long timestamp_ms = ((long)(now_utc.Ticks / TimeSpan.TicksPerMillisecond)); + + // Generate the String to write + char[] overlay = null; + + if (width >= 96) + { + // Need 12 characters of 8x8 pixels. 12*8 = 96 + // HH:MM:SS.mmm + String overlay_str = now_local.ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture); // do not replace : or . by local formats + overlay = overlay_str.ToCharArray(); + } + else + { + // Min for most video formats is 16x16, enough for 2 characters + String overlay_str = now_local.ToString("ss", CultureInfo.InvariantCulture); // do not replace : or . by local formats + overlay = overlay_str.ToCharArray(); + } + + // process each character + int start_row = ((height / 2) - 4); // start 4 pixels above the centre row (4 is half the font height) + for (int c = 0; c < overlay.Length; c++) + { + byte[] font = ascii_space; + if (overlay[c] == '0') font = ascii_0; + if (overlay[c] == '1') font = ascii_1; + if (overlay[c] == '2') font = ascii_2; + if (overlay[c] == '3') font = ascii_3; + if (overlay[c] == '4') font = ascii_4; + if (overlay[c] == '5') font = ascii_5; + if (overlay[c] == '6') font = ascii_6; + if (overlay[c] == '7') font = ascii_7; + if (overlay[c] == '8') font = ascii_8; + if (overlay[c] == '9') font = ascii_9; + if (overlay[c] == ' ') font = ascii_space; + if (overlay[c] == ':') font = ascii_colon; + if (overlay[c] == '.') font = ascii_dot; + + // process the font character + for (int rows = 0; rows < 8; rows++) + { + int y_plane_pos = (start_row * width) + (rows * width) + (c * 8); + byte row_byte = font[rows]; + // bit shift the row byte into individual pixels where the font On/Off maps to Y intensity 50 or 200 + for (int bits = 0; bits < 8; bits++) + { + if ((row_byte & 0x80) == 0x80) + { + // Pixel On + yuv_frame[y_plane_pos] = 200; + } + else + { + yuv_frame[y_plane_pos] = 50; + } + y_plane_pos++; + row_byte = (byte)(row_byte << 1); // shift up so the next 'bit' to process is the most significant bit + } + } + } + + + + + // Toggle the pixel value + byte pixel_value = yuv_frame[(y_position * width) + x_position]; + + // change brightness of pixel + if (pixel_value > 128) pixel_value = 30; + else pixel_value = 230; + + yuv_frame[(y_position * width) + x_position] = pixel_value; + + // move the x and y position + x_position = x_position + 5; + if (x_position >= width) + { + x_position = 0; + y_position = y_position + 1; + } + + if (y_position >= height) + { + y_position = 0; + } + + // fire the Event + if (ReceivedYUVFrame != null) + { + ReceivedYUVFrame((uint)stopwatch.ElapsedMilliseconds, width, height, yuv_frame); + } + } + } + + } +} diff --git a/framework/Inspectron.HawkEye/RTSP/Server/TinyH264Encoder.cs b/framework/Inspectron.HawkEye/RTSP/Server/TinyH264Encoder.cs new file mode 100644 index 0000000..ff739e9 --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/Server/TinyH264Encoder.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Linq; + + +// Tiny H264 Encoder +// World's Smallest h.264 Encoder, by Ben Mesander. +// https://cardinalpeak.com/blog/worlds-smallest-h-264-encoder/ +// +// Ported to C# by Roger Hardiman www.rjh.org.uk + +// Input: YUV image that must be 128x96 +// Output: H264 NAL +// +// This is a very simple lossless H264 encoder. No compression is used and so the output NAL data is as +// large as the input YUV data. +// It is used for a quick example of H264 encoding in pure .Net without needing OS specific APIs +// or cross compiled C libraries. +// +// The H264 SPS/PPS data includes the image size. As the SPS/PPS is hard coded in this example the YUV +// image size must be 128 x 96 + +namespace Inspectron.HawkEye.RTSP.Server +{ + public class TinyH264Encoder + { + + int width = 0; + int height = 0; + int uv_width = 0; + int uv_height = 0; + int y_size = 0; + int u_size = 0; + int v_size = 0; + + byte[] sps = { 0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2 }; + //byte[] sps_b = { 0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2 }; // Annex B + //byte[] sps32 = { 0x00, 0x00, 0x00, 0x07, 0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2 }; // 32 bit size + + byte[] pps = { 0x68, 0xce, 0x38, 0x80 }; + //byte[] pps_b = { 0x00, 0x00, 0x00, 0x01, 0x68, 0xce, 0x38, 0x80 }; // Annex B + //byte[] pps32 = { 0x00, 0x00, 0x00, 0x04, 0x68, 0xce, 0x38, 0x80 }; // 32 bit size + + byte[] slice_header = { 0x05, 0x88, 0x84, 0x21, 0xa0 }; + //byte[] slice_header_b = { 0x00, 0x00, 0x00, 0x01, 0x05, 0x88, 0x84, 0x21, 0xa0 }; + //byte[] slice_header_32 = { 0x00, 0x00, 0x00, 0x00, 0x05, 0x88, 0x84, 0x21, 0xa0 }; // must replace size bytes + byte[] slice_end = { 0x80 }; + byte[] macroblock_header = { 0x0d, 0x00 }; + + List nal = new List(); + + // Constuctor + public TinyH264Encoder() + { + this.width = 128; // Hard coded size that is embedded in the SPS/PPS data + this.height = 96; // Hard coded size that is embedded in the SPS/PPS data + this.uv_width = width >> 1; + this.uv_height = height >> 1; + this.y_size = width * height; + this.u_size = (width >> 1) * (height >> 1); + this.v_size = (width >> 1) * (height >> 1); + } + + public byte[] GetRawSPS() + { + return sps.ToArray(); + } + + public byte[] GetRawPPS() + { + return pps.ToArray(); + } + + public byte[] CompressFrame(byte[] yuv_data) + { + // we can only do 128 x 96 + if (width != 128) return null; + if (height != 96) return null; + + // check size + if (yuv_data.Length < (y_size + u_size + v_size)) + { + // the yuv image is too small. + return null; + } + + nal.Clear(); + + // Slice Header + foreach (byte b in slice_header) nal.Add(b); + + // Add each macro block + for (int i = 0; i < (height / 16); i++) { + for (int j = 0; j < (width / 16); j++) { + macroblock(i, j, yuv_data); + } + } + + // Add slice end + foreach (byte b in slice_end) nal.Add(b); + + byte[] nal_array = nal.ToArray(); + + return nal_array; + } + + /* Write a macroblock's worth of YUV data in I_PCM mode */ + private void macroblock(int i, int j, byte[] frame) + { + int x, y; + + if (!((i == 0) && (j == 0))) + { + foreach (byte b in macroblock_header) nal.Add(b); + } + + for (x = i * 16; x < ((i + 1) * 16); x++) + { + for (y = j * 16; y < ((j + 1) * 16); y++) + { + nal.Add(frame[(x * width) + y]); + } + } + for (x = i * 8; x < (i + 1) * 8; x++) + { + for (y = j * 8; y < (j + 1) * 8; y++) + { + nal.Add(frame[y_size + (x * uv_width) + y]); + } + } + for (x = i * 8; x < (i + 1) * 8; x++) + { + for (y = j * 8; y < (j + 1) * 8; y++) + { + nal.Add(frame[y_size + u_size + (x * uv_width) + y]); + } + } + } + + + public void ChangeAnnexBto32BitSize(byte[] data) + { + + if (data.Length < 4) return; + + // change data from 0x00 0x00 0x00 0x01 format to 32 bit size + int len = data.Length - 4;// subtract Annex B header size + + if (BitConverter.IsLittleEndian) + { + data[0] = (byte)((len >> 24) & 0xFF); + data[1] = (byte)((len >> 16) & 0xFF); + data[2] = (byte)((len >> 8) & 0xFF); + data[3] = (byte)((len << 0) & 0xFF); + } + else + { + data[0] = (byte)((len >> 0) & 0xFF); + data[1] = (byte)((len >> 8) & 0xFF); + data[2] = (byte)((len >> 16) & 0xFF); + data[3] = (byte)((len >> 24) & 0xFF); + } + } + } +} + diff --git a/framework/Inspectron.HawkEye/RTSP/UdpSocket.cs b/framework/Inspectron.HawkEye/RTSP/UdpSocket.cs new file mode 100644 index 0000000..943e8bc --- /dev/null +++ b/framework/Inspectron.HawkEye/RTSP/UdpSocket.cs @@ -0,0 +1,231 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using Inspectron.HawkEye.RTSP.Messages; + +namespace Inspectron.HawkEye.RTSP +{ + public class UDPSocket + { + + private UdpClient data_socket = null; + private UdpClient control_socket = null; + + private Thread data_read_thread = null; + private Thread control_read_thread = null; + + public int data_port = 50000; + public int control_port = 50001; + + bool is_multicast = false; + IPAddress data_mcast_addr; + IPAddress control_mcast_addr; + + /// + /// Initializes a new instance of the class. + /// Creates two new UDP sockets using the start and end Port range + /// + public UDPSocket(int start_port, int end_port) + { + + is_multicast = false; + + // open a pair of UDP sockets - one for data (video or audio) and one for the status channel (RTCP messages) + data_port = start_port; + control_port = start_port + 1; + + bool ok = false; + while (ok == false && (control_port < end_port)) + { + // Video/Audio port must be odd and command even (next one) + try + { + data_socket = new UdpClient(data_port); + control_socket = new UdpClient(control_port); + ok = true; + } + catch (SocketException) + { + // Fail to allocate port, try again + if (data_socket != null) + data_socket.Close(); + if (control_socket != null) + control_socket.Close(); + + // try next data or control port + data_port += 2; + control_port += 2; + } + + if (ok) + { + data_socket.Client.ReceiveBufferSize = 100 * 1024; + data_socket.Client.SendBufferSize = 65535; // default is 8192. Make it as large as possible for large RTP packets which are not fragmented + + control_socket.Client.DontFragment = false; + + } + } + } + + + /// + /// Initializes a new instance of the class. + /// Used with Multicast mode with the Multicast Address and Port + /// + public UDPSocket(String data_multicast_address, int data_multicast_port, String control_multicast_address, int control_multicast_port) + { + + is_multicast = true; + + // open a pair of UDP sockets - one for data (video or audio) and one for the status channel (RTCP messages) + this.data_port = data_multicast_port; + this.control_port = control_multicast_port; + + try + { + IPEndPoint data_ep = new IPEndPoint(IPAddress.Any, data_port); + IPEndPoint control_ep = new IPEndPoint(IPAddress.Any, control_port); + + data_mcast_addr = IPAddress.Parse(data_multicast_address); + control_mcast_addr = IPAddress.Parse(control_multicast_address); + + data_socket = new UdpClient(); + data_socket.Client.Bind(data_ep); + data_socket.JoinMulticastGroup(data_mcast_addr); + + control_socket = new UdpClient(); + control_socket.Client.Bind(control_ep); + control_socket.JoinMulticastGroup(control_mcast_addr); + + + data_socket.Client.ReceiveBufferSize = 100 * 1024; + data_socket.Client.SendBufferSize = 65535; // default is 8192. Make it as large as possible for large RTP packets which are not fragmented + + + control_socket.Client.DontFragment = false; + + } + catch (SocketException) + { + // Fail to allocate port, try again + if (data_socket != null) + data_socket.Close(); + if (control_socket != null) + control_socket.Close(); + + return; + } + } + + /// + /// Starts this instance. + /// + public void Start() + { + if (data_socket == null || control_socket == null) + { + throw new InvalidOperationException("UDP Forwader host was not initialized, can't continue"); + } + + if (data_read_thread != null) + { + throw new InvalidOperationException("Forwarder was stopped, can't restart it"); + } + + data_read_thread = new Thread(() => DoWorkerJob(data_socket, data_port)); + data_read_thread.Name = "DataPort " + data_port; + data_read_thread.Start(); + + control_read_thread = new Thread(() => DoWorkerJob(control_socket, control_port)); + control_read_thread.Name = "ControlPort " + control_port; + control_read_thread.Start(); + } + + /// + /// Stops this instance. + /// + public void Stop() + { + if (is_multicast) + { + // leave the multicast groups + data_socket.DropMulticastGroup(data_mcast_addr); + control_socket.DropMulticastGroup(control_mcast_addr); + } + data_socket.Close(); + control_socket.Close(); + } + + /// + /// Occurs when message is received. + /// + public event EventHandler DataReceived; + + /// + /// Raises the event. + /// + /// The instance containing the event data. + protected void OnDataReceived(RtspChunkEventArgs rtspChunkEventArgs) + { + EventHandler handler = DataReceived; + + if (handler != null) + handler(this, rtspChunkEventArgs); + } + + + /// + /// Does the video job. + /// + private void DoWorkerJob(System.Net.Sockets.UdpClient socket, int data_port) + { + + IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Any, data_port); + try + { + // loop until we get an exception eg the socket closed + while (true) + { + byte[] frame = socket.Receive(ref ipEndPoint); + + // We have an RTP frame. + // Fire the DataReceived event with 'frame' + Console.WriteLine("Received RTP data on port " + data_port); + + RtspChunk currentMessage = new RtspData(); + // aMessage.SourcePort = ?? + currentMessage.Data = frame; + ((RtspData)currentMessage).Channel = data_port; + + + OnDataReceived(new RtspChunkEventArgs(currentMessage)); + + } + } + catch (ObjectDisposedException) + { + } + catch (SocketException) + { + } + } + + /// + /// Write to the RTP Data Port + /// + public void Write_To_Data_Port(byte[] data, String hostname, int port) { + data_socket.Send(data,data.Length, hostname, port); + } + + /// + /// Write to the RTP Control Port + /// + public void Write_To_Control_Port(byte[] data, String hostname, int port) + { + data_socket.Send(data, data.Length, hostname, port); + } + + } +} diff --git a/framework/Inspectron.HawkEye/TCPSocket.cs b/framework/Inspectron.HawkEye/TCPSocket.cs new file mode 100644 index 0000000..48a8e05 --- /dev/null +++ b/framework/Inspectron.HawkEye/TCPSocket.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace Inspectron.HawkEye +{ + public class TCPSocket:IDisposable + { + private Socket _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + private const int bufSize = 8 * 1024; + private State state = new State(); + private EndPoint epFrom = new IPEndPoint(IPAddress.Any, 0); + private AsyncCallback recv = null; + + public class State + { + public byte[] buffer = new byte[bufSize]; + } + + public void Server(string address, int port) + { + _socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.ReuseAddress, true); + _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveBuffer, 10*1024*1024); + + _socket.Bind(new IPEndPoint(IPAddress.Parse(address), port)); + + ReceiveAsync(); + } + + public void Client(string address, int port) + { + _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendBuffer, 10 * 1024 * 1024); + + _socket.Connect(IPAddress.Parse(address), port); + ReceiveAsync(); + } + + public void Send(byte[] data) + { + + //_socket.BeginSend(data, 0, data.Length, SocketFlags.None, (ar) => + //{ + // State so = (State)ar.AsyncState; + // int bytes = _socket.EndSend(ar); + //}, state); + + for (int i = 0; i < Math.Ceiling(data.Length/1050.0); i++) + { + var size = Math.Min(1050, data.Length - i * 1050); + _socket.Send(data,i* 1050, size,SocketFlags.None); + } + + + } + + public event Action Received = delegate { }; + ConcurrentQueue _receivePool = new ConcurrentQueue(); + private void ReceiveAsync() + { + for (int i = 0; i < 1000; i++) + { + _receivePool.Enqueue(new byte[1050]); + } + + _receivePool.TryDequeue(out var buffer); + _socket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref epFrom, recv = (ar) => + { + byte[] so = (byte[])ar.AsyncState; + Task.Run(() => + { + Received(so); + _receivePool.Enqueue(so); + } + ); + int bytes = _socket.EndReceiveFrom(ar, ref epFrom); + + _receivePool.TryDequeue(out var bufferLoc); + _socket.BeginReceiveFrom(bufferLoc, 0, so.Length, SocketFlags.None, ref epFrom, recv, bufferLoc); + + + }, buffer); + } + byte[] _receiveBuffer = new byte[1024*1024]; + public byte[] Receive() + { + int received = 0; + while (received<1024*1024) + { + received+=_socket.Receive(_receiveBuffer); + } + + return _receiveBuffer; + } + + + public void Dispose() + { + _socket?.Dispose(); + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/UDPB/Block.cs b/framework/Inspectron.HawkEye/UDPB/Block.cs new file mode 100644 index 0000000..229694b --- /dev/null +++ b/framework/Inspectron.HawkEye/UDPB/Block.cs @@ -0,0 +1,9 @@ +namespace Inspectron.HawkEye.UDPB +{ + public class Block + { + public byte[] Data; + public uint Length; + public uint MessageNumber; + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/UDPB/UDPBPacket.cs b/framework/Inspectron.HawkEye/UDPB/UDPBPacket.cs new file mode 100644 index 0000000..22b17fc --- /dev/null +++ b/framework/Inspectron.HawkEye/UDPB/UDPBPacket.cs @@ -0,0 +1,91 @@ +using System; +using System.IO; + +namespace Inspectron.HawkEye.UDPB +{ + public class UDPBPacket + { + + public enum EPacketType:uint + { + Data=0, + CloseSequence=1, + BeginSequence=2, + Nak=3, + Ok=4 + } + + public UDPBPacket() + { + } + + private const int sequenceIndex = 0; + private const int messageIndex = 1; + private const int typeIndex = 2; + private const int datalengthIndex = 3; + + public const int packetHeaderSize = 16; + + private uint[] _header=new uint[4]; + + public uint SequenceId + { + get => _header[sequenceIndex]; + set => _header[sequenceIndex] = value; + } + + public uint MessageId + { + get => _header[messageIndex]; + set => _header[messageIndex] = value; + } + + public EPacketType Type + { + get => (EPacketType)_header[typeIndex]; + set => _header[typeIndex] = (uint)value; + } + + private byte[] _payload; + + public byte[] Payload + { + get => _payload??new byte[0]; + set + { + _payload = value; + } + } + public uint Length + { + get => _header[datalengthIndex]; + set => _header[datalengthIndex] = value; + } + + + public byte[] Serialize() + { + + byte[] bytes = new byte[UDPBQueue.PACKET_SIZE+packetHeaderSize]; + Buffer.BlockCopy(_header, 0, bytes, 0, packetHeaderSize); + if(_payload!=null) + Array.Copy(_payload, 0, bytes, packetHeaderSize, _payload.Length); + return bytes; + } + + public void Deserialize(byte[] data,int offset,int len) + { + + Buffer.BlockCopy(data, offset, _header, 0, packetHeaderSize); + var dataLen = _header[datalengthIndex]; + if (dataLen > 0) + { + _payload = new byte[dataLen]; + + + Array.Copy(data, packetHeaderSize+ offset, _payload, 0, (int) dataLen); + } + + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/UDPB/UDPBQueue.cs b/framework/Inspectron.HawkEye/UDPB/UDPBQueue.cs new file mode 100644 index 0000000..f7684e7 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDPB/UDPBQueue.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; + +namespace Inspectron.HawkEye.UDPB +{ + public class UDPBQueue + { + public const int PACKET_SIZE = 1400; + private const int QUEUE_SIZE = 20000; + Block[] _dataQueue = new Block[QUEUE_SIZE]; + private int _queueWritePtr = 0; + private uint _messageId = 1; + private int _queueReadPtr = 0; + public UDPBQueue() + { + for (int i = 0; i < QUEUE_SIZE; i++) + { + _dataQueue[i]=new Block(); + _dataQueue[i].Data=new byte[PACKET_SIZE]; + } + } + + public void Reset() + { + _messageId = 1; + } + public void AddBuffer(byte[] data, int offset, int len) + { + int size = len / PACKET_SIZE; + if ((len % PACKET_SIZE) != 0) + size++; + for (int i = 0; i < size; i++) + { + uint pktlen = (uint)(len - i * PACKET_SIZE); + if (pktlen > PACKET_SIZE) + pktlen = PACKET_SIZE; + Array.Copy(data, i * PACKET_SIZE + offset, _dataQueue[_queueWritePtr].Data,0, pktlen); + _dataQueue[_queueWritePtr].Length = pktlen; + _dataQueue[_queueWritePtr].MessageNumber = _messageId; + + IncrementWrite(); + _messageId++; + if (_messageId == UInt32.MaxValue) _messageId = 1; + } + + + } + + public uint ReadData(ref byte[] data, ref uint msgno) + { + if (_queueReadPtr == _queueWritePtr) + return 0; + + data = _dataQueue[_queueReadPtr].Data; + msgno = _dataQueue[_queueReadPtr].MessageNumber; + uint readlen = _dataQueue[_queueReadPtr].Length; + IncrementRead(); + return readlen; + } + + private void IncrementWrite() + { + _queueWritePtr++; + if (_queueWritePtr == QUEUE_SIZE) _queueWritePtr = 0; + } + private void IncrementRead() + { + _queueReadPtr++; + if (_queueReadPtr == QUEUE_SIZE) _queueReadPtr = 0; + } + + public int PacketsToSend() + { + return _queueReadPtr - _queueWritePtr; + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/UDPB/UDPBReceiveBuffer.cs b/framework/Inspectron.HawkEye/UDPB/UDPBReceiveBuffer.cs new file mode 100644 index 0000000..e41fdd9 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDPB/UDPBReceiveBuffer.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Inspectron.HawkEye.UDPB +{ + public class UDPBReceiveBuffer + { + private readonly UDPBSocket _socket; + private readonly ConcurrentQueue _receiveQueue; + + public UDPBReceiveBuffer(UDPBSocket socket, ConcurrentQueue receiveQueue) + { + _socket = socket; + _receiveQueue = receiveQueue; + } + + List _sequences = new List(); + + public byte[] LastReconstructedBuffer { get; private set; } + + public bool IsSequenceFinished() + { + var res = _sequences.OrderByDescending(x=>x.SequenceId).FirstOrDefault(x => x.IsFinished); + if (res != null) + { + LastReconstructedBuffer = res.LastReconstructedBuffer; + _sequences.Remove(res); + return true; + } + + return false; + } + + public UDPBSequence StartSequence(uint sequence) + { + while (_sequences.Count > 5) + { + _sequences.Remove(_sequences.OrderBy(x => x.SequenceId).First()); + } + var exists = _sequences.FirstOrDefault(x => x.SequenceId == sequence); + if (exists != null) + { + + Console.WriteLine($"Sequence exists {sequence}"); + return exists; + } + + var res = new UDPBSequence(sequence, _socket, _receiveQueue); + _sequences.Add(res); + return res; + } + + public void AddPacketBytes(byte[] data, int offset, int len) + { + var packet = new UDPBPacket(); + packet.Deserialize(data, offset, len); + + + if (packet.Type == UDPBPacket.EPacketType.Data) + { + var seqId = packet.SequenceId; + var sequence = _sequences.FirstOrDefault(x => x.SequenceId == seqId); + if (sequence != null) + { + + sequence.AddData(packet); + + + } + else + { + var s=StartSequence(seqId); + s.AddData(packet); + + } + }else if (packet.Type == UDPBPacket.EPacketType.Ok) + { + var nb = new byte[len]; + Array.Copy(data,offset,nb,0,len); + _receiveQueue.Enqueue(nb); + } + else + { + AddCommand(packet); + } + } + + + + + private void AddCommand(UDPBPacket commandPacket) + { + switch (commandPacket.Type) + { + case UDPBPacket.EPacketType.CloseSequence: + bool finishSuccess; + int dataSize; + var sequence = _sequences.FirstOrDefault(x => x.SequenceId == commandPacket.SequenceId); + if (sequence == null) return; + + finishSuccess = sequence.FinishSequence(commandPacket.MessageId, out var missingPacketIds, + out dataSize); + //inform error + + + + break; + case UDPBPacket.EPacketType.BeginSequence: + StartSequence(commandPacket.SequenceId); + break; + + + } + } + + + + + + + + + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/UDPB/UDPBSequence.cs b/framework/Inspectron.HawkEye/UDPB/UDPBSequence.cs new file mode 100644 index 0000000..eed60b9 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDPB/UDPBSequence.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Inspectron.HawkEye.UDPB +{ + public class UDPBSequence + { + private readonly uint _sequenceId; + private readonly UDPBSocket _socket; + private readonly ConcurrentQueue _receiveQueue; + private readonly byte[] _reconstructionBuffer = new byte[10 * 1024 * 1024]; //10 mb + + + public bool IsFinished { get; private set; } + private readonly UDPBPacket[] _buffer = new UDPBPacket[10000]; + private List _nakPackets; + + public UDPBSequence(uint sequenceId, UDPBSocket socket, ConcurrentQueue receiveQueue) + { + _sequenceId = sequenceId; + _socket = socket; + _receiveQueue = receiveQueue; + } + + public uint SequenceId => _sequenceId; + + + public void AddData(UDPBPacket dataPacket) + { + if (dataPacket.SequenceId != _sequenceId) + { + //error? + Console.WriteLine($"Error! Unexpected packet sequence {dataPacket.SequenceId} expecting {_sequenceId}"); + return; + } + + + _buffer[dataPacket.MessageId] = dataPacket; + + if (IsReconstructing) + { + _nakPackets.Remove(dataPacket.MessageId); + Console.WriteLine($"packet {dataPacket.MessageId} restored"); + + } + + } + + + + public byte[] LastReconstructedBuffer { get; private set; } + + + public bool FinishSequence(uint packets, out List missingPacketIds, + out int dataSize) + { + + missingPacketIds = new List(); + + dataSize = 0; + var defaultDataSize = UDPBQueue.PACKET_SIZE - UDPBPacket.packetHeaderSize; + //validate + for (uint i = 1; i < packets; i++) + if (_buffer[i]==null) + { + missingPacketIds.Add(i); + dataSize += defaultDataSize; + //error + } + else + { + var packetLen = _buffer[i].Payload.Length; + Buffer.BlockCopy(_buffer[i].Payload, 0, _reconstructionBuffer, dataSize, packetLen); + dataSize += packetLen; + } + + if (missingPacketIds.Count > 0) + { + Console.WriteLine($"Error! Missing packets:{missingPacketIds.Count} seq:{_sequenceId}"); + //send Nak + + + + _nakPackets = missingPacketIds; + var nakLine = _nakPackets.Select(x => x.ToString()).Aggregate((s1, s2) => s1 + "," + s2); + Console.WriteLine($"Nak: {nakLine}"); + RequestMissingPackets(missingPacketIds); + RequestAgainIn(TimeSpan.FromMilliseconds(10), missingPacketIds); + IsReconstructing = true; + + return false; + } + else + { + var packet = new UDPBPacket(); + packet.SequenceId = SequenceId; + packet.Type = UDPBPacket.EPacketType.Ok; + IsFinished = true; + Console.WriteLine($"Finished seq:{_sequenceId}"); + _socket.SendPacket(packet); + LastReconstructedBuffer = new byte[dataSize]; + Array.Copy(_reconstructionBuffer, 0, LastReconstructedBuffer, 0, dataSize); + _receiveQueue.Enqueue(LastReconstructedBuffer); + return true; + } + + + } + private bool _shouldRequest=false; + private void RequestAgainIn(TimeSpan timeout, List missingPacketIds) + { + _shouldRequest = true; + + + + } + + private void RequestMissingPackets(List missingPacketIds) + { + MemoryStream ms = new MemoryStream(); + BinaryWriter sw = new BinaryWriter(ms); + var waitFor = missingPacketIds.Take(10).ToList(); + sw.Write(waitFor.Count); + foreach (uint id in waitFor) + { + sw.Write(id); + } + + var packet = new UDPBPacket(); + packet.SequenceId = SequenceId; + packet.Payload = ms.ToArray(); + packet.Length = (uint) packet.Payload.Length; + packet.Type = UDPBPacket.EPacketType.Nak; + _socket.SendPacket(packet); + } + + public bool IsReconstructing { get; private set; } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/UDPB/UDPBSocket.cs b/framework/Inspectron.HawkEye/UDPB/UDPBSocket.cs new file mode 100644 index 0000000..5114f52 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDPB/UDPBSocket.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Inspectron.HawkEye.UDPB +{ + public class UDPBSocket : IDisposable + { + public int LossSimulation { get; set; } + + public EndPoint LastConnection => _lastConnection; + private readonly UDPBQueue _queue; + + private readonly UDPBReceiveBuffer _receiveBuffer; + + private readonly byte[] _receivePacketBuffer = new byte[10 * 1024 * 1024]; + private readonly Random _rnd = new Random(); + private Socket _localSocket; + private uint _currentSquenceId = 1; + + private EndPoint _lastConnection = new IPEndPoint(IPAddress.Any, 27001); + private bool _isServer; + private readonly ConcurrentQueue _receiveQueue = new ConcurrentQueue(); + private bool _isReceiveing = true; + private readonly Thread _receiveThread; + private readonly List _packetCache = new List(); + + public UDPBSocket() + { + _queue = new UDPBQueue(); + _receiveBuffer = new UDPBReceiveBuffer(this, _receiveQueue); + + _receiveThread = new Thread(ReceiveLoop); + } + + public void Dispose() + { + if (!_isServer) _localSocket.Disconnect(false); + _isReceiveing = false; + } + + public static int FindFreePort(IPAddress adapter) + { + var port = 0; + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + try + { + var localEP = new IPEndPoint(adapter, 0); + socket.Bind(localEP); + localEP = (IPEndPoint) socket.LocalEndPoint; + port = localEP.Port; + } + finally + { + socket.Close(); + } + + return port; + } + + public void Listen(IPAddress adapter, int port) + { + Console.WriteLine($"Listen on {adapter}"); + _isServer = true; + _localSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + _localSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.ReuseAddress, true); + //_localSocket.SetSocketOption(SocketOptionLevel.Udp, SocketOptionName.DontFragment, true); + _localSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveBuffer, 10 * 1024 * 1024); + _localSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendBuffer, 10 * 1024 * 1024); + + _localSocket.Bind(new IPEndPoint(adapter, port)); + _receiveThread.Start(); + } + + public void Connect(IPEndPoint endpoint, IPAddress adapter) + { + Console.WriteLine($"Connect to {adapter}"); + _localSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + //_localSocket.SetSocketOption(SocketOptionLevel.Udp, SocketOptionName.DontFragment, true); + _localSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveBuffer, 10 * 1024 * 1024); + _localSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendBuffer, 10 * 1024 * 1024); + + Console.WriteLine($"Binding on {adapter?.MapToIPv4()}"); + + if (adapter != null) + _localSocket.Bind(new IPEndPoint(adapter.MapToIPv4(), 0)); + _localSocket.Connect(endpoint); + _receiveThread.Start(); + } + + + public void SendData(byte[] data) + { + _packetCache.Clear(); + IncrementSequence(); + var begin = new UDPBPacket {Type = UDPBPacket.EPacketType.BeginSequence, SequenceId = _currentSquenceId}; + SendPacket(begin, true); + + + _queue.Reset(); + _queue.AddBuffer(data, 0, data.Length); + var lastId = Flush(); + + + var finish = new UDPBPacket + {Type = UDPBPacket.EPacketType.CloseSequence, MessageId = lastId + 1, SequenceId = _currentSquenceId}; + SendPacket(finish, true); + + + do + { + byte[] dataOK; + + dataOK = Receive(); + + + _localSocket.ReceiveTimeout = 0; + + var packet = new UDPBPacket(); + packet.Deserialize(dataOK, 0, dataOK.Length); + + if (packet.Type == UDPBPacket.EPacketType.Ok) + { + Console.WriteLine("OK"); + + break; + } + + if (packet.Type == UDPBPacket.EPacketType.Nak) + { + var ms = new MemoryStream(packet.Payload); + var br = new BinaryReader(ms); + var packets = br.ReadInt32(); + Console.WriteLine("NAK " + packets); + for (var i = 0; i < packets; i++) + { + var id = br.ReadUInt32(); + var p = _packetCache.First(x => x.MessageId == id); + SendPacket(p, true); + } + + SendPacket(finish, true); + } + } while (true); + } + + + public byte[] Receive() + { + byte[] res; + while (!_receiveQueue.TryDequeue(out res)) Thread.Sleep(1); + + + return res; + } + + internal void SendPacket(UDPBPacket packet, bool noCache = false) + { + if (!noCache) _packetCache.Add(packet); + + if (LossSimulation > 0) + if (_rnd.Next(LossSimulation) == LossSimulation - 1) + return; + + if (_isServer) + _localSocket.SendTo(packet.Serialize(), _lastConnection); + else + _localSocket.Send(packet.Serialize()); + } + + private void ReceiveLoop() + { + while (_isReceiveing) + { + var received = ReceiveFrom(_receivePacketBuffer, 0); + _receiveBuffer.AddPacketBytes(_receivePacketBuffer, 0, received); + } + } + + private int ReceiveFrom(byte[] buffer, int bufferOffset) + { + return _localSocket.ReceiveFrom(buffer, bufferOffset, + UDPBQueue.PACKET_SIZE + UDPBPacket.packetHeaderSize, + SocketFlags.None, ref _lastConnection); + } + + + private void IncrementSequence() + { + _currentSquenceId++; + if (_currentSquenceId == uint.MaxValue) _currentSquenceId = 1; + } + + private uint Flush() + { + uint lastPacketId = 0; + while (_queue.PacketsToSend() != 0) + { + var packet = new UDPBPacket(); + byte[] data = null; + uint msgNo = 0; + var payload = _queue.ReadData(ref data, ref msgNo); + packet.Type = UDPBPacket.EPacketType.Data; + packet.Length = payload; + packet.Payload = data; + packet.MessageId = msgNo; + packet.SequenceId = _currentSquenceId; + SendPacket(packet); + lastPacketId = msgNo; + } + + return lastPacketId; + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/UDPServer.cs b/framework/Inspectron.HawkEye/UDPServer.cs new file mode 100644 index 0000000..a9173a2 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDPServer.cs @@ -0,0 +1,17 @@ +namespace Inspectron.HawkEye +{ + public class UDPServer + { + public UDPServer(string address, int port) + { + UDPSocket server = new UDPSocket(); + server.Server(address, port); + server.Received += Server_Received; + } + + private void Server_Received(byte[] obj) + { + + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/UDPSocket.cs b/framework/Inspectron.HawkEye/UDPSocket.cs new file mode 100644 index 0000000..7d93939 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDPSocket.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace Inspectron.HawkEye +{ + public class UDPSocket:IDisposable + { + private Socket _socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + private const int bufSize = 8 * 1024; + private State state = new State(); + private EndPoint epFrom = new IPEndPoint(IPAddress.Any, 0); + private AsyncCallback recv = null; + + public class State + { + public byte[] buffer = new byte[bufSize]; + } + + public void Server(string address, int port) + { + _socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.ReuseAddress, true); + _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveBuffer, 10*1024*1024); + + _socket.Bind(new IPEndPoint(IPAddress.Parse(address), port)); + + ReceiveAsync(); + } + + public void Client(string address, int port) + { + _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendBuffer, 10 * 1024 * 1024); + + _socket.Connect(IPAddress.Parse(address), port); + ReceiveAsync(); + } + + public void Send(byte[] data) + { + + //_socket.BeginSend(data, 0, data.Length, SocketFlags.None, (ar) => + //{ + // State so = (State)ar.AsyncState; + // int bytes = _socket.EndSend(ar); + //}, state); + + for (int i = 0; i < Math.Ceiling(data.Length/1050.0); i++) + { + var size = Math.Min(1050, data.Length - i * 1050); + _socket.Send(data,i* 1050, size,SocketFlags.None); + } + + + } + + public event Action Received = delegate { }; + ConcurrentQueue _receivePool = new ConcurrentQueue(); + private void ReceiveAsync() + { + for (int i = 0; i < 1000; i++) + { + _receivePool.Enqueue(new byte[1050]); + } + + _receivePool.TryDequeue(out var buffer); + _socket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref epFrom, recv = (ar) => + { + byte[] so = (byte[])ar.AsyncState; + Task.Run(() => + { + Received(so); + _receivePool.Enqueue(so); + } + ); + int bytes = _socket.EndReceiveFrom(ar, ref epFrom); + + _receivePool.TryDequeue(out var bufferLoc); + _socket.BeginReceiveFrom(bufferLoc, 0, so.Length, SocketFlags.None, ref epFrom, recv, bufferLoc); + + + }, buffer); + } + byte[] _receiveBuffer = new byte[1024*1024]; + public byte[] Receive() + { + int received = 0; + while (received<1024*1024) + { + received+=_socket.Receive(_receiveBuffer); + } + + return _receiveBuffer; + } + + + public void Dispose() + { + _socket?.Dispose(); + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/UDT/AckNumber.cs b/framework/Inspectron.HawkEye/UDT/AckNumber.cs new file mode 100644 index 0000000..4b051b3 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/AckNumber.cs @@ -0,0 +1,14 @@ +// UDT ACK Sub-sequence Number: 0 - (2^31 - 1) + +namespace UdtSharp +{ + static class AckNumber + { + public static int incack(int ackno) + { + return (ackno == m_iMaxAckSeqNo) ? 0 : ackno + 1; + } + + public static int m_iMaxAckSeqNo = 0x7FFFFFFF; // maximum ACK sub-sequence number used in UDT + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/UDT/Buffer.cs b/framework/Inspectron.HawkEye/UDT/Buffer.cs new file mode 100644 index 0000000..517ae72 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/Buffer.cs @@ -0,0 +1,476 @@ +using System; +using System.Collections.Generic; + +namespace UdtSharp +{ + public class SndBuffer + { + object m_BufLock = new object(); // used to synchronize buffer operation + + class Block + { + internal byte[] m_pcData; // pointer to the data block + internal int m_iLength; // length of the block + + internal uint m_iMsgNo; // message number + internal ulong m_OriginTime; // original request time + internal int m_iTTL; // time to live (milliseconds) + } + + List mBlockList = new List(); + int m_iLastBlock = 0; + int m_iCurrentBlock = 0; + int m_iFirstBlock = 0; + + uint m_iNextMsgNo; // next message number + + int m_iSize; // buffer size (number of packets) + int m_iMSS; // maximum seqment/packet size + + int m_iCount; // number of used blocks + + public SndBuffer(int size, int mss) + { + m_iSize = size; + m_iMSS = mss; + + // circular linked list for out bound packets + + for (int i = 0; i < m_iSize; ++i) + { + Block block = new Block(); + block.m_iMsgNo = 0; + block.m_pcData = new byte[m_iMSS]; + mBlockList.Add(block); + } + + } + + // Functionality: + // Insert a user buffer into the sending list. + // Parameters: + // 0) [in] data: pointer to the user data block. + // 1) [in] len: size of the block. + // 2) [in] ttl: time to live in milliseconds + // 3) [in] order: if the block should be delivered in order, for DGRAM only + // Returned value: + // None. + public void addBuffer(byte[] data, int offset, int len, int ttl = -1, bool order = false) + { + int size = len / m_iMSS; + if ((len % m_iMSS) != 0) + size++; + + // dynamically increase sender buffer + while (size + m_iCount >= m_iSize) + increase(); + + ulong time = Timer.getTime(); + uint inorder = Convert.ToUInt32(order); + inorder <<= 29; + + for (int i = 0; i < size; ++i) + { + Block s = mBlockList[m_iLastBlock]; + IncrementBlockIndex(ref m_iLastBlock); + int pktlen = len - i * m_iMSS; + if (pktlen > m_iMSS) + pktlen = m_iMSS; + + Array.Copy(data, i * m_iMSS + offset, s.m_pcData, 0, pktlen); + s.m_iLength = pktlen; + s.m_iMsgNo = m_iNextMsgNo | inorder; + if (i == 0) + s.m_iMsgNo |= 0x80000000; + if (i == size - 1) + s.m_iMsgNo |= 0x40000000; + + s.m_OriginTime = time; + s.m_iTTL = ttl; + } + + lock (m_BufLock) + { + m_iCount += size; + } + + m_iNextMsgNo++; + if (m_iNextMsgNo == MessageNumber.m_iMaxMsgNo) + m_iNextMsgNo = 1; + } + + public int readData(ref byte[] data, ref uint msgno) + { + // No data to read + if (m_iCurrentBlock == m_iLastBlock) + return 0; + + data = mBlockList[m_iCurrentBlock].m_pcData; + int readlen = mBlockList[m_iCurrentBlock].m_iLength; + msgno = mBlockList[m_iCurrentBlock].m_iMsgNo; + + IncrementBlockIndex(ref m_iCurrentBlock); + + return readlen; + } + + public int readData(ref byte[] data, int offset, ref uint msgno, out int msglen) + { + msglen = 0; + lock (m_BufLock) + { + int blockIndex = m_iFirstBlock; + IncrementBlockIndex(ref blockIndex, offset); + Block p = mBlockList[blockIndex]; + + if ((p.m_iTTL >= 0) && ((Timer.getTime() - p.m_OriginTime) / 1000 > (ulong)p.m_iTTL)) + { + msgno = p.m_iMsgNo & 0x1FFFFFFF; + + msglen = 1; + + IncrementBlockIndex(ref blockIndex); + p = mBlockList[blockIndex]; + + bool move = false; + while (msgno == (p.m_iMsgNo & 0x1FFFFFFF)) + { + if (blockIndex == m_iCurrentBlock) + move = true; + + IncrementBlockIndex(ref blockIndex); + p = mBlockList[blockIndex]; + + if (move) + m_iCurrentBlock = blockIndex; + msglen++; + } + + return -1; + } + + data = p.m_pcData; + int readlen = p.m_iLength; + msgno = p.m_iMsgNo; + + return readlen; + } + } + + void IncrementBlockIndex(ref int blockIndex, int offset = 1) + { + blockIndex = (blockIndex + offset) % mBlockList.Count; + } + + public void ackData(int offset) + { + lock (m_BufLock) + { + IncrementBlockIndex(ref m_iFirstBlock, offset); + + m_iCount -= offset; + + Timer.triggerEvent(); + } + } + + public int getCurrBufSize() + { + return m_iCount; + } + + void increase() + { + int unitsize = m_iSize; + + for (int i = 0; i < unitsize; ++i) + { + Block block = new Block(); + block.m_iMsgNo = 0; + block.m_pcData = new byte[m_iMSS]; + mBlockList.Add(block); + } + + m_iSize += unitsize; + } + } + + public class RcvBuffer + { + Unit[] m_pUnit; // pointer to the protocol buffer + int m_iSize; // size of the protocol buffer + + int m_iStartPos; // the head position for I/O (inclusive) + int m_iLastAckPos; // the last ACKed position (exclusive) + // EMPTY: m_iStartPos = m_iLastAckPos FULL: m_iStartPos = m_iLastAckPos + 1 + int m_iMaxPos; // the furthest data position + + int m_iNotch; // the starting read point of the first unit + + public RcvBuffer(int bufsize) + { + m_iSize = bufsize; + m_iStartPos = 0; + m_iLastAckPos = 0; + m_iMaxPos = 0; + m_iNotch = 0; + m_pUnit = new Unit[m_iSize]; + for (int i = 0; i < m_iSize; ++i) + m_pUnit[i] = null; + } + + ~RcvBuffer() + { + for (int i = 0; i < m_iSize; ++i) + { + if (null != m_pUnit[i]) + { + m_pUnit[i].m_iFlag = 0; + } + } + } + + public int addData(Unit unit, int offset) + { + int pos = (m_iLastAckPos + offset) % m_iSize; + if (offset > m_iMaxPos) + m_iMaxPos = offset; + + if (null != m_pUnit[pos]) + return -1; + + m_pUnit[pos] = unit; + + unit.m_iFlag = 1; + + return 0; + } + + public int readBuffer(byte[] data, int offset, int len) + { + int p = m_iStartPos; + int lastack = m_iLastAckPos; + int rs = len; + + while ((p != lastack) && (rs > 0)) + { + int unitsize = m_pUnit[p].m_Packet.getLength() - m_iNotch; + if (unitsize > rs) + unitsize = rs; + + unitsize = m_pUnit[p].m_Packet.GetDataBytes(m_iNotch, data, offset, unitsize); + + offset += unitsize; + + if ((rs > unitsize) || (rs == m_pUnit[p].m_Packet.getLength() - m_iNotch)) + { + Unit tmp = m_pUnit[p]; + m_pUnit[p] = null; + tmp.m_iFlag = 0; + + if (++p == m_iSize) + p = 0; + + m_iNotch = 0; + } + else + m_iNotch += rs; + + rs -= unitsize; + } + + m_iStartPos = p; + return len - rs; + } + + public void ackData(int len) + { + m_iLastAckPos = (m_iLastAckPos + len) % m_iSize; + m_iMaxPos -= len; + if (m_iMaxPos < 0) + m_iMaxPos = 0; + + Timer.triggerEvent(); + } + + public int getAvailBufSize() + { + // One slot must be empty in order to tell the difference between "empty buffer" and "full buffer" + return m_iSize - getRcvDataSize() - 1; + } + + public int getRcvDataSize() + { + if (m_iLastAckPos >= m_iStartPos) + return m_iLastAckPos - m_iStartPos; + + return m_iSize + m_iLastAckPos - m_iStartPos; + } + + public void dropMsg(int msgno) + { + for (int i = m_iStartPos, n = (m_iLastAckPos + m_iMaxPos) % m_iSize; i != n; i = (i + 1) % m_iSize) + if ((null != m_pUnit[i]) && (msgno == m_pUnit[i].m_Packet.GetMessageNumber())) + m_pUnit[i].m_iFlag = 3; + } + + public int readMsg(byte[] data, int len) + { + int p = 0; + int q = 0; + bool passack = false; + if (!scanMsg(ref p, ref q, ref passack)) + return 0; + + int rs = len; + int dataOffset = 0; + while (p != (q + 1) % m_iSize) + { + byte[] allData = m_pUnit[p].m_Packet.GetDataBytes(); + int unitsize = allData.Length; + if ((rs >= 0) && (unitsize > rs)) + unitsize = rs; + + if (unitsize > 0) + { + Array.Copy(allData, 0, data, dataOffset, unitsize); + dataOffset += unitsize; + rs -= unitsize; + } + + if (!passack) + { + Unit tmp = m_pUnit[p]; + m_pUnit[p] = null; + tmp.m_iFlag = 0; + } + else + m_pUnit[p].m_iFlag = 2; + + if (++p == m_iSize) + p = 0; + } + + if (!passack) + m_iStartPos = (q + 1) % m_iSize; + + return len - rs; + } + + int getRcvMsgNum() + { + int p = 0; + int q = 0; + bool passack = false; + return scanMsg(ref p, ref q, ref passack) ? 1 : 0; + } + + bool scanMsg(ref int p, ref int q, ref bool passack) + { + // empty buffer + if ((m_iStartPos == m_iLastAckPos) && (m_iMaxPos <= 0)) + return false; + + //skip all bad msgs at the beginning + while (m_iStartPos != m_iLastAckPos) + { + if (null == m_pUnit[m_iStartPos]) + { + if (++m_iStartPos == m_iSize) + m_iStartPos = 0; + continue; + } + + if ((1 == m_pUnit[m_iStartPos].m_iFlag) && (m_pUnit[m_iStartPos].m_Packet.getMsgBoundary() > 1)) + { + bool good = true; + + // look ahead for the whole message + for (int i = m_iStartPos; i != m_iLastAckPos;) + { + if ((null == m_pUnit[i]) || (1 != m_pUnit[i].m_iFlag)) + { + good = false; + break; + } + + if ((m_pUnit[i].m_Packet.getMsgBoundary() == 1) || (m_pUnit[i].m_Packet.getMsgBoundary() == 3)) + break; + + if (++i == m_iSize) + i = 0; + } + + if (good) + break; + } + + Unit tmp = m_pUnit[m_iStartPos]; + m_pUnit[m_iStartPos] = null; + tmp.m_iFlag = 0; + + if (++m_iStartPos == m_iSize) + m_iStartPos = 0; + } + + p = -1; // message head + q = m_iStartPos; // message tail + passack = m_iStartPos == m_iLastAckPos; + bool found = false; + + // looking for the first message + for (int i = 0, n = m_iMaxPos + getRcvDataSize(); i <= n; ++i) + { + if ((null != m_pUnit[q]) && (1 == m_pUnit[q].m_iFlag)) + { + switch (m_pUnit[q].m_Packet.getMsgBoundary()) + { + case 3: // 11 + p = q; + found = true; + break; + + case 2: // 10 + p = q; + break; + + case 1: // 01 + if (p != -1) + found = true; + break; + } + } + else + { + // a hole in this message, not valid, restart search + p = -1; + } + + if (found) + { + // the msg has to be ack'ed or it is allowed to read out of order, and was not read before + if (!passack || !m_pUnit[q].m_Packet.getMsgOrderFlag()) + break; + + found = false; + } + + if (++q == m_iSize) + q = 0; + + if (q == m_iLastAckPos) + passack = true; + } + + // no msg found + if (!found) + { + // if the message is larger than the receiver buffer, return part of the message + if ((p != -1) && ((q + 1) % m_iSize == p)) + found = true; + } + + return found; + } + } +} diff --git a/framework/Inspectron.HawkEye/UDT/Channel.cs b/framework/Inspectron.HawkEye/UDT/Channel.cs new file mode 100644 index 0000000..04aca32 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/Channel.cs @@ -0,0 +1,217 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Text; + +namespace UdtSharp +{ + public class Channel + { + AddressFamily m_iIPversion; // IP version + + Socket m_socket; // socket descriptor + + int m_iSndBufSize; // UDP sending buffer size + int m_iRcvBufSize; + + public Channel() + { + m_iIPversion = AddressFamily.InterNetwork; + m_iSndBufSize = 65536; + m_iRcvBufSize = 65536; + } + + public Channel(AddressFamily addressFamily) + { + m_iIPversion = addressFamily; + m_iSndBufSize = 65536; + m_iRcvBufSize = 65536; + } + + public void open(IPEndPoint addr) + { + // construct a socket + try + { + m_socket = new Socket(m_iIPversion, SocketType.Dgram, ProtocolType.Udp); + } + catch (SocketException e) + { + throw new UdtException(1, 0, e.ErrorCode); + } + + if (null != addr) + { + try + { + m_socket.Bind(addr); + } + catch (SocketException e) + { + throw new UdtException(1, 3, e.ErrorCode); + } + } + else + { + try + { + m_socket.Bind(new IPEndPoint(IPAddress.Any, 0)); + } + catch (SocketException e) + { + throw new UdtException(1, 3, e.ErrorCode); + } + } + + setUDPSockOpt(); + } + + public void open(Socket udpsock) + { + m_socket = udpsock; + setUDPSockOpt(); + } + + void setUDPSockOpt() + { + m_socket.ReceiveBufferSize = m_iRcvBufSize; + m_socket.SendBufferSize = m_iSndBufSize; + } + + public void close() + { + m_socket.Close(); + } + + int getSndBufSize() + { + m_iSndBufSize = (int)m_socket.GetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendBuffer); + return m_iSndBufSize; + } + + int getRcvBufSize() + { + m_iRcvBufSize = (int)m_socket.GetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveBuffer); + return m_iRcvBufSize; + } + + public void setSndBufSize(int size) + { + m_iSndBufSize = size; + } + + public void setRcvBufSize(int size) + { + m_iRcvBufSize = size; + } + + public void getSockAddr(ref IPEndPoint addr) + { + addr = (IPEndPoint)m_socket.LocalEndPoint; + } + + void getPeerAddr(ref IPEndPoint addr) + { + addr = (IPEndPoint)m_socket.RemoteEndPoint; + } + + public int sendto(IPEndPoint addr, Packet packet) + { + TraceSend(addr, packet); + + // convert control information into network order + packet.ConvertControlInfoToNetworkOrder(); + + // convert packet header into network order + packet.ConvertHeaderToNetworkOrder(); + + byte[] data = packet.GetBytes(); + int res = m_socket.SendTo(data, addr); + + // convert back into local host order + packet.ConvertHeaderToHostOrder(); + packet.ConvertControlInfoToHostOrder(); + + return res; + } + + void TraceSend(IPEndPoint destination, Packet packet) + { + return; + StringBuilder sb = new StringBuilder(); + sb.Append(DateTime.Now.ToString("hh:mm:ss.fff")); + sb.AppendFormat(" SND {0} => {1}", m_socket.LocalEndPoint, destination); + sb.AppendLine(); + sb.AppendLine(packet.ToString()); + sb.AppendLine(); + Console.WriteLine(sb.ToString()); + } + + void TraceRecv(IPEndPoint source, Packet packet) + { + return; + StringBuilder sb = new StringBuilder(); + sb.Append(DateTime.Now.ToString("hh:mm:ss.fff")); + sb.AppendFormat(" RCV {0} <= {1}", m_socket.LocalEndPoint, source); + sb.AppendLine(); + sb.AppendLine(packet.ToString()); + sb.AppendLine(); + Console.WriteLine(sb.ToString()); + } + + public int recvfrom(ref IPEndPoint addr, Packet packet) + { + try + { + if (!m_socket.Poll(10000, SelectMode.SelectRead)) + return -1; + } + catch (SocketException sex) + { + return -1; + } + catch (ObjectDisposedException odex) + { + return -1; + } + + byte[] bytes = new byte[Packet.m_iPktHdrSize + packet.getLength()]; + + EndPoint source = addr; + + int res; + try + { + res = m_socket.ReceiveFrom(bytes, ref source); + } + catch (SocketException sex) + { + return -1; + } + catch (ObjectDisposedException odex) + { + return -1; + } + + addr = source as IPEndPoint; + + if (res <= 0) + { + return -1; + } + + bool success = packet.SetHeaderAndDataFromBytes(bytes, res); + if (!success) + return -1; + + // convert back into local host order + packet.ConvertHeaderToHostOrder(); + packet.ConvertControlInfoToHostOrder(); + + TraceRecv(addr, packet); + + return packet.getLength(); + } + } +} diff --git a/framework/Inspectron.HawkEye/UDT/CongestionControl.cs b/framework/Inspectron.HawkEye/UDT/CongestionControl.cs new file mode 100644 index 0000000..6854804 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/CongestionControl.cs @@ -0,0 +1,362 @@ +using System; +using UDTSOCKET = System.Int32; + +namespace UdtSharp +{ + public class CC + { + protected const int m_iSYNInterval = UDT.m_iSYNInterval; // UDT constant parameter, SYN + + public double m_dPktSndPeriod; // Packet sending period, in microseconds + public double m_dCWndSize; // Congestion window size, in packets + + protected int m_iBandwidth; // estimated bandwidth, packets per second + protected double m_dMaxCWndSize; // maximum cwnd size, in packets + + protected int m_iMSS; // Maximum Packet Size, including all packet headers + protected int m_iSndCurrSeqNo; // current maximum seq no sent out + protected int m_iRcvRate; // packet arrive rate at receiver side, packets per second + protected int m_iRTT; // current estimated RTT, microsecond + + protected string m_pcParam; // user defined parameter + + public UDTSOCKET m_UDT; // The UDT entity that this congestion control algorithm is bound to + + public int m_iACKPeriod; // Periodical timer to send an ACK, in milliseconds + public int m_iACKInterval; // How many packets to send one ACK, in packets + + public bool m_bUserDefinedRTO; // if the RTO value is defined by users + public int m_iRTO; // RTO value, microseconds + + PerfMon m_PerfInfo = new PerfMon(); // protocol statistics information + + public CC() + { + m_dPktSndPeriod = 1.0; + m_dCWndSize = 16.0; + m_pcParam = null; + m_iACKPeriod = 0; + m_iACKInterval = 0; + m_bUserDefinedRTO = false; + m_iRTO = -1; + } + + // Functionality: + // Callback function to be called (only) at the start of a UDT connection. + // note that this is different from CCC(), which is always called. + // Parameters: + // None. + // Returned value: + // None. + + public virtual void init() { } + + // Functionality: + // Callback function to be called when a UDT connection is closed. + // Parameters: + // None. + // Returned value: + // None. + + public virtual void close() { } + + // Functionality: + // Callback function to be called when an ACK packet is received. + // Parameters: + // 0) [in] ackno: the data sequence number acknowledged by this ACK. + // Returned value: + // None. + + public virtual void onACK(int seqno) { } + + // Functionality: + // Callback function to be called when a loss report is received. + // Parameters: + // 0) [in] losslist: list of sequence number of packets, in the format describled in packet.cpp. + // 1) [in] size: length of the loss list. + // Returned value: + // None. + + public virtual void onLoss(int[] loss, int length) { } + + // Functionality: + // Callback function to be called when a timeout event occurs. + // Parameters: + // None. + // Returned value: + // None. + + public virtual void onTimeout() { } + + // Functionality: + // Callback function to be called when a data is sent. + // Parameters: + // 0) [in] seqno: the data sequence number. + // 1) [in] size: the payload size. + // Returned value: + // None. + + public virtual void onPktSent(Packet packet) { } + + // Functionality: + // Callback function to be called when a data is received. + // Parameters: + // 0) [in] seqno: the data sequence number. + // 1) [in] size: the payload size. + // Returned value: + // None. + + public virtual void onPktReceived(Packet packet) { } + + // Functionality: + // Callback function to Process a user defined packet. + // Parameters: + // 0) [in] pkt: the user defined packet. + // Returned value: + // None. + + public virtual void processCustomMsg(Packet packet) { } + + + protected void setACKTimer(int msINT) + { + m_iACKPeriod = msINT > m_iSYNInterval ? m_iSYNInterval : msINT; + } + + protected void setACKInterval(int pktINT) + { + m_iACKInterval = pktINT; + } + + protected void setRTO(int usRTO) + { + m_bUserDefinedRTO = true; + m_iRTO = usRTO; + } + + protected void sendCustomMsg(Packet pkt) + { + UDT u = UDT.s_UDTUnited.lookup(m_UDT); + + if (null != u) + { + pkt.SetId(u.m_PeerID); + u.m_pSndQueue.sendto(u.m_pPeerAddr, pkt); + } + } + + protected PerfMon getPerfInfo() + { + try + { + UDT u = UDT.s_UDTUnited.lookup(m_UDT); + if (null != u) + u.sample(m_PerfInfo, false); + } + catch (Exception e) + { + return null; + } + + return m_PerfInfo; + } + + public void setMSS(int mss) + { + m_iMSS = mss; + } + + public void setBandwidth(int bw) + { + m_iBandwidth = bw; + } + + public void setSndCurrSeqNo(int seqno) + { + m_iSndCurrSeqNo = seqno; + } + + public void setRcvRate(int rcvrate) + { + m_iRcvRate = rcvrate; + } + + public void setMaxCWndSize(int cwnd) + { + m_dMaxCWndSize = cwnd; + } + + public void setRTT(int rtt) + { + m_iRTT = rtt; + } + + protected void setUserParam(string param) + { + m_pcParam = param; + } + } + + public class UDTCC : CC + { + int m_iRCInterval; // UDT Rate control interval + ulong m_LastRCTime; // last rate increase time + bool m_bSlowStart; // if in slow start phase + int m_iLastAck; // last ACKed seq no + bool m_bLoss; // if loss happened since last rate increase + int m_iLastDecSeq; // max pkt seq no sent out when last decrease happened + double m_dLastDecPeriod; // value of pktsndperiod when last decrease happened + int m_iNAKCount; // NAK counter + int m_iDecRandom; // random threshold on decrease by number of loss events + int m_iAvgNAKNum; // average number of NAKs per congestion + int m_iDecCount; // number of decreases in a congestion epoch + + static Random m_random = new Random(); + + + public override void init() + { + m_iRCInterval = m_iSYNInterval; + m_LastRCTime = Timer.getTime(); + setACKTimer(m_iRCInterval); + + m_bSlowStart = true; + m_iLastAck = m_iSndCurrSeqNo; + m_bLoss = false; + m_iLastDecSeq = SequenceNumber.decseq(m_iLastAck); + m_dLastDecPeriod = 1; + m_iAvgNAKNum = 0; + m_iNAKCount = 0; + m_iDecRandom = 1; + + m_dCWndSize = 16; + m_dPktSndPeriod = 1; + } + + public override void onACK(int ack) + { + long B = 0; + double inc = 0; + // Note: 1/24/2012 + // The minimum increase parameter is increased from "1.0 / m_iMSS" to 0.01 + // because the original was too small and caused sending rate to stay at low level + // for long time. + const double min_inc = 0.01; + + ulong currtime = Timer.getTime(); + if (currtime - m_LastRCTime < (ulong)m_iRCInterval) + return; + + m_LastRCTime = currtime; + + if (m_bSlowStart) + { + m_dCWndSize += SequenceNumber.seqlen(m_iLastAck, ack); + m_iLastAck = ack; + + if (m_dCWndSize > m_dMaxCWndSize) + { + m_bSlowStart = false; + if (m_iRcvRate > 0) + m_dPktSndPeriod = 1000000.0 / m_iRcvRate; + else + m_dPktSndPeriod = (m_iRTT + m_iRCInterval) / m_dCWndSize; + } + } + else + m_dCWndSize = m_iRcvRate / 1000000.0 * (m_iRTT + m_iRCInterval) + 16; + + // During Slow Start, no rate increase + if (m_bSlowStart) + return; + + if (m_bLoss) + { + m_bLoss = false; + return; + } + + B = (long)(m_iBandwidth - 1000000.0 / m_dPktSndPeriod); + if ((m_dPktSndPeriod > m_dLastDecPeriod) && ((m_iBandwidth / 9) < B)) + B = m_iBandwidth / 9; + if (B <= 0) + inc = min_inc; + else + { + // inc = max(10 ^ ceil(log10( B * MSS * 8 ) * Beta / MSS, 1/MSS) + // Beta = 1.5 * 10^(-6) + + inc = Math.Pow(10.0, Math.Ceiling(Math.Log10(B * m_iMSS * 8.0))) * 0.0000015 / m_iMSS; + + if (inc < min_inc) + inc = min_inc; + } + + m_dPktSndPeriod = (m_dPktSndPeriod * m_iRCInterval) / (m_dPktSndPeriod * inc + m_iRCInterval); + } + + public override void onLoss(int[] losslist, int length) + { + //Slow Start stopped, if it hasn't yet + if (m_bSlowStart) + { + m_bSlowStart = false; + if (m_iRcvRate > 0) + { + // Set the sending rate to the receiving rate. + m_dPktSndPeriod = 1000000.0 / m_iRcvRate; + return; + } + // If no receiving rate is observed, we have to compute the sending + // rate according to the current window size, and decrease it + // using the method below. + m_dPktSndPeriod = m_dCWndSize / (m_iRTT + m_iRCInterval); + } + + m_bLoss = true; + + if (SequenceNumber.seqcmp(losslist[0] & 0x7FFFFFFF, m_iLastDecSeq) > 0) + { + m_dLastDecPeriod = m_dPktSndPeriod; + m_dPktSndPeriod = Math.Ceiling(m_dPktSndPeriod * 1.125); + + m_iAvgNAKNum = (int)Math.Ceiling(m_iAvgNAKNum * 0.875 + m_iNAKCount * 0.125); + m_iNAKCount = 1; + m_iDecCount = 1; + + m_iLastDecSeq = m_iSndCurrSeqNo; + + // remove global synchronization using randomization + m_iDecRandom = (int)Math.Ceiling(m_iAvgNAKNum * m_random.NextDouble()); + if (m_iDecRandom < 1) + m_iDecRandom = 1; + } + else if ((m_iDecCount++ < 5) && (0 == (++m_iNAKCount % m_iDecRandom))) + { + // 0.875^5 = 0.51, rate should not be decreased by more than half within a congestion period + m_dPktSndPeriod = Math.Ceiling(m_dPktSndPeriod * 1.125); + m_iLastDecSeq = m_iSndCurrSeqNo; + } + } + + public override void onTimeout() + { + if (m_bSlowStart) + { + m_bSlowStart = false; + if (m_iRcvRate > 0) + m_dPktSndPeriod = 1000000.0 / m_iRcvRate; + else + m_dPktSndPeriod = m_dCWndSize / (m_iRTT + m_iRCInterval); + } + else + { + /* + m_dLastDecPeriod = m_dPktSndPeriod; + m_dPktSndPeriod = ceil(m_dPktSndPeriod * 2); + m_iLastDecSeq = m_iLastAck; + */ + } + } + } +} diff --git a/framework/Inspectron.HawkEye/UDT/CongestionControlFactory.cs b/framework/Inspectron.HawkEye/UDT/CongestionControlFactory.cs new file mode 100644 index 0000000..8ff0847 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/CongestionControlFactory.cs @@ -0,0 +1,22 @@ +namespace UdtSharp +{ + + public abstract class CCVirtualFactory + { + public abstract CC create(); + public abstract CCVirtualFactory clone(); + } + + public class CCFactory : CCVirtualFactory where T : new() + { + public override CC create() + { + return new T() as CC; + } + + public override CCVirtualFactory clone() + { + return new CCFactory(); + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/UDT/Core.cs b/framework/Inspectron.HawkEye/UDT/Core.cs new file mode 100644 index 0000000..da2d6f5 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/Core.cs @@ -0,0 +1,2530 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Threading; +using UDTSOCKET = System.Int32; + +namespace UdtSharp +{ + public enum UDTOpt + { + UDT_MSS, // the Maximum Transfer Unit + UDT_SNDSYN, // if sending is blocking + UDT_RCVSYN, // if receiving is blocking + UDT_CC, // custom congestion control algorithm + UDT_FC, // Flight flag size (window size) + UDT_SNDBUF, // maximum buffer in sending queue + UDT_RCVBUF, // UDT receiving buffer size + UDT_LINGER, // waiting for unsent data when closing + UDP_SNDBUF, // UDP sending buffer size + UDP_RCVBUF, // UDP receiving buffer size + UDT_MAXMSG, // maximum datagram message size + UDT_MSGTTL, // time-to-live of a datagram message + UDT_RENDEZVOUS, // rendezvous connection mode + UDT_SNDTIMEO, // send() timeout + UDT_RCVTIMEO, // recv() timeout + UDT_REUSEADDR, // reuse an existing port or create a new one + UDT_MAXBW, // maximum bandwidth (bytes per second) that the connection can use + UDT_STATE, // current socket state, see UDTSTATUS, read only + UDT_EVENT, // current avalable events associated with the socket + UDT_SNDDATA, // size of data in the sending buffer + UDT_RCVDATA // size of data available for recv + }; + + public enum UDTSTATUS + { + INIT = 1, + OPENED, + LISTENING, + CONNECTING, + CONNECTED, + BROKEN, + CLOSING, + CLOSED, + NONEXIST + }; + + enum EPOLLOpt + { + // this values are defined same as linux epoll.h + // so that if system values are used by mistake, they should have the same effect + UDT_EPOLL_IN = 0x1, + UDT_EPOLL_OUT = 0x4, + UDT_EPOLL_ERR = 0x8 + }; + + public class PerfMon + { + // global measurements + internal long msTimeStamp; // time since the UDT entity is started, in milliseconds + internal long pktSentTotal; // total number of sent data packets, including retransmissions + internal long pktRecvTotal; // total number of received packets + internal int pktSndLossTotal; // total number of lost packets (sender side) + internal int pktRcvLossTotal; // total number of lost packets (receiver side) + internal int pktRetransTotal; // total number of retransmitted packets + internal int pktSentACKTotal; // total number of sent ACK packets + internal int pktRecvACKTotal; // total number of received ACK packets + internal int pktSentNAKTotal; // total number of sent NAK packets + internal int pktRecvNAKTotal; // total number of received NAK packets + internal long usSndDurationTotal; // total time duration when UDT is sending data (idle time exclusive) + + // local measurements + internal long pktSent; // number of sent data packets, including retransmissions + internal long pktRecv; // number of received packets + internal int pktSndLoss; // number of lost packets (sender side) + internal int pktRcvLoss; // number of lost packets (receiver side) + internal int pktRetrans; // number of retransmitted packets + internal int pktSentACK; // number of sent ACK packets + internal int pktRecvACK; // number of received ACK packets + internal int pktSentNAK; // number of sent NAK packets + internal int pktRecvNAK; // number of received NAK packets + internal double mbpsSendRate; // sending rate in Mb/s + internal double mbpsRecvRate; // receiving rate in Mb/s + internal long usSndDuration; // busy sending time (i.e., idle time exclusive) + + // instant measurements + internal double usPktSndPeriod; // packet sending period, in microseconds + internal int pktFlowWindow; // flow window size, in number of packets + internal int pktCongestionWindow; // congestion window size, in number of packets + internal int pktFlightSize; // number of packets on flight + internal double msRTT; // RTT, in milliseconds + internal double mbpsBandwidth; // estimated bandwidth, in Mb/s + internal int byteAvailSndBuf; // available UDT sender buffer size + internal int byteAvailRcvBuf; // available UDT receiver buffer size + }; + + public class Unit + { + public Packet m_Packet = new Packet(); // packet + public int m_iFlag; // 0: free, 1: occupied, 2: msg read but not freed (out-of-order), 3: msg dropped + }; + + public class UDT + { + public const UDTSOCKET INVALID_SOCK = -1; + public const int ERROR = -1; + const int m_iVersion = 4; + + public static UdtUnited s_UDTUnited = new UdtUnited(); // UDT global management base + + // Identification + public UDTSOCKET m_SocketID; // UDT socket number + public SocketType m_iSockType; // Type of the UDT connection (SOCK_STREAM or SOCK_DGRAM) + public UDTSOCKET m_PeerID; // peer id, for multiplexer + + // Packet sizes + int m_iPktSize; // Maximum/regular packet size, in bytes + public int m_iPayloadSize; // Maximum/regular payload size, in bytes + + // Options + public int m_iMSS; // Maximum Segment Size, in bytes + bool m_bSynSending; // Sending syncronization mode + public bool m_bSynRecving; // Receiving syncronization mode + public int m_iFlightFlagSize; // Maximum number of packets in flight from the peer side + int m_iSndBufSize; // Maximum UDT sender buffer size + int m_iRcvBufSize; // Maximum UDT receiver buffer size + LingerOption m_Linger; // Linger information on close + public int m_iUDPSndBufSize; // UDP sending buffer size + public int m_iUDPRcvBufSize; // UDP receiving buffer size + public AddressFamily m_iIPversion; // IP version + public bool m_bRendezvous; // Rendezvous connection mode + int m_iSndTimeOut; // sending timeout in milliseconds + int m_iRcvTimeOut; // receiving timeout in milliseconds + public bool m_bReuseAddr; // reuse an exiting port or not, for UDP multiplexer + long m_llMaxBW; // maximum data transfer rate (threshold) + + // congestion control + CCVirtualFactory m_pCCFactory; // Factory class to create a specific CC instance + CC m_pCC; // congestion control class + public HashSet m_pCache = new HashSet(); // network information cache + + // Status + volatile bool m_bListening; // If the UDT entit is listening to connection + volatile bool m_bConnecting; // The short phase when connect() is called but not yet completed + public volatile bool m_bConnected; // Whether the connection is on or off + public volatile bool m_bClosing; // If the UDT entity is closing + volatile bool m_bShutdown; // If the peer side has shutdown the connection + public volatile bool m_bBroken; // If the connection has been broken + volatile bool m_bPeerHealth; // If the peer status is normal + bool m_bOpened; // If the UDT entity has been opened + public int m_iBrokenCounter; // a counter (number of GC checks) to let the GC tag this socket as disconnected + + int m_iEXPCount; // Expiration counter + int m_iBandwidth; // Estimated bandwidth, number of packets per second + int m_iRTT; // RTT, in microseconds + int m_iRTTVar; // RTT variance + int m_iDeliveryRate; // Packet arrival rate at the receiver side + + public ulong m_ullLingerExpiration; // Linger expiration time (for GC to close a socket with data in sending buffer) + + public Handshake m_ConnReq = new Handshake(); // connection request + public Handshake m_ConnRes = new Handshake(); // connection response + public long m_llLastReqTime; // last time when a connection request is sent + + // Sending related data + public SndBuffer m_pSndBuffer; // Sender buffer + SndLossList m_pSndLossList; // Sender loss list + PktTimeWindow m_pSndTimeWindow; // Packet sending time window + + /*volatile*/ + ulong m_ullInterval; // Inter-packet time, in CPU clock cycles + ulong m_ullTimeDiff; // aggregate difference in inter-packet time + + volatile int m_iFlowWindowSize; // Flow control window size + /*volatile*/ + double m_dCongestionWindow; // congestion window size + + volatile int m_iSndLastAck; // Last ACK received + volatile int m_iSndLastDataAck; // The real last ACK that updates the sender buffer and loss list + volatile int m_iSndCurrSeqNo; // The largest sequence number that has been sent + int m_iLastDecSeq; // Sequence number sent last decrease occurs + int m_iSndLastAck2; // Last ACK2 sent back + ulong m_ullSndLastAck2Time; // The time when last ACK2 was sent back + + public int m_iISN; // Initial Sequence Number + + // Receiving related data + public RcvBuffer m_pRcvBuffer; // Receiver buffer + RcvLossList m_pRcvLossList; // Receiver loss list + ACKWindow m_pACKWindow; // ACK history window + PktTimeWindow m_pRcvTimeWindow; // Packet arrival time window + + int m_iRcvLastAck; // Last sent ACK + ulong m_ullLastAckTime; // Timestamp of last ACK + int m_iRcvLastAckAck; // Last sent ACK that has been acknowledged + int m_iAckSeqNo; // Last ACK sequence number + int m_iRcvCurrSeqNo; // Largest received sequence number + + ulong m_ullLastWarningTime; // Last time that a warning message is sent + + int m_iPeerISN; // Initial Sequence Number of the peer side + + // synchronization: mutexes and conditions + readonly object m_ConnectionLock = new object(); // used to synchronize connection operation + + readonly EventWaitHandle m_SendBlockCond = new EventWaitHandle(false, EventResetMode.AutoReset); // used to block "send" call + readonly object m_SendBlockLock = new object(); // lock associated to m_SendBlockCond + + readonly object m_AckLock = new object(); // used to protected sender's loss list when processing ACK + + readonly EventWaitHandle m_RecvDataCond = new EventWaitHandle(false, EventResetMode.AutoReset); // used to block "recv" when there is no data + readonly object m_RecvDataLock = new object(); // lock associated to m_RecvDataCond + + readonly object m_SendLock = new object(); // used to synchronize "send" call + readonly object m_RecvLock = new object(); // used to synchronize "recv" call + + // Trace + ulong m_StartTime; // timestamp when the UDT entity is started + long m_llSentTotal; // total number of sent data packets, including retransmissions + long m_llRecvTotal; // total number of received packets + int m_iSndLossTotal; // total number of lost packets (sender side) + int m_iRcvLossTotal; // total number of lost packets (receiver side) + int m_iRetransTotal; // total number of retransmitted packets + int m_iSentACKTotal; // total number of sent ACK packets + int m_iRecvACKTotal; // total number of received ACK packets + int m_iSentNAKTotal; // total number of sent NAK packets + int m_iRecvNAKTotal; // total number of received NAK packets + long m_llSndDurationTotal; // total real time for sending + + ulong m_LastSampleTime; // last performance sample time + long m_llTraceSent; // number of pakctes sent in the last trace interval + long m_llTraceRecv; // number of pakctes received in the last trace interval + int m_iTraceSndLoss; // number of lost packets in the last trace interval (sender side) + int m_iTraceRcvLoss; // number of lost packets in the last trace interval (receiver side) + int m_iTraceRetrans; // number of retransmitted packets in the last trace interval + int m_iSentACK; // number of ACKs sent in the last trace interval + int m_iRecvACK; // number of ACKs received in the last trace interval + int m_iSentNAK; // number of NAKs sent in the last trace interval + int m_iRecvNAK; // number of NAKs received in the last trace interval + long m_llSndDuration; // real time for sending + long m_llSndDurationCounter; // timers to record the sending duration + + // Timers + ulong m_ullCPUFrequency; // CPU clock frequency, used for Timer, ticks per microsecond + + public const int m_iSYNInterval = 10000; // Periodical Rate Control Interval, 10000 microsecond + const int m_iSelfClockInterval = 64; // ACK interval for self-clocking + + ulong m_ullNextACKTime; // Next ACK time, in CPU clock cycles, same below + ulong m_ullNextNAKTime; // Next NAK time + + /*volatile*/ + ulong m_ullSYNInt; // SYN interval + /*volatile*/ + ulong m_ullACKInt; // ACK interval + /*volatile*/ + ulong m_ullNAKInt; // NAK interval + /*volatile*/ + ulong m_ullLastRspTime; // time stamp of last response from the peer + + ulong m_ullMinNakInt; // NAK timeout lower bound; too small value can cause unnecessary retransmission + ulong m_ullMinExpInt; // timeout lower bound threshold: too small timeout can cause problem + + int m_iPktCount; // packet counter for ACK + int m_iLightACKCount; // light ACK counter + + ulong m_ullTargetTime; // scheduled time of next packet sending + + // for UDP multiplexer + public SndQueue m_pSndQueue; // packet sending queue + public RcvQueue m_pRcvQueue; // packet receiving queue + public IPEndPoint m_pPeerAddr; // peer address + public uint[] m_piSelfIP = new uint[4]; // local UDP IP address + public SNode m_pSNode; // node information for UDT list used in snd queue + public RNode m_pRNode; // node information for UDT list used in rcv queue + + public UDT() + { + // Default UDT configurations + m_iMSS = 1500; + m_bSynSending = true; + m_bSynRecving = true; + m_iFlightFlagSize = 204800; + m_iSndBufSize = 65536; + m_iRcvBufSize = 65536; //Rcv buffer MUST NOT be bigger than Flight Flag size + m_Linger = new LingerOption(true, 180); + m_iUDPSndBufSize = 524288; + m_iUDPRcvBufSize = m_iRcvBufSize * m_iMSS; + m_iSockType = SocketType.Stream; + m_iIPversion = AddressFamily.InterNetwork; + m_bRendezvous = false; + m_iSndTimeOut = -1; + m_iRcvTimeOut = -1; + m_bReuseAddr = true; + m_llMaxBW = -1; + + m_pCCFactory = new CCFactory(); + + // Initial status + m_bOpened = false; + m_bListening = false; + m_bConnecting = false; + m_bConnected = false; + m_bClosing = false; + m_bShutdown = false; + m_bBroken = false; + m_bPeerHealth = true; + m_ullLingerExpiration = 0; + } + + public UDT(UDT ancestor) + { + // Default UDT configurations + m_iMSS = ancestor.m_iMSS; + m_bSynSending = ancestor.m_bSynSending; + m_bSynRecving = ancestor.m_bSynRecving; + m_iFlightFlagSize = ancestor.m_iFlightFlagSize; + m_iSndBufSize = ancestor.m_iSndBufSize; + m_iRcvBufSize = ancestor.m_iRcvBufSize; + m_Linger = ancestor.m_Linger; + m_iUDPSndBufSize = ancestor.m_iUDPSndBufSize; + m_iUDPRcvBufSize = ancestor.m_iUDPRcvBufSize; + m_iSockType = ancestor.m_iSockType; + m_iIPversion = ancestor.m_iIPversion; + m_bRendezvous = ancestor.m_bRendezvous; + m_iSndTimeOut = ancestor.m_iSndTimeOut; + m_iRcvTimeOut = ancestor.m_iRcvTimeOut; + m_bReuseAddr = true; // this must be true, because all accepted sockets shared the same port with the listener + m_llMaxBW = ancestor.m_llMaxBW; + + m_pCCFactory = ancestor.m_pCCFactory.clone(); + m_pCC = null; + m_pCache = ancestor.m_pCache; + + // Initial status + m_bOpened = false; + m_bListening = false; + m_bConnecting = false; + m_bConnected = false; + m_bClosing = false; + m_bShutdown = false; + m_bBroken = false; + m_bPeerHealth = true; + m_ullLingerExpiration = 0; + } + + ~UDT() + { + // release mutex/condtion variables + destroySynch(); + + } + + public unsafe void setOpt(UDTOpt optName, int optval, UDTSOCKET socket) + { + setOpt(optName, &optval, socket); + } + + public unsafe void setOpt(UDTOpt optName, void* optval, UDTSOCKET socket) + { + if (m_bBroken || m_bClosing) + throw new UdtException(2, 1, 0); + + lock (m_ConnectionLock) lock(m_SendLock) lock(m_RecvLock) + { + setOpt_unsafe(optName, optval, socket); + } + } + + unsafe void setOpt_unsafe(UDTOpt optName, void* optval, UDTSOCKET socket) + { + switch (optName) + { + case UDTOpt.UDT_MSS: + if (m_bOpened) + throw new UdtException(5, 1, 0); + + if (*(int*)optval < (int)(28 + Handshake.m_iContentSize)) + throw new UdtException(5, 3, 0); + + m_iMSS = *(int*)optval; + + // Packet size cannot be greater than UDP buffer size + if (m_iMSS > m_iUDPSndBufSize) + m_iMSS = m_iUDPSndBufSize; + if (m_iMSS > m_iUDPRcvBufSize) + m_iMSS = m_iUDPRcvBufSize; + + break; + + case UDTOpt.UDT_SNDSYN: + m_bSynSending = *(bool*)optval; + break; + + case UDTOpt.UDT_RCVSYN: + m_bSynRecving = *(bool*)optval; + break; + + case UDTOpt.UDT_CC: + if (m_bConnecting || m_bConnected) + throw new UdtException(5, 1, 0); + //m_pCCFactory = (&(CCCVirtualFactory*)optval).clone(); + + break; + + case UDTOpt.UDT_FC: + if (m_bConnecting || m_bConnected) + throw new UdtException(5, 2, 0); + + if (*(int*)optval < 1) + throw new UdtException(5, 3); + + // Mimimum recv flight flag size is 32 packets + if (*(int*)optval > 32) + m_iFlightFlagSize = *(int*)optval; + else + m_iFlightFlagSize = 32; + + break; + + case UDTOpt.UDT_SNDBUF: + if (m_bOpened) + throw new UdtException(5, 1, 0); + + if (*(int*)optval <= 0) + throw new UdtException(5, 3, 0); + + m_iSndBufSize = *(int*)optval / (m_iMSS - 28); + + break; + + case UDTOpt.UDT_RCVBUF: + if (m_bOpened) + throw new UdtException(5, 1, 0); + + if (*(int*)optval <= 0) + throw new UdtException(5, 3, 0); + + // Mimimum recv buffer size is 32 packets + if (*(int*)optval > (m_iMSS - 28) * 32) + m_iRcvBufSize = *(int*)optval / (m_iMSS - 28); + else + m_iRcvBufSize = 32; + + // recv buffer MUST not be greater than FC size + if (m_iRcvBufSize > m_iFlightFlagSize) + m_iRcvBufSize = m_iFlightFlagSize; + + break; + + case UDTOpt.UDT_LINGER: + m_Linger = ConvertLingerOption.FromVoidPointer(optval); + break; + + case UDTOpt.UDP_SNDBUF: + if (m_bOpened) + throw new UdtException(5, 1, 0); + + m_iUDPSndBufSize = *(int*)optval; + + if (m_iUDPSndBufSize < m_iMSS) + m_iUDPSndBufSize = m_iMSS; + + break; + + case UDTOpt.UDP_RCVBUF: + if (m_bOpened) + throw new UdtException(5, 1, 0); + + m_iUDPRcvBufSize = *(int*)optval; + + if (m_iUDPRcvBufSize < m_iMSS) + m_iUDPRcvBufSize = m_iMSS; + + break; + + case UDTOpt.UDT_RENDEZVOUS: + if (m_bConnecting || m_bConnected) + throw new UdtException(5, 1, 0); + m_bRendezvous = *(bool*)optval; + break; + + case UDTOpt.UDT_SNDTIMEO: + m_iSndTimeOut = *(int*)optval; + break; + + case UDTOpt.UDT_RCVTIMEO: + m_iRcvTimeOut = *(int*)optval; + break; + + case UDTOpt.UDT_REUSEADDR: + if (m_bOpened) + throw new UdtException(5, 1, 0); + m_bReuseAddr = *(bool*)optval; + break; + + case UDTOpt.UDT_MAXBW: + m_llMaxBW = *(long*)optval; + break; + + default: + throw new UdtException(5, 0, 0); + } + } + + public unsafe void getOpt(UDTOpt optName, void* optval, ref int optlen) + { + lock (m_ConnectionLock) + { + getOpt_unsafe(optName, optval, ref optlen); + } + } + + unsafe void getOpt_unsafe(UDTOpt optName, void* optval, ref int optlen) + { + switch (optName) + { + case UDTOpt.UDT_MSS: + *(int*)optval = m_iMSS; + optlen = sizeof(int); + break; + + case UDTOpt.UDT_SNDSYN: + *(bool*)optval = m_bSynSending; + optlen = sizeof(bool); + break; + + case UDTOpt.UDT_RCVSYN: + *(bool*)optval = m_bSynRecving; + optlen = sizeof(bool); + break; + + case UDTOpt.UDT_CC: + if (!m_bOpened) + throw new UdtException(5, 5, 0); + //*(CC**)optval = m_pCC; + //optlen = sizeof(CC*); + + break; + + case UDTOpt.UDT_FC: + *(int*)optval = m_iFlightFlagSize; + optlen = sizeof(int); + break; + + case UDTOpt.UDT_SNDBUF: + *(int*)optval = m_iSndBufSize * (m_iMSS - 28); + optlen = sizeof(int); + break; + + case UDTOpt.UDT_RCVBUF: + *(int*)optval = m_iRcvBufSize * (m_iMSS - 28); + optlen = sizeof(int); + break; + + case UDTOpt.UDT_LINGER: + if (optlen < 5) //?? (int)(sizeof(LingerOption))) + throw new UdtException(5, 3, 0); + ConvertLingerOption.ToVoidPointer(m_Linger, optval); + optlen = 5; //?? + break; + + case UDTOpt.UDP_SNDBUF: + *(int*)optval = m_iUDPSndBufSize; + optlen = sizeof(int); + break; + + case UDTOpt.UDP_RCVBUF: + *(int*)optval = m_iUDPRcvBufSize; + optlen = sizeof(int); + break; + + case UDTOpt.UDT_RENDEZVOUS: + *(bool*)optval = m_bRendezvous; + optlen = sizeof(bool); + break; + + case UDTOpt.UDT_SNDTIMEO: + *(int*)optval = m_iSndTimeOut; + optlen = sizeof(int); + break; + + case UDTOpt.UDT_RCVTIMEO: + *(int*)optval = m_iRcvTimeOut; + optlen = sizeof(int); + break; + + case UDTOpt.UDT_REUSEADDR: + *(bool*)optval = m_bReuseAddr; + optlen = sizeof(bool); + break; + + case UDTOpt.UDT_MAXBW: + *(long*)optval = m_llMaxBW; + optlen = sizeof(long); + break; + + case UDTOpt.UDT_STATE: + *(int*)optval = (int)s_UDTUnited.getStatus(m_SocketID); + optlen = sizeof(int); + break; + + case UDTOpt.UDT_EVENT: + { + int udtevent = 0; + if (m_bBroken) + udtevent |= (int)EPOLLOpt.UDT_EPOLL_ERR; + else + { + if (m_pRcvBuffer != null && (m_pRcvBuffer.getRcvDataSize() > 0)) + udtevent |= (int)EPOLLOpt.UDT_EPOLL_IN; + if (m_pSndBuffer != null && (m_iSndBufSize > m_pSndBuffer.getCurrBufSize())) + udtevent |= (int)EPOLLOpt.UDT_EPOLL_OUT; + } + *(int*)optval = udtevent; + optlen = sizeof(int); + } + break; + + case UDTOpt.UDT_SNDDATA: + if (m_pSndBuffer != null) + *(int*)optval = m_pSndBuffer.getCurrBufSize(); + else + *(int*)optval = 0; + optlen = sizeof(int); + break; + + case UDTOpt.UDT_RCVDATA: + if (m_pRcvBuffer != null) + *(int*)optval = m_pRcvBuffer.getRcvDataSize(); + else + *(int*)optval = 0; + optlen = sizeof(int); + break; + + default: + throw new UdtException(5, 0, 0); + } + } + + public unsafe void open() + { + lock (m_ConnectionLock) + { + open_unsafe(); + } + } + + unsafe void open_unsafe() + { + // Initial sequence number, loss, acknowledgement, etc. + m_iPktSize = m_iMSS - 28; + m_iPayloadSize = m_iPktSize - Packet.m_iPktHdrSize; + + m_iEXPCount = 1; + m_iBandwidth = 1; + m_iDeliveryRate = 16; + m_iAckSeqNo = 0; + m_ullLastAckTime = 0; + + // trace information + + m_StartTime = Timer.getTime(); + m_llSentTotal = m_llRecvTotal = m_iSndLossTotal = m_iRcvLossTotal = m_iRetransTotal = m_iSentACKTotal = m_iRecvACKTotal = m_iSentNAKTotal = m_iRecvNAKTotal = 0; + m_LastSampleTime = Timer.getTime(); + m_llTraceSent = m_llTraceRecv = m_iTraceSndLoss = m_iTraceRcvLoss = m_iTraceRetrans = m_iSentACK = m_iRecvACK = m_iSentNAK = m_iRecvNAK = 0; + m_llSndDuration = m_llSndDurationTotal = 0; + + // structures for queue + if (null == m_pSNode) + m_pSNode = new SNode(); + m_pSNode.m_pUDT = this; + m_pSNode.m_llTimeStamp = 1; + m_pSNode.m_iHeapLoc = -1; + + if (null == m_pRNode) + m_pRNode = new RNode(); + m_pRNode.m_pUDT = this; + m_pRNode.m_llTimeStamp = 1; + //m_pRNode.m_pPrev = m_pRNode.m_pNext = null; + m_pRNode.m_bOnList = false; + + m_iRTT = 10 * m_iSYNInterval; + m_iRTTVar = m_iRTT >> 1; + + m_ullCPUFrequency = Timer.getCPUFrequency(); + + // set up the timers + m_ullSYNInt = m_iSYNInterval * m_ullCPUFrequency; + + // set minimum NAK and EXP timeout to 100ms + m_ullMinNakInt = 300000 * m_ullCPUFrequency; + m_ullMinExpInt = 300000 * m_ullCPUFrequency; + + m_ullACKInt = m_ullSYNInt; + m_ullNAKInt = m_ullMinNakInt; + + + ulong currtime = Timer.rdtsc(); + m_ullLastRspTime = currtime; + m_ullNextACKTime = currtime + m_ullSYNInt; + m_ullNextNAKTime = currtime + m_ullNAKInt; + + m_iPktCount = 0; + m_iLightACKCount = 1; + + m_ullTargetTime = 0; + m_ullTimeDiff = 0; + + // Now UDT is opened. + m_bOpened = true; + } + + public void listen() + { + lock (m_ConnectionLock) + { + listen_unsafe(); + } + } + + void listen_unsafe() + { + if (!m_bOpened) + throw new UdtException(5, 0, 0); + + if (m_bConnecting || m_bConnected) + throw new UdtException(5, 2, 0); + + // listen can be called more than once + if (m_bListening) + return; + + // if there is already another socket listening on the same port + if (m_pRcvQueue.setListener(this) < 0) + throw new UdtException(5, 11, 0); + + m_bListening = true; + } + + public unsafe void connect(IPEndPoint serv_addr) + { + lock (m_ConnectionLock) + { + connect_unsafe(serv_addr); + } + } + + unsafe void connect_unsafe(IPEndPoint serv_addr) + { + if (!m_bOpened) + throw new UdtException(5, 0, 0); + + if (m_bListening) + throw new UdtException(5, 2, 0); + + if (m_bConnecting || m_bConnected) + throw new UdtException(5, 2, 0); + + m_bConnecting = true; + + // record peer/server address + m_pPeerAddr = serv_addr; + + // register this socket in the rendezvous queue + // RendezevousQueue is used to temporarily store incoming handshake, non-rendezvous connections also require this function + ulong ttl = 3000000; + if (m_bRendezvous) + ttl *= 10; + ttl += Timer.getTime(); + m_pRcvQueue.registerConnector(m_SocketID, this, m_iIPversion, serv_addr, ttl); + + // This is my current configurations + m_ConnReq.m_iVersion = m_iVersion; + m_ConnReq.m_iType = m_iSockType; + m_ConnReq.m_iMSS = m_iMSS; + m_ConnReq.m_iFlightFlagSize = (m_iRcvBufSize < m_iFlightFlagSize) ? m_iRcvBufSize : m_iFlightFlagSize; + m_ConnReq.m_iReqType = (!m_bRendezvous) ? 1 : 0; + m_ConnReq.m_iID = m_SocketID; + ConvertIPAddress.ToUintArray(serv_addr.Address, ref m_ConnReq.m_piPeerIP); + + // Random Initial Sequence Number + Random rnd = new Random(); + m_iISN = m_ConnReq.m_iISN = rnd.Next(1, SequenceNumber.m_iMaxSeqNo); + + m_iLastDecSeq = m_iISN - 1; + m_iSndLastAck = m_iISN; + m_iSndLastDataAck = m_iISN; + m_iSndCurrSeqNo = m_iISN - 1; + m_iSndLastAck2 = m_iISN; + m_ullSndLastAck2Time = Timer.getTime(); + + // Inform the server my configurations. + Packet request = new Packet(); + request.pack(m_ConnReq); + // ID = 0, connection request + request.SetId(0); + + m_pSndQueue.sendto(serv_addr, request); + m_llLastReqTime = (long)Timer.getTime(); + + // asynchronous connect, return immediately + if (!m_bSynRecving) + { + return; + } + + // Wait for the negotiated configurations from the peer side. + Packet response = new Packet(); + byte[] resdata = new byte[m_iPayloadSize]; + response.pack(0, resdata); + + UdtException e = new UdtException(0, 0); + + while (!m_bClosing) + { + // avoid sending too many requests, at most 1 request per 250ms + if ((long)Timer.getTime() - m_llLastReqTime > 250000) + { + request.pack(m_ConnReq); + if (m_bRendezvous) + request.SetId(m_ConnRes.m_iID); + m_pSndQueue.sendto(serv_addr, request); + m_llLastReqTime = (long)Timer.getTime(); + } + + response.setLength(m_iPayloadSize); + if (m_pRcvQueue.recvfrom(m_SocketID, response) > 0) + { + if (connect(response) <= 0) + break; + + // new request/response should be sent out immediately on receving a response + m_llLastReqTime = 0; + } + + if (Timer.getTime() > ttl) + { + // timeout + e = new UdtException(1, 1, 0); + break; + } + } + + if (e.getErrorCode() == 0) + { + if (m_bClosing) // if the socket is closed before connection... + e = new UdtException(1); + else if (1002 == m_ConnRes.m_iReqType) // connection request rejected + e = new UdtException(1, 2, 0); + else if ((!m_bRendezvous) && (m_iISN != m_ConnRes.m_iISN)) // secuity check + e = new UdtException(1, 4, 0); + } + + if (e.getErrorCode() != 0) + throw e; + } + + public int connect(Packet response) + { + // this is the 2nd half of a connection request. If the connection is setup successfully this returns 0. + // returning -1 means there is an error. + // returning 1 or 2 means the connection is in process and needs more handshake + + if (!m_bConnecting) + return -1; + + if (m_bRendezvous && ((0 == response.getFlag()) || (1 == response.getType())) && (0 != m_ConnRes.m_iType)) + { + //a data packet or a keep-alive packet comes, which means the peer side is already connected + // in this situation, the previously recorded response will be used + goto POST_CONNECT; + } + + if ((1 != response.getFlag()) || (0 != response.getType())) + return -1; + + m_ConnRes.deserialize(response.GetDataBytes(), response.getLength()); + + if (m_bRendezvous) + { + // regular connect should NOT communicate with rendezvous connect + // rendezvous connect require 3-way handshake + if (1 == m_ConnRes.m_iReqType) + return -1; + + if ((0 == m_ConnReq.m_iReqType) || (0 == m_ConnRes.m_iReqType)) + { + m_ConnReq.m_iReqType = -1; + // the request time must be updated so that the next handshake can be sent out immediately. + m_llLastReqTime = 0; + return 1; + } + } + else + { + // set cookie + if (1 == m_ConnRes.m_iReqType) + { + m_ConnReq.m_iReqType = -1; + m_ConnReq.m_iCookie = m_ConnRes.m_iCookie; + m_llLastReqTime = 0; + return 1; + } + } + + POST_CONNECT: + + // Remove from rendezvous queue + m_pRcvQueue.removeConnector(m_SocketID); + + // Re-configure according to the negotiated values. + m_iMSS = m_ConnRes.m_iMSS; + m_iFlowWindowSize = m_ConnRes.m_iFlightFlagSize; + m_iPktSize = m_iMSS - 28; + m_iPayloadSize = m_iPktSize - Packet.m_iPktHdrSize; + m_iPeerISN = m_ConnRes.m_iISN; + m_iRcvLastAck = m_ConnRes.m_iISN; + m_iRcvLastAckAck = m_ConnRes.m_iISN; + m_iRcvCurrSeqNo = m_ConnRes.m_iISN - 1; + m_PeerID = m_ConnRes.m_iID; + Array.Copy(m_ConnRes.m_piPeerIP, m_piSelfIP, 4); + + // Prepare all data structures + try + { + m_pSndBuffer = new SndBuffer(32, m_iPayloadSize); + m_pRcvBuffer = new RcvBuffer(m_iRcvBufSize); + // after introducing lite ACK, the sndlosslist may not be cleared in time, so it requires twice space. + m_pSndLossList = new SndLossList(m_iFlowWindowSize* 2); + m_pRcvLossList = new RcvLossList(m_iFlightFlagSize); + m_pACKWindow = new ACKWindow(1024); + m_pRcvTimeWindow = new PktTimeWindow(16, 64); + m_pSndTimeWindow = new PktTimeWindow(); + } + catch (Exception e) + { + throw new UdtException(3, 2, 0); + } + + InfoBlock ib = new InfoBlock(m_pPeerAddr.Address); + if (m_pCache.TryGetValue(ib, out ib)) + { + m_iRTT = ib.m_iRTT; + m_iBandwidth = ib.m_iBandwidth; + } + + m_pCC = m_pCCFactory.create(); + m_pCC.m_UDT = m_SocketID; + m_pCC.setMSS(m_iMSS); + m_pCC.setMaxCWndSize(m_iFlowWindowSize); + m_pCC.setSndCurrSeqNo(m_iSndCurrSeqNo); + m_pCC.setRcvRate(m_iDeliveryRate); + m_pCC.setRTT(m_iRTT); + m_pCC.setBandwidth(m_iBandwidth); + m_pCC.init(); + + m_ullInterval = (ulong) (m_pCC.m_dPktSndPeriod* m_ullCPUFrequency); + m_dCongestionWindow = m_pCC.m_dCWndSize; + + // And, I am connected too. + m_bConnecting = false; + m_bConnected = true; + + // register this socket for receiving data packets + m_pRNode.m_bOnList = true; + m_pRcvQueue.setNewEntry(this); + + // acknowledge the management module. + s_UDTUnited.connect_complete(m_SocketID); + + // acknowledde any waiting epolls to write + //s_UDTUnited.m_EPoll.update_events(m_SocketID, m_sPollID, EPOLLOpt.UDT_EPOLL_OUT, true); + + return 0; + } + + public unsafe void connect(IPEndPoint peer, Handshake hs) + { + lock (m_ConnectionLock) + { + connect_unsafe(peer, hs); + } + } + + unsafe void connect_unsafe(IPEndPoint peer, Handshake hs) + { + // Uses the smaller MSS between the peers + if (hs.m_iMSS > m_iMSS) + hs.m_iMSS = m_iMSS; + else + m_iMSS = hs.m_iMSS; + + // exchange info for maximum flow window size + m_iFlowWindowSize = hs.m_iFlightFlagSize; + hs.m_iFlightFlagSize = (m_iRcvBufSize < m_iFlightFlagSize) ? m_iRcvBufSize : m_iFlightFlagSize; + + m_iPeerISN = hs.m_iISN; + + m_iRcvLastAck = hs.m_iISN; + m_iRcvLastAckAck = hs.m_iISN; + m_iRcvCurrSeqNo = hs.m_iISN - 1; + + m_PeerID = hs.m_iID; + hs.m_iID = m_SocketID; + + // use peer's ISN and send it back for security check + m_iISN = hs.m_iISN; + + m_iLastDecSeq = m_iISN - 1; + m_iSndLastAck = m_iISN; + m_iSndLastDataAck = m_iISN; + m_iSndCurrSeqNo = m_iISN - 1; + m_iSndLastAck2 = m_iISN; + m_ullSndLastAck2Time = Timer.getTime(); + + // this is a reponse handshake + hs.m_iReqType = -1; + + // get local IP address and send the peer its IP address (because UDP cannot get local IP address) + Array.Copy(hs.m_piPeerIP, m_piSelfIP, 4); + ConvertIPAddress.ToUintArray(peer.Address, ref hs.m_piPeerIP); + + m_iPktSize = m_iMSS - 28; + m_iPayloadSize = m_iPktSize - Packet.m_iPktHdrSize; + + // Prepare all structures + try + { + m_pSndBuffer = new SndBuffer(32, m_iPayloadSize); + m_pRcvBuffer = new RcvBuffer(m_iRcvBufSize); + m_pSndLossList = new SndLossList(m_iFlowWindowSize * 2); + m_pRcvLossList = new RcvLossList(m_iFlightFlagSize); + m_pACKWindow = new ACKWindow(1024); + m_pRcvTimeWindow = new PktTimeWindow(16, 64); + m_pSndTimeWindow = new PktTimeWindow(); + } + catch (Exception e) + { + throw new UdtException(3, 2, 0); + } + + InfoBlock ib = new InfoBlock(peer.Address); + if (m_pCache.TryGetValue(ib, out ib)) + { + m_iRTT = ib.m_iRTT; + m_iBandwidth = ib.m_iBandwidth; + } + + m_pCC = m_pCCFactory.create(); + m_pCC.m_UDT = m_SocketID; + m_pCC.setMSS(m_iMSS); + m_pCC.setMaxCWndSize(m_iFlowWindowSize); + m_pCC.setSndCurrSeqNo(m_iSndCurrSeqNo); + m_pCC.setRcvRate(m_iDeliveryRate); + m_pCC.setRTT(m_iRTT); + m_pCC.setBandwidth(m_iBandwidth); + m_pCC.init(); + + m_ullInterval = (ulong)(m_pCC.m_dPktSndPeriod * m_ullCPUFrequency); + m_dCongestionWindow = m_pCC.m_dCWndSize; + + m_pPeerAddr = peer; + + // And of course, it is connected. + m_bConnected = true; + + // register this socket for receiving data packets + m_pRNode.m_bOnList = true; + m_pRcvQueue.setNewEntry(this); + + //send the response to the peer, see listen() for more discussions about this + Packet response = new Packet(); + response.pack(hs); + response.SetId(m_PeerID); + m_pSndQueue.sendto(peer, response); + } + + public unsafe void close() + { + if (!m_bOpened) + return; + + if (m_Linger.Enabled) + { + ulong entertime = Timer.getTime(); + + while (!m_bBroken && m_bConnected && (m_pSndBuffer.getCurrBufSize() > 0) && (Timer.getTime() - entertime < (ulong)m_Linger.LingerTime * 1000000)) + { + // linger has been checked by previous close() call and has expired + if (m_ullLingerExpiration >= entertime) + break; + + if (!m_bSynSending) + { + // if this socket enables asynchronous sending, return immediately and let GC to close it later + if (0 == m_ullLingerExpiration) + m_ullLingerExpiration = entertime + (ulong)m_Linger.LingerTime * 1000000; + + return; + } + + System.Threading.Thread.Sleep(1); + } + } + + // remove this socket from the snd queue + if (m_bConnected) + m_pSndQueue.m_pSndUList.remove(this); + + // trigger any pending IO events. + //s_UDTUnited.m_EPoll.update_events(m_SocketID, m_sPollID, EPOLLOpt.UDT_EPOLL_ERR, true); + // then remove itself from all epoll monitoring + //try + //{ + // for (set.iterator i = m_sPollID.begin(); i != m_sPollID.end(); ++i) + // s_UDTUnited.m_EPoll.remove_usock(* i, m_SocketID); + //} + //catch (Exception e) + //{ + //} + + if (!m_bOpened) + return; + + // Inform the threads handler to stop. + m_bClosing = true; + + lock (m_ConnectionLock) + { + close_unsafe(); + } + + // waiting all send and recv calls to stop + lock (m_SendLock) lock(m_RecvLock) + { } + + // CLOSED. + m_bOpened = false; + } + + unsafe void close_unsafe() + { + // Signal the sender and recver if they are waiting for data. + releaseSynch(); + + if (m_bListening) + { + m_bListening = false; + m_pRcvQueue.removeListener(this); + } + else if (m_bConnecting) + { + m_pRcvQueue.removeConnector(m_SocketID); + } + + if (m_bConnected) + { + if (!m_bShutdown) + sendCtrl(5); + + m_pCC.close(); + + // Store current connection information. + InfoBlock ib = new InfoBlock(m_pPeerAddr.Address); + if (!m_pCache.Add(ib)) + { + m_pCache.TryGetValue(ib, out ib); + } + ib.m_iRTT = m_iRTT; + ib.m_iBandwidth = m_iBandwidth; + + m_bConnected = false; + } + + } + + public int send(byte[] data, int offset, int len) + { + + if (SocketType.Dgram == m_iSockType) + return sendmsg(data, offset, len, 255, true); + //throw new UdtException(5, 10, 0); + + // throw an exception if not connected + if (m_bBroken || m_bClosing) + throw new UdtException(2, 1, 0); + else if (!m_bConnected) + throw new UdtException(2, 2, 0); + + if (len <= 0) + return 0; + + if (offset + len > data.Length) + len = data.Length - offset; + + lock (m_SendLock) + { + return send_unsafe(data, offset, len); + } + } + + int send_unsafe(byte[] data, int offset, int len) + { + if (m_pSndBuffer.getCurrBufSize() == 0) + { + // delay the EXP timer to avoid mis-fired timeout + ulong currtime = Timer.rdtsc(); + m_ullLastRspTime = currtime; + } + + if (m_iSndBufSize <= m_pSndBuffer.getCurrBufSize()) + { + if (!m_bSynSending) + throw new UdtException(6, 1, 0); + else + { + // wait here during a blocking sending + + if (m_iSndTimeOut < 0) + { + while (!m_bBroken && m_bConnected && !m_bClosing && (m_iSndBufSize <= m_pSndBuffer.getCurrBufSize()) && m_bPeerHealth) + m_SendBlockCond.WaitOne(Timeout.Infinite); + } + else + { + ulong exptime = Timer.getTime() + (ulong)m_iSndTimeOut * 1000; + + while (!m_bBroken && m_bConnected && !m_bClosing && (m_iSndBufSize <= m_pSndBuffer.getCurrBufSize()) && m_bPeerHealth && (Timer.getTime() < exptime)) + m_SendBlockCond.WaitOne((int)(exptime - Timer.getTime()) / 1000); + } + + // check the connection status + if (m_bBroken || m_bClosing) + throw new UdtException(2, 1, 0); + else if (!m_bConnected) + throw new UdtException(2, 2, 0); + else if (!m_bPeerHealth) + { + m_bPeerHealth = true; + throw new UdtException(7); + } + } + } + + if (m_iSndBufSize <= m_pSndBuffer.getCurrBufSize()) + { + if (m_iSndTimeOut >= 0) + throw new UdtException(6, 3, 0); + + return 0; + } + + int size = (m_iSndBufSize - m_pSndBuffer.getCurrBufSize()) * m_iPayloadSize; + if (size > len) + size = len; + + // record total time used for sending + if (0 == m_pSndBuffer.getCurrBufSize()) + m_llSndDurationCounter = (long)Timer.getTime(); + + // insert the user buffer into the sening list + m_pSndBuffer.addBuffer(data, offset, size); + + // insert this socket to snd list if it is not on the list yet + m_pSndQueue.m_pSndUList.update(this, false); + + //if (m_iSndBufSize <= m_pSndBuffer.getCurrBufSize()) + //{ + // // write is not available any more + // s_UDTUnited.m_EPoll.update_events(m_SocketID, m_sPollID, EPOLLOpt.UDT_EPOLL_OUT, false); + //} + + return size; + } + + public int recv(byte[] data, int offset, int len) + { + if (SocketType.Dgram == m_iSockType) + throw new UdtException(5, 10, 0); + + + // throw an exception if not connected + if (!m_bConnected) + throw new UdtException(2, 2, 0); + else if ((m_bBroken || m_bClosing) && (0 == m_pRcvBuffer.getRcvDataSize())) + throw new UdtException(2, 1, 0); + + if (len <= 0) + return 0; + + lock (m_RecvLock) + { + return recv_unsafe(data, offset, len); + } + } + + int recv_unsafe(byte[] data, int offset, int len) + { + if (0 == m_pRcvBuffer.getRcvDataSize()) + { + if (!m_bSynRecving) + throw new UdtException(6, 2, 0); + else + { + if (m_iRcvTimeOut < 0) + { + while (!m_bBroken && m_bConnected && !m_bClosing && (0 == m_pRcvBuffer.getRcvDataSize())) + m_RecvDataCond.WaitOne(Timeout.Infinite); + } + else + { + ulong enter_time = Timer.getTime(); + + while (!m_bBroken && m_bConnected && !m_bClosing && (0 == m_pRcvBuffer.getRcvDataSize())) + { + int diff = (int)(Timer.getTime() - enter_time) / 1000; + if (diff >= m_iRcvTimeOut) + break; + m_RecvDataCond.WaitOne(m_iRcvTimeOut - diff); + } + } + } + } + + // throw an exception if not connected + if (!m_bConnected) + throw new UdtException(2, 2, 0); + else if ((m_bBroken || m_bClosing) && (0 == m_pRcvBuffer.getRcvDataSize())) + throw new UdtException(2, 1, 0); + + int res = m_pRcvBuffer.readBuffer(data, offset, len); + + //if (m_pRcvBuffer.getRcvDataSize() <= 0) + //{ + // // read is not available any more + // s_UDTUnited.m_EPoll.update_events(m_SocketID, m_sPollID, UDT_EPOLL_IN, false); + //} + + if ((res <= 0) && (m_iRcvTimeOut >= 0)) + throw new UdtException(6, 3, 0); + + return res; + } + + public int sendmsg(byte[] data, int offset, int len, int msttl, bool inorder) + { + if (SocketType.Stream == m_iSockType) + throw new UdtException(5, 9, 0); + + // throw an exception if not connected + if (m_bBroken || m_bClosing) + throw new UdtException(2, 1, 0); + else if (!m_bConnected) + throw new UdtException(2, 2, 0); + + if (len <= 0) + return 0; + + if (len + offset > data.Length) + len = data.Length - offset; + + if (len > m_iSndBufSize * m_iPayloadSize) + throw new UdtException(5, 12, 0); + + lock (m_SendLock) + { + return sendmsg_unsafe(data, offset, len, msttl, inorder); + } + } + + int sendmsg_unsafe(byte[] data, int offset, int len, int msttl, bool inorder) + { + if (m_pSndBuffer.getCurrBufSize() == 0) + { + // delay the EXP timer to avoid mis-fired timeout + m_ullLastRspTime = Timer.rdtsc(); + } + + if ((m_iSndBufSize - m_pSndBuffer.getCurrBufSize()) * m_iPayloadSize < len) + { + if (!m_bSynSending) + throw new UdtException(6, 1, 0); + else + { + // wait here during a blocking sending + if (m_iSndTimeOut < 0) + { + while (!m_bBroken && m_bConnected && !m_bClosing && ((m_iSndBufSize - m_pSndBuffer.getCurrBufSize()) * m_iPayloadSize < len)) + m_SendBlockCond.WaitOne(Timeout.Infinite); + } + else + { + ulong exptime = Timer.getTime() + (ulong)m_iSndTimeOut * 1000; + + while (!m_bBroken && m_bConnected && !m_bClosing && ((m_iSndBufSize - m_pSndBuffer.getCurrBufSize()) * m_iPayloadSize < len) && (Timer.getTime() < exptime)) + m_SendBlockCond.WaitOne((int)(exptime - Timer.getTime()) / 1000); + } + + // check the connection status + if (m_bBroken || m_bClosing) + throw new UdtException(2, 1, 0); + else if (!m_bConnected) + throw new UdtException(2, 2, 0); + } + } + + if ((m_iSndBufSize - m_pSndBuffer.getCurrBufSize()) * m_iPayloadSize < len) + { + if (m_iSndTimeOut >= 0) + throw new UdtException(6, 3, 0); + + return 0; + } + + // record total time used for sending + if (0 == m_pSndBuffer.getCurrBufSize()) + m_llSndDurationCounter = (long)Timer.getTime(); + + // insert the user buffer into the sening list + m_pSndBuffer.addBuffer(data, offset, len, msttl, inorder); + + // insert this socket to the snd list if it is not on the list yet + m_pSndQueue.m_pSndUList.update(this, false); + + //if (m_iSndBufSize <= m_pSndBuffer.getCurrBufSize()) + //{ + // // write is not available any more + // s_UDTUnited.m_EPoll.update_events(m_SocketID, m_sPollID, UDT_EPOLL_OUT, false); + //} + + return len; + } + + public int recvmsg(byte[] data, int len) + { + if (SocketType.Stream == m_iSockType) + throw new UdtException(5, 9, 0); + + // throw an exception if not connected + if (!m_bConnected) + throw new UdtException(2, 2, 0); + + if (len <= 0) + return 0; + + lock (m_RecvLock) + { + return recvmsg_unsafe(data, len); + } + } + + int recvmsg_unsafe(byte[] data, int len) + { + int res = 0; + if (m_bBroken || m_bClosing) + { + res = m_pRcvBuffer.readMsg(data, len); + + //if (m_pRcvBuffer.getRcvMsgNum() <= 0) + //{ + // // read is not available any more + // s_UDTUnited.m_EPoll.update_events(m_SocketID, m_sPollID, EPOLLOpt.UDT_EPOLL_IN, false); + //} + + if (0 == res) + throw new UdtException(2, 1, 0); + else + return res; + } + + if (!m_bSynRecving) + { + res = m_pRcvBuffer.readMsg(data, len); + if (0 == res) + throw new UdtException(6, 2, 0); + else + return res; + } + + bool timeout = false; + + do + { + if (m_iRcvTimeOut < 0) + { + while (!m_bBroken && m_bConnected && !m_bClosing && (0 == (res = m_pRcvBuffer.readMsg(data, len)))) + m_RecvDataCond.WaitOne(Timeout.Infinite); + } + else + { + timeout = !m_RecvDataCond.WaitOne(m_iRcvTimeOut); + res = m_pRcvBuffer.readMsg(data, len); + } + + if (m_bBroken || m_bClosing) + throw new UdtException(2, 1, 0); + else if (!m_bConnected) + throw new UdtException(2, 2, 0); + } + while ((0 == res) && !timeout); + + //if (m_pRcvBuffer.getRcvMsgNum() <= 0) + //{ + // // read is not available any more + // s_UDTUnited.m_EPoll.update_events(m_SocketID, m_sPollID, UDT_EPOLL_IN, false); + //} + + if ((res <= 0) && (m_iRcvTimeOut >= 0)) + throw new UdtException(6, 3, 0); + + return res; + } + + public void sample(PerfMon perf, bool clear) + { + if (!m_bConnected) + throw new UdtException(2, 2, 0); + if (m_bBroken || m_bClosing) + throw new UdtException(2, 1, 0); + + ulong currtime = Timer.getTime(); + perf.msTimeStamp = (long)(currtime - m_StartTime) / 1000; + + perf.pktSent = m_llTraceSent; + perf.pktRecv = m_llTraceRecv; + perf.pktSndLoss = m_iTraceSndLoss; + perf.pktRcvLoss = m_iTraceRcvLoss; + perf.pktRetrans = m_iTraceRetrans; + perf.pktSentACK = m_iSentACK; + perf.pktRecvACK = m_iRecvACK; + perf.pktSentNAK = m_iSentNAK; + perf.pktRecvNAK = m_iRecvNAK; + perf.usSndDuration = m_llSndDuration; + + perf.pktSentTotal = m_llSentTotal; + perf.pktRecvTotal = m_llRecvTotal; + perf.pktSndLossTotal = m_iSndLossTotal; + perf.pktRcvLossTotal = m_iRcvLossTotal; + perf.pktRetransTotal = m_iRetransTotal; + perf.pktSentACKTotal = m_iSentACKTotal; + perf.pktRecvACKTotal = m_iRecvACKTotal; + perf.pktSentNAKTotal = m_iSentNAKTotal; + perf.pktRecvNAKTotal = m_iRecvNAKTotal; + perf.usSndDurationTotal = m_llSndDurationTotal; + + double interval = (double)(currtime - m_LastSampleTime); + + perf.mbpsSendRate = (double)(m_llTraceSent) * m_iPayloadSize * 8.0 / interval; + perf.mbpsRecvRate = (double)(m_llTraceRecv) * m_iPayloadSize * 8.0 / interval; + + perf.usPktSndPeriod = m_ullInterval / (double)m_ullCPUFrequency; + perf.pktFlowWindow = m_iFlowWindowSize; + perf.pktCongestionWindow = (int)m_dCongestionWindow; + perf.pktFlightSize = SequenceNumber.seqlen(m_iSndLastAck, SequenceNumber.incseq(m_iSndCurrSeqNo)) - 1; + perf.msRTT = m_iRTT / 1000.0; + perf.mbpsBandwidth = m_iBandwidth * m_iPayloadSize * 8.0 / 1000000.0; + + if (Monitor.TryEnter(m_ConnectionLock)) + { + perf.byteAvailSndBuf = (null == m_pSndBuffer) ? 0 : (m_iSndBufSize - m_pSndBuffer.getCurrBufSize()) * m_iMSS; + perf.byteAvailRcvBuf = (null == m_pRcvBuffer) ? 0 : m_pRcvBuffer.getAvailBufSize() * m_iMSS; + + Monitor.Exit(m_ConnectionLock); + } + else + { + perf.byteAvailSndBuf = 0; + perf.byteAvailRcvBuf = 0; + } + + if (clear) + { + m_llTraceSent = m_llTraceRecv = m_iTraceSndLoss = m_iTraceRcvLoss = m_iTraceRetrans = m_iSentACK = m_iRecvACK = m_iSentNAK = m_iRecvNAK = 0; + m_llSndDuration = 0; + m_LastSampleTime = currtime; + } + } + + void CCUpdate() + { + m_ullInterval = (ulong)(m_pCC.m_dPktSndPeriod * m_ullCPUFrequency); + m_dCongestionWindow = m_pCC.m_dCWndSize; + + if (m_llMaxBW <= 0) + return; + double minSP = 1000000.0 / ((double)m_llMaxBW / m_iMSS) * m_ullCPUFrequency; + if (m_ullInterval < minSP) + m_ullInterval = (ulong)minSP; + } + + + void destroySynch() + { + m_SendBlockCond.Close(); + m_RecvDataCond.Close(); + } + + void releaseSynch() + { + m_SendBlockCond.Set(); + + bool gotLock = false; + try + { + Monitor.Enter(m_SendLock, ref gotLock); + } + finally + { + if (gotLock) + Monitor.Exit(m_SendLock); + } + + m_RecvDataCond.Set(); + + gotLock = false; + try + { + Monitor.Enter(m_RecvLock, ref gotLock); + } + finally + { + if (gotLock) + Monitor.Exit(m_RecvLock); + } + } + + unsafe void sendCtrl(int pkttype, void* lparam = null, void* rparam = null, int size = 0) + { + Packet ctrlpkt = new Packet(); + + switch (pkttype) + { + case 2: //010 - Acknowledgement + { + int ack; + + // If there is no loss, the ACK is the current largest sequence number plus 1; + // Otherwise it is the smallest sequence number in the receiver loss list. + if (0 == m_pRcvLossList.getLossLength()) + ack = SequenceNumber.incseq(m_iRcvCurrSeqNo); + else + ack = m_pRcvLossList.getFirstLostSeq(); + + if (ack == m_iRcvLastAckAck) + break; + + // send out a lite ACK + // to save time on buffer processing and bandwidth/AS measurement, a lite ACK only feeds back an ACK number + if (4 == size) + { + ctrlpkt.pack(pkttype, null, &ack, size); + ctrlpkt.SetId(m_PeerID); + m_pSndQueue.sendto(m_pPeerAddr, ctrlpkt); + + break; + } + + ulong currtime = Timer.rdtsc(); + + // There are new received packets to acknowledge, update related information. + if (SequenceNumber.seqcmp(ack, m_iRcvLastAck) > 0) + { + int acksize = SequenceNumber.seqoff(m_iRcvLastAck, ack); + + m_iRcvLastAck = ack; + + m_pRcvBuffer.ackData(acksize); + + // signal a waiting "recv" call if there is any data available + if (m_bSynRecving) + m_RecvDataCond.Set(); + + // acknowledge any waiting epolls to read + //s_UDTUnited.m_EPoll.update_events(m_SocketID, m_sPollID, EPOLLOpt.UDT_EPOLL_IN, true); + } + else if (ack == m_iRcvLastAck) + { + if ((currtime - m_ullLastAckTime) < ((ulong)(m_iRTT + 4 * m_iRTTVar) * m_ullCPUFrequency)) + break; + } + else + break; + + // Send out the ACK only if has not been received by the sender before + if (SequenceNumber.seqcmp(m_iRcvLastAck, m_iRcvLastAckAck) > 0) + { + int[] data = new int[6]; + + m_iAckSeqNo = AckNumber.incack(m_iAckSeqNo); + data[0] = m_iRcvLastAck; + data[1] = m_iRTT; + data[2] = m_iRTTVar; + data[3] = m_pRcvBuffer.getAvailBufSize(); + // a minimum flow window of 2 is used, even if buffer is full, to break potential deadlock + if (data[3] < 2) + data[3] = 2; + + if (currtime - m_ullLastAckTime > m_ullSYNInt) + { + data[4] = m_pRcvTimeWindow.getPktRcvSpeed(); + data[5] = m_pRcvTimeWindow.getBandwidth(); + ctrlpkt.pack(pkttype, m_iAckSeqNo, data); + + m_ullLastAckTime = Timer.rdtsc(); + } + else + { + ctrlpkt.pack(pkttype, m_iAckSeqNo, data, 4); + } + + ctrlpkt.SetId(m_PeerID); + m_pSndQueue.sendto(m_pPeerAddr, ctrlpkt); + + m_pACKWindow.store(m_iAckSeqNo, m_iRcvLastAck); + + ++m_iSentACK; + ++m_iSentACKTotal; + } + + break; + } + + case 6: //110 - Acknowledgement of Acknowledgement + ctrlpkt.pack(pkttype, lparam); + ctrlpkt.SetId(m_PeerID); + m_pSndQueue.sendto(m_pPeerAddr, ctrlpkt); + + break; + + case 3: //011 - Loss Report + { + if (null != rparam) + { + if (1 == size) + { + // only 1 loss packet + ctrlpkt.pack(pkttype, null, (int*)rparam + 1, 4); + } + else + { + // more than 1 loss packets + ctrlpkt.pack(pkttype, null, rparam, 8); + } + + ctrlpkt.SetId(m_PeerID); + m_pSndQueue.sendto(m_pPeerAddr, ctrlpkt); + + ++m_iSentNAK; + ++m_iSentNAKTotal; + } + else if (m_pRcvLossList.getLossLength() > 0) + { + // this is periodically NAK report; make sure NAK cannot be sent back too often + + // read loss list from the local receiver loss list + int[] data = new int[m_iPayloadSize / 4]; + int losslen; + m_pRcvLossList.getLossArray(data, out losslen, m_iPayloadSize / 4); + + if (0 < losslen) + { + ctrlpkt.pack(pkttype, data, losslen); + ctrlpkt.SetId(m_PeerID); + m_pSndQueue.sendto(m_pPeerAddr, ctrlpkt); + + ++m_iSentNAK; + ++m_iSentNAKTotal; + } + } + + // update next NAK time, which should wait enough time for the retansmission, but not too long + m_ullNAKInt = (ulong)(m_iRTT + 4 * m_iRTTVar) * m_ullCPUFrequency; + int rcv_speed = m_pRcvTimeWindow.getPktRcvSpeed(); + if (rcv_speed > 0) + m_ullNAKInt += (ulong)(m_pRcvLossList.getLossLength() * 1000000 / rcv_speed) *m_ullCPUFrequency; + if (m_ullNAKInt < m_ullMinNakInt) + m_ullNAKInt = m_ullMinNakInt; + + break; + } + + case 4: //100 - Congestion Warning + ctrlpkt.pack(pkttype); + ctrlpkt.SetId(m_PeerID); + m_pSndQueue.sendto(m_pPeerAddr, ctrlpkt); + + m_ullLastWarningTime = Timer.rdtsc(); + break; + + case 1: //001 - Keep-alive + ctrlpkt.pack(pkttype); + ctrlpkt.SetId(m_PeerID); + m_pSndQueue.sendto(m_pPeerAddr, ctrlpkt); + + break; + + case 0: //000 - Handshake + ctrlpkt.pack(pkttype, null, rparam, Handshake.m_iContentSize); + ctrlpkt.SetId(m_PeerID); + m_pSndQueue.sendto(m_pPeerAddr, ctrlpkt); + + break; + + case 5: //101 - Shutdown + ctrlpkt.pack(pkttype); + ctrlpkt.SetId(m_PeerID); + m_pSndQueue.sendto(m_pPeerAddr, ctrlpkt); + + break; + + case 7: //111 - Msg drop request + ctrlpkt.pack(pkttype, lparam, rparam, 8); + ctrlpkt.SetId(m_PeerID); + m_pSndQueue.sendto(m_pPeerAddr, ctrlpkt); + + break; + + case 8: //1000 - acknowledge the peer side a special error + ctrlpkt.pack(pkttype, lparam); + ctrlpkt.SetId(m_PeerID); + m_pSndQueue.sendto(m_pPeerAddr, ctrlpkt); + + break; + + case 32767: //0x7FFF - Resevered for future use + break; + + default: + break; + } + } + + public unsafe void processCtrl(Packet ctrlpkt) + { + // Just heard from the peer, reset the expiration count. + m_iEXPCount = 1; + ulong currtime = Timer.rdtsc(); + m_ullLastRspTime = currtime; + + switch (ctrlpkt.getType()) + { + case 2: //010 - Acknowledgement + { + int ack; + + // process a lite ACK + if (4 == ctrlpkt.getLength()) + { + ack = ctrlpkt.GetIntFromData(0); + if (SequenceNumber.seqcmp(ack, m_iSndLastAck) >= 0) + { + m_iFlowWindowSize -= SequenceNumber.seqoff(m_iSndLastAck, ack); + m_iSndLastAck = ack; + } + + break; + } + + // read ACK seq. no. + ack = ctrlpkt.getAckSeqNo(); + + // send ACK acknowledgement + // number of ACK2 can be much less than number of ACK + ulong now = Timer.getTime(); + if ((now - m_ullSndLastAck2Time > m_iSYNInterval) || (ack == m_iSndLastAck2)) + { + sendCtrl(6, &ack); + m_iSndLastAck2 = ack; + m_ullSndLastAck2Time = now; + } + + // Got data ACK + ack = ctrlpkt.GetIntFromData(0); + + // check the validation of the ack + if (SequenceNumber.seqcmp(ack, SequenceNumber.incseq(m_iSndCurrSeqNo)) > 0) + { + //this should not happen: attack or bug + m_bBroken = true; + m_iBrokenCounter = 0; + break; + } + + if (SequenceNumber.seqcmp(ack, m_iSndLastAck) >= 0) + { + // Update Flow Window Size, must update before and together with m_iSndLastAck + m_iFlowWindowSize = ctrlpkt.GetIntFromData(3); + m_iSndLastAck = ack; + } + + // protect packet retransmission + bool bLockTaken = false; + Monitor.Enter(m_AckLock, ref bLockTaken); + + int offset = SequenceNumber.seqoff(m_iSndLastDataAck, ack); + if (offset <= 0) + { + // discard it if it is a repeated ACK + if (bLockTaken) + Monitor.Exit(m_AckLock); + break; + } + + // acknowledge the sending buffer + m_pSndBuffer.ackData(offset); + + // record total time used for sending + m_llSndDuration += (long)now - m_llSndDurationCounter; + m_llSndDurationTotal += (long)now - m_llSndDurationCounter; + m_llSndDurationCounter = (long)now; + + // update sending variables + m_iSndLastDataAck = ack; + m_pSndLossList.remove(SequenceNumber.decseq(m_iSndLastDataAck)); + + if (bLockTaken) + Monitor.Exit(m_AckLock); + + if (m_bSynSending) + m_SendBlockCond.Set(); + + // acknowledde any waiting epolls to write + //s_UDTUnited.m_EPoll.update_events(m_SocketID, m_sPollID, EPOLLOpt.UDT_EPOLL_OUT, true); + + // insert this socket to snd list if it is not on the list yet + m_pSndQueue.m_pSndUList.update(this, false); + + // Update RTT + int rtt = ctrlpkt.GetIntFromData(1); + m_iRTTVar = (m_iRTTVar * 3 + Math.Abs(rtt - m_iRTT)) >> 2; + m_iRTT = (m_iRTT * 7 + rtt) >> 3; + + m_pCC.setRTT(m_iRTT); + + if (ctrlpkt.getLength() > 16) + { + // Update Estimated Bandwidth and packet delivery rate + if (ctrlpkt.GetIntFromData(4) > 0) + m_iDeliveryRate = (m_iDeliveryRate * 7 + ctrlpkt.GetIntFromData(4)) >> 3; + + if (ctrlpkt.GetIntFromData(5) > 0) + m_iBandwidth = (m_iBandwidth * 7 + ctrlpkt.GetIntFromData(5)) >> 3; + + m_pCC.setRcvRate(m_iDeliveryRate); + m_pCC.setBandwidth(m_iBandwidth); + } + + m_pCC.onACK(ack); + CCUpdate(); + + ++m_iRecvACK; + ++m_iRecvACKTotal; + + break; + } + + case 6: //110 - Acknowledgement of Acknowledgement + { + int ack = -1; + int rtt = -1; + + // update RTT + rtt = m_pACKWindow.acknowledge(ctrlpkt.getAckSeqNo(), ref ack); + if (rtt <= 0) + break; + + //if increasing delay detected... + // sendCtrl(4); + + // RTT EWMA + m_iRTTVar = (m_iRTTVar * 3 + Math.Abs(rtt - m_iRTT)) >> 2; + m_iRTT = (m_iRTT * 7 + rtt) >> 3; + + m_pCC.setRTT(m_iRTT); + + // update last ACK that has been received by the sender + if (SequenceNumber.seqcmp(ack, m_iRcvLastAckAck) > 0) + m_iRcvLastAckAck = ack; + + break; + } + + case 3: //011 - Loss Report + { + int[] losslist = new int[ctrlpkt.getLength() / 4]; + Buffer.BlockCopy(ctrlpkt.GetDataBytes(), 0, losslist, 0, ctrlpkt.getLength()); + + m_pCC.onLoss(losslist, ctrlpkt.getLength() / 4); + CCUpdate(); + + bool secure = true; + + // decode loss list message and insert loss into the sender loss list + for (int i = 0; i < losslist.Length; ++i) + { + if (0 != (losslist[i] & 0x80000000)) + { + if ((SequenceNumber.seqcmp(losslist[i] & 0x7FFFFFFF, losslist[i + 1]) > 0) || (SequenceNumber.seqcmp(losslist[i + 1], m_iSndCurrSeqNo) > 0)) + { + // seq_a must not be greater than seq_b; seq_b must not be greater than the most recent sent seq + secure = false; + break; + } + + int num = 0; + if (SequenceNumber.seqcmp(losslist[i] & 0x7FFFFFFF, m_iSndLastAck) >= 0) + num = m_pSndLossList.insert(losslist[i] & 0x7FFFFFFF, losslist[i + 1]); + else if (SequenceNumber.seqcmp(losslist[i + 1], m_iSndLastAck) >= 0) + num = m_pSndLossList.insert(m_iSndLastAck, losslist[i + 1]); + + m_iTraceSndLoss += num; + m_iSndLossTotal += num; + + ++i; + } + else if (SequenceNumber.seqcmp(losslist[i], m_iSndLastAck) >= 0) + { + if (SequenceNumber.seqcmp(losslist[i], m_iSndCurrSeqNo) > 0) + { + //seq_a must not be greater than the most recent sent seq + secure = false; + break; + } + + int num = m_pSndLossList.insert(losslist[i], losslist[i]); + + m_iTraceSndLoss += num; + m_iSndLossTotal += num; + } + } + + if (!secure) + { + //this should not happen: attack or bug + m_bBroken = true; + m_iBrokenCounter = 0; + break; + } + + // the lost packet (retransmission) should be sent out immediately + m_pSndQueue.m_pSndUList.update(this); + + ++m_iRecvNAK; + ++m_iRecvNAKTotal; + + break; + } + + case 4: //100 - Delay Warning + // One way packet delay is increasing, so decrease the sending rate + m_ullInterval = (ulong)Math.Ceiling(m_ullInterval * 1.125); + m_iLastDecSeq = m_iSndCurrSeqNo; + + break; + + case 1: //001 - Keep-alive + // The only purpose of keep-alive packet is to tell that the peer is still alive + // nothing needs to be done. + + break; + + case 0: //000 - Handshake + { + Handshake req = new Handshake(); + req.deserialize(ctrlpkt.GetDataBytes(), ctrlpkt.getLength()); + if ((req.m_iReqType > 0) || (m_bRendezvous && (req.m_iReqType != -2))) + { + // The peer side has not received the handshake message, so it keeps querying + // resend the handshake packet + + Handshake initdata = new Handshake(); + initdata.m_iISN = m_iISN; + initdata.m_iMSS = m_iMSS; + initdata.m_iFlightFlagSize = m_iFlightFlagSize; + initdata.m_iReqType = (!m_bRendezvous) ? -1 : -2; + initdata.m_iID = m_SocketID; + + byte[] hs = new byte[m_iPayloadSize]; + int hs_size = m_iPayloadSize; + initdata.serialize(hs); + fixed (byte* pHS = hs) + { + sendCtrl(0, null, pHS, hs_size); + } + } + + break; + } + + case 5: //101 - Shutdown + m_bShutdown = true; + m_bClosing = true; + m_bBroken = true; + m_iBrokenCounter = 60; + + // Signal the sender and recver if they are waiting for data. + releaseSynch(); + + Timer.triggerEvent(); + + break; + + case 7: //111 - Msg drop request + m_pRcvBuffer.dropMsg(ctrlpkt.getMsgSeq()); + m_pRcvLossList.remove(ctrlpkt.GetIntFromData(0), ctrlpkt.GetIntFromData(1)); + + // move forward with current recv seq no. + if ((SequenceNumber.seqcmp(ctrlpkt.GetIntFromData(0), SequenceNumber.incseq(m_iRcvCurrSeqNo)) <= 0) + && (SequenceNumber.seqcmp(ctrlpkt.GetIntFromData(1), m_iRcvCurrSeqNo) > 0)) + { + m_iRcvCurrSeqNo = ctrlpkt.GetIntFromData(1); + } + + break; + + case 8: // 1000 - An error has happened to the peer side + //int err_type = packet.getAddInfo(); + + // currently only this error is signalled from the peer side + // if recvfile() failes (e.g., due to disk fail), blcoked sendfile/send should return immediately + // giving the app a chance to fix the issue + + m_bPeerHealth = false; + + break; + + case 32767: //0x7FFF - reserved and user defined messages + m_pCC.processCustomMsg(ctrlpkt); + CCUpdate(); + + break; + + default: + break; + } + } + + public unsafe int packData(Packet packet, ref ulong ts) + { + int payload = 0; + bool probe = false; + + ulong entertime = Timer.rdtsc(); + + if ((0 != m_ullTargetTime) && (entertime > m_ullTargetTime)) + m_ullTimeDiff += entertime - m_ullTargetTime; + + // Loss retransmission always has higher priority. + packet.SetSequenceNumber(m_pSndLossList.getLostSeq()); + if (packet.GetSequenceNumber() >= 0) + { + // protect m_iSndLastDataAck from updating by ACK processing + lock (m_AckLock) + { + int offset = SequenceNumber.seqoff(m_iSndLastDataAck, packet.GetSequenceNumber()); + if (offset < 0) + return 0; + + int msglen = 0; + byte[] data = null; + uint msgNo = 0; + payload = m_pSndBuffer.readData(ref data, offset, ref msgNo, out msglen); + packet.SetDataFromBytes(data, 0, payload); + packet.SetMessageNumber(msgNo); + + if (-1 == payload) + { + int[] seqpair = new int[2]; + seqpair[0] = packet.GetSequenceNumber(); + seqpair[1] = SequenceNumber.incseq(seqpair[0], msglen); + msgNo = packet.GetMessageNumber(); + fixed (int* pSeqpair = seqpair) + { + sendCtrl(7, &msgNo, pSeqpair, 8); + } + + // only one msg drop request is necessary + m_pSndLossList.remove(seqpair[1]); + + // skip all dropped packets + if (SequenceNumber.seqcmp(m_iSndCurrSeqNo, SequenceNumber.incseq(seqpair[1])) < 0) + m_iSndCurrSeqNo = SequenceNumber.incseq(seqpair[1]); + + return 0; + } + else if (0 == payload) + return 0; + + ++m_iTraceRetrans; + ++m_iRetransTotal; + } + } + else + { + // If no loss, pack a new packet. + + // check congestion/flow window limit + int cwnd = (m_iFlowWindowSize < (int)m_dCongestionWindow) ? m_iFlowWindowSize : (int)m_dCongestionWindow; + if (cwnd >= SequenceNumber.seqlen(m_iSndLastAck, SequenceNumber.incseq(m_iSndCurrSeqNo))) + { + byte[] data = null; + uint msgNo = 0; + payload = m_pSndBuffer.readData(ref data, ref msgNo); + if (0 != payload) + { + packet.SetDataFromBytes(data, 0, payload); + packet.SetMessageNumber(msgNo); + + m_iSndCurrSeqNo = SequenceNumber.incseq(m_iSndCurrSeqNo); + m_pCC.setSndCurrSeqNo(m_iSndCurrSeqNo); + + packet.SetSequenceNumber(m_iSndCurrSeqNo); + + // every 16 (0xF) packets, a packet pair is sent + if (0 == (packet.GetSequenceNumber() & 0xF)) + probe = true; + } + else + { + m_ullTargetTime = 0; + m_ullTimeDiff = 0; + ts = 0; + return 0; + } + } + else + { + m_ullTargetTime = 0; + m_ullTimeDiff = 0; + ts = 0; + return 0; + } + } + + packet.SetTimestamp((int)(Timer.getTime() - m_StartTime)); + packet.SetId(m_PeerID); + packet.setLength(payload); + + m_pCC.onPktSent(packet); + m_pSndTimeWindow.onPktSent(packet.GetTimestamp()); + + ++m_llTraceSent; + ++m_llSentTotal; + + if (probe) + { + // sends out probing packet pair + ts = entertime; + probe = false; + } + else + { + if (m_ullTimeDiff >= m_ullInterval) + { + ts = entertime; + m_ullTimeDiff -= m_ullInterval; + } + else + { + ts = entertime + m_ullInterval - m_ullTimeDiff; + m_ullTimeDiff = 0; + } + } + + m_ullTargetTime = ts; + + return payload; + } + + public unsafe int processData(Unit unit) + { + Packet packet = unit.m_Packet; + + // Just heard from the peer, reset the expiration count. + m_iEXPCount = 1; + ulong currtime = Timer.rdtsc(); + m_ullLastRspTime = currtime; + + m_pCC.onPktReceived(packet); + ++m_iPktCount; + // update time information + m_pRcvTimeWindow.onPktArrival(); + + // check if it is probing packet pair + if (0 == (packet.GetSequenceNumber() & 0xF)) + m_pRcvTimeWindow.probe1Arrival(); + else if (1 == (packet.GetSequenceNumber() & 0xF)) + m_pRcvTimeWindow.probe2Arrival(); + + ++m_llTraceRecv; + ++m_llRecvTotal; + + int offset = SequenceNumber.seqoff(m_iRcvLastAck, packet.GetSequenceNumber()); + if ((offset < 0) || (offset >= m_pRcvBuffer.getAvailBufSize())) + return -1; + + if (m_pRcvBuffer.addData(unit, offset) < 0) + return -1; + + // Loss detection. + if (SequenceNumber.seqcmp(packet.GetSequenceNumber(), SequenceNumber.incseq(m_iRcvCurrSeqNo)) > 0) + { + // If loss found, insert them to the receiver loss list + m_pRcvLossList.insert(SequenceNumber.incseq(m_iRcvCurrSeqNo), SequenceNumber.decseq(packet.GetSequenceNumber())); + + // pack loss list for NAK + int[] lossdata = new int[2]; + lossdata[0] = (int)(SequenceNumber.incseq(m_iRcvCurrSeqNo) | 0x80000000); + lossdata[1] = SequenceNumber.decseq(packet.GetSequenceNumber()); + + // Generate loss report immediately. + fixed (int* pLossdata = lossdata) + { + sendCtrl(3, null, pLossdata, (SequenceNumber.incseq(m_iRcvCurrSeqNo) == SequenceNumber.decseq(packet.GetSequenceNumber())) ? 1 : 2); + } + + int loss = SequenceNumber.seqlen(m_iRcvCurrSeqNo, packet.GetSequenceNumber()) - 2; + m_iTraceRcvLoss += loss; + m_iRcvLossTotal += loss; + } + + // This is not a regular fixed size packet... + //an irregular sized packet usually indicates the end of a message, so send an ACK immediately + if (packet.getLength() != m_iPayloadSize) + m_ullNextACKTime = Timer.rdtsc(); + + // Update the current largest sequence number that has been received. + // Or it is a retransmitted packet, remove it from receiver loss list. + if (SequenceNumber.seqcmp(packet.GetSequenceNumber(), m_iRcvCurrSeqNo) > 0) + m_iRcvCurrSeqNo = packet.GetSequenceNumber(); + else + m_pRcvLossList.remove(packet.GetSequenceNumber()); + + return 0; + } + + public int listen(IPEndPoint endPoint, Packet packet) + { + if (m_bClosing) + return 1002; + + if (packet.getLength() != Handshake.m_iContentSize) + return 1004; + + Handshake hs = new Handshake(); + hs.deserialize(packet.GetDataBytes(), packet.getLength()); + + //IPHostEntry host = Dns.GetHostEntry(endPoint.Address); //TODO SocketException,ArgumentException + IPHostEntry host = new IPHostEntry(){HostName = endPoint.Address.ToString() }; + // SYN cookie + long timestamp = (long)(Timer.getTime() - m_StartTime) / 60000000; // secret changes every one minute + + string cookiestr = string.Format("{0}:{1}:{2}", host.HostName, endPoint.Port, timestamp); + + MD5 md5 = MD5.Create(); + byte[] cookie = md5.ComputeHash(System.Text.Encoding.ASCII.GetBytes(cookiestr)); + + if (1 == hs.m_iReqType) + { + hs.m_iCookie = BitConverter.ToInt32(cookie, 0); + packet.pack(hs); + packet.SetId(hs.m_iID); + m_pSndQueue.sendto(endPoint, packet); + return 0; + } + else + { + if (hs.m_iCookie != BitConverter.ToInt32(cookie, 0)) + { + timestamp--; + cookiestr = string.Format("{0}:{1}:{2}", host.HostName, endPoint.Port, timestamp); + cookie = md5.ComputeHash(System.Text.Encoding.ASCII.GetBytes(cookiestr)); + + if (hs.m_iCookie != BitConverter.ToInt32(cookie, 0)) + return -1; + } + } + + int id = hs.m_iID; + + // When a peer side connects in... + if ((1 == packet.getFlag()) && (0 == packet.getType())) + { + if ((hs.m_iVersion != m_iVersion) || (hs.m_iType != m_iSockType)) + { + // mismatch, reject the request + hs.m_iReqType = 1002; + packet.pack(hs); + packet.SetId(id); + m_pSndQueue.sendto(endPoint, packet); + } + else + { + int result = s_UDTUnited.newConnection(m_SocketID, endPoint, hs); + + if (result == -1) + hs.m_iReqType = 1002; + + // send back a response if connection failed or connection already existed + // new connection response should be sent in connect() + if (result != 1) + { + packet.pack(hs); + packet.SetId(id); + m_pSndQueue.sendto(endPoint, packet); + } + else + { + // a new connection has been created, enable epoll for write + //s_UDTUnited.m_EPoll.update_events(m_SocketID, m_sPollID, EPOLLOpt.UDT_EPOLL_OUT, true); + } + } + } + + return hs.m_iReqType; + } + + public unsafe void checkTimers() + { + // update CC parameters + CCUpdate(); + //ulong minint = (ulong)(m_ullCPUFrequency * m_pSndTimeWindow.getMinPktSndInt() * 0.9); + //if (m_ullInterval < minint) + // m_ullInterval = minint; + + ulong currtime = Timer.rdtsc(); + + if ((currtime > m_ullNextACKTime) || ((m_pCC.m_iACKInterval > 0) && (m_pCC.m_iACKInterval <= m_iPktCount))) + { + // ACK timer expired or ACK interval is reached + + sendCtrl(2); + currtime = Timer.rdtsc(); + if (m_pCC.m_iACKPeriod > 0) + m_ullNextACKTime = currtime + (ulong)m_pCC.m_iACKPeriod * m_ullCPUFrequency; + else + m_ullNextACKTime = currtime + m_ullACKInt; + + m_iPktCount = 0; + m_iLightACKCount = 1; + } + else if (m_iSelfClockInterval * m_iLightACKCount <= m_iPktCount) + { + //send a "light" ACK + sendCtrl(2, null, null, 4); + ++m_iLightACKCount; + } + + // we are not sending back repeated NAK anymore and rely on the sender's EXP for retransmission + //if ((m_pRcvLossList.getLossLength() > 0) && (currtime > m_ullNextNAKTime)) + //{ + // // NAK timer expired, and there is loss to be reported. + // sendCtrl(3); + // + // CTimer.rdtsc(currtime); + // m_ullNextNAKTime = currtime + m_ullNAKInt; + //} + + ulong next_exp_time; + if (m_pCC.m_bUserDefinedRTO) + next_exp_time = m_ullLastRspTime + (ulong)m_pCC.m_iRTO * m_ullCPUFrequency; + else + { + ulong exp_int = (ulong)(m_iEXPCount * (m_iRTT + 4 * m_iRTTVar) + m_iSYNInterval) * m_ullCPUFrequency; + if (exp_int < (ulong)m_iEXPCount * m_ullMinExpInt) + exp_int = (ulong)m_iEXPCount * m_ullMinExpInt; + next_exp_time = m_ullLastRspTime + exp_int; + } + + if (currtime > next_exp_time) + { + // Haven't receive any information from the peer, is it dead?! + // timeout: at least 16 expirations and must be greater than 10 seconds + if ((m_iEXPCount > 16) && (currtime - m_ullLastRspTime > 5000000 * m_ullCPUFrequency)) + { + // + // Connection is broken. + // UDT does not signal any information about this instead of to stop quietly. + // Application will detect this when it calls any UDT methods next time. + // + m_bClosing = true; + m_bBroken = true; + m_iBrokenCounter = 30; + + // update snd U list to remove this socket + m_pSndQueue.m_pSndUList.update(this); + + releaseSynch(); + + // app can call any UDT API to learn the connection_broken error + //s_UDTUnited.m_EPoll.update_events(m_SocketID, m_sPollID, UDT_EPOLL_IN | UDT_EPOLL_OUT | UDT_EPOLL_ERR, true); + + Timer.triggerEvent(); + + return; + } + + // sender: Insert all the packets sent after last received acknowledgement into the sender loss list. + // recver: Send out a keep-alive packet + if (m_pSndBuffer.getCurrBufSize() > 0) + { + if ((SequenceNumber.incseq(m_iSndCurrSeqNo) != m_iSndLastAck) && (m_pSndLossList.getLossLength() == 0)) + { + // resend all unacknowledged packets on timeout, but only if there is no packet in the loss list + int csn = m_iSndCurrSeqNo; + int num = m_pSndLossList.insert(m_iSndLastAck, csn); + m_iTraceSndLoss += num; + m_iSndLossTotal += num; + } + + m_pCC.onTimeout(); + CCUpdate(); + + // immediately restart transmission + m_pSndQueue.m_pSndUList.update(this); + } + else + { + sendCtrl(1); + } + + ++m_iEXPCount; + // Reset last response time since we just sent a heart-beat. + m_ullLastRspTime = currtime; + } + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/UDT/CoreExtensions.cs b/framework/Inspectron.HawkEye/UDT/CoreExtensions.cs new file mode 100644 index 0000000..b77211b --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/CoreExtensions.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Linq; + +namespace UdtSharp +{ + public static class CoreExtensions + { + public static bool TryGetValue(this HashSet self, InfoBlock equalValue, out InfoBlock actualValue) + { + if (self.Contains(equalValue)) + { + actualValue = self.First(x=>x==equalValue); + return true; + } + actualValue = null; + return false; + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/UDT/ERequestType.cs b/framework/Inspectron.HawkEye/UDT/ERequestType.cs new file mode 100644 index 0000000..eeecd23 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/ERequestType.cs @@ -0,0 +1,7 @@ +namespace UdtSharp +{ + public enum ERequestType + { + Trigger + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/UDT/InfoBlock.cs b/framework/Inspectron.HawkEye/UDT/InfoBlock.cs new file mode 100644 index 0000000..93e5eba --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/InfoBlock.cs @@ -0,0 +1,111 @@ +using System; +using System.Net; +using System.Net.Sockets; + +namespace UdtSharp +{ + public class InfoBlock + { + uint[] m_piIP = new uint[4]; // IP address, machine read only, not human readable format + AddressFamily m_iIPversion; // IP version + public ulong m_ullTimeStamp; // last update time + public int m_iRTT; // RTT + public int m_iBandwidth; // estimated bandwidth + public int m_iLossRate; // average loss rate + public int m_iReorderDistance; // packet reordering distance + public double m_dInterval; // inter-packet time, congestion control + public double m_dCWnd; // congestion window size, congestion control + + public InfoBlock(IPAddress address) + { + m_iIPversion = address.AddressFamily; + ConvertIPAddress.ToUintArray(address, ref m_piIP); + } + + public override bool Equals(object value) + { + // Is null? + if (Object.ReferenceEquals(null, value)) + { + return false; + } + + // Is the same object? + if (Object.ReferenceEquals(this, value)) + { + return true; + } + + // Is the same type? + if (value.GetType() != this.GetType()) + { + return false; + } + + return IsEqual((InfoBlock)value); + } + + public bool Equals(InfoBlock infoBlock) + { + if (Object.ReferenceEquals(null, infoBlock)) + { + return false; + } + + // Is the same object? + if (Object.ReferenceEquals(this, infoBlock)) + { + return true; + } + + return IsEqual(infoBlock); + } + + public static bool operator ==(InfoBlock infoBlockA, InfoBlock infoBlockB) + { + if (Object.ReferenceEquals(infoBlockA, infoBlockB)) + { + return true; + } + + // Ensure that "numberA" isn't null + if (Object.ReferenceEquals(null, infoBlockA)) + { + return false; + } + + return (infoBlockA.Equals(infoBlockB)); + } + + public static bool operator !=(InfoBlock infoBlockA, InfoBlock infoBlockB) + { + return !(infoBlockA == infoBlockB); + } + + public override int GetHashCode() + { + if (m_iIPversion == AddressFamily.InterNetwork) + return (int)m_piIP[0]; + + return (int)(m_piIP[0] + m_piIP[1] + m_piIP[2] + m_piIP[3]); + } + + bool IsEqual(InfoBlock infoBlock) + { + if (m_iIPversion != infoBlock.m_iIPversion) + return false; + + else if (m_iIPversion == AddressFamily.InterNetwork) + return (m_piIP[0] == infoBlock.m_piIP[0]); + + for (int i = 0; i < 4; ++i) + { + if (m_piIP[i] != infoBlock.m_piIP[i]) + return false; + } + + return true; + } + } + +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/UDT/LossList.cs b/framework/Inspectron.HawkEye/UDT/LossList.cs new file mode 100644 index 0000000..69bb446 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/LossList.cs @@ -0,0 +1,672 @@ +namespace UdtSharp +{ + public class SndLossList + { + int[] m_piData1; // sequence number starts + int[] m_piData2; // seqnence number ends + int[] m_piNext; // next node in the list + + int m_iHead; // first node + int m_iLength; // loss length + int m_iSize; // size of the static array + int m_iLastInsertPos; // position of last insert node + + object m_ListLock = new object(); // used to synchronize list operation + + public SndLossList(int size) + { + m_iHead = -1; + m_iLength = 0; + m_iSize = size; + m_iLastInsertPos = -1; + + m_piData1 = new int[m_iSize]; + m_piData2 = new int[m_iSize]; + m_piNext = new int[m_iSize]; + + // -1 means there is no data in the node + for (int i = 0; i < size; ++i) + { + m_piData1[i] = -1; + m_piData2[i] = -1; + } + + } + + public int insert(int seqno1, int seqno2) + { + lock (m_ListLock) + { + return insert_unsafe(seqno1, seqno2); + } + } + + int insert_unsafe(int seqno1, int seqno2) + { + if (0 == m_iLength) + { + // insert data into an empty list + + m_iHead = 0; + m_piData1[m_iHead] = seqno1; + if (seqno2 != seqno1) + m_piData2[m_iHead] = seqno2; + + m_piNext[m_iHead] = -1; + m_iLastInsertPos = m_iHead; + + m_iLength += SequenceNumber.seqlen(seqno1, seqno2); + + return m_iLength; + } + + // otherwise find the position where the data can be inserted + int origlen = m_iLength; + int offset = SequenceNumber.seqoff(m_piData1[m_iHead], seqno1); + int loc = (m_iHead + offset + m_iSize) % m_iSize; + + if (offset < 0) + { + // Insert data prior to the head pointer + + m_piData1[loc] = seqno1; + if (seqno2 != seqno1) + m_piData2[loc] = seqno2; + + // new node becomes head + m_piNext[loc] = m_iHead; + m_iHead = loc; + m_iLastInsertPos = loc; + + m_iLength += SequenceNumber.seqlen(seqno1, seqno2); + } + else if (offset > 0) + { + if (seqno1 == m_piData1[loc]) + { + m_iLastInsertPos = loc; + + // first seqno is equivlent, compare the second + if (-1 == m_piData2[loc]) + { + if (seqno2 != seqno1) + { + m_iLength += SequenceNumber.seqlen(seqno1, seqno2) - 1; + m_piData2[loc] = seqno2; + } + } + else if (SequenceNumber.seqcmp(seqno2, m_piData2[loc]) > 0) + { + // new seq pair is longer than old pair, e.g., insert [3, 7] to [3, 5], becomes [3, 7] + m_iLength += SequenceNumber.seqlen(m_piData2[loc], seqno2) - 1; + m_piData2[loc] = seqno2; + } + else + // Do nothing if it is already there + return 0; + } + else + { + // searching the prior node + int i; + if ((-1 != m_iLastInsertPos) && (SequenceNumber.seqcmp(m_piData1[m_iLastInsertPos], seqno1) < 0)) + i = m_iLastInsertPos; + else + i = m_iHead; + + while ((-1 != m_piNext[i]) && (SequenceNumber.seqcmp(m_piData1[m_piNext[i]], seqno1) < 0)) + i = m_piNext[i]; + + if ((-1 == m_piData2[i]) || (SequenceNumber.seqcmp(m_piData2[i], seqno1) < 0)) + { + m_iLastInsertPos = loc; + + // no overlap, create new node + m_piData1[loc] = seqno1; + if (seqno2 != seqno1) + m_piData2[loc] = seqno2; + + m_piNext[loc] = m_piNext[i]; + m_piNext[i] = loc; + + m_iLength += SequenceNumber.seqlen(seqno1, seqno2); + } + else + { + m_iLastInsertPos = i; + + // overlap, coalesce with prior node, insert(3, 7) to [2, 5], ... becomes [2, 7] + if (SequenceNumber.seqcmp(m_piData2[i], seqno2) < 0) + { + m_iLength += SequenceNumber.seqlen(m_piData2[i], seqno2) - 1; + m_piData2[i] = seqno2; + + loc = i; + } + else + return 0; + } + } + } + else + { + m_iLastInsertPos = m_iHead; + + // insert to head node + if (seqno2 != seqno1) + { + if (-1 == m_piData2[loc]) + { + m_iLength += SequenceNumber.seqlen(seqno1, seqno2) - 1; + m_piData2[loc] = seqno2; + } + else if (SequenceNumber.seqcmp(seqno2, m_piData2[loc]) > 0) + { + m_iLength += SequenceNumber.seqlen(m_piData2[loc], seqno2) - 1; + m_piData2[loc] = seqno2; + } + else + return 0; + } + else + return 0; + } + + // coalesce with next node. E.g., [3, 7], ..., [6, 9] becomes [3, 9] + while ((-1 != m_piNext[loc]) && (-1 != m_piData2[loc])) + { + int i = m_piNext[loc]; + + if (SequenceNumber.seqcmp(m_piData1[i], SequenceNumber.incseq(m_piData2[loc])) <= 0) + { + // coalesce if there is overlap + if (-1 != m_piData2[i]) + { + if (SequenceNumber.seqcmp(m_piData2[i], m_piData2[loc]) > 0) + { + if (SequenceNumber.seqcmp(m_piData2[loc], m_piData1[i]) >= 0) + m_iLength -= SequenceNumber.seqlen(m_piData1[i], m_piData2[loc]); + + m_piData2[loc] = m_piData2[i]; + } + else + m_iLength -= SequenceNumber.seqlen(m_piData1[i], m_piData2[i]); + } + else + { + if (m_piData1[i] == SequenceNumber.incseq(m_piData2[loc])) + m_piData2[loc] = m_piData1[i]; + else + m_iLength--; + } + + m_piData1[i] = -1; + m_piData2[i] = -1; + m_piNext[loc] = m_piNext[i]; + } + else + break; + } + + return m_iLength - origlen; + } + + public void remove(int seqno) + { + lock (m_ListLock) + { + remove_unsafe(seqno); + } + } + + void remove_unsafe(int seqno) + { + if (0 == m_iLength) + return; + + // Remove all from the head pointer to a node with a larger seq. no. or the list is empty + int offset = SequenceNumber.seqoff(m_piData1[m_iHead], seqno); + int loc = (m_iHead + offset + m_iSize) % m_iSize; + + if (0 == offset) + { + // It is the head. Remove the head and point to the next node + loc = (loc + 1) % m_iSize; + + if (-1 == m_piData2[m_iHead]) + loc = m_piNext[m_iHead]; + else + { + m_piData1[loc] = SequenceNumber.incseq(seqno); + if (SequenceNumber.seqcmp(m_piData2[m_iHead], SequenceNumber.incseq(seqno)) > 0) + m_piData2[loc] = m_piData2[m_iHead]; + + m_piData2[m_iHead] = -1; + + m_piNext[loc] = m_piNext[m_iHead]; + } + + m_piData1[m_iHead] = -1; + + if (m_iLastInsertPos == m_iHead) + m_iLastInsertPos = -1; + + m_iHead = loc; + + m_iLength--; + } + else if (offset > 0) + { + int h = m_iHead; + + if (seqno == m_piData1[loc]) + { + // target node is not empty, remove part/all of the seqno in the node. + int temp = loc; + loc = (loc + 1) % m_iSize; + + if (-1 == m_piData2[temp]) + m_iHead = m_piNext[temp]; + else + { + // remove part, e.g., [3, 7] becomes [], [4, 7] after remove(3) + m_piData1[loc] = SequenceNumber.incseq(seqno); + if (SequenceNumber.seqcmp(m_piData2[temp], m_piData1[loc]) > 0) + m_piData2[loc] = m_piData2[temp]; + m_iHead = loc; + m_piNext[loc] = m_piNext[temp]; + m_piNext[temp] = loc; + m_piData2[temp] = -1; + } + } + else + { + // target node is empty, check prior node + int i = m_iHead; + while ((-1 != m_piNext[i]) && (SequenceNumber.seqcmp(m_piData1[m_piNext[i]], seqno) < 0)) + i = m_piNext[i]; + + loc = (loc + 1) % m_iSize; + + if (-1 == m_piData2[i]) + m_iHead = m_piNext[i]; + else if (SequenceNumber.seqcmp(m_piData2[i], seqno) > 0) + { + // remove part/all seqno in the prior node + m_piData1[loc] = SequenceNumber.incseq(seqno); + if (SequenceNumber.seqcmp(m_piData2[i], m_piData1[loc]) > 0) + m_piData2[loc] = m_piData2[i]; + + m_piData2[i] = seqno; + + m_piNext[loc] = m_piNext[i]; + m_piNext[i] = loc; + + m_iHead = loc; + } + else + m_iHead = m_piNext[i]; + } + + // Remove all nodes prior to the new head + while (h != m_iHead) + { + if (m_piData2[h] != -1) + { + m_iLength -= SequenceNumber.seqlen(m_piData1[h], m_piData2[h]); + m_piData2[h] = -1; + } + else + m_iLength--; + + m_piData1[h] = -1; + + if (m_iLastInsertPos == h) + m_iLastInsertPos = -1; + + h = m_piNext[h]; + } + } + } + + public int getLossLength() + { + lock (m_ListLock) + { + return m_iLength; + } + } + + public int getLostSeq() + { + if (0 == m_iLength) + return -1; + + lock (m_ListLock) + { + + if (0 == m_iLength) + return -1; + + if (m_iLastInsertPos == m_iHead) + m_iLastInsertPos = -1; + + // return the first loss seq. no. + int seqno = m_piData1[m_iHead]; + + // head moves to the next node + if (-1 == m_piData2[m_iHead]) + { + //[3, -1] becomes [], and head moves to next node in the list + m_piData1[m_iHead] = -1; + m_iHead = m_piNext[m_iHead]; + } + else + { + // shift to next node, e.g., [3, 7] becomes [], [4, 7] + int loc = (m_iHead + 1) % m_iSize; + + m_piData1[loc] = SequenceNumber.incseq(seqno); + if (SequenceNumber.seqcmp(m_piData2[m_iHead], m_piData1[loc]) > 0) + m_piData2[loc] = m_piData2[m_iHead]; + + m_piData1[m_iHead] = -1; + m_piData2[m_iHead] = -1; + + m_piNext[loc] = m_piNext[m_iHead]; + m_iHead = loc; + } + + m_iLength--; + + return seqno; + } + } + } + + public class RcvLossList + { + + int[] m_piData1; // sequence number starts + int[] m_piData2; // sequence number ends + int[] m_piNext; // next node in the list + int[] m_piPrior; // prior node in the list; + + int m_iHead; // first node in the list + int m_iTail; // last node in the list; + int m_iLength; // loss length + int m_iSize; // size of the static array + + public RcvLossList(int size) + { + m_iHead = -1; + m_iTail = -1; + m_iLength = 0; + m_iSize = size; + m_piData1 = new int[m_iSize]; + m_piData2 = new int[m_iSize]; + m_piNext = new int[m_iSize]; + m_piPrior = new int[m_iSize]; + + // -1 means there is no data in the node + for (int i = 0; i < size; ++i) + { + m_piData1[i] = -1; + m_piData2[i] = -1; + } + } + + + public void insert(int seqno1, int seqno2) + { + // Data to be inserted must be larger than all those in the list + // guaranteed by the UDT receiver + + if (0 == m_iLength) + { + // insert data into an empty list + m_iHead = 0; + m_iTail = 0; + m_piData1[m_iHead] = seqno1; + if (seqno2 != seqno1) + m_piData2[m_iHead] = seqno2; + + m_piNext[m_iHead] = -1; + m_piPrior[m_iHead] = -1; + m_iLength += SequenceNumber.seqlen(seqno1, seqno2); + + return; + } + + // otherwise searching for the position where the node should be + int offset = SequenceNumber.seqoff(m_piData1[m_iHead], seqno1); + int loc = (m_iHead + offset) % m_iSize; + + if ((-1 != m_piData2[m_iTail]) && (SequenceNumber.incseq(m_piData2[m_iTail]) == seqno1)) + { + // coalesce with prior node, e.g., [2, 5], [6, 7] becomes [2, 7] + loc = m_iTail; + m_piData2[loc] = seqno2; + } + else + { + // create new node + m_piData1[loc] = seqno1; + + if (seqno2 != seqno1) + m_piData2[loc] = seqno2; + + m_piNext[m_iTail] = loc; + m_piPrior[loc] = m_iTail; + m_piNext[loc] = -1; + m_iTail = loc; + } + + m_iLength += SequenceNumber.seqlen(seqno1, seqno2); + } + + public bool remove(int seqno) + { + if (0 == m_iLength) + return false; + + // locate the position of "seqno" in the list + int offset = SequenceNumber.seqoff(m_piData1[m_iHead], seqno); + if (offset < 0) + return false; + + int loc = (m_iHead + offset) % m_iSize; + + if (seqno == m_piData1[loc]) + { + // This is a seq. no. that starts the loss sequence + + if (-1 == m_piData2[loc]) + { + // there is only 1 loss in the sequence, delete it from the node + if (m_iHead == loc) + { + m_iHead = m_piNext[m_iHead]; + if (-1 != m_iHead) + m_piPrior[m_iHead] = -1; + } + else + { + m_piNext[m_piPrior[loc]] = m_piNext[loc]; + if (-1 != m_piNext[loc]) + m_piPrior[m_piNext[loc]] = m_piPrior[loc]; + else + m_iTail = m_piPrior[loc]; + } + + m_piData1[loc] = -1; + } + else + { + // there are more than 1 loss in the sequence + // move the node to the next and update the starter as the next loss inSeqNo(seqno) + + // find next node + int j = (loc + 1) % m_iSize; + + // remove the "seqno" and change the starter as next seq. no. + m_piData1[j] = SequenceNumber.incseq(m_piData1[loc]); + + // process the sequence end + if (SequenceNumber.seqcmp(m_piData2[loc], SequenceNumber.incseq(m_piData1[loc])) > 0) + m_piData2[j] = m_piData2[loc]; + + // remove the current node + m_piData1[loc] = -1; + m_piData2[loc] = -1; + + // update list pointer + m_piNext[j] = m_piNext[loc]; + m_piPrior[j] = m_piPrior[loc]; + + if (m_iHead == loc) + m_iHead = j; + else + m_piNext[m_piPrior[j]] = j; + + if (m_iTail == loc) + m_iTail = j; + else + m_piPrior[m_piNext[j]] = j; + } + + m_iLength--; + + return true; + } + + // There is no loss sequence in the current position + // the "seqno" may be contained in a previous node + + // searching previous node + int i = (loc - 1 + m_iSize) % m_iSize; + while (-1 == m_piData1[i]) + i = (i - 1 + m_iSize) % m_iSize; + + // not contained in this node, return + if ((-1 == m_piData2[i]) || (SequenceNumber.seqcmp(seqno, m_piData2[i]) > 0)) + return false; + + if (seqno == m_piData2[i]) + { + // it is the sequence end + + if (seqno == SequenceNumber.incseq(m_piData1[i])) + m_piData2[i] = -1; + else + m_piData2[i] = SequenceNumber.decseq(seqno); + } + else + { + // split the sequence + + // construct the second sequence from SequenceNumber.incseq(seqno) to the original sequence end + // located at "loc + 1" + loc = (loc + 1) % m_iSize; + + m_piData1[loc] = SequenceNumber.incseq(seqno); + if (SequenceNumber.seqcmp(m_piData2[i], m_piData1[loc]) > 0) + m_piData2[loc] = m_piData2[i]; + + // the first (original) sequence is between the original sequence start to SequenceNumber.decseq(seqno) + if (seqno == SequenceNumber.incseq(m_piData1[i])) + m_piData2[i] = -1; + else + m_piData2[i] = SequenceNumber.decseq(seqno); + + // update the list pointer + m_piNext[loc] = m_piNext[i]; + m_piNext[i] = loc; + m_piPrior[loc] = i; + + if (m_iTail == i) + m_iTail = loc; + else + m_piPrior[m_piNext[loc]] = loc; + } + + m_iLength--; + + return true; + } + + public bool remove(int seqno1, int seqno2) + { + if (seqno1 <= seqno2) + { + for (int i = seqno1; i <= seqno2; ++i) + remove(i); + } + else + { + for (int j = seqno1; j < SequenceNumber.m_iMaxSeqNo; ++j) + remove(j); + for (int k = 0; k <= seqno2; ++k) + remove(k); + } + + return true; + } + + bool find(int seqno1, int seqno2) + { + if (0 == m_iLength) + return false; + + int p = m_iHead; + + while (-1 != p) + { + if ((SequenceNumber.seqcmp(m_piData1[p], seqno1) == 0) || + ((SequenceNumber.seqcmp(m_piData1[p], seqno1) > 0) && (SequenceNumber.seqcmp(m_piData1[p], seqno2) <= 0)) || + ((SequenceNumber.seqcmp(m_piData1[p], seqno1) < 0) && (m_piData2[p] != -1) && SequenceNumber.seqcmp(m_piData2[p], seqno1) >= 0)) + return true; + + p = m_piNext[p]; + } + + return false; + } + + public int getLossLength() + { + return m_iLength; + } + + public int getFirstLostSeq() + { + if (0 == m_iLength) + return -1; + + return m_piData1[m_iHead]; + } + + public void getLossArray(int[] array, out int len, int limit) + { + len = 0; + + int i = m_iHead; + + while ((len < limit - 1) && (-1 != i)) + { + array[len] = m_piData1[i]; + if (-1 != m_piData2[i]) + { + // there are more than 1 loss in the sequence + array[len] = (int)((uint)array[len] | 0x80000000); + ++len; + array[len] = m_piData2[i]; + } + + ++len; + + i = m_piNext[i]; + } + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/UDT/MessageNumber.cs b/framework/Inspectron.HawkEye/UDT/MessageNumber.cs new file mode 100644 index 0000000..b04a323 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/MessageNumber.cs @@ -0,0 +1,38 @@ +// UDT Message Number: 0 - (2^29 - 1) + +using System; + +namespace UdtSharp +{ + static class MessageNumber + { + public static int msgcmp(int msgno1, int msgno2) + { + return (Math.Abs(msgno1 - msgno2) < m_iMsgNoTH) ? (msgno1 - msgno2) : (msgno2 - msgno1); + } + + public static int msglen(int msgno1, int msgno2) + { + return (msgno1 <= msgno2) ? (msgno2 - msgno1 + 1) : (msgno2 - msgno1 + m_iMaxMsgNo + 2); + } + + public static int msgoff(int msgno1, int msgno2) + { + if (Math.Abs(msgno1 - msgno2) < m_iMsgNoTH) + return msgno2 - msgno1; + + if (msgno1 < msgno2) + return msgno2 - msgno1 - m_iMaxMsgNo - 1; + + return msgno2 - msgno1 + m_iMaxMsgNo + 1; + } + + public static int incmsg(int msgno) + { + return (msgno == m_iMaxMsgNo) ? 0 : msgno + 1; + } + + static int m_iMsgNoTH = 0xFFFFFFF; // threshold for comparing msg. no. + public static int m_iMaxMsgNo = 0x1FFFFFFF; // maximum message number used in UDT + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/UDT/Packet.cs b/framework/Inspectron.HawkEye/UDT/Packet.cs new file mode 100644 index 0000000..bfdc1ec --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/Packet.cs @@ -0,0 +1,764 @@ +////////////////////////////////////////////////////////////////////////////// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Packet Header | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | | +// ~ Data / Control Information Field ~ +// | | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |0| Sequence Number | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |ff |o| Message Number | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Time Stamp | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Destination Socket ID | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// +// bit 0: +// 0: Data Packet +// 1: Control Packet +// bit ff: +// 11: solo message packet +// 10: first packet of a message +// 01: last packet of a message +// bit o: +// 0: in order delivery not required +// 1: in order delivery required +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |1| Type | Reserved | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Additional Info | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Time Stamp | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Destination Socket ID | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// +// bit 1-15: +// 0: Protocol Connection Handshake +// Add. Info: Undefined +// Control Info: Handshake information (see CHandShake) +// 1: Keep-alive +// Add. Info: Undefined +// Control Info: None +// 2: Acknowledgement (ACK) +// Add. Info: The ACK sequence number +// Control Info: The sequence number to which (but not include) all the previous packets have beed received +// Optional: RTT +// RTT Variance +// available receiver buffer size (in bytes) +// advertised flow window size (number of packets) +// estimated bandwidth (number of packets per second) +// 3: Negative Acknowledgement (NAK) +// Add. Info: Undefined +// Control Info: Loss list (see loss list coding below) +// 4: Congestion/Delay Warning +// Add. Info: Undefined +// Control Info: None +// 5: Shutdown +// Add. Info: Undefined +// Control Info: None +// 6: Acknowledgement of Acknowledement (ACK-square) +// Add. Info: The ACK sequence number +// Control Info: None +// 7: Message Drop Request +// Add. Info: Message ID +// Control Info: first sequence number of the message +// last seqeunce number of the message +// 8: Error Signal from the Peer Side +// Add. Info: Error code +// Control Info: None +// 0x7FFF: Explained by bits 16 - 31 +// +// bit 16 - 31: +// This space is used for future expansion or user defined control packets. +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |1| Sequence Number a (first) | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |0| Sequence Number b (last) | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |0| Sequence Number (single) | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// +// Loss List Field Coding: +// For any consectutive lost seqeunce numbers that the differnece between +// the last and first is more than 1, only record the first (a) and the +// the last (b) sequence numbers in the loss list field, and modify the +// the first bit of a to 1. +// For any single loss or consectutive loss less than 2 packets, use +// the original sequence numbers in the field. + +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace UdtSharp +{ + public struct iovec + { + public uint[] iov_base; + public int iov_len; + } + + + + public unsafe class Packet + { + public enum ControlType + { + Handshake = 0, + KeepAlive = 1, + Ack = 2, + Nak = 3, + CongestionWarning = 4, + Shutdown = 5, + Ack2 = 6, + DropMessage = 7, + Error = 8, + UserType = 32767, + } + + const int m_iSeqNoIndex = 0; // alias: sequence number + const int m_iMsgNoIndex = 1; // alias: message number + const int m_iTimeStampIndex = 2; // alias: timestamp + const int m_iIdIndex = 3; // alias: socket ID + + public const int m_iPktHdrSize = 16; // packet header size + + iovec[] m_PacketVector = new iovec[2]; // The 2-demension vector of UDT packet [header, data] + + public override string ToString() + { + StringBuilder stringBuilder = new StringBuilder(); + + if (getFlag() == 0) + { + byte[] data = GetDataBytes(); + stringBuilder.AppendFormat("Data length {0} bytes", data != null ? data.Length : 0); + stringBuilder.AppendLine(); + stringBuilder.AppendLine(" SeqNo " + GetSequenceNumber()); + stringBuilder.AppendLine(" MsgNo " + GetMessageNumber()); + stringBuilder.AppendLine(" Timestamp " + GetTimestamp()); + stringBuilder.AppendLine(" SocketID " + GetId()); + stringBuilder.Append(" Data: {"); + if (data != null) + { + for (int i = 0; i < Math.Min(data.Length, 10); ++i) + { + stringBuilder.Append(data[i] + ","); + } + stringBuilder.Length = stringBuilder.Length - 1; + } + stringBuilder.AppendLine("}"); + } + else if (getFlag() == 1) + { + int type = getType(); + stringBuilder.AppendFormat("CTRL {0} ({1})", (Packet.ControlType)type, type); + stringBuilder.AppendLine(); + switch (type) + { + case 2: //0010 - Acknowledgement (ACK) + stringBuilder.AppendFormat(" Ack sequence {0}", getAckSeqNo()); + stringBuilder.AppendLine(); + break; + + case 6: //0110 - Acknowledgement of Acknowledgement (ACK-2) + stringBuilder.AppendFormat(" Ack2 sequence {0}", getAckSeqNo()); + stringBuilder.AppendLine(); + break; + + case 3: //0011 - Loss Report (NAK) + break; + + case 4: //0100 - Congestion Warning + break; + + case 1: //0001 - Keep-alive + break; + + case 0: //0000 - Handshake + // control info filed is handshake info + Handshake handshake = new Handshake(); + handshake.deserialize(GetDataBytes(), Handshake.m_iContentSize); + stringBuilder.AppendFormat(handshake.ToString()); + stringBuilder.AppendLine(); + break; + + case 5: //0101 - Shutdown + break; + + case 7: //0111 - Message Drop Request + + break; + + case 8: //1000 - Error Signal from the Peer Side + // Error type + stringBuilder.AppendLine("Error: " + m_PacketVector[0].iov_base[m_iMsgNoIndex].ToString()); + + break; + + case 32767: //0x7FFF - Reserved for user defined control packets + break; + + default: + break; + } + } + + return stringBuilder.ToString(); + } + + public Packet() + { + m_PacketVector[0].iov_base = new uint[4]; + m_PacketVector[0].iov_len = m_iPktHdrSize; + m_PacketVector[1].iov_base = null; + m_PacketVector[1].iov_len = 0; + } + + ~Packet() + { + } + + public void Clone(Packet source) + { + Buffer.BlockCopy(source.m_PacketVector[0].iov_base, 0, m_PacketVector[0].iov_base, 0, m_iPktHdrSize); + + if (source.m_PacketVector[1].iov_base == null) + { + m_PacketVector[1].iov_base = null; + m_PacketVector[1].iov_len = source.m_PacketVector[1].iov_len; + return; + } + + m_PacketVector[1].iov_base = new uint[source.m_PacketVector[1].iov_base.Length]; + Buffer.BlockCopy(source.m_PacketVector[1].iov_base, 0, m_PacketVector[1].iov_base, 0, source.m_PacketVector[1].iov_len); + m_PacketVector[1].iov_len = source.m_PacketVector[1].iov_len; + } + + public int GetSequenceNumber() + { + return (int)m_PacketVector[0].iov_base[m_iSeqNoIndex]; + } + + public void SetSequenceNumber(int sequenceNumber) + { + m_PacketVector[0].iov_base[m_iSeqNoIndex] = (uint)sequenceNumber; + } + + public uint GetMessageNumber() + { + return m_PacketVector[0].iov_base[m_iMsgNoIndex]; + } + + public void SetMessageNumber(uint messageNumber) + { + m_PacketVector[0].iov_base[m_iMsgNoIndex] = messageNumber; + } + + public int GetTimestamp() + { + return (int)m_PacketVector[0].iov_base[m_iTimeStampIndex]; + } + + public void SetTimestamp(int timestamp) + { + m_PacketVector[0].iov_base[m_iTimeStampIndex] = (uint)timestamp; + } + + public int GetId() + { + return (int)m_PacketVector[0].iov_base[m_iIdIndex]; + } + + public void SetId(int id) + { + m_PacketVector[0].iov_base[m_iIdIndex] = (uint)id; + } + + public byte[] GetBytes() + { + int dataLength = m_PacketVector[1].iov_len; + + byte[] bytes = new byte[m_iPktHdrSize + dataLength]; + Buffer.BlockCopy(m_PacketVector[0].iov_base, 0, bytes, 0, m_iPktHdrSize); + + if (dataLength == 0 || m_PacketVector[1].iov_base == null) + return bytes; + + Buffer.BlockCopy(m_PacketVector[1].iov_base, 0, bytes, m_iPktHdrSize, dataLength); + return bytes; + } + + public byte[] GetHeaderBytes() + { + byte[] bytes = new byte[m_iPktHdrSize]; + Buffer.BlockCopy(m_PacketVector[0].iov_base, 0, bytes, 0, m_iPktHdrSize); + return bytes; + } + + public int GetDataBytes(int packetOffset, byte[] data, int dataOffset, int length) + { + if (m_PacketVector[1].iov_base == null) + return 0; + + int bufferAvailable = data.Length - dataOffset; + if (bufferAvailable < length) + length = bufferAvailable; + + Buffer.BlockCopy(m_PacketVector[1].iov_base, packetOffset, data, dataOffset, length); + return length; + } + + public int GetIntFromData(int offset) + { + return (int)m_PacketVector[1].iov_base[offset]; + } + + public byte[] GetDataBytes() + { + if (m_PacketVector[1].iov_base == null) + return null; + + int dataLength = m_PacketVector[1].iov_len; + if (dataLength <= 0) + return null; + + byte[] bytes = new byte[dataLength]; + Buffer.BlockCopy(m_PacketVector[1].iov_base, 0, bytes, 0, bytes.Length); + return bytes; + } + + public bool SetHeaderAndDataFromBytes(byte[] bytes, int length) + { + if (length < m_iPktHdrSize) + return false; + + Buffer.BlockCopy(bytes, 0, m_PacketVector[0].iov_base, 0, m_iPktHdrSize); + + int dataLength = length - m_iPktHdrSize; + if (dataLength == 0) + { + m_PacketVector[1].iov_base = null; + m_PacketVector[1].iov_len = 0; + return true; + } + + SetDataFromBytes(bytes, m_iPktHdrSize, dataLength); + + return true; + } + + public void SetDataFromBytes(byte[] bytes) + { + SetDataFromBytes(bytes, 0, bytes.Length); + } + + public void SetDataFromBytes(byte[] bytes, int offset, int byteCount) + { + int intCount = byteCount / 4; + if (byteCount % 4 != 0) + ++intCount; + + m_PacketVector[1].iov_base = new uint[intCount]; + m_PacketVector[1].iov_len = byteCount; + + Buffer.BlockCopy(bytes, offset, m_PacketVector[1].iov_base, 0, byteCount); + } + + public void ConvertControlInfoToNetworkOrder() + { + if (getFlag() == 0) + return; + + if (m_PacketVector[1].iov_base == null || m_PacketVector[1].iov_len == 0) + return; + + for (int i = 0; i < m_PacketVector[1].iov_base.Length; ++i) + { + m_PacketVector[1].iov_base[i] = + (uint)IPAddress.HostToNetworkOrder((int)m_PacketVector[1].iov_base[i]); + } + } + + public void ConvertControlInfoToHostOrder() + { + if (getFlag() == 0) + return; + + if (m_PacketVector[1].iov_base == null || m_PacketVector[1].iov_len == 0) + return; + + for (int i = 0; i < m_PacketVector[1].iov_base.Length; ++i) + { + m_PacketVector[1].iov_base[i] = + (uint)IPAddress.NetworkToHostOrder((int)m_PacketVector[1].iov_base[i]); + } + } + + public void ConvertHeaderToNetworkOrder() + { + for (int i = 0; i < m_PacketVector[0].iov_base.Length; ++i) + { + m_PacketVector[0].iov_base[i] = + (uint)IPAddress.HostToNetworkOrder((int)m_PacketVector[0].iov_base[i]); + } + } + + public void ConvertHeaderToHostOrder() + { + for (int i = 0; i < m_PacketVector[0].iov_base.Length; ++i) + { + m_PacketVector[0].iov_base[i] = + (uint)IPAddress.NetworkToHostOrder((int)m_PacketVector[0].iov_base[i]); + } + } + + public int getLength() + { + return m_PacketVector[1].iov_len; + } + + public void setLength(int len) + { + m_PacketVector[1].iov_len = len; + } + + static iovec MakeIovec(void* rparam, int size) + { + iovec result = new iovec(); + result.iov_len = size; + + if (rparam == null) + return result; + + result.iov_base = new uint[size >> 2]; + + uint* pIn = (uint*)rparam; + for (int i = 0; i < size >> 2; ++i) + { + result.iov_base[i] = *pIn++; + } + + return result; + } + + public void pack(Handshake hs) + { + // TODO avoid this inefficient buffer creation + // we copy this buffer into another buffer later + byte[] bytes = new byte[Handshake.m_iContentSize]; + hs.serialize(bytes); + pack(0, bytes); + } + + public void pack(int pkttype, void* lparam) + { + if (pkttype != 6 && pkttype != 8) + throw new Exception("pkttype must be 6 or 8"); + + pack(pkttype, lparam, (void*)null, 0); + } + + public void pack(int pkttype, byte[] rparam) + { + if (pkttype != 0) + throw new Exception("pkttype must be 0"); + + fixed (byte* prparam = rparam) + { + pack(pkttype, (void*)null, (void*)prparam, rparam.Length); + } + } + + public void pack(int pkttype, int lparam, int[] rparam) + { + if (pkttype != 2) + throw new Exception("pkttype must be 2"); + + fixed (int* prparam = rparam) + { + pack(pkttype, &lparam, (void*)prparam, rparam.Length * 4); + } + } + + public void pack(int pkttype, int lparam, int[] rparam, int length) + { + if (pkttype != 2) + throw new Exception("pkttype must be 2"); + + fixed (int* prparam = rparam) + { + pack(pkttype, &lparam, (void*)prparam, length * 4); + } + } + + public void pack(int pkttype, int[] rparam, int length) + { + if (pkttype != 3) + throw new Exception("pkttype must be 3"); + + fixed (int* prparam = rparam) + { + pack(pkttype, (void*)null, (void*)prparam, length * 4); + } + } + + public void pack(int pkttype) + { + if (pkttype != 1 && pkttype != 4 && pkttype != 5) + throw new Exception("pkttype must be 1, 4 or 5"); + + pack(pkttype, (void*)null, (void*)null, 0); + } + + public void pack(int pkttype, void* lparam, void* rparam, int size) + { + // Set (bit-0 = 1) and (bit-1~15 = type) + m_PacketVector[0].iov_base[m_iSeqNoIndex] = (uint)0x80000000 | (uint)(pkttype << 16); + + // Set additional information and control information field + switch (pkttype) + { + case 2: //0010 - Acknowledgement (ACK) + // ACK packet seq. no. + if (null != lparam) + m_PacketVector[0].iov_base[m_iMsgNoIndex] = *(uint*)lparam; + + // data ACK seq. no. + // optional: RTT (microsends), RTT variance (microseconds) advertised flow window size (packets), and estimated link capacity (packets per second) + m_PacketVector[1] = MakeIovec(rparam, size); + + break; + + case 6: //0110 - Acknowledgement of Acknowledgement (ACK-2) + // ACK packet seq. no. + m_PacketVector[0].iov_base[m_iMsgNoIndex] = *(uint*)lparam; + + // control info field should be none + // but "writev" does not allow this + m_PacketVector[1] = MakeIovec(null, 4); + + break; + + case 3: //0011 - Loss Report (NAK) + // loss list + m_PacketVector[1] = MakeIovec(rparam, size); + + break; + + case 4: //0100 - Congestion Warning + // control info field should be none + // but "writev" does not allow this + m_PacketVector[1] = MakeIovec(null, 4); + + break; + + case 1: //0001 - Keep-alive + // control info field should be none + // but "writev" does not allow this + m_PacketVector[1] = MakeIovec(null, 4); + + break; + + case 0: //0000 - Handshake + // control info filed is handshake info + m_PacketVector[1] = MakeIovec(rparam, size); + + break; + + case 5: //0101 - Shutdown + // control info field should be none + // but "writev" does not allow this + m_PacketVector[1] = MakeIovec(null, 4); + + break; + + case 7: //0111 - Message Drop Request + // msg id + m_PacketVector[0].iov_base[m_iMsgNoIndex] = *(uint*)lparam; + + //first seq no, last seq no + m_PacketVector[1] = MakeIovec(rparam, size); + + break; + + case 8: //1000 - Error Signal from the Peer Side + // Error type + m_PacketVector[0].iov_base[m_iMsgNoIndex] = *(uint*)lparam; + + // control info field should be none + // but "writev" does not allow this + m_PacketVector[1] = MakeIovec(null, 4); + + break; + + case 32767: //0x7FFF - Reserved for user defined control packets + // for extended control packet + // "lparam" contains the extended type information for bit 16 - 31 + // "rparam" is the control information + m_PacketVector[0].iov_base[m_iSeqNoIndex] |= *(uint*)lparam; + + if (null != rparam) + { + m_PacketVector[1] = MakeIovec(rparam, size); + } + else + { + m_PacketVector[1] = MakeIovec(null, 4); + } + + break; + + default: + break; + } + } + + public iovec[] getPacketVector() + { + return m_PacketVector; + } + + public int getFlag() + { + // read bit 0 + return (int)(m_PacketVector[0].iov_base[m_iSeqNoIndex] >> 31); + } + + public int getType() + { + // read bit 1~15 + return (int)((m_PacketVector[0].iov_base[m_iSeqNoIndex] >> 16) & 0x00007FFF); + } + + int getExtendedType() + { + // read bit 16~31 + return (int)(m_PacketVector[0].iov_base[m_iSeqNoIndex] & 0x0000FFFF); + } + + public int getAckSeqNo() + { + // read additional information field + return (int)m_PacketVector[0].iov_base[m_iMsgNoIndex]; + } + + public int getMsgBoundary() + { + // read [1] bit 0~1 + return (int)(m_PacketVector[0].iov_base[m_iMsgNoIndex] >> 30); + } + + public bool getMsgOrderFlag() + { + // read [1] bit 2 + return (1 == ((m_PacketVector[0].iov_base[m_iMsgNoIndex] >> 29) & 1)); + } + + public int getMsgSeq() + { + // read [1] bit 3~31 + return (int)(m_PacketVector[0].iov_base[m_iMsgNoIndex] & 0x1FFFFFFF); + } + } + + public class Handshake + { + public const int m_iContentSize = 48; // Size of hand shake data + + public int m_iVersion; // UDT version + public SocketType m_iType; // UDT socket type + public int m_iISN; // random initial sequence number + public int m_iMSS; // maximum segment size + public int m_iFlightFlagSize; // flow control window size + public int m_iReqType; // connection request type: 1: regular connection request, 0: rendezvous connection request, -1/-2: response + public int m_iID; // socket ID + public int m_iCookie; // cookie + public uint[] m_piPeerIP = new uint[4]; // The IP address that the peer's UDP port is bound to + + public Handshake() + { + for (int i = 0; i < 4; ++i) + m_piPeerIP[i] = 0; + } + + public override string ToString() + { + string type = "connection request"; + if (m_iReqType == 0) + type = "rendezvouz"; + if (m_iReqType < 0) + type = "reponse"; + if (m_iReqType == 1002) + type = "rejected request"; + StringBuilder sb = new StringBuilder(); + sb.AppendLine(" Version " + m_iVersion); + sb.AppendLine(" Type " + type); + sb.AppendLine(" Cookie " + m_iCookie); + //sb.AppendLine(" Socket type " + m_iType.ToString()); + //sb.AppendLine(" Socket id " + m_iID); + sb.AppendLine(" Initial seq# " + m_iISN); + //sb.AppendLine(" MSS " + m_iMSS); + //sb.AppendLine(" Flight size " + m_iFlightFlagSize); + + return sb.ToString(); + } + + public unsafe void serialize(byte[] buf) + { + fixed (byte* pb = buf) + { + int* p = (int*)(pb); + *p++ = m_iVersion; + *p++ = (int)m_iType; + *p++ = m_iISN; + *p++ = m_iMSS; + *p++ = m_iFlightFlagSize; + *p++ = m_iReqType; + *p++ = m_iID; + *p++ = m_iCookie; + for (int i = 0; i < 4; ++i) + *p++ = (int)m_piPeerIP[i]; + } + } + + public unsafe bool deserialize(byte[] buf, int size) + { + if (size < m_iContentSize) + return false; + + fixed (byte* pb = buf) + { + int* p = (int*)(pb); + m_iVersion = *p++; + m_iType = (SocketType)(*p++); + m_iISN = *p++; + m_iMSS = *p++; + m_iFlightFlagSize = *p++; + m_iReqType = *p++; + m_iID = *p++; + m_iCookie = *p++; + for (int i = 0; i < 4; ++i) + m_piPeerIP[i] = (uint)*p++; + } + + return true; + } + } +} diff --git a/framework/Inspectron.HawkEye/UDT/Queue.cs b/framework/Inspectron.HawkEye/UDT/Queue.cs new file mode 100644 index 0000000..6749e78 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/Queue.cs @@ -0,0 +1,903 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace UdtSharp +{ + public class SNode + { + public UDT m_pUDT; // Pointer to the instance of CUDT socket + public ulong m_llTimeStamp; // Time Stamp + + public int m_iHeapLoc; // location on the heap, -1 means not on the heap + }; + + public class RNode + { + public UDT m_pUDT; // Pointer to the instance of CUDT socket + public ulong m_llTimeStamp; // Time Stamp + + public bool m_bOnList; // if the node is already on the list + }; + + class UnitQueue + { + struct QEntry + { + internal Unit[] m_pUnit; // unit queue + internal byte[][] m_pBuffer; // data buffer + internal int m_iSize; // size of each queue + } + List mEntries = new List(); + + int m_iCurrEntry = 0; + int m_iLastEntry = 0; + + int m_iAvailUnit; // recent available unit + int m_iAvailableQueue; + + int m_iSize; // total size of the unit queue, in number of packets + public int m_iCount; // total number of valid packets in the queue + + int m_iMSS; // unit buffer size + AddressFamily m_iIPversion; // IP version + + UnitQueue() + { + m_iSize = 0; + m_iCount = 0; + m_iMSS = 0; + m_iIPversion = 0; + } + + ~UnitQueue() + { + } + + int init(int size, int mss, AddressFamily version) + { + QEntry tempq = new QEntry(); + Unit[] tempu = new Unit[size]; + byte[][] tempb = new byte[size][]; + + for (int i = 0; i < size; ++i) + { + tempb[i] = new byte[mss]; + tempu[i] = new Unit(); + tempu[i].m_iFlag = 0; + + tempu[i].m_Packet.SetDataFromBytes(tempb[i]); + } + tempq.m_pUnit = tempu; + tempq.m_pBuffer = tempb; + tempq.m_iSize = size; + + m_iSize = size; + m_iMSS = mss; + m_iIPversion = version; + + mEntries.Add(tempq); + + return 0; + } + + int increase() + { + // adjust/correct m_iCount + int real_count = 0; + for (int q = 0; q < mEntries.Count; ++q) + { + Unit[] units = mEntries[q].m_pUnit; + for (int u = mEntries[q].m_iSize; u < units.Length; ++u) + if (units[u].m_iFlag != 0) + ++real_count; + } + m_iCount = real_count; + if ((double)m_iCount / m_iSize < 0.9) + return -1; + + // all queues have the same size + int size = mEntries[0].m_iSize; + + QEntry tempq = new QEntry(); + Unit[] tempu = new Unit[size]; + byte[][] tempb = new byte[size][]; + + for (int i = 0; i < size; ++i) + { + tempb[i] = new byte[m_iMSS]; + tempu[i].m_iFlag = 0; + tempu[i].m_Packet.SetDataFromBytes(tempb[i]); + } + tempq.m_pUnit = tempu; + tempq.m_pBuffer = tempb; + tempq.m_iSize = size; + + mEntries.Add(tempq); + + m_iSize += size; + + return 0; + } + + int shrink() + { + // currently queue cannot be shrunk. + return -1; + } + + Unit getNextAvailUnit() + { + if (m_iCount * 10 > m_iSize * 9) + increase(); + + if (m_iCount >= m_iSize) + return null; + + QEntry entrance = mEntries[m_iCurrEntry]; + + //do + //{ + // QEntry currentEntry = mEntries[m_iCurrEntry]; + // Unit sentinel = currentEntry.m_pUnit[currentEntry.m_iSize - 1]; + // for (CUnit* sentinel = m_pCurrQueue.m_pUnit + m_pCurrQueue.m_iSize - 1; m_pAvailUnit != sentinel; ++m_pAvailUnit) + // if (m_pAvailUnit.m_iFlag == 0) + // return m_pAvailUnit; + + + + // if (m_pCurrQueue.m_pUnit.m_iFlag == 0) + // { + // m_pAvailUnit = m_pCurrQueue.m_pUnit; + // return m_pAvailUnit; + // } + + // m_pCurrQueue = m_pCurrQueue.m_pNext; + // m_pAvailUnit = m_pCurrQueue.m_pUnit; + //} while (m_pCurrQueue != entrance); + + increase(); + + return null; + } + } + + public class SndUList + { + object m_ListLock = new object(); + + public object m_pWindowLock; + public EventWaitHandle m_pWindowCond; + + SNode[] m_pHeap; // The heap array + int m_iArrayLength; // physical length of the array + int m_iLastEntry; // position of last entry on the heap array + + public Timer m_pTimer; + + public SndUList() + { + m_iArrayLength = 4096; + m_iLastEntry = -1; + + m_pHeap = new SNode[m_iArrayLength]; + } + + public void insert(ulong ts, UDT u) + { + lock (m_ListLock) + { + // increase the heap array size if necessary + if (m_iLastEntry == m_iArrayLength - 1) + { + Array.Resize(ref m_pHeap, m_iArrayLength * 2); + m_iArrayLength *= 2; + } + + insert_(ts, u); + } + } + + public void update(UDT u, bool reschedule = true) + { + lock (m_ListLock) + { + SNode n = u.m_pSNode; + + if (n.m_iHeapLoc >= 0) + { + if (!reschedule) + return; + + if (n.m_iHeapLoc == 0) + { + n.m_llTimeStamp = 1; + m_pTimer.interrupt(); + return; + } + + remove_(u); + } + + insert_(1, u); + } + } + + public int pop(ref IPEndPoint addr, ref Packet pkt) + { + lock (m_ListLock) + { + if (-1 == m_iLastEntry) + return -1; + + // no pop until the next schedulled time + ulong ts = Timer.rdtsc(); + if (ts < m_pHeap[0].m_llTimeStamp) + return -1; + + UDT u = m_pHeap[0].m_pUDT; + remove_(u); + + if (!u.m_bConnected || u.m_bBroken) + return -1; + + // pack a packet from the socket + if (u.packData(pkt, ref ts) <= 0) + return -1; + + addr = u.m_pPeerAddr; + + // insert a new entry, ts is the next processing time + if (ts > 0) + insert_(ts, u); + + return 1; + } + } + + public void remove(UDT u) + { + lock (m_ListLock) + { + remove_(u); + } + } + + public ulong getNextProcTime() + { + lock (m_ListLock) + { + if (-1 == m_iLastEntry) + return 0; + + return m_pHeap[0].m_llTimeStamp; + } + } + + void insert_(ulong ts, UDT u) + { + SNode n = u.m_pSNode; + + // do not insert repeated node + if (n.m_iHeapLoc >= 0) + return; + + m_iLastEntry++; + m_pHeap[m_iLastEntry] = n; + n.m_llTimeStamp = ts; + + int q = m_iLastEntry; + int p = q; + while (p != 0) + { + p = (q - 1) >> 1; + if (m_pHeap[p].m_llTimeStamp > m_pHeap[q].m_llTimeStamp) + { + SNode t = m_pHeap[p]; + m_pHeap[p] = m_pHeap[q]; + m_pHeap[q] = t; + t.m_iHeapLoc = q; + q = p; + } + else + break; + } + + n.m_iHeapLoc = q; + + // an earlier event has been inserted, wake up sending worker + if (n.m_iHeapLoc == 0) + m_pTimer.interrupt(); + + // first entry, activate the sending queue + if (0 == m_iLastEntry) + { + m_pWindowCond.Set(); + } + } + + void remove_(UDT u) + { + SNode n = u.m_pSNode; + + if (n.m_iHeapLoc >= 0) + { + // remove the node from heap + m_pHeap[n.m_iHeapLoc] = m_pHeap[m_iLastEntry]; + m_iLastEntry--; + m_pHeap[n.m_iHeapLoc].m_iHeapLoc = n.m_iHeapLoc; + + int q = n.m_iHeapLoc; + int p = q * 2 + 1; + while (p <= m_iLastEntry) + { + if ((p + 1 <= m_iLastEntry) && (m_pHeap[p].m_llTimeStamp > m_pHeap[p + 1].m_llTimeStamp)) + p++; + + if (m_pHeap[q].m_llTimeStamp > m_pHeap[p].m_llTimeStamp) + { + SNode t = m_pHeap[p]; + m_pHeap[p] = m_pHeap[q]; + m_pHeap[p].m_iHeapLoc = p; + m_pHeap[q] = t; + m_pHeap[q].m_iHeapLoc = q; + + q = p; + p = q * 2 + 1; + } + else + break; + } + + n.m_iHeapLoc = -1; + } + + // the only event has been deleted, wake up immediately + if (0 == m_iLastEntry) + m_pTimer.interrupt(); + } + } + + public class RendezvousQueue + { + struct CRL + { + internal int m_iID; // UDT socket ID (self) + internal UDT m_pUDT; // UDT instance + internal AddressFamily m_iIPversion; // IP version + internal IPEndPoint m_pPeerAddr; // UDT sonnection peer address + internal ulong m_ullTTL; // the time that this request expires + }; + List m_lRendezvousID = new List(); // The sockets currently in rendezvous mode + + object m_RIDVectorLock = new object(); + + public void insert(int id, UDT u, AddressFamily ipv, IPEndPoint addr, ulong ttl) + { + CRL r; + r.m_iID = id; + r.m_pUDT = u; + r.m_iIPversion = ipv; + r.m_pPeerAddr = addr; + r.m_ullTTL = ttl; + + lock (m_RIDVectorLock) + { + m_lRendezvousID.Add(r); + } + } + + public void remove(int id) + { + lock (m_RIDVectorLock) + { + for (int i = 0; i < m_lRendezvousID.Count; ++i) + { + if (m_lRendezvousID[i].m_iID == id) + { + m_lRendezvousID.RemoveAt(i); + return; + } + } + } + } + + public UDT retrieve(IPEndPoint addr, ref int id) + { + lock (m_RIDVectorLock) + { + foreach (CRL crl in m_lRendezvousID) + { + if (crl.m_pPeerAddr.Equals(addr) && (id == 0) || (id == crl.m_iID)) + { + id = crl.m_iID; + return crl.m_pUDT; + } + } + + return null; + } + } + + public void updateConnStatus() + { + if (m_lRendezvousID.Count == 0) + return; + + lock (m_RIDVectorLock) + { + + foreach (CRL crl in m_lRendezvousID) + { + // avoid sending too many requests, at most 1 request per 250ms + if (Timer.getTime() - (ulong)crl.m_pUDT.m_llLastReqTime > 250000) + { + //if (Timer.getTime() >= crl.m_ullTTL) + //{ + // // connection timer expired, acknowledge app via epoll + // i->m_pUDT->m_bConnecting = false; + // CUDT::s_UDTUnited.m_EPoll.update_events(i->m_iID, i->m_pUDT->m_sPollID, UDT_EPOLL_ERR, true); + // continue; + //} + + Packet request = new Packet(); + request.pack(crl.m_pUDT.m_ConnReq); + // ID = 0, connection request + request.SetId(!crl.m_pUDT.m_bRendezvous ? 0 : crl.m_pUDT.m_ConnRes.m_iID); + crl.m_pUDT.m_pSndQueue.sendto(crl.m_pPeerAddr, request); + crl.m_pUDT.m_llLastReqTime = (long)Timer.getTime(); + } + } + } + } + + } + + public class SndQueue + { + public SndUList m_pSndUList; // List of UDT instances for data sending + public Channel m_pChannel; // The UDP channel for data sending + Timer m_pTimer; // Timing facility + + object m_WindowLock; + EventWaitHandle m_WindowCond; + + volatile bool m_bClosing; // closing the worker + EventWaitHandle m_ExitCond; + + Thread m_WorkerThread; + + public SndQueue() + { + m_WindowLock = new object(); + m_WindowCond = new EventWaitHandle(false, EventResetMode.AutoReset); + m_ExitCond = new EventWaitHandle(false, EventResetMode.AutoReset); + } + + public void Close() + { + m_bClosing = true; + + m_WindowCond.Set(); + if (null != m_WorkerThread) + m_ExitCond.WaitOne(Timeout.Infinite); + + m_WindowCond.Close(); + m_ExitCond.Close(); + } + + public void init(Channel c, Timer t) + { + m_pChannel = c; + m_pTimer = t; + m_pSndUList = new SndUList(); + m_pSndUList.m_pWindowLock = m_WindowLock; + m_pSndUList.m_pWindowCond = m_WindowCond; + m_pSndUList.m_pTimer = m_pTimer; + + m_WorkerThread = new Thread(worker); + m_WorkerThread.IsBackground = true; + m_WorkerThread.Start(this); + } + + static void worker(object param) + { + SndQueue self = param as SndQueue; + if (self == null) + return; + + while (!self.m_bClosing) + { + ulong ts = self.m_pSndUList.getNextProcTime(); + + if (ts > 0) + { + // wait until next processing time of the first socket on the list + ulong currtime = Timer.rdtsc(); + if (currtime < ts) + self.m_pTimer.sleepto(ts); + + // it is time to send the next pkt + IPEndPoint addr = null; + Packet pkt = new Packet(); + if (self.m_pSndUList.pop(ref addr, ref pkt) < 0) + continue; + + self.m_pChannel.sendto(addr, pkt); + } + else + { + // wait here if there is no sockets with data to be sent + self.m_WindowCond.WaitOne(Timeout.Infinite); + } + } + + self.m_ExitCond.Set(); + } + + public int sendto(IPEndPoint addr, Packet packet) + { + // send out the packet immediately (high priority), this is a control packet + m_pChannel.sendto(addr, packet); + return packet.getLength(); + } + } + + public class RcvUList + { + public List m_nodeList = new List(); + + public void insert(UDT u) + { + RNode n = u.m_pRNode; + n.m_llTimeStamp = Timer.rdtsc(); + + // always insert at the end for RcvUList + m_nodeList.Add(n); + } + + public void remove(UDT u) + { + RNode n = u.m_pRNode; + + if (!n.m_bOnList) + return; + + m_nodeList.Remove(n); + } + + public void update(UDT u) + { + RNode n = u.m_pRNode; + + if (!n.m_bOnList) + return; + + RNode match = m_nodeList.Find(x => x.Equals(n)); + if (match.Equals(default(RNode))) + return; + + match.m_llTimeStamp = Timer.rdtsc(); + } + } + + public class RcvQueue + { + RcvUList m_pRcvUList = new RcvUList(); // List of UDT instances that will read packets from the queue + Channel m_pChannel; // UDP channel for receving packets + Timer m_pTimer; // shared timer with the snd queue + + int m_iPayloadSize; // packet payload size + + volatile bool m_bClosing; // closing the workder + EventWaitHandle m_ExitCond; + + object m_LSLock; + UDT m_pListener; // pointer to the (unique, if any) listening UDT entity + RendezvousQueue m_pRendezvousQueue = new RendezvousQueue(); // The list of sockets in rendezvous mode + + List m_vNewEntry = new List(); // newly added entries, to be inserted + object m_IDLock; + + Dictionary> m_mBuffer = new Dictionary>(); // temporary buffer for rendezvous connection request + + object m_PassLock; + EventWaitHandle m_PassCond; + + Thread m_WorkerThread; + + Dictionary m_hash = new Dictionary(); + + public RcvQueue() + { + m_PassLock = new object(); + m_PassCond = new EventWaitHandle(false, EventResetMode.AutoReset); + m_LSLock = new object(); + m_IDLock = new object(); + m_ExitCond = new EventWaitHandle(false, EventResetMode.AutoReset); + } + + public void Close() + { + m_bClosing = true; + + if (null != m_WorkerThread) + m_ExitCond.WaitOne(Timeout.Infinite); + + m_PassCond.Close(); + m_ExitCond.Close(); + } + + public void init(int qsize, int payload, AddressFamily version, int hsize, Channel cc, Timer t) + { + m_iPayloadSize = payload; + + m_pChannel = cc; + m_pTimer = t; + + m_WorkerThread = new Thread(worker); + m_WorkerThread.IsBackground = true; + m_WorkerThread.Start(this); + } + + static void worker(object param) + { + RcvQueue self = param as RcvQueue; + if (self == null) + return; + + IPEndPoint addr = new IPEndPoint(IPAddress.Any, 0); + UDT u = null; + int id; + + while (!self.m_bClosing) + { + self.m_pTimer.tick(); + + // check waiting list, if new socket, insert it to the list + while (self.ifNewEntry()) + { + UDT ne = self.getNewEntry(); + if (null != ne) + { + self.m_pRcvUList.insert(ne); + self.m_hash.Add(ne.m_SocketID, ne); + } + } + + // find next available slot for incoming packet + Unit unit = new Unit(); + unit.m_Packet.setLength(self.m_iPayloadSize); + + // reading next incoming packet, recvfrom returns -1 is nothing has been received + if (self.m_pChannel.recvfrom(ref addr, unit.m_Packet) < 0) + goto TIMER_CHECK; + + id = unit.m_Packet.GetId(); + + // ID 0 is for connection request, which should be passed to the listening socket or rendezvous sockets + if (0 == id) + { + if (null != self.m_pListener) + self.m_pListener.listen(addr, unit.m_Packet); + else if (null != (u = self.m_pRendezvousQueue.retrieve(addr, ref id))) + { + // asynchronous connect: call connect here + // otherwise wait for the UDT socket to retrieve this packet + if (!u.m_bSynRecving) + u.connect(unit.m_Packet); + else + { + Packet newPacket = new Packet(); + newPacket.Clone(unit.m_Packet); + self.storePkt(id, newPacket); + } + } + } + else if (id > 0) + { + if (self.m_hash.TryGetValue(id, out u)) + { + if (addr.Equals(u.m_pPeerAddr)) + { + if (u.m_bConnected && !u.m_bBroken && !u.m_bClosing) + { + if (0 == unit.m_Packet.getFlag()) + u.processData(unit); + else + u.processCtrl(unit.m_Packet); + + u.checkTimers(); + self.m_pRcvUList.update(u); + } + } + } + else if (null != (u = self.m_pRendezvousQueue.retrieve(addr, ref id))) + { + if (!u.m_bSynRecving) + u.connect(unit.m_Packet); + else + { + Packet newPacket = new Packet(); + newPacket.Clone(unit.m_Packet); + self.storePkt(id, newPacket); + } + } + } + + TIMER_CHECK: + // take care of the timing event for all UDT sockets + + ulong currtime = Timer.rdtsc(); + + ulong ctime = currtime - 100000 * Timer.getCPUFrequency(); + for (int i = 0; i < self.m_pRcvUList.m_nodeList.Count; ++i) + { + RNode ul = self.m_pRcvUList.m_nodeList[0]; + if (ul.m_llTimeStamp >= ctime) + break; + + u = ul.m_pUDT; + + if (u.m_bConnected && !u.m_bBroken && !u.m_bClosing) + { + u.checkTimers(); + self.m_pRcvUList.update(u); + } + else + { + // the socket must be removed from Hash table first, then RcvUList + self.m_hash.Remove(u.m_SocketID); + self.m_pRcvUList.remove(u); + u.m_pRNode.m_bOnList = false; + } + } + + // Check connection requests status for all sockets in the RendezvousQueue. + self.m_pRendezvousQueue.updateConnStatus(); + } + + + self.m_ExitCond.Set(); + } + + public int recvfrom(int id, Packet packet) + { + bool gotLock = false; + Monitor.Enter(m_PassLock, ref gotLock); + + Queue packetQueue; + if (!m_mBuffer.TryGetValue(id, out packetQueue)) + { + if (gotLock) + Monitor.Exit(m_PassLock); + m_PassCond.WaitOne(1000); + + lock (m_PassLock) + { + + if (!m_mBuffer.TryGetValue(id, out packetQueue)) + { + packet.setLength(-1); + return -1; + } + } + } + + if (gotLock && Monitor.IsEntered(m_PassLock)) + Monitor.Exit(m_PassLock); + + // retrieve the earliest packet + Packet newpkt = packetQueue.Peek(); + + if (packet.getLength() < newpkt.getLength()) + { + packet.setLength(-1); + return -1; + } + + // copy packet content + + packet.Clone(newpkt); + + packetQueue.Dequeue(); + if (packetQueue.Count == 0) + { + lock (m_PassLock) + { + m_mBuffer.Remove(id); + } + } + + return packet.getLength(); + } + + public int setListener(UDT u) + { + lock (m_LSLock) + { + + if (null != m_pListener) + return -1; + + m_pListener = u; + return 0; + } + } + + public void removeListener(UDT u) + { + lock (m_LSLock) + { + if (u == m_pListener) + m_pListener = null; + } + } + + public void registerConnector(int id, UDT u, AddressFamily ipv, IPEndPoint addr, ulong ttl) + { + m_pRendezvousQueue.insert(id, u, ipv, addr, ttl); + } + + public void removeConnector(int id) + { + m_pRendezvousQueue.remove(id); + lock (m_PassLock) + { + m_mBuffer.Remove(id); + } + } + + public void setNewEntry(UDT u) + { + lock (m_IDLock) + { + m_vNewEntry.Add(u); + } + } + + bool ifNewEntry() + { + return !(m_vNewEntry.Count == 0); + } + + UDT getNewEntry() + { + lock (m_IDLock) + { + if (m_vNewEntry.Count == 0) + return null; + + UDT u = m_vNewEntry[0]; + m_vNewEntry.RemoveAt(0); + return u; + } + } + + void storePkt(int id, Packet pkt) + { + lock (m_PassLock) + { + Queue packetQueue; + if (!m_mBuffer.TryGetValue(id, out packetQueue)) + { + packetQueue = new Queue(); + packetQueue.Enqueue(pkt); + m_mBuffer.Add(id, packetQueue); + + m_PassCond.Set(); + } + else + { + //avoid storing too many packets, in case of malfunction or attack + if (packetQueue.Count > 16) + return; + + packetQueue.Enqueue(pkt); + } + } + } + } +} diff --git a/framework/Inspectron.HawkEye/UDT/SequenceNumber.cs b/framework/Inspectron.HawkEye/UDT/SequenceNumber.cs new file mode 100644 index 0000000..b3677e3 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/SequenceNumber.cs @@ -0,0 +1,55 @@ +// UDT Sequence Number 0 - (2^31 - 1) + +// seqcmp: compare two seq#, considering the wraping +// seqlen: length from the 1st to the 2nd seq#, including both +// seqoff: offset from the 2nd to the 1st seq# +// incseq: increase the seq# by 1 +// decseq: decrease the seq# by 1 +// incseq: increase the seq# by a given offset + +using System; + +namespace UdtSharp +{ + static class SequenceNumber + { + public static int seqcmp(int seq1, int seq2) + { + return (Math.Abs(seq1 - seq2) < m_iSeqNoTH) ? (seq1 - seq2) : (seq2 - seq1); + } + + public static int seqlen(int seq1, int seq2) + { + return (seq1 <= seq2) ? (seq2 - seq1 + 1) : (seq2 - seq1 + m_iMaxSeqNo + 2); + } + + public static int seqoff(int seq1, int seq2) + { + if (Math.Abs(seq1 - seq2) < m_iSeqNoTH) + return seq2 - seq1; + + if (seq1 < seq2) + return seq2 - seq1 - m_iMaxSeqNo - 1; + + return seq2 - seq1 + m_iMaxSeqNo + 1; + } + + public static int incseq(int seq) + { + return (seq == m_iMaxSeqNo) ? 0 : seq + 1; + } + + public static int decseq(int seq) + { + return (seq == 0) ? m_iMaxSeqNo : seq - 1; + } + + public static int incseq(int seq, int inc) + { + return (m_iMaxSeqNo - seq >= inc) ? seq + inc : seq - m_iMaxSeqNo + inc - 1; + } + + public static int m_iSeqNoTH = 0x3FFFFFFF; // threshold for comparing seq. no. + public static int m_iMaxSeqNo = 0x7FFFFFFF; // maximum sequence number used in UDT + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/UDT/Timer.cs b/framework/Inspectron.HawkEye/UDT/Timer.cs new file mode 100644 index 0000000..e107fc7 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/Timer.cs @@ -0,0 +1,115 @@ +using System; +using System.Diagnostics; +using System.Threading; + +namespace UdtSharp +{ + public class Timer + { + ulong m_ullSchedTime; // next schedulled time + static ulong s_ullCPUFrequency = readCPUFrequency();// CPU frequency : clock cycles per microsecond + + EventWaitHandle m_TickCond = new EventWaitHandle(false, EventResetMode.AutoReset); + object m_TickLock = new object(); + + static EventWaitHandle m_EventCond = new EventWaitHandle(false, EventResetMode.AutoReset); + static object m_EventLock = new object(); + + static bool m_bUseMicroSecond = false; // sepcial handling if timer frequency is low (< 10 ticks per microsecond) + + public Timer() + { + + } + + public static ulong rdtsc() + { + if (m_bUseMicroSecond) + { + return getTime(); + } + return (ulong)Stopwatch.GetTimestamp(); + } + + public void Stop() + { + m_TickCond.Close(); + } + + static ulong readCPUFrequency() + { + long ticksPerSecond = Stopwatch.Frequency; + long ticksPerMicroSecond = ticksPerSecond / 1000000L; + + if (ticksPerMicroSecond < 10) + { + m_bUseMicroSecond = true; + return 1; + } + + return (ulong)ticksPerMicroSecond; + } + + public static ulong getCPUFrequency() + { + // ticks per microsecond + return (ulong)s_ullCPUFrequency; + } + + void sleep(ulong interval) + { + ulong t = rdtsc(); + + // sleep next "interval" time + sleepto(t + interval); + } + + public void sleepto(ulong nexttime) + { + // Use class member such that the method can be interrupted by others + m_ullSchedTime = nexttime; + + ulong t = rdtsc(); + + while (t < m_ullSchedTime) + { + m_TickCond.WaitOne(1); + + t = rdtsc(); + } + } + + public void interrupt() + { + // schedule the sleepto time to the current CCs, so that it will stop + m_ullSchedTime = rdtsc(); + tick(); + } + + public void tick() + { + m_TickCond.Set(); + } + + public static ulong getTime() + { + // microsecond resolution + return (ulong)DateTime.Now.Ticks / 10; + } + + public static void triggerEvent() + { + m_EventCond.Set(); + } + + static void waitForEvent() + { + m_EventCond.WaitOne(1); + } + + static void sleep() + { + Thread.Sleep(1); + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.HawkEye/UDT/UdtException.cs b/framework/Inspectron.HawkEye/UDT/UdtException.cs new file mode 100644 index 0000000..a404fbb --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/UdtException.cs @@ -0,0 +1,291 @@ +using System; +using System.Runtime.InteropServices; + +namespace UdtSharp +{ + public class UdtException : Exception + { + int m_iMajor; + int m_iMinor; + int m_iErrno; + + public UdtException(int major = 0, int minor = 0, int err = -1) + { + m_iMajor = major; + m_iMinor = minor; + if (-1 == err) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + m_iErrno = Marshal.GetLastWin32Error(); + + // TODO handle non-windows error + } + else + m_iErrno = err; + } + + public string getErrorMessage() + { + // translate "Major:Minor" code into text message. + + string strMsg = string.Empty; + + switch (m_iMajor) + { + case 0: + strMsg = "Success"; + break; + + case 1: + strMsg = "Connection setup failure"; + + switch (m_iMinor) + { + case 1: + strMsg += ": connection time out"; + break; + + case 2: + strMsg += ": connection rejected"; + break; + + case 3: + strMsg += ": unable to create/configure UDP socket"; + break; + + case 4: + strMsg += ": abort for security reasons"; + break; + + default: + break; + } + + break; + + case 2: + switch (m_iMinor) + { + case 1: + strMsg = "Connection was broken"; + break; + + case 2: + strMsg = "Connection does not exist"; + break; + + default: + break; + } + + break; + + case 3: + strMsg = "System resource failure"; + + switch (m_iMinor) + { + case 1: + strMsg += ": unable to create new threads"; + break; + + case 2: + strMsg += ": unable to allocate buffers"; + break; + + default: + break; + } + + break; + + case 4: + strMsg = "File system failure"; + + switch (m_iMinor) + { + case 1: + strMsg += ": cannot seek read position"; + break; + + case 2: + strMsg += ": failure in read"; + break; + + case 3: + strMsg += ": cannot seek write position"; + break; + + case 4: + strMsg += ": failure in write"; + break; + + default: + break; + } + + break; + + case 5: + strMsg = "Operation not supported"; + + switch (m_iMinor) + { + case 1: + strMsg += ": Cannot do this operation on a BOUND socket"; + break; + + case 2: + strMsg += ": Cannot do this operation on a CONNECTED socket"; + break; + + case 3: + strMsg += ": Bad parameters"; + break; + + case 4: + strMsg += ": Invalid socket ID"; + break; + + case 5: + strMsg += ": Cannot do this operation on an UNBOUND socket"; + break; + + case 6: + strMsg += ": Socket is not in listening state"; + break; + + case 7: + strMsg += ": Listen/accept is not supported in rendezous connection setup"; + break; + + case 8: + strMsg += ": Cannot call connect on UNBOUND socket in rendezvous connection setup"; + break; + + case 9: + strMsg += ": This operation is not supported in SOCK_STREAM mode"; + break; + + case 10: + strMsg += ": This operation is not supported in SOCK_DGRAM mode"; + break; + + case 11: + strMsg += ": Another socket is already listening on the same port"; + break; + + case 12: + strMsg += ": Message is too large to send (it must be less than the UDT send buffer size)"; + break; + + case 13: + strMsg += ": Invalid epoll ID"; + break; + + default: + break; + } + + break; + + case 6: + strMsg = "Non-blocking call failure"; + + switch (m_iMinor) + { + case 1: + strMsg += ": no buffer available for sending"; + break; + + case 2: + strMsg += ": no data available for reading"; + break; + + default: + break; + } + + break; + + case 7: + strMsg = "The peer side has signalled an error"; + + break; + + default: + strMsg = "Unknown error"; + break; + } + + // // Adding "errno" information + // if ((0 != m_iMajor) && (0 < m_iErrno)) + // { + // strMsg += ": "; + //# ifndef WIN32 + // char errmsg[1024]; + // if (strerror_r(m_iErrno, errmsg, 1024) == 0) + // strMsg += errmsg; + //#else + // LPVOID lpMsgBuf; + // FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, m_iErrno, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) & lpMsgBuf, 0, NULL); + // strMsg += (char*)lpMsgBuf; + // LocalFree(lpMsgBuf); + //#endif + // } + + return strMsg; + } + + public int getErrorCode() + { + return m_iMajor * 1000 + m_iMinor; + } + + void clear() + { + m_iMajor = 0; + m_iMinor = 0; + m_iErrno = 0; + } + + const int SUCCESS = 0; + const int ECONNSETUP = 1000; + const int ENOSERVER = 1001; + const int ECONNREJ = 1002; + const int ESOCKFAIL = 1003; + const int ESECFAIL = 1004; + const int ECONNFAIL = 2000; + const int ECONNLOST = 2001; + const int ENOCONN = 2002; + const int ERESOURCE = 3000; + const int ETHREAD = 3001; + const int ENOBUF = 3002; + const int EFILE = 4000; + const int EINVRDOFF = 4001; + const int ERDPERM = 4002; + const int EINVWROFF = 4003; + const int EWRPERM = 4004; + const int EINVOP = 5000; + const int EBOUNDSOCK = 5001; + const int ECONNSOCK = 5002; + const int EINVPARAM = 5003; + const int EINVSOCK = 5004; + const int EUNBOUNDSOCK = 5005; + const int ENOLISTEN = 5006; + const int ERDVNOSERV = 5007; + const int ERDVUNBOUND = 5008; + const int ESTREAMILL = 5009; + const int EDGRAMILL = 5010; + const int EDUPLISTEN = 5011; + const int ELARGEMSG = 5012; + const int EINVPOLLID = 5013; + const int EASYNCFAIL = 6000; + const int EASYNCSND = 6001; + const int EASYNCRCV = 6002; + const int ETIMEOUT = 6003; + const int EPEERERR = 7000; + const int EUNKNOWN = -1; + + } +} diff --git a/framework/Inspectron.HawkEye/UDT/UdtNetworkStream.cs b/framework/Inspectron.HawkEye/UDT/UdtNetworkStream.cs new file mode 100644 index 0000000..212dc44 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/UdtNetworkStream.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; + +namespace UdtSharp +{ + public class UdtNetworkStream : Stream + { + public UdtNetworkStream(UdtSocket socket) + { + mSocket = socket; + } + + public override bool CanRead { get { return true; } } + + public override bool CanSeek { get { return false; } } + + public override bool CanWrite { get { return true; } } + + public override long Length { get { throw new NotImplementedException(); } } + + public override long Position { get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + return mSocket.Receive(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + mSocket.Send(buffer, offset, count); + } + + UdtSocket mSocket; + } +} diff --git a/framework/Inspectron.HawkEye/UDT/UdtSocket.cs b/framework/Inspectron.HawkEye/UDT/UdtSocket.cs new file mode 100644 index 0000000..a1e160a --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/UdtSocket.cs @@ -0,0 +1,156 @@ +using System; +using System.Net; +using System.Net.Sockets; + +namespace UdtSharp +{ + public class UdtSocket + { + public UdtSocket(AddressFamily addressFamily, SocketType socketType) + { + UDT.s_UDTUnited.startup(); + + try + { + mSocketId = UDT.s_UDTUnited.newSocket(addressFamily, socketType); + mLocalEndPoint = new IPEndPoint(IPAddress.Any, 0); + } + catch (UdtException udtException) + { + throw new Exception(udtException.getErrorMessage(), udtException); + } + } + + public int Bind(IPEndPoint serverAddress) + { + try + { + int status = UDT.s_UDTUnited.bind(mSocketId, serverAddress); + mLocalEndPoint = serverAddress; + return status; + } + catch (UdtException udtException) + { + throw new Exception(udtException.getErrorMessage(), udtException); + } + } + + public int Listen(int maxConnections) + { + try + { + return UDT.s_UDTUnited.listen(mSocketId, maxConnections); + } + catch (UdtException udtException) + { + throw new Exception(udtException.getErrorMessage(), udtException); + } + } + + public UdtSocket Accept() + { + try + { + IPEndPoint clientEndPoint = null; + int clientSocketId = UDT.s_UDTUnited.accept(mSocketId, ref clientEndPoint); + if (clientSocketId == UDT.INVALID_SOCK) + return null; + + return new UdtSocket(clientSocketId, clientEndPoint, mLocalEndPoint); + } + catch (UdtException udtException) + { + throw new Exception(udtException.getErrorMessage(), udtException); + } + } + + public int Connect(IPEndPoint server) + { + try + { + int status = UDT.s_UDTUnited.connect(mSocketId, server); + mRemoteEndPoint = server; + return status; + } + catch (UdtException udtException) + { + throw new Exception(udtException.getErrorMessage(), udtException); + } + } + + public bool IsConnected() + { + return UDT.s_UDTUnited.getStatus(mSocketId) == UDTSTATUS.CONNECTED; + } + + public int Send(byte[] data, int offset, int length) + { + try + { + UDT udt = UDT.s_UDTUnited.lookup(mSocketId); + + return udt.send(data, offset, length); + } + catch (UdtException udtException) + { + throw new Exception(udtException.getErrorMessage(), udtException); + } + } + + public int Receive(byte[] data, int offset, int length) + { + try + { + UDT udt = UDT.s_UDTUnited.lookup(mSocketId); + return udt.recv(data, offset, length); + } + catch (UdtException udtException) + { + throw new Exception(udtException.getErrorMessage(), udtException); + } + } + public int ReceiveMSG(byte[] data, int length) + { + try + { + UDT udt = UDT.s_UDTUnited.lookup(mSocketId); + return udt.recvmsg(data, length); + } + catch (UdtException udtException) + { + throw new Exception(udtException.getErrorMessage(), udtException); + } + } + public int Close() + { + try + { + return UDT.s_UDTUnited.close(mSocketId); + } + catch (UdtException udtException) + { + throw new Exception(udtException.getErrorMessage(), udtException); + } + } + + public IPEndPoint LocalEndPoint { get { return mLocalEndPoint; } } + public IPEndPoint RemoteEndPoint { get { return mRemoteEndPoint; } } + + UdtSocket(int iSocketID, IPEndPoint localEndPoint, IPEndPoint remoteEndPoint) + { + mSocketId = iSocketID; + mLocalEndPoint = localEndPoint; + mRemoteEndPoint = remoteEndPoint; + } + + int mSocketId; + IPEndPoint mLocalEndPoint; + IPEndPoint mRemoteEndPoint; + + delegate int ReceiveDelegate(byte[] buffer, int offset, int count); + ReceiveDelegate mReceiveDelegate = null; + delegate int SendDelegate(byte[] buffer, int offset, int count); + SendDelegate mSendDelegate = null; + } +} + diff --git a/framework/Inspectron.HawkEye/UDT/UdtUnited.cs b/framework/Inspectron.HawkEye/UDT/UdtUnited.cs new file mode 100644 index 0000000..c3232d9 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/UdtUnited.cs @@ -0,0 +1,960 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +using UDTSOCKET = System.Int32; + +namespace UdtSharp +{ + class Multiplexer + { + internal SndQueue m_pSndQueue; // The sending queue + internal RcvQueue m_pRcvQueue; // The receiving queue + internal Channel m_pChannel; // The UDP channel for sending and receiving + internal Timer m_pTimer; // The timer + + internal int m_iPort; // The UDP port number of this multiplexer + internal AddressFamily m_iIPversion; // IP version + internal int m_iMSS; // Maximum Segment Size + internal int m_iRefCount; // number of UDT instances that are associated with this multiplexer + internal bool m_bReusable; // if this one can be shared with others + + internal int m_iID; // multiplexer ID + } + + internal class UdtSocketInternal + { + public UDTSTATUS m_Status; // current socket state + + public ulong m_TimeStamp; // time when the socket is closed + + public AddressFamily m_iIPversion; // IP version + public IPEndPoint m_pSelfAddr; // pointer to the local address of the socket + public IPEndPoint m_pPeerAddr; // pointer to the peer address of the socket + + public UDTSOCKET m_SocketID; // socket ID + public UDTSOCKET m_ListenSocket; // ID of the listener socket; 0 means this is an independent socket + + public UDTSOCKET m_PeerID; // peer socket ID + public int m_iISN; // initial sequence number, used to tell different connection from same IP:port + + public UDT m_pUDT; // pointer to the UDT entity + + public HashSet m_pQueuedSockets; // set of connections waiting for accept() + public HashSet m_pAcceptSockets; // set of accept()ed connections + + public EventWaitHandle m_AcceptCond = new EventWaitHandle(false, EventResetMode.AutoReset);// used to block "accept" call + public object m_AcceptLock = new object(); // mutex associated to m_AcceptCond + + public uint m_uiBackLog; // maximum number of connections in queue + + public int m_iMuxID; // multiplexer ID + + public object m_ControlLock = new object(); // lock this socket exclusively for control APIs: bind/listen/connect + + public UdtSocketInternal() + { + m_Status = UDTSTATUS.INIT; + m_iMuxID = -1; + } + + public void Close() + { + m_AcceptCond.Close(); + } + } + + public class UdtUnited + { + Dictionary m_Sockets = new Dictionary(); // stores all the socket structures + + object m_ControlLock = new object(); // used to synchronize UDT API + + object m_IDLock = new object(); // used to synchronize ID generation + UDTSOCKET m_SocketID; // seed to generate a new unique socket ID + + Dictionary> m_PeerRec = new Dictionary>();// record sockets from peers to avoid repeated connection request, int64_t = (socker_id << 30) + isn + + //pthread_key_t m_TLSError; // thread local error record (last error) + Dictionary m_mTLSRecord; + object m_TLSLock = new object(); + + Dictionary m_mMultiplexer = new Dictionary(); // UDP multiplexer + object m_MultiplexerLock = new object(); + + HashSet m_pCache = new HashSet(); // UDT network information cache + + volatile bool m_bClosing; + object m_GCStopLock = new object(); + EventWaitHandle m_GCStopCond = new EventWaitHandle(false, EventResetMode.AutoReset); + + object m_InitLock = new object(); + int m_iInstanceCount; // number of startup() called by application + + Dictionary m_ClosedSockets = new Dictionary(); // temporarily store closed sockets + + static Random m_random = new Random(); + + public UdtUnited() + { + // Socket ID MUST start from a random value + m_SocketID = 1 + (int)((1 << 30) * m_random.NextDouble()); + + //m_TLSError = TlsAlloc(); + } + + ~UdtUnited() + { + //TlsFree(m_TLSError); + } + + public int startup() + { + lock (m_InitLock) + { + ++m_iInstanceCount; + return 0; + } + } + + public int cleanup() + { + lock (m_InitLock) + { + if (--m_iInstanceCount > 0) + return 0; + } + + m_bClosing = true; + return 0; + } + + public UDTSOCKET newSocket(AddressFamily af, SocketType type) + { + if ((type != SocketType.Stream) && (type != SocketType.Dgram)) + throw new UdtException(5, 3, 0); + + UdtSocketInternal ns = new UdtSocketInternal(); + ns.m_pUDT = new UDT(); + ns.m_pSelfAddr = new IPEndPoint(IPAddress.Any, 0); + + lock (m_IDLock) + { + ns.m_SocketID = --m_SocketID; + } + + ns.m_Status = UDTSTATUS.INIT; + ns.m_ListenSocket = 0; + ns.m_pUDT.m_SocketID = ns.m_SocketID; + ns.m_pUDT.m_iSockType = type; + ns.m_pUDT.m_iIPversion = af; + ns.m_pUDT.m_pCache = m_pCache; + + // protect the m_Sockets structure. + lock (m_ControlLock) + { + m_Sockets[ns.m_SocketID] = ns; + } + + return ns.m_SocketID; + } + + public int newConnection(UDTSOCKET listen, IPEndPoint peer, Handshake hs) + { + UdtSocketInternal ns = null; + UdtSocketInternal ls = locate(listen); + + if (null == ls) + return -1; + + // if this connection has already been processed + if (null != (ns = locate(peer, hs.m_iID, hs.m_iISN))) + { + if (ns.m_pUDT.m_bBroken) + { + // last connection from the "peer" address has been broken + ns.m_Status = UDTSTATUS.CLOSED; + ns.m_TimeStamp = Timer.getTime(); + + lock (ls.m_AcceptLock) + { + ls.m_pQueuedSockets.Remove(ns.m_SocketID); + ls.m_pAcceptSockets.Remove(ns.m_SocketID); + } + } + else + { + // connection already exist, this is a repeated connection request + // respond with existing HS information + + hs.m_iISN = ns.m_pUDT.m_iISN; + hs.m_iMSS = ns.m_pUDT.m_iMSS; + hs.m_iFlightFlagSize = ns.m_pUDT.m_iFlightFlagSize; + hs.m_iReqType = -1; + hs.m_iID = ns.m_SocketID; + + return 0; + + //except for this situation a new connection should be started + } + } + + // exceeding backlog, refuse the connection request + if (ls.m_pQueuedSockets.Count >= ls.m_uiBackLog) + return -1; + + ns = new UdtSocketInternal(); + ns.m_pUDT = new UDT(ls.m_pUDT); + ns.m_pSelfAddr = new IPEndPoint(IPAddress.Any, 0); + ns.m_pPeerAddr = peer; + + lock (m_IDLock) + { + ns.m_SocketID = --m_SocketID; + } + + ns.m_ListenSocket = listen; + ns.m_iIPversion = ls.m_iIPversion; + ns.m_pUDT.m_SocketID = ns.m_SocketID; + ns.m_PeerID = hs.m_iID; + ns.m_iISN = hs.m_iISN; + + int error = 0; + + try + { + // bind to the same addr of listening socket + ns.m_pUDT.open(); + updateMux(ns, ls); + ns.m_pUDT.connect(peer, hs); + } + catch (Exception e) + { + error = 1; + goto ERR_ROLLBACK; + } + + ns.m_Status = UDTSTATUS.CONNECTED; + + // copy address information of local node + ns.m_pUDT.m_pSndQueue.m_pChannel.getSockAddr(ref ns.m_pSelfAddr); + ConvertIPAddress.ToUintArray(ns.m_pSelfAddr.Address, ref ns.m_pUDT.m_piSelfIP); + + // protect the m_Sockets structure. + lock (m_ControlLock) + { + m_Sockets[ns.m_SocketID] = ns; + HashSet sockets; + if (!m_PeerRec.TryGetValue((ns.m_PeerID << 30) + ns.m_iISN, out sockets)) + { + sockets = new HashSet(); + m_PeerRec.Add((ns.m_PeerID << 30) + ns.m_iISN, sockets); + } + + sockets.Add(ns.m_SocketID); + } + + lock (ls.m_AcceptLock) + { + ls.m_pQueuedSockets.Add(ns.m_SocketID); + } + + // acknowledge users waiting for new connections on the listening socket + //m_EPoll.update_events(listen, ls.m_pUDT.m_sPollID, UDT_EPOLL_IN, true); + + Timer.triggerEvent(); + + ERR_ROLLBACK: + if (error > 0) + { + ns.m_pUDT.close(); + ns.m_Status = UDTSTATUS.CLOSED; + ns.m_TimeStamp = Timer.getTime(); + + return -1; + } + + // wake up a waiting accept() call + ls.m_AcceptCond.Set(); + + return 1; + } + + public UDT lookup(UDTSOCKET u) + { + // protects the m_Sockets structure + lock (m_ControlLock) + { + UdtSocketInternal socket; + if (!m_Sockets.TryGetValue(u, out socket) || socket.m_Status == UDTSTATUS.CLOSED) + throw new UdtException(5, 4, 0); + + return socket.m_pUDT; + } + } + + public UDTSTATUS getStatus(UDTSOCKET u) + { + // protects the m_Sockets structure + lock (m_ControlLock) + { + UdtSocketInternal socket; + if (m_Sockets.TryGetValue(u, out socket)) + { + if (socket.m_pUDT.m_bBroken) + return UDTSTATUS.BROKEN; + + return socket.m_Status; + } + + if (m_ClosedSockets.ContainsKey(u)) + return UDTSTATUS.CLOSED; + + return UDTSTATUS.NONEXIST; + } + } + + public int bind(UDTSOCKET u, IPEndPoint name) + { + UdtSocketInternal s = locate(u); + if (null == s) + throw new UdtException(5, 4, 0); + + lock (s.m_ControlLock) + { + // cannot bind a socket more than once + if (UDTSTATUS.INIT != s.m_Status) + throw new UdtException(5, 0, 0); + + s.m_pUDT.open(); + updateMux(s, name); + s.m_Status = UDTSTATUS.OPENED; + + // copy address information of local node + s.m_pUDT.m_pSndQueue.m_pChannel.getSockAddr(ref s.m_pSelfAddr); + + return 0; + } + } + + + public int bind(UDTSOCKET u, Socket udpsock) + { + UdtSocketInternal s = locate(u); + if (null == s) + throw new UdtException(5, 4, 0); + + lock (s.m_ControlLock) + { + + // cannot bind a socket more than once + if (UDTSTATUS.INIT != s.m_Status) + throw new UdtException(5, 0, 0); + + IPEndPoint name = null; + s.m_pUDT.m_pSndQueue.m_pChannel.getSockAddr(ref name); //TODO CHECK THIS + + s.m_pUDT.open(); + updateMux(s, name, udpsock); + s.m_Status = UDTSTATUS.OPENED; + + // copy address information of local node + s.m_pUDT.m_pSndQueue.m_pChannel.getSockAddr(ref s.m_pSelfAddr); + + return 0; + } + } + + public int listen(UDTSOCKET u, int backlog) + { + UdtSocketInternal s = locate(u); + if (null == s) + throw new UdtException(5, 4, 0); + + lock (s.m_ControlLock) + { + + // do nothing if the socket is already listening + if (UDTSTATUS.LISTENING == s.m_Status) + return 0; + + // a socket can listen only if is in UDTSTATUS.OPENED status + if (UDTSTATUS.OPENED != s.m_Status) + throw new UdtException(5, 5, 0); + + // listen is not supported in rendezvous connection setup + if (s.m_pUDT.m_bRendezvous) + throw new UdtException(5, 7, 0); + + if (backlog <= 0) + throw new UdtException(5, 3, 0); + + s.m_uiBackLog = (uint)backlog; + + s.m_pQueuedSockets = new HashSet(); + s.m_pAcceptSockets = new HashSet(); + + s.m_pUDT.listen(); + + s.m_Status = UDTSTATUS.LISTENING; + + return 0; + } + } + + public UDTSOCKET accept(UDTSOCKET listen, ref IPEndPoint addr) + { + if (null != addr) + throw new UdtException(5, 3, 0); + + UdtSocketInternal ls = locate(listen); + + if (ls == null) + throw new UdtException(5, 4, 0); + + // the "listen" socket must be in UDTSTATUS.LISTENING status + if (UDTSTATUS.LISTENING != ls.m_Status) + throw new UdtException(5, 6, 0); + + // no "accept" in rendezvous connection setup + if (ls.m_pUDT.m_bRendezvous) + throw new UdtException(5, 7, 0); + + UDTSOCKET u = UDT.INVALID_SOCK; + bool accepted = false; + + // !!only one conection can be set up each time!! + while (!accepted) + { + lock (ls.m_AcceptLock) + { + if (ls.m_pQueuedSockets.Count > 0) + { + HashSet.Enumerator e = ls.m_pQueuedSockets.GetEnumerator(); + e.MoveNext(); + u = e.Current; + ls.m_pAcceptSockets.Add(u); + ls.m_pQueuedSockets.Remove(u); + + accepted = true; + } + else if (!ls.m_pUDT.m_bSynRecving) + accepted = true; + } + + if (!accepted & (UDTSTATUS.LISTENING == ls.m_Status)) + ls.m_AcceptCond.WaitOne(Timeout.Infinite); + + if ((UDTSTATUS.LISTENING != ls.m_Status) || ls.m_pUDT.m_bBroken) + { + // Send signal to other threads that are waiting to accept. + ls.m_AcceptCond.Set(); + accepted = true; + } + + //if (ls.m_pQueuedSockets.Count == 0) + // m_EPoll.update_events(listen, ls.m_pUDT.m_sPollID, UDT_EPOLL_IN, false); + } + + if (u == UDT.INVALID_SOCK) + { + // non-blocking receiving, no connection available + if (!ls.m_pUDT.m_bSynRecving) + throw new UdtException(6, 2, 0); + + // listening socket is closed + throw new UdtException(5, 6, 0); + } + + addr = locate(u).m_pPeerAddr; + + return u; + } + + public int connect(UDTSOCKET u, IPEndPoint name) + { + UdtSocketInternal s = locate(u); + if (null == s) + throw new UdtException(5, 4, 0); + + lock (s.m_ControlLock) + { + // a socket can "connect" only if it is in INIT or UDTSTATUS.OPENED status + if (UDTSTATUS.INIT == s.m_Status) + { + if (!s.m_pUDT.m_bRendezvous) + { + s.m_pUDT.open(); + updateMux(s); + s.m_Status = UDTSTATUS.OPENED; + } + else + throw new UdtException(5, 8, 0); + } + else if (UDTSTATUS.OPENED != s.m_Status) + throw new UdtException(5, 2, 0); + + // connect_complete() may be called before connect() returns. + // So we need to update the status before connect() is called, + // otherwise the status may be overwritten with wrong value (CONNECTED vs. CONNECTING). + s.m_Status = UDTSTATUS.CONNECTING; + try + { + s.m_pUDT.connect(name); + } + catch (UdtException e) + { + s.m_Status = UDTSTATUS.OPENED; + throw e; + } + + // record peer address + s.m_pPeerAddr = name; + + return 0; + } + } + + public void connect_complete(UDTSOCKET u) + { + UdtSocketInternal s = locate(u); + if (null == s) + throw new UdtException(5, 4, 0); + + // copy address information of local node + // the local port must be correctly assigned BEFORE CUDT.connect(), + // otherwise if connect() fails, the multiplexer cannot be located by garbage collection and will cause leak + s.m_pUDT.m_pSndQueue.m_pChannel.getSockAddr(ref s.m_pSelfAddr); + ConvertIPAddress.ToUintArray(s.m_pSelfAddr.Address, ref s.m_pUDT.m_piSelfIP); + + s.m_Status = UDTSTATUS.CONNECTED; + } + + public int close(UDTSOCKET u) + { + UdtSocketInternal s = locate(u); + if (null == s) + throw new UdtException(5, 4, 0); + + lock (s.m_ControlLock) + { + + if (s.m_Status == UDTSTATUS.LISTENING) + { + if (s.m_pUDT.m_bBroken) + return 0; + + s.m_TimeStamp = Timer.getTime(); + s.m_pUDT.m_bBroken = true; + + // broadcast all "accept" waiting + s.m_AcceptCond.Set(); + + return 0; + } + + s.m_pUDT.close(); + + // synchronize with garbage collection. + lock (m_ControlLock) + { + + // since "s" is located before m_ControlLock, locate it again in case it became invalid + if (!m_Sockets.TryGetValue(u, out s) || s.m_Status == UDTSTATUS.CLOSED) + { + return 0; + } + + s.m_Status = UDTSTATUS.CLOSED; + + // a socket will not be immediated removed when it is closed + // in order to prevent other methods from accessing invalid address + // a timer is started and the socket will be removed after approximately 1 second + s.m_TimeStamp = Timer.getTime(); + + m_Sockets.Remove(s.m_SocketID); + m_ClosedSockets.Add(s.m_SocketID, s); + + Timer.triggerEvent(); + + return 0; + } + } + } + + // int CUDTUnited.getpeername(const UDTSOCKET u, sockaddr*name, int* namelen) + //{ + // if (CONNECTED != getStatus(u)) + // throw new UdtException(2, 2, 0); + + // UdtSocket* s = locate(u); + + // if (null == s) + // throw new UdtException(5, 4, 0); + + // if (!s.m_pUDT.m_bConnected || s.m_pUDT.m_bBroken) + // throw new UdtException(2, 2, 0); + + // if (AF_INET == s.m_iIPversion) + // *namelen = sizeof(sockaddr_in); + // else + // *namelen = sizeof(sockaddr_in6); + + // // copy address information of peer node + // memcpy(name, s.m_pPeerAddr, *namelen); + + // return 0; + // } + + // int CUDTUnited.getsockname(const UDTSOCKET u, sockaddr*name, int* namelen) + //{ + // UdtSocket* s = locate(u); + + // if (null == s) + // throw new UdtException(5, 4, 0); + + // if (s.m_pUDT.m_bBroken) + // throw new UdtException(5, 4, 0); + + // if (INIT == s.m_Status) + // throw new UdtException(2, 2, 0); + + // if (AF_INET == s.m_iIPversion) + // *namelen = sizeof(sockaddr_in); + // else + // *namelen = sizeof(sockaddr_in6); + + // // copy address information of local node + // memcpy(name, s.m_pSelfAddr, *namelen); + + // return 0; + // } + + + + + internal UdtSocketInternal locate(UDTSOCKET u) + { + lock (m_ControlLock) + { + UdtSocketInternal s; + if (!m_Sockets.TryGetValue(u, out s) || s.m_Status == UDTSTATUS.CLOSED) + { + return null; + } + + return s; + } + } + + UdtSocketInternal locate(IPEndPoint peer, UDTSOCKET id, int isn) + { + lock (m_ControlLock) + { + HashSet sockets; + if (!m_PeerRec.TryGetValue((id << 30) + isn, out sockets)) + return null; + + foreach (int iSocket in sockets) + { + UdtSocketInternal socket; + if (!m_Sockets.TryGetValue(iSocket, out socket)) + continue; + + if (socket.m_pPeerAddr.Equals(peer)) + return socket; + } + + return null; + } + } + + public void checkBrokenSockets() + { + lock (m_ControlLock) + { + checkBrokenSockets_unsafe(); + } + } + + void checkBrokenSockets_unsafe() + { + // set of sockets To Be Closed and To Be Removed + List tbc = new List(); + List tbr = new List(); + + foreach (KeyValuePair item in m_Sockets) + { + // check broken connection + if (item.Value.m_pUDT.m_bBroken) + { + if (item.Value.m_Status == UDTSTATUS.LISTENING) + { + // for a listening socket, it should wait an extra 3 seconds in case a client is connecting + if (Timer.getTime() - item.Value.m_TimeStamp < 3000000) + continue; + } + else if ((item.Value.m_pUDT.m_pRcvBuffer != null) && (item.Value.m_pUDT.m_pRcvBuffer.getRcvDataSize() > 0) && (item.Value.m_pUDT.m_iBrokenCounter-- > 0)) + { + // if there is still data in the receiver buffer, wait longer + continue; + } + + //close broken connections and start removal timer + item.Value.m_Status = UDTSTATUS.CLOSED; + item.Value.m_TimeStamp = Timer.getTime(); + tbc.Add(item.Key); + m_ClosedSockets[item.Key] = item.Value; + + // remove from listener's queue + UdtSocketInternal listenSocket; + if (!m_Sockets.TryGetValue(item.Value.m_ListenSocket, out listenSocket)) + { + if (!m_ClosedSockets.TryGetValue(item.Value.m_ListenSocket, out listenSocket)) + { + continue; + } + } + + Monitor.Enter(listenSocket.m_AcceptLock); + listenSocket.m_pQueuedSockets.Remove(item.Value.m_SocketID); + listenSocket.m_pAcceptSockets.Remove(item.Value.m_SocketID); + Monitor.Exit(listenSocket.m_AcceptLock); + } + } + + foreach (KeyValuePair j in m_ClosedSockets) + { + if (j.Value.m_pUDT.m_ullLingerExpiration > 0) + { + // asynchronous close: + if ((null == j.Value.m_pUDT.m_pSndBuffer) || (0 == j.Value.m_pUDT.m_pSndBuffer.getCurrBufSize()) || (j.Value.m_pUDT.m_ullLingerExpiration <= Timer.getTime())) + { + j.Value.m_pUDT.m_ullLingerExpiration = 0; + j.Value.m_pUDT.m_bClosing = true; + j.Value.m_TimeStamp = Timer.getTime(); + } + } + + // timeout 1 second to destroy a socket AND it has been removed from RcvUList + if ((Timer.getTime() - j.Value.m_TimeStamp > 1000000) && ((null == j.Value.m_pUDT.m_pRNode) || !j.Value.m_pUDT.m_pRNode.m_bOnList)) + { + tbr.Add(j.Key); + } + } + + // move closed sockets to the ClosedSockets structure + foreach (UDTSOCKET k in tbc) + m_Sockets.Remove(k); + + // remove those timeout sockets + foreach (UDTSOCKET l in tbr) + removeSocket(l); + } + + void removeSocket(UDTSOCKET u) + { + UdtSocketInternal closedSocket; + if (!m_ClosedSockets.TryGetValue(u, out closedSocket)) + return; + + // decrease multiplexer reference count, and remove it if necessary + int mid = closedSocket.m_iMuxID; + + if (null != closedSocket.m_pQueuedSockets) + { + Monitor.Enter(closedSocket.m_AcceptLock); + + // if it is a listener, close all un-accepted sockets in its queue and remove them later + foreach (UDTSOCKET q in closedSocket.m_pQueuedSockets) + { + m_Sockets[q].m_pUDT.m_bBroken = true; + m_Sockets[q].m_pUDT.close(); + m_Sockets[q].m_TimeStamp = Timer.getTime(); + m_Sockets[q].m_Status = UDTSTATUS.CLOSED; + m_ClosedSockets[q] = m_Sockets[q]; + m_Sockets.Remove(q); + } + + Monitor.Exit(closedSocket.m_AcceptLock); + } + + // remove from peer rec + HashSet sockets; + if (m_PeerRec.TryGetValue((closedSocket.m_PeerID << 30) + closedSocket.m_iISN, out sockets)) + { + sockets.Remove(u); + if (sockets.Count == 0) + m_PeerRec.Remove(closedSocket.m_PeerID); + } + + // delete this one + closedSocket.m_pUDT.close(); + closedSocket.Close(); + m_ClosedSockets.Remove(u); + + Multiplexer m; + if (!m_mMultiplexer.TryGetValue(mid, out m)) + { + //something is wrong!!! + return; + } + + m.m_iRefCount--; + if (0 == m.m_iRefCount) + { + m.m_pChannel.close(); + m.m_pSndQueue.Close(); + m.m_pRcvQueue.Close(); + m.m_pTimer.Stop(); + + m_mMultiplexer.Remove(mid); + } + } + + // void setError(UdtException e) + //{ + // CGuard tg(m_TLSLock); + // delete(UdtException *)TlsGetValue(m_TLSError); + // TlsSetValue(m_TLSError, e); + // m_mTLSRecord[GetCurrentThreadId()] = e; + // } + + // UdtException getError() + //{ + // CGuard tg(m_TLSLock); + // if (null == TlsGetValue(m_TLSError)) + // { + // UdtException* e = new UdtException; + // TlsSetValue(m_TLSError, e); + // m_mTLSRecord[GetCurrentThreadId()] = e; + // } + // return (UdtException*)TlsGetValue(m_TLSError); + // } + + // void checkTLSValue() + //{ + // CGuard tg(m_TLSLock); + + // vector tbr; + // for (map.iterator i = m_mTLSRecord.begin(); i != m_mTLSRecord.end(); ++i) + // { + // HANDLE h = OpenThread(THREAD_QUERY_INFORMATION, FALSE, i.first); + // if (null == h) + // { + // tbr.push_back(i.first); + // break; + // } + // if (WAIT_OBJECT_0 == WaitForSingleObject(h, 0)) + // { + // delete i.second; + // tbr.push_back(i.first); + // } + // CloseHandle(h); + // } + // for (vector.iterator j = tbr.begin(); j != tbr.end(); ++j) + // m_mTLSRecord.erase(*j); + // } + + void updateMux(UdtSocketInternal s, IPEndPoint addr = null, Socket udpsock = null) + { + lock (m_ControlLock) + { + Multiplexer m; + if ((s.m_pUDT.m_bReuseAddr) && (null != addr)) + { + int port = addr.Port; + + // find a reusable address + foreach (KeyValuePair item in m_mMultiplexer) + { + // reuse the existing multiplexer + m = item.Value; + if ((m.m_iIPversion == s.m_pUDT.m_iIPversion) && (m.m_iMSS == s.m_pUDT.m_iMSS) && m.m_bReusable) + { + if (m.m_iPort == port) + { + // reuse the existing multiplexer + ++m.m_iRefCount; + s.m_pUDT.m_pSndQueue = m.m_pSndQueue; + s.m_pUDT.m_pRcvQueue = m.m_pRcvQueue; + s.m_iMuxID = m.m_iID; + return; + } + } + } + } + + // a new multiplexer is needed + m = new Multiplexer(); + m.m_iMSS = s.m_pUDT.m_iMSS; + m.m_iIPversion = s.m_pUDT.m_iIPversion; + m.m_iRefCount = 1; + m.m_bReusable = s.m_pUDT.m_bReuseAddr; + m.m_iID = s.m_SocketID; + + m.m_pChannel = new Channel(s.m_pUDT.m_iIPversion); + m.m_pChannel.setSndBufSize(s.m_pUDT.m_iUDPSndBufSize); + m.m_pChannel.setRcvBufSize(s.m_pUDT.m_iUDPRcvBufSize); + + try + { + if (null != udpsock) + m.m_pChannel.open(udpsock); + else + m.m_pChannel.open(addr); + } + catch (UdtException e) + { + m.m_pChannel.close(); + throw e; + } + + IPEndPoint sa = new IPEndPoint(IPAddress.Any, 0); + m.m_pChannel.getSockAddr(ref sa); + m.m_iPort = sa.Port; + + m.m_pTimer = new Timer(); + + m.m_pSndQueue = new SndQueue(); + m.m_pSndQueue.init(m.m_pChannel, m.m_pTimer); + m.m_pRcvQueue = new RcvQueue(); + m.m_pRcvQueue.init(32, s.m_pUDT.m_iPayloadSize, m.m_iIPversion, 1024, m.m_pChannel, m.m_pTimer); + + m_mMultiplexer[m.m_iID] = m; + + s.m_pUDT.m_pSndQueue = m.m_pSndQueue; + s.m_pUDT.m_pRcvQueue = m.m_pRcvQueue; + s.m_iMuxID = m.m_iID; + } + } + + void updateMux(UdtSocketInternal s, UdtSocketInternal ls) + { + lock (m_ControlLock) + { + + int port = ls.m_pSelfAddr.Port; + + // find the listener's address + foreach (KeyValuePair item in m_mMultiplexer) + { + if (item.Value.m_iPort == port) + { + // reuse the existing multiplexer + Multiplexer multiplexer = item.Value; + ++multiplexer.m_iRefCount; + s.m_pUDT.m_pSndQueue = multiplexer.m_pSndQueue; + s.m_pUDT.m_pRcvQueue = multiplexer.m_pRcvQueue; + s.m_iMuxID = multiplexer.m_iID; + return; + } + } + } + } + } + + +} // namespace UdtSharp diff --git a/framework/Inspectron.HawkEye/UDT/Utilities.cs b/framework/Inspectron.HawkEye/UDT/Utilities.cs new file mode 100644 index 0000000..af0abf2 --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/Utilities.cs @@ -0,0 +1,51 @@ +using System.Net; +using System.Net.Sockets; + +namespace UdtSharp +{ + public static class ConvertIPAddress + { + // TODO use BitConverter/Block.Copy in this class + + public static void ToUintArray(IPAddress ipAddress, ref uint[] outAddress) + { + byte[] bytes = ipAddress.GetAddressBytes(); + if (ipAddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + { + // TODO addressBytes length must be 4 in this case + outAddress[0] = (uint)((bytes[3] << 24) + (bytes[2] << 16) + (bytes[1] << 8) + bytes[0]); + return; + } + + // TODO addresFamily must by InterNetworkV6 + // addressBytesLenth must be 16 + outAddress[3] = (uint)((bytes[15] << 24) + (bytes[14] << 16) + (bytes[13] << 8) + bytes[12]); + outAddress[2] = (uint)((bytes[11] << 24) + (bytes[10] << 16) + (bytes[9] << 8) + bytes[8]); + outAddress[1] = (uint)((bytes[7] << 24) + (bytes[6] << 16) + (bytes[5] << 8) + bytes[4]); + outAddress[0] = (uint)((bytes[3] << 24) + (bytes[2] << 16) + (bytes[1] << 8) + bytes[0]); + } + } + + public static class ConvertLingerOption + { + public unsafe static LingerOption FromVoidPointer(void* option) + { + bool* pEnabled = (bool*)option; + bool bEnabled = *pEnabled; + + int* pTime = (int*)(++pEnabled); + int timeSeconds = *pTime; + + return new LingerOption(bEnabled, timeSeconds); + } + + public unsafe static void ToVoidPointer(LingerOption lingerOption, void* option) + { + bool* pEnabled = (bool*)option; + *pEnabled = lingerOption.Enabled; + + int* pTime = (int*)(++pEnabled); + *pTime = lingerOption.LingerTime; + } + } +} diff --git a/framework/Inspectron.HawkEye/UDT/Window.cs b/framework/Inspectron.HawkEye/UDT/Window.cs new file mode 100644 index 0000000..01bbbec --- /dev/null +++ b/framework/Inspectron.HawkEye/UDT/Window.cs @@ -0,0 +1,309 @@ +using System; + +namespace UdtSharp +{ + public class ACKWindow + { + public ACKWindow(int size = 1024) + { + m_iSize = size; + m_piACKSeqNo = new int[m_iSize]; + m_piACK = new int[m_iSize]; + m_pTimeStamp = new ulong[m_iSize]; + + m_piACKSeqNo[0] = -1; + } + + // Functionality: + // Write an ACK record into the window. + // Parameters: + // 0) [in] seq: ACK seq. no. + // 1) [in] ack: DATA ACK no. + // Returned value: + // None. + + public void store(int seq, int ack) + { + m_piACKSeqNo[m_iHead] = seq; + m_piACK[m_iHead] = ack; + m_pTimeStamp[m_iHead] = Timer.getTime(); + + m_iHead = (m_iHead + 1) % m_iSize; + + // overwrite the oldest ACK since it is not likely to be acknowledged + if (m_iHead == m_iTail) + m_iTail = (m_iTail + 1) % m_iSize; + } + + // Functionality: + // Search the ACK-2 "seq" in the window, find out the DATA "ack" and caluclate RTT . + // Parameters: + // 0) [in] seq: ACK-2 seq. no. + // 1) [out] ack: the DATA ACK no. that matches the ACK-2 no. + // Returned value: + // RTT. + + public int acknowledge(int seq, ref int ack) + { + if (m_iHead >= m_iTail) + { + // Head has not exceeded the physical boundary of the window + + for (int i = m_iTail, n = m_iHead; i < n; ++i) + { + // looking for indentical ACK Seq. No. + if (seq == m_piACKSeqNo[i]) + { + // return the Data ACK it carried + ack = m_piACK[i]; + + // calculate RTT + int rtt = (int)(Timer.getTime() - m_pTimeStamp[i]); + + if (i + 1 == m_iHead) + { + m_iTail = m_iHead = 0; + m_piACKSeqNo[0] = -1; + } + else + m_iTail = (i + 1) % m_iSize; + + return rtt; + } + } + + // Bad input, the ACK node has been overwritten + return -1; + } + + // Head has exceeded the physical window boundary, so it is behind tail + for (int j = m_iTail, n = m_iHead + m_iSize; j < n; ++j) + { + // looking for indentical ACK seq. no. + if (seq == m_piACKSeqNo[j % m_iSize]) + { + // return Data ACK + j %= m_iSize; + ack = m_piACK[j]; + + // calculate RTT + int rtt = (int)(Timer.getTime() - m_pTimeStamp[j]); + + if (j == m_iHead) + { + m_iTail = m_iHead = 0; + m_piACKSeqNo[0] = -1; + } + else + m_iTail = (j + 1) % m_iSize; + + return rtt; + } + } + + // bad input, the ACK node has been overwritten + return -1; + } + + int[] m_piACKSeqNo; // Seq. No. for the ACK packet + int[] m_piACK; // Data Seq. No. carried by the ACK packet + ulong[] m_pTimeStamp; // The timestamp when the ACK was sent + + int m_iSize; // Size of the ACK history window + int m_iHead; // Pointer to the lastest ACK record + int m_iTail; // Pointer to the oldest ACK record + } + + //////////////////////////////////////////////////////////////////////////////// + + public class PktTimeWindow + { + public PktTimeWindow(int asize = 16, int psize = 16) + { + m_iAWSize = asize; + m_iPWSize = psize; + m_iMinPktSndInt = 1000000; + m_piPktWindow = new int[m_iAWSize]; + m_piPktReplica = new int[m_iAWSize]; + m_piProbeWindow = new int[m_iPWSize]; + m_piProbeReplica = new int[m_iPWSize]; + + m_LastArrTime = Timer.getTime(); + + for (int i = 0; i < m_iAWSize; ++i) + m_piPktWindow[i] = 1000000; + + for (int k = 0; k < m_iPWSize; ++k) + m_piProbeWindow[k] = 1000; + } + + // Functionality: + // read the minimum packet sending interval. + // Parameters: + // None. + // Returned value: + // minimum packet sending interval (microseconds). + + public int getMinPktSndInt() + { + return m_iMinPktSndInt; + } + + // Functionality: + // Calculate the packes arrival speed. + // Parameters: + // None. + // Returned value: + // Packet arrival speed (packets per second). + + public int getPktRcvSpeed() + { + // get median value, but cannot change the original value order in the window + Array.Copy(m_piPktWindow, m_piPktReplica, m_iAWSize - 1); // why -1 ??? + Array.Sort(m_piPktReplica); // need -1 here ??? + int median = m_piPktReplica[m_iAWSize / 2]; + + int count = 0; + int sumMicrosecond = 0; + int upper = median << 3; + int lower = median >> 3; + + // median filtering + for (int i = 0, n = m_iAWSize; i < n; ++i) + { + if ((m_piPktWindow[i] < upper) && (m_piPktWindow[i] > lower)) + { + ++count; + sumMicrosecond += m_piPktWindow[i]; + } + } + double packetsPerMicrosecond = (double)count / sumMicrosecond; + + // claculate speed, or return 0 if not enough valid value + if (count > (m_iAWSize >> 1)) + return (int)Math.Ceiling(1000000 * packetsPerMicrosecond); + else + return 0; + } + + // Functionality: + // Estimate the bandwidth. + // Parameters: + // None. + // Returned value: + // Estimated bandwidth (packets per second). + + public int getBandwidth() + { + // get median value, but cannot change the original value order in the window + Array.Copy(m_piProbeWindow, m_piProbeReplica, m_iPWSize - 1); // why -1 ??? + Array.Sort(m_piProbeReplica); // need -1 here ??? + int median = m_piProbeReplica[m_iPWSize / 2]; + + int count = 1; + int sum = median; + int upper = median << 3; + int lower = median >> 3; + + // median filtering + for (int i = 0, n = m_iPWSize; i < n; ++i) + { + if ((m_piProbeWindow[i] < upper) && (m_piProbeWindow[i] > lower)) + { + ++count; + sum += m_piProbeWindow[i]; + } + } + + return (int)Math.Ceiling(1000000.0 / ((double)sum / (double)count)); + } + + // Functionality: + // Record time information of a packet sending. + // Parameters: + // 0) currtime: timestamp of the packet sending. + // Returned value: + // None. + + public void onPktSent(int currtime) + { + int interval = currtime - m_iLastSentTime; + + if ((interval < m_iMinPktSndInt) && (interval > 0)) + m_iMinPktSndInt = interval; + + m_iLastSentTime = currtime; + } + + // Functionality: + // Record time information of an arrived packet. + // Parameters: + // None. + // Returned value: + // None. + + public void onPktArrival() + { + m_CurrArrTime = Timer.getTime(); + + // record the packet interval between the current and the last one + m_piPktWindow[m_iPktWindowPtr] = (int)(m_CurrArrTime - m_LastArrTime); + + // the window is logically circular + ++m_iPktWindowPtr; + if (m_iPktWindowPtr == m_iAWSize) + m_iPktWindowPtr = 0; + + // remember last packet arrival time + m_LastArrTime = m_CurrArrTime; + } + + // Functionality: + // Record the arrival time of the first probing packet. + // Parameters: + // None. + // Returned value: + // None. + + public void probe1Arrival() + { + m_ProbeTime = Timer.getTime(); + } + + // Functionality: + // Record the arrival time of the second probing packet and the interval between packet pairs. + // Parameters: + // None. + // Returned value: + // None. + + public void probe2Arrival() + { + m_CurrArrTime = Timer.getTime(); + + // record the probing packets interval + m_piProbeWindow[m_iProbeWindowPtr] = (int)(m_CurrArrTime - m_ProbeTime); + // the window is logically circular + ++m_iProbeWindowPtr; + if (m_iProbeWindowPtr == m_iPWSize) + m_iProbeWindowPtr = 0; + } + + int m_iAWSize; // size of the packet arrival history window + int[] m_piPktWindow; // packet information window + int[] m_piPktReplica; + int m_iPktWindowPtr; // position pointer of the packet info. window. + + int m_iPWSize; // size of probe history window size + int[] m_piProbeWindow; // record inter-packet time for probing packet pairs + int[] m_piProbeReplica; + int m_iProbeWindowPtr; // position pointer to the probing window + + int m_iLastSentTime; // last packet sending time + int m_iMinPktSndInt; // Minimum packet sending interval + + ulong m_LastArrTime; // last packet arrival time + ulong m_CurrArrTime; // current packet arrival time + ulong m_ProbeTime; // arrival time of the first probing packet + } +} diff --git a/framework/Inspectron.HawkEye/UDTClient.cs b/framework/Inspectron.HawkEye/UDTClient.cs new file mode 100644 index 0000000..c54e16d --- /dev/null +++ b/framework/Inspectron.HawkEye/UDTClient.cs @@ -0,0 +1,58 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using Inspectron.HawkEye.Protocol; +using UdtSharp; + +namespace Inspectron.HawkEye +{ + public class UDTClient + { + UdtSharp.UdtSocket _client; + + public UDTClient():this(IPAddress.Loopback, 27001) + { + + } + public UDTClient(IPAddress address,int port) + { + + _client = new UdtSharp.UdtSocket(AddressFamily.InterNetwork, SocketType.Stream); + + _client.Connect(new IPEndPoint(address, port)); + } + + public byte[] RequestImage() + { + var request = CreateTriggerRequest(); + _client.Send(request, 0, request.Length); + + byte[] buffer = new byte[2048 * 4 * 1000]; + + int bytes = 0; + while (bytes