Compare commits

..
Author SHA1 Message Date
michal 23235626ba chore(build): bump AssemblyVersion and AssemblyFileVersion to 3.9.3159.0 2026-10-06 21:28:33 +02:00
michal 9444959efe feat(asic): add experimental optical processing for ASIC readers with configurable batch handling, UI enhancements, and extensive test coverage 2026-10-06 17:52:33 +02:00
michal 7a10b9f4b6 feat(asic): implement FlyingEarlyCapture for ASIC readers, adding forward optical measurement with configuration options, updates to UI, and full test coverage 2026-10-06 13:18:15 +02:00
michal d440dbcc58 feat(asic): introduce FlyingDrain functionality for ASIC readers, including enhanced measurement handling, new configuration options, and supporting test coverage 2026-10-06 12:25:11 +02:00
michal ae787622d2 feat(build): add picture loading, validation, and error handling; introduce CopyReleasePictures.ps1 for release asset management 2026-10-06 10:00:31 +02:00
michal d248630ddc fix(build): exclude NLog.dll and NLog.xml from common package files; add validation for NLog runtime version in TBF output 2026-10-05 16:54:33 +02:00
michal b15d0a0a08 feat<fix>: add RegisterReaderGroupScheduler and ManualCommunicationGuard to TBF project files 2026-10-05 16:52:56 +02:00
michal 683ca25aaf feat<improvement>: improve COM port lifecycle handling and manual communication guard implementation across ASIC and ALLY readers 2026-10-05 16:52:25 +02:00
michal d3cc50b6e8 <fix> Add NLog binding redirect to App.config for version consistency 2026-10-05 16:49:10 +02:00
michal 8c3c866eff <FIX> GCI: remove unused NLog.Fluent reference from InterfaceGCIToLaatzen.cs 2026-10-05 16:48:30 +02:00
michal 78907de455 Merge updated TBF into ALLY_update while preserving ASIC/ALLY workflows 2026-10-05 10:58:44 +02:00
michal 500adfa789 Merge remote-tracking branch 'private-origin/develop/SLM-PT50_genesisDirectDecode_ALLY' into develop/SLM-PT50_genesisDirectDecode_ALLY 2026-10-05 10:01:06 +02:00
michal 9658fc28bd feat(ally): introduce "additional common parameters" support with comprehensive tests, diagnostics, and enhanced logging 2026-10-05 10:00:54 +02:00
michal 42ad98ab1f feat(ally): introduce "additional common parameters" support with comprehensive tests, diagnostics, and enhanced logging 2026-10-05 10:00:30 +02:00
michal bd0a876b5b <Fix> Merge : Remove unused resource strings and replace project references with DLL references for CordonelPreadjustmentUi. 2026-10-05 09:57:00 +02:00
marekf 806cf0d670 <Merge-fix>: Merge develop/Local-master into develop/SLM-PT50_genesisDirectDecode_mirror, increase revision to 3.9.3147.112
Cause:

- develop/SLM-PT50_genesisDirectDecode_mirror lacked the latest changes from develop/Local-master.
- The target branch needed the updated results workflow, configuration persistence, diagnostics and packaged dependencies.

Solution:

1. Merge source branch into target branch
   - Merged develop/Local-master into develop/SLM-PT50_genesisDirectDecode_mirror.
   - Integrate the source branch changes into the target branch while retaining both parent histories.

2. Integrate application and dependency updates
   - Include the ResultsWorkspace and per-meter ResultsWriter export updates, persistent Previous results component settings and ASIC optical logging improvements.
   - Include tracked packages/Common files and runtime library updates.
   - Preserve the existing merge contents; this amendment changes only the commit message and application revision.

3. Regression verification
   - Verified the source and target branch names against the merge commit and its parent history.
   - Verified that AssemblyVersion and AssemblyFileVersion both use 3.9.3147.112 and that the revision diff passes whitespace checks.
   - No regression tests were added or run for this amendment; application build and merged runtime behavior remain unverified.

4. Increased revision
   - Updated AssemblyVersion and AssemblyFileVersion from 3.9.3147.111 to 3.9.3147.112.
2026-10-02 11:50:57 +02:00
marekf 82a51293b9 <Update>: Track Common package dependencies and refresh runtime libraries, increase revision to 3.9.3147.111
Cause:

- The /packages ignore rule excluded new files under packages/Common.
- Required local dependencies and supporting files were missing from version control, making them unavailable in fresh checkouts.

Solution:

1. Track Common package files
   - Replace /packages with /packages/* and add an exception for /packages/Common/.
   - Add existing Common binaries, symbols, configuration, documentation and source files to version control.

2. Refresh runtime libraries
   - Include the staged ERegister data package and MagFlux/eRegister streaming protocol DLL and PDB updates.
   - Preserve ignoring of other package directories.

3. Regression verification
   - Verified that packages/Common allows new files while other package directories remain ignored.
   - Verified that no non-ignored Common files remain untracked.
   - No automated regression tests were added; application build and runtime tests were not run.
   - Whitespace checks reported existing trailing whitespace in imported package files; those files were preserved as supplied.

4. Increased revision
   - Updated AssemblyVersion and AssemblyFileVersion from 3.9.3147.110 to 3.9.3147.111.
2026-10-02 11:44:02 +02:00
michal 4cd7867e04 <FIX> otho standing/flying ASIC/ALLY, Com ports loks
`chore: remove IperlHead.cs as part of codebase cleanup`
2026-10-02 11:43:28 +02:00
michal 6560278568 feat(ally): add C1 measurement validation, default scaling, and enhanced error handling with extended test coverage 2026-09-30 21:39:39 +02:00
michal 08a233b2c5 feat(ally): add C1 measurement scaling, update C2 parsing, expand unit tests, and implement configuration enhancements 2026-09-30 10:46:31 +02:00
michal 7aa92d4806 feat(ally): add comprehensive unit tests for Ally optical payloads and protocols, implement new optical codec classes 2026-09-30 07:36:49 +02:00
michal 468bcf786e feat(optical-diagnostics): integrate OpticalTestTrace across test sequences and readers, add diagnostic tracing and enhanced logging 2026-09-29 20:32:34 +02:00
michal 21324985db feat(asic, poseidon): add optical diagnostic logging, enhanced debug controls, and ASIC/Poseidon configuration updates 2026-09-29 12:38:57 +02:00
marekf 4529602d97 <Feature>: Add per-meter export view to ResultsWriter, increase revision to 3.9.3147.110
Cause:

- ResultsWriter exported batch results primarily as one row per meter test.
- File and database Insert outputs also needed one row per meter with selected meter-level values.

Solution:

1. Added configurable row granularity
   - Add the "One row per" selector with Meter test and Meter options.
   - Preserve independent column selections for both views.
   - Keep the existing view as the default for older configurations.

2. Added meter-level variable selection
   - Define meter-view availability in WMeterRsltItemSpec.
   - Filter available variables and hide TestID in the meter view.
   - Reject unsupported mappings before writing output.

3. Extended Insert exports
   - Write one row per enabled meter without a test context.
   - Preserve existing test-level export behavior.
   - Leave XML export and MS SQL stored procedure processing unchanged.

4. Added regression coverage
   - Verify CSV and XLSX row counts, disabled meters and batches without tests.
   - Verify configuration compatibility and selector visibility.
   - Align the test project's System.Memory dependency with the application.
   - All 11 regression tests passed.

5. Increased revision
   - Updated AssemblyVersion and AssemblyFileVersion from 3.9.3147.109 to 3.9.3147.110.
2026-09-28 19:35:50 +02:00
marekf bce5af56e0 <Fix>: Persist component settings edited from Previous results, increase revision to 3.9.3147.109
Cause:

- Edit writer and Edit printer changed only the in-memory configuration.
- Confirmed settings were not saved to the database and were lost after restarting the application.

Solution:

1. Added persistent configuration saving
   - Save confirmed changes to the configuration database in a transaction.
   - Preserve component identity, ordering and associations.
   - Roll back failed saves and display an error.

2. Isolated configuration editing
   - Load the latest saved configuration when opening the editor.
   - Keep cancelled edits separate from the running component.
   - Apply saved settings after restarting the application.

3. Added regression coverage
   - Verify saved settings survive reopening the database session.
   - Verify rollback without commit when saving fails.

4. Increased revision
   - Updated AssemblyVersion and AssemblyFileVersion from 3.9.3147.108 to 3.9.3147.109.
2026-09-28 13:23:10 +02:00
marekf f8643c4de1 <Fix>: Improve ASIC optical communication logging, increase revision to 3.9.3147.108
Cause:

- ANSI iPerl optical communication needed more detailed diagnostics to investigate communication and parsing failures.
- Optical logs needed batch, test and meter context for troubleshooting.

Solution:

1. Added dedicated optical logging
   - Added AnsiIPerlOptical logger and rolling file appender to the sample configuration.
   - Configured optical diagnostics output to C:\TBF\Logs\AnsiIPerlOptical.txt.

2. Improved communication diagnostics
   - Added session and test markers with batch, meter and repetition details.
   - Added logging of raw HEX and ASCII telegrams, parsed samples and parsing failures.
   - Added logging of port opening errors, read errors and timeouts, including manual reads.

3. Increased revision
   - Updated AssemblyVersion and AssemblyFileVersion from 3.9.3147.107 to 3.9.3147.108.
2026-09-28 10:54:15 +02:00
michal 161b20fea7 fix ally in testing + new head
feat(ally): add extensive integration tests, factory seal unseal verification, baud rate discovery, and optical mode enhancements
2026-09-26 14:37:45 +02:00
marekf 882e894fba Merge branch 'feature/task/Universal-feature-UI-button-Show' into ' develop/Local-master' 2026-09-25 09:24:19 +02:00
marekf 79c87bedfd <Feature>: Add ResultsWorkspace for batch results, increase revision to 3.9.3147.107
Cause:

- Batch results needed a more flexible workspace for reviewing and comparing multiple water meters.
- Presentation changes had to remain independent of stored results and legacy view settings.

Solution:

1. Added an independent results workspace
   - Open ResultsWorkspace from Previous Results.
   - Provide Tables overview, Graphs overview and Compare selected tabs.
   - Retain access to the original results view.
   - Preserve serial-number filtering and exclude disabled positions.

2. Improved meter selection and layout
   - Support Ctrl+click selection, Shift+click ranges and All/None actions.
   - Synchronize selection between the meter list, tables and graphs.
   - Add meter reordering, selected-only filtering and independent child windows.
   - Support automatic or explicit grid layouts and configurable visible rows.

3. Improved scrolling and table configuration
   - Add optional linked horizontal and vertical table scrolling.
   - Route the mouse wheel to the overview and Ctrl+wheel to the hovered table.
   - Keep all meters reachable after maximizing and restoring the window.
   - Isolate result fields and column widths from legacy settings and other windows.

4. Added comparison and reusable views
   - Compare selected meters by exact test name.
   - Mark missing results and ambiguous repeated test names.
   - Display relative-error graphs against flow.
   - Save and load XML presentation presets without storing or modifying result data.

5. Added regression coverage
   - Cover filtering, selection, comparison, layout, scrolling and graph rendering.
   - Verify independent window settings and empty-data handling.

6. Increased revision
   - Set AssemblyVersion and AssemblyFileVersion to 3.9.3147.107.
2026-09-25 09:22:16 +02:00
marekf b1324c567c <Feature>: Add universal Schematic drawing editor, increase revision to 3.9.3147.106
Cause:

- The existing schematic editor required time-consuming manual operations.
- Drawing improvements had to preserve compatibility with existing components, pipes and stored schematics.

Solution:

1. Added an alternative schematic editor
   - Keep the original editor available.
   - Preserve the existing database representation and legacy schematic compatibility.
   - Stage drawing changes until Save and support Undo/Redo.

2. Improved canvas interaction
   - Add zoom, panning, automatic fit, multiple selection and canvas search.
   - Add context menus, keyboard shortcuts and component configuration access.
   - Support copying drawing symbols with their internal connections.

3. Added interactive pipe editing
   - Reconnect pipes using endpoint handles.
   - Create bends by dragging selected pipe segments.
   - Stage junctions as J<n>fict and assign final names when saving.
   - Remove straightened automatic bends while preserving manual junctions and branches.
   - Add the Add junction quick action.

4. Added JSON drawing export and import
   - Export drawing geometry and connections without operating settings.
   - Match existing components by name and type.
   - Validate imports and support restoring the previous drawing through Undo.

5. Reorganized the editor layout
   - Move save and close actions into File.
   - Highlight Unlock editing in green until editing is unlocked.
   - Place canvas controls above the bordered canvas.
   - Add a right-side object panel with directional movement and transformation controls.
   - Add optional component types and adaptive palette width.
   - Fit the drawing after maximizing or restoring the window.

6. Increased revision
   - Updated AssemblyVersion and AssemblyFileVersion from 3.9.3147.105 to 3.9.3147.106.
2026-09-25 09:17:49 +02:00
marekf d2627be55b <Fix>: Handle missing pipes components in schematic editor, increase revision to 3.9.3147.105
Cause:

- The schematic editor required Pipes components for all four sizes: S, M, L and XL.
- If any size was missing, existing pipes were not loaded and new connections for the missing size were ignored.

Solution:

1. Initialize missing pipes configurations
   - Prepare missing sizes in memory with empty pipe lists.
   - Generate unique component names and append component ordering.
   - Save new components only when confirming the editor with OK.

2. Preserve existing schematic connections
   - Load pipes even when some size components are missing.
   - Normalize null pipe lists to empty arrays.

3. Increased revision
   - Updated AssemblyVersion and AssemblyFileVersion from 3.9.3147.104 to 3.9.3147.105.
2026-09-25 09:16:43 +02:00
marekf 2ba6092385 Merge branch 'bugfix/Mexico-NOKvsTestPassed()' into ' develop/Local-master' 2026-09-24 15:06:23 +02:00
marekf fee0651595 <Update>: Version to 3.9.3147.1 in AssemblyInfo files 2026-09-24 15:03:38 +02:00
michal 58fc16d70b Fix point 4. - take % validation from procedure to Q3 Calib factor CH1-3 verification
Implement error limit configuration for Q3 calibration factors.

- Add default and configurable error limits for Q3 calibration validation.
- Introduce `CalculateQ3CalibrationWithErrorLimits` for flexible validation handling.
- Overhaul Q3 workflow to respect configured limits, with fallback to defaults.
- Extend tests for calibration, including boundary and invalid inputs.
2026-09-24 14:53:49 +02:00
marekf ddf8996f01 <Fix>: Fit Previous Results columns to content, increase revision to 3.9.3147.104
Cause:

- Fixed column widths left unnecessary empty space and did not adapt to displayed values.
- Action buttons required sizing that accounts for captions, icons and padding.

Solution:

1. Added content-based column sizing
   - Fit Batch, Start time, End time, SW version, OK and NOK to headers and formatted values on the current page.
   - Recalculate widths when the grid is created or the displayed page changes.
   - Preserve manual resizing between page changes.

2. Adjusted action button widths
   - Fit OK/NOK diag, Write, the mode-dependent action and Show to their captions.
   - Include icon width and button padding.
   - Keep Procedure and Purchase order filling the remaining space.

3. Increased revision
   - Updated AssemblyVersion and AssemblyFileVersion from 3.9.3147.103 to 3.9.3147.104.
2026-09-24 11:01:56 +02:00
marekf 0c2cc0dad2 <Feat>: Add batch OK/NOK diagnostics, increase revision to 3.9.3147.103
Cause:

- Previous Results displayed overall OK/NOK counts without explaining why a meter failed.
- Individual tests could show TestPassed() = Yes while the overall verdict was NOK due to other failed or incomplete tests or error flags.
- Stored verdicts could not be easily compared with evaluation of saved test results.

Solution:

1. Added batch OK/NOK diagnostics
   - Added an "OK/NOK diag" action to Previous Results.
   - Displayed stored and recalculated verdicts for each meter and highlighted mismatches.
   - Identified failed or incomplete evaluated tests and contributing error flags.

2. Added detailed diagnostic reports
   - Included test names, repetitions, completion states, measurements, limits and timestamps.
   - Distinguished error flags from information flags that do not cause NOK.
   - Added copying of the full report.
   - Kept diagnostics read-only without changing stored results or evaluation rules.

3. Added unit tests
   - Covered incomplete tests, error flags, excluded tests and verdict mismatches.
   - Covered failed repetitions and missing test definitions.

4. Increased revision
   - Updated AssemblyVersion and AssemblyFileVersion from 3.9.3147.102 to 3.9.3147.103.
2026-09-24 09:31:18 +02:00
marekf 1e0c65f213 <Fix>: Improve crash diagnostics, increase revision to 3.9.3147.102
Cause:

- The Out of memory error occurred while creating the WinForms error dialog, potentially masking the original exception.
- Crash logs lacked resource usage details needed to investigate memory or graphics resource exhaustion.

Solution:

1. Improved exception logging
   - Added first-chance logging for memory and native exceptions.
   - Included full exception details, HRESULT, native error code and termination status.
   - Protected logging against recursion and diagnostic failures while preserving existing exception handling.

2. Added resource diagnostics
   - Logged process memory usage, handle and thread counts.
   - Recorded current and peak GDI and USER object counts.
   - Included application version, thread ID and process architecture.

3. Increased revision
   - Updated AssemblyVersion and AssemblyFileVersion from 3.9.3147.101 to 3.9.3147.102.
2026-09-24 08:31:30 +02:00
marekf c53a2d9078 Merge branch 'bugfix/SLM-PT150-9-2026-RR-units-FSMC-QuantMt-empty' into ' develop/Local-master' 2026-09-23 12:20:24 +02:00
marekf 6223bc925f <Update>: Version to 3.9.3147.0 in AssemblyInfo files 2026-09-23 12:18:13 +02:00
marekf aca1d79023 Merge branch 'feature/task/Universal-feature-styled-Previous-result' into feature/task/UnionTown-Cordonel-completion-at-the-customer-milestone 2026-09-23 12:15:46 +02:00
marekf 45d12dea6f <Fix>: Fix FSMC mass meter test results and preserve final master pulse count, increase revision to 3.9.3147.101
Cause:

- The calculated mass meter quantity was not assigned to meterRslt.MassMeter, leaving Quant Mt empty after the test ended.
- tstRslt.PulsesMaster was read directly from cBrd.RefPulses during final result processing, when the value could already be reset or changed.

Solution:

1. Fixed mass meter result calculation
   - Calculated meterRslt.MassMeter from meterRslt.PulsesMeter and regReader.LtrsPerPulse.
   - Ensured that Quant Mt is available in the final test results.

2. Preserved the final master pulse count
   - Captured cBrd.RefPulses in finalPulsesCount during the measurement loop.
   - Used the preserved value when assigning tstRslt.PulsesMaster.

3. Increased revision
   - Updated revision to 3.9.3147.101.
2026-09-22 15:06:24 +02:00
marekf 2cee6cb0f2 <Feat>: Replace Previous Results layout with DataGridView and add procedure access, increase revision to 3.9.3146.122
Cause:

- The custom row controls caused slow resizing and repainting.
- Column resizing required manual synchronization between rows and headers.
- Test procedures could not be opened directly from previous results.

Solution:

1. Replaced the custom results layout
   - Added a double-buffered DataGridView with native column resizing and scrollbars.
   - Removed the resize timer and per-row control layout.
   - Added full-row selection and automatic row heights to fill the available space.
   - Deferred row-height updates until fill-column resizing completes.
   - Centered the window on screen and added bottom spacing.

2. Improved table appearance
   - Centered columns except Procedure and Purchase order.
   - Highlighted positive OK/NOK counts with green/red backgrounds and white text.
   - Kept zero counts neutral.
   - Strengthened the selected-row background.
   - Added gray Write/Print buttons and a blue Show button with embedded PNG icons.
   - Restricted hover highlighting to the actual button area.

3. Added direct procedure access
   - Made procedure names clickable.
   - Loaded the exact procedure name and revision recorded in the batch.
   - Opened active revisions in locked editing mode with existing authorization.
   - Saved accepted changes as a new revision and archived the original.
   - Opened historical revisions as read-only.
   - Reported missing or ambiguous procedure matches.

4. Preserved existing behavior and compatibility
   - Retained filters, paging, printing, writing, result viewing and build-specific actions.
   - Used FlushMode.Never with explicit Flush() for the existing NHibernate version.
   - Corrected embedded-resource naming and image disposal.
   - Added English code documentation and cleaned up source formatting.

5. Increased revision
   - Updated revision to 3.9.3146.122
2026-09-22 12:19:50 +02:00
michal e9cda221bb feat(smart-comm): enhance logging, add family selection mask handling, and improve close behavior 2026-09-21 19:23:22 +02:00
michal c6181a76ec feat(asic): implement ASIC reader and test method integration with legacy compatibility, add detailed logging and new unit tests 2026-09-21 14:08:02 +02:00
marekf cd24373c80 <Fix>: Restore volume and mass unit selection in register reader procedures, increase revision to 3.9.3147.100
Cause:

* The Units combobox in StandingStartStop and PulsesFromUniCB filtered units by MultiFunctionalVariables, leaving volume and mass units unavailable.
* StandingStartStop.QuantityUnits was not connected to the unit stored in procedure parameters.

Solution:

1. Updated unit selection

   * Replaced the MultiFunctionalVariables filter with Volume and Mass filters in both components.
   * Preserved the default "---" option.

2. Connected runtime units to procedure parameters

   * Updated StandingStartStop.QuantityUnits to read and write ProcParams.Units.
   * Aligned unit handling with PulsesFromUniCB.

3. Increased revision

   * Updated revision to 3.9.3147.100.
2026-09-18 13:47:15 +02:00
marekf a464b1fc21 <Feat>: Add per-meter simulation outcomes and retry scenarios, increase revision to 3.9.3146.121
Cause:

- Simulation Success applied the same result to every meter in a test.
- Mixed failures and successful retries could not be reproduced reliably.

Solution:

1. Added per-slot simulation configuration
   - Add the Simulation slot results procedure parameter.
   - Support sequences such as 4=Fail,Pass; 7=Fail,Fail,Pass.
   - Preserve existing defaults for unspecified slots.
   - Repeat the last configured outcome when its sequence is exhausted.

2. Integrated outcomes with existing retries
   - Track outcomes independently for each slot and operation.
   - Reset counters for each new test invocation.
   - Retry only failed slots within the configured retry count.
   - Preserve previously successful slot results and selection.

3. Improved result reporting
   - Use individual slot outcomes for phase status and TBF results.
   - Log the configured outcome and its source.
   - Prevent stale outcomes from affecting later tests.

4. Preserved physical execution
   - Keep measurement algorithms, timing, parallel execution and port handling unchanged.

5. Increased revision
   - Update revision to 3.9.3146.121.
2026-09-18 13:44:03 +02:00
michal b8f6af7112 Fix point 4. - take % validation from procedure to Q3 Calib factor CH1-3 verification
Implement error limit configuration for Q3 calibration factors.

- Add default and configurable error limits for Q3 calibration validation.
- Introduce `CalculateQ3CalibrationWithErrorLimits` for flexible validation handling.
- Overhaul Q3 workflow to respect configured limits, with fallback to defaults.
- Extend tests for calibration, including boundary and invalid inputs.
2026-09-16 10:23:04 +02:00
michal 1c8d01562f Ally meter integration to test method and unit tests / integration tests supports Ally communication improving
feat(ally): add detailed logging, calibration flow updates, and test communication sequence implementation
2026-09-16 09:30:50 +02:00
marekf 2608ee4de6 Merge branch 'feature/task/UnionTown-Cordonel-completion-at-the-customer-last-task-11' into feature/task/UnionTown-Cordonel-completion-at-the-customer-milestone 2026-09-15 20:25:58 +02:00
marekf e63f706f41 <Fix>: Handle scale underload before mass measurement, increase revision to 3.9.3146.120
Cause:

- Starting measurement overwrote Underload with Busy.
- IND400 then returned S I, causing repeated measurements without zeroing.

Solution:

1. Check underload before measurement in both Mettler Toledo implementations.
2. Zero the scale first and resume measurement after successful zeroing.
3. Return an error if zeroing fails or times out.
4. Preserve existing SICS and ID1 commands and response handling.
5. Increased revision to 3.9.3146.120.
2026-09-15 20:25:08 +02:00
marekf 94cd07cd3c <Fix>: Add volume and mass units for multifunctional Results variables, increase revision to 3.9.3146.119
Cause:

- The units combobox offered no physical units for MultiFunctionalVariables, including Quant. Mt. and Quant. rm.

Solution:

1. Extended unit selection
   - Include both volume and mass units for MultiFunctionalVariables.
   - Preserve existing result formatting and unit conversion.

2. Increased revision
   - Update revision to 3.9.3146.119.
2026-09-15 19:15:01 +02:00
marekf 967ca817ba Merge branch 'feature/task/UnionTown-Cordonel-completion-at-the-customer-last-task-2-3' into feature/task/UnionTown-Cordonel-completion-at-the-customer-milestone 2026-09-15 18:43:52 +02:00
marekf 979637cc07 <Fix>: Correct initial filling behavior for Start Cycle and Start Test, increase revision to 3.9.3146.118
Cause:

- Start Cycle bypassed the configured Purge Begin transition.
- Start Test prompted for filling only when FillState was Full, skipping confirmation after application startup when the state was Unknown.

Solution:

1. Restored automatic cycle filling
   - Remove Selection.Cycle from the filling bypass condition.
   - Execute the configured Purge Begin transition on initial cycle startup without confirmation.

2. Corrected Start Test confirmation
   - Remove the FillState.Full condition.
   - Ask whether to fill on initial Start Test regardless of the recorded fill state.
   - Preserve Yes/No and STOP handling.

3. Preserved existing restore behavior
   - Keep filling confirmation for restored batches.
   - Keep the filling bypass for restored interrupted sessions.

4. Increased revision
   - Update revision to 3.9.3146.118.
2026-09-15 18:42:53 +02:00
marekf 5c7ff08a78 <Fix>: Remove blocking UI access from preadjustment retries, fixed last-task-9 increase revision to 3.9.3146.117
Cause:

- Retry preparation called SetChecked(true), introducing synchronous UI access before the next phase attempt.
- This could stall retries and delay operator STOP.

Solution:

1. Fixed retry preparation
   - Restore Failed, Ok and IsEnabled directly without updating the checkbox.
   - Check cancellation while preparing slots for the next attempt.
   - Preserve existing phase execution, parallel processing and streaming port ownership.

2. Increased revision
   - Update revision to 3.9.3146.117.

Validation:

- Confirmed working retry behavior on the test bench.
2026-09-15 16:07:20 +02:00
marekf a64d1a82ed <Fix>: Enable editing of water meter error limits, increase revision to 3.9.3146.116
Cause:

- ErrLimHiQ and ErrLimLoQ were missing from parameter validation and update handling.
- Editing either limit produced an Invalid index message.

Solution:

1. Fixed error limit editing
   - Add validation for parameter indices 10 and 11.
   - Assign edited values to ErrLimHiQ and ErrLimLoQ.
   - Use the existing non-negative numeric parsing.

2. Increased revision
   - Update revision to 3.9.3146.116.
2026-09-15 02:36:44 +02:00
marekf bf1766f90a <Feat>: Add default purchase order without dialog, increase revision to 3.9.3146.115
Cause:

- Cycle start required manual purchase order confirmation even when no specific order was needed.

Solution:

1. Added default purchase order configuration
   - Add Default (without dialog) to the Orders selection.
   - Add a configurable default purchase order, initially DEFAULT.
   - Validate 1 to 10 characters without whitespace or control characters.

2. Added automatic order assignment
   - Assign the default order to all meters in the current batch.
   - Skip creating and displaying CycleBeginningForm.
   - Clear previous order details and complete through the existing operation flow.

3. Preserved existing behavior
   - Keep manual and Oracle order modes unchanged.
   - Preserve compatibility with existing configurations.

4. Increased revision
   - Update revision to 3.9.3146.115.
2026-09-15 02:27:03 +02:00
marekf 1b789ad5d5 Merge branch 'feature/task/UnionTown-Cordonel-completion-at-the-customer-last-task-2-3' into feature/task/UnionTown-Cordonel-completion-at-the-customer-milestone 2026-09-15 01:41:38 +02:00
marekf a7404d0e24 Merge branch 'feature/task/UnionTown-Cordonel-completion-at-the-customer-last-task-5' into feature/task/UnionTown-Cordonel-completion-at-the-customer-milestone 2026-09-15 01:39:58 +02:00
marekf a64327ac36 <Fix>: Adjust filling behavior and fix parallel flow meter configuration, increase revision to 3.9.3146.113
Cause:

- Start Cycle could prompt for filling before executing the procedure.
- Start Test bypassed the filling confirmation.
- Parallel flow meter configuration threw NotImplementedException during XML serialization of IsOffline.

Solution:

1. Updated Start Cycle
   - Proceed directly to the procedure, allowing its Purge Begin step to handle filling.

2. Updated Start Test
   - Ask whether to fill with water when FillState is Full.
   - Preserve the existing filling path for other fill states.
   - Keep restored-batch confirmation and STOP handling.

3. Fixed parallel flow meter configuration
   - Replace the unimplemented IsOffline accessors with an automatic property.
   - Preserve IsOffline when copying configuration.
   - Prevent the exception when opening component configuration.

4. Increased revision
   - Update revision to 3.9.3146.113.
2026-09-15 01:37:33 +02:00
marekf c886303458 <Fix>: Add US flow units for parallel flow meters, increase revision to 3.9.3146.113
Cause:

- Parallel flow meter configuration offered only m3/h and l/h.

Solution:

1. Extended display unit selection
   - Add gal/m to the available display units.

2. Increased revision
   - Update revision to 3.9.3146.113.

ToDo:

- Map gal/m to the corresponding US gallons per minute unit in UpdateParam.
2026-09-15 01:03:33 +02:00
marekf 38003592df <Fix>: Adjust filling behavior for Start Cycle and Start Test, increase revision to 3.9.3146.114
Cause:

- Start Cycle could prompt for filling before executing the procedure.
- Start Test bypassed the filling confirmation.

Solution:

1. Updated Start Cycle
   - Proceed directly to the procedure, allowing its Purge Begin step to handle filling.

2. Updated Start Test
   - Ask whether to fill with water when FillState is Full.
   - Preserve the existing filling path for other fill states.

3. Preserved existing controls
   - Keep restored-batch filling confirmation.
   - Keep STOP handling during the confirmation.

4. Increased revision
   - Update revision to 3.9.3146.114.
2026-09-15 01:01:30 +02:00
marekf 82c2b17a1f <Feat>: Add per-phase retries for failed preadjustment slots, increase revision 3.9.3146.112
Cause:

- Failed meters were excluded without retrying the current phase.
- The procedure Temperature column was redundant when using T1/T2 sensors.

Solution:

1. Added configurable retries
   - Replace the Temperature column with Retries on failure.
   - Interpret the value as additional attempts; default to zero.
   - Preserve legacy Temperature XML compatibility.

2. Added failed-slot retry execution
   - Retry only failed slots, in parallel, within the current phase.
   - Preserve successful slots and their captured results.
   - Restore the complete GCI batch after each attempt.
   - Exclude unsuccessful slots after retries are exhausted.

3. Improved attempt diagnostics
   - Log attempt numbers and outcomes to slot windows and files.
   - Keep successful slot tabs visible during retries.

4. Preserved execution behavior
   - Keep existing measurement algorithms, sensor readings and port handling.
   - Process STOP during retries and hold once after all attempts finish.
   - Prevent further retries after cancellation or streaming cleanup failure.

5. Increase revision 3.9.3146.112
2026-09-15 00:01:53 +02:00
marekf 2660c564a6 <Feat>: Expose Genesis preadjustment results in Results and improve result mapping
Cause:

- Preadjustment values were not available for persistent Results mapping.
- Older Genesis item IDs could resolve to different result variables.
- Meter-specific variables were difficult to find.

Solution:

1. Added preadjustment result storage
   - Store common execution metadata and available phase-specific values per slot and test.
   - Expose temperature comparison and Completion values in Results.
   - Preserve existing measurement execution, timing, parallelism and STOP handling.

2. Improved result selection
   - Add Water meter type under the existing Category tab.
   - Group available items into Common, Genesis and iPerl.

3. Corrected saved mappings
   - Migrate recognized legacy Genesis mappings.
   - Store stable item identifiers while preserving existing numeric IDs.
   - Mark ambiguous mappings as Review mapping.
   - Preserve the one-based Genesis repetition number.
2026-09-14 22:48:10 +02:00
marekf 7e2b4ed908 Merge branch 'feature/task/UnionTown-Cordonel-completion-at-the-customer-last-task-6' into feature/task/UnionTown-Cordonel-completion-at-the-customer-milestone 2026-09-14 19:31:10 +02:00
marekf c1960f2150 <Feat>: Add per-slot preadjustment file logs and diagnostic headers, increase revision to 3.9.3146.110
Cause:

- Preadjustment diagnostics required persistent log files for each slot.
- Phase boundaries and file storage outcomes were not clearly visible.

Solution:

1. Added per-slot file logging
   - Store logs under C:\Tbf\Logs\Preadjustment.
   - Create a separate directory for each batch and a separate file for each slot.
   - Capture phase diagnostics, Completion SQL messages and STOP requests.
   - Support normal execution and simulation independently of the diagnostic window.

2. Added phase formatting
   - Add batch and phase headers with slot and start time.
   - Include phase result and duration in file footers.
   - Display headers in the corresponding slot diagnostic window without duplicate file entries.

3. Added file storage diagnostics
   - Display the full log path and creation/write status per slot.
   - Confirm successful writes after closing the file.
   - Report write failures and incomplete logs.

4. Added background file writing
   - Buffer and write log entries without blocking serial reception.
   - Preserve parallel slot execution and existing calibration behaviour.

5. Increased revision
   - Updated revision from 3.9.3146.109 to 3.9.3146.110.
2026-09-14 19:00:39 +02:00
marekf 4124443f36 <Feat>: Add Completion and Q3 calibration database writes through GCI, increase revision to 3.9.3146.109
Cause:

- Preadjustment Completion and TBF Q3 calibration results required configurable database persistence.
- Both write operations required diagnostics and simulation support.

Solution:

1. Added independent database writer configurations
   - Added PreadjustmentCompletionWriter and Q3CalibrationWriter to gci_config.
   - Support SQL commands, stored procedures and parameter mappings.
   - Added SQL table creation and verification scripts.

2. Added Preadjustment Completion storage
   - Store PCB ID, meter size, amplitude settings, temperature offsets, zero-flow offsets, timestamp and overall result.
   - Use captured Completion readout values.
   - Preserve the original HTTP reporting.
   - Report SQL connection and write status in the corresponding slot diagnostics.

3. Added Q3 calibration storage
   - Forward TBF results through GCI Bridge to the GCI database API.
   - Store signed deviations and written calibration factors for all three paths.
   - Write to SQL after successful calibration register writes and StoreCalibration.
   - Include PCB ID and UTC timestamp.

4. Added simulation support
   - Generate sample Completion and Q3 records for simulated slots.
   - Perform real SQL writes when the corresponding writer is enabled.
   - Log connection progress, write outcomes and generated record IDs.

5. Separated calibration and database outcomes
   - Preserve meter operation results when database storage fails.
   - Expose database write status independently.

6. Increased revision
   - Updated revision from 3.9.3146.108 to 3.9.3146.109.

Validation:

- Confirmed successful SQL storage for simulated Completion.
- Confirmed successful Q3 calibration result storage.

ToDo:

- Use the GCI Q3 storage API as the foundation for future migration of Q3 calibration from the direct TBF communication branch to GCI.
2026-09-14 17:06:22 +02:00
marekf 6bab62a062 <Fix>: Integrate GCI and direct streaming workflows, restore parallel preadjustment execution and update revision to 3.9.3146.108
Cause:

- GCI streaming port ownership interfered with regular TBF measurements using direct streaming access.
- Streaming handoff recreated both connections and required unnecessary login attempts between phases.
- Sequential port acquisition delayed execution, while unavailable meters could prevent healthy slots from continuing.
- Repeated initialization retained exclusions from the previous attempt.

Solution:

1. Integrated two communication approaches
   - Enable coexistence of the GCI Bridge workflow, which uses request and streaming ports, and the TBF workflow, which accesses the streaming port directly.
   - Transfer streaming port ownership explicitly between both workflows.
   - Preserve the GCI request connection and authentication during streaming handoff.

2. Updated ExternalProjects - GenesisCore/GenesisMeter
   - Added ConnectStreamingPort and DisconnectStreamingPort.
   - Separate streaming port lifecycle from request connection lifecycle.
   - Allow streaming port ownership to move between both technological branches without reconnecting the request port or repeating login.
   - Preserve process configuration and release streaming resources on completion, cancellation or acquisition failure.

3. Updated ExternalProjects - CordonelPreadjustmentUi
   - Adapt ZeroFlowGenesisMeter data collection to streaming port handoff.
   - Support repeated StartRecordData calls without duplicate subscriptions.
   - Prevent duplicate record processing and repeated amplitude scaling.
   - Coordinate preadjustment data subscription startup and shutdown with streaming ownership.

4. Restored parallel slot handling
   - Acquire, check and release ports concurrently across slots.
   - Isolate unavailable or failed meters without excluding healthy slots.
   - Drain pending operations before cleanup on STOP.

5. Corrected repeated execution and results
   - Reset exclusions and result state on explicit preadjustment initialization.
   - Evaluate results separately for each slot.
   - Prevent empty phases from being reported as successful measurements.
   - Return the streaming port for subsequent non-GCI measurements.

6. Updated revision
   - Set revision to 3.9.3146.108.

Related dependency changes:

- This commit requires the corresponding changes in:
  - ExternalProjects - GenesisCore/GenesisMeter.
  - ExternalProjects - CordonelPreadjustmentUi.
- Build and deploy both updated assemblies together with the matching TBF/GCI changes.

Architecture milestone:

- Established functional integration of two communication architectures: GCI Bridge access to both ports and direct TBF access to the streaming port.
- This version, together with the corresponding ExternalProjects changes, provides the baseline for their future unification.

ToDo:

- Use this baseline to unify request and streaming communication behind the GCI interface.
- Migrate remaining direct streaming access to GCI while preserving parallel slot execution, cancellation and diagnostic logging.
- Centralize ownership and lifecycle management of both ports in GCI.
2026-09-12 00:23:37 +02:00
marekf c426df990c <Feat>: Add per-slot temperature calibration results, diagnostic hold and settling delay, increase revision to 3.9.3146.107
Cause:

- Temperature calibration results lacked measured meter temperatures and comparison details.
- Diagnostics closed immediately after a phase, preventing inspection of failures.
- Push Temperature remained visible when temperature was supplied directly from T1/T2.
- Automatic phase transitions provided insufficient settling time before temperature calibration, causing temperature checks to fail.

Solution:

1. Added structured temperature calibration results
   - Return reference temperature, meter temperature, absolute difference, effective limit and sample count per slot.
   - Distinguish temperature check status from overall calibration completion.
   - Preserve existing API signatures and add typed result methods.

2. Improved temperature diagnostics
   - Log sample count, invalid values, minimum, maximum and mean for each path.
   - Add bounded TEMP-RAW capture with raw temperature, scaling factor, decoded temperature, timestamps and frame content.
   - Attach diagnostic capture only during the temperature check and detach it afterwards, including on cancellation.
   - Preserve the original streaming handler used during Amplitude Test.
   - Keep the original comparison algorithm and temperature deviation limit.
   - Remove configurable deviation from the GCI Bridge component dialog.

3. Added optional hold after preadjustment
   - Add the Hold after preadjustment procedure parameter.
   - Keep completed results and diagnostics visible until Continue is pressed.
   - Process STOP during hold and exclude hold time from measurement duration.

4. Updated phase display
   - Hide Push Temperature in sensor mode unless that action is explicitly running.
   - Keep failed slots and their exclusion status visible during hold.

5. Corrected settling time
   - Remove initialization overrides that forced OffsetTestSettlingTime to zero.
   - Set OffsetTestSettlingTime to 20 seconds.
   - Apply settling before temperature calibration and during Offset Test.
   - Preserve STOP handling during the wait.
   - The existing inclusive countdown produces approximately 21 seconds of waiting.

6. Aligned project versions
   - Update TBF revision to 3.9.3146.107.
   - The corresponding Laatzen PreadjustmentUI project state matches this TBF revision exactly.
   - Build and deploy these matching project versions together.

Validation:

- Compared raw temperature frames from GCI GUI and GenesisCommunication.
- Confirmed that decoded Meter temperatures matched the raw frame values.
- Confirmed that adding a pause after Amplitude Test allowed temperature calibration to pass.
2026-09-11 19:05:17 +02:00
marekf 20614fb9e6 <Fix>: Add real-time slot progress logging to Offset Test, increase revision to 3.9.3146.106
Cause:

- Settling and data collection updated only StatusLabel, leaving slot diagnostic memos without progress messages.
- One error message had no slot assigned and was ignored by slot diagnostics.

Solution:

1. Added slot-specific progress logging
   - Log settling countdown every second.
   - Log collected record count every collection cycle.
   - Log completion of data collection.

2. Corrected error routing
   - Include slot and PCB ID in the bad-data warning.

3. Preserved test behaviour
   - Keep existing timing, calculations and validation unchanged.

4. Increased revision
   - Updated revision to 3.9.3146.106.
2026-09-11 03:49:31 +02:00
marekf 302431b64c <Fix>: Add skipped preadjustment phase state and improve diagnostic colors, increase revision to 3.9.3146.105
Cause:

- Optional preadjustment phases remained in the Pending state after a later phase had already started.
- Running and skipped phases required clearer visual distinction.

Solution:

1. Improved preadjustment phase states
   - Mark preceding unexecuted phases as Skipped when a later preadjustment phase starts.
   - Allow a skipped phase to transition normally to Running if it is executed later.

2. Improved diagnostic colors
   - Display running phases in blue.
   - Display skipped phases in orange.
   - Preserve green for passed, red for failed and grey for pending phases.

3. Increased revision
   - Updated revision to 3.9.3146.105.
2026-09-11 03:12:31 +02:00
marekf db34c579d0 <Fix>: Correct GCI streaming port ownership for preadjustment, increase revision to 3.9.3146.104
Cause:

- GenesisRegReader configured the GCI streaming port as NA.
- GCI Bridge therefore received no calibration streaming data during the Amplitude Test.
- The request port remained functional, so register writes succeeded while TOF data collection failed.

Solution:

1. Corrected streaming port ownership
   - Pass the configured Genesis opto COM port to GCI Bridge as the UART streaming port.
   - Stop GenesisRegReader streaming before handing the port over to GCI Bridge.
   - Validate request and streaming ports during preadjustment detection.
   - Release the GCI connection after preadjustment so the port can be reused by subsequent tests.

2. Increased revision
   - Updated revision to 3.9.3146.104.
2026-09-11 02:49:09 +02:00
marekf 83afddc918 <Feat>: Add real-time Genesis preadjustment diagnostics and configurable per-slot simulation, increase revision to 3.9.3146.103
Cause:

* Genesis preadjustment activities lacked a dedicated real-time diagnostic view.
* It was difficult to monitor slot workers, requests, phase progress and slot-specific logs during execution.
* GCI Bridge simulation did not provide configurable timing and results for individual preadjustment activities.
* Failed slots could incorrectly continue in subsequent preadjustment phases.
* Live log refresh prevented reliable scrolling and copying while messages were arriving.

Solution:

1. Added preadjustment diagnostic logging

   * Added detailed logging for GenesisCommunication preadjustment execution.
   * Added GCI Bridge worker, queue, request and operation diagnostics.
   * Routed diagnostic messages through AppDiagnostic.

2. Added a dedicated preadjustment diagnostics form

   * Opens automatically while a preadjustment activity is running.
   * Closes automatically when the activity finishes.
   * Uses a stable position within the TBF results area.
   * Displays GCI slot and worker status in real time.

3. Added dynamic slot diagnostics

   * Creates a tab for every selected slot.
   * Displays all eight preadjustment phases.
   * Shows Pending, Running, Passed and Failed states using distinct colors.
   * Preserves phase history between independently executed activities.
   * Keeps failed and excluded slots visible.

4. Added slot-specific live logs

   * Routes progress and simulation messages to the corresponding slot memo.
   * Appends only new messages instead of replacing the complete text.
   * Preserves the operator's scroll position while new messages arrive.
   * Removed duplicate timestamps.

5. Added configurable preadjustment simulation

   * Added configurable simulation duration, Success and ErrorMessage parameters.
   * Added per-operation simulation profiles to GCI Bridge.
   * Applies procedure-specific simulation values dynamically before executing a GCI operation.
   * Rejects invalid simulation-mode combinations.

6. Added parallel per-slot simulation workers

   * Executes simulated preadjustment operations independently for every selected slot.
   * Uses a dedicated worker and queue state for each slot.
   * Waits for all participating operations using Task.WhenAll.
   * Aggregates individual slot results into the batch result.

7. Added failed-slot exclusion

   * Stores preadjustment results for the current batch.
   * Excludes failed slots from subsequent preadjustment activities.
   * Keeps excluded slots visible and marked as failed.
   * Limits exclusion to preadjustment activities.

8. Increased revision

   * Updated revision to 3.9.3146.103.

Validation:

* Confirmed real-time phase and slot diagnostics.
* Confirmed parallel simulation using dedicated slot workers.
* Confirmed configurable simulation timing and results.
* Confirmed live slot logs remain scrollable and copyable.
2026-09-11 02:36:17 +02:00
marekf cf77faf972 kick 2 - prva dobra s dialogom testov 2026-09-11 00:33:16 +02:00
michal 684513ee59 Fix Genesis scheduling and synchronization for up to 10 workers 2026-09-10 22:09:30 +02:00
marekf 45a6d59b5e kick1 - priebezne stabilne - pred velkym zadanim 2026-09-10 21:39:00 +02:00
marekf 73c8fecdf2 <Update>: Version to 3.9.3146.102 in AssemblyInfo files 2026-09-10 17:52:22 +02:00
marekf 8c77df207a <Feat>: Add universal NLog diagnostics and improve AppDiagnostic UI
Cause:

- Live diagnostics required logging from multiple applications without blocking TBF.
- Shared nlog.config files caused configuration conflicts.
- The original viewer lacked configurable colors and practical log controls.
- Inconsistent diagnostic deployment caused log transfer failures and locked shared DLLs during builds.

Solution:

1. Added universal diagnostic transport
   - Added AppDiagnostic.Logging with an NLog target and named-pipe communication.
   - Added background buffering and automatic reconnection.
   - Registered the diagnostic connector programmatically.

2. Separated diagnostic window lifecycle
   - Run AppDiagnostic as an independent process.
   - Activate the existing window on repeated launches.
   - Keep TBF running when the diagnostic window closes.

3. Corrected diagnostic deployment
   - Copy the complete diagnostic output into a dedicated AppDiagnostic subdirectory during build.
   - Overwrite existing deployment files to prevent inconsistent versions.
   - Validate and launch the executable from that subdirectory with an explicit working directory.

4. Improved the log viewer
   - Added configurable substring colors with a palette and live preview.
   - Added text, level and application filters.
   - Added line wrapping, clipboard actions and log export.
   - Added Pause/Resume, Refresh, Clear, auto-scroll and Always on top.
   - Added connection and log counters.
   - Persist color rules between sessions.

5. Removed legacy dependencies
   - Replaced LiveLogCache with structured diagnostic storage.
   - Removed AppDiagnostic dependencies on log4net and SharedComponents.

Validation:

- Confirmed successful log transfer when launching AppDiagnostic from TBF.
2026-09-10 17:45:29 +02:00
marekf 1c6f9af2bb <Feat>: Add standalone Genesis preadjustment activities, fix configuration control and increase revision to 3.9.3146.101
Cause:

- GenesisCommunication did not expose GCI preadjustment phases as selectable test activities.
- Meter size and temperature inputs required by preadjustment could not be configured.
- The configuration control incorrectly expected the iPerlCommunication configuration type.
- Preadjustment results were not propagated to TBF test and meter results.

Solution:

1. Added standalone preadjustment activities
   - Added Initialization, Detect, Preparation, Amplitude Test, Temperature Calibration, Push Temperature, Offset Test and Completion.
   - Each phase can be selected and executed independently.

2. Added preadjustment parameters
   - Added configurable meter size and temperature.
   - Added validation and supported meter-size selection.

3. Added batch preadjustment execution
   - Resolves enabled Genesis register readers and their slots.
   - Verifies that all readers use the same GCI Bridge instance.
   - Dispatches the selected activity through the corresponding GCI Bridge API.

4. Added result processing
   - Stores execution time and completion status.
   - Updates test and meter results according to the GCI response.

5. Fixed the configuration control
   - Corrected TestMethodCfgCtrl to accept the GenesisCommunication TestMethodCfg type.

6. Increased revision
   - Updated revision to 3.9.3146.101.
2026-09-10 14:08:00 +02:00
marekf 0cfde7f06b <Update>: Version to 3.9.3146.0 in AssemblyInfo files 2026-09-09 17:31:27 +02:00
marekf d3c361b544 <Merge-fix>Solve merge conflicts - multichange: PassedFromTests() renamed to PassedFromAllTests() 2026-09-09 17:28:19 +02:00
marekf b35b7d92f9 <Merge-fix> Solve merge conflicts 2026-09-09 17:27:35 +02:00
michal 52bd0d01ce Restore Genesis communication and Q3 calibration from special branches
A – Registration and execution
- Register GenesisCommunication Factory in the component list.
- Separate the Genesis form and sequence from iPerl communication.

B – Communication activities
- Restore initialization, connection, PCB reading, password and login.
- Include grouped login, mode switching and disconnection.
- Support processing up to 10 slots.

C1 – Input calibration factors
- Read three factors from Water meters / Text1–Text3.
- Validate integer values in the range 1–65535.
- Preserve the default of 15625 when all three fields are empty.

D – Q3 calibration
- Connect the Prepare Q3 → measurement → Write Q3 workflow.
- Add channel processing to FlyingStart and FlyingStartMassCollection.
- Reset previous measurement data and validate calculated factors.
- Mark factors as stored only after StoreCalibration succeeds.

E – Results and database
- Store calibration factors separately for each meter and channel.
- Add result entities, mappings and Q3 data.
- Extend DB.cs / EnsureSchema to create and update the schema.
- Preserve compatibility with the existing binary format.

Validation:
- Debug build and 18 tests passed.
- Simulated communication runs follow the same activity sequence.
- The complete Q3 workflow has not yet been verified on hardware.

Known limitation:
- An inherited mismatch in simulated responses and error propagation
  can produce an incorrect OK result; this change does not fix it.
2026-09-09 15:04:49 +02:00
marekf eb4f7ad971 <Fix>: Prevent component configuration crashes, add a copyable error dialog, increase revision to 3.9.3145.109
Cause:

* A configuration control can reject an incompatible configuration and return null.
* ComponentsManagerDlg accessed the rejected configuration without validation, causing a NullReferenceException and closing TBF.
* The standard MessageBox was difficult to read and did not provide convenient access to technical details.

Solution:

1. Added configuration validation

   * Validates the configuration returned by the component control.
   * Logs the rejected control and configuration types.
   * Cancels the add operation without terminating TBF.

2. Added a reusable error details dialog

   * Displays a clear error summary and structured technical details.
   * Allows the diagnostic text to be selected and copied.
   * Added Copy and Close actions and registered the new form in TBF.csproj.

3. Increased revision

   * Updated revision to 3.9.3145.109.
2026-09-09 10:09:49 +02:00
marekf 8677e49680 <Fix>: Correct GciBridge component class name generation, increase revision to 3.9.3145.108
Cause:

- GciBridge Factory used an incorrect namespace substring offset when generating the component class name.
- The generated value was `geComponents.GciBridge` instead of `BridgeComponents.GciBridge`.
- The invalid class name could prevent correct component registration and lookup.

Solution:

1. Corrected class name generation
   - Changed the namespace substring offset from 12 to 8.
   - GciBridge Factory now returns the correct `BridgeComponents.GciBridge` class name.

2. Restored component identification
   - GciBridge can now be registered and resolved using the expected TBF component class name.

3. Increased revision
   - Updated revision to 3.9.3145.108.
2026-09-08 22:00:31 +02:00
marekf 29faef86e3 <Fix>: Prevent GciBridge startup failure when storage components are unavailable, increase revision to 3.9.3145.107
Cause:

- GciBridge threw an exception when the configured UniDataStorageReader or UniDataStorageWriter was not available during component initialization.
- The exception prevented TBF from starting and made it impossible to correct the component configuration through the UI.

Solution:

1. Improved missing component handling
   - Replaced fatal exceptions for missing storage components with warning logs.
   - Allowed GciBridge and TBF to continue initialization.

2. Preserved existing behavior
   - Available UniDataStorageReader and UniDataStorageWriter components are still linked as before.
   - Existing GciBridge configurations remain compatible.

3. Increased revision
   - Updated revision to 3.9.3145.107.
2026-09-08 21:59:29 +02:00
marekf 6ef42fcc21 <Feat>: Add optional TestID filtering for repeating XML results, increase revision to 3.9.3145.106
Cause:

- Repeating XML output always included every published meter test.
- The existing TestID/Prüfpunkt configuration was not used to filter generated XML test records.
- A configured TestID could be evaluated in an incorrect repeating test context.

Solution:

1. Added optional test filtering
   - Non-empty TestID values define the tests included in repeating XML output.
   - Multiple configured TestID values act as a whitelist.

2. Preserved existing behavior
   - When no TestID is configured, all published tests are exported as before.
   - Existing configurations remain compatible without migration.

3. Prevented cross-test evaluation
   - Test-dependent items are evaluated only for their matching TestID.
   - Values from one test are no longer reused in another test record.

4. Improved source formatting
   - Reformatted ResultsWriter.cs for easier reading and searching.

5. Increased revision
   - Updated revision to 3.9.3145.106.
2026-09-08 14:11:09 +02:00
marekf d2d41df155 <Fix>: Restore parent UniDataStorageWriter in Previous results editor, increase revision to 3.9.3145.105
Cause:

- Previous results opens the ResultsWriter configuration through GetControl(null).
- The component entity list is therefore unavailable.
- The DropDownList could not display the saved ParentName because it was not present in its Items collection.

Solution:

1. Restored the saved parent writer
   - Adds the configured ParentName to the list when it is not already available.
   - Selects the restored UniDataStorageWriter in parentComboBox.

2. Preserved standard configuration behavior
   - All available UDSW components are still loaded when component entities are provided.
   - Previous results can now display and use the previously configured parent writer.

3. Improved source formatting
   - Removed inappropriate line wrapping from ResultsWriterCfgCtrl.cs.

4. Increased revision
   - Updated revision to 3.9.3145.105.
2026-09-08 13:20:55 +02:00
marekf 6d2477be4a <Feat>: Enable TestID column in ResultsWriter configuration, increase revision to 3.9.3145.104
Cause:

- ResultsWriter explicitly suppressed the existing TestID column.
- Users could not configure the test identifier for test-dependent result variables.
- In the German localization, the corresponding Prüfpunkt column was therefore hidden.

Solution:

1. Enabled TestID configuration
   - Set SupressTestIDColumn to false.
   - The TestID/Prüfpunkt column is now displayed after Merge.

2. Preserved existing configuration handling
   - TestID values continue to use the existing WMeterRsltItemSpec serialization.
   - No configuration format change was required.

3. Increased revision
   - Updated revision to 3.9.3145.104.
2026-09-08 12:54:53 +02:00
marekf 57152ae749 <Feat>: Add XML-based payload archive filename configuration, increase revision to 3.9.3145.103
Cause:

- Payload archive filenames required values from the generated XML payload.
- Date/time values needed filename-safe formatting.
- XML result values such as Passed and Failed needed configurable conversion to P and F.

Solution:

1. Added XML filename tokens
   - Supports any unique non-repeating XML value using {Xml:/path}.
   - Resolves filename values directly from the generated payload.

2. Added user-friendly token configuration
   - Clicking inside a token opens the XML value chooser.
   - Added Time format and Mask transformations.

3. Improved filename generation
   - Validates XML paths and rejects ambiguous repeating values.
   - Sanitizes resolved values for safe filesystem usage.
   - Passes the per-meter payload context from ResultsWriter.

4. Integrated the new dialog
   - Added PayloadFileNameXmlTokenDlg and its Designer file.
   - Updated the TBF project configuration.

5. Increased revision
   - Updated revision to 3.9.3145.103.
2026-09-08 12:52:40 +02:00
michal 88b8bc5534 fix precision correct factor (Mexico), added diff, correction in percentage in Result Configuration 2026-09-07 14:07:07 +02:00
marekf 41408b30da <Feat>: Improve Previous Results layout and resizable result columns, increase revision to 3.9.3145.103
Cause:

- Previous Results used fixed column widths, making long procedure and purchase order values difficult to read.
- Result columns could not be resized interactively.
- Window resizing caused unnecessary UI redraws and visible lag.
- The existing result layout lacked clear column headers and visual separation.

Solution:

1. Added resizable result columns
   - Added draggable column separators.
   - Keeps column widths synchronized across all displayed result rows.
   - Added optimized resize preview to reduce UI lag.

2. Improved responsive layout
   - Enlarged the default Previous Results window.
   - Keeps Purchase Order, OK, NOK and Actions aligned to the right.
   - Uses Procedure as the flexible column when resizing the window.

3. Improved result presentation
   - Added aligned column headers.
   - Added modern column colors and styled action buttons.
   - Reduced spacing between result rows and improved separators.

4. Increased revision
   - Updated revision to 3.9.3145.103.
2026-09-04 08:25:27 +02:00
marekf 136be96e1b <Feat>: Add customer XML structure mapping support in ResultsWriter, increase revision to 3.9.3145.102
Cause:

- The customer XML format requires batch, DUT, group and test-level metadata.
- ResultsWriter did not provide variables for test position, repetition index, batch duration, DUT test duration or aggregated test statuses.
- Missing WaterMeterData could cause a NullReferenceException while evaluating WM Type.

Solution:

1. Added XML mapping variables
   - Added TestPositionInBatch and TestRepetitionNr.
   - Added BatchTime and TestsTotalTime.
   - Added PassedFromAllTests and PassedFromRepeatedTests.

2. Added hierarchical result evaluation
   - Evaluates the overall PANEL/DUT status from all tests.
   - Evaluates GROUP status from repeated instances of the selected test.

3. Improved meter data handling
   - Made WM Type evaluation null-safe when WaterMeterData is unavailable.

4. Increased revision
   - Updated revision to 3.9.3145.102.
2026-09-04 08:20:50 +02:00
michal 0258e54b4e Poseidon integration fix, ReadMe_BugFix.md actualisation 2026-09-03 10:50:20 +02:00
marekf d3c8813012 <Fix>: Correct test-dependent result evaluation in ResultsWriter, increase revision to 3.9.3145.101
Cause:

- ResultsWriter evaluated selected test-dependent items without the actual TestID.
- This caused values such as Test passed(), timestamps, flow, pressure, temperature, conductivity and error data to be empty or incorrect.

Solution:

1. Fixed test-aware result evaluation
   - Uses published regular meter test results.
   - Passes mtr.Name() as TestID to item.Print(wm, testId).

2. Restored correct test result mapping
   - Test-dependent values are now resolved from the correct TestRslt / MeterTestRslt.

3. Increased revision
   - Updated revision to 3.9.3145.101.
2026-09-03 10:00:40 +02:00
michal b8e54e1fc9 Poseidon - merge project 2026-09-02 16:18:21 +02:00
marekf 054075a341 <Feat>: Add XML result generation with file and MSSQL output support, increase revision to 3.9.3145.100
Cause:

- ResultsWriter required a generic way to generate customer-specific XML result data from TBF measurement results.
- The customer reference XML contains example runtime values and repeated result structures, so it cannot be used directly as the generated output.
- TBF result variables must be explicitly mapped to destinations in the customer XML structure.
- The generated XML result data must support two output targets:
  - direct creation of an XML file,
  - delivery of the XML payload to a Microsoft SQL stored procedure.
- Preview generation must allow the XML structure and configured mappings to be verified without executing the production database write.
- Increase revision to 3.9.3145.100.

Solution:

1. Added XML reference analysis
   - Creates a clean base XML structure.
   - Extracts the repeating result prototype.
   - Prevents sample runtime values from the customer reference XML from leaking into generated results.
2. Added configurable TBF-to-XML result mapping
   - Allows explicit mapping of TBF result variables to customer XML destinations.
   - Supports one-time and repeating XML destinations.
   - Keeps the mapping independent of the semantic meaning of customer XML attribute names.
3. Added XML destination viewer and configurator
   - Shows the current mapping.
   - Highlights repeating destinations.
   - Identifies already used one-time destinations.
4. Added runtime XML result generation
   - Uses the configured TBF-to-XML mappings.
   - Uses measurement procedure result data.
   - Builds the output from the clean base XML and repeating XML prototype.
   - Generates repeated result records according to the executed measurement procedure.
5. Added simulation-based Preview request
   - Generates a complete XML payload using simulation values.
   - Allows XML structure and mapping verification before production execution.
   - Does not execute the production stored procedure.
6. Added direct XML file output support to UniDataStorageWriter
   - Supports File / .xml as a physical output target.
   - Creates the generated XML result file in the configured output directory.
7. Added Microsoft SQL stored-procedure XML output
   - Supports Microsoft SQL / StoredProcedure as a physical output target.
   - Passes the generated XML payload through the configured stored procedure parameter.
8. Added optional XML payload archiving
   - Allows generated XML payloads to be stored in the configured Payload archive.
   - Can be used together with the Microsoft SQL stored-procedure output.
9. Kept ResultsWriter independent of the physical output target
   - ResultsWriter generates the result payload.
   - UniDataStorageWriter decides how and where the payload is physically written.
   - The same ResultsWriter XML generation mechanism is therefore used for both XML file and MSSQL outputs.
10. Increased revision
   - Updated revision to 3.9.3145.100.
2026-09-02 12:05:10 +02:00
michal 8d42232589 Poseidon move from feature/morisville/fix_cli_timeout 2026-09-01 10:27:04 +02:00
michal e4d4d75642 feat(ally): add ASIC-style designer configuration and manual test controls 2026-09-01 09:00:12 +02:00
michal 2b5a0ffe14 Merge remote-tracking branch 'private-origin/develop/SLM-PT50_genesisDirectDecode_ALLY' into develop/SLM-PT50_genesisDirectDecode_ALLY 2026-08-25 14:35:13 +02:00
marekf a0d53f9ec9 <Fix>: RefPulses carry-over by restoring testInProgress behavior in StandingStart, increase revision to 3.9.3145
Cause:
1. Background: Restore/ensure testInProgress behavior before the DataEntry sequence and explicitly prevent to reset the pulse.
2. Increase revision to 3.9.3145

Solution:
1. Deleted .AddOperation(testInProgress) calling
2. Increased revision to 3.9.3145
2026-08-25 13:49:49 +02:00
michal dfa8693be9 Added table columns TestRslt -> ConductMean, .... 2026-08-25 10:38:08 +02:00
michal 8157a752bd Ally multy reg readers implementation in SmartCommunicationForm.cs 2026-08-25 10:26:35 +02:00
michal 4dd805bb1c Ally multy reg readers implementation in SmartCommunicationForm.cs 2026-08-25 10:26:15 +02:00
marekf 1e45f48c75 <Update>: Version to 3.9.3144.0 in AssemblyInfo files 2026-08-25 08:23:27 +02:00
marekf d833eca584 <Merge_Fix>: System.Data.SQLite, System.Data.SQLite.Core, Stub.System.Data.SQLite.Core.NetFramework 2026-08-24 15:36:50 +02:00
marekf c966a7034c <Merge-Fix>: Solve merge conflicts 2026-08-24 15:04:11 +02:00
marekf f051d51ae0 <Fix>: Incorrect cycle resume behavior after Stop and Start, increase revision to 3.9.3134.114
Cause:
1. When the Cycle was stopped during the second or a later test and then resumed using the Start Test button, the resume confirmation was handled incorrectly. After selecting to continue, the sequence restarted from the first test instead of continuing from the interrupted test.
2. Increase revision to 3.9.3134.114

Solution:
1. Corrected the Cycle resume logic so that, after confirming continuation, the sequence resumes from the interrupted test instead of restarting from the first test.
2. Increased revision to 3.9.3134.114
2026-08-24 14:25:19 +02:00
marekf 267159e0a9 <Fix>: RefPulses carry-over by restoring testInProgress behavior in StandingStart, increase revision to 3.9.3134.113
Cause:
1. Background: there was a suspicion that the Bukoven board left RefPulses active across operations; this was investigated and ruled out. The pulse counter is reset by testInProgress when an operation is reused. During the third DataEntry window testInProgress had been temporarily disabled (SLM vs Karlsruhe difference), which allowed RefPulses to appear to persist.
2. Increase revision to 3.9.3134.113

Solution:
1. Restore/ensure testInProgress behavior during the DataEntry sequence and explicitly reset the pulse counter when starting a new operation.
2. Increased revision to 3.9.3134.113
2026-08-24 12:54:15 +02:00
michal 26db172153 Camera WM Purchase Order - crop disabled 2026-08-24 12:21:49 +02:00
michal 2e75f65b4a Update version to 3.9.3142.0 in AssemblyInfo files 2026-08-20 00:27:39 +02:00
michal 432cb91e5a Update Flip Mode handling and introduce SetTestModeOpto7Str alias
Refactor Flip Mode parameter type in `WMeterRsltItemSpec` to `Quantity.String` for consistency. Add `SetTestModeOpto7Str` alias to improve readability and replace legacy A0 test mode reference in `iPerlCommunicationParams`.
2026-08-20 00:27:06 +02:00
michal b0083afa10 DataEntry.UNI - timeout, autoclose improve
Introduce configurable timeouts for data entry volume reads in `IRegReaderSmart` and its implementations: added optional timeout parameters and updated default values across associated forms and methods. Updated `EntryFormCfg` to support customizable timeout settings.
2026-08-19 23:47:50 +02:00
michal 955b59a195 Improve UNI Data Entry auto close
Add tooltip functionality for parameter names in `ComponentCfgCtrl` and auto-close countdown improvements in UNI Data Entry forms
2026-08-19 22:32:14 +02:00
michal b904d0ca41 Fix Flip Mode - ver. 3.9.3141.0
Add robust handling for form closure in UNI Data Entry: prevent operations on disposed/closing forms, enhance logging, and update logic in serial number and volume updates. Increment version to 3.9.3141.0.
2026-08-19 19:13:41 +02:00
michal f34046df2a Update version to 3.9.3140.0 in AssemblyInfo files - FIX DataEntry.UNI 2026-08-19 14:49:24 +02:00
michal 889782068e Added counter to OK button, 5 sec default now
Add auto-close countdown timer for UNI data entry forms: implement timer logic, UI updates, and early exit handling
2026-08-19 14:47:34 +02:00
michal d12ee008db FIX UNI Data Entry - Smart Reader + missing files
Implement smart meter interface enhancements for AllyReader: Add `ISmartMeterReader` interface, update `RegisterReaderSelection` logic, integrate smart-meter position selection, and expand unit test coverage.
2026-08-19 14:33:06 +02:00
michal 6c3c6cd0ef FIX UNI Data Entry - corrensponded with iPerlCommunicationForm.cs
Add debug logging and UI enhancements for data entry processes: Improve register reader handling in sequences and forms, add detailed data entry debug logs, integrate serial number and volume watermarking, and update localized strings in multiple languages.
2026-08-19 13:18:34 +02:00
michal 5803f06bdb Flip mode related to test result - DB column in MeterTestRslt, support back compability
Add Flip Mode support across iPerl-related test methods and database schema: implement FlipMode property, update MeterTestRslt mappings, integrate schema migration logic, and refine test result handling.
2026-08-19 11:52:46 +02:00
michal a4acf30771 Refactor FlipMode enum and update references: rename DisabledConstant to Constant and EnabledRandomized to Randomized across codebase, including unit tests and integration tests, for improved readability and consistency. 2026-08-19 10:47:45 +02:00
michal a4ef208233 Fix ver. 3.9.3139 iPerlCommunicationForm.cs problems + exceptions if another regreadrer has mistakely same ports
Enhance iPerl dialog layout and error handling: improve `ShuffleTextBoxes` with positional mapping, refine checkbox state tracking, add detailed logging for labels and checkboxes, and handle serial port exceptions. Increment version to 3.9.3139.0.
2026-08-18 16:09:21 +02:00
michal be6b40b5cd Fix Optical heads ASIC iPerl dialog manual test communication 2026-08-18 14:05:33 +02:00
michal 8738edd0c5 Added FLIP MODE Constant, Randomised
Add support for Flip Mode configuration in iPerl: Implement `SetFlipMode` functionality in `OptoHeadTest` and `RadioService`, integrate constants, enhance UI, and add unit and integration tests for constant/randomized flip mode settings.
2026-08-18 13:59:59 +02:00
michal c71fe75446 Ally iPerl initial - reader,test method, unit test
Add AllyReader and AllyCalibration support with tests: Introduce new register readers, calibration methods, and comprehensive unit tests for integration and functionality validation. Update project files accordingly.
2026-08-18 10:01:02 +02:00
michal c619c7d3b8 Merge remote-tracking branch 'private-origin/develop/SLM-PT50_genesisDirectDecode' into develop/SLM-PT50_genesisDirectDecode 2026-08-17 10:27:57 +02:00
michal f3e5527145 ver. 3.9.3138.0 - OPTHO FORMATING improve (ASIC,genesis) 2026-08-17 10:27:10 +02:00
michal 39941842d4 OPTHO FORMATING improve (ASIC,genesis)
Enhance iPerlHat protocol logging and error handling: add optical HEX formatting, improve diagnostic messages, and extend test command descriptions.
2026-08-17 10:25:35 +02:00
michal 3488939995 FIX iPerl ASIC meter - mexico
Update MultiplyFactor constant to 1 and increment version to 3.9.3137.0
2026-08-14 14:14:33 +02:00
michal 0f2ef5a5bf bugfix IPERL_ASIC conductivity
update mappings , version 3.9.3136.0
2026-08-13 15:16:58 +02:00
michal 55d2381e8a Mexico - read additional parameters and store to DB, ver. 3.9.3135.0
Add support for reading and handling additional common parameters in OptoHeadTest. Extend RadioService and related classes with enhanced parsing and validation logic. Update UI and constants for functionality integration.
2026-08-12 16:20:16 +02:00
michal 126322584f Merge remote-tracking branch 'private-origin/develop/SLM-PT50_genesisDirectDecode' into develop/SLM-PT50_genesisDirectDecode 2026-08-05 12:48:48 +02:00
michal 4d9ddef3ca IperlASIC - added aditional parameters
Add support for reading additional common parameters in OptoHeadTest. Enhance `RadioService` and `VersionTypeResult` with new parsing logic. Update related constants and UI elements for functionality integration.
2026-08-05 10:57:34 +02:00
marekf 7139cdbe80 Merge branch 'develop/SLM-PT50-7-2026-merged-smart-cam-slm-mex' into develop/SLM-PT50_genesisDirectDecode 2026-08-04 16:31:57 +02:00
marekf 93d7456ccd <Fix>: RefPulses carry-over by restoring testInProgress behavior in StandingStartMassCollection, increase revision to 3.9.3134.112
Cause:
1. Background: there was a suspicion that the Bukoven board left RefPulses active across operations; this was investigated and ruled out. The pulse counter is reset by testInProgress when an operation is reused. During the third DataEntry window testInProgress had been temporarily disabled (SLM vs Karlsruhe difference), which allowed RefPulses to appear to persist.
2. Increase revision to 3.9.3134.112

Solution:
1. Restore/ensure testInProgress behavior during the DataEntry sequence and explicitly reset the pulse counter when starting a new operation.
2. Increased revision to 3.9.3134.112
2026-08-04 16:21:33 +02:00
michal 7a8ce7b6ab IperlASIC - Improved and added tests, hardware integtration tests works
Improve `VersionTypeResult` parsing and handling: Add compact/legacy payload support. Extend `ReadingUnits`. Update `IperlHatIntegrationTests` and `RadioService` with new tests and functionality integration.
2026-08-04 11:53:47 +02:00
michal 865528e344 iPerlASIC - additional parameters.
Add protocolCommons classes and enhance RadioService with support for CalibrationFactor, VersionType, ReadingUnits, and FlipMode operations. Add corresponding unit tests for validation. Update project files.
2026-08-03 15:42:16 +02:00
michal 02bb9ee9fa Update RadioService: Replace IsLoggedOn with IsConnected condition checks 2026-07-31 14:09:10 +02:00
marekf 79de40515a Upgrade: Deleted LANG_PL compilation condition, disabled legalizator before test, and version to 3.9.3134.111
Cause:
1. Unnecessary and mandatory user verification before the test, in addition, a bug appears: the "Approval by legalizer" window is displayed behind the main application window. The window was only needed for the Polish project.
2. `LANG_PL` constant in `TBF.csproj` was no longer needed.
3. Assembly version and file version in `AssemblyInfo.cs` needed to be incremented to `3.9.3134.111`.

Solution:
1. Made `altProcNameLabel` and `altProcNameTextBox` always visible and disabled specific UI controls in `ProcedureDlg.cs`.
2. Removed `LANG_PL` from `DefineConstants` in `TBF.csproj`.
3. Updated assembly version and file version in `AssemblyInfo.cs` to `3.9.3134.111`.
2026-07-28 11:49:37 +02:00
marekf e38c6c1199 Upgrade: Add remaining time display for tests, version 3.9.3134.110
Cause:
1. Missing functionality to display remaining time during tests in `StandingStartMassCollectionSeq.cs` and `StandingStartMassCollectionAdvanceSeq.cs`.
2. Assembly version and file version required an update to reflect changes.

Solution:
1. Implemented remaining time display in `StandingStartMassCollectionSeq.cs` and `StandingStartMassCollectionAdvanceSeq.cs` using `Bridge.OnActivity` to show time in minutes and seconds or seconds only.
2. Updated assembly version and file version in `AssemblyInfo.cs` to `3.9.3134.110`.
2026-07-28 11:32:53 +02:00
marekf dbb35a203e Update: Adjust input unit constants in DataEntry form and increment version to 3.9.3134.109
Cause:
1. Updated `unit` constant in `TestStartEndForm` class within `TBF.Rig.DataEntry.StandardCamera` namespace from `Unit.m3` to `Unit.l`.
2. Updated `unit` constant in `TestStartEndForm` class within `TBF.Rig.DataEntry.StandartCameraPurchaseOrder` namespace from `Unit.m3` to `Unit.l`.
3. Incremented assembly version and file version in `AssemblyInfo.cs` to `3.9.3134.109`.

Solution:
1. Changed `unit` constant in `TestStartEndForm` class within `TBF.Rig.DataEntry.StandardCamera` namespace to `Unit.l`.
2. Changed `unit` constant in `TestStartEndForm` class within `TBF.Rig.DataEntry.StandartCameraPurchaseOrder` namespace to `Unit.l`.
3. Updated assembly version and file version in `AssemblyInfo.cs` to `3.9.3134.109`.
2026-07-28 07:38:30 +02:00
marekf a88a094e86 Release SLM-PT50 3.9.3134.108, increase revision after merge 2026-07-23 13:08:52 +02:00
marekf ffaebbdf07 Merge branch 'develop/SLM-PT50_genesisDirectDecode' of http://87.197.120.129:33300/michal/tbf into develop/SLM-PT50 2026-07-23 12:57:20 +02:00
michal e7d04b55b3 Live Stream Operations: Add StartLiveStream to CJMS11 camera 2026-07-22 10:02:22 +02:00
michal 8301971906 Fix StartEndForm in StandartCameraPurchaseOrder layout and positioning, remove Scroll Bar 2026-07-21 16:03:43 +02:00
michal 693cf7e4e2 Refactor TestStartEndForm - Remove unused methods and comments, simplify image grouping logic. 2026-07-21 15:22:50 +02:00
michal 2fa212e6ff FIX CAMERA - Improve error handling, modularize image processing functions for CJMS11 cameras, and enhance multi-image support in TestStartEndForm. 2026-07-21 14:50:59 +02:00
marekf a210a0d1f2 Upgrade (UniDataStorageWriter): improve writer configuration UX and CSV handling
- added technology-specific default configuration for new writers
- auto-fill default SQL/XLSX write templates
- disabled write templates for CSV technology
- added technology-specific data source hints
- changed test parameter format to Column=Value and Where=Value;Set=Value
- improved parameter validation to prevent invalid input crashes
- added automatic CSV header creation and header validation
- aligned CSV writer behaviour with XLSX writer
- Increase revision number
2026-07-21 11:18:11 +02:00
marekf 4cd191fcb2 Merge branch 'develop/SLM-PT50-7-2026-merged-smart-cam-slm-mex' into develop/Mexico-6-2026 2026-07-20 15:54:01 +02:00
marekf e21833fb5f Release Mexico 3.9.3069.1 2026-07-20 15:47:06 +02:00
marekf b746ceb34a Upgrade - Replace PulsesTypeByQuantity with MultiFunctionalVariables unit filtering based on RegisterReader quantity type - remove PulsesTypeByQuantity-based parameter values
- generate available units using Units.IsQuantity(...)
- use Quantity.MultiFunctionalVariables to populate supported units
- centralize unit selection through Units.GetQuantity()
2026-07-20 14:09:29 +02:00
marekf 83c1f0e817 Update - Actualized revision nr. 2026-07-17 14:15:22 +02:00
marekf a88a5a8db2 Fix - 1. StandingStartMassCollection and StandingStartMassCollectionAdvance - changed all delayed Activity strings, 2. Added UI/Bridge onDelayedActivity() method for universal using 2026-07-17 14:15:04 +02:00
marekf 271200a8da Upgrade - StandingStartMassCollectionAdvance method - 1. changed structure and residual flow delays by changes in StandingStartMassCollection method, 2. added variables, counting and evaluation for volume/mass type of RegisterReader 2026-07-17 12:42:07 +02:00
marekf b703297f5e Fix - 1. repaired StandingStartMassCollection MassEN evaluation, the problem was caused by the end scale measuring only after recalculating the basic values - 2. removed fixed delay for the first diverter switching and first diverter switching was moved after a configurable delay - 3. hidden unitComboBox in the StartEnd dialog of the DataEntry.Standard24 component, because units are now defined using the RegisterReader component 2026-07-17 09:55:03 +02:00
marekf 637d1d021d Fix - StandingMassCollection method - add end mass measurement 2026-07-16 15:19:28 +02:00
marekf dbe424b17a Add - StandingStart method - Store and log default PumpPower for dynamic test repetitions - Read PumpPower from the current pump before starting dynamic tests.
- Store the value as the default PumpPower for subsequent test instances in the cycle.
- Add logging when PumpPower is initialized from the pump.
- Add logging at test start including test name, repetition number,
  last repetition flag, and PumpPower to improve diagnostics.
2026-07-16 14:21:00 +02:00
marekf be0525a319 Upgrade - improved StandingStart method by SLM requirements, added fixed delays for meters stabilization 2026-07-16 13:29:42 +02:00
marekf 0ce130f0ef Fix - repaired merging compatibility problem of GCI return type GciConnectResult - IsLoggedOn and IsConnected, increased VisualStudio version from 17.8 to 18.5 2026-07-16 12:37:59 +02:00
michal c1ee29cd06 Merge remote-tracking branch 'private-origin/develop/SLM-PT50_genesisDirectDecode' into develop/SLM-PT50_genesisDirectDecode 2026-07-16 12:28:36 +02:00
michal 28607e2729 fix Strings - Camera CJMS11 - support dialog multi pictures 2026-07-16 12:28:28 +02:00
michal f1bc71fb92 Camera CJMS11 - support dialog multi pictures 2026-07-16 12:19:27 +02:00
michal 69b607f963 Merge remote-tracking branch 'private-origin/develop/SLM-PT50_genesisDirectDecode' into develop/SLM-PT50_genesisDirectDecode
# Conflicts:
#	TBF/TBF.csproj
2026-07-16 10:28:15 +02:00
marekf 6ac3ea87ef Upgrade - improved StandingStartMassCollection method by SLM requirements, added fixed delays, added residual flow delays 2026-07-16 09:30:59 +02:00
marekf 38973e0030 Add - Store and log default PumpPower for dynamic test repetitions - Read PumpPower from the current pump before starting dynamic tests.
- Store the value as the default PumpPower for subsequent test instances in the cycle.
- Add logging when PumpPower is initialized from the pump.
- Add logging at test start including test name, repetition number,
  last repetition flag, and PumpPower to improve diagnostics.
2026-07-15 11:35:32 +02:00
marekf 37c89a4c9c Fix - missing safe shutdown when PerformSteps returns UiCmdStop
- When STOP is pressed during PerformSteps() in the BeforeTest transition,
Transition() returned immediately and skipped the common shutdown logic.
This caused FM pumps and valves to remain in their previous state.
- Perform the required safe shutdown (FM pump off and default valve setup)
before returning UiCmdStop or Error.
2026-07-14 14:41:53 +02:00
marekf 0075362f92 Fix - Handle scale underload condition
- Added Underload measurement state
- Detect underload responses from Mettler Toledo scales
- Automatically zero the scale after underload detection
- Restart mass measurement after successful zeroing
- Prevent zeroing response from being used as a valid measurement
- Preserve measurement stability by clearing buffered readings after underload
2026-07-13 14:01:57 +02:00
marekf 38d28599ad Merge branch 'develop/SLM-PT50_genesisDirectDecode' of http://87.197.120.129:33300/michal/tbf into develop/SLM-PT50 2026-07-03 13:37:26 +02:00
marekf c9a4b0f3ce Add (develop) - component ResultsWriter - added diag + english comments 2026-07-03 13:35:39 +02:00
marekf b74f1fd644 Add (develop) - first succesfull test - new component ResultsWriter - note: IResultsProcessor type componet, store result variables to database by using of component UniDataStorageWriter to the user-configured storage. The selection of variables is done using the standard UI for selecting the display of measurement results . In other words: The measurement results selected by the user are saved to a user-defined storage. 2026-07-03 12:46:56 +02:00
michal 26b7aa7e8b correction atempt MultiplyFactor for timestamp, volume calculation, 2026-07-02 23:53:15 +02:00
michal 92f63e744d fix INIT - Overload time fix for OptoTelegramRawTest 2026-07-02 23:24:00 +02:00
michal b0581d81ef Extend measurement window for low-flow tests 2026-07-02 23:08:28 +02:00
michal 540b9af871 Overload time fix for OptoTelegramRawTest 2026-07-02 22:32:19 +02:00
michal d11f7107fe fix time calculation from raw data 2026-07-02 21:49:01 +02:00
michal 3c5a28f69d changed LED state from 4 to 7 in 2026-07-02 21:47:14 +02:00
marekf 110b880304 Add (release) new component Modbus.WaterAnalyzerUni derived from original component Modbus.WaterAnalyzer. New component is expanded by the possibility to choose conductivity reading directly from the sensor (Modbus protocol) (original solution) or using a 4-20mA converter to serial line (RS232/485) (Modbus protocol). 2026-07-02 09:01:00 +02:00
marekf 7867044c0a Add (develop) new component ConductivityMeter, because we have papouch AD4RS converter (4-20mA to modbus) - first correct version for testing 2026-07-01 15:58:36 +02:00
marekf 88903f3dac Merge branch 'feature/task/Cordonel-preadjustmentUI-GenesisMeter-ZeroFlowGenesisMeter' into develop/UnionTown 2026-06-12 10:44:51 +02:00
marekf 4e223549ee Develop - GciBridge -> GCI -> PreadjustmentUI functions 7 - successful processes - testbench is exported to UnionTown - without CalibParams DB writer 2026-06-12 10:43:34 +02:00
marekf 2ea0df8faf Develop - GciBridge -> GCI -> create database core in GCI - SkeletoKey - CalibParams - first ok 2026-06-09 19:55:36 +02:00
marekf 061e85ca6b Develop - GciBridge -> GCI -> PreadjustmentUI functions 6 - first successful processes - without CalibParams DB reader and CalibParams DB writer 2026-06-09 08:53:54 +02:00
marekf 269e650b38 Merge branch 'feature/task/SWDocumentation-DocFX' into develop/UnionTown 2026-06-08 10:29:04 +02:00
marekf a3bfc7a43e Develop - GciBridge -> GCI -> PreadjustmentUI functions 5 2026-06-05 15:21:18 +02:00
marekf c2ff40d816 Develop - GCI - create documentation model 2, using DocFX tool - using .md files - output as html page 2026-06-02 07:26:36 +02:00
marekf eb09ae2eca Develop - GCI - create documentation model, using DocFX tool - using .md files - output as html page 2026-05-29 09:49:28 +02:00
marekf d1b6247bb7 Develop - GciBridge -> GCI -> create database core in GCI - SkeletoKey - CalibParams 2026-05-29 09:43:51 +02:00
marekf c5f52e06fa Develop - GciBridge -> GCI -> PreadjustmentUI functions 4 2026-05-26 10:51:18 +02:00
marekf 9700ba7900 Develop - GciBridge -> GCI -> PreadjustmentUI functions 3 2026-05-22 08:52:40 +02:00
marekf bc72130d09 Develop - GciBridge -> GCI -> PreadjustmentUI functions 2 2026-05-21 08:16:02 +02:00
marekf ad208f9633 Develop - GciBridge -> GCI -> PreadjustmentUI functions 2026-05-18 19:20:10 +02:00
marekf ad2bfd3639 Upgrade (develop) - GCI - GciBridge - improved Disconnect 2026-05-14 13:05:21 +02:00
marekf 5e637ae02e Upgrade (develop) - GCI - GciBridge - full parallel threading slots actions 2026-05-13 09:30:27 +02:00
marekf 63f8dd7467 Upgrade (develop) - GCI - GciBridge - InitSlot() - only ports config update if slot already exists 2026-05-12 14:59:15 +02:00
marekf aac3af8714 Upgrade (develop) - GCI - GciBridge - delete meter.Logout() in GetPcbId() 2026-05-12 14:20:24 +02:00
marekf 378917f59d Upgrade (develop) - GCI - GciBridge - GCI-Laatzen interface debugging 2026-05-12 13:30:21 +02:00
marekf 76e7ccb4a6 Update (develop) - GCI - GciBridge - better return for Connect 2026-05-11 17:14:56 +02:00
marekf 953b02a6e8 Upgrade (develop) - GCI - GciBridge - new CleanSlot() - Disconnect Hard Dispose 2026-05-08 11:49:14 +02:00
marekf 71a66dca2d File modified due to the need for MF to compile in advance, but the change is ultimately not necessary for either MF or MB 2026-05-07 18:23:50 +02:00
marekf 978c620287 Update (develop) - GCI - GciBridge - improved PublicModels - fixed bugs 2026-05-07 18:20:18 +02:00
marekf 8489dafdf7 Update (develop) - GCI - GciBridge - progress 2026-05-06 14:58:44 +02:00
michal cfc52f195b Merge remote-tracking branch 'private-origin/develop/SLM-PT50_genesisDirectDecode' into develop/SLM-PT50_genesisDirectDecode
# Conflicts:
#	TBF/TBF.csproj
2026-05-06 10:26:15 +02:00
michal 68a1eb9941 Update project references and paths to replace project dependencies with direct DLL references. Standardize formatting in GenesisImplHeadTestCtrl and refine Operations enum declarations. 2026-05-06 10:24:33 +02:00
marekf c1573390b5 Merge branch 'develop/UnionTown' into develop/SLM-PT50 2026-05-06 09:08:09 +02:00
marekf 5d60523e87 Fixed - next issue with old Newtonsoft.Json v 12.0 - updated to 13.0 2026-05-06 07:38:49 +02:00
marekf 91c63e0792 Fixed - bad copying GenesisCordonelInterface runtime files to TBF target folder 2026-05-06 07:35:17 +02:00
marekf 0940df2f1a Fixed - issue with old Newtonsoft.Json v 12.0 - updated to 13.0 2026-05-06 07:32:55 +02:00
michal 16a76720b8 Update references and paths for Xylem.Common components, refine OptoHeadTest handling for connection requests, and increment assembly version to 3.9.3063.1. 2026-05-05 08:34:21 +02:00
marekf 7101fd679e Merge branch 'develop/UnionTown' into develop/SLM-PT50 2026-05-04 15:45:07 +02:00
marekf 86299da682 Fixed - bad runtime files copying in postbuilding of GenesisCordonelInterface project. Copying is replaced from TBF/Build/ to GCI/Build 2026-05-04 15:40:05 +02:00
marekf 39b951b709 Merge branch 'develop/UnionTown' into develop/SLM-PT50 2026-05-04 13:08:10 +02:00
marekf a475bd80f3 Change - ExternalProjects folder to gitignore 2026-05-04 13:07:28 +02:00
marekf 4e64bcd117 Merged branch Uniontown - GenesisCordonelInterface + GciBridge 2026-05-04 12:49:02 +02:00
marekf fbd0219a4a Update (develop) GCI - GciBridge - big change - for implement to GenesisHead and SmartReader 2026-05-04 11:29:13 +02:00
michal 104a68d9dc Remove unused System.IO.Ports, Common, Config, and other redundant namespaces. Update NuGet package configurations to reflect new paths and tools. 2026-04-29 09:51:05 +02:00
michal 33a5e479a9 Refactor RadioService to improve asynchronous handling, replace legacy code with CommInterfaceBridge, and standardize methods for GenesisHead communication. Remove unused GenesisCommunication components and increment assembly version to 3.9.3062.1. 2026-04-28 16:26:26 +02:00
michal 5482d4ac96 Merge remote-tracking branch 'private-origin/develop/SLM-PT50_genesisDirectDecode' into develop/SLM-PT50_genesisDirectDecode
# Conflicts:
#	TBF/Rig/TestMethods/GenesisCommunication/GenesisHead/GenesisHead.cs
#	TBF/TBF.csproj
2026-04-28 07:51:09 +02:00
michal 0d984a77b3 Refactor RadioService to utilize GciBridge for communication, replace SerialDriver, and add SlotNr support in Genesis configuration and UI. 2026-04-28 07:08:53 +02:00
marekf 7e95e18207 Fixed - genesis library path issue 2026-04-27 16:52:50 +02:00
marekf 38fa86d243 Merge branch 'develop/UnionTown' into develop/SLM-PT50 2026-04-27 16:42:47 +02:00
marekf 4c1ce73de9 Upgrade (develop) - GCI - GciBridge - big change 2026-04-27 16:40:42 +02:00
michal eea80d39c0 Remove GenesisCommunication test methods and components:
- Deleted `CalibrationStruct`, `ConfigStruct`, `Enums`, `Factory`, `GenesisHead`, and related files.
2026-04-27 10:48:27 +02:00
marekf 15dc325275 Upgrade (develop) - GCI - big change 2026-04-27 07:57:16 +02:00
marekf ca333d1772 Upgrade (develop) - GCI UI for TBF - change Forms to Views 2026-04-26 11:40:21 +02:00
marekf a4ece06b05 Upgrade (develop) - GCI UI for TBF - change Forms to Views 2026-04-26 11:39:35 +02:00
marekf 992452052e Upgrade GenesisCommunication - after adding GCI functionality to xylem DLL's updater, adding PortCore DLL is needed 2026-04-26 09:32:30 +02:00
marekf 75192bbd09 Add (develop) GCI - new functionality - xylem DLL's updater for TBF - actualize DLL's in TBF.packages.common of TBF project path - fixed problem witch multiple referencing of GenesisCommunication and GciBridge on one DLL but with difference version 2026-04-26 08:13:43 +02:00
marekf 6199bd48c5 Upgrade .NET Framework 4.7.2 to 4.8 for Laatzen + GCI + TBF compatibility 2026-04-24 13:00:53 +02:00
marekf 25434d70d4 Merge branch 'feature/task/Cordonel-communiation-tests' into develop/SLM-PT50 2026-04-23 10:00:59 +02:00
michal de6dd45dcb Fix Flow - Increment assembly version to 3.9.3059.1. 2026-04-23 09:56:32 +02:00
marekf 74f2e99ed3 Merge branch 'bugfix/Flow-meter-in-parallel-reverse-bug' into develop/SLM-PT50 2026-04-23 09:43:39 +02:00
marekf 99b1f4a171 Fixed FlowMetersInParallel - incorrect ltrPerPulse counting from average value of nominalFreq 2026-04-23 09:40:05 +02:00
marekf 8c10aa3752 Add (develop) GCI - GciBridge 2026-04-23 09:09:27 +02:00
michal 7170da4a70 Add GenesisCommunication test methods and related components:
- Introduce `TestMethod`, `SequenceConditionOp`, and `Factory` for `GenesisCommunication`.
- Add `TestMethodCfgCtrl` and its designer for test configuration UI.
- Update `.csproj` to include new files.
2026-04-22 10:18:42 +02:00
michal 842f009b42 Increment assembly version to 3.9.3058.1 and fix logging format issue in FlowMeter initialization. 2026-04-21 07:43:35 +02:00
michal 7f30ca0cd1 Merge branch 'develop/SLM-PT50_genesisDirectDecode' into develop/SLM-PT50_genesisDirectDecode_morisville
# Conflicts:
#	TBF/Properties/AssemblyInfo.cs
#	TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs
2026-04-20 14:25:13 +02:00
michal 93e207c211 Update GenesisSmartReader for refined start/end marker logic, add _endDeltaSeconds property, and improve telegram index resolution methods. Increment assembly version to 3.9.3031.1. 2026-04-20 13:25:25 +02:00
marekf 64cafc088a Merge branch 'bugfix/Flow-meter-in-parallel-reverse-bug' into develop/SLM-PT50 2026-04-20 12:45:25 +02:00
marekf 8a0da27a07 Fixed RegValve + FlowMetersInParallel - incorrect NominalFreq sum 2026-04-20 12:39:51 +02:00
michal 5a8478e782 Refactor GenesisSmartReader Q3 calibration tests and logic:
- Expand Q3 calibration tests with diverse scenarios in `GenesisSmartReader_Q3Test`.
- Introduce channel-specific validation for calibration tests.
- Adjust methods for Q3 calibration in `GenesisSmartReader` to support per-channel factors.
- Add utilities for preparing simulation data and refinements for test consistency.
- Update `CliRunner` with improved task management, timeout handling, and logging enhancements.
2026-04-17 16:05:16 +02:00
marekf 368cd95114 Bug (fixing) RegValve OK, FlowMeterInParallel NOK 2026-04-15 10:50:27 +02:00
michal 33ef914984 Update GenesisReaderTests: add real input parsing, format variation handling, and rollover cases; remove outdated Q3 calibration tests; increment version to 3.9.3056.1 2026-04-15 09:56:54 +02:00
michal 3eb0b87f0d Add Q3 calibration tests for GenesisSmartReader and refactor test cases:
- Introduce new `GenesisSmartReader_Q3Test` class with comprehensive Q3 calibration scenarios.
- Refactor existing tests to utilize simulation data for better consistency and validation.
- Adjust Q3 calibration logic in `GenesisSmartReader` to enhance calculation accuracy and validation criteria.
2026-04-15 09:22:09 +02:00
marekf c13cb725f0 Merged branch > UniDataStorageWriter-and-UniDataStorageReader 2026-04-15 09:13:57 +02:00
marekf c6d4a016b5 Add (develop) GCI - interface of GCI and Laatzen - GciBridge component 2026-04-15 08:46:19 +02:00
marekf eda0644b4b Add (develop) GCI - interface of GCI and Laatzen 2026-04-15 08:41:27 +02:00
michal 711dfc03ef Fix Q3 calibration logic in GenesisSmartReader: replace _rawStartEndByChannel with recalculatedVariablesByChannel for accurate data computation. 2026-04-14 15:54:28 +02:00
michal 93459ccfab Refactor water meter handling in SequenceBase and FlyingStartMassCollectionSeq:
- Enhance logic for enabling/disabling channels based on water meter data.
- Introduce Q3 calibration validation in `GenesisSmartReader` with configurable thresholds.
- Update `GenesisCfgCtrl` UI to disable certain inputs.
2026-04-14 15:41:13 +02:00
marekf 5b73835d94 Add (develop) GCI - interface of GCI and Laatzen 2026-04-14 12:33:06 +02:00
michal 009aefb7c9 Refactor GenesisSmartReader and SequenceBase logic, enhance channel handling in water meters, and update GUI components:
- Adjust `Disabled` logic for water meters in `SequenceBase` allowing conditional enabling of channels based on serial number.
- Introduce `EnableShowChanels` property in `GenesisSmartReader` and integrate into `FlyingStartMassCollectionSeq`.
- Modify `StartupFlushMs` to derive configuration dynamically.
- Update `GenesisCfgCtrl` with a checkbox for enabling channels and input for flush timing.
- Increment assembly version to `3.9.3054.1`.
2026-04-14 08:03:02 +02:00
michal d2e18d615d Bump assembly version to 3.9.3053.1. 2026-04-13 14:17:56 +02:00
michal 69b475eb77 Refactor GenesisSmartReader for improved simulation and testing:
- Add `PrepareCalculatedChannelDataSimulationForTest` and supporting utility methods for generating and cloning test data.
- Update `StartupFlushMs` and sampling intervals for increased flexibility.
- Revise water meter handling logic in `FlyingStartMassCollectionSeq` to support multiple channels.
2026-04-13 14:17:34 +02:00
marekf 9848b97082 Update (develop) UniDataStorageWriter - first successful version 2026-04-13 12:21:52 +02:00
marekf 0b429ffbc2 Merge branch UniDataStorageReader 2026-04-12 10:53:53 +02:00
marekf ef146efaf0 Merge branch UniDataStorageWriter 2026-04-12 10:49:16 +02:00
marekf 431061a685 Add (developing) UniDataStorageWriter 2026-04-12 02:38:58 +02:00
marekf b6edd9c0a0 Add (developing) UniDataStoreWriter 2026-04-12 02:37:14 +02:00
michal 2210bb8240 Enhance FlyingStartMassCollectionSeq with GenesisSmartReader support; add water meter handling logic and bump assembly version to 3.9.3050.1. 2026-04-11 21:55:34 +02:00
marekf 8d7c1adf37 Update (developing) UniDataStorageReader 2026-04-11 20:49:11 +02:00
marekf 3376ac41b4 Merge branch 'develop/SLM-PT50_genesisDirectDecode' of http://87.197.120.129:33300/michal/tbf into develop/SLM-PT50 2026-04-11 11:54:37 +02:00
marekf f9181fee74 Fixed SelectComponentDlg > missing assignment sharedSearchForListBox1.ListBoxEx = availCmpntsLstBox, because of this the application crashes 2026-04-11 11:04:00 +02:00
marekf 4c77d6c06e Add (develop) GenesisCordonelInterface changed project name 2026-04-11 09:33:05 +02:00
marekf b7db39853b Add (develop) GenesisCordonelinterface add TBF.sln project name 2026-04-09 16:44:48 +02:00
marekf 66157ba988 Add (develop) GenesisCordonelinterface - changed GenesisCordonelTester to GenesisCordonelInterface, Add new FormCordonelPreadjustmentUI by Laatzen tool 2026-04-09 16:41:29 +02:00
michal 65ab19290f simulate mode - flow reached 2026-04-09 12:49:00 +02:00
marekf e7e6fc7920 Add (develop) Genesis Cordonel interface - used Laatzen tools in our new tool GenesisCordonelTester 2026-04-09 10:12:00 +02:00
michal 94c7279da1 Add localized string resources for "Test in calculation" and integrate it into FlyingStartMassCollectionSeq
- Add `Test_in_calculation` localized strings to `.resx` files for English, Slovak, and Czech.
- Update `FlyingStartMassCollectionSeq` to display the new activity message during the calculation phase.
2026-04-09 10:08:50 +02:00
michal 2136695b80 Fix Stop 2026-04-08 16:37:27 +02:00
michal c0f9b0ffa0 Refactor GenesisSmartReader stop logic for enhanced thread safety and post-processing; bump assembly version to 3.9.3047.1. 2026-04-08 15:44:59 +02:00
michal c4ef22436f Update GenesisSmartReader Stop logic and bump assembly version to 3.9.3045.1:
- Refactor `Stop` for improved thread safety using `lock` on `_stateSync` and `_stopSync`.
- Enhance `Stop` behavior by introducing state updates during immediate stop requests.
- Fix redundant operations and reorganize log and processing statements for clarity.
- Update `RequestImmediateStopInternal` to accept a new state parameter.
2026-04-08 15:06:45 +02:00
michal 0b73871518 Refactor GenesisSmartReader and add advanced test utilities:
- Introduce `ForceProcessingEnabled`, `InstallSuccessfulProcessingOverride`, and `EnqueueIncomingLine` helpers to streamline test setup.
- Add new test suite `GenesisSmartReaderStopTests` to validate `Stop` behavior for different processing states.
- Enhance `GenesisSmartReader` state management with a dedicated `Reset` method and improved `Stop` logic.
- Improve buffer initialization in `ProcessOptoLine` to address null-value edge cases.
- Update assembly version to `3.9.3043.1`.
2026-04-08 13:51:55 +02:00
michal f7dc6fee5e immediate Stop improve 2026-04-07 16:37:19 +02:00
michal ba848a167d improve 2026-04-07 15:44:32 +02:00
michal dcb0bf9a88 Update GenesisSmartReader logging and bump assembly version to 3.9.3040.1:
- Enhance `Stop` logs with `registerReader` and `optoSerialPort` details for improved traceability.
2026-04-07 15:27:20 +02:00
michal f6433134c1 Add WaitForStartupFlushToFinish and EnableProcessingLoopForTests utilities for improved test reliability:
- Incorporate `WaitForStartupFlushToFinish` in `GenesisSmartReaderThreadedReadTests` to ensure startup flush completion before processing.
- Introduce `EnableProcessingLoopForTests` in `GenesisSmartReaderTest` to streamline test initialization.
- Fix thread-safety issues in startup flush handling and reorder operations in `GenesisSmartReader.Stop`.
- Bump assembly version to `3.9.3039.1`.
2026-04-07 14:45:37 +02:00
michal 7e3df77b5b Refactor GenesisSmartReader processing logic and tests for robust handling:
- Replace property getters with concise expressions in `GenesisSmartReader`.
- Implement FIFO and concurrent producer-consumer unit tests for `ProcessingLoop`.
- Add startup flush mechanism for improved input handling on initialization.
- Refactor processing loop with structured start/stop tasks and cancellation tokens.
- Fix thread-safety issues and enhance logic in `Stop` and `StopDataStreamProcessing`.
- Add internal visibility for tests in `TBFTests` via `InternalsVisibleTo`.
- Update assembly version to `3.9.3038.1`.
2026-04-07 10:42:27 +02:00
marekf ff5954d85b Bugfix FlowMeterInParallel and RegValve OUT OF RANGE regulation part1 2026-04-02 12:02:26 +02:00
michal 08dff1272b Add overflow volume/timestamp handling and related tests for GenesisSmartReader:
- Extend `CalibrationRecord` constructor to include overflow fields (`OverflowVolumeCm`, `OverflowTimeS`).
- Implement additional unit tests in `OptoTelegramRawTest` to validate multi-rollover scenarios and extended calculations.
- Add `GenesisSmartReaderThreadedReadTests` to cover threaded read-loop functionality and serial processing.
- Enhance logging in `Pump` classes (`TurnOff` methods).
- Update project file to include new test files.
2026-04-02 09:25:17 +02:00
marekf c09e266b2c Bugfix ComponentPropertiesDlg frm size 140x80 2026-04-01 12:12:04 +02:00
michal d5c8fd1c8f Refactor GenesisSmartReader with enhanced channel-specific volume and timestamp processing:
- Replace `AverageNullable` with `AverageCachedTime` and introduce `GetCachedVolume`/`GetCachedTime` helpers.
- Add recalibration logic with `_recalculatedStartEndByChannel` and `_rawStartEndByChannel`.
- Implement `PrepareCalculatedChannelData` for refined multi-channel synchronization.
- Add unit tests (`GenesisSmartReaderChannelTests`) for improved coverage and validation of new methods.
- Update `OptoTelegramRaw.Copy` to fix field assignment issues.
2026-03-27 15:12:43 +01:00
michal a34740e838 Update GenesisSmartReaderTest to handle DiscardOutCalls and configure block repetition resets:
- Modify tests to validate `DiscardOutCalls` alongside `DiscardInCalls`.
- Add support for configuring `ResetAfterBlockRepetitions` in test initialization.
2026-03-27 13:19:10 +01:00
michal 836ba5a41f Refactor GenesisSmartReader processing logic and add advanced multi-channel volume and timestamp handling:
- Introduce `TimeDeltaPerChannel` and `GroupRecordsPerChannel` for calculating time and volume deltas across channels.
- Add recalibration logic to synchronize volume and timestamp data with channel-specific maxima.
- Fix `ResetDataBuffer` implementation and replace redundant `ReasetDataBuffer` calls.
- Update handling of flow markers and block completion logic for improved buffer management.
- Add `Copy` method to `OptoTelegramRaw` for efficient cloning and recalculation.
- Bump `AssemblyVersion` and `AssemblyFileVersion` to `3.9.3019.1`.
2026-03-27 13:18:37 +01:00
marekf 545a747ebc Add UniDataStorageWriter component - initial commit 2026-03-27 12:49:35 +01:00
michal 23c83ab452 Enhance GenesisSmartReader multi-channel processing and add tests for telegram handling:
- Refactor `ProcessOptoLine` logic to support output buffer resets at configurable repetition intervals.
- Add tests:
  - `ProcessOptoLine_Block_f_h1_h2_h3_f_ShouldResetBuffersOnlyAfterLastF`
  - `ReadOptoData_FullBlock_ShouldDiscardBuffersAfterClosingF`
  - `ProcessOptoLine_LastF_AfterH3_ShouldRequestBufferReset`
  - `ProcessOptoLine_ShouldReset
2026-03-26 14:45:36 +01:00
michal bc9e583c29 FIX Add ChannelAverages to project and remove unused imports in GenesisHead. 2026-03-26 07:29:13 +01:00
michal 18cef617da Enhance Genesis SmartReader with channel-specific volume and timestamp tracking:
- Add per-channel `VolumeStart`, `VolumeEnd`, `TimeStart`, and `TimeEnd` processing for improved multi-channel support.
- Introduce `ChannelAverages` for centralized average calculations across channels.
- Refactor `GenesisSmartReader` to improve data decoding and logging for calibration and flow test records.
- Add telemetry unwrapping helpers for precise volume and timestamp calculations.
- Update `OptoTelegramRaw` to support channel-specific updates and robust data processing.
- Update `AssemblyVersion` and `AssemblyFileVersion` to `3.9.3013.1`.
2026-03-26 07:19:22 +01:00
michal 4ee736cff5 Add advanced multi-channel processing for Genesis SmartReader:
- Refactor `VolumeLtrStart` and `VolumeLtrEnd` to compute channel-specific averages at test boundaries.
- Implement `ProcessOptoLine_ShouldInsertTelegrams` tests for validating telegram insertion and channel updates.
- Add `VolumeLtrEnd_ShouldAverageOnlyAvailableChannels` to handle partial channel data gracefully.
- Introduce `VolumeFromChannelsAtStart` and `VolumeFromChannelsAtEnd` helpers for refined channel data handling.
- Update Opto telegram processing logic to support robust volume and timestamp unwrapping.
- Enhance `GenesisSmartReader.ToString()` for clearer configuration representation.
- Add `OptoTelegramRawTest` to validate telegram behavior, flag handling, and delta computations.
- Update `AssemblyVersion` to `3.9.3012.1`.
2026-03-24 15:23:06 +01:00
michal 4e40dc8601 Comment out Detect_BaudRate_By_ViewFactoryId test method in TouchReadBaudRateDetectionTests. 2026-03-24 10:07:24 +01:00
michal a5a68ec457 Refactor Genesis serial communication handling (ISerialDriver interface compatible with SerialPort):
- Replace method-based `IsOpen()` checks with property-based `IsOpen` usage in `ISerialDriver` and related classes.
- Simplify `Open()` and add `Close()` methods to unify connection management.
- Introduce `InitializeThreeChannelState()` helper in `GenesisSmartReaderTest` for multi-channel state initialization.
- Adjust calculations in `CalculateTimeByChannels()` and `CalculateVolumeByChannels()` for clarity.
- Add new tests for multi-channel processing and validation of updated telegram handling logic.
2026-03-24 10:04:30 +01:00
michal e26d88d437 Refactor Genesis use 3 chanels reader structure:
- Replace `Xylem.Common` namespace references with `TBF.Rig.RegisterReaders`.
- Introduce `GenesisSmartReaderTest` and `FakeSerialDriver` for unit testing.
- Revamp `FlowDirectionDetection` to support multiple channels.
- Make constants in `StreamingDecoder` public for enhanced accessibility.
- Update `AssemblyVersion` and `AssemblyFileVersion` to `3.9.3010.1`.
2026-03-24 08:53:16 +01:00
marekf 98ba7d2dee Update UniDataStorageReader developing 2026-03-19 19:28:16 +01:00
marekf 78835c1af6 Add UniDataStorageReader initial commit 2026-03-19 16:30:32 +01:00
michal 2522d1da9f Add Genesis radio/opto communication and configuration components
- Replace `OpthoHeadService` with `RadioService` to enhance Genesis communication handling via serial interface.
- Introduce `OptoReceivedEventArgs` to manage event-driven communication.
- Add foundational protocol implementations (`BaseProtocol`, `IProtocol`, etc.) for Genesis configuration and processing.
- Implement `SirtConfig`, `MeterConfig`, and `ProcessConfig` classes for flexible Genesis configuration management.
- Expand Genesis framework to support diagnostics, activity modes, and version retrieval.
2026-03-18 15:57:04 +01:00
michal 8983c01591 Add integration tests for IperlHat communication and diagnostic LED parsing
- Implement `IperlHatIntegrationTests` to validate serial communication, diagnostic LED states, and Opto data handling.
- Add `HexFormatterTest` for verifying Hex formatting utilities.
- Add `DiagnosticLedParserTest` to test parsing for all diagnostic LED states.
2026-03-17 12:15:52 +01:00
michal 61b1320d4f Add Genesis COM port configuration and update Genesis meter setup logic
- Introduce `OptoComPortNr` and `HeadComPortNr` properties in `GenesisHead`.
- Update `SetupGenesisMeter` to use new COM port configurations.
- Add reference to `Xylem.Common.Hardware.WaterMeter.Genesis.GenesisConfig`.
2026-03-16 15:48:41 +01:00
michal 2c8a0922d1 Genesis dialog with Com ports 2026-03-16 15:47:12 +01:00
michal b078d9586d Add detailed logging for Genesis operations in FlyingStartMassCollectionSeq and FlyingStartSeq.
- Introduce debug and info logs for Genesis initialization, calibration, measurement, and data storage.
- Ensure proper logging during Genesis operations' start and stop phases.
2026-03-16 15:42:45 +01:00
michal 6e35de5f4d Update AssemblyVersion and AssemblyFileVersion to 3.9.3001.1 2026-03-16 15:36:25 +01:00
michal 2dbe4cded4 Genesis slot max count 10, Add DB writer component
Integrate `DatabaseWriter` component and update GenesisHead configuration:

- Add `DatabaseWriter` factory and related classes (`WriteDBCfgCtrl`, `WriterCfg`, `WritingToDB`).
- Extend GenesisHead slot number range to 1-10 and adjust UI layout accordingly.
2026-03-16 15:36:02 +01:00
michal 10865c6878 Update AssemblyVersion and AssemblyFileVersion to 3.9.3000.1 2026-03-16 15:35:06 +01:00
michal ee1353fe54 Add DatabaseWriter component:
- Implement `DatabaseWriter` factory and configuration logic.
- Add `WriteDbCfgCtrl` for component configuration UI.
- Introduce database connection handling, network adapter selection, and tracing record management.
2026-03-16 15:31:58 +01:00
michal 5cefd7fc3a Integrate Genesis meter support into FlyingStartMassCollectionSeq:
- Add initialization, calibration, and measurement logic for Genesis meters.
- Extend result handling to capture Genesis-specific data (timestamps, volumes, errors).
- Ensure proper cleanup of Genesis meters at test conclusion.
2026-03-16 15:31:49 +01:00
michal ab0e5ea82a Genesis heap 2026-03-16 15:17:56 +01:00
michal b2276c086c Merge branch 'develop/SLM-PT50' into develop/Mexico 2026-03-16 13:33:21 +01:00
michal 4fa462661c Merge remote-tracking branch 'remotes/private-origin/develop/SLM-PT50' into develop/Goeteborg 2026-03-16 13:11:34 +01:00
michal 2b30d0cf34 packages add/clean up 2026-03-13 13:51:42 +01:00
michal df2c56e6c4 FIX NaN - devide by zero
Add simulation logic to `StandingStartStopTestOp` and handle zero `TestTime` edge cases in `FlyingStartMassCollectionSeq`
2026-03-12 16:25:26 +01:00
michal a997c3d7be iPerl ASIC - implementation 2026-03-12 14:50:47 +01:00
marekf 3c964f2aab BugFix - repair test RegisterReader deserialize - FSMC method as in SSMC 2026-03-09 14:37:51 +01:00
marekf c445b3e098 BugFix - after applying the transition valve 40% in procedure-purge begin, the RegValve was not regulating correctly after starting the test 2026-03-09 14:20:37 +01:00
marekf 2ea2a2592e Merge branch 'feature/task_6-add-new-admins' into develop/SLM-PT50 2026-03-04 19:14:22 +01:00
marekf 27072598cd Merge branch 'feature/task_5-new-app-icon' into develop/SLM-PT50 2026-03-04 19:14:02 +01:00
marekf 4a2d4772d7 Add new icon to forms of application 2026-03-04 18:48:46 +01:00
marekf 2bf00606d3 Merge branch 'feature/task_4-ListView-ListBox-navigation' into develop/SLM-PT50 2026-03-04 18:42:25 +01:00
marekf 3773e1d96c Merge branch 'feature/task_3-new-pump-grundfoss' into develop/SLM-PT50 2026-03-04 18:42:15 +01:00
marekf 19ddc753c7 Merge branch 'feature/task_2-dripping-on-the-scale-meassure-mass' into develop/SLM-PT50 2026-03-04 18:33:20 +01:00
marekf 0c41c750e8 Add parameter and management IsOffline for components: Modbus>TempMeter>Groch, Meret and Modbus>PressureMeter>Meret and Rig>Keithley>TempMeter and add indicator redColor itemValue for SchematicDrawing 2026-03-04 18:25:28 +01:00
marekf 4bfd5c8e11 Update output printer OnePerMeter - 1. printer configuration in PrintResultsSelectionDlg, 2. document preview, 3. edit document name, 4. enable more save destinations 2026-03-04 13:21:45 +01:00
marekf f0fb70e38b Add new type of pump - Grundfoss 2026-03-04 11:54:39 +01:00
marekf 6e5bcafec0 Update dripping on the scale and measuring the start mass 2026-03-04 10:37:35 +01:00
marekf d5cb18c910 Fixed incorrect loading of RegisterReader in the StandingStartMassCollection method, when the RR Units and RR Quantity variables in results were filled with only string.empty 2026-03-04 09:44:42 +01:00
marekf 3a2069b255 Refactor variable naming in results in response to mass meter pulse processing 2026-03-04 09:42:55 +01:00
marekf 80c651dbfd Add search and navigation as UI>shared for ListBox-es and ListView-s to components listview, components listbox, procedure listview 2026-02-25 13:22:56 +01:00
marekf 5a3b9b57c4 Add new admins marek and michal 2026-02-25 10:02:09 +01:00
marekf ead350eef3 Add initial MassMeter implementation 2026-02-19 19:22:02 +01:00
marekf b43657a4db Initial import from local copy 2026-02-19 12:02:07 +01:00
michal 8fffb9e366 Update clean.bat to remove build artifacts for AppDiagnostic, NfcC7_DLL, NfcC7_DLL.Tests, and TBFTests 2026-01-23 11:04:23 +01:00
michal 0fdd229141 Refactor IPerlImplHeadTestCtrl to use descriptive enums for operations, update command mappings, introduce IPerlASICCorrections, and improve test configuration checks and NFC handling. 2026-01-23 10:51:47 +01:00
michal 2f1ada3f5b Add DiagnosticLedState classes for States #4-#7, enums for PipeStatus and SpikeDetectionStatus, and implement TouchReadProtocol commands, frames, and builders. 2026-01-19 08:10:36 +01:00
michal 988a8c6642 Update NuGetToolVersion to 7.0.0 in SharedComponents project configuration 2025-12-07 11:27:17 +01:00
michal 42ee9eaee3 Poseidon - Flying - Opto communication
Enhance `FlyingStartMassCollectionSeq` with Poseidon reader support, detailed error handling, and improved test data processing.
2025-12-07 11:27:00 +01:00
michal 3f28051d79 Update .gitignore to exclude NfcC7_DLL.Tests build artifacts 2025-12-07 10:59:19 +01:00
michal 65f13cc868 Add UNIHeadTestCtrl interface and implement IperlHeadTestCtrl with command handling logic. 2025-12-06 12:51:51 +01:00
michal cd8ba3cda3 Add System.Threading 4.3.0 package with support for multiple frameworks and localized XML documentation. 2025-12-06 12:49:58 +01:00
michal a5936c8f19 Fix standing start Store Data
Refactor to use `ICommonRegReader` in `StandingStartSeq` and update parameter handling in `SmartCommunicationSeq`.
2025-12-06 12:48:42 +01:00
michal 2842425673 Add simulation logic for test operations and water metrology, enhance UI initialization, and implement optohead flow rate handling.
- Introduced simulation timers in `FlyingStartStopTestOp`.
- Added `Simulate` methods in `WaterMetrologyData`, `WaterMetrologyDataC7`, and `WaterMetrologyDataC2`.
- Enhanced `SmartCommunicationForm` with textbox resizing logic and initialization code.
- Implemented flow rate and volume calculation from optohead telemetry in `SmartReader`.
- Added unit tests for Poseidon correction parsing.
2025-12-06 12:46:12 +01:00
michal 06a401a1ff bugFix - Morrisville - SerialNr, WMBegginValue
Add support for `ICommonRegReader` interface, enhance `ISmartReader` functionality, and update water meter state handling logic across multiple classes.
2025-11-21 14:14:23 +01:00
michal 3a2fc50fec PurchaseOrderDialog logger Improvement - WRCSwindon
Add detailed logging for purchase order workflows in `CycleBeginningForm`

- Introduced `log_selected` for enhanced debugging of selected orders, items, and serial numbers.
- Added exception handling and logging for serial number loading.
- Improved visibility into runtime behavior with contextual debug and error logs.
2025-11-21 14:12:12 +01:00
michal 38a58ed8b7 Scale rainnig solution, see:
Inplementing mass collection update.
Update AssemblyVersion to 3.9.2149.4, refactor mass collection sequence in StandingStartMassCollection, extend PoseidonCmd with enhanced configuration and dialog options, and improve task handling and UI automation.
2025-11-19 16:44:21 +01:00
michal c7cba20de0 Add ReadStableMassOp unit test and enhance MettlerToledo Scale support
- Introduced `ReadStableMassOpTest` to validate mass reading operations.
- Refactored `Scale` to support `ISerialPort` for improved testability.
- Added `SerialPortDevice` and `SerialPortDeviceFake` implementations, with `GetComponent` overload in `Factory`.
- Enhanced `StateMachine` with `InitializeBoardEtc_Fake` for custom setups.
- Updated project files to include new classes.
2025-11-19 16:28:23 +01:00
michal 6311df49c3 Inplementing mass collection update.
Update AssemblyVersion to 3.9.2149.4, refactor mass collection sequence in StandingStartMassCollection, extend PoseidonCmd with enhanced configuration and dialog options, and improve task handling and UI automation.
2025-11-18 10:59:21 +01:00
michal f1d32a39ba ver 3.9.2149.2 ReadPulses added in Run()
Update AssemblyVersion to 3.9.2149.2 and enhance PoseidonReader with new timing and task-tracking functionality.
2025-11-13 15:58:09 +01:00
michal 224b53f774 Ver 3.9.2149.1 - Update AssemblyVersion and AssemblyFileVersion 2025-11-13 14:39:07 +01:00
michal 1e2777c5e7 Enable ShowDialog for PoseidonCmd, add UI improvements in TestStartEndForm, and introduce new methods for handling reader data and controls. 2025-11-13 14:21:23 +01:00
michal c3c21649fb new Test method Poseidon => StandingStartMassCollectionPoseidon
Add support for `StandingStartMassCollectionPoseidon` test methods.

- Introduced `Compound`, `HeatMeters`, and `Single` components for `StandingStartMassCollectionPoseidon`.
- Included factories, configuration classes, and test parameters for new test methods.
- Updated `TBF.csproj` to include new files and resources.
2025-11-12 13:42:19 +01:00
michal 99ccabc142 Smart meters support MainWnd dialog (must be finished - mouse right click), test supports Poseidon RegisterReader
Refactor `ISmartReader` and related classes to support `SmartHeadsUni`. Remove redundant `IperlHeadsUni` references and introduce `SmartMeterFlyingStartMassCollection` functionality. Enhance regex-based head position extraction.
2025-11-12 12:34:29 +01:00
michal f1e6e94450 work backup
- Register Reader Poseidon works
- Smart Form - wrong functionality
- refactoring
2025-11-06 14:11:13 +01:00
michal 320cddb177 compatible version to TBF
Refactor `NfcHanler` namespace to `NfcHandler` and enhance code consistency.
2025-10-29 16:05:21 +01:00
michal 42002e3fa4 iPerl Fixes 2025-10-27 11:49:42 +01:00
2802 changed files with 2746785 additions and 18367 deletions
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Root Type="DevExpress.CodeRush.Foundation.CodePlaces.Options.FavoritesListContainer">
<Options Language="Neutral">
<Groups />
</Options>
</Root>
+76 -1
View File
@@ -1,3 +1,10 @@
# Generated build outputs for all projects, including Debug/Release archives.
[Bb]in/
[Oo]bj/
# Generated test results and hardware diagnostic captures.
[Tt]est[Rr]esults/
AppDiagnostic/bin/
AppDiagnostic/obj/
Common/bin/
@@ -34,6 +41,8 @@ MergeResultsDBs/bin/
MergeResultsDBs/obj/
NfcC7_Dll/bin/
NfcC7_Dll/obj/
NfcC7_DLL.Tests/bin/
NfcC7_DLL.Tests/obj/
OrderManagement/bin/
OrderManagement/obj/
ProductionTracing/bin/
@@ -88,4 +97,70 @@ Users/obj/
.idea/
*.suo
*.bak
TBF.sln.DotSettings.user
TBF.sln.DotSettings.user
/ExternalProjects
/GenesisCordonelTester
/packages/*
!/packages/Common/
/GenesisCordonelInterface/bin/Debug/GenesisCordonelInterfaceLogger.log
/GenesisCordonelInterface/bin/Debug/GenesisCordonelTesterLogger.log
/GenesisCordonelInterface/bin/Debug/SirtConfig.json
/GenesisCordonelInterface/bin/Release
/GenesisCordonelInterface/obj/Debug
/GenesisCordonelInterface/obj/Release
/ExternalProjects
/AppDiagnostic.Logging/bin/Debug/net472/AppDiagnostic.Logging.dll
/AppDiagnostic.Logging/bin/Debug/net472/AppDiagnostic.Logging.pdb
/AppDiagnostic.Logging/obj/Debug/net472/AppDiagnostic.Logging.AssemblyInfo.cs
/AppDiagnostic.Logging/obj/Debug/net472/AppDiagnostic.Logging.AssemblyInfoInputs.cache
/AppDiagnostic.Logging/obj/Debug/net472/AppDiagnostic.Logging.csproj.AssemblyReference.cache
/AppDiagnostic.Logging/obj/Debug/net472/AppDiagnostic.Logging.dll
/AppDiagnostic.Logging/obj/Debug/net472/AppDiagnostic.Logging.pdb
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Metrology.Measurements.dll
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Metrology.Measurements.pdb
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Ui.CordonelPreadjustmentUi.dll
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Ui.CordonelPreadjustmentUi.pdb
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Ui.GenesisToolBox.exe
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Ui.GenesisToolBox.pdb
/GenesisCordonelInterface/bin/Debug/XylemCommonUiLegacyGenCtl.dll
/GenesisCordonelInterface/bin/Debug/XylemCommonUiLegacyGenCtl.pdb
/GenesisCordonelInterface/bin/Debug/de/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisFile.resources.dll
/GenesisCordonelInterface/bin/Debug/GenesisCordonelInterface.exe
/GenesisCordonelInterface/bin/Debug/GenesisCordonelInterface.pdb
/GenesisCordonelInterface/bin/Debug/TempFlansh.exe
/GenesisCordonelInterface/bin/Debug/TempFlansh.pdb
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.Interfaces.Protocols.ProtocolCore.dll
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.Interfaces.Protocols.ProtocolCore.pdb
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.Interfaces.Protocols.TransmitProtocol.dll
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.Interfaces.Protocols.TransmitProtocol.pdb
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.eRegister.eRegisterCore.dll
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.eRegister.eRegisterCore.pdb
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.DataPackages.dll
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.DataPackages.pdb
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.dll
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.pdb
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisFile.dll
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisFile.pdb
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisPwd.dll
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisPwd.pdb
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.RequestProtocol.dll
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.RequestProtocol.pdb
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.StreamingProtocol.dll
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.StreamingProtocol.pdb
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.MagFlux.MagFluxConfig.dll
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.MagFlux.MagFluxConfig.pdb
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.MagFlux.MagFluxCore.dll
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.MagFlux.MagFluxCore.pdb
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Logic.RelatePcb.dll
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Logic.RelatePcb.pdb
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Logic.ServiceCore.dll
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Logic.ServiceCore.pdb
/GenesisCordonelInterface/RuntimePackage/Package
/SharedComponents/obj/SharedComponents.csproj.nuget.g.props
/AppDiagnostic.Logging/obj/Release/net472/AppDiagnostic.Logging.AssemblyInfo.cs
/AppDiagnostic.Logging/obj/Release/net472/AppDiagnostic.Logging.AssemblyInfoInputs.cache
/AppDiagnostic.Logging/obj/Release/net472/AppDiagnostic.Logging.csproj.AssemblyReference.cache
/AppDiagnostic.Logging/obj/Release/net472/AppDiagnostic.Logging.dll
/AppDiagnostic.Logging/obj/Release/net472/AppDiagnostic.Logging.pdb
/AppDiagnostic.Logging/bin/Release/net472/AppDiagnostic.Logging.dll
/AppDiagnostic.Logging/bin/Release/net472/AppDiagnostic.Logging.pdb
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<LangVersion>7.3</LangVersion>
<RootNamespace>AppDiagnostic.Logging</RootNamespace>
<AssemblyName>AppDiagnostic.Logging</AssemblyName>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<ProjectGuid>{A7AF8ED2-362E-4DBB-B607-EBA2820719EC}</ProjectGuid>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NLog" Version="5.2.2" />
<Reference Include="System.Runtime.Serialization" />
</ItemGroup>
</Project>
@@ -0,0 +1,41 @@
using NLog;
using NLog.Config;
using NLog.Layouts;
namespace AppDiagnostic.Logging
{
public static class AppDiagnosticConnector
{
private static readonly object syncRoot = new object();
public static void Register()
{
lock (syncRoot)
{
LoggingConfiguration configuration = LogManager.Configuration ?? new LoggingConfiguration();
NLog.Targets.Target existingTarget = configuration.FindTargetByName("appDiagnostic");
if (existingTarget != null)
{
return;
}
var target = new AppDiagnosticTarget
{
Name = "appDiagnostic",
PipeName = DiagnosticProtocol.DefaultPipeName,
QueueLimit = DiagnosticProtocol.DefaultQueueLimit,
ReconnectDelayMilliseconds = DiagnosticProtocol.DefaultReconnectDelayMilliseconds,
ConnectTimeoutMilliseconds = DiagnosticProtocol.DefaultConnectTimeoutMilliseconds,
Layout = new SimpleLayout("${message}")
};
configuration.AddTarget(target.Name, target);
configuration.AddRule(LogLevel.Trace, LogLevel.Fatal, target, "*");
LogManager.Configuration = configuration;
LogManager.ReconfigExistingLoggers();
}
}
}
}
@@ -0,0 +1,233 @@
using NLog;
using NLog.Targets;
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;
namespace AppDiagnostic.Logging
{
[Target("AppDiagnostic")]
public sealed class AppDiagnosticTarget : TargetWithLayout
{
private readonly object connectionLock = new object();
private readonly string applicationName;
private readonly int processId;
private BlockingCollection<DiagnosticLogEntry> queue;
private CancellationTokenSource cancellation;
private Task senderTask;
private NamedPipeClientStream pipeClient;
public AppDiagnosticTarget()
{
using (Process process = Process.GetCurrentProcess())
{
applicationName = process.ProcessName;
processId = process.Id;
}
PipeName = DiagnosticProtocol.DefaultPipeName;
QueueLimit = DiagnosticProtocol.DefaultQueueLimit;
ReconnectDelayMilliseconds =
DiagnosticProtocol.DefaultReconnectDelayMilliseconds;
ConnectTimeoutMilliseconds =
DiagnosticProtocol.DefaultConnectTimeoutMilliseconds;
}
public string PipeName { get; set; }
public int QueueLimit { get; set; }
public int ReconnectDelayMilliseconds { get; set; }
public int ConnectTimeoutMilliseconds { get; set; }
protected override void InitializeTarget()
{
base.InitializeTarget();
int queueLimit = QueueLimit > 0
? QueueLimit
: DiagnosticProtocol.DefaultQueueLimit;
queue = new BlockingCollection<DiagnosticLogEntry>(queueLimit);
cancellation = new CancellationTokenSource();
senderTask = Task.Factory.StartNew(
ProcessQueue,
cancellation.Token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
}
protected override void Write(LogEventInfo logEvent)
{
if (logEvent == null || queue == null || queue.IsAddingCompleted)
{
return;
}
var entry = new DiagnosticLogEntry
{
TimestampUtc = logEvent.TimeStamp.ToUniversalTime(),
MachineName = Environment.MachineName,
ApplicationName = applicationName,
ProcessId = processId,
LoggerName = logEvent.LoggerName ?? string.Empty,
Level = logEvent.Level?.Name ?? string.Empty,
Message = Layout != null
? Layout.Render(logEvent)
: logEvent.FormattedMessage,
Exception = logEvent.Exception?.ToString() ?? string.Empty
};
try
{
queue.TryAdd(entry);
}
catch (ObjectDisposedException)
{
// A concurrent NLog shutdown may dispose the queue.
}
catch (InvalidOperationException)
{
// A concurrent NLog shutdown may complete the queue.
}
}
protected override void CloseTarget()
{
BlockingCollection<DiagnosticLogEntry> currentQueue = queue;
CancellationTokenSource currentCancellation = cancellation;
if (currentQueue != null)
{
currentQueue.CompleteAdding();
}
if (currentCancellation != null)
{
currentCancellation.Cancel();
}
CloseConnection();
try
{
senderTask?.Wait(2000);
}
catch (AggregateException)
{
// Cancellation during application shutdown is expected.
}
currentQueue?.Dispose();
currentCancellation?.Dispose();
queue = null;
cancellation = null;
senderTask = null;
base.CloseTarget();
}
private void ProcessQueue()
{
CancellationToken token = cancellation.Token;
try
{
foreach (DiagnosticLogEntry entry in
queue.GetConsumingEnumerable(token))
{
SendWhenConnected(entry, token);
}
}
catch (OperationCanceledException)
{
// Target shutdown is expected to cancel the sender.
}
}
private void SendWhenConnected(
DiagnosticLogEntry entry,
CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
EnsureConnected();
DiagnosticMessageSerializer.WriteFramed(pipeClient, entry);
return;
}
catch (Exception exception) when (
exception is IOException ||
exception is TimeoutException ||
exception is InvalidOperationException)
{
CloseConnection();
int delay = ReconnectDelayMilliseconds > 0
? ReconnectDelayMilliseconds
: DiagnosticProtocol.DefaultReconnectDelayMilliseconds;
if (token.WaitHandle.WaitOne(delay))
{
return;
}
}
}
}
private void EnsureConnected()
{
lock (connectionLock)
{
if (pipeClient != null && pipeClient.IsConnected)
{
return;
}
CloseConnectionWithoutLock();
pipeClient = new NamedPipeClientStream(
".",
string.IsNullOrWhiteSpace(PipeName)
? DiagnosticProtocol.DefaultPipeName
: PipeName,
PipeDirection.Out,
PipeOptions.Asynchronous);
int timeout = ConnectTimeoutMilliseconds > 0
? ConnectTimeoutMilliseconds
: DiagnosticProtocol.DefaultConnectTimeoutMilliseconds;
pipeClient.Connect(timeout);
}
}
private void CloseConnection()
{
lock (connectionLock)
{
CloseConnectionWithoutLock();
}
}
private void CloseConnectionWithoutLock()
{
if (pipeClient == null)
{
return;
}
pipeClient.Dispose();
pipeClient = null;
}
}
}
@@ -0,0 +1,33 @@
using System;
using System.Runtime.Serialization;
namespace AppDiagnostic.Logging
{
[DataContract]
public sealed class DiagnosticLogEntry
{
[DataMember(Order = 1)]
public DateTime TimestampUtc { get; set; }
[DataMember(Order = 2)]
public string MachineName { get; set; }
[DataMember(Order = 3)]
public string ApplicationName { get; set; }
[DataMember(Order = 4)]
public int ProcessId { get; set; }
[DataMember(Order = 5)]
public string LoggerName { get; set; }
[DataMember(Order = 6)]
public string Level { get; set; }
[DataMember(Order = 7)]
public string Message { get; set; }
[DataMember(Order = 8)]
public string Exception { get; set; }
}
}
@@ -0,0 +1,113 @@
using System;
using System.IO;
using System.Runtime.Serialization.Json;
namespace AppDiagnostic.Logging
{
public static class DiagnosticMessageSerializer
{
private static readonly DataContractJsonSerializer Serializer =
new DataContractJsonSerializer(typeof(DiagnosticLogEntry));
private static readonly object SerializerLock = new object();
public static byte[] Serialize(DiagnosticLogEntry entry)
{
if (entry == null)
{
throw new ArgumentNullException(nameof(entry));
}
using (var stream = new MemoryStream())
{
lock (SerializerLock)
{
Serializer.WriteObject(stream, entry);
}
return stream.ToArray();
}
}
public static DiagnosticLogEntry Deserialize(byte[] payload)
{
if (payload == null)
{
throw new ArgumentNullException(nameof(payload));
}
if (payload.Length == 0 ||
payload.Length > DiagnosticProtocol.MaximumMessageSize)
{
throw new InvalidDataException("Invalid diagnostic message size.");
}
using (var stream = new MemoryStream(payload, false))
{
lock (SerializerLock)
{
return (DiagnosticLogEntry)Serializer.ReadObject(stream);
}
}
}
public static void WriteFramed(Stream stream, DiagnosticLogEntry entry)
{
if (stream == null)
{
throw new ArgumentNullException(nameof(stream));
}
byte[] payload = Serialize(entry);
if (payload.Length > DiagnosticProtocol.MaximumMessageSize)
{
throw new InvalidDataException("Diagnostic message is too large.");
}
byte[] length = BitConverter.GetBytes(payload.Length);
stream.Write(length, 0, length.Length);
stream.Write(payload, 0, payload.Length);
stream.Flush();
}
public static DiagnosticLogEntry ReadFramed(Stream stream)
{
if (stream == null)
{
throw new ArgumentNullException(nameof(stream));
}
byte[] lengthBuffer = ReadExactly(stream, sizeof(int));
int payloadLength = BitConverter.ToInt32(lengthBuffer, 0);
if (payloadLength <= 0 ||
payloadLength > DiagnosticProtocol.MaximumMessageSize)
{
throw new InvalidDataException("Invalid diagnostic message size.");
}
return Deserialize(ReadExactly(stream, payloadLength));
}
private static byte[] ReadExactly(Stream stream, int count)
{
byte[] buffer = new byte[count];
int offset = 0;
while (offset < count)
{
int bytesRead = stream.Read(buffer, offset, count - offset);
if (bytesRead == 0)
{
throw new EndOfStreamException();
}
offset += bytesRead;
}
return buffer;
}
}
}
@@ -0,0 +1,11 @@
namespace AppDiagnostic.Logging
{
public static class DiagnosticProtocol
{
public const string DefaultPipeName = "Sensus.AppDiagnostic.LogPipe";
public const int MaximumMessageSize = 1024 * 1024;
public const int DefaultQueueLimit = 10000;
public const int DefaultReconnectDelayMilliseconds = 500;
public const int DefaultConnectTimeoutMilliseconds = 250;
}
}
+37
View File
@@ -0,0 +1,37 @@
# AppDiagnostic.Logging
Reusable NLog target for sending structured diagnostic events from multiple
.NET Framework processes to `AppDiagnostic.exe` through a local named pipe.
## Producer integration
Reference this project from the producer application and add the extension and
target to its NLog configuration:
```xml
<extensions>
<add assembly="AppDiagnostic.Logging" />
</extensions>
<targets>
<target name="appDiagnostic"
xsi:type="AppDiagnostic"
layout="${message}" />
</targets>
<rules>
<logger name="*" minlevel="Trace" writeTo="appDiagnostic" />
</rules>
```
The target writes to a bounded background queue. Producer threads are not
blocked while the diagnostic viewer is unavailable. Queued messages are sent
after the viewer becomes available; new messages are discarded when the queue
limit is reached.
The receiver can read messages with:
```csharp
DiagnosticLogEntry entry =
DiagnosticMessageSerializer.ReadFramed(namedPipeServerStream);
```
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,65 @@
{
"format": 1,
"restore": {
"C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj": {}
},
"projects": {
"C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj",
"projectName": "AppDiagnostic.Logging",
"projectPath": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj",
"packagesPath": "C:\\Users\\MFRNIAK\\.nuget\\packages\\",
"outputPath": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\obj\\",
"projectStyle": "PackageReference",
"fallbackFolders": [
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
],
"configFilePaths": [
"C:\\Users\\MFRNIAK\\AppData\\Roaming\\NuGet\\NuGet.Config",
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config",
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
],
"originalTargetFrameworks": [
"net472"
],
"sources": {
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
"https://api.nuget.org/v3/index.json": {}
},
"frameworks": {
"net472": {
"framework": "net472",
"targetAlias": "net472",
"projectReferences": {}
}
},
"warningProperties": {
"warnAsError": [
"NU1605"
]
},
"restoreAuditProperties": {
"enableAudit": "true",
"auditLevel": "low",
"auditMode": "direct"
},
"SdkAnalysisLevel": "10.0.300"
},
"frameworks": {
"net472": {
"framework": "net472",
"targetAlias": "net472",
"dependencies": {
"NLog": {
"target": "Package",
"version": "[5.2.2, )"
}
},
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.303\\RuntimeIdentifierGraph.json"
}
}
}
}
}
@@ -1,15 +1,16 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\micha\.nuget\packages\</NuGetPackageFolders>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\MFRNIAK\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.0</NuGetToolVersion>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="C:\Users\micha\.nuget\packages\" />
<SourceRoot Include="C:\Users\MFRNIAK\.nuget\packages\" />
<SourceRoot Include="C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages\" />
</ItemGroup>
</Project>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />
@@ -0,0 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
@@ -0,0 +1,8 @@
is_global = true
build_property.RootNamespace = AppDiagnostic.Logging
build_property.ProjectDir = C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =
build_property.CsWinRTUseWindowsUIXamlProjections = false
build_property.EffectiveAnalysisLevelStyle =
build_property.EnableCodeStyleSeverity =
@@ -0,0 +1 @@
551ba94934db30a277a1a9cec2dacbe03f8393107e5d0b2436695f9bd62bd569
@@ -0,0 +1,11 @@
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\bin\Debug\net472\AppDiagnostic.Logging.dll
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\bin\Debug\net472\AppDiagnostic.Logging.pdb
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\bin\Debug\net472\NLog.dll
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.csproj.AssemblyReference.cache
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.GeneratedMSBuildEditorConfig.editorconfig
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.AssemblyInfoInputs.cache
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.AssemblyInfo.cs
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.csproj.CoreCompileInputs.cache
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagn.CFEA7AC0.Up2Date
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.dll
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.pdb
@@ -0,0 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
@@ -0,0 +1,8 @@
is_global = true
build_property.RootNamespace = AppDiagnostic.Logging
build_property.ProjectDir = C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =
build_property.CsWinRTUseWindowsUIXamlProjections = false
build_property.EffectiveAnalysisLevelStyle =
build_property.EnableCodeStyleSeverity =
@@ -0,0 +1 @@
a53d59482cad49b656138b1af5cf3e55edf2108969aedc574c362be6c671436b
@@ -0,0 +1,11 @@
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\bin\Release\net472\AppDiagnostic.Logging.dll
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\bin\Release\net472\AppDiagnostic.Logging.pdb
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\bin\Release\net472\NLog.dll
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.csproj.AssemblyReference.cache
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.GeneratedMSBuildEditorConfig.editorconfig
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.AssemblyInfoInputs.cache
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.AssemblyInfo.cs
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.csproj.CoreCompileInputs.cache
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagn.CFEA7AC0.Up2Date
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.dll
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.pdb
@@ -0,0 +1,119 @@
{
"version": 4,
"targets": {
"net472": {
"NLog/5.2.2": {
"type": "package",
"frameworkAssemblies": [
"System",
"System.Configuration",
"System.Core",
"System.IO.Compression",
"System.Xml"
],
"compile": {
"lib/net46/NLog.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/net46/NLog.dll": {
"related": ".xml"
}
}
}
}
},
"libraries": {
"NLog/5.2.2": {
"sha512": "r6Q9740g29gTwmTWlsgdIFm0mhNsfNZmbvWKX/Fxmi8X89ZrpUowHM2T2X1lP7RVpND+ef+XnfKL5g6Q1iNGXA==",
"type": "package",
"path": "nlog/5.2.2",
"files": [
".nupkg.metadata",
".signature.p7s",
"N.png",
"lib/net35/NLog.dll",
"lib/net35/NLog.xml",
"lib/net45/NLog.dll",
"lib/net45/NLog.xml",
"lib/net46/NLog.dll",
"lib/net46/NLog.xml",
"lib/netstandard1.3/NLog.dll",
"lib/netstandard1.3/NLog.xml",
"lib/netstandard1.5/NLog.dll",
"lib/netstandard1.5/NLog.xml",
"lib/netstandard2.0/NLog.dll",
"lib/netstandard2.0/NLog.xml",
"nlog.5.2.2.nupkg.sha512",
"nlog.nuspec"
]
}
},
"projectFileDependencyGroups": {
"net472": [
"NLog >= 5.2.2"
]
},
"packageFolders": {
"C:\\Users\\MFRNIAK\\.nuget\\packages\\": {},
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages": {}
},
"project": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj",
"projectName": "AppDiagnostic.Logging",
"projectPath": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj",
"packagesPath": "C:\\Users\\MFRNIAK\\.nuget\\packages\\",
"outputPath": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\obj\\",
"projectStyle": "PackageReference",
"fallbackFolders": [
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
],
"configFilePaths": [
"C:\\Users\\MFRNIAK\\AppData\\Roaming\\NuGet\\NuGet.Config",
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config",
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
],
"originalTargetFrameworks": [
"net472"
],
"sources": {
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
"https://api.nuget.org/v3/index.json": {}
},
"frameworks": {
"net472": {
"framework": "net472",
"targetAlias": "net472",
"projectReferences": {}
}
},
"warningProperties": {
"warnAsError": [
"NU1605"
]
},
"restoreAuditProperties": {
"enableAudit": "true",
"auditLevel": "low",
"auditMode": "direct"
},
"SdkAnalysisLevel": "10.0.300"
},
"frameworks": {
"net472": {
"framework": "net472",
"targetAlias": "net472",
"dependencies": {
"NLog": {
"target": "Package",
"version": "[5.2.2, )"
}
},
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.303\\RuntimeIdentifierGraph.json"
}
}
}
}
@@ -0,0 +1,10 @@
{
"version": 2,
"dgSpecHash": "ezwKOPZAAXU=",
"success": true,
"projectFilePath": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj",
"expectedPackageFiles": [
"C:\\Users\\MFRNIAK\\.nuget\\packages\\nlog\\5.2.2\\nlog.5.2.2.nupkg.sha512"
],
"logs": []
}
+9 -14
View File
@@ -3,17 +3,12 @@
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<log4net>
<appender name="LogCacheAppender" type="Namespace.LogCacheAppender, AssemblyName">
<param name="Cache" value="YourCacheInstance" />
<layout type="log4net.Layout.PatternLayout">
<param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n" />
</layout>
</appender>
<root>
<level value="DEBUG" />
<appender-ref ref="LogCacheAppender" />
</root>
</log4net>
</configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="NLog" publicKeyToken="5120e14c03d0593c" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
+15 -12
View File
@@ -33,9 +33,6 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="log4net, Version=3.0.3.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a, processorArchitecture=MSIL">
<HintPath>..\packages\log4net.3.0.3\lib\net462\log4net.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
@@ -51,12 +48,15 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="BufferedListView.cs">
<SubType>Component</SubType>
<Compile Include="ColorRuleEditorForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="DiagApi.cs" />
<Compile Include="LogAdapter.cs" />
<Compile Include="LogFilter.cs" />
<Compile Include="DiagnosticLogFormatter.cs" />
<Compile Include="DiagnosticLogStore.cs" />
<Compile Include="DiagnosticPipeServer.cs" />
<Compile Include="LogColorizer.cs" />
<Compile Include="LogColorRule.cs" />
<Compile Include="LogColorRuleRepository.cs" />
<Compile Include="MainForm.cs">
<SubType>Form</SubType>
</Compile>
@@ -76,8 +76,8 @@
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
</Compile>
<None Include="packages.config" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
@@ -92,10 +92,13 @@
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SharedComponents\SharedComponents.csproj">
<Project>{8f942729-f454-4c99-ba6c-746962065ae3}</Project>
<Name>SharedComponents</Name>
<ProjectReference Include="..\AppDiagnostic.Logging\AppDiagnostic.Logging.csproj">
<Project>{A7AF8ED2-362E-4DBB-B607-EBA2820719EC}</Project>
<Name>AppDiagnostic.Logging</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="Resources\TBF_icon.ico" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
+319
View File
@@ -0,0 +1,319 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
namespace AppDiagnostic
{
public sealed class ColorRuleEditorForm : Form
{
private readonly List<LogColorRule> rules;
private readonly ListBox ruleList = new ListBox();
private readonly TextBox patternTextBox = new TextBox();
private readonly CheckBox enabledCheckBox = new CheckBox();
private readonly CheckBox boldCheckBox = new CheckBox();
private readonly CheckBox backgroundCheckBox = new CheckBox();
private readonly Button foregroundButton = new Button();
private readonly Button backgroundButton = new Button();
private readonly RichTextBox previewBox = new RichTextBox();
private readonly Font previewBoldFont;
private bool loadingRule;
public ColorRuleEditorForm(IEnumerable<LogColorRule> sourceRules)
{
rules = sourceRules.Select(rule => rule.Clone()).ToList();
InitializeWindow();
previewBoldFont = new Font(previewBox.Font, FontStyle.Bold);
RefreshRuleList(0);
RefreshPreview();
}
public List<LogColorRule> Rules => rules.Select(rule => rule.Clone()).ToList();
protected override void Dispose(bool disposing)
{
if (disposing)
{
previewBoldFont.Dispose();
}
base.Dispose(disposing);
}
private void InitializeWindow()
{
Text = "Log color rules";
Icon = Properties.Resources.TBF_icon;
StartPosition = FormStartPosition.CenterParent;
MinimumSize = new Size(760, 500);
ClientSize = new Size(860, 560);
var root = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 3, Padding = new Padding(10) };
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 145F));
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 42F));
var split = new SplitContainer { Dock = DockStyle.Fill, SplitterDistance = 280 };
split.Panel1.Padding = new Padding(0, 0, 8, 0);
split.Panel2.Padding = new Padding(8, 0, 0, 0);
ruleList.Dock = DockStyle.Fill;
ruleList.IntegralHeight = false;
ruleList.SelectedIndexChanged += RuleList_SelectedIndexChanged;
var listButtons = new FlowLayoutPanel { Dock = DockStyle.Bottom, Height = 36, FlowDirection = FlowDirection.LeftToRight };
listButtons.Controls.Add(CreateButton("Add", AddRule_Click));
listButtons.Controls.Add(CreateButton("Delete", DeleteRule_Click));
listButtons.Controls.Add(CreateButton("Up", MoveUp_Click));
listButtons.Controls.Add(CreateButton("Down", MoveDown_Click));
split.Panel1.Controls.Add(ruleList);
split.Panel1.Controls.Add(listButtons);
var editor = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, ColumnCount = 2, Padding = new Padding(4) };
editor.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F));
editor.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
enabledCheckBox.Text = "Rule enabled";
enabledCheckBox.AutoSize = true;
enabledCheckBox.CheckedChanged += EditorValueChanged;
AddEditorRow(editor, "State", enabledCheckBox);
patternTextBox.Dock = DockStyle.Fill;
patternTextBox.TextChanged += EditorValueChanged;
AddEditorRow(editor, "Substring", patternTextBox);
foregroundButton.Text = "Choose text color...";
foregroundButton.AutoSize = true;
foregroundButton.Click += ForegroundButton_Click;
AddEditorRow(editor, "Text color", foregroundButton);
var backgroundPanel = new FlowLayoutPanel { Dock = DockStyle.Fill, AutoSize = true };
backgroundCheckBox.Text = "Use background";
backgroundCheckBox.AutoSize = true;
backgroundCheckBox.CheckedChanged += EditorValueChanged;
backgroundButton.Text = "Choose...";
backgroundButton.AutoSize = true;
backgroundButton.Click += BackgroundButton_Click;
backgroundPanel.Controls.Add(backgroundCheckBox);
backgroundPanel.Controls.Add(backgroundButton);
AddEditorRow(editor, "Background", backgroundPanel);
boldCheckBox.Text = "Bold text";
boldCheckBox.AutoSize = true;
boldCheckBox.CheckedChanged += EditorValueChanged;
AddEditorRow(editor, "Font", boldCheckBox);
var resetButton = CreateButton("Restore default rules", RestoreDefaults_Click);
resetButton.AutoSize = true;
AddEditorRow(editor, string.Empty, resetButton);
split.Panel2.Controls.Add(editor);
previewBox.Dock = DockStyle.Fill;
previewBox.ReadOnly = true;
previewBox.WordWrap = true;
previewBox.Font = new Font("Consolas", 9F);
previewBox.Text = "2026-09-10 14:25:31.042|DEBUG|TBF[14280]|TBF.UI.MainWnd|Device configuration loaded.\n2026-09-10 14:25:31.384|INFO |TBF[14280]|TBF.Rig.StateMachine|Test bench started.\n2026-09-10 14:25:32.018|WARN |TBF[14280]|TBF.Device|Pressure is approaching the limit.\n2026-09-10 14:25:32.611|ERROR|TBF[14280]|TBF.Device|Communication failed.";
var previewGroup = new GroupBox { Text = "Live preview", Dock = DockStyle.Fill, Padding = new Padding(8) };
previewGroup.Controls.Add(previewBox);
var footer = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.RightToLeft, Padding = new Padding(0, 8, 0, 0) };
var okButton = new Button { Text = "OK", DialogResult = DialogResult.OK, AutoSize = true };
var cancelButton = new Button { Text = "Cancel", DialogResult = DialogResult.Cancel, AutoSize = true };
footer.Controls.Add(okButton);
footer.Controls.Add(cancelButton);
AcceptButton = okButton;
CancelButton = cancelButton;
root.Controls.Add(split, 0, 0);
root.Controls.Add(previewGroup, 0, 1);
root.Controls.Add(footer, 0, 2);
Controls.Add(root);
}
private static Button CreateButton(string text, EventHandler clickHandler)
{
var button = new Button { Text = text, AutoSize = true };
button.Click += clickHandler;
return button;
}
private static void AddEditorRow(TableLayoutPanel panel, string labelText, Control control)
{
int row = panel.RowCount++;
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
var label = new Label { Text = labelText, AutoSize = true, Anchor = AnchorStyles.Left, Margin = new Padding(3, 8, 3, 3) };
control.Margin = new Padding(3, 5, 3, 5);
panel.Controls.Add(label, 0, row);
panel.Controls.Add(control, 1, row);
}
private void RefreshRuleList(int selectedIndex)
{
ruleList.BeginUpdate();
ruleList.Items.Clear();
foreach (LogColorRule rule in rules)
{
ruleList.Items.Add(rule);
}
ruleList.EndUpdate();
if (rules.Count > 0)
{
ruleList.SelectedIndex = Math.Max(0, Math.Min(selectedIndex, rules.Count - 1));
}
}
private void RuleList_SelectedIndexChanged(object sender, EventArgs e)
{
LogColorRule rule = ruleList.SelectedItem as LogColorRule;
if (rule == null)
{
return;
}
loadingRule = true;
enabledCheckBox.Checked = rule.Enabled;
patternTextBox.Text = rule.Pattern;
foregroundButton.BackColor = rule.ForeColor;
backgroundCheckBox.Checked = rule.UseBackColor;
backgroundButton.Enabled = rule.UseBackColor;
backgroundButton.BackColor = rule.UseBackColor ? rule.BackColor : SystemColors.Control;
boldCheckBox.Checked = rule.Bold;
loadingRule = false;
}
private void EditorValueChanged(object sender, EventArgs e)
{
if (loadingRule)
{
return;
}
LogColorRule rule = ruleList.SelectedItem as LogColorRule;
if (rule == null)
{
return;
}
rule.Enabled = enabledCheckBox.Checked;
rule.Pattern = patternTextBox.Text;
rule.UseBackColor = backgroundCheckBox.Checked;
rule.Bold = boldCheckBox.Checked;
backgroundButton.Enabled = rule.UseBackColor;
int index = ruleList.SelectedIndex;
RefreshRuleList(index);
RefreshPreview();
}
private void ForegroundButton_Click(object sender, EventArgs e)
{
LogColorRule rule = ruleList.SelectedItem as LogColorRule;
if (rule == null)
{
return;
}
using (var dialog = new ColorDialog { Color = rule.ForeColor, FullOpen = true })
{
if (dialog.ShowDialog(this) != DialogResult.OK)
{
return;
}
rule.ForeColorArgb = dialog.Color.ToArgb();
foregroundButton.BackColor = dialog.Color;
RefreshPreview();
}
}
private void BackgroundButton_Click(object sender, EventArgs e)
{
LogColorRule rule = ruleList.SelectedItem as LogColorRule;
if (rule == null)
{
return;
}
Color initialColor = rule.UseBackColor ? rule.BackColor : SystemColors.Window;
using (var dialog = new ColorDialog { Color = initialColor, FullOpen = true })
{
if (dialog.ShowDialog(this) != DialogResult.OK)
{
return;
}
rule.BackColorArgb = dialog.Color.ToArgb();
rule.UseBackColor = true;
backgroundCheckBox.Checked = true;
backgroundButton.BackColor = dialog.Color;
RefreshPreview();
}
}
private void AddRule_Click(object sender, EventArgs e)
{
rules.Add(new LogColorRule { Pattern = "NEW SUBSTRING", ForeColorArgb = Color.MediumPurple.ToArgb() });
RefreshRuleList(rules.Count - 1);
patternTextBox.Focus();
patternTextBox.SelectAll();
}
private void DeleteRule_Click(object sender, EventArgs e)
{
int index = ruleList.SelectedIndex;
if (index < 0)
{
return;
}
rules.RemoveAt(index);
RefreshRuleList(Math.Min(index, rules.Count - 1));
RefreshPreview();
}
private void MoveUp_Click(object sender, EventArgs e)
{
MoveSelectedRule(-1);
}
private void MoveDown_Click(object sender, EventArgs e)
{
MoveSelectedRule(1);
}
private void MoveSelectedRule(int offset)
{
int index = ruleList.SelectedIndex;
int newIndex = index + offset;
if (index < 0 || newIndex < 0 || newIndex >= rules.Count)
{
return;
}
LogColorRule rule = rules[index];
rules.RemoveAt(index);
rules.Insert(newIndex, rule);
RefreshRuleList(newIndex);
RefreshPreview();
}
private void RestoreDefaults_Click(object sender, EventArgs e)
{
rules.Clear();
rules.AddRange(LogColorRule.CreateDefaults());
RefreshRuleList(0);
RefreshPreview();
}
private void RefreshPreview()
{
int selectionStart = previewBox.SelectionStart;
int selectionLength = previewBox.SelectionLength;
string text = previewBox.Text;
LogColorizer.Apply(previewBox, 0, text.Length, text, rules, previewBox.Font, previewBoldFont);
previewBox.Select(Math.Min(selectionStart, previewBox.TextLength), Math.Min(selectionLength, Math.Max(0, previewBox.TextLength - selectionStart)));
}
}
}
+24
View File
@@ -0,0 +1,24 @@
using AppDiagnostic.Logging;
using System;
namespace AppDiagnostic
{
public static class DiagnosticLogFormatter
{
public static string Format(DiagnosticLogEntry entry)
{
if (entry == null)
{
return string.Empty;
}
string level = (entry.Level ?? string.Empty).ToUpperInvariant().PadRight(5);
string application = string.IsNullOrWhiteSpace(entry.ApplicationName) ? "Unknown" : entry.ApplicationName;
string logger = entry.LoggerName ?? string.Empty;
string message = entry.Message ?? string.Empty;
string exception = string.IsNullOrWhiteSpace(entry.Exception) ? string.Empty : Environment.NewLine + entry.Exception;
return $"{entry.TimestampUtc.ToLocalTime():yyyy-MM-dd HH:mm:ss.fff}|{level}|{application}[{entry.ProcessId}]|{logger}|{message}{exception}";
}
}
}
+82
View File
@@ -0,0 +1,82 @@
using AppDiagnostic.Logging;
using System.Collections.Generic;
using System.Linq;
namespace AppDiagnostic
{
public sealed class DiagnosticLogStore
{
private const int MaximumLogCount = 10000;
private static readonly DiagnosticLogStore instance = new DiagnosticLogStore();
private readonly object syncRoot = new object();
private readonly List<DiagnosticLogEntry> entries = new List<DiagnosticLogEntry>();
private long totalReceived;
private long version;
private DiagnosticLogStore()
{
}
public static DiagnosticLogStore Instance => instance;
public long TotalReceived
{
get
{
lock (syncRoot)
{
return totalReceived;
}
}
}
public long Version
{
get
{
lock (syncRoot)
{
return version;
}
}
}
public void Add(DiagnosticLogEntry entry)
{
if (entry == null)
{
return;
}
lock (syncRoot)
{
entries.Add(entry);
totalReceived++;
version++;
int excessCount = entries.Count - MaximumLogCount;
if (excessCount > 0)
{
entries.RemoveRange(0, excessCount);
}
}
}
public List<DiagnosticLogEntry> GetSnapshot()
{
lock (syncRoot)
{
return entries.ToList();
}
}
public void Clear()
{
lock (syncRoot)
{
entries.Clear();
version++;
}
}
}
}
+164
View File
@@ -0,0 +1,164 @@
using AppDiagnostic.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;
namespace AppDiagnostic
{
public sealed class DiagnosticPipeServer : IDisposable
{
private readonly Action<DiagnosticLogEntry> messageReceived;
private readonly CancellationTokenSource cancellation = new CancellationTokenSource();
private readonly object syncRoot = new object();
private readonly List<NamedPipeServerStream> connectedClients = new List<NamedPipeServerStream>();
private NamedPipeServerStream waitingServer;
private Task serverTask;
public DiagnosticPipeServer(Action<DiagnosticLogEntry> messageReceived)
{
this.messageReceived = messageReceived ?? throw new ArgumentNullException(nameof(messageReceived));
}
public int ConnectedClientCount
{
get
{
lock (syncRoot)
{
return connectedClients.Count;
}
}
}
public void Start()
{
if (serverTask != null)
{
return;
}
serverTask = Task.Run((Action)AcceptConnections);
}
private void AcceptConnections()
{
while (!cancellation.IsCancellationRequested)
{
NamedPipeServerStream server = null;
try
{
server = new NamedPipeServerStream(DiagnosticProtocol.DefaultPipeName, PipeDirection.In, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
lock (syncRoot)
{
waitingServer = server;
}
server.WaitForConnection();
lock (syncRoot)
{
waitingServer = null;
connectedClients.Add(server);
}
NamedPipeServerStream connectedServer = server;
server = null;
Task.Run(() => ReadClient(connectedServer));
}
catch (ObjectDisposedException)
{
if (!cancellation.IsCancellationRequested)
{
throw;
}
}
catch (IOException)
{
if (!cancellation.IsCancellationRequested)
{
Thread.Sleep(250);
}
}
finally
{
lock (syncRoot)
{
if (ReferenceEquals(waitingServer, server))
{
waitingServer = null;
}
}
server?.Dispose();
}
}
}
private void ReadClient(NamedPipeServerStream client)
{
try
{
while (client.IsConnected && !cancellation.IsCancellationRequested)
{
DiagnosticLogEntry entry = DiagnosticMessageSerializer.ReadFramed(client);
messageReceived(entry);
}
}
catch (EndOfStreamException)
{
// The producer application was closed.
}
catch (IOException)
{
// The producer application disconnected.
}
catch (ObjectDisposedException)
{
// The diagnostic application is shutting down.
}
finally
{
lock (syncRoot)
{
connectedClients.Remove(client);
}
client.Dispose();
}
}
public void Dispose()
{
cancellation.Cancel();
lock (syncRoot)
{
waitingServer?.Dispose();
waitingServer = null;
foreach (NamedPipeServerStream client in connectedClients.ToArray())
{
client.Dispose();
}
connectedClients.Clear();
}
try
{
serverTask?.Wait(2000);
}
catch (AggregateException)
{
// Cancellation during shutdown is expected.
}
cancellation.Dispose();
}
}
}
+65
View File
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Drawing;
namespace AppDiagnostic
{
[Serializable]
public sealed class LogColorRule
{
public bool Enabled { get; set; } = true;
public string Pattern { get; set; } = string.Empty;
public int ForeColorArgb { get; set; } = Color.Black.ToArgb();
public int BackColorArgb { get; set; }
public bool UseBackColor { get; set; }
public bool Bold { get; set; }
public Color ForeColor => Color.FromArgb(ForeColorArgb);
public Color BackColor => Color.FromArgb(BackColorArgb);
public LogColorRule Clone()
{
return new LogColorRule
{
Enabled = Enabled,
Pattern = Pattern,
ForeColorArgb = ForeColorArgb,
BackColorArgb = BackColorArgb,
UseBackColor = UseBackColor,
Bold = Bold
};
}
public override string ToString()
{
string state = Enabled ? string.Empty : "[disabled] ";
return state + Pattern;
}
public static List<LogColorRule> CreateDefaults()
{
return new List<LogColorRule>
{
Create("|TRACE|", Color.DimGray, false),
Create("|DEBUG|", Color.RoyalBlue, false),
Create("|INFO |", Color.ForestGreen, false),
Create("|WARN |", Color.DarkOrange, true),
Create("|ERROR|", Color.Crimson, true),
Create("|FATAL|", Color.White, true, Color.DarkRed)
};
}
private static LogColorRule Create(string pattern, Color foreground, bool bold, Color? background = null)
{
return new LogColorRule
{
Enabled = true,
Pattern = pattern,
ForeColorArgb = foreground.ToArgb(),
BackColorArgb = background?.ToArgb() ?? Color.Empty.ToArgb(),
UseBackColor = background.HasValue,
Bold = bold
};
}
}
}
+46
View File
@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
namespace AppDiagnostic
{
public static class LogColorRuleRepository
{
private static readonly string settingsFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Sensus", "AppDiagnostic");
private static readonly string settingsPath = Path.Combine(settingsFolder, "color-rules.xml");
public static List<LogColorRule> Load()
{
try
{
if (!File.Exists(settingsPath))
{
return LogColorRule.CreateDefaults();
}
var serializer = new XmlSerializer(typeof(List<LogColorRule>));
using (FileStream stream = File.OpenRead(settingsPath))
{
var rules = serializer.Deserialize(stream) as List<LogColorRule>;
return rules == null || rules.Count == 0 ? LogColorRule.CreateDefaults() : rules;
}
}
catch
{
return LogColorRule.CreateDefaults();
}
}
public static void Save(IEnumerable<LogColorRule> rules)
{
Directory.CreateDirectory(settingsFolder);
var serializer = new XmlSerializer(typeof(List<LogColorRule>));
using (FileStream stream = File.Create(settingsPath))
{
serializer.Serialize(stream, new List<LogColorRule>(rules));
}
}
}
}
+47
View File
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
namespace AppDiagnostic
{
public static class LogColorizer
{
public static void Apply(RichTextBox target, int startIndex, int length, string text, IEnumerable<LogColorRule> rules, Font regularFont, Font boldFont)
{
if (target == null || length <= 0)
{
return;
}
target.Select(startIndex, length);
target.SelectionColor = target.ForeColor;
target.SelectionBackColor = target.BackColor;
target.SelectionFont = regularFont;
foreach (LogColorRule rule in rules)
{
if (rule == null || !rule.Enabled || string.IsNullOrEmpty(rule.Pattern))
{
continue;
}
int searchIndex = 0;
while (searchIndex < text.Length)
{
int matchIndex = text.IndexOf(rule.Pattern, searchIndex, StringComparison.OrdinalIgnoreCase);
if (matchIndex < 0)
{
break;
}
target.Select(startIndex + matchIndex, rule.Pattern.Length);
target.SelectionColor = rule.ForeColor;
target.SelectionBackColor = rule.UseBackColor ? rule.BackColor : target.BackColor;
target.SelectionFont = rule.Bold ? boldFont : regularFont;
searchIndex = matchIndex + rule.Pattern.Length;
}
}
}
}
}
+281 -250
View File
@@ -1,278 +1,309 @@
using System.Windows.Forms;
namespace AppDiagnostic
{
partial class MainForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
private System.Windows.Forms.ToolStrip mainToolStrip;
private System.Windows.Forms.ToolStripButton pauseButton;
private System.Windows.Forms.ToolStripButton refreshButton;
private System.Windows.Forms.ToolStripButton clearButton;
private System.Windows.Forms.ToolStripButton saveButton;
private System.Windows.Forms.ToolStripButton copyButton;
private System.Windows.Forms.ToolStripButton copyAllButton;
private System.Windows.Forms.ToolStripButton wrapButton;
private System.Windows.Forms.ToolStripButton autoScrollButton;
private System.Windows.Forms.ToolStripButton alwaysOnTopButton;
private System.Windows.Forms.ToolStripButton colorRulesButton;
private System.Windows.Forms.ToolStripButton closeButton;
private System.Windows.Forms.TableLayoutPanel filterPanel;
private System.Windows.Forms.Label filterLabel;
private System.Windows.Forms.TextBox filterTextBox;
private System.Windows.Forms.Label levelLabel;
private System.Windows.Forms.ComboBox levelComboBox;
private System.Windows.Forms.Label applicationLabel;
private System.Windows.Forms.ComboBox applicationComboBox;
private System.Windows.Forms.Button applyFilterButton;
private System.Windows.Forms.Button clearFilterButton;
private System.Windows.Forms.RichTextBox logView;
private System.Windows.Forms.StatusStrip statusStrip;
private System.Windows.Forms.ToolStripStatusLabel receiverStatusLabel;
private System.Windows.Forms.ToolStripStatusLabel statusSpacer;
private System.Windows.Forms.ToolStripStatusLabel countStatusLabel;
private System.Windows.Forms.ContextMenuStrip logContextMenu;
private System.Windows.Forms.ToolStripMenuItem copySelectionMenuItem;
private System.Windows.Forms.ToolStripMenuItem copyLineMenuItem;
private System.Windows.Forms.ToolStripMenuItem copyAllMenuItem;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
if (disposing && components != null)
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
//private BufferedListView memoListView;
private System.Windows.Forms.ColumnHeader columnHeaderTime;
private System.Windows.Forms.ColumnHeader columnHeaderMessage;
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.columnHeaderTime = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderMessage = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.panel1 = new System.Windows.Forms.Panel();
this.refreshMemoButton = new System.Windows.Forms.Button();
this.button6 = new System.Windows.Forms.Button();
this.button7 = new System.Windows.Forms.Button();
this.filterManagementPanel = new System.Windows.Forms.Panel();
this.button5 = new System.Windows.Forms.Button();
this.filterButton = new System.Windows.Forms.Button();
this.components = new System.ComponentModel.Container();
this.mainToolStrip = new System.Windows.Forms.ToolStrip();
this.pauseButton = new System.Windows.Forms.ToolStripButton();
this.refreshButton = new System.Windows.Forms.ToolStripButton();
this.clearButton = new System.Windows.Forms.ToolStripButton();
this.saveButton = new System.Windows.Forms.ToolStripButton();
this.copyButton = new System.Windows.Forms.ToolStripButton();
this.copyAllButton = new System.Windows.Forms.ToolStripButton();
this.wrapButton = new System.Windows.Forms.ToolStripButton();
this.autoScrollButton = new System.Windows.Forms.ToolStripButton();
this.alwaysOnTopButton = new System.Windows.Forms.ToolStripButton();
this.colorRulesButton = new System.Windows.Forms.ToolStripButton();
this.closeButton = new System.Windows.Forms.ToolStripButton();
this.filterPanel = new System.Windows.Forms.TableLayoutPanel();
this.filterLabel = new System.Windows.Forms.Label();
this.filterTextBox = new System.Windows.Forms.TextBox();
this.label2 = new System.Windows.Forms.Label();
this.wndControlPanel = new System.Windows.Forms.Panel();
this.button2 = new System.Windows.Forms.Button();
this.flowControlsPanel = new System.Windows.Forms.Panel();
this.runButton = new System.Windows.Forms.Button();
this.stopButton = new System.Windows.Forms.Button();
this.label1 = new System.Windows.Forms.Label();
this.memoListView = new BufferedListView();
this.panel1.SuspendLayout();
this.filterManagementPanel.SuspendLayout();
this.wndControlPanel.SuspendLayout();
this.flowControlsPanel.SuspendLayout();
this.levelLabel = new System.Windows.Forms.Label();
this.levelComboBox = new System.Windows.Forms.ComboBox();
this.applicationLabel = new System.Windows.Forms.Label();
this.applicationComboBox = new System.Windows.Forms.ComboBox();
this.applyFilterButton = new System.Windows.Forms.Button();
this.clearFilterButton = new System.Windows.Forms.Button();
this.logContextMenu = new System.Windows.Forms.ContextMenuStrip(this.components);
this.copySelectionMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.copyLineMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.copyAllMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.logView = new System.Windows.Forms.RichTextBox();
this.statusStrip = new System.Windows.Forms.StatusStrip();
this.receiverStatusLabel = new System.Windows.Forms.ToolStripStatusLabel();
this.statusSpacer = new System.Windows.Forms.ToolStripStatusLabel();
this.countStatusLabel = new System.Windows.Forms.ToolStripStatusLabel();
this.mainToolStrip.SuspendLayout();
this.filterPanel.SuspendLayout();
this.logContextMenu.SuspendLayout();
this.statusStrip.SuspendLayout();
this.SuspendLayout();
//
// columnHeaderTime
//
this.columnHeaderTime.Text = "Time";
this.columnHeaderTime.Width = 150;
//
// columnHeaderMessage
//
this.columnHeaderMessage.Text = "Message";
this.columnHeaderMessage.Width = 350;
//
// panel1
//
this.panel1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.panel1.Controls.Add(this.refreshMemoButton);
this.panel1.Controls.Add(this.button6);
this.panel1.Controls.Add(this.button7);
this.panel1.Location = new System.Drawing.Point(189, 356);
this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(283, 26);
this.panel1.TabIndex = 16;
//
// refreshMemoButton
//
this.refreshMemoButton.Location = new System.Drawing.Point(108, 0);
this.refreshMemoButton.Name = "refreshMemoButton";
this.refreshMemoButton.Size = new System.Drawing.Size(84, 26);
this.refreshMemoButton.TabIndex = 17;
this.refreshMemoButton.Text = "Refresh memo";
this.refreshMemoButton.UseVisualStyleBackColor = true;
this.refreshMemoButton.Click += new System.EventHandler(this.refreshMemoButton_Click);
//
// button6
//
this.button6.Location = new System.Drawing.Point(0, 0);
this.button6.Name = "button6";
this.button6.Size = new System.Drawing.Size(102, 26);
this.button6.TabIndex = 4;
this.button6.Text = "Clean diag cache";
this.button6.UseVisualStyleBackColor = true;
this.button6.Click += new System.EventHandler(this.button6_Click);
//
// button7
//
this.button7.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.button7.Location = new System.Drawing.Point(198, 0);
this.button7.Name = "button7";
this.button7.Size = new System.Drawing.Size(72, 26);
this.button7.TabIndex = 5;
this.button7.Text = "Save logs";
this.button7.UseVisualStyleBackColor = true;
this.button7.Click += new System.EventHandler(this.button7_Click);
//
// filterManagementPanel
//
this.filterManagementPanel.Controls.Add(this.button5);
this.filterManagementPanel.Controls.Add(this.filterButton);
this.filterManagementPanel.Controls.Add(this.filterTextBox);
this.filterManagementPanel.Controls.Add(this.label2);
this.filterManagementPanel.Location = new System.Drawing.Point(127, 6);
this.filterManagementPanel.Name = "filterManagementPanel";
this.filterManagementPanel.Size = new System.Drawing.Size(393, 26);
this.filterManagementPanel.TabIndex = 15;
//
// button5
//
this.button5.Enabled = false;
this.button5.Location = new System.Drawing.Point(294, 4);
this.button5.Name = "button5";
this.button5.Size = new System.Drawing.Size(81, 20);
this.button5.TabIndex = 11;
this.button5.Text = "Add new filter";
this.button5.UseVisualStyleBackColor = true;
//
// filterButton
//
this.filterButton.Location = new System.Drawing.Point(225, 4);
this.filterButton.Name = "filterButton";
this.filterButton.Size = new System.Drawing.Size(64, 20);
this.filterButton.TabIndex = 10;
this.filterButton.Text = "Set filter";
this.filterButton.UseVisualStyleBackColor = true;
this.filterButton.Click += new System.EventHandler(this.filterButton_Click_1);
//
// filterTextBox
//
this.filterTextBox.Location = new System.Drawing.Point(33, 4);
this.filterTextBox.Name = "filterTextBox";
this.filterTextBox.Size = new System.Drawing.Size(187, 20);
this.filterTextBox.TabIndex = 7;
//
// label2
//
this.label2.AutoSize = true;
this.label2.Location = new System.Drawing.Point(3, 8);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(29, 13);
this.label2.TabIndex = 6;
this.label2.Text = "Filter";
//
// wndControlPanel
//
this.wndControlPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.wndControlPanel.Controls.Add(this.button2);
this.wndControlPanel.Location = new System.Drawing.Point(831, 356);
this.wndControlPanel.Name = "wndControlPanel";
this.wndControlPanel.Size = new System.Drawing.Size(70, 26);
this.wndControlPanel.TabIndex = 14;
//
// button2
//
this.button2.Location = new System.Drawing.Point(3, 0);
this.button2.Name = "button2";
this.button2.Size = new System.Drawing.Size(64, 26);
this.button2.TabIndex = 2;
this.button2.Text = "Close";
this.button2.UseVisualStyleBackColor = true;
this.button2.Click += new System.EventHandler(this.button2_Click);
//
// flowControlsPanel
//
this.flowControlsPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.flowControlsPanel.Controls.Add(this.runButton);
this.flowControlsPanel.Controls.Add(this.stopButton);
this.flowControlsPanel.Location = new System.Drawing.Point(12, 356);
this.flowControlsPanel.Name = "flowControlsPanel";
this.flowControlsPanel.Size = new System.Drawing.Size(133, 26);
this.flowControlsPanel.TabIndex = 13;
//
// runButton
//
this.runButton.Enabled = false;
this.runButton.Location = new System.Drawing.Point(0, 0);
this.runButton.Name = "runButton";
this.runButton.Size = new System.Drawing.Size(64, 26);
this.runButton.TabIndex = 4;
this.runButton.Text = "Run";
this.runButton.UseVisualStyleBackColor = true;
this.runButton.Click += new System.EventHandler(this.runButton_Click);
//
// stopButton
//
this.stopButton.Location = new System.Drawing.Point(69, 0);
this.stopButton.Name = "stopButton";
this.stopButton.Size = new System.Drawing.Size(64, 26);
this.stopButton.TabIndex = 5;
this.stopButton.Text = "Stop";
this.stopButton.UseVisualStyleBackColor = true;
this.stopButton.Click += new System.EventHandler(this.stopButton_Click);
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(12, 19);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(76, 13);
this.label1.TabIndex = 12;
this.label1.Text = "Flow of events";
//
// memoListView
//
this.memoListView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.memoListView.FullRowSelect = true;
this.memoListView.GridLines = true;
this.memoListView.HideSelection = false;
this.memoListView.Location = new System.Drawing.Point(12, 38);
this.memoListView.Name = "memoListView";
this.memoListView.Size = new System.Drawing.Size(889, 312);
this.memoListView.TabIndex = 0;
this.memoListView.UseCompatibleStateImageBehavior = false;
this.memoListView.View = System.Windows.Forms.View.Details;
this.memoListView.DoubleClick += new System.EventHandler(this.MemoListView_DoubleClick);
//
//
// mainToolStrip
//
this.mainToolStrip.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
this.mainToolStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.pauseButton,
this.refreshButton,
this.clearButton,
this.saveButton,
new System.Windows.Forms.ToolStripSeparator(),
this.copyButton,
this.copyAllButton,
new System.Windows.Forms.ToolStripSeparator(),
this.wrapButton,
this.autoScrollButton,
this.alwaysOnTopButton,
this.colorRulesButton,
this.closeButton});
this.mainToolStrip.Location = new System.Drawing.Point(0, 0);
this.mainToolStrip.Name = "mainToolStrip";
this.mainToolStrip.Padding = new System.Windows.Forms.Padding(6, 3, 6, 3);
this.mainToolStrip.Size = new System.Drawing.Size(1184, 31);
this.mainToolStrip.TabIndex = 0;
//
// pauseButton
//
this.pauseButton.CheckOnClick = true;
this.pauseButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.pauseButton.Text = "Pause";
this.pauseButton.ToolTipText = "Pause screen updates while continuing to receive logs";
this.pauseButton.Click += new System.EventHandler(this.pauseButton_Click);
//
// refreshButton
//
this.refreshButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.refreshButton.Text = "Refresh";
this.refreshButton.Click += new System.EventHandler(this.refreshButton_Click);
//
// clearButton
//
this.clearButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.clearButton.Text = "Clear";
this.clearButton.Click += new System.EventHandler(this.clearButton_Click);
//
// saveButton
//
this.saveButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.saveButton.Text = "Save...";
this.saveButton.Click += new System.EventHandler(this.saveButton_Click);
//
// copyButton
//
this.copyButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.copyButton.Text = "Copy";
this.copyButton.ToolTipText = "Copy selected text or the current log line";
this.copyButton.Click += new System.EventHandler(this.copyButton_Click);
//
// copyAllButton
//
this.copyAllButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.copyAllButton.Text = "Copy all";
this.copyAllButton.Click += new System.EventHandler(this.copyAllButton_Click);
//
// wrapButton
//
this.wrapButton.Checked = true;
this.wrapButton.CheckOnClick = true;
this.wrapButton.CheckState = System.Windows.Forms.CheckState.Checked;
this.wrapButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.wrapButton.Text = "Wrap lines";
this.wrapButton.Click += new System.EventHandler(this.wrapButton_Click);
//
// autoScrollButton
//
this.autoScrollButton.Checked = true;
this.autoScrollButton.CheckOnClick = true;
this.autoScrollButton.CheckState = System.Windows.Forms.CheckState.Checked;
this.autoScrollButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.autoScrollButton.Text = "Auto-scroll";
//
// alwaysOnTopButton
//
this.alwaysOnTopButton.CheckOnClick = true;
this.alwaysOnTopButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.alwaysOnTopButton.Text = "Always on top";
this.alwaysOnTopButton.Click += new System.EventHandler(this.alwaysOnTopButton_Click);
//
// colorRulesButton
//
this.colorRulesButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.colorRulesButton.Text = "Color rules...";
this.colorRulesButton.Click += new System.EventHandler(this.colorRulesButton_Click);
//
// closeButton
//
this.closeButton.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right;
this.closeButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.closeButton.Text = "Close";
this.closeButton.Click += new System.EventHandler(this.closeButton_Click);
//
// filterPanel
//
this.filterPanel.ColumnCount = 8;
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 42F));
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 42F));
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 105F));
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 72F));
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 180F));
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 66F));
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 66F));
this.filterPanel.Controls.Add(this.filterLabel, 0, 0);
this.filterPanel.Controls.Add(this.filterTextBox, 1, 0);
this.filterPanel.Controls.Add(this.levelLabel, 2, 0);
this.filterPanel.Controls.Add(this.levelComboBox, 3, 0);
this.filterPanel.Controls.Add(this.applicationLabel, 4, 0);
this.filterPanel.Controls.Add(this.applicationComboBox, 5, 0);
this.filterPanel.Controls.Add(this.applyFilterButton, 6, 0);
this.filterPanel.Controls.Add(this.clearFilterButton, 7, 0);
this.filterPanel.Dock = System.Windows.Forms.DockStyle.Top;
this.filterPanel.Location = new System.Drawing.Point(0, 31);
this.filterPanel.Name = "filterPanel";
this.filterPanel.Padding = new System.Windows.Forms.Padding(6, 5, 6, 5);
this.filterPanel.RowCount = 1;
this.filterPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.filterPanel.Size = new System.Drawing.Size(1184, 38);
this.filterPanel.TabIndex = 1;
//
// filter controls
//
this.filterLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
this.filterLabel.AutoSize = true;
this.filterLabel.Text = "Filter";
this.filterTextBox.Dock = System.Windows.Forms.DockStyle.Fill;
this.filterTextBox.KeyDown += new System.Windows.Forms.KeyEventHandler(this.filterTextBox_KeyDown);
this.levelLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
this.levelLabel.AutoSize = true;
this.levelLabel.Text = "Level";
this.levelComboBox.Dock = System.Windows.Forms.DockStyle.Fill;
this.levelComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.levelComboBox.SelectedIndexChanged += new System.EventHandler(this.filterValueChanged);
this.applicationLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
this.applicationLabel.AutoSize = true;
this.applicationLabel.Text = "Application";
this.applicationComboBox.Dock = System.Windows.Forms.DockStyle.Fill;
this.applicationComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.applicationComboBox.SelectedIndexChanged += new System.EventHandler(this.filterValueChanged);
this.applyFilterButton.Dock = System.Windows.Forms.DockStyle.Fill;
this.applyFilterButton.Text = "Apply";
this.applyFilterButton.Click += new System.EventHandler(this.applyFilterButton_Click);
this.clearFilterButton.Dock = System.Windows.Forms.DockStyle.Fill;
this.clearFilterButton.Text = "Reset";
this.clearFilterButton.Click += new System.EventHandler(this.clearFilterButton_Click);
//
// logContextMenu
//
this.logContextMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.copySelectionMenuItem,
this.copyLineMenuItem,
this.copyAllMenuItem});
this.copySelectionMenuItem.Text = "Copy selection";
this.copySelectionMenuItem.Click += new System.EventHandler(this.copySelectionMenuItem_Click);
this.copyLineMenuItem.Text = "Copy current line";
this.copyLineMenuItem.Click += new System.EventHandler(this.copyLineMenuItem_Click);
this.copyAllMenuItem.Text = "Copy all visible logs";
this.copyAllMenuItem.Click += new System.EventHandler(this.copyAllButton_Click);
//
// logView
//
this.logView.BackColor = System.Drawing.SystemColors.Window;
this.logView.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.logView.ContextMenuStrip = this.logContextMenu;
this.logView.DetectUrls = false;
this.logView.Dock = System.Windows.Forms.DockStyle.Fill;
this.logView.Font = new System.Drawing.Font("Consolas", 9F);
this.logView.HideSelection = false;
this.logView.Location = new System.Drawing.Point(0, 69);
this.logView.Name = "logView";
this.logView.ReadOnly = true;
this.logView.Size = new System.Drawing.Size(1184, 554);
this.logView.TabIndex = 2;
this.logView.Text = "";
this.logView.WordWrap = true;
//
// statusStrip
//
this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.receiverStatusLabel,
this.statusSpacer,
this.countStatusLabel});
this.statusStrip.Location = new System.Drawing.Point(0, 623);
this.statusStrip.Name = "statusStrip";
this.statusStrip.Size = new System.Drawing.Size(1184, 22);
this.statusStrip.TabIndex = 3;
this.receiverStatusLabel.Text = "Listening";
this.statusSpacer.Spring = true;
this.countStatusLabel.Text = "Received: 0 | Visible: 0";
//
// MainForm
//
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(913, 388);
this.Controls.Add(this.memoListView);
this.Controls.Add(this.panel1);
this.Controls.Add(this.filterManagementPanel);
this.Controls.Add(this.wndControlPanel);
this.Controls.Add(this.flowControlsPanel);
this.Controls.Add(this.label1);
this.ClientSize = new System.Drawing.Size(1184, 645);
this.Controls.Add(this.logView);
this.Controls.Add(this.filterPanel);
this.Controls.Add(this.mainToolStrip);
this.Controls.Add(this.statusStrip);
this.KeyPreview = true;
this.MinimumSize = new System.Drawing.Size(860, 480);
this.Name = "MainForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "Application live diagnostic";
this.panel1.ResumeLayout(false);
this.filterManagementPanel.ResumeLayout(false);
this.filterManagementPanel.PerformLayout();
this.wndControlPanel.ResumeLayout(false);
this.flowControlsPanel.ResumeLayout(false);
this.KeyDown += new System.Windows.Forms.KeyEventHandler(this.MainForm_KeyDown);
this.mainToolStrip.ResumeLayout(false);
this.mainToolStrip.PerformLayout();
this.filterPanel.ResumeLayout(false);
this.filterPanel.PerformLayout();
this.logContextMenu.ResumeLayout(false);
this.statusStrip.ResumeLayout(false);
this.statusStrip.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Panel panel1;
private System.Windows.Forms.Button button6;
private System.Windows.Forms.Button button7;
private System.Windows.Forms.Panel filterManagementPanel;
private System.Windows.Forms.Button button5;
private System.Windows.Forms.Button filterButton;
private System.Windows.Forms.TextBox filterTextBox;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Panel wndControlPanel;
private System.Windows.Forms.Button button2;
private System.Windows.Forms.Panel flowControlsPanel;
private System.Windows.Forms.Button runButton;
private System.Windows.Forms.Button stopButton;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Button refreshMemoButton;
private BufferedListView memoListView;
}
}
+393 -213
View File
@@ -1,265 +1,445 @@
using log4net.Config;
using log4net;
using AppDiagnostic.Logging;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using log4net.Appender;
using SharedComponents;
using static System.Windows.Forms.VisualStyles.VisualStyleElement;
using System.IO; // Pre File.WriteAllLines a prácu so súbormi
using System.Linq;
namespace AppDiagnostic
{
public partial class MainForm : Form
{
private bool stopUpdate = false; // Premenná na zastavenie aktualizácie
private int lastDisplayedLogIndex = 0;
private bool autoScrollEnabled = true; // Automatický posun, predvolene povolený
private MainForm appDiagnosticForm;
private Timer refreshTimer;
private LogAdapter _logAdapter; // Pre LogAdapter
private LogFilter _logFilter; // LogFilter objekt pre filtráciu
private readonly Func<int> connectedClientCountProvider;
private readonly Func<bool> activationRequestProvider;
private readonly Timer refreshTimer;
private readonly Font regularLogFont;
private readonly Font boldLogFont;
private List<LogColorRule> colorRules;
private long lastRenderedVersion = -1;
private int observedEntryCount;
private int visibleEntryCount;
private bool loadingFilters;
public MainForm()
public MainForm() : this(null, null)
{
}
public MainForm(Func<int> connectedClientCountProvider, Func<bool> activationRequestProvider)
{
this.connectedClientCountProvider = connectedClientCountProvider;
this.activationRequestProvider = activationRequestProvider;
InitializeComponent();
Icon = Properties.Resources.TBF_icon;
// Inicializujeme LogAdapter a priradíme ho k memoListView
_logAdapter = new LogAdapter(memoListView);
regularLogFont = new Font(logView.Font, FontStyle.Regular);
boldLogFont = new Font(logView.Font, FontStyle.Bold);
colorRules = LogColorRuleRepository.Load();
RefreshLogView();
InitializeFilters();
// Pripojenie na udalosť pri pridaní nového logu
// nakoľko sa memo refreshuje cyklicky, tak refresh na udalosť nepotrebujem
//LiveLogCache.Instance.LogAdded += OnLogAdded;
refreshTimer = new Timer { Interval = 250 };
refreshTimer.Tick += refreshTimer_Tick;
refreshTimer.Start();
// Vytvoríme LogFilter objekt
//_logFilter = new LogFilter(memoListView);
}
private void OnLogAdded(string log)
{
try
{
RefreshLogView();
}
catch (Exception ex)
{
MessageBox.Show($"Error in OnLogAdded: {ex.Message}");
throw;
}
}
private void RefreshLogView()
{
// Skontrolujte, či voláme z iného vlákna
if (InvokeRequired)
{
Invoke((Action)RefreshLogView);
return;
}
memoListView.BeginUpdate(); // Zabraňuje vizuálnym aktualizáciám počas vykresľovania
try
{
// Skontrolujte, či je užívateľ na poslednom viditeľnom zázname
if (memoListView.Items.Count > 0)
{
var lastVisibleIndex = memoListView.TopItem.Index + memoListView.ClientRectangle.Height / memoListView.Items[0].Bounds.Height;
autoScrollEnabled = lastVisibleIndex >= memoListView.Items.Count - 1;
}
// Získajte nové logy od posledného indexu
var logs = LiveLogCache.Instance.GetLogs(lastDisplayedLogIndex, LiveLogCache.Instance.Logs.Count - lastDisplayedLogIndex);
// Pridajte len nové logy
foreach (var log in logs)
{
memoListView.Items.Add(new ListViewItem(log));
}
// Aktualizujte posledný zobrazený index
lastDisplayedLogIndex = LiveLogCache.Instance.Logs.Count;
// Ak je autoScrollEnabled, posuňte sa na posledný záznam
if (autoScrollEnabled && memoListView.Items.Count > 0)
{
memoListView.EnsureVisible(memoListView.Items.Count - 1);
}
}
finally
{
memoListView.EndUpdate(); // Umožní vizuálne aktualizácie
}
}
// Táto metóda sa spustí pri scrollovaní v memoListView (tzn. ak sa používateľ dostane na spodok listu)
private void memoListView_Scroll(object sender, EventArgs e)
{
// Skontrolujte, či používateľ dosiahol spodok ListView
if (memoListView.Items.Count > 0 &&
memoListView.Items[memoListView.Items.Count - 1].Bounds.Bottom <= memoListView.ClientSize.Height)
{
// Ak áno, načítame ďalšie logy
RefreshLogView();
}
}
private void button6_Click(object sender, EventArgs e)
{
//LiveLogCache.Instance.AddLog("Nový log z hlavnej aplikácie");
LiveLogCache.Instance.ClearLogs();
ClearListView();
}
private void runButton_Click(object sender, EventArgs e)
{
if (stopUpdate) // Ak je časovač zastavený, spustíme ho
{
stopUpdate = false; // Zastav aktualizáciu
runButton.Enabled = false; // Zakážeme tlačidlo Start počas behu
stopButton.Enabled = true; // Povolenie tlačidla Stop
refreshTimer.Start();
}
}
private void stopButton_Click(object sender, EventArgs e)
{
if (!stopUpdate) // Ak časovač beží, môžeme ho zastaviť
{
stopUpdate = true; // Spusti aktualizáciu
runButton.Enabled = true; // Povolenie tlačidla Start
stopButton.Enabled = false; // Zakážeme tlačidlo Stop
refreshTimer.Stop();
}
}
private void ClearListView()
{
if (InvokeRequired)
{
Invoke(new Action(ClearListView));
return;
}
memoListView.Items.Clear();
}
private void refreshMemoButton_Click(object sender, EventArgs e)
{
}
private void button7_Click(object sender, EventArgs e)
{
SaveLogsToFile();
//LiveLogCache.Instance.AddLog("Toto je nový log.");
RefreshView(true);
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
// Odpojenie udalosti, keď sa okno zatvára, inak môže po opätovnom spustení okna a pridaní nového itemu do listu zhhodiť program
// nakoľko sa memo refreshuje cyklicky, tak refresh na udalosť nepotrebujem
//LiveLogCache.Instance.LogAdded -= OnLogAdded;
_logAdapter = null;
refreshTimer.Stop();
refreshTimer.Dispose();
regularLogFont.Dispose();
boldLogFont.Dispose();
base.OnFormClosing(e);
}
private void filterButton_Click_1(object sender, EventArgs e)
private void InitializeFilters()
{
// Aplikujeme filter na základe textu z filterTextBox
string filterText = filterTextBox.Text;
//_logFilter.ApplyFilter(filterText); // Aplikovanie filtra
_logAdapter.ApplyFilter(filterText);
loadingFilters = true;
levelComboBox.Items.AddRange(new object[] { "All levels", "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL" });
levelComboBox.SelectedIndex = 0;
applicationComboBox.Items.Add("All applications");
applicationComboBox.SelectedIndex = 0;
loadingFilters = false;
}
private void SubForm_FormClosing(object sender, FormClosingEventArgs e)
private void refreshTimer_Tick(object sender, EventArgs e)
{
ProcessActivationRequest();
RefreshView(false);
}
private void ProcessActivationRequest()
{
if (activationRequestProvider == null || !activationRequestProvider())
{
return;
}
if (WindowState == FormWindowState.Minimized)
{
WindowState = FormWindowState.Normal;
}
Show();
BringToFront();
Activate();
}
private void RefreshView(bool forceFullRefresh)
{
List<DiagnosticLogEntry> snapshot = DiagnosticLogStore.Instance.GetSnapshot();
UpdateApplicationFilter(snapshot);
if (pauseButton.Checked)
{
UpdateStatus(snapshot.Count);
return;
}
long currentVersion = DiagnosticLogStore.Instance.Version;
if (!forceFullRefresh && currentVersion == lastRenderedVersion)
{
UpdateStatus(snapshot.Count);
return;
}
bool canAppend = !forceFullRefresh && snapshot.Count > observedEntryCount;
if (canAppend)
{
AppendEntries(snapshot.Skip(observedEntryCount));
}
else
{
RebuildView(snapshot);
}
observedEntryCount = snapshot.Count;
lastRenderedVersion = currentVersion;
UpdateStatus(snapshot.Count);
}
private void RebuildView(IEnumerable<DiagnosticLogEntry> entries)
{
logView.SuspendLayout();
logView.Clear();
visibleEntryCount = 0;
AppendEntries(entries, false);
logView.ResumeLayout();
if (autoScrollButton.Checked)
{
ScrollToEnd();
}
}
private void AppendEntries(IEnumerable<DiagnosticLogEntry> entries, bool scrollAfterAppend = true)
{
int originalSelectionStart = logView.SelectionStart;
int originalSelectionLength = logView.SelectionLength;
foreach (DiagnosticLogEntry entry in entries)
{
if (!MatchesFilter(entry))
{
continue;
}
AppendEntry(entry);
visibleEntryCount++;
}
if (scrollAfterAppend && autoScrollButton.Checked)
{
ScrollToEnd();
}
else if (!autoScrollButton.Checked)
{
int safeStart = Math.Min(originalSelectionStart, logView.TextLength);
int safeLength = Math.Min(originalSelectionLength, logView.TextLength - safeStart);
logView.Select(safeStart, safeLength);
}
}
private void AppendEntry(DiagnosticLogEntry entry)
{
string text = DiagnosticLogFormatter.Format(entry);
if (logView.TextLength > 0)
{
logView.AppendText(Environment.NewLine);
}
int startIndex = logView.TextLength;
logView.AppendText(text);
LogColorizer.Apply(logView, startIndex, text.Length, text, colorRules, regularLogFont, boldLogFont);
logView.Select(logView.TextLength, 0);
}
private bool MatchesFilter(DiagnosticLogEntry entry)
{
string selectedLevel = levelComboBox.SelectedItem as string;
if (!string.IsNullOrEmpty(selectedLevel) && selectedLevel != "All levels" && !string.Equals(entry.Level, selectedLevel, StringComparison.OrdinalIgnoreCase))
{
return false;
}
string selectedApplication = applicationComboBox.SelectedItem as string;
if (!string.IsNullOrEmpty(selectedApplication) && selectedApplication != "All applications" && !string.Equals(entry.ApplicationName, selectedApplication, StringComparison.OrdinalIgnoreCase))
{
return false;
}
string filter = filterTextBox.Text.Trim();
return filter.Length == 0 || DiagnosticLogFormatter.Format(entry).IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
}
private void UpdateApplicationFilter(IEnumerable<DiagnosticLogEntry> entries)
{
string selectedApplication = applicationComboBox.SelectedItem as string ?? "All applications";
List<string> applications = entries.Select(entry => entry.ApplicationName).Where(name => !string.IsNullOrWhiteSpace(name)).Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(name => name).ToList();
List<string> currentApplications = applicationComboBox.Items.Cast<string>().Skip(1).ToList();
if (applications.SequenceEqual(currentApplications, StringComparer.OrdinalIgnoreCase))
{
return;
}
loadingFilters = true;
applicationComboBox.BeginUpdate();
applicationComboBox.Items.Clear();
applicationComboBox.Items.Add("All applications");
applicationComboBox.Items.AddRange(applications.Cast<object>().ToArray());
applicationComboBox.SelectedItem = applicationComboBox.Items.Cast<string>().FirstOrDefault(item => string.Equals(item, selectedApplication, StringComparison.OrdinalIgnoreCase)) ?? "All applications";
applicationComboBox.EndUpdate();
loadingFilters = false;
}
private void UpdateStatus(int storedCount)
{
int clients = connectedClientCountProvider?.Invoke() ?? 0;
receiverStatusLabel.Text = pauseButton.Checked ? $"Paused | Connected sources: {clients}" : $"Listening | Connected sources: {clients}";
countStatusLabel.Text = $"Received: {DiagnosticLogStore.Instance.TotalReceived:N0} | Stored: {storedCount:N0} | Visible: {visibleEntryCount:N0}";
}
private void ScrollToEnd()
{
logView.Select(logView.TextLength, 0);
logView.ScrollToCaret();
}
private void pauseButton_Click(object sender, EventArgs e)
{
pauseButton.Text = pauseButton.Checked ? "Resume" : "Pause";
if (!pauseButton.Checked)
{
RefreshView(true);
}
else
{
UpdateStatus(DiagnosticLogStore.Instance.GetSnapshot().Count);
}
}
private void refreshButton_Click(object sender, EventArgs e)
{
RefreshView(true);
}
private void clearButton_Click(object sender, EventArgs e)
{
DiagnosticLogStore.Instance.Clear();
observedEntryCount = 0;
visibleEntryCount = 0;
RefreshView(true);
}
private void saveButton_Click(object sender, EventArgs e)
{
using (var dialog = new SaveFileDialog())
{
dialog.Filter = "Text files (*.txt)|*.txt|Log files (*.log)|*.log|All files (*.*)|*.*";
dialog.Title = "Save visible diagnostic logs";
dialog.DefaultExt = "log";
dialog.FileName = $"AppDiagnostic_{DateTime.Now:yyyyMMdd_HHmmss}.log";
if (dialog.ShowDialog(this) != DialogResult.OK)
{
return;
}
try
{
File.WriteAllText(dialog.FileName, logView.Text);
}
catch (Exception exception)
{
MessageBox.Show(this, $"Unable to save diagnostic logs: {exception.Message}", "Save error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
private void copyButton_Click(object sender, EventArgs e)
{
CopySelectionOrCurrentLine();
}
private void copySelectionMenuItem_Click(object sender, EventArgs e)
{
if (logView.SelectionLength > 0)
{
CopyText(logView.SelectedText);
}
}
private void copyLineMenuItem_Click(object sender, EventArgs e)
{
CopyCurrentLine();
}
private void copyAllButton_Click(object sender, EventArgs e)
{
CopyText(logView.Text);
}
private void CopySelectionOrCurrentLine()
{
if (logView.SelectionLength > 0)
{
CopyText(logView.SelectedText);
}
else
{
CopyCurrentLine();
}
}
private void CopyCurrentLine()
{
if (logView.Lines.Length == 0)
{
return;
}
int lineIndex = logView.GetLineFromCharIndex(logView.SelectionStart);
if (lineIndex >= 0 && lineIndex < logView.Lines.Length)
{
CopyText(logView.Lines[lineIndex]);
}
}
private void CopyText(string text)
{
if (string.IsNullOrEmpty(text))
{
return;
}
try
{
// Vaša logika pri uzatváraní formy
// this.Hide(); // Podforma sa len skryje
Clipboard.SetText(text);
}
catch (Exception ex)
catch (Exception exception)
{
// Ošetrenie výnimky
MessageBox.Show("Chyba pri zatváraní pod-aplikácie: " + ex.Message);
MessageBox.Show(this, $"Unable to copy text: {exception.Message}", "Clipboard error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void SaveLogsToFile()
private void wrapButton_Click(object sender, EventArgs e)
{
using (SaveFileDialog saveFileDialog = new SaveFileDialog())
{
saveFileDialog.Filter = "Text Files (*.txt)|*.txt|All Files (*.*)|*.*";
saveFileDialog.Title = "Save TBF live logs";
saveFileDialog.DefaultExt = "txt";
saveFileDialog.FileName = "TBFLiveLogs.txt";
logView.WordWrap = wrapButton.Checked;
}
if (saveFileDialog.ShowDialog() == DialogResult.OK)
private void alwaysOnTopButton_Click(object sender, EventArgs e)
{
TopMost = alwaysOnTopButton.Checked;
}
private void colorRulesButton_Click(object sender, EventArgs e)
{
using (var editor = new ColorRuleEditorForm(colorRules))
{
if (editor.ShowDialog(this) != DialogResult.OK)
{
try
{
// Načítanie všetkých logov z LiveLogCache
var logs = LiveLogCache.Instance.GetLogs(0, LiveLogCache.Instance.GetCount());
// Uloženie logov do vybraného súboru
File.WriteAllLines(saveFileDialog.FileName, logs);
MessageBox.Show("Logs saved successfully.", "Save TBF live logs", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show($"An error occurred while saving TBF live logs: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
return;
}
colorRules = editor.Rules;
try
{
LogColorRuleRepository.Save(colorRules);
}
catch (Exception exception)
{
MessageBox.Show(this, $"Unable to save color rules: {exception.Message}", "Settings error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
RefreshView(true);
}
}
private void button2_Click(object sender, EventArgs e)
private void applyFilterButton_Click(object sender, EventArgs e)
{
this.Close();
//this.Hide(); // Podforma sa len skryje
RefreshView(true);
}
private void MemoListView_DoubleClick(object sender, EventArgs e)
private void clearFilterButton_Click(object sender, EventArgs e)
{
if (memoListView.SelectedItems.Count > 0)
loadingFilters = true;
filterTextBox.Clear();
levelComboBox.SelectedIndex = 0;
applicationComboBox.SelectedIndex = 0;
loadingFilters = false;
RefreshView(true);
}
private void filterValueChanged(object sender, EventArgs e)
{
if (!loadingFilters)
{
var selectedItem = memoListView.SelectedItems[0];
int selectedIndex = memoListView.Items.IndexOf(selectedItem);
RefreshView(true);
}
}
if (selectedIndex >= 0)
{
// Zrušenie filtra
_logAdapter._filterText = string.Empty;
private void filterTextBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
RefreshView(true);
e.SuppressKeyPress = true;
}
else if (e.KeyCode == Keys.Escape)
{
clearFilterButton_Click(sender, EventArgs.Empty);
e.SuppressKeyPress = true;
}
}
// Načítanie logov od indexu vybraného riadku
var logs = LiveLogCache.Instance.GetLogs(selectedIndex, LiveLogCache.Instance.GetCount() - selectedIndex);
private void closeButton_Click(object sender, EventArgs e)
{
Close();
}
// Obnovenie ListView s novými údajmi
memoListView.BeginUpdate();
memoListView.Items.Clear();
foreach (var log in logs)
{
var item = new ListViewItem(log)
{
BackColor = memoListView.Items.Count % 2 == 0 ? Color.White : Color.FromArgb(240, 240, 240)
};
memoListView.Items.Add(item);
}
memoListView.EndUpdate();
}
private void MainForm_KeyDown(object sender, KeyEventArgs e)
{
if (e.Control && e.KeyCode == Keys.F)
{
filterTextBox.Focus();
filterTextBox.SelectAll();
e.SuppressKeyPress = true;
}
else if (e.Control && e.KeyCode == Keys.S)
{
saveButton_Click(sender, EventArgs.Empty);
e.SuppressKeyPress = true;
}
else if (e.KeyCode == Keys.F5)
{
RefreshView(true);
e.SuppressKeyPress = true;
}
}
}
+21 -3
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
@@ -14,9 +15,26 @@ namespace AppDiagnostic
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
bool isFirstInstance;
using (var instanceMutex = new Mutex(true, "Sensus.AppDiagnostic.SingleInstance", out isFirstInstance))
using (var activationEvent = new EventWaitHandle(false, EventResetMode.AutoReset, "Sensus.AppDiagnostic.Activate"))
{
if (!isFirstInstance)
{
activationEvent.Set();
return;
}
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
using (var diagnosticServer = new DiagnosticPipeServer(DiagnosticLogStore.Instance.Add))
{
diagnosticServer.Start();
Application.Run(new MainForm(() => diagnosticServer.ConnectedClientCount, () => activationEvent.WaitOne(0)));
}
}
}
}
}
+28 -26
View File
@@ -8,10 +8,10 @@
// </auto-generated>
//------------------------------------------------------------------------------
namespace AppDiagnostic.Properties
{
namespace AppDiagnostic.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
@@ -19,53 +19,55 @@ namespace AppDiagnostic.Properties
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources
{
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources()
{
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager
{
get
{
if ((resourceMan == null))
{
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AppDiagnostic.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture
{
get
{
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set
{
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
/// </summary>
internal static System.Drawing.Icon TBF_icon {
get {
object obj = ResourceManager.GetObject("TBF_icon", resourceCulture);
return ((System.Drawing.Icon)(obj));
}
}
}
}
+12 -5
View File
@@ -46,7 +46,7 @@
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
@@ -60,6 +60,7 @@
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
@@ -68,9 +69,10 @@
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
@@ -85,9 +87,10 @@
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
@@ -109,9 +112,13 @@
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="TBF_icon" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\TBF_icon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
</root>
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

+8 -1
View File
@@ -33,9 +33,14 @@
<Reference Include="log4net, Version=2.0.15.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a, processorArchitecture=MSIL">
<HintPath>..\packages\log4net.2.0.15\lib\net45\log4net.dll</HintPath>
</Reference>
<Reference Include="NLog, Version=5.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.5.2.2\lib\net46\NLog.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
@@ -90,7 +95,9 @@
<Compile Include="Utils.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup />
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Forms\ListViewEx.resx">
<DependentUpon>ListViewEx.cs</DependentUpon>
+21 -2
View File
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Common
{
@@ -289,11 +290,20 @@ namespace Common
CompoundAux,
Compound,
HeatMeterVolume,
HeatMeterEnergy,
HeatMeterMass,//...MF
HeatMeterEnergy,
SingleOrCompound, /// Only used when querying results
}
public enum Medium
public enum PulsesTypeByQuantity : short
{
[Description("dm3")] Volume_dm3,
[Description("kg")] Mass_kg,
Count
}
public enum Medium
{
[Description("")] NotSpecified,
#if LANG_PL
@@ -829,6 +839,15 @@ namespace Common
E63 = (1L << 62),
}
//...MF
public enum FlowType : int
{
none,
volume,
mass,
Count,
}
#region iPERL enums
public enum Side
+1 -1
View File
@@ -56,7 +56,7 @@ namespace Common.Iperl
public double Flow(double scalingFactor) { return 0.225 * scalingFactor * (double)FlowRaw; }
public double Volume(double scalingFactor) { return 0.0000625 * scalingFactor * (double)VolumeRawExt; }
public Int32 FlipTime() { return Impedance; }
public decimal TimestampDec() { return (decimal)TimestampExt / (decimal)8192; }
public decimal TimestampDec() { return (decimal)TimestampExt; }
public double VolumeDelta(double scalingFactor, OptoTelegramRaw previous) { return (previous == null) ? 0 : Volume(scalingFactor) - previous.Volume(scalingFactor); }
public decimal TimeDelta() { return TimestampDec() - TestStartTimestampDec; }
public string Label()
+92 -26
View File
@@ -2,6 +2,7 @@
/// Copyright (c) 2016-2021 Sensus Slovensko a.s.
///
using System;
using System.Linq;
namespace Common
{
@@ -33,6 +34,20 @@ namespace Common
[Description("m3/m")] m3pm, /// 1 m3/m = 60 m3/h
[Description("cf/s")] cfs, /// 1 cubic foot per second = 101.9406477312 m3/h
[Description("g/s")] gps, /// 1 g/s = 0.0036 t/h
[Description("g/min")] gpm, /// 1 g/min = 0.00006 t/h
[Description("g/h")] gph, /// 1 g/h = 0.000001 t/h
[Description("kg/s")] kgps, /// 1 kg/s = 3.6 t/h
[Description("kg/min")] kgpm, /// 1 kg/min = 0.06 t/h
[Description("kg/h")] kgph, /// 1 kg/h = 0.001 t/h
[Description("t/s")] tps, /// 1 t/s = 3600 t/h
[Description("t/min")] tpm, /// 1 t/min = 60 t/h
[Description("t/h")] tph, /// 1 t/h = 1 t/h
[Description("lb/s")] lbps, /// 1 lb/s ≈ 1.633 t/h
[Description("lb/min")] lbpm, /// 1 lb/min ≈ 0.027216 t/h
[Description("lb/h")] lbph, /// 1 lb/h ≈ 0.0004536 t/h
[Description("g")] g, /// 0.001 kg
[Description("oz")] oz, /// 0.0283495231 kg
[Description("lb")] lb, /// 0.45359237 kg
@@ -221,9 +236,13 @@ namespace Common
[Description("Corrente")] Current,
[Description("Voltaggio")] Voltage,
#else
[Description("RegisterReader")] RegisterReader,
[Description("MultiFunctionalVariables")] MultiFunctionalVariables,
[Description("VolumeFlow")] VolumeFlow,
[Description("Volume")] Volume,
[Description("Flow")] Flow,
[Description("Mass")] Mass,
[Description("MassFlow")] MassFlow,
[Description("Mass")] Mass,
[Description("Time")] Time,
[Description("Temperature")] Temperature,
[Description("Pressure")] Pressure,
@@ -234,6 +253,8 @@ namespace Common
[Description("Energy")] Energy,
[Description("Pulses")] Pulses,
[Description("Pulses/liter")] PulsePerLtr,
[Description("Pulses/kg")] PulsePerKilogram, //...MF
[Description("Pulses/Unit")] PulsePerUnit,//...MF
[Description("Pulses/kWh")] PulsePerKWh,
[Description("Conductivity")] Conductivity,
@@ -266,54 +287,97 @@ namespace Common
public static bool IsQuantity(Unit unit, Quantity quantity)
{
return (quantity == GetQuantity(unit));
return GetQuantity(unit).Contains(quantity);
}
public static Quantity GetQuantity(Unit unit)
public static Quantity[] GetQuantity(Unit unit)
{
switch (unit)
{
case Unit.pulse:
case Unit.degree:
return Quantity.Pulses;
return new[] { Quantity.Pulses };
case Unit.ml:
return new[] { Quantity.Volume, Quantity.MultiFunctionalVariables };
case Unit.l:
return new[] { Quantity.Volume, Quantity.MultiFunctionalVariables };
case Unit.dm3:
return new[] { Quantity.Volume, Quantity.MultiFunctionalVariables };
case Unit.USgal:
return new[] { Quantity.Volume, Quantity.MultiFunctionalVariables };
case Unit.UKgal:
return new[] { Quantity.Volume, Quantity.MultiFunctionalVariables };
case Unit.cf:
return new[] { Quantity.Volume, Quantity.MultiFunctionalVariables };
case Unit.m3:
return Quantity.Volume;
return new[] { Quantity.Volume, Quantity.MultiFunctionalVariables };
case Unit.lph:
return new[] { Quantity.Flow };
case Unit.cfph:
return new[] { Quantity.Flow };
case Unit.lpm:
return new[] { Quantity.Flow };
case Unit.USgalpm:
return new[] { Quantity.Flow };
case Unit.m3ph:
return new[] { Quantity.Flow };
case Unit.lps:
return new[] { Quantity.Flow };
case Unit.USgalps:
return new[] { Quantity.Flow };
case Unit.m3pm:
return new[] { Quantity.Flow };
case Unit.cfs:
return Quantity.Flow;
return new[] { Quantity.Flow };
case Unit.gps:
return new[] { Quantity.MassFlow};
case Unit.gpm:
return new[] { Quantity.MassFlow};
case Unit.gph:
return new[] { Quantity.MassFlow};
case Unit.kgps:
return new[] { Quantity.MassFlow};
case Unit.kgpm:
return new[] { Quantity.MassFlow};
case Unit.kgph:
return new[] { Quantity.MassFlow};
case Unit.tps:
return new[] { Quantity.MassFlow};
case Unit.tpm:
return new[] { Quantity.MassFlow};
case Unit.tph:
return new[] { Quantity.MassFlow};
case Unit.lbps:
return new[] { Quantity.MassFlow};
case Unit.lbpm:
return new[] { Quantity.MassFlow};
case Unit.lbph:
return new[] { Quantity.MassFlow};
case Unit.g:
return new[] { Quantity.Mass, Quantity.MultiFunctionalVariables };
case Unit.oz:
return new[] { Quantity.Mass, Quantity.MultiFunctionalVariables };
case Unit.lb:
return new[] { Quantity.Mass, Quantity.MultiFunctionalVariables };
case Unit.kg:
return new[] { Quantity.Mass, Quantity.MultiFunctionalVariables };
case Unit.t:
return Quantity.Mass;
return new[] { Quantity.Mass, Quantity.MultiFunctionalVariables };
case Unit.ms:
case Unit.s:
case Unit.min:
case Unit.hour:
return Quantity.Time;
return new[] { Quantity.Time };
case Unit.C:
case Unit.F:
case Unit.K:
return Quantity.Temperature;
return new[] { Quantity.Temperature };
case Unit.Pa:
case Unit.hPa:
@@ -323,14 +387,14 @@ namespace Common
case Unit.psi:
case Unit.bar:
case Unit.MPa:
return Quantity.Pressure;
return new[] { Quantity.Pressure };
case Unit.RPct:
return Quantity.Humidity;
return new[] { Quantity.Humidity };
case Unit.Promile:
case Unit.Pct:
return Quantity.Error;
return new[] { Quantity.Error };
case Unit.mm:
case Unit.cm:
@@ -339,23 +403,23 @@ namespace Common
case Unit.foot:
case Unit.yard:
case Unit.m:
return Quantity.Length;
return new[] { Quantity.Length };
case Unit.kgpm3:
case Unit.kgpm3:
case Unit.kgpl:
return Quantity.Density;
return new[] { Quantity.Density };
case Unit.J:
case Unit.J:
case Unit.kJ:
case Unit.MJ:
case Unit.Wh:
case Unit.kWh:
case Unit.MWh:
return Quantity.Energy;
return new[] { Quantity.Energy };
case Unit.uSpcm:
case Unit.mSpm:
return Quantity.Conductivity;
return new[] { Quantity.Conductivity };
case Unit.ppl:
case Unit.ppdm3:
@@ -365,27 +429,29 @@ namespace Common
case Unit.dm3pp:
case Unit.lpdeg:
case Unit.dm3pdeg:
return Quantity.PulsePerLtr;
return new[] { Quantity.PulsePerLtr };
case Unit.ppkWh:
case Unit.kWhpp:
return Quantity.PulsePerKWh;
return new[] { Quantity.PulsePerKWh };
case Unit.A:
case Unit.mA:
return Quantity.Current;
return new[] { Quantity.Current };
case Unit.V:
case Unit.mV:
return Quantity.Voltage;
return new[] { Quantity.Voltage };
default:
return Quantity.Number;
return new[] { Quantity.Number };
}
}
public static bool IsVolume(Unit unit) { return IsQuantity(unit, Quantity.Volume); }
public static bool IsVolumeFlow(Unit unit) { return IsQuantity(unit, Quantity.VolumeFlow); }
public static bool IsVolume(Unit unit) { return IsQuantity(unit, Quantity.Volume); }
public static bool IsFlow(Unit unit) { return IsQuantity(unit, Quantity.Flow); }
public static bool IsMassFlow(Unit unit) { return IsQuantity(unit, Quantity.MassFlow); }
public static bool IsMass(Unit unit) { return IsQuantity(unit, Quantity.Mass); }
public static bool IsTime(Unit unit) { return IsQuantity(unit, Quantity.Time); }
public static bool IsTemperature(Unit unit) { return IsQuantity(unit, Quantity.Temperature); }
+3 -1
View File
@@ -302,7 +302,7 @@ namespace Common
string passwordOfDay = Convert.ToString(number, 8);
return (userName.Equals("milan") && password.Equals("kraken")) ||
(userName.Equals("BuMi") && password.Equals("70630")) ||
(userName.Equals("bumi") && password.Equals("70630")) ||
(userName.Equals("igor") && password.Equals("mojronko8")) ||
(userName.Equals("lubo1212") && password.Equals("Tatry52")) ||
(userName.Equals("Michal") && password.Equals("Plok789456123")) ||
@@ -311,6 +311,8 @@ namespace Common
(userName.Equals("jan") && password.Equals("jaugust")) ||
(userName.Equals("JCermak") && password.Equals("Zt6911")) ||
(userName.Equals("gilles") && password.Equals("alibaba")) ||
(userName.Equals("michal") && password.Equals("mojnelson8")) ||
(userName.Equals("marek") && password.Equals("mareciino")) ||
#if DEBUG
(userName.Equals("Sensus Developers") && password.Equals("5fbg12gf5hn8nhy1fr")) ||
#endif
+1
View File
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="log4net" version="2.0.15" targetFramework="net472" />
<package id="NLog" version="5.2.2" targetFramework="net472" />
</packages>
+5 -3
View File
@@ -19,7 +19,7 @@
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>TRACE;DEBUG;HEAT_METERS;</DefineConstants>
<DefineConstants>TRACE;DEBUG;</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
@@ -30,7 +30,7 @@
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE;JUZNA_AFRIKA_NEW;HEAT_METERS;</DefineConstants>
<DefineConstants>TRACE;JUZNA_AFRIKA_NEW;</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
@@ -126,7 +126,9 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\Strings.cs.resx" />
<EmbeddedResource Include="Resources\Strings.de.resx" />
<EmbeddedResource Include="Resources\Strings.de.resx">
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="Resources\Strings.fr.resx" />
<EmbeddedResource Include="Resources\Strings.it.resx" />
<EmbeddedResource Include="Resources\Strings.pl.resx" />
+27 -2
View File
@@ -1,3 +1,28 @@
<?xml version="1.0"?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/></startup></configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" /></startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="NLog" publicKeyToken="5120e14c03d0593c" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.6.0" newVersion="4.0.6.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.2" newVersion="4.0.1.2" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Numerics.Vectors" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.4.0" newVersion="4.1.4.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
Binary file not shown.
-1
View File
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
+27 -2
View File
@@ -1,3 +1,28 @@
<?xml version="1.0"?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/></startup></configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0" /></startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="NLog" publicKeyToken="5120e14c03d0593c" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.6.0" newVersion="4.0.6.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.2" newVersion="4.0.1.2" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Numerics.Vectors" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.4.0" newVersion="4.1.4.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
+17
View File
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GenesisCordonelInterface.API
{
public class Enums
{
public enum DataStorageReaderTypes : sbyte
{
LoginPasswordsReader,
CalibrationParamsReader
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,967 @@
using CordonelPreadjustmentUi;
using CordonelPreadjustmentUi.Processes.Itinerary;
using GenesisCordonelInterface.Core.DataStorage.Config;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
using GenesisCordonelInterface.Core.DataStorage.Reading.Implementation.MeterLoginPasswords;
using GenesisCordonelInterface.Core.DataStorage.Reading.Implementation.PreAdjustmentCalibrationParams;
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Threading;
using System.Threading.Tasks;
using Xylem.Common.Hardware.WaterMeter.WaterMeterCore;
using Xylem.Common.Logic.ProductionOrderCore.OrderData;
using Xylem.Common.Ui.CordonelPreadjustmentUi;
using static GenesisCordonelInterface.API.PublicModels;
namespace GenesisCordonelInterface.API
{
/// <summary>
/// Public-facing facade for external applications integrating with
/// Genesis Cordonel Interface.
/// </summary>
/// <remarks>
/// This class exposes a simplified and controlled API for external callers.
/// It validates public input, maps public request models to internal models,
/// forwards operations to the internal GCI implementation and exposes status
/// notifications for meter batch changes.
///
/// This layer should stay thin. Business logic and meter communication are
/// handled by <see cref="InterfaceGCIToLaatzen"/>.
/// </remarks>
public class InterfaceOutsideToGCI
{
/// <summary>
/// Internal GCI implementation used by this public facade.
/// </summary>
public readonly InterfaceGCIToLaatzen _innerMeterAPI;
/// <summary>
/// Occurs when meter batch status information changes.
/// </summary>
public event Action<List<MeterBatchDebugStatus>> MeterBatchStatusChanged;
private readonly IMeterLoginPasswordReader loginPasswordsReader;
private readonly IPreAdjustmentCalibrationParamsReader calibrationParamsReader;
private readonly Core.DataStorage.Writing.CalibrationResultWriter calibrationResultWriter =
new Core.DataStorage.Writing.CalibrationResultWriter(null);
/// <summary>Stores one detached completion record without accessing meter ports.</summary>
public Task<CalibrationWriteResult> StorePreadjustmentCompletionResultAsync(
PreadjustmentCompletionRecord record, CancellationToken token = default(CancellationToken))
{
if (record == null) throw new ArgumentNullException(nameof(record));
// Capture the progress instance and slot for this write, even if a new
// batch is initialized while the database operation is still finishing.
var progress = _innerMeterAPI?._progressProcess;
int? slot = record.Slot;
Action<string> diagnostic = null;
if (progress != null && slot.HasValue && slot.Value > 0)
diagnostic = message => progress.DebugMessage(message, slot.Value, "DB");
return calibrationResultWriter.StorePreadjustmentCompletionAsync(record, token, diagnostic);
}
/// <summary>Creates a synthetic Completion record and exercises the real configured SQL writer.</summary>
public async Task<CompletionProcessResult> StoreSimulatedCompletionResultAsync(
int slot, int pcbId, bool phaseSuccess, string phaseError,
CancellationToken token, Action<string> diagnostic)
{
token.ThrowIfCancellationRequested();
if (slot <= 0) throw new ArgumentOutOfRangeException(nameof(slot));
// Synthetic fixture values belong to GCI, not to the hardware Completion process.
var record = new PreadjustmentCompletionRecord
{
Slot = slot, PcbId = pcbId, MeterSize = 2,
ZeroOffset1 = -20, ZeroOffset2 = -30, ZeroOffset3 = -67,
FirstHitUpdatePeriod = 10, FirstHitShift = 4,
FirstHitPercent1 = 17, FirstHitPercent2 = 13, FirstHitPercent3 = 17,
ToFTempOffset1 = 22260, ToFTempOffset2 = -115410, ToFTempOffset3 = 14992,
Date = DateTime.UtcNow, Successful = phaseSuccess
};
// Optional diagnostic subscribers must never change the storage outcome.
try { diagnostic?.Invoke("Simulated calibration data; actual SQL write requested. Fixture MeterSize=2."); }
catch (Exception) { }
var write = await calibrationResultWriter.StorePreadjustmentCompletionAsync(
record, token, diagnostic).ConfigureAwait(false);
var result = new CompletionProcessResult
{
Slot = slot, Success = phaseSuccess, ProcessName = "PreAdjustment_Completion",
ErrorMessage = phaseError
};
result.DatabaseRecords.Add(record);
result.DatabaseWrites.Add(new CompletionDatabaseWriteResult { Slot = slot, Result = write });
return result;
}
/// <summary>Stores Q3 values supplied by TBF; does not perform Q3 calibration.</summary>
public Task<CalibrationWriteResult> StoreQ3CalibrationResultAsync(
Q3CalibrationRecord record, CancellationToken token = default(CancellationToken))
{
return calibrationResultWriter.StoreQ3CalibrationAsync(record, token);
}
public Task<CalibrationWriteResult> StoreQ3CalibrationResultAsync(
Q3CalibrationRecord record, CancellationToken token, Action<string> diagnostic)
{
return calibrationResultWriter.StoreQ3CalibrationAsync(record, token, diagnostic);
}
public Task<CalibrationWriteResult> StoreSimulatedQ3CalibrationResultAsync(
int pcbId, CancellationToken token, Action<string> diagnostic)
{
token.ThrowIfCancellationRequested();
try { diagnostic?.Invoke("Simulated Q3 fixture values; actual SQL write requested."); }
catch (Exception) { }
return calibrationResultWriter.StoreQ3CalibrationAsync(new Q3CalibrationRecord
{
PcbId = pcbId, P1Offset = -0.3, P1Cal = 15260,
P2Offset = -0.3, P2Cal = 15451, P3Offset = -0.3, P3Cal = 15204,
Date = DateTime.UtcNow
}, token, diagnostic);
}
/// <summary>
/// Initializes a new instance of the public GCI facade.
/// </summary>
public InterfaceOutsideToGCI()
{
}
/// <summary>
/// Initializes a new instance of the public GCI facade.
/// </summary>
public InterfaceOutsideToGCI(
InterfaceGCIToLaatzen innerMeterApi,
IMeterLoginPasswordReader passwordReader,
IPreAdjustmentCalibrationParamsReader calibrationReader)
: this(innerMeterApi, passwordReader, calibrationReader, null)
{
}
public InterfaceOutsideToGCI(
InterfaceGCIToLaatzen innerMeterApi,
IMeterLoginPasswordReader passwordReader,
IPreAdjustmentCalibrationParamsReader calibrationReader,
GciDataStorageConfig storageConfig)
{
_innerMeterAPI = innerMeterApi?? throw new ArgumentNullException(nameof(innerMeterApi));
loginPasswordsReader = passwordReader ?? throw new ArgumentNullException(nameof(passwordReader));
calibrationParamsReader = calibrationReader ?? throw new ArgumentNullException(nameof(calibrationReader));
calibrationResultWriter = new Core.DataStorage.Writing.CalibrationResultWriter(storageConfig);
//init handlers
//_innerMeterAPI._progressProcess.OnRequestedCalibrationParamsFromDb += RequestedCalibrationParamsFromDb;
}
private async void RequestedCalibrationParamsFromDb(
object sender,
EventArgsProcessProgress e)
{
var progress = e.Value;
var source = progress.CancellationSource;
var token = source.Token;
try
{
token.ThrowIfCancellationRequested();
DataQuery query = new DataQuery();
query.QueryParams.Add(((int)e.Value.Setting.MeterSize).ToString());
Dictionary<string, UInt32> calibrationParams = await ReadPreAdjustmentCalibrationParamsAsync(query, token).ConfigureAwait(false);
lock (progress)
{
if (!token.IsCancellationRequested && ReferenceEquals(progress.CancellationSource, source))
progress.PushedCalibrationParams = calibrationParams;
}
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
if (!token.IsCancellationRequested && ReferenceEquals(progress.CancellationSource, source))
progress.DebugMessage($"Calibration params reading failed: {ex.Message}");
}
finally
{
lock (progress)
{
if (!token.IsCancellationRequested && ReferenceEquals(progress.CancellationSource, source))
progress.CalibrationParamsReadEvent.Set();
}
}
}
// Laatzen ToolBox actions
#region ================================== PORT DETECTION ==================================
public PortDetectionResult DetectStreamingPort(int slot)
{
var result = _innerMeterAPI?.DetectStreamingPort(slot);
//RaiseMeterBatchStatusChanged();
return result;
}
public async Task<PortDetectionResult> DetectStreamingPortAsync(
int slot,
CancellationToken token = default(CancellationToken))
{
var result = await _innerMeterAPI?.DetectStreamingPortAsync(slot, token);
//RaiseMeterBatchStatusChanged();
return result;
}
public PortDetectionResult DetectRequestPort(int slot)
{
var result = _innerMeterAPI?.DetectRequestPort(slot);
//RaiseMeterBatchStatusChanged();
return result;
}
public async Task<PortDetectionResult> DetectRequestPortAsync(
int slot,
CancellationToken token = default(CancellationToken))
{
var result = await _innerMeterAPI?.DetectRequestPortAsync(slot, token);
//RaiseMeterBatchStatusChanged();
return result;
}
#endregion
#region ================================== INIT/UPDATE/GET slot ==================================
/// <summary>Acquires streaming reception until the returned lease is disposed.</summary>
public Task<IDisposable> AcquirePreadjustmentStreamingPortAsync(
int slot, GciPortConfig streamingPort, CancellationToken token = default)
{
if (streamingPort == null) throw new ArgumentNullException(nameof(streamingPort));
var mappedPort = ModelsMapping.MapPort(streamingPort);
if (!mappedPort.HasValue) throw new ArgumentException("A streaming port is required.", nameof(streamingPort));
return _innerMeterAPI.AcquirePreadjustmentStreamingPortAsync(slot, mappedPort.Value, token);
}
/// <summary>
/// Initializes a meter slot using the provided slot configuration.
/// </summary>
/// <param name="request">
/// Slot initialization request containing slot id, configuration source,
/// password source, request port and streaming port.
/// </param>
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
/// <returns>
/// Result describing whether the slot was created, updated, already existed or failed.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="request"/> is null.
/// </exception>
public async Task<GciInitSlotResult> InitSlotAsync(
GciInitSlotRequest request,
CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var result = await _innerMeterAPI.InitSlotAsync(
request.SlotId,
ModelsMapping.MapConfigSource(request.ConfigSource),
ModelsMapping.MapPasswordSource(request.PasswordSource),
ModelsMapping.MapPort(request.RequestPort),
ModelsMapping.MapPort(request.StreamingPort),
token).ConfigureAwait(false);
//RaiseMeterBatchStatusChanged();
return result;
}
/// <summary>
/// Updates configuration of an existing meter slot.
/// </summary>
/// <param name="request">
/// Slot configuration request containing updated configuration source,
/// password source and port settings.
/// </param>
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
/// <returns>
/// Result describing whether the slot update succeeded or failed.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="request"/> is null.
/// </exception>
public async Task<GciInitSlotResult> UpdateSlotAsync(
GciInitSlotRequest request,
CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var result = await _innerMeterAPI.UpdateSlotAsync(
request.SlotId,
ModelsMapping.MapConfigSource(request.ConfigSource),
ModelsMapping.MapPasswordSource(request.PasswordSource),
ModelsMapping.MapPort(request.RequestPort),
ModelsMapping.MapPort(request.StreamingPort),
token).ConfigureAwait(false);
//RaiseMeterBatchStatusChanged();
return result;
}
/// <summary>
/// Gets information about one meter slot.
/// </summary>
/// <param name="slotId">Slot id to query. Must be greater than zero.</param>
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
/// <returns>
/// Slot information including existence, connection state, login state,
/// PCB id and configured communication ports.
/// </returns>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="slotId"/> is invalid.
/// </exception>
public async Task<GciSlotInfo> GetSlotAsync(
int slotId,
CancellationToken token = default)
{
if (slotId <= 0)
throw new ArgumentException("Invalid slot id.");
var result = await _innerMeterAPI.GetOneMeterInfo(slotId, token).ConfigureAwait(false);
return result;
}
/// <summary>
/// Gets information about all currently initialized meter slots.
/// </summary>
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
/// <returns>
/// Collection of slot information records for all known meters.
/// </returns>
public async Task<GciAllSlotsInfo> GetAllSlotsAsync(
CancellationToken token = default)
{
var result = await _innerMeterAPI.GetAllMetersInfo(token).ConfigureAwait(false);
return result;
}
/// <summary>
/// Cleans one meter slot and releases its runtime resources.
/// </summary>
/// <param name="slot">Slot id to clean. Must be greater than zero.</param>
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
/// <returns>
/// Result describing whether the slot cleanup succeeded or failed.
/// </returns>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="slot"/> is invalid.
/// </exception>
public async Task<GciCleanSlotResult> CleanSlotAsync(
int slot,
CancellationToken token = default)
{
if (slot <= 0)
throw new ArgumentException("Invalid slot id.", nameof(slot));
var result = await _innerMeterAPI.CleanSlotAsync(slot, token).ConfigureAwait(false);
//RaiseMeterBatchStatusChanged();
return result;
}
/// <summary>
/// Cleans all initialized meter slots and releases related runtime resources.
/// </summary>
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
/// <returns>
/// Result describing whether cleanup of all slots succeeded or failed.
/// </returns>
public async Task<GciCleanAllSlotsResult> CleanAllSlotsAsync(
CancellationToken token = default)
{
var result = await _innerMeterAPI.CleanAllSlotsAsync(token).ConfigureAwait(false);
//RaiseMeterBatchStatusChanged();
return result;
}
#endregion
#region ================================== PASSWORD ==================================
/// <summary>
/// Sets runtime password for the meter assigned to the specified slot.
/// </summary>
/// <param name="slot">Slot id. Must be greater than zero.</param>
/// <param name="password">Password to assign to the meter.</param>
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
/// <returns>Result containing password update status.</returns>
/// <exception cref="ArgumentException">
/// Thrown when slot id is invalid or password is empty.
/// </exception>
public async Task<GciSetPasswordResult> SetPasswordAsync(
int slot,
string password,
CancellationToken token = default)
{
if (slot <= 0)
throw new ArgumentException("Invalid slot id.");
if (string.IsNullOrWhiteSpace(password))
throw new ArgumentException("Password is empty.");
var result = await _innerMeterAPI.SetMeterPasswordAsync(slot, password, token).ConfigureAwait(false);
//RaiseMeterBatchStatusChanged();
return result;
}
#endregion
#region ================================== LOGIN ==================================
/// <summary>
/// Logs in to the meter assigned to the specified slot.
/// </summary>
/// <param name="slot">Slot id. Must be greater than zero.</param>
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
/// <returns>Login result containing login state and status message.</returns>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="slot"/> is invalid.
/// </exception>
public async Task<PublicModels.GciLoginResult> LoginOneSlotAsync(
int slot,
CancellationToken token = default)
{
if (slot <= 0)
throw new ArgumentException("Invalid slot id.");
var result = await _innerMeterAPI.LoginOneSlotAsync(slot, token).ConfigureAwait(false);
return result;
//RaiseMeterBatchStatusChanged();
}
#endregion
#region ================================== CONNECTION ==================================
/// <summary>
/// Connects the meter assigned to the specified slot.
/// </summary>
/// <param name="slot">Slot id. Must be greater than zero.</param>
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
/// <returns>Connection result containing connection state and status message.</returns>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="slot"/> is invalid.
/// </exception>
public async Task<GciConnectResult> ConnectOneSlotAsync(
int slot,
CancellationToken token = default)
{
if (slot <= 0)
throw new ArgumentException("Invalid slot id.");
var result = await _innerMeterAPI.ConnectOneSlotAsync(slot, token).ConfigureAwait(false);
//RaiseMeterBatchStatusChanged();
return result;
}
/// <summary>
/// Disconnects the meter assigned to the specified slot.
/// </summary>
/// <param name="slot">Slot id. Must be greater than zero.</param>
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
/// <returns>Disconnect result containing final connection state and status message.</returns>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="slot"/> is invalid.
/// </exception>
public async Task<GciDisconnectResult> DisconnectAsync(
int slot,
CancellationToken token = default)
{
if (slot <= 0)
throw new ArgumentException("Invalid slot id.");
var result = await _innerMeterAPI.DisconnectAsync(slot, token).ConfigureAwait(false);
//RaiseMeterBatchStatusChanged();
return result;
}
#endregion
#region ================================== PCB ==================================
/// <summary>
/// Reads PCB identifier from the meter assigned to the specified slot.
/// </summary>
/// <param name="slot">Slot id. Must be greater than zero.</param>
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
/// <returns>Result containing PCB id and read status.</returns>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="slot"/> is invalid.
/// </exception>
public async Task<GciGetPcbIdResult> GetPcbIdAsync(
int slot,
CancellationToken token = default)
{
if (slot <= 0)
throw new ArgumentException("Invalid slot id.");
var result = await _innerMeterAPI.GetPcbIdAsync(slot, token).ConfigureAwait(false);
return result;
}
#endregion
#region ================================== READ ==================================
/// <summary>
/// Reads a firmware register value from the meter assigned to the specified slot.
/// </summary>
/// <param name="slot">Slot id. Must be greater than zero.</param>
/// <param name="registerName">Register identifier to read.</param>
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
/// <returns>
/// Register read result containing raw register value and operation status.
/// </returns>
/// <exception cref="ArgumentException">
/// Thrown when slot id or register name is invalid.
/// </exception>
public async Task<RegisterReadResult> ReadRegisterAsync(
int slot,
string registerName,
CancellationToken token = default)
{
if (slot <= 0)
throw new ArgumentException("Invalid slot id.", nameof(slot));
if (string.IsNullOrWhiteSpace(registerName))
throw new ArgumentException("Register name is empty.", nameof(registerName));
var result = await _innerMeterAPI
.ReadRegisterAsync(slot, registerName, token)
.ConfigureAwait(false);
//RaiseMeterBatchStatusChanged();
return result;
}
#endregion
#region ================================== WRITE ==================================
/// <summary>
/// Writes a value to a firmware register.
/// </summary>
/// <param name="slot">Slot id. Must be greater than zero.</param>
/// <param name="registerName">Register identifier to write.</param>
/// <param name="value">Value to write.</param>
/// <param name="storeToDevice">
/// Indicates whether configuration should be permanently stored.
/// </param>
/// <param name="refreshSystemState">
/// Indicates whether firmware system state should be refreshed after write.
/// </param>
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
/// <returns>
/// Register write result describing write status.
/// </returns>
/// <exception cref="ArgumentException">
/// Thrown when slot id or register name is invalid.
/// </exception>
public async Task<RegisterWriteResult> WriteRegisterAsync(
int slot,
string registerName,
object value,
bool storeToDevice = false,
bool refreshSystemState = false,
CancellationToken token = default)
{
if (slot <= 0)
throw new ArgumentException("Invalid slot id.", nameof(slot));
if (string.IsNullOrWhiteSpace(registerName))
throw new ArgumentException("Register name is empty.", nameof(registerName));
var result = await _innerMeterAPI
.WriteRegisterAsync(
slot,
registerName,
value,
storeToDevice,
refreshSystemState,
token)
.ConfigureAwait(false);
//RaiseMeterBatchStatusChanged();
return result;
}
#endregion
#region ================================== Password ==================================
/// <summary>
/// Updates meter password for the specified slot.
/// </summary>
/// <param name="slot">Slot id. Must be greater than zero.</param>
/// <param name="password">New password.</param>
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
/// <returns>
/// Password update result.
/// </returns>
/// <exception cref="ArgumentException">
/// Thrown when slot id or password is invalid.
/// </exception>
public async Task<GciSetPasswordResult> SetMeterPasswordAsync(
int slot,
string password,
CancellationToken token = default)
{
if (slot <= 0)
throw new ArgumentException("Invalid slot id.", nameof(slot));
if (string.IsNullOrWhiteSpace(password))
throw new ArgumentException("Password is empty.", nameof(password));
var result = await _innerMeterAPI
.SetMeterPasswordAsync(slot, password, token)
.ConfigureAwait(false);
//RaiseMeterBatchStatusChanged();
return result;
}
#endregion
#region ================================== DEBUG STATUS ==================================
/// <summary>
/// Gets runtime diagnostic information for all active workers.
/// </summary>
/// <returns>
/// Collection containing worker state, queue information,
/// current operation and activity timestamps.
/// </returns>
public List<WorkerDebugStatus> GetWorkerDebugStatuses()
{
return _innerMeterAPI?.GetWorkerDebugStatuses();
}
/// <summary>
/// Gets runtime diagnostic information for all meter slots.
/// </summary>
/// <returns>
/// Collection containing slot state, connection state,
/// selected state and communication configuration.
/// </returns>
public List<MeterBatchDebugStatus> GetMeterBatchDebugStatuses()
{
return _innerMeterAPI?.GetMeterBatchDebugStatuses();
}
/// <summary>
/// Raises meter batch status change notification.
/// </summary>
/// <remarks>
/// Intended to notify external consumers after changes in meter state.
/// </remarks>
public void RaiseMeterBatchStatusChanged()
{
var statuses = GetMeterBatchDebugStatuses();
var handler = MeterBatchStatusChanged;
if (handler != null)
handler(statuses);
}
#endregion
#region ================================== SLOT SELECTION ==================================
/// <summary>
/// Sets selection state for a slot.
/// </summary>
/// <param name="slot">Slot id.</param>
/// <param name="selected">Selection state.</param>
public void SetSlotSelected(int slot, bool selected)
{
_innerMeterAPI?.SetSlotSelected(slot, selected);
RaiseMeterBatchStatusChanged();
}
/// <summary>
/// Determines whether the specified slot is selected.
/// </summary>
/// <param name="slot">Slot id.</param>
/// <returns>
/// True if slot is selected; otherwise false.
/// </returns>
public bool IsSlotSelected(int slot)
{
return (bool)(_innerMeterAPI?.IsSlotSelected(slot));
}
/// <summary>
/// Gets all selected slot identifiers.
/// </summary>
/// <returns>
/// Ordered collection of selected slot ids.
/// </returns>
public List<int> GetSelectedSlots()
{
return _innerMeterAPI?.GetSelectedSlots();
}
#endregion
#region ================================== Register names ==================================
/// <summary>
/// Gets all available firmware register identifiers.
/// </summary>
/// <returns>
/// Ordered collection of register names.
/// </returns>
public List<string> GetAllRegisterNames()
{
return _innerMeterAPI?.GetAllRegisterNames();
}
#endregion
// Laatzen Preadjustment processes
#region ================================== PreAdjustment ==================================
public PreAdjustmentInitializationResult Preadjustment_Initialization(
ProcessProgress pp,
List<MeterStateControl> mc)
{
//init handlers
pp.OnRequestedCalibrationParamsFromDb -= RequestedCalibrationParamsFromDb;
pp.OnRequestedCalibrationParamsFromDb += RequestedCalibrationParamsFromDb;
return _innerMeterAPI?.Preadjustment_Initialization(pp, mc);
}
public PreAdjustmentInitializationResult Preadjustment_Initialization(
ProcessProgress pp,
List<MeterStateControl> mc,
CancellationToken token)
{
pp.OnRequestedCalibrationParamsFromDb -= RequestedCalibrationParamsFromDb;
pp.OnRequestedCalibrationParamsFromDb += RequestedCalibrationParamsFromDb;
return _innerMeterAPI?.Preadjustment_Initialization(pp, mc, token);
}
public void CancelPreadjustment()
{
_innerMeterAPI?.CancelPreadjustment();
}
public Task<PreadjustmentDetectResult> PreAdjustment_DetectAsync(
IEnumerable<PublicModels.MeterBatchDebugStatus> selectedSlots,
CancellationToken token = default)
{
return _innerMeterAPI.PreAdjustment_DetectAsync(selectedSlots, token);
}
public PreadjustmentDetectResult PreAdjustment_DetectDirect(
IEnumerable<PublicModels.MeterBatchDebugStatus> selectedSlots,
CancellationToken token = default)
{
return _innerMeterAPI.PreAdjustment_DetectDirect(selectedSlots, token);
}
public Task<PreAdjustmentProcessResult> PreAdjustment_PreparationAsync(
int slot,
CancellationToken token = default)
{
return _innerMeterAPI.PreAdjustment_PreparationAsync(slot, token);
}
public PreAdjustmentProcessResult PreAdjustment_PreparationDirect()
{
return _innerMeterAPI.PreAdjustment_PreparationDirect();
}
public PreAdjustmentProcessResult PreAdjustment_PreparationDirect(CancellationToken token)
{
return _innerMeterAPI.PreAdjustment_PreparationDirect(token);
}
public Task<PreAdjustmentProcessResult> PreAdjustment_AmplitudeTestAsync(
int slot,
CancellationToken token = default)
{
return _innerMeterAPI.PreAdjustment_AmplitudeTestAsync(slot, token);
}
public PreAdjustmentProcessResult PreAdjustment_AmplitudeTestDirect()
{
return _innerMeterAPI.PreAdjustment_AmplitudeTestDirect();
}
public PreAdjustmentProcessResult PreAdjustment_AmplitudeTestDirect(CancellationToken token)
{
return _innerMeterAPI.PreAdjustment_AmplitudeTestDirect(token);
}
public Task<PreAdjustmentProcessResult> PreAdjustment_TemperatureCalibrationAsync(
int slot,
CancellationToken token = default)
{
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationAsync(slot, token);
}
public PreAdjustmentProcessResult PreAdjustment_TemperatureCalibrationDirect()
{
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationDirect();
}
public PreAdjustmentProcessResult PreAdjustment_TemperatureCalibrationDirect(CancellationToken token)
{
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationDirect(token);
}
public PreAdjustmentProcessResult PreAdjustment_TemperatureCalibrationDirect(
CalibrationTemperatureInput temperature, CancellationToken token)
{
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationDirect(temperature, token);
}
public Task<PreAdjustmentProcessResult> PreAdjustment_TemperatureCalibrationAsync(
int slot, CalibrationTemperatureInput temperature, CancellationToken token)
{
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationAsync(slot, temperature, token);
}
public TemperatureCalibrationResult PreAdjustment_TemperatureCalibrationDetailedDirect(
CalibrationTemperatureInput temperature, CancellationToken token)
{
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationDetailedDirect(temperature, token);
}
public Task<TemperatureCalibrationResult> PreAdjustment_TemperatureCalibrationDetailedAsync(
int slot, CalibrationTemperatureInput temperature, CancellationToken token)
{
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationDetailedAsync(slot, temperature, token);
}
public bool PreAdjustment_PushTemperature(
double temperature)
{
return _innerMeterAPI.PreAdjustment_PushTemperature(temperature);
}
public bool PreAdjustment_PushTemperature(
double temperature,
CancellationToken token)
{
token.ThrowIfCancellationRequested();
return _innerMeterAPI.PreAdjustment_PushTemperature(temperature);
}
public Task<PreAdjustmentProcessResult> PreAdjustment_OffsetTestAsync(
int slot,
CancellationToken token = default)
{
return _innerMeterAPI.PreAdjustment_OffsetTestAsync(slot, token);
}
public PreAdjustmentProcessResult PreAdjustment_OffsetTestDirect()
{
return _innerMeterAPI.PreAdjustment_OffsetTestDirect();
}
public PreAdjustmentProcessResult PreAdjustment_OffsetTestDirect(CancellationToken token)
{
return _innerMeterAPI.PreAdjustment_OffsetTestDirect(token);
}
public Task<PreAdjustmentProcessResult> PreAdjustment_CompletionAsync(
int slot,
CancellationToken token = default)
{
return _innerMeterAPI.PreAdjustment_CompletionAsync(slot, token);
}
public PreAdjustmentProcessResult PreAdjustment_CompletionDirect()
{
return _innerMeterAPI.PreAdjustment_CompletionDirect();
}
public PreAdjustmentProcessResult PreAdjustment_CompletionDirect(CancellationToken token)
{
return _innerMeterAPI.PreAdjustment_CompletionDirect(token);
}
#endregion
#region ================================== UNI DATA STORAGE READER ==================================
//LoginPasswords reading
/// <summary>
/// Reads meter login password from configured GCI data storage.
/// </summary>
/// <param name="query">
/// Data query containing PCB ID or another configured lookup value.
/// </param>
/// <param name="token">
/// Cancellation token used to cancel the asynchronous operation.
/// </param>
/// <returns>
/// Password if found; otherwise null.
/// </returns>
public Task<string> ReadMeterLoginPasswordAsync(
DataQuery query,
CancellationToken token = default)
{
return loginPasswordsReader.ReadMeterLoginPasswordAsync(
query,
token);
}
//CalibrationParams reading
/// <summary>
/// Reads pre-adjustment calibration parameters
/// from configured GCI data storage.
/// </summary>
/// <param name="query">
/// Data query containing meter size or another configured lookup value.
/// </param>
/// <param name="token">
/// Cancellation token used to cancel the asynchronous operation.
/// </param>
/// <returns>
/// Dictionary:
///
/// Key:
/// Calibration parameter name
///
/// Value:
/// Calibration parameter value
/// </returns>
public Task<Dictionary<string, UInt32>> ReadPreAdjustmentCalibrationParamsAsync(
DataQuery query,
CancellationToken token = default)
{
return calibrationParamsReader.ReadCalibrationParamsAsync(
query,
token);
}
#endregion
}
}
@@ -0,0 +1,170 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xylem.Common.Hardware.Interfaces.Ports.PortCore;
using static GenesisCordonelInterface.API.InterfaceOutsideToGCI;
using static GenesisCordonelInterface.API.PublicModels;
using static Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.GenesisMeter;
namespace GenesisCordonelInterface.API
{
/// <summary>
/// Public data contract layer for the Genesis Cordonel Interface (GCI).
///
/// This class defines all Data Transfer Objects (DTOs) that are exposed
/// to external consumers (e.g. TBF, UI, or other integration layers).
///
/// Responsibilities:
/// - Provide stable, dependency-free models for external usage
/// - Decouple internal GCI implementation (GenesisMeter, Xylem libraries)
/// from external systems
/// - Define request/response contracts for all supported operations
/// - Contain mapping methods between public DTOs and internal domain models
///
/// Architecture:
/// External world (TBF / UI)
/// ↓
/// GciPublicModels (this layer)
/// ↓
/// Internal GCI API (InterfaceGCIToLaatzen, GenesisMeter, etc.)
///
/// Notes:
/// - Public models must NOT expose internal types (e.g. GenesisMeter, IPort, etc.)
/// - All mapping between internal and external representations must be done here
/// - DTOs are designed to be simple, serializable, and stable over time
/// - Any change in internal implementation should not affect these models
///
/// Pattern:
/// Each operation follows a consistent structure:
/// Request → Operation → Result
///
/// Example:
/// GciInitSlotRequest → InitSlot → GciInitSlotResult
/// GetSlot → GciSlotInfo
/// GetPcbId → GciGetPcbIdResult
///
/// This layer acts as a boundary between domain logic and integration logic.
/// </summary>
public class ModelsMapping
{
/// <summary>
/// Mapping methods between public DTOs and internal GCI models.
/// Ensures separation between external contracts and internal domain objects.
/// </summary>
#region ================================== OUTERN/INTERN and back models mapping ==================================
public static PasswordSource MapPasswordSource(PublicModels.GciPasswordSource src)
{
return (PasswordSource)src;
}
public static ConfigSource MapConfigSource(PublicModels.GciConfigSource src)
{
return (ConfigSource)src;
}
public static Xylem.Common.Hardware.Interfaces.Ports.PortCore.PortConfig? MapPort(PublicModels.GciPortConfig port)
{
if (port == null)
return null;
Xylem.Common.Hardware.Interfaces.Ports.PortCore.PortConfig result = new Xylem.Common.Hardware.Interfaces.Ports.PortCore.PortConfig();
result.PortName = port.PortName;
result.Type = port.Type;
return result;
}
public static PublicModels.GciInitSlotResult MapInitResult(PublicModels.GciInitSlotResult result)
{
if (result == null)
return null;
return new PublicModels.GciInitSlotResult
{
SlotId = result.SlotId,
Success = result.Success,
Message = result.Message
};
}
public static PublicModels.GciPasswordSource MapPasswordSourceBack(PasswordSource src)
{
return (PublicModels.GciPasswordSource)src;
}
public static PublicModels.GciConfigSource MapConfigSourceBack(ConfigSource src)
{
return (PublicModels.GciConfigSource)src;
}
public static PublicModels.GciPortConfig MapPortBack(PortConfig? port)
{
if (!port.HasValue)
return null;
PortConfig value = port.Value;
return new PublicModels.GciPortConfig
{
PortName = value.PortName,
Type = value.Type
};
}
public static PublicModels.GciPortConfig MapPortBack(IPort port)
{
if (port == null)
return null;
return new PublicModels.GciPortConfig
{
PortName = port.GetPortName(),
Type = port.GetType().Name
};
}
public static PublicModels.GciPortConfig MapPortBack(string portName, string portTyoe)
{
return new PublicModels.GciPortConfig
{
PortName = portName,
Type = portTyoe
};
}
public static MeterBatchDebugStatus ToMeterBatchDebugStatus(GciSlotInfo slot)
{
if (slot == null)
return null;
return new MeterBatchDebugStatus
{
Slot = slot.SlotId,
Selected = true,
PcbId = slot.PcbId,
IsConnected = false,
IsLoggedOn = false,
RequestPort = slot.RequestPort == null ? "" : slot.RequestPort.PortName,
StreamingPort = slot.StreamingPort == null ? "" : slot.StreamingPort.PortName,
FwVersion = "",
InterfaceVersion = ""
};
}
public static List<MeterBatchDebugStatus> ToMeterBatchDebugStatusList(
IEnumerable<GciSlotInfo> slots)
{
if (slots == null)
return new List<MeterBatchDebugStatus>();
return slots
.Where(x => x != null)
.Select(ToMeterBatchDebugStatus)
.ToList();
}
#endregion
}
}
@@ -0,0 +1,636 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Xylem.Common.Hardware.Interfaces.Ports.PortCore;
using static GenesisCordonelInterface.API.InterfaceOutsideToGCI;
using static Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.GenesisMeter;
namespace GenesisCordonelInterface.API
{
/// <summary>
/// Public data contract layer for the Genesis Cordonel Interface (GCI).
///
/// This class defines all Data Transfer Objects (DTOs) that are exposed
/// to external consumers (e.g. TBF, UI, or other integration layers).
///
/// Responsibilities:
/// - Provide stable, dependency-free models for external usage
/// - Decouple internal GCI implementation (GenesisMeter, Xylem libraries)
/// from external systems
/// - Define request/response contracts for all supported operations
/// - Contain mapping methods between public DTOs and internal domain models
///
/// Architecture:
/// External world (TBF / UI)
/// ↓
/// GciPublicModels (this layer)
/// ↓
/// Internal GCI API (InterfaceGCIToLaatzen, GenesisMeter, etc.)
///
/// Notes:
/// - Public models must NOT expose internal types (e.g. GenesisMeter, IPort, etc.)
/// - All mapping between internal and external representations must be done here
/// - DTOs are designed to be simple, serializable, and stable over time
/// - Any change in internal implementation should not affect these models
///
/// Pattern:
/// Each operation follows a consistent structure:
/// Request → Operation → Result
///
/// Example:
/// GciInitSlotRequest → InitSlot → GciInitSlotResult
/// GetSlot → GciSlotInfo
/// GetPcbId → GciGetPcbIdResult
///
/// This layer acts as a boundary between domain logic and integration logic.
/// </summary>
public class PublicModels
{
/// <summary>Persisted values for one meter and one preadjustment attempt.</summary>
public sealed class PreadjustmentCompletionRecord
{
// Diagnostic routing only. Not a database column or SQL parameter.
public int? Slot { get; set; }
public int PcbId { get; set; }
public int? MeterSize { get; set; }
public int? ZeroOffset1 { get; set; }
public int? ZeroOffset2 { get; set; }
public int? ZeroOffset3 { get; set; }
public int? FirstHitUpdatePeriod { get; set; }
public int? FirstHitShift { get; set; }
public int? FirstHitPercent1 { get; set; }
public int? FirstHitPercent2 { get; set; }
public int? FirstHitPercent3 { get; set; }
public int? ToFTempOffset1 { get; set; }
public int? ToFTempOffset2 { get; set; }
public int? ToFTempOffset3 { get; set; }
// Timestamp of the attempt, not the time of a possible later write retry.
public DateTime Date { get; set; }
public bool Successful { get; set; }
}
/// <summary>Q3 values calculated and confirmed by the caller; no port access.</summary>
public sealed class Q3CalibrationRecord
{
public int PcbId { get; set; }
// Preserve the existing TBF meaning/unit of each offset (do not rescale here).
public double? P1Offset { get; set; }
public int? P1Cal { get; set; }
public double? P2Offset { get; set; }
public int? P2Cal { get; set; }
public double? P3Offset { get; set; }
public int? P3Cal { get; set; }
public DateTime Date { get; set; }
}
/// <summary>Completion outcome plus detached SQL inputs and separate write outcomes.</summary>
public sealed class CompletionProcessResult : PreAdjustmentProcessResult
{
public List<PreadjustmentCompletionRecord> DatabaseRecords { get; set; } =
new List<PreadjustmentCompletionRecord>();
public List<CompletionDatabaseWriteResult> DatabaseWrites { get; set; } =
new List<CompletionDatabaseWriteResult>();
public override string ToString()
{
return base.ToString() + ", SQL=[" + string.Join("; ",
DatabaseWrites.Select(w => "Slot=" + w.Slot + ", " + w.Result.Status)) + "]";
}
}
public sealed class CompletionDatabaseWriteResult
{
public int Slot { get; set; }
public CalibrationWriteResult Result { get; set; }
}
public enum CalibrationWriteStatus
{
Failed = 0, Stored = 1, Disabled = 2, Simulated = 3,
// The connection failed during commit; verify the DB before retrying.
OutcomeUnknown = 4
}
/// <summary>Database outcome, independent of the physical calibration outcome.</summary>
public sealed class CalibrationWriteResult
{
public CalibrationWriteStatus Status { get; set; }
public bool Success { get { return Status == CalibrationWriteStatus.Stored; } }
public int PcbId { get; set; }
public long? RecordId { get; set; }
public string ErrorMessage { get; set; }
public override string ToString()
{
return string.Format("Status={0}, PcbId={1}, RecordId={2}, Error={3}",
Status, PcbId, RecordId, ErrorMessage ?? "None");
}
}
public static bool HideSensitiveValues { get; set; } = true;
private static string FormatPassword(string password)
{
if (!HideSensitiveValues)
return password ?? "<null>";
if (string.IsNullOrEmpty(password))
return "<empty>";
return "********";
}
/// <summary>
/// Public DTOs exposed to external systems.
/// These models represent the contract of the GCI API.
/// They must remain stable and independent of internal implementation.
/// </summary>
#region ================================== PUBLIC MODELS ===========================================
public class RegisterReadResult
{
public bool Success { get; set; }
public string RegisterName { get; set; }
public string RawHex { get; set; }
public string ErrorMessage { get; set; }
public override string ToString()
{
return string.Format(
"Success={0}, RegisterName={1}, RawHex={2}, ErrorMessage={3}",
Success,
RegisterName,
RawHex,
ErrorMessage);
}
}
public class WorkerDebugStatus
{
public int Slot { get; set; }
public string Name { get; set; }
public int QueueLength { get; set; }
public bool IsBusy { get; set; }
public string CurrentOperation { get; set; }
public string LastError { get; set; }
public DateTime LastActivity { get; set; }
}
public class MeterBatchDebugStatus
{
public int Slot { get; set; }
public bool Selected { get; set; }
public string PcbId { get; set; }
public bool IsConnected { get; set; }
public bool IsLoggedOn { get; set; }
public string RequestPort { get; set; }
public string StreamingPort { get; set; }
public string RequestPortType { get; set; }
public string FwVersion { get; set; }
public string InterfaceVersion { get; set; }
}
public class RegisterWriteResult
{
public bool Success { get; set; }
public string RegisterName { get; set; }
public object WrittenValue { get; set; }
public bool StoreToDevice { get; set; }
public bool RefreshSystemState { get; set; }
public string ErrorMessage { get; set; }
public override string ToString()
{
return $"Success={Success}, " +
$"Register={RegisterName ?? "<null>"}, " +
$"Value={WrittenValue ?? "<null>"}, " +
$"StoreToDevice={StoreToDevice}, " +
$"RefreshSystemState={RefreshSystemState}, " +
$"Error={ErrorMessage ?? "<none>"}";
}
}
public class PortDetectionResult
{
public bool Success { get; set; }
public int Slot { get; set; }
public string PortName { get; set; }
public string PcbId { get; set; }
public string ErrorMessage { get; set; }
public override string ToString()
{
return string.Format(
"Slot={0}, Success={1}, PortName={2}, PcbId={3}, ErrorMessage={4}",
Slot,
Success,
PortName,
PcbId,
ErrorMessage);
}
}
public class GciSlotInfo
{
public int SlotId { get; set; }
public bool Exists { get; set; }
public bool Success { get; set; }
public bool IsConnected { get; set; }
public bool IsLoggedOn { get; set; }
public string Message { get; set; }
public string PcbId { get; set; }
public string Password { get; set; }
public GciConfigSource ConfigSource { get; set; }
public GciPasswordSource PasswordSource { get; set; }
public GciPortConfig RequestPort { get; set; }
public GciPortConfig StreamingPort { get; set; }
public override string ToString()
{
return string.Format(
"SlotId={0}, Exists={1}, Success={2}, IsConnected={3}, IsLoggedOn={4}, Message={5}, PcbId={6}, Password={7}, ConfigSource={8}, PasswordSource={9}, RequestPort={10}, StreamingPort={11}",
SlotId,
Exists,
Success,
IsConnected,
IsLoggedOn,
Message,
PcbId,
FormatPassword(Password),
ConfigSource,
PasswordSource,
RequestPort,
StreamingPort);
}
}
public class GciAllSlotsInfo
{
public bool Success { get; set; }
public string Message { get; set; }
public List<GciSlotInfo> Slots { get; set; }
public override string ToString()
{
return $"Success={Success}, Count={Slots?.Count ?? 0}, Message={Message}";
}
}
public class Result
{
public int SlotId { get; set; }
public bool Success { get; set; }
public string Message { get; set; }
public override string ToString()
{
return string.Format(
"SlotId={0}, Success={1}, Message={2}",
SlotId,
Success,
Message);
}
}
public enum GciPasswordSource
{
RestApi = 0,
OfflineFile = 1,
InterfaceInputPassword = 2,
}
public enum GciConfigSource
{
FileConfig = 0,
InterfaceInputConfig = 1,
}
/// <summary>
/// Collection of port settings
/// </summary>
public class GciPortConfig
{
public string PortName { get; set; }
public string Type { get; set; }
public override string ToString()
{
return string.Format("PortName={0}, Type={1}", PortName, Type);
}
}
public static GciPortConfig CreatePortConfig(string portName, string type)
{
if (string.IsNullOrWhiteSpace(portName))
return null;
return new GciPortConfig
{
PortName = portName,
Type = MapPortType(type)
};
}
public static string MapPortType(string type)
{
switch ((type ?? "").Trim().ToUpperInvariant())
{
case "RFID":
return "Xylem.Common.Hardware.Interfaces.Ports.SerialPorts.RfidSerialPort";
case "UART":
return "Xylem.Common.Hardware.Interfaces.Ports.SerialPorts.UartSerialPort";
case "IRDA":
return "Xylem.Common.Hardware.Interfaces.Ports.SerialPorts.IrdaSerialPort";
default:
return "Xylem.Common.Hardware.Interfaces.Ports.SerialPorts.IrdaSerialPort";
}
}
public class GciInitSlotRequest
{
public int SlotId { get; set; }
public GciConfigSource ConfigSource { get; set; }
public GciPasswordSource PasswordSource { get; set; }
public GciPortConfig RequestPort { get; set; }
public GciPortConfig StreamingPort { get; set; }
public string Password { get; set; }
public override string ToString()
{
return string.Format(
"SlotId={0}, GciConfigSource={1}, PasswordSource={2}, RequestPort={3}, StreamingPort={4}, Password={5}",
SlotId,
ConfigSource,
PasswordSource,
RequestPort,
StreamingPort,
FormatPassword(Password));
}
}
public class GciInitSlotResult
{
public int SlotId { get; set; }
public bool Success { get; set; }
public string Message { get; set; }
public string PcbId { get; set; }
public bool Created { get; set; }
public bool Updated { get; set; }
public bool AlreadyExists { get; set; }
public override string ToString()
{
return string.Format(
"SlotId={0}, Success={1}, Message={2}, PcbId={3}, Created={4}, Updated={5}, AlreadyExists={6}",
SlotId,
Success,
Message,
PcbId,
Created,
Updated,
AlreadyExists);
}
}
public class GciCleanAllSlotsResult
{
public bool Success { get; set; }
public string Message { get; set; }
public override string ToString()
{
return string.Format(
"Success={0}, Message={1}",
Success,
Message);
}
}
public class GciCleanSlotResult
{
public int SlotId { get; set; }
public bool Success { get; set; }
public string Message { get; set; }
public override string ToString()
{
return string.Format(
"SlotId={0}, Success={1}, Message={2}",
SlotId,
Success,
Message);
}
}
public class GciGetPcbIdResult
{
public int SlotId { get; set; }
public bool Success { get; set; }
public string PcbId { get; set; }
public string Message { get; set; }
public override string ToString()
{
return string.Format(
"SlotId={0}, Success={1}, PcbId={2}, Message={3}",
SlotId,
Success,
PcbId,
Message);
}
}
public class GciConnectResult
{
public int SlotId { get; set; }
public bool Success { get; set; }
public string PcbId { get; set; }
public bool IsConnected { get; set; }
public string Message { get; set; }
public override string ToString()
{
return string.Format(
"SlotId={0}, Success={1}, PcbId={2}, IsConnected={3}, Message={4}",
SlotId,
Success,
PcbId,
IsConnected,
Message);
}
}
public class GciLoginResult
{
public int SlotId { get; set; }
public bool Success { get; set; }
public string PcbId { get; set; }
public bool IsLoggedOn { get; set; }
public string Message { get; set; }
public override string ToString()
{
return $"Slot={SlotId}, Success={Success}, PcbId={PcbId}, IsLoggedOn={IsLoggedOn}, Message={Message ?? "<none>"}";
}
}
public class GciRegisterSnapshot
{
public string Name { get; set; }
public string Type { get; set; }
public string RawValue { get; set; }
public string Min { get; set; }
public string Max { get; set; }
public string Description { get; set; }
public string Version { get; set; }
public string IsAvailable { get; set; }
public string Privilege { get; set; }
}
public class GciDisconnectResult
{
public int SlotId { get; set; }
public bool Success { get; set; }
public string Message { get; set; }
public override string ToString()
{
return string.Format(
"SlotId={0}, Success={1}, Message={2}",
SlotId,
Success,
Message);
}
}
public class GciSetPasswordResult
{
public int SlotId { get; set; }
public bool Success { get; set; }
public string Password { get; set; }
public string Message { get; set; }
public override string ToString()
{
return string.Format(
"SlotId={0}, Success={1}, Password={2}, Message={3}",
SlotId,
Success,
FormatPassword(Password),
Message);
}
}
public class PreAdjustmentProcessResult
{
public bool Success { get; set; }
public int Slot { get; set; }
public string ProcessName { get; set; }
public string ErrorMessage { get; set; }
/// <inheritdoc />
public override string ToString()
{
return
$"Success={Success}, " +
$"Slot={Slot}, " +
$"Process={ProcessName}, " +
$"Error={ErrorMessage ?? "None"}";
}
}
public class TemperatureCalibrationSlotResult : PreAdjustmentProcessResult
{
public decimal? ReferenceTemperatureCelsius { get; set; }
public decimal? MeterTemperatureCelsius { get; set; }
public decimal? AbsoluteDifferenceCelsius { get; set; }
// The effective existing process limit, reported for diagnostics only.
public decimal? DeviationLimitCelsius { get; set; }
public int SampleCount { get; set; }
public bool? TemperatureCheckPassed { get; set; }
public override string ToString()
{
var culture = System.Globalization.CultureInfo.InvariantCulture;
return base.ToString() + string.Format(culture,
", Reference={0} C, Meter={1} C, Difference={2} C, Limit={3} C, Samples={4}, Check={5}",
ReferenceTemperatureCelsius?.ToString(culture) ?? "N/A",
MeterTemperatureCelsius?.ToString(culture) ?? "N/A",
AbsoluteDifferenceCelsius?.ToString(culture) ?? "N/A",
DeviationLimitCelsius?.ToString(culture) ?? "N/A", SampleCount,
TemperatureCheckPassed.HasValue ? (TemperatureCheckPassed.Value ? "PASSED" : "FAILED") : "NOT_EVALUATED");
}
}
public class TemperatureCalibrationResult : PreAdjustmentProcessResult
{
public List<TemperatureCalibrationSlotResult> Slots { get; set; } =
new List<TemperatureCalibrationSlotResult>();
public bool IsSimulated { get; set; }
public override string ToString()
{
return base.ToString() + ", Simulated=" + IsSimulated +
", TemperatureResults=[" + string.Join("; ", Slots.Select(s => s.ToString())) + "]";
}
// Preserve status for failures before process creation and for simulation.
// Missing measurements must never be represented as a measured zero.
public static TemperatureCalibrationResult FromProcessResult(
PreAdjustmentProcessResult result, bool simulated = false)
{
if (result == null) throw new ArgumentNullException(nameof(result));
var detailed = result as TemperatureCalibrationResult;
if (detailed != null) return detailed;
return new TemperatureCalibrationResult
{
Success = result.Success, Slot = result.Slot, ProcessName = result.ProcessName,
ErrorMessage = result.ErrorMessage, IsSimulated = simulated
};
}
}
public class PreadjustmentDetectResult
{
public bool Success { get; set; }
public int DetectedMeterCount { get; set; }
public int DetectedThermometerCount { get; set; }
public string ErrorMessage { get; set; }
/// <inheritdoc />
public override string ToString()
{
return
$"Success={Success}, " +
$"Meters={DetectedMeterCount}, " +
$"Thermometers={DetectedThermometerCount}, " +
$"Error={ErrorMessage ?? "None"}";
}
}
public class PreAdjustmentInitializationResult
{
public bool Success { get; set; }
public string Slots { get; set; }
public string ErrorMessage { get; set; }
/// <inheritdoc />
public override string ToString()
{
return
$"Success={Success}, " +
$"Slots={Slots}, " +
$"Error={ErrorMessage ?? "None"}";
}
}
#endregion
}
}
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="NLog" publicKeyToken="5120e14c03d0593c" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Xylem.Common.CommonCore" publicKeyToken="null" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.8.18.17954" newVersion="2.8.18.17954" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
@@ -0,0 +1,36 @@
{
"_Comment": "GCI DataStorage configuration",
"DataStorage": {
"MeterLoginPasswords": {
"___Documentation___": {
"Description": "Reads login passwords for meters by PCB ID",
"Type": "Supported: LocalDatabase, RemoteDatabase, LocalCsv, LocalJson, RestApi",
"DataSource": "Database connection string",
"QueryTemplate": "QUERYPARAM is placeholder for runtime value. Example: WHERE [PcbId]=QUERYPARAM -> PCB ID provided during GetPasswordAsync()."
},
"Name": "MeterLoginPasswords",
"Type": "LocalDatabase",
"DataSource": "Server=(localdb)\\MojaDB;Database=UnionTownCalibAndSkeleton;Integrated Security=True;",
"QueryTemplate": "SELECT [Password] FROM [dbo].[SkeletonKeys] WHERE [PcbId] = QUERYPARAM"
},
"PreAdjustmentCalibrationParams": {
"___Documentation___": {
"Description": "Reads pre-adjustment calibration parameters by meter size",
"Type": "Supported: LocalDatabase, RemoteDatabase, LocalCsv, LocalJson, RestApi",
"DataSource": "Database connection string",
"QueryTemplate": "QUERYPARAM is placeholder for runtime value. Example: WHERE [MeterSize]=QUERYPARAM -> meter size provided during ReadCalibrationParamsAsync()."
},
"Name": "PreAdjustmentCalibrationParams",
"Type": "LocalDatabase",
"DataSource": "Server=(localdb)\\MojaDB;Database=UnionTownCalibAndSkeleton;Integrated Security=True;",
"QueryTemplate": "SELECT [ParameterName], [ParameterValue] FROM [dbo].[PreAdjustmentCalibrationParams] WHERE [MeterSize] = QUERYPARAM"
}
}
}
@@ -0,0 +1,32 @@
namespace GenesisCordonelInterface.Core.Config
{
/// <summary>
/// Root GCI configuration object loaded from gci_config.json.
///
/// This class represents the top-level configuration structure
/// and serves as an entry point for all configurable GCI features.
///
/// Example:
///
/// {
/// "DataStorageSection":
/// {
/// ...
/// }
/// }
/// </summary>
public class GciConfig
{
/// <summary>
/// Data storage configuration section.
///
/// Contains definitions for all configured readers and writers,
/// such as:
///
/// - MeterLoginPasswords
/// - PreAdjustmentCalibrationParams
/// - future storage providers
/// </summary>
public DataStorage.Config.GciDataStorageConfig DataStorageSection { get; set; }
}
}
@@ -0,0 +1,177 @@
using System;
using System.IO;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
using Newtonsoft.Json;
namespace GenesisCordonelInterface.Core.Config
{
/// <summary>
/// Loads GCI configuration from gci_config.json.
///
/// Responsibilities:
/// - Locate configuration file
/// - Deserialize JSON into strongly typed objects
/// - Normalize relative file paths
/// - Preserve database connection strings and REST URLs
///
/// Example:
///
/// Config/
/// gci_config.json
///
/// Data/
/// meter_passwords.csv
///
/// Relative paths:
/// Data\file.csv
///
/// become:
///
/// C:\App\bin\Debug\Data\file.csv
///
/// Database sources remain unchanged:
///
/// Server=(localdb)\MojaDB;Database=...
/// </summary>
public static class GciConfigLoader
{
/// <summary>
/// Loads default GCI configuration from:
///
/// Config\gci_config.json
/// </summary>
/// <returns>
/// Loaded GCI configuration.
/// </returns>
public static GciConfig LoadDefault()
{
string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
string configPath = Path.Combine(
baseDirectory,
"Config",
"gci_config.json");
return Load(configPath, baseDirectory);
}
/// <summary>
/// Loads GCI configuration from specified file.
/// </summary>
/// <param name="configPath">
/// Path to configuration json file.
/// </param>
/// <param name="baseDirectory">
/// Base directory used for resolving relative paths.
/// </param>
/// <returns>
/// Loaded configuration object.
/// </returns>
public static GciConfig Load(
string configPath,
string baseDirectory = null)
{
if (string.IsNullOrWhiteSpace(configPath))
throw new ArgumentException(
"Config path must not be empty.",
nameof(configPath));
if (!File.Exists(configPath))
throw new FileNotFoundException(
"GCI config file was not found.",
configPath);
string json = File.ReadAllText(configPath);
GciConfig config =
JsonConvert.DeserializeObject<GciConfig>(json);
if (config == null)
throw new InvalidOperationException(
"GCI config could not be loaded.");
NormalizeDataStoragePaths(
config,
baseDirectory ?? Path.GetDirectoryName(configPath));
return config;
}
/// <summary>
/// Normalizes paths for all configured data storage entries.
///
/// Converts relative file paths into absolute paths.
/// Database connection strings remain untouched.
/// </summary>
/// <param name="config">
/// Loaded configuration object.
/// </param>
/// <param name="baseDirectory">
/// Base path used for relative resolution.
/// </param>
private static void NormalizeDataStoragePaths(
GciConfig config,
string baseDirectory)
{
if (config.DataStorageSection == null)
return;
NormalizePath(
config.DataStorageSection.MeterLoginPasswords,
baseDirectory);
NormalizePath(
config.DataStorageSection.PreAdjustmentCalibrationParams,
baseDirectory);
}
/// <summary>
/// Converts relative file paths into absolute paths.
///
/// Applies only to:
/// - LocalCsv
/// - RemoteCsv
/// - LocalJson
/// - RemoteJson
///
/// Does not modify:
/// - Database connection strings
/// - REST URLs
/// </summary>
/// <param name="storageConfig">
/// Storage configuration.
/// </param>
/// <param name="baseDirectory">
/// Base path for resolution.
/// </param>
private static void NormalizePath(
DataStorageConfig storageConfig,
string baseDirectory)
{
if (storageConfig == null)
return;
if (string.IsNullOrWhiteSpace(storageConfig.DataSource))
return;
// Database connection strings and URLs
// must never be treated as file paths.
if (storageConfig.Type == DataStorageType.LocalDatabase ||
storageConfig.Type == DataStorageType.RemoteDatabase ||
storageConfig.Type == DataStorageType.RestApi)
{
return;
}
if (Path.IsPathRooted(storageConfig.DataSource))
return;
storageConfig.DataSource =
Path.GetFullPath(
Path.Combine(
baseDirectory,
storageConfig.DataSource));
}
}
}
@@ -0,0 +1,78 @@
{
"_Comment": "GCI DataStorage configuration",
"DataStorageSection": {
"MeterLoginPasswords": {
"___Documentation___": {
"Description": "Reads login passwords for meters by PCB ID",
"Type": "Supported: LocalDatabase, RemoteDatabase, LocalCsv, LocalJson, RestApi",
"DataSource": "Database connection string",
"QueryTemplate": "QUERYPARAM is placeholder for runtime value. Example: WHERE [PcbId]=QUERYPARAM -> PCB ID provided during GetPasswordAsync()."
},
"Name": "MeterLoginPasswords",
"Type": "LocalDatabase",
"DataSource": "Server=(localdb)\\SensusLocalDB;Database=UnionTownCalibAndSkeleton;Integrated Security=True;",
"QueryTemplate": "SELECT [Password] FROM [dbo].[SkeletonKeys] WHERE [PcbId] = QUERYPARAM"
},
"PreAdjustmentCalibrationParams": {
"___Documentation___": {
"Description": "Reads pre-adjustment calibration parameters by meter size",
"Type": "Supported: LocalDatabase, RemoteDatabase, LocalCsv, LocalJson, RestApi",
"DataSource": "Database connection string",
"QueryTemplate": "QUERYPARAM is placeholder for runtime value. Example: WHERE [MeterSize]=QUERYPARAM -> meter size provided during ReadCalibrationParamsAsync()."
},
"Name": "PreAdjustmentCalibrationParams",
"Type": "LocalDatabase",
"DataSource": "Server=(localdb)\\SensusLocalDB;Database=UnionTownCalibAndSkeleton;Integrated Security=True;",
"QueryTemplate": "SELECT [ParameterName], [ParameterValue] FROM [dbo].[PreAdjustmentCalibrationParams] WHERE [MeterSize] = QUERYPARAM"
},
"PreadjustmentCompletionWriter": {
"Enabled": true,
"DataSource": "Server=(localdb)\\MojaDB;Database=UnionTownCalibAndSkeleton;Integrated Security=True;",
"CommandText": "INSERT INTO [dbo].[PreadjustmentCompletion] ([PcbId], [ZeroOffset1], [ZeroOffset2], [ZeroOffset3], [FirstHitUpdatePeriod], [FirstHitShift], [FirstHitPercent1], [FirstHitPercent2], [FirstHitPercent3], [ToFTempOffset1], [ToFTempOffset2], [ToFTempOffset3], [MeterSize], [Date], [Successful]) OUTPUT INSERTED.[Id] VALUES (@PcbId, @ZeroOffset1, @ZeroOffset2, @ZeroOffset3, @FirstHitUpdatePeriod, @FirstHitShift, @FirstHitPercent1, @FirstHitPercent2, @FirstHitPercent3, @ToFTempOffset1, @ToFTempOffset2, @ToFTempOffset3, @MeterSize, @Date, @Successful);",
"UseStoredProcedure": false,
"CommandTimeoutSeconds": 30,
"ParameterMappings": {
"PcbId": "@PcbId",
"ZeroOffset1": "@ZeroOffset1",
"ZeroOffset2": "@ZeroOffset2",
"ZeroOffset3": "@ZeroOffset3",
"FirstHitUpdatePeriod": "@FirstHitUpdatePeriod",
"FirstHitShift": "@FirstHitShift",
"FirstHitPercent1": "@FirstHitPercent1",
"FirstHitPercent2": "@FirstHitPercent2",
"FirstHitPercent3": "@FirstHitPercent3",
"ToFTempOffset1": "@ToFTempOffset1",
"ToFTempOffset2": "@ToFTempOffset2",
"ToFTempOffset3": "@ToFTempOffset3",
"MeterSize": "@MeterSize",
"Date": "@Date",
"Successful": "@Successful"
}
},
"Q3CalibrationWriter": {
"Enabled": true,
"DataSource": "Server=(localdb)\\MojaDB;Database=UnionTownCalibAndSkeleton;Integrated Security=True;",
"CommandText": "INSERT INTO [dbo].[Q3Calibration] ([PcbId], [P1Offset], [P1Cal], [P2Offset], [P2Cal], [P3Offset], [P3Cal], [Date]) OUTPUT INSERTED.[Id] VALUES (@PcbId, @P1Offset, @P1Cal, @P2Offset, @P2Cal, @P3Offset, @P3Cal, @Date);",
"UseStoredProcedure": false,
"CommandTimeoutSeconds": 30,
"ParameterMappings": {
"PcbId": "@PcbId",
"P1Offset": "@P1Offset",
"P1Cal": "@P1Cal",
"P2Offset": "@P2Offset",
"P2Cal": "@P2Cal",
"P3Offset": "@P3Offset",
"P3Cal": "@P3Cal",
"Date": "@Date"
}
}
}
}
@@ -0,0 +1,32 @@
namespace GenesisCordonelInterface.Core.DataStorage.Config
{
/// <summary>
/// Root GCI configuration object loaded from gci_config.json.
///
/// This class represents the top-level configuration structure
/// and serves as an entry point for all configurable GCI features.
///
/// Example:
///
/// {
/// "DataStorage":
/// {
/// ...
/// }
/// }
/// </summary>
public class GciConfig
{
/// <summary>
/// Data storage configuration section.
///
/// Contains definitions for all configured readers and writers,
/// such as:
///
/// - MeterLoginPasswords
/// - PreAdjustmentCalibrationParams
/// - future storage providers
/// </summary>
public GciDataStorageConfig DataStorage { get; set; }
}
}
@@ -0,0 +1,177 @@
using System;
using System.IO;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
using Newtonsoft.Json;
namespace GenesisCordonelInterface.Core.DataStorage.Config
{
/// <summary>
/// Loads GCI configuration from gci_config.json.
///
/// Responsibilities:
/// - Locate configuration file
/// - Deserialize JSON into strongly typed objects
/// - Normalize relative file paths
/// - Preserve database connection strings and REST URLs
///
/// Example:
///
/// Config/
/// gci_config.json
///
/// Data/
/// meter_passwords.csv
///
/// Relative paths:
/// Data\file.csv
///
/// become:
///
/// C:\App\bin\Debug\Data\file.csv
///
/// Database sources remain unchanged:
///
/// Server=(localdb)\MojaDB;Database=...
/// </summary>
public static class GciConfigLoader
{
/// <summary>
/// Loads default GCI configuration from:
///
/// Config\gci_config.json
/// </summary>
/// <returns>
/// Loaded GCI configuration.
/// </returns>
public static GciConfig LoadDefault()
{
string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
string configPath = Path.Combine(
baseDirectory,
"Config",
"gci_config.json");
return Load(configPath, baseDirectory);
}
/// <summary>
/// Loads GCI configuration from specified file.
/// </summary>
/// <param name="configPath">
/// Path to configuration json file.
/// </param>
/// <param name="baseDirectory">
/// Base directory used for resolving relative paths.
/// </param>
/// <returns>
/// Loaded configuration object.
/// </returns>
public static GciConfig Load(
string configPath,
string baseDirectory = null)
{
if (string.IsNullOrWhiteSpace(configPath))
throw new ArgumentException(
"Config path must not be empty.",
nameof(configPath));
if (!File.Exists(configPath))
throw new FileNotFoundException(
"GCI config file was not found.",
configPath);
string json = File.ReadAllText(configPath);
GciConfig config =
JsonConvert.DeserializeObject<GciConfig>(json);
if (config == null)
throw new InvalidOperationException(
"GCI config could not be loaded.");
NormalizeDataStoragePaths(
config,
baseDirectory ?? Path.GetDirectoryName(configPath));
return config;
}
/// <summary>
/// Normalizes paths for all configured data storage entries.
///
/// Converts relative file paths into absolute paths.
/// Database connection strings remain untouched.
/// </summary>
/// <param name="config">
/// Loaded configuration object.
/// </param>
/// <param name="baseDirectory">
/// Base path used for relative resolution.
/// </param>
private static void NormalizeDataStoragePaths(
GciConfig config,
string baseDirectory)
{
if (config.DataStorage == null)
return;
NormalizePath(
config.DataStorage.MeterLoginPasswords,
baseDirectory);
NormalizePath(
config.DataStorage.PreAdjustmentCalibrationParams,
baseDirectory);
}
/// <summary>
/// Converts relative file paths into absolute paths.
///
/// Applies only to:
/// - LocalCsv
/// - RemoteCsv
/// - LocalJson
/// - RemoteJson
///
/// Does not modify:
/// - Database connection strings
/// - REST URLs
/// </summary>
/// <param name="storageConfig">
/// Storage configuration.
/// </param>
/// <param name="baseDirectory">
/// Base path for resolution.
/// </param>
private static void NormalizePath(
DataStorageConfig storageConfig,
string baseDirectory)
{
if (storageConfig == null)
return;
if (string.IsNullOrWhiteSpace(storageConfig.DataSource))
return;
// Database connection strings and URLs
// must never be treated as file paths.
if (storageConfig.Type == DataStorageType.LocalDatabase ||
storageConfig.Type == DataStorageType.RemoteDatabase ||
storageConfig.Type == DataStorageType.RestApi)
{
return;
}
if (Path.IsPathRooted(storageConfig.DataSource))
return;
storageConfig.DataSource =
Path.GetFullPath(
Path.Combine(
baseDirectory,
storageConfig.DataSource));
}
}
}
@@ -0,0 +1,33 @@
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
namespace GenesisCordonelInterface.Core.DataStorage.Config
{
/// <summary>
/// Contains all configured GCI data storage sources.
///
/// Each property represents one logical use-case
/// and points to its storage configuration.
///
/// Example:
/// MeterLoginPasswords
/// -> SQL database
///
/// PreAdjustmentCalibrationParams
/// -> CSV file
/// </summary>
public class GciDataStorageConfig
{
public GciDatabaseWriterConfig PreadjustmentCompletionWriter { get; set; }
public GciDatabaseWriterConfig Q3CalibrationWriter { get; set; }
/// <summary>
/// Configuration for meter login password lookup.
/// </summary>
public DataStorageConfig MeterLoginPasswords { get; set; }
/// <summary>
/// Configuration for pre-adjustment calibration data lookup.
/// </summary>
public DataStorageConfig PreAdjustmentCalibrationParams { get; set; }
}
}
@@ -0,0 +1,22 @@
using System.Collections.Generic;
namespace GenesisCordonelInterface.Core.DataStorage.Config
{
/// <summary>SQL Server destination for one logical result type.</summary>
public sealed class GciDatabaseWriterConfig
{
// Missing sections in existing configuration files leave writes disabled.
public bool Enabled { get; set; }
public string DataSource { get; set; }
public string CommandText { get; set; }
public bool UseStoredProcedure { get; set; }
public int CommandTimeoutSeconds { get; set; } = 30;
/// <summary>
/// Optional complete mapping: DTO property name -> SQL parameter name.
/// Null/empty uses the documented default parameter names.
/// Commands must return the persisted positive identity as their first scalar.
/// </summary>
public Dictionary<string, string> ParameterMappings { get; set; }
}
}
@@ -0,0 +1,49 @@
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts
{
/// <summary>
/// Defines common contract for all GCI data storage readers.
///
/// Implementations can read data from different storage types:
/// - SQL database
/// - CSV file
/// - JSON file
/// - REST API
///
/// Consumers should use this interface instead of concrete readers.
/// </summary>
public interface IDataStorageReader
{
/// <summary>
/// Reads data from configured storage using provided query.
/// </summary>
/// <param name="query">
/// Query object containing lookup parameters.
/// </param>
/// <returns>
/// Storage-specific result object.
/// </returns>
object GetData(Models.DataQuery query);
/// <summary>
/// Tests whether configured data source is accessible.
/// </summary>
/// <param name="enableDiagnostics">
/// Enables detailed diagnostic output.
/// </param>
/// <returns>
/// Diagnostic result of source test.
/// </returns>
Models.ReaderDiagnosticResult TestSource(bool enableDiagnostics);
/// <summary>
/// Tests whether configured query is valid for the data source.
/// </summary>
/// <param name="enableDiagnostics">
/// Enables detailed diagnostic output.
/// </param>
/// <returns>
/// Diagnostic result of query test.
/// </returns>
Models.ReaderDiagnosticResult TestQuery(bool enableDiagnostics);
}
}
@@ -0,0 +1,94 @@
using System;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts;
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common
{
/// <summary>
/// Factory responsible for creating appropriate
/// data storage reader implementations.
///
/// The reader type is selected according to:
///
/// DataStorageConfig.Type
///
/// Example:
///
/// LocalDatabase
/// -> DatabaseDataStorageReader
///
/// LocalCsv
/// -> CsvDataStorageReader
///
/// LocalJson
/// -> JsonDataStorageReader
///
/// RestApi
/// -> RestApiDataStorageReader
///
/// This factory hides implementation details from
/// higher layers and keeps consumers independent
/// of storage technology.
///
/// Trace:
///
/// MeterLoginPasswordReader
/// -> DataStorageReaderFactory.Create()
/// -> DatabaseDataStorageReader
/// -> CsvDataStorageReader
/// -> JsonDataStorageReader
/// -> RestApiDataStorageReader
/// </summary>
public static class DataStorageReaderFactory
{
/// <summary>
/// Creates appropriate reader implementation
/// according to storage configuration.
/// </summary>
/// <param name="config">
/// Data storage configuration loaded from gci_config.json.
/// </param>
/// <returns>
/// Configured storage reader implementation.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Configuration is null.
/// </exception>
/// <exception cref="NotSupportedException">
/// Unsupported storage type.
/// </exception>
public static IDataStorageReader Create(
DataStorageConfig config)
{
if (config == null)
throw new ArgumentNullException(nameof(config));
switch (config.Type)
{
case DataStorageType.RemoteDatabase:
case DataStorageType.LocalDatabase:
return new Providers.DatabaseDataStorageReader(config);
case DataStorageType.RemoteCsv:
case DataStorageType.LocalCsv:
return new Providers.CsvDataStorageReader(config);
case DataStorageType.RemoteJson:
case DataStorageType.LocalJson:
return new Providers.JsonDataStorageReader(config);
case DataStorageType.RestApi:
return new Providers.RestApiDataStorageReader(config);
default:
throw new NotSupportedException(
$"Unsupported DataStorageType: '{config.Type}'");
}
}
}
}
@@ -0,0 +1,58 @@
using System.Collections.Generic;
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models
{
/// <summary>
/// Represents query input for data storage readers.
///
/// Query parameters are used as lookup values
/// during storage search operations.
///
/// A single parameter:
///
/// 231630279
///
/// executes one lookup.
///
/// Multiple parameters:
///
/// 231630279
/// 231630243
/// 231630148
///
/// can execute batch operations.
///
/// Example:
///
/// QueryTemplate:
///
/// SELECT [Password]
/// WHERE [PcbId]=QUERYPARAM
///
/// Query:
///
/// QueryParams:
/// 231630279
///
/// Result:
///
/// Password belonging to 231630279.
/// </summary>
public class DataQuery
{
/// <summary>
/// Collection of query values.
///
/// Single item:
///
/// QueryParams[0]
///
/// represents one lookup.
///
/// Multiple items may be processed
/// as batch requests.
/// </summary>
public List<string> QueryParams { get; }
= new List<string>();
}
}
@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models
{
/// <summary>
/// Configuration for one storage source.
/// Loaded from gci_config.json
/// </summary>
public class DataStorageConfig
{
/// <summary>
/// Logical storage name.
/// Example:
/// MeterLoginPasswords
/// CalibrationParameters
/// </summary>
public string Name { get; set; }
/// <summary>
/// Storage implementation type.
/// </summary>
public DataStorageType Type { get; set; }
/// <summary>
/// Connection string,
/// file path,
/// URL etc.
/// </summary>
public string DataSource { get; set; }
/// <summary>
/// Query template.
/// Example:
///
/// SQL:
/// SELECT Password
/// FROM Passwords
/// WHERE PcbId=QUERYPARAM
///
/// CSV:
/// SELECT [Password]
/// WHERE [PcbId]=QUERYPARAM
/// </summary>
public string QueryTemplate { get; set; }
public DataStorageConfig(
string name,
DataStorageType type,
string dataSource,
string queryTemplate)
{
Name = name;
Type = type;
DataSource = dataSource;
QueryTemplate = queryTemplate;
}
}
}
@@ -0,0 +1,79 @@
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models
{
/// <summary>
/// Identifies supported data storage implementations.
///
/// The type determines which reader implementation
/// will be created by DataStorageReaderFactory.
///
/// Example:
///
/// LocalDatabase
/// -> DatabaseDataStorageReader
///
/// LocalCsv
/// -> CsvDataStorageReader
///
/// RestApi
/// -> RestApiDataStorageReader
///
/// Source:
///
/// gci_config.json
///
/// Example:
///
/// "Type":"LocalDatabase"
/// </summary>
public enum DataStorageType
{
/// <summary>
/// REST API endpoint.
///
/// Example:
///
/// https://api.company.com/passwords
/// </summary>
RestApi,
/// <summary>
/// Database located on remote server.
///
/// Example:
///
/// SQL server on network machine.
/// </summary>
RemoteDatabase,
/// <summary>
/// Database located on local machine.
///
/// Example:
///
/// SQL LocalDB
/// SQLite
/// local SQL Server instance
/// </summary>
LocalDatabase,
/// <summary>
/// JSON source stored remotely.
/// </summary>
RemoteJson,
/// <summary>
/// JSON file stored locally.
/// </summary>
LocalJson,
/// <summary>
/// CSV source stored remotely.
/// </summary>
RemoteCsv,
/// <summary>
/// CSV file stored locally.
/// </summary>
LocalCsv
}
}
@@ -0,0 +1,53 @@
using System.Collections.Generic;
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models
{
/// <summary>
/// Represents result returned by database-based
/// data storage readers.
///
/// Supports both:
///
/// - Single-row lookups
/// (e.g. MeterLoginPasswords)
///
/// - Multi-row queries
/// (e.g. PreAdjustmentCalibrationParams)
/// </summary>
internal class DatabaseSearchResult
{
/// <summary>
/// Indicates whether at least one record
/// was found.
/// </summary>
public bool Found { get; set; }
/// <summary>
/// Executed SQL query text.
/// Mainly intended for diagnostics
/// and troubleshooting.
/// </summary>
public string Query { get; set; }
/// <summary>
/// First returned row represented as
/// column/value pairs.
///
/// Preserved for backward compatibility
/// with existing readers expecting
/// a single database record.
/// </summary>
public Dictionary<string, object> Values { get; }
= new Dictionary<string, object>();
/// <summary>
/// All returned rows represented as
/// a collection of column/value dictionaries.
///
/// Intended for queries returning
/// multiple records.
/// </summary>
public List<Dictionary<string, object>> Rows { get; }
= new List<Dictionary<string, object>>();
}
}
@@ -0,0 +1,141 @@
using System.Collections.Generic;
using System.Text;
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models
{
/// <summary>
/// Represents diagnostic result of data storage operations.
///
/// Used for:
/// - source validation
/// - query validation
/// - connection tests
/// - detailed diagnostic logging
///
/// Example:
///
/// TestSource()
/// -> connection successful
///
/// TestQuery()
/// -> column validation
///
/// Can also carry additional payload data.
/// </summary>
public class ReaderDiagnosticResult
{
/// <summary>
/// Indicates whether operation completed successfully.
/// </summary>
public bool Success { get; set; }
/// <summary>
/// Human readable summary message.
///
/// Example:
///
/// "Connection successful."
///
/// or:
///
/// "Column not found."
/// </summary>
public string Message { get; set; }
/// <summary>
/// Detailed diagnostic output lines.
///
/// Used for debugging and troubleshooting.
/// </summary>
public List<string> Diagnostics { get; set; }
/// <summary>
/// Optional operation payload.
///
/// Example:
///
/// loaded CSV lines
/// SQL test value
/// parsed content
/// </summary>
public object Data { get; set; }
/// <summary>
/// Creates empty diagnostic result.
/// </summary>
public ReaderDiagnosticResult()
{
Diagnostics = new List<string>();
}
/// <summary>
/// Creates successful diagnostic result.
/// </summary>
/// <param name="message">
/// Success message.
/// </param>
/// <returns>
/// Successful result object.
/// </returns>
public static ReaderDiagnosticResult SuccessResult(
string message = "OK")
{
return new ReaderDiagnosticResult
{
Success = true,
Message = message
};
}
/// <summary>
/// Creates failed diagnostic result.
/// </summary>
/// <param name="message">
/// Failure message.
/// </param>
/// <returns>
/// Failed result object.
/// </returns>
public static ReaderDiagnosticResult Failure(
string message)
{
return new ReaderDiagnosticResult
{
Success = false,
Message = message
};
}
/// <summary>
/// Converts diagnostic information into display text.
///
/// Output contains:
///
/// diagnostic lines
/// +
/// summary message
/// </summary>
/// <returns>
/// Formatted text representation.
/// </returns>
public string ToDisplayDiag()
{
StringBuilder sb = new StringBuilder();
if (Diagnostics != null &&
Diagnostics.Count > 0)
{
foreach (string line in Diagnostics)
sb.AppendLine(line);
if (!string.IsNullOrWhiteSpace(Message))
sb.AppendLine();
}
if (!string.IsNullOrWhiteSpace(Message))
sb.AppendLine(Message);
return sb.ToString();
}
}
}
@@ -0,0 +1,394 @@
using System;
using System.Collections.Generic;
using System.IO;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Searching;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts;
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Providers
{
/// <summary>
/// Reads and searches CSV based data sources.
///
/// Supported query syntax:
/// SELECT [ReturnColumn] WHERE [MatchColumn] = QUERYPARAM
///
/// Example:
/// SELECT [Password] WHERE [PcbId] = QUERYPARAM
///
/// Supports:
/// - column names: [ColumnName]
/// - column indexes: COLUMN(n)
///
/// Trace:
/// MeterLoginPasswordReader
/// -> CsvDataStorageReader.GetData()
/// -> ConnectToSource()
/// -> ExecuteQuery()
/// </summary>
public class CsvDataStorageReader : IDataStorageReader
{
private readonly DataStorageConfig config;
private string resolvedPath;
private string[] loadedLines;
private string[] loadedHeaders;
/// <summary>
/// Creates CSV data storage reader using provided configuration.
/// </summary>
/// <param name="config">
/// Data storage configuration loaded from gci_config.json.
/// </param>
public CsvDataStorageReader(DataStorageConfig config)
{
this.config = config ?? throw new ArgumentNullException(nameof(config));
}
/// <summary>
/// Reads values from CSV using configured QueryTemplate.
/// </summary>
/// <param name="query">
/// Query parameters used for lookup.
/// </param>
/// <returns>
/// ReaderDiagnosticResult containing lookup result.
/// </returns>
public object GetData(DataQuery query)
{
if (query == null)
throw new ArgumentNullException(nameof(query));
ReaderDiagnosticResult sourceResult = ConnectToSource(true);
if (!sourceResult.Success)
throw new InvalidOperationException(sourceResult.Message);
ReaderDiagnosticResult queryResult = ExecuteQuery(query, true);
if (!queryResult.Success)
throw new InvalidOperationException(queryResult.Message);
return queryResult;
}
/// <summary>
/// Validates CSV source accessibility and content.
/// </summary>
/// <param name="enableDiagnostics">
/// Enables detailed diagnostic output.
/// </param>
/// <returns>
/// Diagnostic result.
/// </returns>
public ReaderDiagnosticResult TestSource(bool enableDiagnostics)
{
return ConnectToSource(enableDiagnostics);
}
/// <summary>
/// Validates configured QueryTemplate against CSV header.
/// </summary>
/// <param name="enableDiagnostics">
/// Enables detailed diagnostic output.
/// </param>
/// <returns>
/// Diagnostic result.
/// </returns>
public ReaderDiagnosticResult TestQuery(bool enableDiagnostics)
{
ReaderDiagnosticResult sourceResult = ConnectToSource(enableDiagnostics);
if (!sourceResult.Success)
return sourceResult;
ReaderDiagnosticResult result = new ReaderDiagnosticResult();
try
{
Log(result, enableDiagnostics, "Starting QueryTemplate validation.");
SearchOrderDefinition definition =
SearchOrderParser.Parse(config.QueryTemplate);
int selectIndex = ResolveColumnIndex(
loadedHeaders,
definition.SelectColumn);
int whereIndex = ResolveColumnIndex(
loadedHeaders,
definition.WhereColumn);
Log(result, enableDiagnostics, "QueryTemplate parsed successfully.");
Log(result, enableDiagnostics, "Select column index: " + selectIndex);
Log(result, enableDiagnostics, "Where column index: " + whereIndex);
result.Success = true;
result.Message = "QueryTemplate validation finished successfully.";
}
catch (Exception ex)
{
result.Success = false;
result.Message = ex.Message;
Log(result, enableDiagnostics, "ERROR: " + ex.Message);
}
return result;
}
/// <summary>
/// Opens CSV file and loads its content.
/// Validates file existence and header.
/// </summary>
/// <param name="enableDiagnostics">
/// Enables detailed diagnostic output.
/// </param>
/// <returns>
/// Source validation result.
/// </returns>
private ReaderDiagnosticResult ConnectToSource(bool enableDiagnostics)
{
ReaderDiagnosticResult result = new ReaderDiagnosticResult();
try
{
Log(result, enableDiagnostics, "Starting CSV source connection test.");
if (string.IsNullOrWhiteSpace(config.DataSource))
throw new InvalidOperationException("CSV data source is empty.");
resolvedPath = Path.GetFullPath(config.DataSource);
Log(result, enableDiagnostics, "Resolved full path: " + resolvedPath);
if (!File.Exists(resolvedPath))
throw new FileNotFoundException("CSV file was not found.", resolvedPath);
loadedLines = File.ReadAllLines(resolvedPath);
if (loadedLines == null || loadedLines.Length == 0)
throw new InvalidOperationException("CSV file is empty.");
loadedHeaders = SplitCsvLine(loadedLines[0]);
if (loadedHeaders == null || loadedHeaders.Length == 0)
throw new InvalidOperationException("CSV header is empty.");
Log(result, enableDiagnostics, "CSV line count: " + loadedLines.Length);
Log(result, enableDiagnostics, "CSV header column count: " + loadedHeaders.Length);
result.Success = true;
result.Message = "CSV source connection finished successfully.";
result.Data = loadedLines;
}
catch (Exception ex)
{
result.Success = false;
result.Message = ex.Message;
result.Data = null;
Log(result, enableDiagnostics, "ERROR: " + ex.Message);
}
return result;
}
/// <summary>
/// Executes configured QueryTemplate against loaded CSV data.
/// </summary>
/// <param name="query">
/// Query values used for matching.
/// </param>
/// <param name="enableDiagnostics">
/// Enables detailed diagnostic output.
/// </param>
/// <returns>
/// Query result.
/// </returns>
private ReaderDiagnosticResult ExecuteQuery(
DataQuery query,
bool enableDiagnostics)
{
ReaderDiagnosticResult result = new ReaderDiagnosticResult();
try
{
if (query.QueryParams == null || query.QueryParams.Count == 0)
throw new InvalidOperationException("DataQuery.QueryParams is empty.");
SearchOrderDefinition definition =
SearchOrderParser.Parse(config.QueryTemplate);
int selectIndex = ResolveColumnIndex(
loadedHeaders,
definition.SelectColumn);
int whereIndex = ResolveColumnIndex(
loadedHeaders,
definition.WhereColumn);
List<string> matchedValues = new List<string>();
foreach (string param in query.QueryParams)
{
string queryValue = (param ?? string.Empty).Trim();
bool found = false;
for (int lineIndex = 1; lineIndex < loadedLines.Length; lineIndex++)
{
if (string.IsNullOrWhiteSpace(loadedLines[lineIndex]))
continue;
string[] values = SplitCsvLine(loadedLines[lineIndex]);
if (whereIndex >= values.Length)
continue;
string currentValue = (values[whereIndex] ?? string.Empty).Trim();
if (!string.Equals(
currentValue,
queryValue,
StringComparison.OrdinalIgnoreCase))
{
continue;
}
string returnValue =
selectIndex < values.Length
? values[selectIndex]
: string.Empty;
matchedValues.Add(returnValue);
found = true;
break;
}
if (!found)
Log(result, enableDiagnostics, "No match found for: " + queryValue);
}
result.Success = true;
if (query.QueryParams.Count == 1)
{
result.Data = matchedValues.Count > 0 ? matchedValues[0] : null;
result.Message = matchedValues.Count > 0
? "Value found."
: "No match found.";
}
else
{
result.Data = matchedValues;
result.Message =
"Batch query finished. Matches found: " +
matchedValues.Count +
" of " +
query.QueryParams.Count +
".";
}
}
catch (Exception ex)
{
result.Success = false;
result.Message = ex.Message;
result.Data = null;
Log(result, enableDiagnostics, "ERROR: " + ex.Message);
}
return result;
}
/// <summary>
/// Resolves column index either by name or by explicit index.
/// </summary>
/// <param name="headers">
/// CSV header columns.
/// </param>
/// <param name="columnReference">
/// Parsed QueryTemplate column definition.
/// </param>
/// <returns>
/// Zero-based column index.
/// </returns>
private int ResolveColumnIndex(
string[] headers,
ColumnReference columnReference)
{
if (columnReference == null)
throw new InvalidOperationException("Column reference is null.");
if (columnReference.HasIndex)
{
int index = columnReference.Index.Value;
if (index < 0 || index >= headers.Length)
throw new InvalidOperationException("Column index is out of range.");
return index;
}
if (columnReference.HasName)
{
for (int i = 0; i < headers.Length; i++)
{
if (string.Equals(
headers[i]?.Trim(),
columnReference.Name?.Trim(),
StringComparison.OrdinalIgnoreCase))
{
return i;
}
}
throw new InvalidOperationException(
"Column '" + columnReference.Name + "' was not found in CSV header.");
}
throw new InvalidOperationException("Column reference is not defined.");
}
/// <summary>
/// Splits CSV line using common separators.
/// Supports:
/// ';'
/// ','
/// '\t'
/// </summary>
/// <param name="line">
/// Input CSV line.
/// </param>
/// <returns>
/// Parsed columns.
/// </returns>
private string[] SplitCsvLine(string line)
{
if (string.IsNullOrEmpty(line))
return new string[0];
if (line.Contains(";"))
return line.Split(';');
if (line.Contains(","))
return line.Split(',');
if (line.Contains("\t"))
return line.Split('\t');
return new[] { line };
}
/// <summary>
/// Adds diagnostic line when diagnostics are enabled.
/// </summary>
private void Log(
ReaderDiagnosticResult result,
bool enableDiagnostics,
string message)
{
if (!enableDiagnostics || result == null)
return;
result.Diagnostics.Add(message);
}
}
}
@@ -0,0 +1,397 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts;
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Providers
{
/// <summary>
/// Reads data from SQL Server based data storage.
///
/// The reader uses configuration loaded from gci_config.json.
///
/// QueryTemplate must contain QUERYPARAM placeholder.
///
/// Example:
///
/// SELECT [Password]
/// FROM [dbo].[SkeletonKeys]
/// WHERE [PcbId] = QUERYPARAM
///
/// or
///
/// SELECT [ParameterName], [ParameterValue]
/// FROM [dbo].[PreAdjustmentCalibrationParams]
/// WHERE [Dn_InternalId] = QUERYPARAM
///
/// The placeholder is internally converted
/// to SQL parameter @value.
///
/// Supports both:
///
/// - Single-row lookups
/// - Multi-row result sets
/// </summary>
public class DatabaseDataStorageReader : IDataStorageReader
{
/// <summary>
/// Data storage configuration containing
/// connection string and query template.
/// </summary>
private readonly DataStorageConfig config;
/// <summary>
/// Creates SQL Server data storage reader
/// using provided configuration.
/// </summary>
/// <param name="config">
/// Data storage configuration loaded
/// from gci_config.json.
/// </param>
public DatabaseDataStorageReader(
DataStorageConfig config)
{
this.config =
config ?? throw new ArgumentNullException(nameof(config));
}
/// <summary>
/// Executes configured SQL query and returns
/// matching database records.
///
/// Single-row queries populate:
/// DatabaseSearchResult.Values
///
/// Multi-row queries populate:
/// DatabaseSearchResult.Rows
///
/// For backward compatibility, the first row
/// is also stored in Values.
/// </summary>
/// <param name="query">
/// Query object containing lookup parameter.
/// </param>
/// <returns>
/// DatabaseSearchResult containing returned
/// database records.
///
/// Values contains the first returned row.
///
/// Rows contains the complete result set.
/// </returns>
public object GetData(DataQuery query)
{
if (query == null)
throw new ArgumentNullException(nameof(query));
ReaderDiagnosticResult sourceResult =
TestSource(true);
if (!sourceResult.Success)
throw new InvalidOperationException(sourceResult.Message);
string sqlText =
PrepareSqlText(config.QueryTemplate);
object queryValue =
ExtractQueryValue(query);
using (SqlConnection connection =
new SqlConnection(config.DataSource))
using (SqlCommand command =
new SqlCommand(sqlText, connection))
{
AddQueryParameter(command, queryValue);
connection.Open();
// Full result set is required because some
// storage definitions return multiple records
// (e.g. PreAdjustmentCalibrationParams).
using (SqlDataReader reader =
command.ExecuteReader())
{
DatabaseSearchResult result =
new DatabaseSearchResult
{
Query = sqlText
};
while (reader.Read())
{
result.Found = true;
// Represents one database row.
Dictionary<string, object> row =
new Dictionary<string, object>();
for (int i = 0; i < reader.FieldCount; i++)
{
object value =
reader.GetValue(i);
object normalizedValue =
value == DBNull.Value
? null
: value;
row[reader.GetName(i)] =
normalizedValue;
}
// Preserve first row for legacy consumers
// expecting a single returned database record
// (e.g. MeterLoginPasswordReader).
if (result.Rows.Count == 0)
{
foreach (var item in row)
{
result.Values[item.Key] =
item.Value;
}
}
// Store complete database result set.
result.Rows.Add(row);
}
if (result.Rows.Count == 0)
{
result.Found = false;
}
return result;
}
}
}
/// <summary>
/// Tests whether SQL Server connection
/// can be opened.
/// </summary>
/// <param name="enableDiagnostics">
/// Enables detailed diagnostic output.
/// </param>
/// <returns>
/// Diagnostic result of SQL connection test.
/// </returns>
public ReaderDiagnosticResult TestSource(
bool enableDiagnostics)
{
ReaderDiagnosticResult result =
new ReaderDiagnosticResult();
try
{
if (string.IsNullOrWhiteSpace(config.DataSource))
{
throw new InvalidOperationException(
"Data source is empty.");
}
Log(
result,
enableDiagnostics,
"Opening SQL connection.");
using (SqlConnection connection =
new SqlConnection(config.DataSource))
{
connection.Open();
Log(
result,
enableDiagnostics,
"Connection opened successfully.");
using (SqlCommand command =
new SqlCommand("SELECT 1", connection))
{
object value =
command.ExecuteScalar();
Log(
result,
enableDiagnostics,
"Test query result: " + value);
}
}
result.Success = true;
result.Message =
"Connection to SQL Server OK.";
}
catch (Exception ex)
{
result.Success = false;
result.Message =
"Failed to connect to SQL Server. " + ex.Message;
Log(
result,
enableDiagnostics,
ex.ToString());
}
return result;
}
/// <summary>
/// Tests whether configured SQL query
/// can be prepared and executed.
/// </summary>
/// <param name="enableDiagnostics">
/// Enables detailed diagnostic output.
/// </param>
/// <returns>
/// Diagnostic result of query execution test.
/// </returns>
public ReaderDiagnosticResult TestQuery(
bool enableDiagnostics)
{
ReaderDiagnosticResult result =
new ReaderDiagnosticResult();
try
{
if (string.IsNullOrWhiteSpace(config.QueryTemplate))
{
throw new InvalidOperationException(
"Query template is empty.");
}
string sqlText =
PrepareSqlText(config.QueryTemplate);
Log(result, enableDiagnostics, "Original template:");
Log(result, enableDiagnostics, config.QueryTemplate);
Log(result, enableDiagnostics, "Prepared SQL:");
Log(result, enableDiagnostics, sqlText);
using (SqlConnection connection =
new SqlConnection(config.DataSource))
using (SqlCommand command =
new SqlCommand(sqlText, connection))
{
AddQueryParameter(command, "TEST");
connection.Open();
object value =
command.ExecuteScalar();
result.Data = value;
Log(
result,
enableDiagnostics,
"Query executed successfully.");
}
result.Success = true;
result.Message =
"Query executed successfully.";
}
catch (Exception ex)
{
result.Success = false;
result.Message =
"Query execution failed. " + ex.Message;
Log(
result,
enableDiagnostics,
ex.ToString());
}
return result;
}
/// <summary>
/// Converts configured QueryTemplate
/// to executable SQL text.
/// </summary>
/// <param name="queryTemplate">
/// SQL query template containing
/// QUERYPARAM placeholder.
/// </param>
/// <returns>
/// SQL text with QUERYPARAM replaced
/// by @value parameter.
/// </returns>
private static string PrepareSqlText(
string queryTemplate)
{
if (string.IsNullOrWhiteSpace(queryTemplate))
{
throw new ArgumentException(
"Query template must not be empty.",
nameof(queryTemplate));
}
if (!queryTemplate.Contains("QUERYPARAM"))
{
throw new InvalidOperationException(
"Query template must contain QUERYPARAM placeholder.");
}
return queryTemplate.Replace(
"QUERYPARAM",
"@value");
}
/// <summary>
/// Extracts first query parameter value.
/// </summary>
private static object ExtractQueryValue(
DataQuery query)
{
if (query.QueryParams == null ||
query.QueryParams.Count == 0)
{
throw new InvalidOperationException(
"DataQuery does not contain any query parameter.");
}
return query.QueryParams[0];
}
/// <summary>
/// Adds lookup parameter to SQL command.
/// </summary>
private static void AddQueryParameter(
SqlCommand command,
object value)
{
command.Parameters.Clear();
SqlParameter parameter =
command.Parameters.Add(
"@value",
SqlDbType.Variant);
parameter.Value =
value ?? DBNull.Value;
}
/// <summary>
/// Adds diagnostic line when diagnostics
/// are enabled.
/// </summary>
private static void Log(
ReaderDiagnosticResult result,
bool enableDiagnostics,
string message)
{
if (!enableDiagnostics || result == null)
return;
result.Diagnostics.Add(message);
}
}
}
@@ -0,0 +1,31 @@
using System;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts;
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Providers
{
public class JsonDataStorageReader : IDataStorageReader
{
private readonly DataStorageConfig config;
public JsonDataStorageReader(DataStorageConfig config)
{
this.config = config ?? throw new ArgumentNullException(nameof(config));
}
public object GetData(DataQuery query)
{
throw new NotImplementedException();
}
public ReaderDiagnosticResult TestSource(bool enableDiagnostics)
{
throw new NotImplementedException();
}
public ReaderDiagnosticResult TestQuery(bool enableDiagnostics)
{
throw new NotImplementedException();
}
}
}
@@ -0,0 +1,31 @@
using System;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts;
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Providers
{
public class RestApiDataStorageReader : IDataStorageReader
{
private readonly DataStorageConfig config;
public RestApiDataStorageReader(DataStorageConfig config)
{
this.config = config ?? throw new ArgumentNullException(nameof(config));
}
public object GetData(DataQuery query)
{
throw new NotImplementedException();
}
public ReaderDiagnosticResult TestSource(bool enableDiagnostics)
{
throw new NotImplementedException();
}
public ReaderDiagnosticResult TestQuery(bool enableDiagnostics)
{
throw new NotImplementedException();
}
}
}
@@ -0,0 +1,125 @@
using System;
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Searching
{
/// <summary>
/// Parsed representation of QueryTemplate expression.
///
/// Example:
///
/// SELECT [Password] WHERE [PcbId] = QUERYPARAM
///
/// becomes:
///
/// SelectColumn:
/// [Password]
///
/// WhereColumn:
/// [PcbId]
/// </summary>
public class SearchOrderDefinition
{
/// <summary>
/// Column to be returned from lookup result.
///
/// Example:
///
/// SELECT [Password]
///
/// returns:
///
/// [Password]
/// </summary>
public ColumnReference SelectColumn { get; set; }
/// <summary>
/// Column used for row matching.
///
/// Example:
///
/// WHERE [PcbId] = QUERYPARAM
///
/// uses:
///
/// [PcbId]
/// </summary>
public ColumnReference WhereColumn { get; set; }
}
/// <summary>
/// Represents one column definition inside QueryTemplate.
///
/// Supports two forms:
///
/// [ColumnName]
///
/// or:
///
/// COLUMN(number)
///
/// Examples:
///
/// [Password]
///
/// COLUMN(2)
/// </summary>
public class ColumnReference
{
/// <summary>
/// Column name used in named lookup mode.
///
/// Example:
///
/// [Password]
/// </summary>
public string Name { get; set; }
/// <summary>
/// Zero-based column index used in indexed lookup mode.
///
/// Example:
///
/// COLUMN(2)
/// </summary>
public int? Index { get; set; }
/// <summary>
/// Indicates whether column uses name-based lookup.
/// </summary>
public bool HasName
{
get
{
return !string.IsNullOrWhiteSpace(Name);
}
}
/// <summary>
/// Indicates whether column uses index-based lookup.
/// </summary>
public bool HasIndex
{
get
{
return Index.HasValue;
}
}
/// <summary>
/// Returns human readable representation.
/// </summary>
/// <returns>
/// Column expression text.
/// </returns>
public override string ToString()
{
if (HasName)
return "[" + Name + "]";
if (HasIndex)
return "COLUMN(" + Index.Value + ")";
return "<undefined column reference>";
}
}
}
@@ -0,0 +1,167 @@
using System;
using System.Text.RegularExpressions;
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Searching
{
/// <summary>
/// Parses SQL-like QueryTemplate expressions used by GCI data storage readers.
///
/// Supported syntax:
///
/// SELECT [ReturnColumn] WHERE [MatchColumn] = QUERYPARAM
///
/// Examples:
///
/// SELECT [Password] WHERE [PcbId] = QUERYPARAM
///
/// SELECT COLUMN(1) WHERE COLUMN(0) = QUERYPARAM
///
/// Mixed forms are also supported:
///
/// SELECT [Password] WHERE COLUMN(0)=QUERYPARAM
///
/// The parser converts text expressions into structured
/// SearchOrderDefinition objects which are later consumed
/// by CSV and other readers.
/// </summary>
public static class SearchOrderParser
{
/// <summary>
/// Full QueryTemplate validation pattern.
///
/// Expected syntax:
///
/// SELECT [Column] WHERE [Column] = QUERYPARAM
/// </summary>
private static readonly Regex FullPattern = new Regex(
@"^\s*SELECT\s+(?<select>\[[^\]]+\]|COLUMN\(\d+\))\s+WHERE\s+(?<where>\[[^\]]+\]|COLUMN\(\d+\))\s*=\s*QUERYPARAM\s*$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
/// <summary>
/// Matches:
///
/// [ColumnName]
/// </summary>
private static readonly Regex NamedColumnPattern = new Regex(
@"^\[(?<name>[^\]]+)\]$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
/// <summary>
/// Matches:
///
/// COLUMN(number)
/// </summary>
private static readonly Regex IndexedColumnPattern = new Regex(
@"^COLUMN\((?<index>\d+)\)$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
/// <summary>
/// Parses QueryTemplate text into structured definition.
///
/// Example:
///
/// SELECT [Password] WHERE [PcbId] = QUERYPARAM
///
/// Result:
///
/// SelectColumn:
/// Password
///
/// WhereColumn:
/// PcbId
/// </summary>
/// <param name="searchOrder">
/// QueryTemplate expression.
/// </param>
/// <returns>
/// Parsed query definition.
/// </returns>
public static SearchOrderDefinition Parse(string searchOrder)
{
if (string.IsNullOrWhiteSpace(searchOrder))
throw new InvalidOperationException(
"QueryTemplate is empty.");
Match match = FullPattern.Match(searchOrder);
if (!match.Success)
{
throw new InvalidOperationException(
"Invalid QueryTemplate syntax. Expected: SELECT [ReturnColumn] WHERE [MatchColumn] = QUERYPARAM");
}
return new SearchOrderDefinition
{
SelectColumn =
ParseColumnReference(
match.Groups["select"].Value),
WhereColumn =
ParseColumnReference(
match.Groups["where"].Value)
};
}
/// <summary>
/// Parses single column definition.
///
/// Supported forms:
///
/// [ColumnName]
///
/// COLUMN(number)
/// </summary>
/// <param name="token">
/// Raw token extracted from QueryTemplate.
/// </param>
/// <returns>
/// Parsed column reference.
/// </returns>
private static ColumnReference ParseColumnReference(
string token)
{
if (string.IsNullOrWhiteSpace(token))
throw new InvalidOperationException(
"Column reference token is empty.");
Match nameMatch =
NamedColumnPattern.Match(token);
if (nameMatch.Success)
{
return new ColumnReference
{
Name =
nameMatch
.Groups["name"]
.Value
.Trim(),
Index = null
};
}
Match indexMatch =
IndexedColumnPattern.Match(token);
if (indexMatch.Success)
{
return new ColumnReference
{
Name = null,
Index =
int.Parse(
indexMatch
.Groups["index"]
.Value)
};
}
throw new InvalidOperationException(
"Invalid column reference '" +
token +
"'. Use [ColumnName] or COLUMN(number).");
}
}
}
@@ -0,0 +1,51 @@
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
using System.Threading;
using System.Threading.Tasks;
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Implementation.MeterLoginPasswords
{
/// <summary>
/// Defines contract for reading meter login passwords
/// from configured GCI data storage.
///
/// Implementations hide storage details and provide
/// a unified API for password lookup.
///
/// Supported storage types:
/// - SQL database
/// - CSV
/// - JSON
/// - REST API
/// </summary>
public interface IMeterLoginPasswordReader
{
/// <summary>
/// Reads meter login password synchronously.
/// </summary>
/// <param name="queryParam">
/// PCB ID or another configured lookup value.
/// </param>
/// <returns>
/// Password if found; otherwise null.
/// </returns>
string GetPassword(DataQuery query);
/// <summary>
/// Reads meter login password asynchronously.
/// Thread-safe implementation may serialize
/// access to the underlying storage.
/// </summary>
/// <param name="queryParam">
/// PCB ID or another configured lookup value.
/// </param>
/// <param name="token">
/// Cancellation token.
/// </param>
/// <returns>
/// Password if found; otherwise null.
/// </returns>
Task<string> ReadMeterLoginPasswordAsync(
DataQuery query,
CancellationToken token = default);
}
}
@@ -0,0 +1,126 @@
using GenesisCordonelInterface.Core.DataStorage.Reading.Common;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts;
using GenesisCordonelInterface.Core.DataStorage.Reading.Implementation.MeterLoginPasswords;
using System;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Implementation.MeterLoginPasswords
{
/// <summary>
/// Reads meter login password from configured GCI data storage.
/// </summary>
public class MeterLoginPasswordReader : IMeterLoginPasswordReader
{
/// <summary>
/// Universal data storage reader selected by configuration.
/// Can represent database, CSV, JSON or REST reader.
/// </summary>
private readonly IDataStorageReader reader;
/// <summary>
/// Ensures that only one password lookup is executed at a time.
/// </summary>
private readonly SemaphoreSlim readLock = new SemaphoreSlim(1, 1);
/// <summary>
/// Creates password reader using provided data storage configuration.
/// </summary>
/// <param name="config">Data storage configuration loaded from gci_config.json.</param>
public MeterLoginPasswordReader(DataStorageConfig config)
{
reader = DataStorageReaderFactory.Create(config);
}
/// <summary>
/// Asynchronously reads meter login password using
/// the provided data query.
///
/// Thread-safe implementation may serialize
/// access to the underlying storage.
/// </summary>
/// <param name="query">
/// Data query containing lookup parameter values.
/// </param>
/// <param name="token">
/// Cancellation token.
/// </param>
/// <returns>
/// Password if found; otherwise null.
/// </returns>
public async Task<string> ReadMeterLoginPasswordAsync(
DataQuery query,
CancellationToken token = default)
{
if (query == null)
throw new ArgumentNullException(nameof(query));
if (query.QueryParams.Count == 0)
throw new ArgumentException(
"Query does not contain any parameter.",
nameof(query));
await readLock.WaitAsync(token);
try
{
return await Task.Run(
() => GetPassword(query),
token);
}
finally
{
readLock.Release();
}
}
/// <summary>
/// Reads meter login password using
/// the provided data query.
/// </summary>
/// <param name="query">
/// Data query containing lookup parameter values.
/// </param>
/// <returns>
/// Password if found; otherwise null.
/// </returns>
public string GetPassword(
DataQuery query)
{
if (query == null)
throw new ArgumentNullException(nameof(query));
if (query.QueryParams.Count == 0)
throw new ArgumentException(
"Query does not contain any parameter.",
nameof(query));
object result =
reader.GetData(query);
if (result is ReaderDiagnosticResult diagnosticResult)
{
return diagnosticResult.Data?.ToString();
}
if (result is DatabaseSearchResult dbResult)
{
if (!dbResult.Found ||
dbResult.Values == null ||
dbResult.Values.Count == 0)
{
return null;
}
foreach (object value in dbResult.Values.Values)
{
return value?.ToString();
}
}
return result?.ToString();
}
}
}
@@ -0,0 +1,62 @@
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Implementation.PreAdjustmentCalibrationParams
{
/// <summary>
/// Provides access to pre-adjustment calibration parameters
/// stored in the configured GCI data source.
///
/// Expected structure:
///
/// MeterSize | ParameterName | ParameterValue
///
/// Example:
///
/// 2 | GENESISFLOW_MinValidToF | 21990232
/// 2 | GENESISFLOW_Timeout | 1
///
/// Parameters are grouped by MeterSize and returned
/// as key/value pairs where:
///
/// Key = ParameterName
/// Value = ParameterValue
/// </summary>
public interface IPreAdjustmentCalibrationParamsReader
{
/// <summary>
/// Reads all pre-adjustment calibration parameters
/// using the provided data query synchronously.
/// </summary>
/// <param name="query">
/// Data query containing meter size as lookup parameter.
/// </param>
/// <returns>
/// Dictionary where key is GENESISFLOW parameter name
/// and value is stored parameter value.
/// </returns>
Dictionary<string, UInt32> GetCalibrationParams(
DataQuery query);
/// <summary>
/// Reads all pre-adjustment calibration parameters
/// using the provided data query asynchronously.
/// </summary>
/// <param name="query">
/// Data query containing meter size as lookup parameter.
/// </param>
/// <param name="token">
/// Cancellation token.
/// </param>
/// <returns>
/// Dictionary where key is GENESISFLOW parameter name
/// and value is stored parameter value.
/// </returns>
Task<Dictionary<string, UInt32>> ReadCalibrationParamsAsync(
DataQuery query,
CancellationToken token = default);
}
}
@@ -0,0 +1,305 @@
using GenesisCordonelInterface.Core.DataStorage.Reading.Common;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Implementation.PreAdjustmentCalibrationParams
{
/// <summary>
/// Reads pre-adjustment calibration parameters
/// from configured GCI data storage.
///
/// The underlying storage type is selected
/// through IDataStorageReader and may represent:
///
/// - SQL database
/// - CSV
/// - JSON
/// - REST API
///
/// Expected storage structure:
///
/// MeterSize | ParameterName | ParameterValue
///
/// Example:
///
/// 2 | GENESISFLOW_MinValidToF | 21990232
/// 2 | GENESISFLOW_Timeout | 1
///
/// Returned data are represented as:
///
/// Key = ParameterName
/// Value = ParameterValue
/// </summary>
public class PreAdjustmentCalibrationParamsReader
: IPreAdjustmentCalibrationParamsReader
{
/// <summary>
/// Universal data storage reader selected by configuration.
/// Can represent database, CSV, JSON or REST reader.
/// </summary>
private readonly IDataStorageReader reader;
/// <summary>
/// Ensures that only one calibration parameter lookup
/// is executed at a time.
/// </summary>
private readonly SemaphoreSlim readLock =
new SemaphoreSlim(1, 1);
/// <summary>
/// Creates calibration parameter reader using
/// provided data storage configuration.
/// </summary>
/// <param name="config">
/// Data storage configuration loaded from gci_config.json.
/// </param>
public PreAdjustmentCalibrationParamsReader(
DataStorageConfig config)
{
if (config == null)
throw new ArgumentNullException(nameof(config));
reader = DataStorageReaderFactory.Create(config);
}
/// <summary>
/// Reads all pre-adjustment calibration parameters
/// using the provided data query asynchronously.
///
/// The query is expected to contain meter size
/// as the first lookup parameter.
/// </summary>
/// <param name="query">
/// Data query containing lookup parameter values.
/// </param>
/// <param name="token">
/// Cancellation token.
/// </param>
/// <returns>
/// Dictionary:
///
/// Key:
/// GENESISFLOW parameter name
///
/// Value:
/// Stored parameter value
/// </returns>
public async Task<Dictionary<string, UInt32>> ReadCalibrationParamsAsync(
DataQuery query,
CancellationToken token = default)
{
ValidateQuery(query);
return await Task.Run(
() => GetCalibrationParams(query),
token).ConfigureAwait(false); ;
/*await readLock.WaitAsync(token);
try
{
return await Task.Run(
() => GetCalibrationParams(query),
token);
}
finally
{
readLock.Release();
}*/
}
/// <summary>
/// Reads all pre-adjustment calibration parameters
/// using the provided data query.
///
/// The query is expected to contain meter size
/// as the first lookup parameter.
/// </summary>
/// <param name="query">
/// Data query containing lookup parameter values.
/// </param>
/// <returns>
/// Dictionary:
///
/// Key:
/// GENESISFLOW parameter name
///
/// Value:
/// Stored parameter value
/// </returns>
public Dictionary<string, UInt32> GetCalibrationParams(
DataQuery query)
{
ValidateQuery(query);
object result =
reader.GetData(query);
return ConvertResultToDictionary(result);
}
/// <summary>
/// Validates that data query exists
/// and contains at least one lookup parameter.
/// </summary>
/// <param name="query">
/// Data query to validate.
/// </param>
private static void ValidateQuery(
DataQuery query)
{
if (query == null)
throw new ArgumentNullException(nameof(query));
if (query.QueryParams == null ||
query.QueryParams.Count == 0)
{
throw new ArgumentException(
"Query does not contain any parameter.",
nameof(query));
}
}
/// <summary>
/// Converts provider-specific result objects
/// returned by IDataStorageReader into a unified
/// dictionary representation.
/// </summary>
/// <param name="result">
/// Raw result returned by the configured storage reader.
/// </param>
/// <returns>
/// Dictionary containing calibration parameter
/// name/value pairs.
/// </returns>
private Dictionary<string, UInt32> ConvertResultToDictionary(
object result)
{
Dictionary<string, UInt32> values =
new Dictionary<string, UInt32>();
if (result == null)
return values;
if (result is DatabaseSearchResult dbResult)
{
if (!dbResult.Found)
return values;
foreach (Dictionary<string, object> row in dbResult.Rows)
{
if (!row.TryGetValue("ParameterName", out object parameterNameObject))
continue;
if (!row.TryGetValue("ParameterValue", out object parameterValueObject))
continue;
string parameterName =
parameterNameObject?.ToString();
if (string.IsNullOrWhiteSpace(parameterName))
continue;
if (TryConvertToUInt32(parameterValueObject, out UInt32 parameterValue))
{
values[parameterName] =
parameterValue;
}
}
return values;
}
if (result is ReaderDiagnosticResult diagnosticResult)
{
if (diagnosticResult.Data is Dictionary<string, UInt32> uintDictionary)
return uintDictionary;
if (diagnosticResult.Data is Dictionary<string, string> stringDictionary)
{
foreach (var item in stringDictionary)
{
if (TryConvertToUInt32(item.Value, out UInt32 value))
values[item.Key] = value;
}
return values;
}
if (diagnosticResult.Data is Dictionary<string, object> objectDictionary)
{
foreach (var item in objectDictionary)
{
if (TryConvertToUInt32(item.Value, out UInt32 value))
values[item.Key] = value;
}
return values;
}
}
if (result is Dictionary<string, UInt32> directUIntDictionary)
return directUIntDictionary;
if (result is Dictionary<string, string> directStringDictionary)
{
foreach (var item in directStringDictionary)
{
if (TryConvertToUInt32(item.Value, out UInt32 value))
values[item.Key] = value;
}
return values;
}
if (result is Dictionary<string, object> directObjectDictionary)
{
foreach (var item in directObjectDictionary)
{
if (TryConvertToUInt32(item.Value, out UInt32 value))
values[item.Key] = value;
}
}
return values;
}
private static bool TryConvertToUInt32(
object value,
out UInt32 result)
{
result = 0;
if (value == null)
return false;
if (value is UInt32 uintValue)
{
result = uintValue;
return true;
}
if (value is int intValue && intValue >= 0)
{
result = Convert.ToUInt32(intValue);
return true;
}
if (value is long longValue &&
longValue >= 0 &&
longValue <= UInt32.MaxValue)
{
result = Convert.ToUInt32(longValue);
return true;
}
return UInt32.TryParse(
value.ToString(),
out result);
}
}
}
@@ -0,0 +1,232 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using GenesisCordonelInterface.Core.DataStorage.Config;
using static GenesisCordonelInterface.API.PublicModels;
namespace GenesisCordonelInterface.Core.DataStorage.Writing
{
/// <summary>
/// Stores detached result records. Each call owns its connection and transaction;
/// slot workers, request sessions and streaming ownership are never involved.
/// </summary>
public sealed class CalibrationResultWriter
{
private readonly GciDatabaseWriterConfig completion;
private readonly GciDatabaseWriterConfig q3;
public CalibrationResultWriter(GciDataStorageConfig config)
{
completion = Snapshot(config?.PreadjustmentCompletionWriter);
q3 = Snapshot(config?.Q3CalibrationWriter);
}
private static GciDatabaseWriterConfig Snapshot(GciDatabaseWriterConfig config)
{
if (config == null) return null;
return new GciDatabaseWriterConfig
{
Enabled = config.Enabled, DataSource = config.DataSource,
CommandText = config.CommandText, UseStoredProcedure = config.UseStoredProcedure,
CommandTimeoutSeconds = config.CommandTimeoutSeconds,
ParameterMappings = config.ParameterMappings == null ? null :
new Dictionary<string, string>(config.ParameterMappings, StringComparer.Ordinal)
};
}
public Task<CalibrationWriteResult> StorePreadjustmentCompletionAsync(
PreadjustmentCompletionRecord record, CancellationToken token)
{
return StorePreadjustmentCompletionAsync(record, token, null);
}
public Task<CalibrationWriteResult> StorePreadjustmentCompletionAsync(
PreadjustmentCompletionRecord record, CancellationToken token, Action<string> diagnostic)
{
token.ThrowIfCancellationRequested();
if (record == null) throw new ArgumentNullException(nameof(record));
// Materialize all values before the first await; no mutable record is retained.
var values = new Dictionary<string, object>
{
{ "PcbId", record.PcbId }, { "MeterSize", record.MeterSize },
{ "ZeroOffset1", record.ZeroOffset1 }, { "ZeroOffset2", record.ZeroOffset2 },
{ "ZeroOffset3", record.ZeroOffset3 },
{ "FirstHitUpdatePeriod", record.FirstHitUpdatePeriod },
{ "FirstHitShift", record.FirstHitShift },
{ "FirstHitPercent1", record.FirstHitPercent1 },
{ "FirstHitPercent2", record.FirstHitPercent2 },
{ "FirstHitPercent3", record.FirstHitPercent3 },
{ "ToFTempOffset1", record.ToFTempOffset1 },
{ "ToFTempOffset2", record.ToFTempOffset2 },
{ "ToFTempOffset3", record.ToFTempOffset3 },
{ "Date", record.Date }, { "Successful", record.Successful }
};
string error = !record.MeterSize.HasValue || record.MeterSize.Value < 0
? "MeterSize is required and must not be negative." : null;
if (record.Successful && values.Any(v => v.Value == null))
error = "Successful preadjustment requires all calibration values.";
return WriteAsync(completion, record.PcbId, values, error, token, diagnostic);
}
public Task<CalibrationWriteResult> StoreQ3CalibrationAsync(
Q3CalibrationRecord record, CancellationToken token)
{
return StoreQ3CalibrationAsync(record, token, null);
}
public Task<CalibrationWriteResult> StoreQ3CalibrationAsync(
Q3CalibrationRecord record, CancellationToken token, Action<string> diagnostic)
{
token.ThrowIfCancellationRequested();
if (record == null) throw new ArgumentNullException(nameof(record));
var values = new Dictionary<string, object>
{
{ "PcbId", record.PcbId }, { "P1Offset", record.P1Offset },
{ "P1Cal", record.P1Cal }, { "P2Offset", record.P2Offset },
{ "P2Cal", record.P2Cal }, { "P3Offset", record.P3Offset },
{ "P3Cal", record.P3Cal }, { "Date", record.Date }
};
string error = values.Any(v => v.Value == null)
? "Q3 requires the offset and confirmed calibration factor for all three paths."
: null;
if (values.Values.OfType<double>().Any(v => double.IsNaN(v) || double.IsInfinity(v)))
error = "Q3 offsets must be finite numbers.";
return WriteAsync(q3, record.PcbId, values, error, token, diagnostic);
}
private static async Task<CalibrationWriteResult> WriteAsync(
GciDatabaseWriterConfig config, int pcbId, IDictionary<string, object> values,
string validationError, CancellationToken token, Action<string> diagnostic = null)
{
string operation = values.ContainsKey("Successful") ? "Completion" : "Q3 calibration";
Report(diagnostic, operation + " database write: START.");
var result = new CalibrationWriteResult { PcbId = pcbId };
if (config == null || !config.Enabled)
{
result.Status = CalibrationWriteStatus.Disabled;
result.ErrorMessage = "Database writer is not enabled.";
Report(diagnostic, operation + " database write skipped: writer is disabled or not configured.");
return result;
}
bool commitStarted = false;
bool connectionOpened = false;
string stage = "validation";
try
{
if (validationError != null) throw new ArgumentException(validationError);
if (pcbId <= 0) throw new ArgumentException("PcbId must be positive.");
if ((DateTime)values["Date"] == default(DateTime))
throw new ArgumentException("The calibration timestamp is required.");
if (string.IsNullOrWhiteSpace(config.DataSource) || string.IsNullOrWhiteSpace(config.CommandText))
throw new ArgumentException("Writer DataSource and CommandText are required.");
if (config.CommandTimeoutSeconds < 1)
throw new ArgumentException("CommandTimeoutSeconds must be positive.");
using (var connection = new SqlConnection(config.DataSource))
using (var command = new SqlCommand(config.CommandText, connection))
{
command.CommandType = config.UseStoredProcedure ? CommandType.StoredProcedure : CommandType.Text;
command.CommandTimeout = config.CommandTimeoutSeconds;
AddParameters(command, config, values);
stage = "server connection";
// Only log the destination, never credentials or the full connection string.
var destination = new SqlConnectionStringBuilder(config.DataSource);
Report(diagnostic, "Connecting to server '" + destination.DataSource +
"', database '" + destination.InitialCatalog + "'.");
await connection.OpenAsync(token).ConfigureAwait(false);
connectionOpened = true;
Report(diagnostic, "Connection opened.");
token.ThrowIfCancellationRequested();
stage = "transaction start";
using (var transaction = connection.BeginTransaction())
{
command.Transaction = transaction;
stage = "data write";
Report(diagnostic, "Writing " + operation + " data (" +
(config.UseStoredProcedure ? "stored procedure" : "SQL command") + ").");
// The command/procedure must return the stored Id (OUTPUT INSERTED.Id).
object scalar = await command.ExecuteScalarAsync(token).ConfigureAwait(false);
stage = "returned record Id validation";
if (scalar == null || scalar == DBNull.Value)
throw new InvalidOperationException("Writer did not return a record Id; transaction rolled back.");
decimal id = Convert.ToDecimal(scalar, CultureInfo.InvariantCulture);
if (id <= 0 || id != decimal.Truncate(id) || id > long.MaxValue)
throw new InvalidOperationException("Writer returned an invalid record Id; transaction rolled back.");
token.ThrowIfCancellationRequested();
Report(diagnostic, "Write command completed; confirming transaction.");
stage = "transaction commit";
// Once commit starts, cancellation must not hide a committed record.
commitStarted = true;
transaction.Commit();
result.RecordId = (long)id;
result.Status = CalibrationWriteStatus.Stored;
Report(diagnostic, "Transaction committed; RecordId=" + result.RecordId + ".");
}
}
}
catch (OperationCanceledException) when (!commitStarted)
{
Report(diagnostic, operation + " database write cancelled during " + stage + ".");
// Disposing the uncommitted transaction rolls back its write.
throw;
}
catch (Exception ex)
{
result.Status = commitStarted ? CalibrationWriteStatus.OutcomeUnknown : CalibrationWriteStatus.Failed;
result.ErrorMessage = ex.Message;
// Server messages can contain record values or login names. Keep
// the slot log free of these; retain the original error in the result.
var sqlError = ex as SqlException;
string detail = sqlError == null ? ex.GetType().Name :
"SQL error " + sqlError.Number + ", state " + sqlError.State;
Report(diagnostic, operation + " database write failed during " + stage + " (" + detail + ").");
if (commitStarted)
Report(diagnostic, "Commit outcome is unknown. Verify the database before retrying.");
}
finally
{
// The SQL using scopes have already disposed their resources here.
if (connectionOpened) Report(diagnostic, "SQL connection released.");
}
return result;
}
private static void Report(Action<string> diagnostic, string message)
{
if (diagnostic == null) return;
try { diagnostic(message); }
catch (Exception)
{
// A closed/failing diagnostic subscriber must not change the DB outcome.
}
}
private static void AddParameters(SqlCommand command, GciDatabaseWriterConfig config,
IDictionary<string, object> values)
{
var mappings = config.ParameterMappings;
bool custom = mappings != null && mappings.Count > 0;
if (custom && (mappings.Count != values.Count || mappings.Keys.Any(k => !values.ContainsKey(k))))
throw new ArgumentException("ParameterMappings must map every documented record field exactly once.");
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var value in values)
{
string name = custom ? mappings[value.Key] : "@" + value.Key;
if (string.IsNullOrWhiteSpace(name) || name.Length < 2 || name.Length > 128 ||
name[0] != '@' || !(char.IsLetter(name[1]) || name[1] == '_') ||
name.Skip(2).Any(c => !char.IsLetterOrDigit(c) && c != '_') || !names.Add(name))
throw new ArgumentException("Invalid or duplicate SQL parameter name for " + value.Key + ".");
SqlDbType type = value.Key == "Date" ? SqlDbType.DateTime2 :
value.Key == "Successful" ? SqlDbType.Bit :
value.Key == "P1Offset" || value.Key == "P2Offset" || value.Key == "P3Offset"
? SqlDbType.Float : SqlDbType.Int;
command.Parameters.Add(name, type).Value = value.Value ?? DBNull.Value;
}
}
}
}
@@ -0,0 +1,74 @@
using GenesisCordonelInterface.API;
using GenesisCordonelInterface.Core.Config;
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.AccessControl;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using UdsReaderType_CalibrationParams = GenesisCordonelInterface.Core.DataStorage.Reading.Implementation.PreAdjustmentCalibrationParams.PreAdjustmentCalibrationParamsReader;
using UdsReaderType_LoginPasswords = GenesisCordonelInterface.Core.DataStorage.Reading.Implementation.MeterLoginPasswords.MeterLoginPasswordReader;
using GciPublicModels = GenesisCordonelInterface.API.PublicModels;
using GciEnums = GenesisCordonelInterface.API.Enums;
using GciType = GenesisCordonelInterface.API.InterfaceOutsideToGCI;
using GciDataStorageReadingModels = GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
using GciGUIType = GenesisCordonelInterface.UI.MainView;
namespace GenesisCordonelInterface.Core
{
public class Engine
{
GciConfig gciConfig;
public UdsReaderType_LoginPasswords loginPasswordsDataStorageReader;
public UdsReaderType_CalibrationParams calibrationParamsStorageReader;
//readonly UdsWriterType writer;
//diag GUI for GCI
GciGUIType gciGUI;
Form gciGuiHostForm;
//diag GUI for GciBridge
public UserControl gciBridgeGUIUserControl;
public UI.MainForm gciBridgeGuiForm;
public InterfaceOutsideToGCI gciExternalInterface;
/// <summary>
/// Initializes a new instance of the public GCI facade.
/// </summary>
public Engine()
{
gciConfig = GciConfigLoader.LoadDefault();
loginPasswordsDataStorageReader = new UdsReaderType_LoginPasswords
(
new GciDataStorageReadingModels.DataStorageConfig
(
nameof(GciEnums.DataStorageReaderTypes.LoginPasswordsReader),
GciDataStorageReadingModels.DataStorageType.LocalDatabase,
gciConfig.DataStorageSection.MeterLoginPasswords.DataSource,
gciConfig.DataStorageSection.MeterLoginPasswords.QueryTemplate
)
);
calibrationParamsStorageReader = new UdsReaderType_CalibrationParams
(
new GciDataStorageReadingModels.DataStorageConfig
(
nameof(GciEnums.DataStorageReaderTypes.CalibrationParamsReader),
GciDataStorageReadingModels.DataStorageType.LocalDatabase,
gciConfig.DataStorageSection.PreAdjustmentCalibrationParams.DataSource,
gciConfig.DataStorageSection.PreAdjustmentCalibrationParams.QueryTemplate
)
);
gciExternalInterface = new InterfaceOutsideToGCI(
new InterfaceGCIToLaatzen(),
loginPasswordsDataStorageReader,
calibrationParamsStorageReader,
gciConfig.DataStorageSection);
}
}
}
@@ -0,0 +1,23 @@
Navod na logovanie projektu Laatzen/Common/ pre Genesis Cordonel meradla:
1. Princip:
- logovanie je zabezpecene pomocou nastroja NLog, ktory pouziva Laatzen
- Laatzen vsak pouziva nad NLogom svoj wrapper NLogHelper.cs a teda sa na pracu pri ich projektoch konfiguruje NLog pomocou ich suboru Nlogconfig.xml,
no nakolko ja som nechcel zasahovat do ich konfiguracneho suboru NLogu, pouzil som svoj, ktory je definovany v mojom projekte GenesisCordonelInterface,
s nazvom nlog.config
- z dovodu, ze som teda nechcel zasahovat do ich konfiguracie a zaroven som musel NLog na svoje ucely (presmerovanie logov do mema v mojom projekte),
musel som teda konfigurovat svojim konfiguracnym suborom, a zaroven plati ze pouzivam raw NLog.
- btw ked som pouzil ich wrapper a zaroven moj konfiguracny subor NLogu, tak pri spusten appky ho aj nacitalo, no nasledne sa nacitali Laatzenacke projekty
a nacital sa aj ich konfigurak Nlogu, ci sa prepisal ten moj. Pouzil som ten raw Nlog a uz to bolo ok
- takto je mozne presmerovat pomocou nizsie uvedenej metody logy az do mema v mojom projekte a tiez aj do suboru, pricom vsetko definujem v nlog.config
mojho projektu a nie v projekte Laatzenu
2. prilozene subory UiLogBus.cs a UiTarget.cs musia byt nakopirovane do projektu v ramci Solution s nazvom Common od Laatzenu
- kopiruju sa do top adresara projektu
- musia obsahovat ""namespace Xylem.Common.Utils.Logging"
- v mieste kde chcem logovat, je potrebne:
- zadefinovat:
private readonly ILogger Logger = NLogHelper.CreateOrGetLogger("GenesisCordonelInterface");
- pouzit log:
Logger.Error(ex, $"Slot #{slotNR} failed: {ex.Message}");
Logger.Trace("CONFIG: Setup loaded.");
Logger.Info("CONFIG: Setup loaded.");
...whatever
@@ -0,0 +1,18 @@
using System;
/*namespace Xylem.Common.Utils.Logging
{
public static class UiLogBus
{
public static event Action<string> MessageReceived;
public static void Publish(string message)
{
var handler = MessageReceived;
if (handler != null)
{
handler(message);
}
}
}
}*/
@@ -0,0 +1,15 @@
using NLog;
using NLog.Targets;
/*namespace Xylem.Common.Utils.Logging
{
[Target("UiTarget")]
public class UiTarget : TargetWithLayout
{
protected override void Write(LogEventInfo logEvent)
{
string message = Layout.Render(logEvent);
UiLogBus.Publish(message);
}
}
}*/
@@ -0,0 +1,518 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace GenesisCordonelInterface.Core.Threading
{
/*
ApiWorker – per-slot sequential execution worker
This class provides a lightweight background worker that executes actions
sequentially on a dedicated thread.
PRIMARY PURPOSE
---------------
ApiWorker is designed to safely execute hardware-related operations
(e.g. meter communication) without blocking the UI thread and without
allowing concurrent access to the same device.
Each ApiWorker instance typically represents:
1 worker = 1 slot = 1 meter = 1 communication channel
KEY PROPERTIES
--------------
- Single dedicated background thread
- FIFO queue (first-in, first-out)
- Sequential execution (NO parallelism inside one worker)
- Thread-safe enqueueing
- Task-based async interface for callers
WHY THIS IS IMPORTANT
--------------------
Hardware communication (serial ports, meters, etc.) is usually NOT thread-safe.
If multiple commands are executed in parallel, communication may break or corrupt data.
ApiWorker guarantees:
- operations are executed one-by-one
- order is preserved
- no race conditions on the device
HIGH-LEVEL FLOW
---------------
Caller (UI/API)
|
v
RunAsync(...)
|
v
TaskCompletionSource created
|
v
Action wrapped into queue item
|
v
Added to BlockingCollection queue
|
v
Worker thread consumes queue
|
v
Action executed (blocking HW call)
|
v
Result propagated via TaskCompletionSource
|
v
Caller receives result via await
GRAPH
-----
Caller thread (UI)
|
v
RunAsync()
|
v
Queue (BlockingCollection)
|
v
-----------------------------
| Worker Thread (background)|
| while(queue) |
| Execute Action |
-----------------------------
|
v
Task result (await)
THREADING MODEL
---------------
- Producer/Consumer pattern
- Producer: any thread calling RunAsync
- Consumer: single worker thread
- Synchronization handled by BlockingCollection
MAIN COMPONENTS
---------------
1. BlockingCollection<Action> queue
- thread-safe queue
- stores work items
- supports blocking consumption
2. Dedicated Thread
- runs WorkerLoop()
- continuously processes queue
3. TaskCompletionSource<T>
- bridges sync execution → async API
- allows caller to await result
METHODS
-------
RunAsync<T>(Func<T>)
--------------------
- Enqueues a function returning a value
- Wraps it into Action
- Executes on worker thread
- Returns Task<T> to caller
RunAsync(Action)
----------------
- Convenience overload for void methods
- Internally wraps into Func<object>
WorkerLoop()
------------
- Infinite loop consuming queue
- Executes actions one-by-one
- Stops when queue is completed
Dispose()
---------
- Stops accepting new items
- Cleans up queue
- Does NOT forcibly stop running task
CANCELLATION
------------
- CancellationToken is checked BEFORE execution
- If cancelled → Task is cancelled
- Does NOT interrupt running operation
IMPORTANT LIMITATIONS
--------------------
- No parallel execution inside one worker (by design)
- Long-running action blocks worker thread
- No built-in timeout handling
- Dispose does not abort running work
WHEN TO USE
-----------
✔ Per-device communication (serial, TCP, HW)
✔ Ordered execution required
✔ UI must stay responsive
WHEN NOT TO USE
---------------
✘ CPU parallel processing (use Task.Run / Parallel)
✘ High-throughput parallel workloads
✘ Fire-and-forget background tasks
SUMMARY
-------
ApiWorker is a simple, robust solution for:
"Execute commands sequentially per resource, asynchronously from UI"
It is a perfect fit for:
- hardware interfaces
- device drivers
- IO-bound serialized workflows
*/
public sealed class ApiWorker : IDisposable
{
private static readonly NLog.ILogger Logger = NLog.LogManager.GetLogger("GenesisCordonelInterface.SlotWorker");
private static long nextRequestId;
/// <summary>
/// Thread-safe FIFO queue holding work items.
/// </summary>
private readonly BlockingCollection<Action> queue = new BlockingCollection<Action>();
/// <summary>
/// Dedicated worker thread processing the queue.
/// </summary>
private readonly Thread thread;
/// <summary>
/// Indicates whether this worker has been disposed.
/// </summary>
private bool disposed;
public string Name { get; private set; }
public int QueueLength
{
get
{
if (disposed || queue == null)
return 0;
try
{
return queue.Count;
}
catch (ObjectDisposedException)
{
return 0;
}
}
}
public bool IsBusy { get; private set; }
public string CurrentOperation { get; private set; }
public string LastError { get; private set; }
public DateTime LastActivity { get; private set; }
public bool IsDisposed
{
get { return disposed; }
}
/// <summary>
/// Creates a new ApiWorker with its own background thread.
/// </summary>
public ApiWorker(string name)
{
Name = name;
LastActivity = DateTime.Now;
thread = new Thread(WorkerLoop)
{
IsBackground = true,
Name = name
};
thread.Start();
Logger.Info(
"[SLOT-WORKER] CREATED Worker={0}, WorkerThread={1}, CallerThread={2}",
Name,
thread.ManagedThreadId,
Thread.CurrentThread.ManagedThreadId);
}
/// <summary>
/// Enqueues a function returning a value for sequential execution.
/// </summary>
public Task<T> RunAsync<T>(
Func<T> action,
CancellationToken token = default,
string operationName = null)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
if (disposed)
throw new ObjectDisposedException(nameof(ApiWorker));
var tcs = new TaskCompletionSource<T>(
TaskCreationOptions.RunContinuationsAsynchronously);
long requestId = Interlocked.Increment(ref nextRequestId);
string operation = string.IsNullOrWhiteSpace(operationName) ? action.Method.Name : operationName;
int callerThreadId = Thread.CurrentThread.ManagedThreadId;
Stopwatch queueWait = Stopwatch.StartNew();
try
{
Logger.Debug(
"[SLOT-REQUEST] ENQUEUE RequestId={0}, Worker={1}, Operation={2}, CallerThread={3}, QueueLengthBefore={4}",
requestId,
Name,
operation,
callerThreadId,
QueueLength);
queue.Add(() =>
{
if (token.IsCancellationRequested)
{
queueWait.Stop();
LastActivity = DateTime.Now;
Logger.Warn(
"[SLOT-REQUEST] CANCELLED_BEFORE_START RequestId={0}, Worker={1}, Operation={2}, WorkerThread={3}, QueueWaitMs={4}, QueueLength={5}",
requestId,
Name,
operation,
Thread.CurrentThread.ManagedThreadId,
queueWait.ElapsedMilliseconds,
QueueLength);
tcs.TrySetCanceled();
return;
}
Stopwatch execution = Stopwatch.StartNew();
try
{
IsBusy = true;
CurrentOperation = operation;
LastActivity = DateTime.Now;
LastError = null;
queueWait.Stop();
Logger.Debug(
"[SLOT-REQUEST] START RequestId={0}, Worker={1}, Operation={2}, WorkerThread={3}, CallerThread={4}, QueueWaitMs={5}, QueueLength={6}",
requestId,
Name,
operation,
Thread.CurrentThread.ManagedThreadId,
callerThreadId,
queueWait.ElapsedMilliseconds,
QueueLength);
var result = action();
tcs.TrySetResult(result);
execution.Stop();
Logger.Debug(
"[SLOT-REQUEST] COMPLETE RequestId={0}, Worker={1}, Operation={2}, WorkerThread={3}, DurationMs={4}, QueueLength={5}",
requestId,
Name,
operation,
Thread.CurrentThread.ManagedThreadId,
execution.ElapsedMilliseconds,
QueueLength);
}
catch (Exception ex)
{
execution.Stop();
LastError = ex.Message;
Logger.Error(
ex,
"[SLOT-REQUEST] FAILED RequestId={0}, Worker={1}, Operation={2}, WorkerThread={3}, DurationMs={4}, QueueLength={5}, Error={6}",
requestId,
Name,
operation,
Thread.CurrentThread.ManagedThreadId,
execution.ElapsedMilliseconds,
QueueLength,
ex.Message);
tcs.TrySetException(ex);
}
finally
{
IsBusy = false;
CurrentOperation = null;
LastActivity = DateTime.Now;
}
}, token);
}
catch (OperationCanceledException)
{
queueWait.Stop();
Logger.Warn(
"[SLOT-REQUEST] ENQUEUE_CANCELLED RequestId={0}, Worker={1}, Operation={2}, CallerThread={3}, QueueWaitMs={4}",
requestId,
Name,
operation,
callerThreadId,
queueWait.ElapsedMilliseconds);
tcs.TrySetCanceled();
}
catch (ObjectDisposedException ex)
{
queueWait.Stop();
Logger.Error(
ex,
"[SLOT-REQUEST] ENQUEUE_FAILED RequestId={0}, Worker={1}, Operation={2}, Reason=WorkerDisposed",
requestId,
Name,
operation);
tcs.TrySetException(ex);
}
catch (InvalidOperationException ex)
{
queueWait.Stop();
var disposedException = new ObjectDisposedException(nameof(ApiWorker), ex);
Logger.Error(
disposedException,
"[SLOT-REQUEST] ENQUEUE_FAILED RequestId={0}, Worker={1}, Operation={2}, Reason=QueueCompleted",
requestId,
Name,
operation);
tcs.TrySetException(disposedException);
}
return tcs.Task;
}
public Task RunAsync(
Action action,
CancellationToken token = default(CancellationToken),
string operationName = null)
{
return RunAsync<object>(() =>
{
action();
return null;
}, token, operationName);
}
/// <summary>
/// Enqueues a void action for sequential execution.
/// </summary>
public Task RunAsync(Action action, CancellationToken token = default)
{
return RunAsync<object>(() =>
{
action();
return null;
}, token);
}
/// <summary>
/// Main worker loop processing queued actions.
/// </summary>
private void WorkerLoop()
{
Logger.Info(
"[SLOT-WORKER] THREAD_STARTED Worker={0}, WorkerThread={1}",
Name,
Thread.CurrentThread.ManagedThreadId);
try
{
foreach (var item in queue.GetConsumingEnumerable())
{
if (item == null)
continue;
item();
}
}
catch (ObjectDisposedException)
{
Logger.Debug(
"[SLOT-WORKER] QUEUE_DISPOSED Worker={0}, WorkerThread={1}",
Name,
Thread.CurrentThread.ManagedThreadId);
}
catch (Exception ex)
{
Logger.Error(
ex,
"[SLOT-WORKER] THREAD_FAILED Worker={0}, WorkerThread={1}, Error={2}",
Name,
Thread.CurrentThread.ManagedThreadId,
ex.Message);
}
finally
{
Logger.Info(
"[SLOT-WORKER] THREAD_STOPPED Worker={0}, WorkerThread={1}",
Name,
Thread.CurrentThread.ManagedThreadId);
}
}
/// <summary>
/// Stops the worker and releases resources.
/// </summary>
public void Dispose()
{
if (disposed)
return;
Logger.Info(
"[SLOT-WORKER] DISPOSE_BEGIN Worker={0}, CallerThread={1}, WorkerThread={2}, IsBusy={3}, CurrentOperation={4}, QueueLength={5}",
Name,
Thread.CurrentThread.ManagedThreadId,
thread.ManagedThreadId,
IsBusy,
CurrentOperation ?? "<none>",
QueueLength);
disposed = true;
queue.CompleteAdding();
if (Thread.CurrentThread != thread && thread.IsAlive)
{
bool stopped = thread.Join(1000);
if (!stopped)
{
Logger.Warn(
"[SLOT-WORKER] DISPOSE_WAIT_TIMEOUT Worker={0}, WorkerThread={1}, CurrentOperation={2}, QueueLength={3}",
Name,
thread.ManagedThreadId,
CurrentOperation ?? "<none>",
QueueLength);
}
}
queue.Dispose();
Logger.Info(
"[SLOT-WORKER] DISPOSE_END Worker={0}, CallerThread={1}, WorkerThread={2}, ThreadAlive={3}",
Name,
Thread.CurrentThread.ManagedThreadId,
thread.ManagedThreadId,
thread.IsAlive);
}
}
}
@@ -0,0 +1,150 @@
using System;
using System.Threading.Tasks;
namespace GenesisCordonelInterface.Core.Threading
{
public sealed class RetryResult<T>
{
public T Result { get; set; }
public bool Success { get; set; }
public int Attempts { get; set; }
public TimeSpan Duration { get; set; }
public bool TimedOut { get; set; }
public override string ToString()
{
return string.Format(
"Success={0}, Attempts={1}, Duration={2} ms, TimedOut={3}, Result={4}",
Success,
Attempts,
(int)Duration.TotalMilliseconds,
TimedOut,
Result == null ? "<null>" : Result.ToString());
}
}
public static class RetryWorker
{
/// <summary>
/// Executes an asynchronous operation with retry and timeout protection.
///
/// Important:
/// The timeout here protects the caller from waiting forever, but it does not forcibly abort
/// the underlying hardware operation. Because hardware/COM calls may continue running after
/// the timeout signal, this method waits for the original action to finish before starting
/// the next retry. This prevents overlapping COM requests on the same device.
/// </summary>
public static async Task<RetryResult<T>> RunWithRetryAsync<T>(
Func<Task<T>> action,
Func<T, bool> isSuccess,
Action<string> log,
Action<string, T> logResult,
string operationName,
int maxAttempts = 3,
int delayMs = 500,
int timeoutMs = 30000)
{
var started = DateTime.Now;
T lastResult = default(T);
bool timedOut = false;
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
Task<T> actionTask = null;
try
{
log?.Invoke($"{operationName} started. Attempt {attempt}/{maxAttempts}");
// Start real operation, for example Connect/GetPcbId/Login.
actionTask = action();
// Start independent timeout timer.
var timeoutTask = Task.Delay(timeoutMs);
// Wait until either operation completes or timeout expires.
var completedTask = await Task.WhenAny(actionTask, timeoutTask)
.ConfigureAwait(false);
if (completedTask == timeoutTask)
{
timedOut = true;
log?.Invoke(
$"{operationName} timed out. Attempt {attempt}/{maxAttempts}. " +
"Waiting for the running operation to finish before retry.");
// Critical part:
// Do NOT immediately start another retry.
// The hardware operation may still be active in ApiWorker.
// Starting another attempt immediately could corrupt communication.
try
{
lastResult = await actionTask.ConfigureAwait(false);
}
catch (Exception ex)
{
log?.Invoke(
$"{operationName} finished after timeout with exception: " +
$"{ex.GetType().Name}: {ex.Message}");
}
}
else
{
// Operation completed before timeout.
lastResult = await actionTask.ConfigureAwait(false);
}
// Decide whether the returned result means success.
if (lastResult != null && isSuccess(lastResult))
{
return new RetryResult<T>
{
Result = lastResult,
Success = true,
Attempts = attempt,
Duration = DateTime.Now - started,
TimedOut = timedOut
};
}
logResult?.Invoke(
$"{operationName} failed. Attempt {attempt}/{maxAttempts}",
lastResult);
}
catch (Exception ex)
{
// Covers exceptions thrown before timeout handling or by action startup.
log?.Invoke(
$"{operationName} exception on attempt {attempt}/{maxAttempts}: " +
$"{ex.GetType().Name}: {ex.Message}");
}
// Delay before next retry, except after the final attempt.
if (attempt < maxAttempts)
{
log?.Invoke($"{operationName} waiting {delayMs} ms before retry.");
await Task.Delay(delayMs).ConfigureAwait(false);
}
}
return new RetryResult<T>
{
Result = lastResult,
Success = false,
Attempts = maxAttempts,
Duration = DateTime.Now - started,
TimedOut = timedOut
};
}
public static void EnsureSuccess<T>(
RetryResult<T> retryResult,
string operationName)
{
if (!retryResult.Success)
throw new Exception($"{operationName} failed after {retryResult.Attempts} attempts.");
}
}
}
@@ -0,0 +1,334 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{C955D8AC-76B8-42D8-A83F-8AEB56CF2567}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>GenesisCordonelInterface</RootNamespace>
<AssemblyName>GenesisCordonelInterface</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<LangVersion>7.3</LangVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Common">
<HintPath>..\packages\Common\Common.dll</HintPath>
</Reference>
<Reference Include="Logic.ProductionToProductMapper">
<HintPath>..\packages\Common\Logic.ProductionToProductMapper.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="NLog, Version=5.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.5.2.2\lib\net46\NLog.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Deployment" />
<Reference Include="System.Drawing" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="Xylem.Common.CommonCore">
<HintPath>..\packages\Common\Xylem.Common.CommonCore.dll</HintPath>
</Reference>
<Reference Include="Xylem.Common.CommonCore.Configuration">
<HintPath>..\packages\Common\Xylem.Common.CommonCore.Configuration.dll</HintPath>
</Reference>
<Reference Include="Xylem.Common.CommonCore.ThreadWatcher">
<HintPath>..\packages\Common\Xylem.Common.CommonCore.ThreadWatcher.dll</HintPath>
</Reference>
<Reference Include="Xylem.Common.Hardware.Interfaces.Ports.PortCore">
<HintPath>..\packages\Common\Xylem.Common.Hardware.Interfaces.Ports.PortCore.dll</HintPath>
</Reference>
<Reference Include="Xylem.Common.Hardware.Interfaces.Ports.SerialPorts">
<HintPath>..\packages\Common\Xylem.Common.Hardware.Interfaces.Ports.SerialPorts.dll</HintPath>
</Reference>
<Reference Include="Xylem.Common.Hardware.WaterMeter.Genesis.Applications">
<HintPath>..\packages\Common\Xylem.Common.Hardware.WaterMeter.Genesis.Applications.dll</HintPath>
</Reference>
<Reference Include="Xylem.Common.Hardware.WaterMeter.Genesis.GenesisConfig">
<HintPath>..\packages\Common\Xylem.Common.Hardware.WaterMeter.Genesis.GenesisConfig.dll</HintPath>
</Reference>
<Reference Include="Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore">
<HintPath>..\packages\Common\Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.dll</HintPath>
</Reference>
<Reference Include="Xylem.Common.Hardware.WaterMeter.Genesis.Registers">
<HintPath>..\packages\Common\Xylem.Common.Hardware.WaterMeter.Genesis.Registers.dll</HintPath>
</Reference>
<Reference Include="Xylem.Common.Hardware.WaterMeter.MagFlux.Protocols.RequestProtocol">
<HintPath>..\packages\Common\Xylem.Common.Hardware.WaterMeter.MagFlux.Protocols.RequestProtocol.dll</HintPath>
</Reference>
<Reference Include="Xylem.Common.Hardware.WaterMeter.WaterMeterCore">
<HintPath>..\packages\Common\Xylem.Common.Hardware.WaterMeter.WaterMeterCore.dll</HintPath>
</Reference>
<Reference Include="Xylem.Common.Hardware.WaterMeter.WaterMeterRegisters">
<HintPath>..\packages\Common\Xylem.Common.Hardware.WaterMeter.WaterMeterRegisters.dll</HintPath>
</Reference>
<Reference Include="Xylem.Common.Logic.ProductionOrderCore">
<HintPath>..\packages\Common\Xylem.Common.Logic.ProductionOrderCore.dll</HintPath>
</Reference>
<Reference Include="Xylem.Common.Logic.SoftwareAccessHelper">
<HintPath>..\packages\Common\Xylem.Common.Logic.SoftwareAccessHelper.dll</HintPath>
</Reference>
<Reference Include="Xylem.Common.Utils.ByteArrayStyle">
<HintPath>..\packages\Common\Xylem.Common.Utils.ByteArrayStyle.dll</HintPath>
</Reference>
<Reference Include="Xylem.Common.Utils.Crc16Ccitt">
<HintPath>..\packages\Common\Xylem.Common.Utils.Crc16Ccitt.dll</HintPath>
</Reference>
<Reference Include="Xylem.Common.Utils.Logging">
<HintPath>..\packages\Common\Xylem.Common.Utils.Logging.dll</HintPath>
</Reference>
<Reference Include="Xylem.Common.Utils.ProcessExec">
<HintPath>..\packages\Common\Xylem.Common.Utils.ProcessExec.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="API\ModelsMapping.cs" />
<Compile Include="API\InterfaceOutsideToGCI.cs" />
<Compile Include="API\InterfaceGCIToLaatzen.cs" />
<Compile Include="API\PublicModels.cs" />
<Compile Include="Core\Config\GciConfig.cs" />
<Compile Include="Core\Config\GciConfigLoader.cs" />
<Compile Include="Core\DataStorage\Config\GciDataStorageConfig.cs" />
<Compile Include="Core\DataStorage\Config\GciDatabaseWriterConfig.cs" />
<Compile Include="Core\DataStorage\Writing\CalibrationResultWriter.cs" />
<Compile Include="API\Enums.cs" />
<Compile Include="Core\DataStorage\Reading\Common\Providers\CsvDataStorageReader.cs" />
<Compile Include="Core\DataStorage\Reading\Common\Providers\DatabaseDataStorageReader.cs" />
<Compile Include="Core\DataStorage\Reading\Common\Models\DatabaseSearchResult.cs" />
<Compile Include="Core\DataStorage\Reading\Common\Models\DataQuery.cs" />
<Compile Include="Core\DataStorage\Reading\Common\Models\DataStorageConfig.cs" />
<Compile Include="Core\DataStorage\Reading\Common\DataStorageReaderFactory.cs" />
<Compile Include="Core\DataStorage\Reading\Common\Models\DataStorageType.cs" />
<Compile Include="Core\DataStorage\Reading\Common\Contracts\IDataStorageReader..cs" />
<Compile Include="Core\DataStorage\Reading\Common\Providers\JsonDataStorageReader.cs" />
<Compile Include="Core\DataStorage\Reading\Common\Models\ReaderDiagnosticResult.cs" />
<Compile Include="Core\DataStorage\Reading\Common\Providers\RestApiDataStorageReader.cs" />
<Compile Include="Core\DataStorage\Reading\Common\Searching\SearchOrderDefinition.cs" />
<Compile Include="Core\DataStorage\Reading\Common\Searching\SearchOrderParser.cs" />
<Compile Include="Core\DataStorage\Reading\Implementation\MeterLoginPasswords\IMeterLoginPasswordReader.cs" />
<Compile Include="Core\DataStorage\Reading\Implementation\MeterLoginPasswords\MeterLoginPasswordReader.cs" />
<Compile Include="Core\DataStorage\Reading\Implementation\PreAdjustmentCalibrationParams\IPreAdjustmentCalibrationParamsReader.cs" />
<Compile Include="Core\DataStorage\Reading\Implementation\PreAdjustmentCalibrationParams\PreAdjustmentCalibrationParamsReader.cs" />
<Compile Include="Core\Engine\Engine.cs" />
<Compile Include="Core\Logging\UiLogBus.cs" />
<Compile Include="Core\Logging\UiTarget.cs" />
<Compile Include="Core\Threading\ApiWorker\ApiWorker.cs" />
<Compile Include="Core\Threading\RetryWorker\RetryWorker.cs" />
<Compile Include="UI\Debug\MeterBatchConfigPanel.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="UI\Debug\MeterBatchConfigPanel.Designer.cs">
<DependentUpon>MeterBatchConfigPanel.cs</DependentUpon>
</Compile>
<Compile Include="UI\Debug\WorkerDebugPanel.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="UI\Debug\WorkerDebugPanel.Designer.cs">
<DependentUpon>WorkerDebugPanel.cs</DependentUpon>
</Compile>
<Compile Include="UI\Grid\MeterGridColumnConfig.cs" />
<Compile Include="UI\Grid\MeterGridConfigProvider.cs" />
<Compile Include="UI\Grid\MeterGridManager.cs" />
<Compile Include="UI\Grid\MeterRowDto.cs" />
<Compile Include="UI\LaatzenAPI_GenesisToolBox\FrmConfigurations.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="UI\LaatzenAPI_GenesisToolBox\FrmConfigurations.Designer.cs">
<DependentUpon>FrmConfigurations.cs</DependentUpon>
</Compile>
<Compile Include="UI\LaatzenAPI_CordonelPreadjustmentUI\FrmCordonelPreadjustmentUI.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="UI\LaatzenAPI_CordonelPreadjustmentUI\FrmCordonelPreadjustmentUI.Designer.cs">
<DependentUpon>FrmCordonelPreadjustmentUI.cs</DependentUpon>
</Compile>
<Compile Include="UI\LaatzenAPI_GenesisToolBox\FrmRegisterStore.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="UI\LaatzenAPI_GenesisToolBox\FrmRegisterStore.Designer.cs">
<DependentUpon>FrmRegisterStore.cs</DependentUpon>
</Compile>
<Compile Include="UI\LaatzenAPI_GenesisToolBox\FrmSetup.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="UI\LaatzenAPI_GenesisToolBox\FrmSetup.Designer.cs">
<DependentUpon>FrmSetup.cs</DependentUpon>
</Compile>
<Compile Include="UI\MainForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="UI\MainForm.Designer.cs">
<DependentUpon>MainForm.cs</DependentUpon>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UI\LaatzenAPI_CordonelPreadjustmentUI\PreAdjustmentControl.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="UI\LaatzenAPI_CordonelPreadjustmentUI\PreAdjustmentControl.Designer.cs">
<DependentUpon>PreAdjustmentControl.cs</DependentUpon>
</Compile>
<Compile Include="UI\MainView.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="UI\MainView.Designer.cs">
<DependentUpon>MainView.cs</DependentUpon>
</Compile>
<Compile Include="UI\StaraTuraAPI_GenesisCordonelInterface\FrmGCIAPI.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="UI\StaraTuraAPI_GenesisCordonelInterface\FrmGCIAPI.Designer.cs">
<DependentUpon>FrmGCIAPI.cs</DependentUpon>
</Compile>
<Compile Include="UI\StaraTuraAPI_GenesisCordonelInterface\MeterInitView.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="UI\StaraTuraAPI_GenesisCordonelInterface\MeterInitView.Designer.cs">
<DependentUpon>MeterInitView.cs</DependentUpon>
</Compile>
<Compile Include="UI\StaraTuraAPI_GenesisCordonelInterface\MetersActionView.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="UI\StaraTuraAPI_GenesisCordonelInterface\MetersActionView.Designer.cs">
<DependentUpon>MetersActionView.cs</DependentUpon>
</Compile>
<Content Include="docs\images\GciBridge_component_GUI.png" />
<Content Include="docs\images\GCI__Onboarding_Overview_drawio.svg" />
<Content Include="docs\images\GCI__Onboarding_Overview_drawio__API_architecture__Current_state.svg" />
<Content Include="docs\images\GCI__Onboarding_Overview_drawio__API_architecture__Target_state.svg" />
<Content Include="docs\images\GCI__Onboarding_Overview_drawio__Data_collection_domain.svg" />
<Content Include="docs\images\GCI__Onboarding_Overview_drawio__Meter_management_domain.svg" />
<Content Include="docs\images\logo\logo.png" />
<Content Include="docs\images\logo\logo.svg" />
<Content Include="docs\images\logo\sensus-logo-white-green-rgb.png" />
<Content Include="docs\images\logo\sensus-logo-white-green-rgb.svg" />
<Content Include="docs\styles\main.css" />
<Content Include="RuntimePackage\Build\Copy.targets.xml" />
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
</Compile>
<EmbeddedResource Include="UI\LaatzenAPI_GenesisToolBox\FrmSetup.resx">
<DependentUpon>FrmSetup.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="UI\MainForm.resx">
<DependentUpon>MainForm.cs</DependentUpon>
</EmbeddedResource>
<Content Include="Core\Logging\ImportantInstructions.txt" />
<Content Include="nlog.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<EmbeddedResource Include="UI\LaatzenAPI_CordonelPreadjustmentUI\PreAdjustmentControl.resx">
<DependentUpon>PreAdjustmentControl.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="UI\StaraTuraAPI_GenesisCordonelInterface\FrmGCIAPI.resx">
<DependentUpon>FrmGCIAPI.cs</DependentUpon>
</EmbeddedResource>
<Content Include="Core\Config\gci_config.json">
<Link>Config\gci_config.json</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<None Include="Core\Config\gci_config.json" />
<None Include="docs\articles\API\index.md" />
<None Include="docs\articles\API\PublicModels.md" />
<None Include="docs\articles\API\InterfaceGCIToLaatzen.md" />
<None Include="docs\articles\API\InterfaceOutsideToGCI.md" />
<None Include="docs\docfx.json" />
<None Include="docs\index.md" />
<None Include="docs\pages\development\page_dev__current_state.md" />
<None Include="docs\pages\development\page_dev__home.md" />
<None Include="docs\pages\development\page_dev__target_state.md" />
<None Include="docs\pages\development\page_dev__migration.md" />
<None Include="docs\pages\development\page_dev__refactoring.md" />
<None Include="docs\pages\development\page_dev__target_architecture.md" />
<None Include="docs\pages\gci\page_gci__app_environment.md" />
<None Include="docs\pages\gci\page_gci__internal_architecture.md" />
<None Include="docs\pages\gci\page_gci__datastorage.md" />
<None Include="docs\pages\gci\page_gci__home.md" />
<None Include="docs\pages\gci\page_gci__interfaces.md" />
<None Include="docs\pages\gci\page_gci__runtime.md" />
<None Include="docs\pages\gci\page_gci__implementation_to_tbf.md" />
<None Include="docs\pages\gci\page_gci__implementation_universal.md" />
<None Include="docs\pages\gci\page_gci__workers.md" />
<None Include="docs\pages\platform\page_platform__configuration_domain.md" />
<None Include="docs\pages\platform\page_platform__data_collection_domain.md" />
<None Include="docs\pages\platform\page_platform__tbf_implementation.md" />
<None Include="docs\pages\platform\page_platform__current_state.md" />
<None Include="docs\pages\platform\page_platform__home.md" />
<None Include="docs\toc.yml" />
<None Include="packages.config" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<Folder Include="Config\" />
<Folder Include="Core\DataStorage\Writing\NewFolder1\" />
<Folder Include="RuntimePackage\Package\" />
</ItemGroup>
<ItemGroup>
<WCFMetadata Include="Connected Services\" />
</ItemGroup>
<ItemGroup>
<Reference Include="Xylem.Common.Ui.CordonelPreadjustmentUi">
<HintPath>..\packages\Common\Xylem.Common.Ui.CordonelPreadjustmentUi.dll</HintPath>
<Private>True</Private>
</Reference>
<ProjectReference Include="..\ExternalProjects\Laatzen\Genesis\Common\Ui\GenesisToolBox\GenesisToolBox.csproj">
<Project>{95A50087-99EE-4349-9A17-C547290DB42F}</Project>
<Name>GenesisToolBox</Name>
</ProjectReference>
</ItemGroup>
<!-- Import custom GCI runtime packaging logic -->
<Import Project="RuntimePackage\Build\Copy.targets.xml" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
+22
View File
@@ -0,0 +1,22 @@
using GenesisCordonelInterface.UI;
using NLog;
using System;
using System.Windows.Forms;
using Xylem.Common.Utils.Logging;
using System.IO;
namespace GenesisCordonelInterface
{
static class Program
{
private static readonly ILogger Logger = NLogHelper.CreateOrGetLogger("GenesisCordonelInterface");
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
}
@@ -0,0 +1,33 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("GenesisCordonelInterface")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Windows Org")]
[assembly: AssemblyProduct("GenesisCordonelInterface")]
[assembly: AssemblyCopyright("Copyright © Windows Org 2026")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("c955d8ac-76b8-42d8-a83f-8aeb56cf2567")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
@@ -0,0 +1,63 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace GenesisCordonelInterface.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GenesisCordonelInterface.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
+26
View File
@@ -0,0 +1,26 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace GenesisCordonelInterface.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.14.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More