From 71ff74122eb6a47687f6dea3f7fa850fab39351d Mon Sep 17 00:00:00 2001 From: Michal Buzik Date: Wed, 15 Jul 2026 16:12:21 +0200 Subject: [PATCH] camera send implemenatation, dialog support, ver 3.9.3134 --- TBF/Properties/AssemblyInfo.cs | 4 +- .../TestStartEndForm.cs | 685 ++++++++++++- TBF/Rig/Network/Camera/CJMS11/Camera.cs | 795 +++++++++++++-- TBF/Rig/Network/Camera/CJMS11/GrabImagesOp.cs | 934 ++++++++++++++++-- .../Camera/CJMS11/POJO/SendImagePayload.cs | 37 + .../Camera/CJMS11/POJO/SendImagesRequest.cs | 16 + .../Camera/CJMS11/POJO/SendSummaryPayload.cs | 16 + .../Camera/CJMS11/POJO/TimedCameraImage.cs | 20 + .../RoiForFixedStartCJMS11/ProcedureParams.cs | 26 +- .../Camera/RoiForFixedStartCJMS11/Roi.cs | 76 +- TBF/Rig/Network/Telnet/EventArgsClasses.cs | 23 +- TBF/TBF.csproj | 4 + 12 files changed, 2401 insertions(+), 235 deletions(-) create mode 100644 TBF/Rig/Network/Camera/CJMS11/POJO/SendImagePayload.cs create mode 100644 TBF/Rig/Network/Camera/CJMS11/POJO/SendImagesRequest.cs create mode 100644 TBF/Rig/Network/Camera/CJMS11/POJO/SendSummaryPayload.cs create mode 100644 TBF/Rig/Network/Camera/CJMS11/POJO/TimedCameraImage.cs diff --git a/TBF/Properties/AssemblyInfo.cs b/TBF/Properties/AssemblyInfo.cs index 785599942..1b9e1281e 100644 --- a/TBF/Properties/AssemblyInfo.cs +++ b/TBF/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // Build Number // Revision // -[assembly: AssemblyVersion("3.9.3133.0")] -[assembly: AssemblyFileVersion("3.9.3133.0")] +[assembly: AssemblyVersion("3.9.3134.0")] +[assembly: AssemblyFileVersion("3.9.3134.0")] diff --git a/TBF/Rig/DataEntry/StandartCameraPurchaseOrder/TestStartEndForm.cs b/TBF/Rig/DataEntry/StandartCameraPurchaseOrder/TestStartEndForm.cs index 8d222e048..d94f7471f 100644 --- a/TBF/Rig/DataEntry/StandartCameraPurchaseOrder/TestStartEndForm.cs +++ b/TBF/Rig/DataEntry/StandartCameraPurchaseOrder/TestStartEndForm.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Windows.Forms; using log4net; @@ -70,6 +71,9 @@ namespace TBF.Rig.DataEntry.StandartCameraPurchaseOrder int lastWmNr1; Bitmap lastImage; /// non-zoomed image Bitmap zoomedImage; + private readonly List currentImageGroup = new List();//multiple pictures + private int currentImageGroupIndex; + private bool mouseOverPictureBox; Thread imageProcessingThread; /// Thread to process images public delegate void OcrTextObtainedDelegate(int ix, bool isEnd, string text); @@ -96,6 +100,12 @@ namespace TBF.Rig.DataEntry.StandartCameraPurchaseOrder public TestStartEndForm() { InitializeComponent(); + + KeyPreview = true; + + pictureBox1.MouseWheel += pictureBox1_MouseWheel; + pictureBox1.MouseEnter += pictureBox1_MouseEnter; + pictureBox1.MouseLeave += pictureBox1_MouseLeave; this.Icon = Properties.Resources.TBF_icon; @@ -483,15 +493,46 @@ namespace TBF.Rig.DataEntry.StandartCameraPurchaseOrder startTextBoxes[i].TextChanged += new System.EventHandler(this.startTextBoxX_TextChanged); startTextBoxes[i].KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.startTextBoxX_KeyPress); startTextBoxes[i].MouseDown += new System.Windows.Forms.MouseEventHandler(this.startTextBoxX_MouseDown); + startTextBoxes[i].Enter += startTextBoxX_Enter; } + private void startTextBoxX_Enter(object sender, EventArgs e) + { + var textBox = sender as TextBox; + if (textBox == null) { return; } + + int physPos = PhysPos(textBox, startTextBoxes); + + if (physPos >= 0) + { + LoadImage(sender, Tst.Start, physPos); + } + } + + + private void EndTextBox_EnableHanders(int i) { endTextBoxes[i].AcceptsTabChanged += new System.EventHandler(this.endTextBoxX_AcceptsTabChanged); endTextBoxes[i].TextChanged += new System.EventHandler(this.endTextBoxX_TextChanged); endTextBoxes[i].KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.endTextBoxX_KeyPress); endTextBoxes[i].MouseDown += new System.Windows.Forms.MouseEventHandler(this.endTextBoxX_AcceptsTabChanged); + endTextBoxes[i].Enter += endTextBoxX_Enter; } + + private void endTextBoxX_Enter(object sender, EventArgs e) + { + var textBox = sender as TextBox; + if (textBox == null) { return; } + + int physPos = PhysPos(textBox, endTextBoxes); + + if (physPos >= 0) + { + LoadImage(sender, Tst.End, physPos); + } + } + private void TestStartEndForm_FormClosed(object sender, FormClosedEventArgs e) { @@ -763,7 +804,7 @@ namespace TBF.Rig.DataEntry.StandartCameraPurchaseOrder TextBox clickedTextBox = sender as TextBox; if (clickedTextBox != null) { - var physPos = PhysPos(clickedTextBox, startTextBoxes); + var physPos = PhysPos(clickedTextBox, endTextBoxes);//startTextBoxes); if (physPos < 0) { @@ -1069,7 +1110,7 @@ namespace TBF.Rig.DataEntry.StandartCameraPurchaseOrder TextBox clickedTextBox = sender as TextBox; if (clickedTextBox != null) { - var physPos = PhysPos(clickedTextBox, startTextBoxes); + var physPos = PhysPos(clickedTextBox, endTextBoxes);//startTextBoxes); if (physPos < 0) { @@ -1134,65 +1175,479 @@ namespace TBF.Rig.DataEntry.StandartCameraPurchaseOrder /// /// Tst.Start or Tst.End /// 0..23 - void LoadImage(object sender, Tst startOrEnd, int physPos) - { - int wmNr1b = phys2wm[physPos]; /// 0 means no WM - if (wmNr1b <= 0) return; + void LoadImage( object sender, Tst startOrEnd, int physPos) + { + if (physPos < 0 || physPos >= phys2wm.Length) + { + return; + } - wmNr0b = wmNr1b - 1; + int wmNr1b = phys2wm[physPos]; - string imageFileName; - if (startOrEnd == Tst.Start && startImages[wmNr0b] != null) - imageFileName = startImages[wmNr0b]; - else if (startOrEnd == Tst.End && endImages[wmNr0b] != null) - imageFileName = endImages[wmNr0b]; - else - imageFileName = string.Empty; + // Hodnota 0 znamená, že na tejto fyzickej pozícii nie je vodomer. + if (wmNr1b <= 0) + { + return; + } - if (imageFileName == lastImageName) - { - return; - } + wmNr0b = wmNr1b - 1; - lastImageName = imageFileName; - lastWmNr1 = wmNr1b; + string imageFileName = GetBaseImageFileName( + startOrEnd, + wmNr0b); - if (string.IsNullOrEmpty(imageFileName) || !File.Exists(imageFileName)) - { - lastImage = null; - } - else - { - using (Bitmap tempBmp = Image.FromFile(imageFileName) as Bitmap) + lastWmNr1 = wmNr1b; + + if (string.IsNullOrWhiteSpace(imageFileName)) + { + ClearImageGroup(); + LoadImageFile(null); + return; + } + + /* + * Ak používateľ zvolil iný vodomer alebo Start/End obrázok, + * vytvoríme novú skupinu: + * + * TestAdvance-1-start.bmp + * TestAdvance-1-start_img01_....bmp + * TestAdvance-1-start_img02_....bmp + */ + if (!string.Equals( + imageFileName, + lastImageName, + StringComparison.OrdinalIgnoreCase)) + { + lastImageName = imageFileName; + + LoadImageGroup(imageFileName); + + // Pôvodný kompatibilný obrázok bude zobrazený ako prvý. + currentImageGroupIndex = 0; + } + else + { + /* + * Súbory mohli vzniknúť až po prvom otvorení dialógu, + * preto skupinu pri opätovnom výbere obnovíme. + */ + RefreshCurrentImageGroup(); + } + + if (currentImageGroup.Count == 0) + { + LoadImageFile(null); + return; + } + + currentImageGroupIndex = Math.Max( + 0, + Math.Min( + currentImageGroupIndex, + currentImageGroup.Count - 1)); + + LoadImageFile( + currentImageGroup[currentImageGroupIndex]); + } + + /// + /// base name of image file + /// + /// + /// + /// + private string GetBaseImageFileName( + Tst startOrEnd, + int waterMeterIndex) + { + if (waterMeterIndex < 0 || + waterMeterIndex >= WaterMetersCount) + { + return null; + } + + if (startOrEnd == Tst.Start) + { + if (startImages == null || + waterMeterIndex >= startImages.Length) { - Rectangle rect; - if (tempBmp.Height < tempBmp.Width) + return null; + } + + return startImages[waterMeterIndex]; + } + + if (startOrEnd == Tst.End) + { + if (endImages == null || + waterMeterIndex >= endImages.Length) + { + return null; + } + + return endImages[waterMeterIndex]; + } + + return null; + } + + /// + /// load group of images + /// + /// + private void LoadImageGroup( + string baseImageFileName) + { + currentImageGroup.Clear(); + currentImageGroupIndex = 0; + + if (string.IsNullOrWhiteSpace(baseImageFileName)) + { + return; + } + + string fullBaseFileName; + + try + { + fullBaseFileName = + Path.GetFullPath(baseImageFileName); + } + catch (Exception exc) + { + log.Warn( + $"Cannot create image group from invalid path: " + + $"file='{baseImageFileName}', " + + $"error={exc.Message}"); + + return; + } + + string directory = + Path.GetDirectoryName(fullBaseFileName); + + string baseName = + Path.GetFileNameWithoutExtension(fullBaseFileName); + + string extension = + Path.GetExtension(fullBaseFileName); + + if (string.IsNullOrWhiteSpace(directory) || + string.IsNullOrWhiteSpace(baseName)) + { + return; + } + + /* + * Pôvodný obrázok pridáme ako prvý. + */ + if (File.Exists(fullBaseFileName)) + { + currentImageGroup.Add(fullBaseFileName); + } + + if (!Directory.Exists(directory)) + { + return; + } + + string searchPattern = + $"{baseName}_img*{extension}"; + + try + { + List timedImages = + Directory + .GetFiles( + directory, + searchPattern, + SearchOption.TopDirectoryOnly) + .OrderBy(GetTimedImageIndex) + .ThenBy( + fileName => fileName, + StringComparer.OrdinalIgnoreCase) + .ToList(); + + foreach (string timedImage in timedImages) + { + if (!currentImageGroup.Contains( + timedImage, + StringComparer.OrdinalIgnoreCase)) { - int hght = tempBmp.Height; - int leftMargin = (tempBmp.Width - hght) / 2; - rect = new Rectangle(leftMargin, 0, hght, hght); - } - else - { - int hght = tempBmp.Height; - int wdth = tempBmp.Width; - int margin = (hght - wdth) / 2; - rect = new Rectangle(0, margin, wdth, wdth); - } - //int hght = tempBmp.Height; - //int leftMargin = (tempBmp.Width - hght) / 2; - //lastImage = new Bitmap(tempBmp.Clone(new Rectangle(leftMargin, 0, hght, hght), tempBmp.PixelFormat)); - lastImage = new Bitmap(tempBmp.Clone(rect, tempBmp.PixelFormat)); - if (isZoomed[wmNr0b]) - { - zoomedImage = MakeZoomedImage(lastImage, zoomCenterLoc[wmNr0b]); + currentImageGroup.Add(timedImage); } } - } - ShowImage(); - } + log.Debug( + $"Image group loaded: " + + $"baseFile='{fullBaseFileName}', " + + $"searchPattern='{searchPattern}', " + + $"imagesCount={currentImageGroup.Count}"); + } + catch (Exception exc) + { + log.Warn( + $"Cannot load image group: " + + $"baseFile='{fullBaseFileName}', " + + $"pattern='{searchPattern}', " + + $"error={exc.Message}"); + } + } + /// + /// grouping by time + /// + /// + /// + private static int GetTimedImageIndex( + string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + return int.MaxValue; + } + string nameWithoutExtension = + Path.GetFileNameWithoutExtension(fileName); + + Match match = Regex.Match( + nameWithoutExtension, + @"_img(?\d+)_", + RegexOptions.IgnoreCase); + + if (!match.Success) + { + return int.MaxValue; + } + + int imageIndex; + + return int.TryParse( + match.Groups["index"].Value, + out imageIndex) + ? imageIndex + : int.MaxValue; + } + + /// + /// refreshing current image group + /// + private void RefreshCurrentImageGroup() + { + if (string.IsNullOrWhiteSpace(lastImageName)) + { + return; + } + + string currentlySelectedFile = + currentImageGroupIndex >= 0 && + currentImageGroupIndex < currentImageGroup.Count + ? currentImageGroup[currentImageGroupIndex] + : null; + + LoadImageGroup(lastImageName); + + if (string.IsNullOrWhiteSpace(currentlySelectedFile)) + { + return; + } + + int restoredIndex = + currentImageGroup.FindIndex( + fileName => + string.Equals( + fileName, + currentlySelectedFile, + StringComparison.OrdinalIgnoreCase)); + + if (restoredIndex >= 0) + { + currentImageGroupIndex = restoredIndex; + } + } + /// --- picture load --- + private void LoadImageFile( + string imageFileName) + { + DisposeLoadedImages(); + + if (string.IsNullOrWhiteSpace(imageFileName) || + !File.Exists(imageFileName)) + { + log.Debug( + $"Image cannot be displayed because the file does not exist: " + + $"file='{imageFileName}'"); + + ShowImage(); + return; + } + + try + { + using (var tempBmp = + new Bitmap(imageFileName)) + { + Rectangle rect = + CreateSquareCropRectangle(tempBmp); + + using (Bitmap croppedImage = + tempBmp.Clone( + rect, + tempBmp.PixelFormat)) + { + lastImage = + new Bitmap(croppedImage); + } + } + + if (isZoomed[wmNr0b]) + { + zoomedImage = + MakeZoomedImage( + lastImage, + zoomCenterLoc[wmNr0b]); + } + + log.Debug( + $"Image displayed from group: " + + $"waterMeter={lastWmNr1}, " + + $"groupIndex={currentImageGroupIndex + 1}/" + + $"{currentImageGroup.Count}, " + + $"file='{imageFileName}'"); + } + catch (Exception exc) + { + log.Error( + $"Cannot load image: " + + $"file='{imageFileName}', " + + $"error={exc.Message}", + exc); + + DisposeLoadedImages(); + } + + ShowImage(); + } + + /// --- block picture Crop help method --- + private static Rectangle CreateSquareCropRectangle( Bitmap bitmap) + { + int size = Math.Min( bitmap.Width, bitmap.Height); + int x = (bitmap.Width - size) / 2; + int y = (bitmap.Height - size) / 2; + + return new Rectangle( x, y, size, size); + } + + // --- block release old pictures --- + private void DisposeLoadedImages() + { + if (lastImage != null) + { + lastImage.Dispose(); + lastImage = null; + } + + if (zoomedImage != null) + { + zoomedImage.Dispose(); + zoomedImage = null; + } + } + + private void ClearImageGroup() + { + currentImageGroup.Clear(); + currentImageGroupIndex = 0; + lastImageName = null; + } + + // --- block picture exchange --- + private void ShowPreviousImageFromGroup() + { + ShowImageFromGroup(-1); + } + + private void ShowNextImageFromGroup() + { + ShowImageFromGroup(1); + } + + private void ShowImageFromGroup( + int direction) + { + if (direction == 0 || + currentImageGroup.Count <= 1) + { + return; + } + + int newIndex = + currentImageGroupIndex + direction; + + /* + * Cyklické prepínanie: + * + * posledný -> prvý + * prvý -> posledný + */ + if (newIndex < 0) + { + newIndex = + currentImageGroup.Count - 1; + } + else if (newIndex >= currentImageGroup.Count) + { + newIndex = 0; + } + + currentImageGroupIndex = newIndex; + + LoadImageFile( + currentImageGroup[currentImageGroupIndex]); + } + + + // --- mouse events --- + + private void pictureBox1_MouseEnter( + object sender, + EventArgs e) + { + mouseOverPictureBox = true; + + /* + * PictureBox štandardne nemusí mať focus. + * Focus pomôže zachytiť klávesy aj koliesko. + */ + pictureBox1.Focus(); + } + + private void pictureBox1_MouseLeave( + object sender, + EventArgs e) + { + mouseOverPictureBox = false; + } + + private void pictureBox1_MouseWheel( + object sender, + MouseEventArgs e) + { + if (!mouseOverPictureBox || + currentImageGroup.Count <= 1) + { + return; + } + + if (e.Delta > 0) + { + ShowPreviousImageFromGroup(); + } + else if (e.Delta < 0) + { + ShowNextImageFromGroup(); + } + } /// /// Left click = Zoom, Right click = Unzoom @@ -1276,7 +1731,9 @@ namespace TBF.Rig.DataEntry.StandartCameraPurchaseOrder { if (lastImage == null) { + Image previousImage = pictureBox1.Image; pictureBox1.Image = new Bitmap(TBF.Properties.Resources.Empty); + previousImage?.Dispose(); return; } @@ -1312,10 +1769,108 @@ namespace TBF.Rig.DataEntry.StandartCameraPurchaseOrder Brush brush = (brightness > 160) ? darkBrush : lightBrush; grph.DrawString(lastWmNr1.ToString(), font, brush, margin, margin); + + DrawImageInfo(grph, tempBmp); } + Image oldPictureBoxImage = pictureBox1.Image; pictureBox1.Image = tempBmp; + oldPictureBoxImage?.Dispose(); } + + private void DrawImageInfo( Graphics graphics, Bitmap bitmap) + { + if (graphics == null) + { + throw new ArgumentNullException(nameof(graphics)); + } + + if (bitmap == null) + { + throw new ArgumentNullException(nameof(bitmap)); + } + + string currentImageFileName = GetCurrentImageFileName(); + + if (string.IsNullOrWhiteSpace(currentImageFileName)) + { + return; + } + + string fileName = Path.GetFileName(currentImageFileName); + + string displayText = fileName; + + Match match = Regex.Match( fileName, @"_img(?\d+)_(?\d{8})_(? - public static void OnImagesReceived(object sender, int idxImage, Image image, ImageRotation imageRotation, int iImageIdx = 0, int imageCount = 1) + public static void OnImagesReceived( + object sender, + int cameraIndex, + Image image, + int imageIndex, + int imageCount, + DateTimeOffset? frameTime) { - - log.Info($"Images received from camera: {idxImage}, iImageIdx: {iImageIdx}, imageCount: {imageCount}"); - - if (null != ImageCameraHandler) + if (image == null) { - try - { - //rotation - ImageUtils.Rotate(image, imageRotation); - - //Send image to bridge - ImageCameraHandler(sender, new PromptReceivedImageEventArgs(idxImage, image)); - log.Info($"ImageCameraHandler reached camera: {idxImage}, iImageIdx: {iImageIdx}, imageCount: {imageCount}"); - } - catch (Exception ex) - { - log.Info("Internal error: PromptReceivedHandler exception: " + ex.Message); - } + return; } - + log.Info( $"Images received from camera: {cameraIndex}, imageIndex={imageIndex}, " + + $"imageCount={imageCount}, frameTime={frameTime:O}"); - if (null != ImagesCameraHandler) + EventHandler handler = ImagesCameraHandler; + + if (handler == null) { - try - { - //rotation - ImageUtils.Rotate(image, imageRotation); - - //Send images to bridge - ImagesCameraHandler(sender, new PromptReceivedImagesEventArgs(idxImage, image,iImageIdx, imageCount)); - log.Info($"ImagesCameraHandler reached camera: {idxImage}, iImageIdx: {iImageIdx}, imageCount: {imageCount}"); - } - catch (Exception ex) - { - log.Info("Internal error: PromptReceivedHandler exception: " + ex.Message); - } + image.Dispose(); + return; + } + + try + { + handler( sender, + new PromptReceivedImagesEventArgs( + cameraIndex, + imageIndex, + imageCount, + image, + frameTime)); + + log.Info( $"ImagesCameraHandler reached camera: {cameraIndex}, imageIndex={imageIndex}, " + + $"imageCount={imageCount}"); + } + catch (Exception exc) + { + log.Error( $"ImagesCameraHandler failed: cameraIdx={cameraIndex}, imageIndex={imageIndex}, " + + $"error={exc.Message}", exc); + image.Dispose(); } } @@ -2934,6 +3086,517 @@ namespace TBF.Rig.Network.Camera.CJMS11 return await readTask; } + + + //------------------------------ SEND PART -------------------------------------- + + public Task> StartSendImagesPreviewAsync( + int count, + int delayMs, + string taskId) + { + if (count <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(count), + "Image count must be greater than zero."); + } + + if (delayMs < 0) + { + throw new ArgumentOutOfRangeException( + nameof(delayMs), + "Delay cannot be negative."); + } + + if (string.IsNullOrWhiteSpace(taskId)) + { + throw new ArgumentException( + "Task ID is required.", + nameof(taskId)); + } + + if (sendImagesTask != null && + !sendImagesTask.IsCompleted) + { + log.Warn( + $"Send images preview already running: " + + $"cameraIdx={cameraIdx}, taskId={taskId}"); + + return sendImagesTask; + } + + sendImagesCts?.Cancel(); + sendImagesCts?.Dispose(); + + sendImagesCts = + new CancellationTokenSource(); + + sendImagesTask = + SendImagesPreviewListenerAsync( + count, + delayMs, + taskId, + sendImagesCts.Token); + + return sendImagesTask; + } + + public void CancelSendImagesPreview() + { + try + { + sendImagesCts?.Cancel(); + } + catch (ObjectDisposedException) + { + } + } + + private async Task> SendImagesPreviewListenerAsync( + int requestedCount, + int delayMs, + string taskId, + CancellationToken ct) + { + bool lockAcquired = false; + TcpClient client = null; + NetworkStream stream = null; + CameraLineReader reader = null; + + var imagesByIndex = new Dictionary(); + + try + { + await cameraOperationLock.WaitAsync(ct).ConfigureAwait(false); + + lockAcquired = true; + + client = await ConnectCameraAsync( timeoutMs: 5000, ct: ct).ConfigureAwait(false); + stream = client.GetStream(); + + reader = new CameraLineReader( stream, 64 * 1024); + + /* + * Kamera musí byť pripravená. + */ + JmsPacket prepareAnswer = await SendCommandAndWaitToAnswerAsync( + stream, + reader, + CommandM.prepare_camera_, + null, + 3000, + ct) + .ConfigureAwait(false); + + bool prepared = prepareAnswer != null && + prepareAnswer.JmsMessage != null && + prepareAnswer.JmsMessage.Status == MessageStatus.ACK; + + bool alreadyOpened = + prepareAnswer != null && + prepareAnswer.JmsMessage != null && + prepareAnswer.JmsMessage.Status == MessageStatus.NACK && + !string.IsNullOrEmpty( prepareAnswer.JmsMessage.Payload) && + prepareAnswer.JmsMessage.Payload.Contains( "OPENED"); + + if (!prepared && !alreadyOpened) + { + throw new IOException( $"Prepare camera for send failed: cameraIdx={cameraIdx}, " + + $"answer={FormatPacketForLog(prepareAnswer)}"); + } + + var request = new SendImagesRequest { Count = requestedCount, Delay = delayMs, TaskId = taskId }; + string requestPayload = JsonConvert.SerializeObject( request, Formatting.None); + + log.Debug( $"Starting send images preview: cameraIdx={cameraIdx}, count={requestedCount}, " + + $"delay={delayMs}, taskId={taskId}"); + + /* + * Pri send_ neposielame cez + * SendCommandAndWaitToAnswerAsync(), + * pretože očakávame viac odpovedí. + */ + await SendCommandAsync( + stream, + CommandM.send_, + ct, + requestPayload) + .ConfigureAwait(false); + + /* + * Timeout musí zahŕňať plánovaný čas snímania. + * + * count=5, delay=2050: + * približne 4 * 2050 ms medzi prvým a posledným frame + * plus rezerva na prenos Base64 obrázkov. + */ + int totalTimeoutMs = CalculateSendTimeout( requestedCount, delayMs); + + using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct)) + { + timeoutCts.CancelAfter(totalTimeoutMs); + + while (imagesByIndex.Count < requestedCount) + { + timeoutCts.Token.ThrowIfCancellationRequested(); + + string response = await reader.ReadLineAsync( 10000, timeoutCts.Token).ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(response)) + { + continue; + } + + JmsPacket packet; + + try + { + packet = new JmsPacket(response); + } + catch (Exception exc) + { + log.Warn( $"Cannot parse send response: cameraIdx={cameraIdx}, error={exc.Message}"); + continue; + } + + if (packet.JmsMessage == null) + { + continue; + } + + if (packet.JmsMessage.Command != CommandM.send_) + { + log.Debug( $"Unexpected packet during send: cameraIdx={cameraIdx}, command={packet.JmsMessage.Command}"); + continue; + } + + if (packet.JmsMessage.Status != MessageStatus.ACK) + { + log.Warn( $"Send image returned NACK: cameraIdx={cameraIdx}, payload={packet.JmsMessage.Payload}"); + continue; + } + + string payload = packet.JmsMessage.Payload; + + if (string.IsNullOrWhiteSpace(payload)) + { + continue; + } + + /* + * Posledný sumarizačný packet nemá + * pictureBase64 ani workerId. + */ + if (!PayloadContainsImage(payload)) + { + TryLogSendSummary( payload, cameraIdx); + continue; + } + + SendImagePayload imagePayload; + + try + { + imagePayload = JsonConvert.DeserializeObject(payload); + } + catch (Exception exc) + { + log.Warn( $"Cannot deserialize send image: cameraIdx={cameraIdx}, error={exc.Message}"); + continue; + } + + if (imagePayload == null || string.IsNullOrWhiteSpace( imagePayload.PictureBase64)) + { + continue; + } + + if (!string.Equals( imagePayload.Status, "SUCCESSFUL", StringComparison.OrdinalIgnoreCase)) + { + log.Warn( $"Send image status is not successful: cameraIdx={cameraIdx}, " + + $"workerId={imagePayload.WorkerId}, status={imagePayload.Status}"); + continue; + } + + /* + * workerId je podľa logu 1..count. + */ + int imageIndex = imagePayload.WorkerId; + + if (imageIndex <= 0) + { + log.Warn( $"Invalid send image index: cameraIdx={cameraIdx}, workerId={imageIndex}"); + continue; + } + + /* + * Duplicitný packet ignorujeme. + */ + if (imagesByIndex.ContainsKey( imageIndex)) + { + log.Warn( $"Duplicate send image: cameraIdx={cameraIdx}, workerId={imageIndex}"); + continue; + } + + Image image = DecodeBase64Image( imagePayload.PictureBase64); + + if (!IsValidGrabbedImage(image)) + { + image?.Dispose(); + + log.Warn( + $"Invalid send image: " + + $"cameraIdx={cameraIdx}, " + + $"workerId={imageIndex}"); + + continue; + } + + DateTimeOffset? frameTime = + ParseFrameTime( + imagePayload.FrameTime); + + /* + * Úpravy obrázka dokončíme ešte predtým, + * než ho odovzdáme UI. + */ + ImageUtils.Rotate( + image, + RoiConfiguration.ImageRotation); + + DrawOverlay(image); + + var timedImage = + new TimedCameraImage + { + Index = imageIndex, + TaskId = + imagePayload.TaskId, + FrameTime = frameTime, + FrameTimeMillis = + imagePayload.FrameTimeMillis, + FrameGap = + imagePayload.FrameGap, + Image = image + }; + + imagesByIndex.Add( + imageIndex, + timedImage); + + /* + * UI dostane klon. + * Originál zostáva vo výslednom zozname. + */ + Image imageForUi = CloneImageToBitmap(image); + + OnImagesReceived( + this, + cameraIdx, + imageForUi, + imageIndex, + requestedCount, + frameTime); + + log.Debug( $"Send image received: cameraIdx={cameraIdx}, workerId={imageIndex}, " + + $"received={imagesByIndex.Count}/{requestedCount}, frameTime={frameTime:O}, " + + $"size={image.Width}x{image.Height}"); + } + } + + List result = + imagesByIndex + .OrderBy(x => x.Key) + .Select(x => x.Value) + .ToList(); + + log.Debug( + $"Send images preview completed: " + + $"cameraIdx={cameraIdx}, " + + $"received={result.Count}, " + + $"taskId={taskId}"); + + return result; + } + catch (OperationCanceledException) + { + foreach (TimedCameraImage image + in imagesByIndex.Values) + { + image.Image?.Dispose(); + } + + log.Debug( + $"Send images preview cancelled: " + + $"cameraIdx={cameraIdx}, " + + $"taskId={taskId}"); + + throw; + } + catch (Exception exc) + { + foreach (TimedCameraImage image + in imagesByIndex.Values) + { + image.Image?.Dispose(); + } + + log.Error( + $"Send images preview failed: " + + $"cameraIdx={cameraIdx}, " + + $"taskId={taskId}, " + + $"error={exc.Message}", + exc); + + UiBridge.Bridge.OnImage( + cameraIdx, + null); + + throw; + } + finally + { + if (stream != null && + reader != null) + { + try + { + await TryCloseCameraAsync( + stream, + reader) + .ConfigureAwait(false); + } + catch (Exception exc) + { + log.Warn( + $"Send preview cleanup failed: " + + $"cameraIdx={cameraIdx}, " + + $"error={exc.Message}"); + } + } + + try + { + stream?.Dispose(); + } + catch + { + } + + try + { + client?.Close(); + client?.Dispose(); + } + catch + { + } + + if (lockAcquired) + { + cameraOperationLock.Release(); + } + } + } + + private static int CalculateSendTimeout( + int count, + int delayMs) + { + /* + * Medzi count obrázkami je count - 1 intervalov. + * Pridáme 15 sekúnd rezervu na capture, + * Base64 a sieťový prenos. + */ + long captureDuration = + (long)Math.Max(0, count - 1) * + delayMs; + + long timeout = + captureDuration + 15000L; + + return timeout > int.MaxValue + ? int.MaxValue + : (int)timeout; + } + + private static Image DecodeBase64Image( string pictureBase64) + { + if (string.IsNullOrWhiteSpace( pictureBase64)) + { + return null; + } + + try + { + byte[] imageBytes = Convert.FromBase64String( pictureBase64); + + using (var stream = new MemoryStream(imageBytes)) + using (Image decoded = Image.FromStream( stream, useEmbeddedColorManagement: false, validateImageData: true)) + { + /* + * Bitmap odpojí výsledok od MemoryStream. + */ + return new Bitmap(decoded); + } + } + catch + { + return null; + } + } + + private static bool PayloadContainsImage( string payload) + { + if (string.IsNullOrWhiteSpace(payload)) + { + return false; + } + + return payload.IndexOf( "\"pictureBase64\"", StringComparison.OrdinalIgnoreCase) >= 0; + } + + private static DateTimeOffset? ParseFrameTime( string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + DateTimeOffset parsed; + + if (DateTimeOffset.TryParse( + value, + System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.RoundtripKind, + out parsed)) + { + return parsed; + } + + return null; + } + + private static void TryLogSendSummary( string payload, int cameraIdx) + { + try + { + SendSummaryPayload summary = JsonConvert.DeserializeObject(payload); + + if (summary == null) + { + return; + } + + log.Debug( $"Send summary received: cameraIdx={cameraIdx}, picCount={summary.PictureCount}, " + + $"delay={summary.Delay}, timeStamp={summary.TimeStamp}"); + } + catch (Exception exc) + { + log.Debug( $"Payload is not a send summary: cameraIdx={cameraIdx}, error={exc.Message}"); + } + } + } diff --git a/TBF/Rig/Network/Camera/CJMS11/GrabImagesOp.cs b/TBF/Rig/Network/Camera/CJMS11/GrabImagesOp.cs index d33cb33c6..af7048168 100644 --- a/TBF/Rig/Network/Camera/CJMS11/GrabImagesOp.cs +++ b/TBF/Rig/Network/Camera/CJMS11/GrabImagesOp.cs @@ -5,20 +5,30 @@ using System; using System.Collections.Generic; using System.Drawing; +using System.Globalization; using System.IO; +using System.Linq; using System.Threading.Tasks; using Common; using Config.Entities; using log4net; +using TBF.Rig.Network.Camera.CJMS11.POJO; using TBF.Rig.Network.Telnet; namespace TBF.Rig.Network.Camera.CJMS11 { public class GrabImagesOp : IOperation { - private static readonly ILog log = - LogManager.GetLogger(typeof(GrabImagesOp)); + private static readonly ILog log = LogManager.GetLogger(typeof(GrabImagesOp)); + private enum GrabOperationMode + { + None, + SingleImage, + SendImages + } + + private readonly Camera camera; private readonly string[] imgFileNames; private readonly bool blackAndWhite; @@ -29,19 +39,21 @@ namespace TBF.Rig.Network.Camera.CJMS11 private bool grabImageCommandSent; private bool transferringGrabbedImage; + + private GrabOperationMode operationMode; private Image grabedImage; - private readonly Dictionary grabedImages = - new Dictionary(); + private readonly Dictionary grabedImages = new Dictionary(); private Task grabImageTask; + private Task> sendImagesTask; private Task saveImageTask; - public override string ToString() - { - return "GrabImageOp()"; - } + private IReadOnlyList timedCameraImages; + private readonly List savedFileNames = new List(); + + public override string ToString() { return "GrabImageOp()"; } public GrabImagesOp( Camera camera, @@ -63,6 +75,8 @@ namespace TBF.Rig.Network.Camera.CJMS11 SubscribeEvents(); } + + private void SubscribeEvents() { @@ -77,30 +91,35 @@ namespace TBF.Rig.Network.Camera.CJMS11 object sender, PromptReceivedImageEventArgs args) { - if (args == null || - args.idxCamera != camera.CameraIdx || - args.image == null) + if (args == null || args.idxCamera != camera.CameraIdx || args.image == null) { return; } + /* + * Tento obrázok používame iba pre kompatibilitu + * s existujúcim eventovým mechanizmom. + * + * Hlavným zdrojom výsledku je Task. + */ lock (imageSync) { grabedImage = args.image; } } - private void ImagesReceived( - object sender, - PromptReceivedImagesEventArgs args) + private void ImagesReceived( object sender, PromptReceivedImagesEventArgs args) { - if (args == null || - args.idxCamera != camera.CameraIdx || - args.image == null) + if (args == null || args.idxCamera != camera.CameraIdx || args.image == null) { return; } + /* + * Náhľadové obrázky z eventu nebudeme ukladať + * na disk. Originálne obrázky dostaneme vo + * výsledku sendImagesTask. + */ lock (imageSync) { grabedImage = args.image; @@ -115,75 +134,145 @@ namespace TBF.Rig.Network.Camera.CJMS11 grabImageCommandSent = false; transferringGrabbedImage = false; + operationMode = GrabOperationMode.None; + grabImageTask = null; + sendImagesTask = null; saveImageTask = null; + timedCameraImages = null; + lock (imageSync) { grabedImage = null; grabedImages.Clear(); } - if (imgFileNames == null || - imgFileNames.Length == 0 || - string.IsNullOrWhiteSpace(imgFileNames[0])) - { - return; - } + savedFileNames.Clear(); - if (File.Exists(imgFileNames[0])) - { - File.Delete(imgFileNames[0]); - } + DeletePreviousOutputFiles(); } + public Event Run() { - if (imgFileNames == null || - imgFileNames.Length == 0 || - string.IsNullOrWhiteSpace(imgFileNames[0])) + if (!HasValidOutputFileName()) { - UnsubscribeEvents(); + log.Warn( + $"No output image file configured: " + + $"cameraIdx={camera.CameraIdx}"); + + FinishOperation(); + return Event.GrabPassed; } + /* + * 1. Spustenie príslušnej operácie. + */ if (!grabImageCommandSent) { - log.Debug( - $"Starting asynchronous grab: " + - $"cameraIdx={camera.CameraIdx}"); + return StartGrabOperation(); + } - grabImageCommandSent = true; + /* + * 2. Spracovanie podľa režimu. + */ + switch (operationMode) + { + case GrabOperationMode.SingleImage: + return RunSingleImageMode(); - try + case GrabOperationMode.SendImages: + return RunSendImagesMode(); + + default: + log.Error( + $"Grab operation mode was not selected: " + + $"cameraIdx={camera.CameraIdx}"); + + FinishOperation(); + + return Event.GrabFailed; + } + } + + private Event StartGrabOperation() + { + grabImageCommandSent = true; + + try + { + int requestedImageCount = + GetRequestedImageCount(); + + if (requestedImageCount <= 1) { + operationMode = + GrabOperationMode.SingleImage; + + log.Debug( + $"Starting single asynchronous grab: " + + $"cameraIdx={camera.CameraIdx}"); + grabImageTask = camera.StartGrabImageListenerAsync( retryUntilValidImage: true, maxAttempts: 5, retryDelayMs: 200); } - catch (Exception exc) + else { - log.Error( - $"Cannot start grab image task: " + - $"cameraIdx={camera.CameraIdx}", - exc); + operationMode = + GrabOperationMode.SendImages; - UnsubscribeEvents(); - return Event.GrabFailed; + int delayMs = + GetRequestedDelayMs(); + + string taskId = + CreateSendTaskId(); + + log.Debug( + $"Starting send images operation: " + + $"cameraIdx={camera.CameraIdx}, " + + $"count={requestedImageCount}, " + + $"delayMs={delayMs}, " + + $"taskId={taskId}"); + + sendImagesTask = + camera.StartSendImagesPreviewAsync( + count: requestedImageCount, + delayMs: delayMs, + taskId: taskId); } return Event.CameraBusy; } + catch (Exception exc) + { + log.Error( + $"Cannot start image operation: " + + $"cameraIdx={camera.CameraIdx}, " + + $"mode={operationMode}, " + + $"error={exc.Message}", + exc); + FinishOperation(); + + return Event.GrabFailed; + } + } + + private Event RunSingleImageMode() + { if (grabImageTask == null) { log.Error( - $"Grab task was not created: " + + $"Single grab task was not created: " + $"cameraIdx={camera.CameraIdx}"); - UnsubscribeEvents(); + FinishOperation(); + return Event.GrabFailed; } @@ -195,21 +284,23 @@ namespace TBF.Rig.Network.Camera.CJMS11 if (grabImageTask.IsCanceled) { log.Warn( - $"Grab task cancelled: " + + $"Single grab task was cancelled: " + $"cameraIdx={camera.CameraIdx}"); - UnsubscribeEvents(); + FinishOperation(); + return Event.GrabFailed; } if (grabImageTask.IsFaulted) { log.Error( - $"Grab task failed: " + + $"Single grab task failed: " + $"cameraIdx={camera.CameraIdx}", grabImageTask.Exception?.Flatten()); - UnsubscribeEvents(); + FinishOperation(); + return Event.GrabFailed; } @@ -220,26 +311,31 @@ namespace TBF.Rig.Network.Camera.CJMS11 try { imageToSave = - grabImageTask.GetAwaiter().GetResult(); + grabImageTask + .GetAwaiter() + .GetResult(); } catch (Exception exc) { log.Error( $"Cannot obtain grabbed image: " + - $"cameraIdx={camera.CameraIdx}", + $"cameraIdx={camera.CameraIdx}, " + + $"error={exc.Message}", exc); - UnsubscribeEvents(); + FinishOperation(); + return Event.GrabFailed; } if (imageToSave == null) { log.Error( - $"Grab completed without valid image: " + + $"Single grab completed without image: " + $"cameraIdx={camera.CameraIdx}"); - UnsubscribeEvents(); + FinishOperation(); + return Event.GrabFailed; } @@ -248,37 +344,275 @@ namespace TBF.Rig.Network.Camera.CJMS11 grabedImage = imageToSave; } + string outputFileName = + imgFileNames[0]; + try { saveImageTask = camera.StartSaveImageToFileAsync( imageToSave, - imgFileNames[0]); + outputFileName); + + savedFileNames.Clear(); + savedFileNames.Add( + outputFileName); transferringGrabbedImage = true; } catch (Exception exc) { log.Error( - $"Cannot start image save task: " + + $"Cannot start single image save: " + $"cameraIdx={camera.CameraIdx}, " + - $"file={imgFileNames[0]}", + $"file={outputFileName}, " + + $"error={exc.Message}", exc); - UnsubscribeEvents(); + FinishOperation(); + return Event.GrabFailed; } return Event.CameraBusy; } - if (saveImageTask == null) + return CheckSaveTaskAndFinish(); + } + + + + private Event RunSendImagesMode() + { + if (sendImagesTask == null) { log.Error( - $"Save image task was not created: " + + $"Send images task was not created: " + $"cameraIdx={camera.CameraIdx}"); - UnsubscribeEvents(); + FinishOperation(); + + return Event.GrabFailed; + } + + if (!sendImagesTask.IsCompleted) + { + return Event.CameraBusy; + } + + if (sendImagesTask.IsCanceled) + { + log.Warn( + $"Send images task was cancelled: " + + $"cameraIdx={camera.CameraIdx}"); + + FinishOperation(); + + return Event.GrabFailed; + } + + if (sendImagesTask.IsFaulted) + { + log.Error( + $"Send images task failed: " + + $"cameraIdx={camera.CameraIdx}", + sendImagesTask.Exception?.Flatten()); + + FinishOperation(); + + return Event.GrabFailed; + } + + if (!transferringGrabbedImage) + { + try + { + timedCameraImages = + sendImagesTask + .GetAwaiter() + .GetResult(); + } + catch (Exception exc) + { + log.Error( + $"Cannot obtain send image results: " + + $"cameraIdx={camera.CameraIdx}, " + + $"error={exc.Message}", + exc); + + FinishOperation(); + + return Event.GrabFailed; + } + + if (timedCameraImages == null || + timedCameraImages.Count == 0) + { + log.Error( + $"Send operation returned no images: " + + $"cameraIdx={camera.CameraIdx}"); + + FinishOperation(); + + return Event.GrabFailed; + } + + int requestedCount = + GetRequestedImageCount(); + + if (timedCameraImages.Count != + requestedCount) + { + log.Error( + $"Send operation returned unexpected image count: " + + $"cameraIdx={camera.CameraIdx}, " + + $"requested={requestedCount}, " + + $"received={timedCameraImages.Count}"); + + DisposeTimedImages(); + FinishOperation(); + + return Event.GrabFailed; + } + + IReadOnlyList + orderedImages = + timedCameraImages + .OrderBy(image => image.Index) + .ToList(); + + try + { + /* + * Camera.StartSaveImageToFileAsync používa + * jeden interný save task, preto viac + * obrázkov uložíme sekvenčne v jednom + * samostatnom tasku. + */ + saveImageTask = + Task.Run( + () => SaveTimedImages( + orderedImages)); + + transferringGrabbedImage = true; + } + catch (Exception exc) + { + log.Error( + $"Cannot start send images save task: " + + $"cameraIdx={camera.CameraIdx}, " + + $"error={exc.Message}", + exc); + + DisposeTimedImages(); + FinishOperation(); + + return Event.GrabFailed; + } + + return Event.CameraBusy; + } + + return CheckSaveTaskAndFinish(); + } + + private void SaveTimedImages( IReadOnlyList images) + { + if (images == null) + { + throw new ArgumentNullException(nameof(images)); + } + + if (images.Count == 0) + { + throw new ArgumentException( "No timed camera images were provided.", nameof(images)); + } + + if (!HasValidOutputFileName()) + { + throw new InvalidOperationException( "No base output image file name is configured."); + } + + savedFileNames.Clear(); + + IReadOnlyList orderedImages = + images + .Where(image => image != null) + .OrderBy(image => image.Index) + .ToList(); + + if (orderedImages.Count != images.Count) + { + throw new InvalidOperationException( $"One or more timed camera images are null. cameraIdx={camera.CameraIdx}, " + + $"expected={images.Count}, valid={orderedImages.Count}"); + } + + log.Debug( $"Starting timed images save: cameraIdx={camera.CameraIdx}, imagesCount={orderedImages.Count}, " + + $"baseFile='{imgFileNames[0]}'"); + + foreach (TimedCameraImage timedImage in orderedImages) + { + if (timedImage.Image == null) + { + throw new InvalidOperationException( $"Timed camera image contains no image data: " + + $"cameraIdx={camera.CameraIdx}, imageIndex={timedImage.Index}, frameTime={timedImage.FrameTime:O}"); + } + + /* + * Každý obrázok uložíme pod unikátnym názvom + * obsahujúcim index obrázka a časovú značku. + */ + string timedFileName = CreateTimedImageFileName( timedImage, orderedImages.Count); + + log.Debug( $"Saving timed camera image: cameraIdx={camera.CameraIdx}, imageIndex={timedImage.Index}, " + + $"frameTime={timedImage.FrameTime:O}, file='{timedFileName}'"); + + camera.SaveImageToFile( + timedImage.Image, + timedFileName); + + savedFileNames.Add( + timedFileName); + + log.Debug( $"Timed camera image saved successfully: cameraIdx={camera.CameraIdx}, " + + $"imageIndex={timedImage.Index}, file='{timedFileName}'"); + + /* + * Prvý obrázok uložíme ešte raz pod pôvodným názvom. + * + * Tento súbor používa existujúci dialóg cez + * startImages[] alebo endImages[]. + * + * Napríklad: + * TestAdvance-1-start.bmp + */ + if (timedImage.Index == 1) + { + string compatibilityFileName = + imgFileNames[0]; + + log.Debug($"Saving first timed image under compatibility name: cameraIdx={camera.CameraIdx}, " + + $"imageIndex={timedImage.Index}, frameTime={timedImage.FrameTime:O}, file='{compatibilityFileName}'"); + + camera.SaveImageToFile(timedImage.Image, compatibilityFileName); + savedFileNames.Add(compatibilityFileName); + + log.Debug($"First timed image saved under compatibility name: cameraIdx={camera.CameraIdx}, " + + $"imageIndex={timedImage.Index}, file='{compatibilityFileName}'"); + } + } + + log.Debug( + $"Timed images save completed: cameraIdx={camera.CameraIdx}, receivedImages={orderedImages.Count}, " + + $"savedFiles={savedFileNames.Count}"); + } + + private Event CheckSaveTaskAndFinish() + { + if (saveImageTask == null) + { + log.Error( $"Save image task was not created: cameraIdx={camera.CameraIdx}, mode={operationMode}"); + FinishOperation(); return Event.GrabFailed; } @@ -289,59 +623,501 @@ namespace TBF.Rig.Network.Camera.CJMS11 if (saveImageTask.IsCanceled) { - log.Warn( - $"Save image task cancelled: " + - $"cameraIdx={camera.CameraIdx}"); + log.Warn( $"Save image task was cancelled: cameraIdx={camera.CameraIdx}, mode={operationMode}"); + + DisposeOperationImages(); + FinishOperation(); - UnsubscribeEvents(); return Event.GrabFailed; } if (saveImageTask.IsFaulted) { - log.Error( - $"Save image task failed: " + - $"cameraIdx={camera.CameraIdx}", + log.Error( $"Save image task failed: cameraIdx={camera.CameraIdx}, mode={operationMode}", saveImageTask.Exception?.Flatten()); - UnsubscribeEvents(); + DisposeOperationImages(); + FinishOperation(); + return Event.GrabFailed; } - bool fileSaved = - File.Exists(imgFileNames[0]) && - new FileInfo(imgFileNames[0]).Length > 0; + bool allFilesSaved = VerifySavedFiles(); - UnsubscribeEvents(); + if (allFilesSaved) + { + log.Debug( $"Image operation completed successfully: cameraIdx={camera.CameraIdx}, " + + $"mode={operationMode}, savedFiles={savedFileNames.Count}"); + } + else + { + log.Error( $"One or more image files were not saved: cameraIdx={camera.CameraIdx}, mode={operationMode}"); + } - return fileSaved + DisposeOperationImages(); + FinishOperation(); + + return allFilesSaved ? Event.GrabPassed : Event.GrabFailed; } + private bool VerifySavedFiles() + { + if (savedFileNames.Count == 0) + { + return false; + } + + foreach (string fileName + in savedFileNames) + { + try + { + if (string.IsNullOrWhiteSpace(fileName) || + !File.Exists(fileName)) + { + return false; + } + + var fileInfo = + new FileInfo(fileName); + + if (fileInfo.Length <= 0) + { + return false; + } + } + catch (Exception exc) + { + log.Error( + $"Cannot verify saved image: " + + $"cameraIdx={camera.CameraIdx}, " + + $"file={fileName}, " + + $"error={exc.Message}", + exc); + + return false; + } + } + + return true; + } + + private string CreateTimedImageFileName( + TimedCameraImage timedImage, + int totalImageCount) + { + if (timedImage == null) + { + throw new ArgumentNullException(nameof(timedImage)); + } + + if (!HasValidOutputFileName()) + { + throw new InvalidOperationException( + "No base output image file name is configured."); + } + + int imagePosition = timedImage.Index; + + if (imagePosition <= 0) + { + throw new InvalidOperationException( + $"Invalid image position: {imagePosition}."); + } + + // Prvý obrázok musí zostať pod pôvodným názvom, + // pretože tento názov používa startImages/endImages. + if (imagePosition == 1) + { + return imgFileNames[0]; + } + + string configuredFileName = imgFileNames[0]; + + string directory = + Path.GetDirectoryName(configuredFileName); + + string baseName = + Path.GetFileNameWithoutExtension(configuredFileName); + + string extension = + Path.GetExtension(configuredFileName); + + if (string.IsNullOrWhiteSpace(extension)) + { + extension = ".png"; + } + + DateTime timestampUtc = + timedImage.FrameTime.HasValue + ? timedImage.FrameTime.Value.UtcDateTime + : DateTime.UtcNow; + + string timestampText = + timestampUtc.ToString( + "yyyyMMdd_HHmmss_fff", + CultureInfo.InvariantCulture); + + int digits = Math.Max( + 2, + totalImageCount.ToString( + CultureInfo.InvariantCulture).Length); + + string indexText = + imagePosition.ToString( + "D" + digits, + CultureInfo.InvariantCulture); + + string newFileName = + $"{baseName}_img{indexText}_{timestampText}{extension}"; + + return string.IsNullOrWhiteSpace(directory) + ? newFileName + : Path.Combine(directory, newFileName); + } + + private string GetConfiguredFileName( + int zeroBasedIndex) + { + if (imgFileNames != null && + zeroBasedIndex >= 0 && + zeroBasedIndex < imgFileNames.Length && + !string.IsNullOrWhiteSpace( + imgFileNames[zeroBasedIndex])) + { + return imgFileNames[zeroBasedIndex]; + } + + /* + * Ak nie je pripravený názov pre každý obrázok, + * použijeme prvý názov ako základ. + */ + return imgFileNames[0]; + } + + private int GetRequestedImageCount() + { + var roi = camera.CamRoi; + + if (roi == null) + { + return 1; + } + + return Math.Max(1, roi.RoiPicCount); + } + + private int GetRequestedDelayMs() + { + const int MinDelayMs = 500; + + var roi = camera.CamRoi; + + if (roi == null) + { + return MinDelayMs; + } + + int configuredDelay = roi.RoiDeltaTimeMs; + + if (configuredDelay < MinDelayMs) + { + log.Warn( $"Configured ROI delay {configuredDelay} ms is below minimum. Using {MinDelayMs} ms instead."); + } + + return Math.Max(MinDelayMs, configuredDelay); + } + + private string CreateSendTaskId() + { + /* + * Timestamp + kamera + krátka časť GUID. + * ID je čitateľné a zároveň odolné voči + * kolíziám pri rýchlom opakovaní. + */ + string timestamp = DateTime.UtcNow.ToString( "yyyyMMddHHmmssfff", CultureInfo.InvariantCulture); + + string uniquePart = Guid.NewGuid() .ToString("N") .Substring(0, 8); + + return $"{timestamp}_cam{camera.CameraIdx}_" + uniquePart; + } + + private bool HasValidOutputFileName() + { + return + imgFileNames != null && + imgFileNames.Length > 0 && + !string.IsNullOrWhiteSpace( + imgFileNames[0]); + } + + + + + + + + public void Stop() { try { - camera.CancelGrabImage(); + switch (operationMode) + { + case GrabOperationMode.SingleImage: + camera.CancelGrabImage(); + break; + + case GrabOperationMode.SendImages: + camera.CancelSendImagesPreview(); + break; + } } catch (Exception exc) { - log.Warn( - $"Cancel grab image failed: " + - $"cameraIdx={camera.CameraIdx}, " + + log.Warn( $"Cancel image operation failed: cameraIdx={camera.CameraIdx}, mode={operationMode}, " + $"error={exc.Message}"); } finally { - UnsubscribeEvents(); + FinishOperation(); } } + private void FinishOperation() + { + UnsubscribeEvents(); + } + private void UnsubscribeEvents() { Camera.ImageCameraHandler -= ImageReceived; Camera.ImagesCameraHandler -= ImagesReceived; } + + private static string AppendTimestampToFileName( + string originalFileName, + DateTime timestamp) + { + if (string.IsNullOrWhiteSpace(originalFileName)) + { + throw new ArgumentException( + "File name is required.", + nameof(originalFileName)); + } + + string directory = + Path.GetDirectoryName(originalFileName); + + string fileNameWithoutExtension = + Path.GetFileNameWithoutExtension(originalFileName); + + string extension = + Path.GetExtension(originalFileName); + + string timestampText = + timestamp + .ToUniversalTime() + .ToString( + "yyyyMMdd_HHmmss_fff", + System.Globalization.CultureInfo.InvariantCulture); + + string newFileName = + $"{fileNameWithoutExtension}_{timestampText}{extension}"; + + return string.IsNullOrWhiteSpace(directory) + ? newFileName + : Path.Combine(directory, newFileName); + } + + + private void DisposeOperationImages() + { + if (operationMode == + GrabOperationMode.SendImages) + { + DisposeTimedImages(); + return; + } + + Image imageToDispose = null; + + lock (imageSync) + { + imageToDispose = grabedImage; + grabedImage = null; + } + + try + { + imageToDispose?.Dispose(); + } + catch + { + } + } + + private void DisposeTimedImages() + { + if (timedCameraImages == null) + { + return; + } + + foreach (TimedCameraImage timedImage + in timedCameraImages) + { + try + { + timedImage?.Image?.Dispose(); + } + catch + { + } + } + + timedCameraImages = null; + } + + private void DeletePreviousOutputFiles() + { + if (!HasValidOutputFileName()) + { + return; + } + + var filesToDelete = new HashSet( + StringComparer.OrdinalIgnoreCase); + + foreach (string configuredFileName in imgFileNames) + { + if (string.IsNullOrWhiteSpace(configuredFileName)) + { + continue; + } + + string fullConfiguredFileName; + + try + { + fullConfiguredFileName = + Path.GetFullPath(configuredFileName); + } + catch (Exception exc) + { + log.Warn( + $"Invalid configured image file name: " + + $"cameraIdx={camera.CameraIdx}, " + + $"file='{configuredFileName}', " + + $"error={exc.Message}"); + + continue; + } + + string directory = + Path.GetDirectoryName(fullConfiguredFileName); + + string baseName = + Path.GetFileNameWithoutExtension( + fullConfiguredFileName); + + string extension = + Path.GetExtension( + fullConfiguredFileName); + + if (string.IsNullOrWhiteSpace(directory) || + string.IsNullOrWhiteSpace(baseName)) + { + continue; + } + + if (!Directory.Exists(directory)) + { + log.Debug( + $"Image output directory does not exist; " + + $"nothing to delete: " + + $"cameraIdx={camera.CameraIdx}, " + + $"directory='{directory}'"); + + continue; + } + + /* + * Pôvodný kompatibilný súbor: + * + * TestAdvance-1-start.bmp + */ + filesToDelete.Add( + fullConfiguredFileName); + + /* + * Časované súbory: + * + * TestAdvance-1-start_img01_20260715_120000_100.bmp + * TestAdvance-1-start_img02_20260715_120000_600.bmp + * + * Časovú značku neanalyzujeme. + * Hľadáme podľa stabilného prefixu názvu. + */ + string searchPattern = + $"{baseName}_img*{extension}"; + + try + { + foreach (string foundFile in + Directory.GetFiles( + directory, + searchPattern, + SearchOption.TopDirectoryOnly)) + { + filesToDelete.Add( + Path.GetFullPath(foundFile)); + } + } + catch (Exception exc) + { + log.Warn( + $"Cannot search previous timed images: " + + $"cameraIdx={camera.CameraIdx}, " + + $"directory='{directory}', " + + $"pattern='{searchPattern}', " + + $"error={exc.Message}"); + } + } + + log.Debug( + $"Previous image files found for deletion: " + + $"cameraIdx={camera.CameraIdx}, " + + $"count={filesToDelete.Count}"); + + foreach (string fileName in filesToDelete) + { + try + { + if (!File.Exists(fileName)) + { + continue; + } + + File.Delete(fileName); + + log.Debug( + $"Previous image file deleted: " + + $"cameraIdx={camera.CameraIdx}, " + + $"file='{fileName}'"); + } + catch (Exception exc) + { + log.Warn( + $"Cannot delete previous image file: " + + $"cameraIdx={camera.CameraIdx}, " + + $"file='{fileName}', " + + $"error={exc.Message}"); + } + } + } + } } \ No newline at end of file diff --git a/TBF/Rig/Network/Camera/CJMS11/POJO/SendImagePayload.cs b/TBF/Rig/Network/Camera/CJMS11/POJO/SendImagePayload.cs new file mode 100644 index 000000000..c691d2667 --- /dev/null +++ b/TBF/Rig/Network/Camera/CJMS11/POJO/SendImagePayload.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; + +namespace TBF.Rig.Network.Camera.CJMS11.POJO +{ + public class SendImagePayload + { + [JsonProperty("frameGap")] + public int FrameGap { get; set; } + + [JsonProperty("frameTime")] + public string FrameTime { get; set; } + + [JsonProperty("frameTimeMillis")] + public long FrameTimeMillis { get; set; } + + [JsonProperty("pictureBase64")] + public string PictureBase64 { get; set; } + + [JsonProperty("picturesComplete")] + public int PicturesComplete { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("taskId")] + public string TaskId { get; set; } + + [JsonProperty("taskPath")] + public string TaskPath { get; set; } + + [JsonProperty("taskTime")] + public string TaskTime { get; set; } + + [JsonProperty("workerId")] + public int WorkerId { get; set; } + } +} \ No newline at end of file diff --git a/TBF/Rig/Network/Camera/CJMS11/POJO/SendImagesRequest.cs b/TBF/Rig/Network/Camera/CJMS11/POJO/SendImagesRequest.cs new file mode 100644 index 000000000..c460d23bf --- /dev/null +++ b/TBF/Rig/Network/Camera/CJMS11/POJO/SendImagesRequest.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace TBF.Rig.Network.Camera.CJMS11.POJO +{ + public class SendImagesRequest + { + [JsonProperty("count")] + public int Count { get; set; } + + [JsonProperty("delay")] + public int Delay { get; set; } + + [JsonProperty("taskId")] + public string TaskId { get; set; } + } +} \ No newline at end of file diff --git a/TBF/Rig/Network/Camera/CJMS11/POJO/SendSummaryPayload.cs b/TBF/Rig/Network/Camera/CJMS11/POJO/SendSummaryPayload.cs new file mode 100644 index 000000000..47cdd0927 --- /dev/null +++ b/TBF/Rig/Network/Camera/CJMS11/POJO/SendSummaryPayload.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace TBF.Rig.Network.Camera.CJMS11.POJO +{ + public class SendSummaryPayload + { + [JsonProperty("delay")] + public int Delay { get; set; } + + [JsonProperty("pic_count")] + public int PictureCount { get; set; } + + [JsonProperty("timeStamp")] + public string TimeStamp { get; set; } + } +} \ No newline at end of file diff --git a/TBF/Rig/Network/Camera/CJMS11/POJO/TimedCameraImage.cs b/TBF/Rig/Network/Camera/CJMS11/POJO/TimedCameraImage.cs new file mode 100644 index 000000000..35529d7a1 --- /dev/null +++ b/TBF/Rig/Network/Camera/CJMS11/POJO/TimedCameraImage.cs @@ -0,0 +1,20 @@ +using System; +using System.Drawing; + +namespace TBF.Rig.Network.Camera.CJMS11.POJO +{ + public class TimedCameraImage + { + public int Index { get; set; } + + public string TaskId { get; set; } + + public DateTimeOffset? FrameTime { get; set; } + + public long FrameTimeMillis { get; set; } + + public int FrameGap { get; set; } + + public Image Image { get; set; } + } +} \ No newline at end of file diff --git a/TBF/Rig/Network/Camera/RoiForFixedStartCJMS11/ProcedureParams.cs b/TBF/Rig/Network/Camera/RoiForFixedStartCJMS11/ProcedureParams.cs index 5b0226d14..9cfe9965a 100644 --- a/TBF/Rig/Network/Camera/RoiForFixedStartCJMS11/ProcedureParams.cs +++ b/TBF/Rig/Network/Camera/RoiForFixedStartCJMS11/ProcedureParams.cs @@ -27,6 +27,12 @@ namespace TBF.Rig.Network.Camera.RoiForFixedStartCJMS11 /// /// delta time for send parameter 500ms is minimum public int RoiDeltaTimeMs; + + /// + /// count of picture for send > 1 / grabImage == 0|1 parameter - switch method + /// + /// count of picture for send > 1 / grabImage == 0|1 parameter - switch method + public int RoiPicCount; string[] paramNames = new string[] { @@ -35,11 +41,7 @@ namespace TBF.Rig.Network.Camera.RoiForFixedStartCJMS11 Strings.DelaySendImage, }; - /// - /// count of picture for send > 1 / grabImage == 0|1 parameter - switch method - /// - /// count of picture for send > 1 / grabImage == 0|1 parameter - switch method - public int RoiPicCount; + public override void InitializeAll() { @@ -68,8 +70,8 @@ namespace TBF.Rig.Network.Camera.RoiForFixedStartCJMS11 switch (i) { case 0: PulsesPerLtr = Utils.ParseUDouble(strValue); return CfgUpdateFlags.None; - case 1: PulsesPerLtr = Int32.Parse(strValue); return CfgUpdateFlags.None; - case 2: PulsesPerLtr = Int32.Parse(strValue); return CfgUpdateFlags.None; + case 1: RoiPicCount = Int32.Parse(strValue); return CfgUpdateFlags.None; + case 2: RoiDeltaTimeMs = Int32.Parse(strValue); return CfgUpdateFlags.None; default: return CfgUpdateFlags.None; } } @@ -87,21 +89,19 @@ namespace TBF.Rig.Network.Camera.RoiForFixedStartCJMS11 if (Utils.TryParseUDouble(strValue, out dummy)) return true; break; case 1: - if (Int32.TryParse(strValue, out dummyInt)) return true; - if (dummyInt < 0 || dummyInt < 50) + if (!Int32.TryParse(strValue, out dummyInt) || (dummyInt < 0 || dummyInt > 50)) { message = ParamName(i) + " is invalid. Param must be between 0 and 50!"; return false; } - break; + return true; case 2: - if (Int32.TryParse(strValue, out dummyInt)) return true; - if (dummyInt < 500 || dummyInt < 5500) + if (!Int32.TryParse(strValue, out dummyInt) || (dummyInt < 500 || dummyInt < 5500)) { message = ParamName(i) + " is invalid. Param must be between 500 and 5500!"; return false; } - break; + return true; default: message = "Invalid index"; return false; diff --git a/TBF/Rig/Network/Camera/RoiForFixedStartCJMS11/Roi.cs b/TBF/Rig/Network/Camera/RoiForFixedStartCJMS11/Roi.cs index 6c29ec345..c28c43730 100644 --- a/TBF/Rig/Network/Camera/RoiForFixedStartCJMS11/Roi.cs +++ b/TBF/Rig/Network/Camera/RoiForFixedStartCJMS11/Roi.cs @@ -3,6 +3,7 @@ /// using System; using System.Collections.Generic; +using System.IO; using System.Text; using log4net; using Common; @@ -41,6 +42,8 @@ namespace TBF.Rig.Network.Camera.RoiForFixedStartCJMS11 get { return roiCfg.ProcParams.PulsesPerLtr; } set { roiCfg.ProcParams.PulsesPerLtr = value; } } + public int RoiDeltaTimeMs { get { return roiCfg.ProcParams.RoiDeltaTimeMs; } } + public int RoiPicCount { get { return roiCfg.ProcParams.RoiPicCount; } } public double LtrsPerPulse { get { return (PulsesPerLtr <= float.Epsilon) ? 1.0 : (1 / PulsesPerLtr); } } public string QuantityUnits { get; set; } @@ -123,26 +126,65 @@ namespace TBF.Rig.Network.Camera.RoiForFixedStartCJMS11 return GrabImageOp(imageFileName, roiCfg.BlackAndWhite, roiCfg.LowResolution, roiCfg.ImageRotation); } - public IOperation GrabImageOp(string imageFileName, - bool blackAndWhite, bool lowResolution, ImageRotation imageRotation) + // Roi.cs + public IOperation GrabImageOp( + string imageFileName, + bool blackAndWhite, + bool lowResolution, + ImageRotation imageRotation) { - if (NetCamera != null) + if (NetCamera == null) { - ///TODO BUMI - implementing grabing multiple images - /// 1. Create a list of image file names - string[] imageNames = new string[roiCfg.ImageOpacity.Count]; - for (int i = 0; i < imageNames.Length; i++) - { - imageNames[i] = imageFileName + string.Format("{0}_{1}",imageNames.Length, i); - } - /// 2. implement count of images to grab - /// 3. implement grabing multiple images - - /// TODO: Implement support for sharing one camera by multiple ROI-s - return NetCamera.GrabImagesOp(new string[] { imageFileName }, blackAndWhite, lowResolution, imageRotation); - } - else return null; + } + + int imageCount = Math.Max(1, RoiPicCount); + + string[] imageNames = CreateImageFileNames( + imageFileName, + imageCount); + + return NetCamera.GrabImagesOp( + imageNames, + blackAndWhite, + lowResolution, + imageRotation); + } + + private static string[] CreateImageFileNames( + string originalFileName, + int imageCount) + { + if (imageCount <= 1) + { + return new[] { originalFileName }; + } + + string directory = Path.GetDirectoryName(originalFileName); + string baseName = Path.GetFileNameWithoutExtension(originalFileName); + string extension = Path.GetExtension(originalFileName); + string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss_fff"); + + var result = new string[imageCount]; + + // Prvý obrázok zostáva kompatibilný s dialógom. + result[0] = originalFileName; + + for (int i = 1; i < imageCount; i++) + { + string fileName = string.Format( + "{0}_{1}_{2:D2}{3}", + baseName, + timestamp, + i + 1, + extension); + + result[i] = string.IsNullOrEmpty(directory) + ? fileName + : Path.Combine(directory, fileName); + } + + return result; } public IValve GetValve() diff --git a/TBF/Rig/Network/Telnet/EventArgsClasses.cs b/TBF/Rig/Network/Telnet/EventArgsClasses.cs index 8d156e092..4a1f82aee 100644 --- a/TBF/Rig/Network/Telnet/EventArgsClasses.cs +++ b/TBF/Rig/Network/Telnet/EventArgsClasses.cs @@ -94,17 +94,24 @@ namespace TBF.Rig.Network.Telnet public class PromptReceivedImagesEventArgs : EventArgs { - public Image image; /// server image. - public int idxCamera; /// camera index. - public int idxImage; /// image index. - public int imageCounts; /// number of images. - - public PromptReceivedImagesEventArgs(int idxCamera, Image image, int idxImage, int imageCounts) + public int idxCamera; + public int idxImage; + public int imageCount; + public Image image; + public DateTimeOffset? frameTime; + + public PromptReceivedImagesEventArgs( + int idxCamera, + int idxImage, + int imageCount, + Image image, + DateTimeOffset? frameTime) { - this.image = image; this.idxCamera = idxCamera; this.idxImage = idxImage; - this.imageCounts = imageCounts; + this.imageCount = imageCount; + this.image = image; + this.frameTime = frameTime; } } diff --git a/TBF/TBF.csproj b/TBF/TBF.csproj index 032b98f26..3a996a651 100644 --- a/TBF/TBF.csproj +++ b/TBF/TBF.csproj @@ -1030,6 +1030,10 @@ + + + + Form