diff --git a/.github/workflows/test-pipeline.yml b/.github/workflows/test-pipeline.yml index 59ea4a4..8f2bf9b 100644 --- a/.github/workflows/test-pipeline.yml +++ b/.github/workflows/test-pipeline.yml @@ -9,31 +9,15 @@ on: required: false type: string push: - branches: - - main - - master paths-ignore: - '**.md' permissions: write-all jobs: - - #Check the online status of the test PLCs first - check-plcs-online: - name: 'Test PLC online status' - runs-on: [self-hosted, linux, x64, womed-local-linux] - steps: - - name: 'Ping FPX-H-C30T' - run: ping 192.168.115.210 -w 5 - - name: 'Ping FPX-H-C14R' - run: ping 192.168.115.212 -w 5 - - name: 'Ping FPX-C30T' - run: ping 192.168.115.213 -w 5 #Run unit tests on the test PLCs run-unit-tests: - name: 'Run unit tests' - needs: check-plcs-online + name: 'Run tests and documentation' runs-on: [self-hosted, linux, x64, womed-local-linux] steps: - uses: actions/checkout@v3 @@ -44,58 +28,44 @@ jobs: dotnet-version: '6.0.x' - name: 'Run tests' - run: | - cd './MewtocolTests' - dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=./TestResults/coverage.opencover.xml - cd ../ + run: dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=../Builds/TestResults/coverage.opencover.xml - name: Report Generator uses: danielpalme/ReportGenerator-GitHub-Action@5.1.22 with: - reports: './MewtocolTests/TestResults/coverage.opencover.xml' - targetdir: './MewtocolTests/TestResults' + reports: './Builds/TestResults/coverage.opencover.xml' + targetdir: './Builds/TestResults' reporttypes: HtmlSummary;MarkdownSummaryGithub;Badges - historydir: './MewtocolTests/Hist' + historydir: './Builds/Hist' title: Report - name: Markdown report and copy for badges branch run: | - cat './MewtocolTests/TestResults/SummaryGithub.md' >> $GITHUB_STEP_SUMMARY - cp ./MewtocolTests/TestResults/badge_combined.svg ~/badge_combined.svg - cp ./MewtocolTests/TestResults/summary.html ~/summary.html + cat './Builds/TestResults/SummaryGithub.md' >> $GITHUB_STEP_SUMMARY + cp ./Builds/TestResults/badge_combined.svg ~/badge_combined.svg + cp ./Builds/TestResults/summary.html ~/summary.html ls -l ~ - - #Upload to codecov - # - name: Upload coverage reports to Codecov - # uses: codecov/codecov-action@v3 - # env: - # CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - #- uses: actions/upload-artifact@v3 - # with: - # name: test-results - # path: ./MewtocolTests/TestResults/ - + - name: Cache test results if: ${{ github.event_name == 'workflow_call' }} uses: actions/cache/save@v3 with: - key: ${{ inputs.cache-id }} + key: ${{ inputs.cache-id }}-${{ GITHUB_REF##*/ }} path: | - ${{ github.workspace }}/MewtocolTests/TestResults + ${{ github.workspace }}/Builds/TestResults - name: Commit badge continue-on-error: true run: | git fetch git checkout badges - cp ~/summary.html ./MewtocolTests/TestResults/summary.html - cp ~/badge_combined.svg ./MewtocolTests/TestResults/badge_combined.svg + cp ~/summary.html ./Builds/TestResults/summary_${{ GITHUB_REF##*/ }}.html + cp ~/badge_combined.svg ./Builds/TestResults/badge_combined_${{ GITHUB_REF##*/ }}.svg git config --local user.email "action@github.com" git config --local user.name "GitHub Action" - git add "./MewtocolTests/TestResults/badge_combined.svg" -f - git add "./MewtocolTests/TestResults/summary.html" -f - git commit -m "Add/Update badge" + git add "./Builds/TestResults/badge_combined_${{ GITHUB_REF##*/ }}.svg" -f + git add "./Builds/TestResults/summary_${{ GITHUB_REF##*/ }}.html" -f + git commit -m "Add/Update badge for branch ${{ GITHUB_REF##*/ }}" - name: Push badge commit uses: ad-m/github-push-action@master diff --git a/DocBuilder/DocBuilder.csproj b/DocBuilder/DocBuilder.csproj new file mode 100644 index 0000000..c2b9011 --- /dev/null +++ b/DocBuilder/DocBuilder.csproj @@ -0,0 +1,18 @@ + + + + + false + false + Exe + net6.0 + enable + enable + + + + + + + + diff --git a/DocBuilder/Docs/plctypes.md b/DocBuilder/Docs/plctypes.md new file mode 100644 index 0000000..6bd3ce1 --- /dev/null +++ b/DocBuilder/Docs/plctypes.md @@ -0,0 +1,557 @@ +# PLC Type Table +All supported PLC types for auto recognition are listed in this table. Other ones might also be supported but are shown as unknown in the library +> Discontinued PLCs
+> These are PLCs that are no longer sold by Panasonic. Marked with ⚠️ + +> EXRT PLCs
+> These are PLCs that utilize the basic `%EE#RT` and `%EE#EX00RT` command. All newer models do this. Old models only use the `%EE#RT` command. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeCapacityCodeEnumDCNTEXRTTested
📟 FP0
C10, C14, C16 2.7k 0x40FP0_2c7k__C10_C14_C16⚠️
C32, SL1 5k 0x41FP0_5k__C32_SL1⚠️
T32 10k 0x42FP0_10c0k__T32⚠️
📟 FP0H
C32T/P 32k 0xB0FP0H_32k__C32TsP
C32ET/EP 32k 0xB1FP0H_32k__C32ETsEP
📟 FP0R
C10, C14, C16 16k 0x46FP0R_16k__C10_C14_C16
C32 32k 0x47FP0R_32k__C32
T32 32k 0x48FP0R_32k__T32
F32 32k 0x49FP0R_32k__F32
📟 FP1, FP-M
C14, C16 0.9k 0x04FP1_0c9k__C14_C16_OR_FPdM_0c9k__C16T⚠️
C16T 0.9k 0x04FP1_0c9k__C14_C16_OR_FPdM_0c9k__C16T⚠️
C24, C40 2.7k 0x05FP1_2c7k__C24_C40_OR_FPdM_2c7k__C20R_C20T_C32T⚠️
C20R, C20T, C32T 2.7k 0x05FP1_2c7k__C24_C40_OR_FPdM_2c7k__C20R_C20T_C32T⚠️
C56, C72 5k 0x06FP1_5k__C56_C72_OR_FPdM_5k__C20RC_C20TC_C32TC⚠️
C20RC, C20TC, C32TC 5k 0x06FP1_5k__C56_C72_OR_FPdM_5k__C20RC_C20TC_C32TC⚠️
📟 FP10, FP10S
- 30k 0x20FP10_30k_OR_FP10_60k_OR_FP10S_30k⚠️
- 60k 0x20FP10_30k_OR_FP10_60k_OR_FP10S_30k⚠️
- 30k 0x20FP10_30k_OR_FP10_60k_OR_FP10S_30k⚠️
📟 FP10SH
- 30k 0x30FP10SH_30k_OR_FP10SH_60k_OR_FP10SH_120k⚠️
- 60k 0x30FP10SH_30k_OR_FP10SH_60k_OR_FP10SH_120k⚠️
- 120k 0x30FP10SH_30k_OR_FP10SH_60k_OR_FP10SH_120k⚠️
📟 FP2
- 16k 0x50FP2_16k_OR_FP2_32k⚠️
- 32k 0x50FP2_16k_OR_FP2_32k⚠️
📟 FP2SH
- 60k 0x60FP2SH_60k⚠️
- 32k 0x62FP2SH_32k⚠️
- 120k 0xE0FP2SH_120k⚠️
📟 FP3
- 10k 0x03FP3_10k⚠️
📟 FP3, FP-C
- 16k 0x13FP3_16k_OR_FPdC_16k⚠️
- 16k 0x13FP3_16k_OR_FPdC_16k⚠️
📟 FP5
- 16k 0x02FP5_16k⚠️
- 24k 0x12FP5_24k⚠️
📟 FP-e
- 2.7k 0x45FPde_2c7k⚠️
📟 FP-SIGMA
- 12k 0x43FPdSIGMA_12k⚠️
- 32k 0x44FPdSIGMA_32k⚠️
- 16k 0xE1FPdSIGMA_16k_OR_FPdSIGMA_40k⚠️
- 40k 0xE1FPdSIGMA_16k_OR_FPdSIGMA_40k⚠️
📟 FP-X
C14R 16k 0x70FPdX_16k__C14R⚠️
C30R, C60R 32k 0x71FPdX_32k__C30R_C60R⚠️
L14 16k 0x73FPdX_16k__L14⚠️
L30, L60 32k 0x74FPdX_32k__L30_L60⚠️
C14T/P 16k 0x76FPdX_16k__C14TsP⚠️
C30T/P, C60T/P, C38AT, C40T 32k 0x77FPdX_32k__C30TsP_C60TsP_C38AT_C40T⚠️
C40RT0A 2.5k 0x7AFPdX_2c5k__C40RT0A⚠️
📟 FP-X0
L14, L30 2.5k 0x72FPdX0_2c5k__L14_L30⚠️
L40, L60 8k 0x75FPdX0_8k__L40_L60⚠️
L40, L60 16k 0x7FFPdX0_16k__L40_L60⚠️
📟 FP-XH
C14R 16k 0xA0FPdXH_16k__C14R
C30R, C40R, C60R 32k 0xA1FPdXH_32k__C30R_C40R_C60R
C14T/P 16k 0xA4FPdXH_16k__C14TsP
C30T/P, C40T, C60T/P 32k 0xA5FPdXH_32k__C30TsP_C40T_C60TsP
C38AT 32k 0xA7FPdXH_32k__C38AT
M4T/L 32k 0xA8FPdXH_32k__M4TsL
M8N16T/P 32k 0xACFPdXH_32k__M8N16TsP
M8N30T 32k 0xADFPdXH_32k__M8N30T
C40ET, C60ET 32k 0xAEFPdXH_32k__C40ET_C60ET
C60ETF 32k 0xAFFPdXH_32k__C60ETF
+ + diff --git a/DocBuilder/Docs/reverse_engineering_data.md b/DocBuilder/Docs/reverse_engineering_data.md new file mode 100644 index 0000000..0bb6a8c --- /dev/null +++ b/DocBuilder/Docs/reverse_engineering_data.md @@ -0,0 +1,97 @@ +# Open Questions + +- What is the HW information byte for? +- What are the last bytes of the EXRT message for? + +# Actual Readouts + +| PLC TYPE | CPU Code | Machine Code | HW Information| +|--------------|--------------|--------------|---------------| +| FPX C14 R | 20 | 70 | 02 | +| FPX C30 T | 20 | 77 | 02 | +| FPX-H C14 R | 20 | A0 | 01 | +| FPX-H C30 T | 20 | A5 | 01 | + +## FPX 16k C14R Actual Response Examples + +### %EE$RT + +|Reponse Byte|Description| +|------------|-----------| +| 20 | Model code | +| 25 | Version | +| 16 | Prog capacity | +| 80 | Op mode | +| 00 | Link unit | +| E1 | Error flag | +| 2D00 | Self diag error | + +### %EE$EX00RT Normal Operation + +|Reponse Byte|Description| +|------------|-----------| +| 00 | Extended mode +| 32 | Data item count +| 70 | Machine type +| 00 | Version (Fixed to 00) +| 16 | Prog capacity in K +| 80 | Operation mode / status +| 00 | Link unit +| E1 | Error flag +| 2D00 | Self diag error +| 50 | Version +| 02 | Hardware information +| 0 | Number of programs +| 4100 | Program size BCD +| 1600 | Header size (no. of words) bcd +| 1604 | System register size +| 96230000001480004 | ? + +### %EE$EX00RT with error + +|Reponse Byte|Description| +|------------|-----------| +| 00 | Extended mode +| 32 | Data item count +| 70 | Machine type +| 00 | Version (Fixed to 00) +| 16 | Prog capacity in K +| 81 | Operation mode / status +| 00 | Link unit +| 60 | Error flag +| 0000 | Self diag error +| 50 | Version +| 02 | Hardware information +| 0 | Number of programs +| 4100 | Program size BCD +| 1600 | Header size (no. of words) bcd +| 1604 | System register size +| 96230000001480004 | ? + +What are the last bytes? + +FP-X 16k C14R +96 23 00 00 00 14 80 00 4 + +FP-X 32k C30T/P +96 23 00 00 00 14 80 00 4 + +FP-XH 32k C30T/P +96 23 00 00 00 40 00 00 4 + +FP-XH 16k C14R +96 23 00 00 00 40 00 00 4 + +## FP-XH 16k C14R Actual Response Examples + +### %EE$RT + +|Reponse Byte|Description| +|------------|-----------| +| 20 | Model code | +| 16 | Version | +| 16 | Prog capacity | +| 81 | Op mode | +| 00 | Link unit | +| 60 | Error flag | +| 0000 | Self diag error | \ No newline at end of file diff --git a/DocBuilder/Program.cs b/DocBuilder/Program.cs new file mode 100644 index 0000000..d2dddfb --- /dev/null +++ b/DocBuilder/Program.cs @@ -0,0 +1,140 @@ +//This program builds Markdown and docs for all kinds of data + +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Reflection; +using System.Reflection.PortableExecutable; +using System.Text; +using MewtocolNet; +using MewtocolNet.DocAttributes; + +Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; +Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + +Console.WriteLine("Building docs for PLC types..."); + +var entryLoc = Assembly.GetEntryAssembly(); +ArgumentNullException.ThrowIfNull(entryLoc); + +string filePath = Path.Combine(entryLoc.Location, @"..\..\..\..\Docs\plctypes.md"); +Console.WriteLine($"{filePath}"); + +StringBuilder markdownBuilder = new StringBuilder(); + +var plcs = Enum.GetValues(typeof(PlcType)).Cast().OrderBy(x => x.ToString()); + +void WritePlcTypeTable(IEnumerable vals) { + + var groups = vals.GroupBy(x => x.ToNameDecompose()[0].Group) + .SelectMany(grouping => grouping.OrderBy(b => (int)b)) + .GroupBy( + x => string.Join(", ", + x.ToNameDecompose() + .DistinctBy(y => y.Group) + .Select(y => y.Group)) + ); + + markdownBuilder.AppendLine(""); + + bool isFirstIt = true; + + foreach (var group in groups) { + + group.OrderBy(x => (int)x); + + bool isFirstGroup = true; + + foreach (var enu in group) { + + ParsedPlcName[] decomposed = null!; + string cpuOrMachCode = null!; + + decomposed = enu.ToNameDecompose(); + + cpuOrMachCode = ((int)enu).ToString("X2"); + ArgumentNullException.ThrowIfNull(decomposed); + + //first iteration + if (isFirstIt) { + + markdownBuilder.AppendLine(""); + + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + + markdownBuilder.AppendLine(""); + + isFirstIt = false; + + } + + if(isFirstGroup) { + + markdownBuilder.AppendLine(""); + + markdownBuilder.AppendLine($""); + + markdownBuilder.AppendLine(""); + + isFirstGroup = false; + + } + + foreach (var decomp in decomposed) { + + markdownBuilder.AppendLine(""); + + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + + if(enu.IsDiscontinued()) { + + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + + } else { + + markdownBuilder.AppendLine($""); + + } + + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + + markdownBuilder.AppendLine(""); + + } + + + } + + isFirstIt = false; + + } + + markdownBuilder.AppendLine("
TypeCapacityCodeEnumDCNTEXRTTested
📟 {group.Key}
{(decomp.SubTypes.Length == 0 ? "-" : string.Join(", ", decomp.SubTypes))} {decomp.Size}k 0x{cpuOrMachCode}{enu.ToString()}⚠️{enu.ToString()} {(enu.IsEXRTPLC() ? "✅" : "❌")} {(enu.WasTestedLive() ? "✅" : "❌")}
"); + markdownBuilder.AppendLine("\n"); + +} + +markdownBuilder.AppendLine($"# PLC Type Table"); +markdownBuilder.AppendLine($"All supported PLC types for auto recognition are listed in this table. " + + $"Other ones might also be supported but are shown as unknown in the library"); + +markdownBuilder.AppendLine($"> Discontinued PLCs
"); +markdownBuilder.AppendLine($"> These are PLCs that are no longer sold by Panasonic. Marked with ⚠️\n"); + +markdownBuilder.AppendLine($"> EXRT PLCs
"); +markdownBuilder.AppendLine($"> These are PLCs that utilize the basic `%EE#RT` and `%EE#EX00RT` command. All newer models do this. Old models only use the `%EE#RT` command.\n"); + +WritePlcTypeTable(plcs); + + +File.WriteAllText(filePath, markdownBuilder.ToString()); \ No newline at end of file diff --git a/Examples/ExampleScenarios.cs b/Examples/ExampleScenarios.cs deleted file mode 100644 index a0ccdf9..0000000 --- a/Examples/ExampleScenarios.cs +++ /dev/null @@ -1,183 +0,0 @@ -using MewtocolNet.Logging; -using MewtocolNet; -using System; -using System.Reflection; -using System.Threading.Tasks; -using System.Collections; - -namespace Examples; - -public class ExampleScenarios { - - public static bool MewtocolLoggerEnabled = false; - - public void SetupLogger () { - - //attaching the logger - Logger.LogLevel = LogLevel.Verbose; - Logger.OnNewLogMessage((date, msg) => { - if(MewtocolLoggerEnabled) - Console.WriteLine($"{date.ToString("HH:mm:ss")} {msg}"); - }); - - } - - [Scenario("Permament connection with poller")] - public async Task RunCyclicPollerAsync () { - - Console.WriteLine("Starting poller scenario"); - - int runTime = 10000; - int remainingTime = runTime; - - //setting up a new PLC interface and register collection - MewtocolInterface interf = new MewtocolInterface("192.168.115.210"); - TestRegisters registers = new TestRegisters(); - - //attaching the register collection and an automatic poller - interf.WithRegisterCollection(registers).WithPoller(); - - await interf.ConnectAsync(); - - _ = Task.Factory.StartNew(async () => { - - while (interf.IsConnected) { - - //flip the bool register each tick and wait for it to be registered - await interf.SetRegisterAsync(nameof(registers.TestBool1), !registers.TestBool1); - - Console.Title = $"Polling Paused: {interf.PollingPaused}, " + - $"Poller active: {interf.PollerActive}, " + - $"Speed UP: {interf.BytesPerSecondUpstream} B/s, " + - $"Speed DOWN: {interf.BytesPerSecondDownstream} B/s, " + - $"Poll delay: {interf.PollerDelayMs} ms, " + - $"Queued MSGs: {interf.QueuedMessages}"; - - Console.Clear(); - Console.WriteLine("Underlying registers on tick: \n"); - - foreach (var register in interf.Registers) { - - Console.WriteLine($"{register.GetCombinedName()} / {register.GetRegisterPLCName()} - Value: {register.GetValueString()}"); - - } - - Console.WriteLine($"{registers.TestBool1}"); - Console.WriteLine($"{registers.TestDuplicate}"); - - remainingTime -= 1000; - - Console.WriteLine($"\nStopping in: {remainingTime}ms"); - - await Task.Delay(1000); - - } - - }); - - await Task.Delay(runTime); - interf.Disconnect(); - - } - - [Scenario("Dispose and disconnect connection")] - public async Task RunDisposalAndDisconnectAsync () { - - //automatic disposal - using (var interf = new MewtocolInterface("192.168.115.210")) { - - await interf.ConnectAsync(); - - if (interf.IsConnected) { - - Console.WriteLine("Opened connection"); - - await Task.Delay(5000); - - } - - } - - Console.WriteLine("Disposed, closed connection"); - - //manual close - var interf2 = new MewtocolInterface("192.168.115.210"); - - await interf2.ConnectAsync(); - - if (interf2.IsConnected) { - - Console.WriteLine("Opened connection"); - - await Task.Delay(5000); - - } - - interf2.Disconnect(); - - Console.WriteLine("Disconnected, closed connection"); - - } - - [Scenario("Test auto enums and bitwise, needs the example program from MewtocolNet/PLC_Test")] - public async Task RunEnumsBitwiseAsync () { - - Console.WriteLine("Starting auto enums and bitwise"); - - //setting up a new PLC interface and register collection - MewtocolInterface interf = new MewtocolInterface("192.168.115.210"); - TestRegistersEnumBitwise registers = new TestRegistersEnumBitwise(); - - //attaching the register collection and an automatic poller - interf.WithRegisterCollection(registers).WithPoller(); - - registers.PropertyChanged += (s, e) => { - - Console.Clear(); - - var props = registers.GetType().GetProperties(); - - foreach (var prop in props) { - - var val = prop.GetValue(registers); - string printVal = val?.ToString() ?? "null"; - - if (val is BitArray bitarr) { - printVal = bitarr.ToBitString(); - } - - Console.Write($"{prop.Name} - "); - - if(printVal == "True") { - Console.ForegroundColor = ConsoleColor.Green; - } - - Console.Write($"{printVal}"); - - Console.ResetColor(); - - Console.WriteLine(); - - } - - }; - - await interf.ConnectAsync(); - - //use the async method to make sure the cycling is stopped - await interf.SetRegisterAsync(nameof(registers.StartCyclePLC), false); - - await Task.Delay(5000); - - //set the register without waiting for it async - registers.StartCyclePLC = true; - - await Task.Delay(5000); - - registers.StartCyclePLC = false; - - await Task.Delay(2000); - - } - -} diff --git a/Examples/Examples.csproj b/Examples/Examples.csproj deleted file mode 100644 index b9f9fac..0000000 --- a/Examples/Examples.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - Exe - net6.0 - - - diff --git a/Examples/Program.cs b/Examples/Program.cs deleted file mode 100644 index c3bd799..0000000 --- a/Examples/Program.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; - -namespace Examples; - -class Program { - - static ExampleScenarios ExampleSzenarios = new ExampleScenarios(); - - static void Main(string[] args) { - - AppDomain.CurrentDomain.UnhandledException += (s,e) => { - Console.WriteLine(e.ExceptionObject.ToString()); - }; - - TaskScheduler.UnobservedTaskException += (s,e) => { - Console.WriteLine(e.Exception.ToString()); - }; - - ExampleSzenarios.SetupLogger(); - - LoopInput(); - - } - - private static void LoopInput () { - - Console.WriteLine("All available scenarios\n"); - - var methods = ExampleSzenarios.GetType().GetMethods(); - var invokeableMethods = new List(); - - for (int i = 0, j = 0; i < methods.Length; i++) { - - MethodInfo method = methods[i]; - var foundAtt = method.GetCustomAttribute(typeof(ScenarioAttribute)); - - if(foundAtt != null && foundAtt is ScenarioAttribute att) { - - Console.WriteLine($"[{j + 1}] {method.Name}() - {att.Description}"); - invokeableMethods.Add(method); - - j++; - - } - - } - - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("\nEnter a number to excecute a example"); - Console.ResetColor(); - - Console.WriteLine("\nOther possible commands: \n\n" + - "'toggle logger' - toggle the built in mewtocol logger on/off\n" + - "'exit' - to close this program \n" + - "'clear' - to clear the output \n"); - - Console.Write("> "); - - var line = Console.ReadLine(); - - if (line == "toggle logger") { - - ExampleScenarios.MewtocolLoggerEnabled = !ExampleScenarios.MewtocolLoggerEnabled; - - Console.WriteLine(ExampleScenarios.MewtocolLoggerEnabled ? "Logger enabled" : "Logger disabled"); - - } else if (line == "exit") { - - Environment.Exit(0); - - } else if (line == "clear") { - - Console.Clear(); - - } else if (int.TryParse(line, out var lineNum)) { - - var index = Math.Clamp(lineNum - 1, 0, invokeableMethods.Count - 1); - - var task = (Task)invokeableMethods.ElementAt(index).Invoke(ExampleSzenarios, null); - - task.Wait(); - - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("The program ran to completition"); - Console.ResetColor(); - - } else { - - Console.WriteLine("Wrong input"); - - } - - LoopInput(); - - } - -} diff --git a/Examples/ScenarioAttribute.cs b/Examples/ScenarioAttribute.cs deleted file mode 100644 index 5a869c2..0000000 --- a/Examples/ScenarioAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Examples; - -public class ScenarioAttribute : Attribute { - - public string Description { get; private set; } - - public ScenarioAttribute(string description) { - - Description = description; - - } - -} \ No newline at end of file diff --git a/Examples/TestRegisters.cs b/Examples/TestRegisters.cs deleted file mode 100644 index dfa77cc..0000000 --- a/Examples/TestRegisters.cs +++ /dev/null @@ -1,80 +0,0 @@ -using MewtocolNet; -using MewtocolNet.RegisterAttributes; -using System; -using System.Collections; - -namespace Examples { - public class TestRegisters : RegisterCollectionBase { - - //corresponds to a R100 boolean register in the PLC - [Register(1000, RegisterType.R)] - public bool TestBool1 { get; private set; } - - private int testDuplicate; - - [Register(1000)] - public int TestDuplicate { - get => testDuplicate; - set => AutoSetter(value, ref testDuplicate); - } - - //corresponds to a XD input of the PLC - [Register(RegisterType.X, SpecialAddress.D)] - public bool TestBoolInputXD { get; private set; } - - //corresponds to a DT1101 - DT1104 string register in the PLC with (STRING[4]) - //[Register(1101, 4)] - //public string TestString1 { get; private set; } - - //corresponds to a DT7000 16 bit int register in the PLC - [Register(899)] - public short TestInt16 { get; private set; } - - //corresponds to a DTD7001 - DTD7002 32 bit int register in the PLC - [Register(7001)] - public int TestInt32 { get; private set; } - - //corresponds to a DTD7001 - DTD7002 32 bit float register in the PLC (REAL) - [Register(7003)] - public float TestFloat32 { get; private set; } - - //corresponds to a DT7005 - DT7009 string register in the PLC with (STRING[5]) - [Register(7005, 5)] - public string TestString2 { get; private set; } - - //corresponds to a DT7010 as a 16bit word/int and parses the word as single bits - [Register(7010)] - public BitArray TestBitRegister { get; private set; } - - //corresponds to a DT1204 as a 16bit word/int takes the bit at index 9 and writes it back as a boolean - [Register(1204, 9, BitCount.B16)] - public bool BitValue { get; private set; } - - [Register(1204, 5, BitCount.B16)] - public bool FillTest { get; private set; } - - //corresponds to a DT7012 - DT7013 as a 32bit time value that gets parsed as a timespan (TIME) - //the smallest value to communicate to the PLC is 10ms - [Register(7012)] - public TimeSpan TestTime { get; private set; } - - public enum CurrentState { - Undefined = 0, - State1 = 1, - State2 = 2, - //State3 = 3, - State4 = 4, - State5 = 5, - StateBetween = 100, - State6 = 6, - State7 = 7, - } - - [Register(50)] - public CurrentState TestEnum { get; private set; } - - [Register(100)] - public TimeSpan TsTest2 { get; private set; } - - } -} diff --git a/Examples/TestRegistersEnumBitwise.cs b/Examples/TestRegistersEnumBitwise.cs deleted file mode 100644 index 2a24825..0000000 --- a/Examples/TestRegistersEnumBitwise.cs +++ /dev/null @@ -1,106 +0,0 @@ -using MewtocolNet; -using MewtocolNet.RegisterAttributes; -using System; -using System.Collections; - -namespace Examples { - - public class TestRegistersEnumBitwise : RegisterCollectionBase { - - private bool startCyclePLC; - - [Register(50, RegisterType.R)] - public bool StartCyclePLC { - get => startCyclePLC; - set => AutoSetter(value, ref startCyclePLC); - } - - //the enum you want to read out - public enum CurrentState { - - Undefined = 0, - State1 = 1, - State2 = 2, - //If you leave an enum empty it still works - //State3 = 3, - State4 = 4, - State5 = 5, - State6 = 6, - State7 = 7, - State8 = 8, - State9 = 9, - State10 = 10, - State11 = 11, - State12 = 12, - State13 = 13, - State14 = 14, - State15 = 15, - - } - - //automatically convert the short (PLC int) register to an enum - [Register(500)] - public CurrentState TestEnum16 { get; private set; } - - //also works for 32bit registers - [Register(501, BitCount.B32)] - public CurrentState TestEnum32 { get; private set; } - - //get the whole bit array from DT503 - - [Register(503)] - public BitArray TestBitRegister16 { get; private set; } - - //you can also extract single bits from DT503 - - [Register(503, 0, BitCount.B16)] - public bool BitValue0 { get; private set; } - - [Register(503, 1, BitCount.B16)] - public bool BitValue1 { get; private set; } - - [Register(503, 2, BitCount.B16)] - public bool BitValue2 { get; private set; } - - [Register(503, 3, BitCount.B16)] - public bool BitValue3 { get; private set; } - - [Register(503, 4, BitCount.B16)] - public bool BitValue4 { get; private set; } - - [Register(503, 5, BitCount.B16)] - public bool BitValue5 { get; private set; } - - [Register(503, 6, BitCount.B16)] - public bool BitValue6 { get; private set; } - - [Register(503, 7, BitCount.B16)] - public bool BitValue7 { get; private set; } - - [Register(503, 8, BitCount.B16)] - public bool BitValue8 { get; private set; } - - [Register(503, 9, BitCount.B16)] - public bool BitValue9 { get; private set; } - - [Register(503, 10, BitCount.B16)] - public bool BitValue10 { get; private set; } - - [Register(503, 11, BitCount.B16)] - public bool BitValue11 { get; private set; } - - [Register(503, 12, BitCount.B16)] - public bool BitValue12 { get; private set; } - - [Register(503, 13, BitCount.B16)] - public bool BitValue13 { get; private set; } - - [Register(503, 14, BitCount.B16)] - public bool BitValue14 { get; private set; } - - [Register(503, 15, BitCount.B16)] - public bool BitValue15 { get; private set; } - - } - -} diff --git a/MewExplorer/App.xaml b/MewExplorer/App.xaml new file mode 100644 index 0000000..eba1b07 --- /dev/null +++ b/MewExplorer/App.xaml @@ -0,0 +1,26 @@ + + + + + + #512bdf + White + + + + + + + + \ No newline at end of file diff --git a/MewExplorer/App.xaml.cs b/MewExplorer/App.xaml.cs new file mode 100644 index 0000000..42f7b60 --- /dev/null +++ b/MewExplorer/App.xaml.cs @@ -0,0 +1,9 @@ +namespace MewExplorer; + +public partial class App : Application { + public App() { + InitializeComponent(); + + MainPage = new MainPage(); + } +} diff --git a/MewExplorer/Main.razor b/MewExplorer/Main.razor new file mode 100644 index 0000000..8d4cb6f --- /dev/null +++ b/MewExplorer/Main.razor @@ -0,0 +1,11 @@ + + + + + + + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/MewExplorer/MainPage.xaml b/MewExplorer/MainPage.xaml new file mode 100644 index 0000000..fc365d0 --- /dev/null +++ b/MewExplorer/MainPage.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/MewExplorer/MainPage.xaml.cs b/MewExplorer/MainPage.xaml.cs new file mode 100644 index 0000000..0aa8fd8 --- /dev/null +++ b/MewExplorer/MainPage.xaml.cs @@ -0,0 +1,7 @@ +namespace MewExplorer; + +public partial class MainPage : ContentPage { + public MainPage() { + InitializeComponent(); + } +} diff --git a/MewExplorer/MauiProgram.cs b/MewExplorer/MauiProgram.cs new file mode 100644 index 0000000..2d13104 --- /dev/null +++ b/MewExplorer/MauiProgram.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Logging; + +namespace MewExplorer; +public static class MauiProgram { + public static MauiApp CreateMauiApp() { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .ConfigureFonts(fonts => { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + }); + + builder.Services.AddMauiBlazorWebView(); + + #if DEBUG + builder.Services.AddBlazorWebViewDeveloperTools(); + builder.Logging.AddDebug(); + #endif + + return builder.Build(); + } +} diff --git a/MewExplorer/MewExplorer.csproj b/MewExplorer/MewExplorer.csproj new file mode 100644 index 0000000..a21d09b --- /dev/null +++ b/MewExplorer/MewExplorer.csproj @@ -0,0 +1,55 @@ + + + + + false + + net7.0-android; + $(TargetFrameworks);net7.0-windows10.0.19041.0 + Exe + MewExplorer + true + true + enable + false + + + MewExplorer + + + com.companyname.mewexplorer + 5DD46388-985C-4BE2-94EF-3BFE6B5A280B + + + 1.0 + 1 + + 24.0 + 10.0.17763.0 + 10.0.17763.0 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MewExplorer/Pages/Index.razor b/MewExplorer/Pages/Index.razor new file mode 100644 index 0000000..a351049 --- /dev/null +++ b/MewExplorer/Pages/Index.razor @@ -0,0 +1,31 @@ +@page "/" + +
+ +
+ +
+ + Plc Overview + FPX-H C30T + +
+ 192.168.0.1 +
+ +
+ +
+ + Plc Overview + Text + +
+ +
+ +
+ +
+ +
\ No newline at end of file diff --git a/MewExplorer/Pages/Index.razor.css b/MewExplorer/Pages/Index.razor.css new file mode 100644 index 0000000..3a4f079 --- /dev/null +++ b/MewExplorer/Pages/Index.razor.css @@ -0,0 +1,48 @@ +.index-main { + + display: flex; + flex-direction: column; + flex: 1; + align-items: stretch; + justify-content: stretch; + height: 100%; + +} + +.plc-header { + + background: var(--dark-2); + display: flex; + flex-direction: row; + flex-basis: 30%; + + color: white; + +} + +.plc-header-left { + display: flex; + flex-direction: column; + flex-basis: 50%; + padding: 1em; +} + +.plc-header-left > span:nth-child(1) { + + font-size: 1em; + +} + +.plc-header-left > span:nth-child(2) { + + font-size: 2em; + +} + +.plc-header-right { + + display: flex; + flex-direction: column; + padding: 1em; + +} diff --git a/MewExplorer/Platforms/Android/AndroidManifest.xml b/MewExplorer/Platforms/Android/AndroidManifest.xml new file mode 100644 index 0000000..dbf9e7e --- /dev/null +++ b/MewExplorer/Platforms/Android/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/MewExplorer/Platforms/Android/MainActivity.cs b/MewExplorer/Platforms/Android/MainActivity.cs new file mode 100644 index 0000000..7e7bca8 --- /dev/null +++ b/MewExplorer/Platforms/Android/MainActivity.cs @@ -0,0 +1,8 @@ +using Android.App; +using Android.Content.PM; +using Android.OS; + +namespace MewExplorer; +[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] +public class MainActivity : MauiAppCompatActivity { +} diff --git a/MewExplorer/Platforms/Android/MainApplication.cs b/MewExplorer/Platforms/Android/MainApplication.cs new file mode 100644 index 0000000..18a3a2c --- /dev/null +++ b/MewExplorer/Platforms/Android/MainApplication.cs @@ -0,0 +1,12 @@ +using Android.App; +using Android.Runtime; + +namespace MewExplorer; +[Application] +public class MainApplication : MauiApplication { + public MainApplication(IntPtr handle, JniHandleOwnership ownership) + : base(handle, ownership) { + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/MewExplorer/Platforms/Android/Resources/values/colors.xml b/MewExplorer/Platforms/Android/Resources/values/colors.xml new file mode 100644 index 0000000..c04d749 --- /dev/null +++ b/MewExplorer/Platforms/Android/Resources/values/colors.xml @@ -0,0 +1,6 @@ + + + #512BD4 + #2B0B98 + #2B0B98 + \ No newline at end of file diff --git a/MewExplorer/Platforms/Windows/App.xaml b/MewExplorer/Platforms/Windows/App.xaml new file mode 100644 index 0000000..6d32a5b --- /dev/null +++ b/MewExplorer/Platforms/Windows/App.xaml @@ -0,0 +1,17 @@ + + + + + #201C21 + #201C21 + #FFF + #FFF + + + + diff --git a/MewExplorer/Platforms/Windows/App.xaml.cs b/MewExplorer/Platforms/Windows/App.xaml.cs new file mode 100644 index 0000000..5f4e8dc --- /dev/null +++ b/MewExplorer/Platforms/Windows/App.xaml.cs @@ -0,0 +1,21 @@ +using Microsoft.UI.Xaml; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace MewExplorer.WinUI; +/// +/// Provides application-specific behavior to supplement the default Application class. +/// +public partial class App : MauiWinUIApplication { + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() { + this.InitializeComponent(); + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} + diff --git a/MewExplorer/Platforms/Windows/Package.appxmanifest b/MewExplorer/Platforms/Windows/Package.appxmanifest new file mode 100644 index 0000000..7125b1b --- /dev/null +++ b/MewExplorer/Platforms/Windows/Package.appxmanifest @@ -0,0 +1,46 @@ + + + + + + + + + $placeholder$ + User Name + $placeholder$.png + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MewExplorer/Platforms/Windows/app.manifest b/MewExplorer/Platforms/Windows/app.manifest new file mode 100644 index 0000000..6abc802 --- /dev/null +++ b/MewExplorer/Platforms/Windows/app.manifest @@ -0,0 +1,15 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + diff --git a/MewExplorer/Properties/launchSettings.json b/MewExplorer/Properties/launchSettings.json new file mode 100644 index 0000000..edf8aad --- /dev/null +++ b/MewExplorer/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Windows Machine": { + "commandName": "MsixPackage", + "nativeDebugging": false + } + } +} \ No newline at end of file diff --git a/MewExplorer/Resources/AppIcon/appicon.svg b/MewExplorer/Resources/AppIcon/appicon.svg new file mode 100644 index 0000000..9d63b65 --- /dev/null +++ b/MewExplorer/Resources/AppIcon/appicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/MewExplorer/Resources/AppIcon/appiconfg.svg b/MewExplorer/Resources/AppIcon/appiconfg.svg new file mode 100644 index 0000000..21dfb25 --- /dev/null +++ b/MewExplorer/Resources/AppIcon/appiconfg.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/MewExplorer/Resources/Fonts/OpenSans-Regular.ttf b/MewExplorer/Resources/Fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000..293a1cd Binary files /dev/null and b/MewExplorer/Resources/Fonts/OpenSans-Regular.ttf differ diff --git a/MewExplorer/Resources/Images/dotnet_bot.svg b/MewExplorer/Resources/Images/dotnet_bot.svg new file mode 100644 index 0000000..abfaff2 --- /dev/null +++ b/MewExplorer/Resources/Images/dotnet_bot.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MewExplorer/Resources/Raw/AboutAssets.txt b/MewExplorer/Resources/Raw/AboutAssets.txt new file mode 100644 index 0000000..531df33 --- /dev/null +++ b/MewExplorer/Resources/Raw/AboutAssets.txt @@ -0,0 +1,15 @@ +Any raw assets you want to be deployed with your application can be placed in +this directory (and child directories). Deployment of the asset to your application +is automatically handled by the following `MauiAsset` Build Action within your `.csproj`. + + + +These files will be deployed with you package and will be accessible using Essentials: + + async Task LoadMauiAsset() + { + using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); + using var reader = new StreamReader(stream); + + var contents = reader.ReadToEnd(); + } diff --git a/MewExplorer/Resources/Splash/splash.svg b/MewExplorer/Resources/Splash/splash.svg new file mode 100644 index 0000000..21dfb25 --- /dev/null +++ b/MewExplorer/Resources/Splash/splash.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/MewExplorer/Shared/MainLayout.razor b/MewExplorer/Shared/MainLayout.razor new file mode 100644 index 0000000..8593ecd --- /dev/null +++ b/MewExplorer/Shared/MainLayout.razor @@ -0,0 +1,21 @@ +@inherits LayoutComponentBase + +
+ +
+ + + +
+
+ @Body +
+
+ +
+ + + +
diff --git a/MewExplorer/Shared/MainLayout.razor.css b/MewExplorer/Shared/MainLayout.razor.css new file mode 100644 index 0000000..61e35f4 --- /dev/null +++ b/MewExplorer/Shared/MainLayout.razor.css @@ -0,0 +1,40 @@ +.page { + + background: var(--dark-3); + position: relative; + height: 100vh; + display: flex; + flex: 1; + flex-direction: column; + align-items: stretch; + justify-content: stretch; + +} + +.hor-layout { + + display: flex; + flex-direction: row; + flex: 1; + +} + +main { + + flex: 1; + display: flex; + +} + +main article { + + flex: 1; + +} + +.sidebar { + + background: var(--dark-1); + flex-basis: 25%; + +} \ No newline at end of file diff --git a/MewExplorer/Shared/NavMenu.razor b/MewExplorer/Shared/NavMenu.razor new file mode 100644 index 0000000..700dffe --- /dev/null +++ b/MewExplorer/Shared/NavMenu.razor @@ -0,0 +1,24 @@ +
+ MewExplorer +
+ +
+ +
+ +@code { + private bool collapseNavMenu = true; + + private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; + + private void ToggleNavMenu() + { + collapseNavMenu = !collapseNavMenu; + } +} diff --git a/MewExplorer/Shared/NavMenu.razor.css b/MewExplorer/Shared/NavMenu.razor.css new file mode 100644 index 0000000..a5b8bd3 --- /dev/null +++ b/MewExplorer/Shared/NavMenu.razor.css @@ -0,0 +1,44 @@ +.app-name-header { + + margin: 2em; + color: white; + +} + +.navbar-brand { + font-size: 1.1rem; +} + +.oi { + margin: 1em; + width: 2rem; + font-size: 1.1rem; + vertical-align: text-top; + top: -2px; +} + +.nav-item { + + font-size: 0.9rem; + padding: 2em; + +} + +.nav-item ::deep a { + color: #d7d7d7; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; +} + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.25); + color: white; +} + +.nav-item ::deep a:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} \ No newline at end of file diff --git a/MewExplorer/Shared/PlcStatus.razor b/MewExplorer/Shared/PlcStatus.razor new file mode 100644 index 0000000..813e7bc --- /dev/null +++ b/MewExplorer/Shared/PlcStatus.razor @@ -0,0 +1,6 @@ +

PlcStatus

+ +@code { + + +} \ No newline at end of file diff --git a/MewExplorer/Shared/PlcStatus.razor.css b/MewExplorer/Shared/PlcStatus.razor.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/MewExplorer/Shared/PlcStatus.razor.css @@ -0,0 +1 @@ + diff --git a/MewExplorer/Shared/PlcStatusBar.razor b/MewExplorer/Shared/PlcStatusBar.razor new file mode 100644 index 0000000..1177caf --- /dev/null +++ b/MewExplorer/Shared/PlcStatusBar.razor @@ -0,0 +1,7 @@ +
+ Stat bar +
+ +@code { + +} diff --git a/MewExplorer/Shared/PlcStatusBar.razor.css b/MewExplorer/Shared/PlcStatusBar.razor.css new file mode 100644 index 0000000..ca66444 --- /dev/null +++ b/MewExplorer/Shared/PlcStatusBar.razor.css @@ -0,0 +1,10 @@ +.plc-stat-bar { + color: white; + background: var(--vibrant-dark); + display: flex; + flex-direction: row; + flex-basis: 1.5em; + align-items: center; + padding-left: .5em; + font-size: 1rem; +} diff --git a/MewExplorer/Shared/SurveyPrompt.razor b/MewExplorer/Shared/SurveyPrompt.razor new file mode 100644 index 0000000..fc67435 --- /dev/null +++ b/MewExplorer/Shared/SurveyPrompt.razor @@ -0,0 +1,16 @@ +
+ + @Title + + + Please take our + brief survey + + and tell us what you think. +
+ +@code { + // Demonstrates how a parent component can supply parameters + [Parameter] + public string Title { get; set; } +} diff --git a/MewExplorer/_Imports.razor b/MewExplorer/_Imports.razor new file mode 100644 index 0000000..a7075a3 --- /dev/null +++ b/MewExplorer/_Imports.razor @@ -0,0 +1,8 @@ +@using System.Net.Http +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using MewExplorer +@using MewExplorer.Shared diff --git a/MewExplorer/wwwroot/css/app.css b/MewExplorer/wwwroot/css/app.css new file mode 100644 index 0000000..c2feb37 --- /dev/null +++ b/MewExplorer/wwwroot/css/app.css @@ -0,0 +1,94 @@ +@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); + +:root { + --vibrant-dark: #00CC14; + --vibrant-light: #5CFF6C; + --dark-1: #201C21; + --dark-2: #2B262C; + --dark-3: #352F37; + --light-1: #EFF2EF; +} + +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + margin: 0; +} + +h1:focus { + outline: none; +} + +a, .btn-link { + color: #0071c1; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.content { + padding-top: 1.1rem; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + +.blazor-error-boundary::after { + content: "An error has occurred." +} + +.status-bar-safe-area { + display: none; +} + +@supports (-webkit-touch-callout: none) { + .status-bar-safe-area { + display: flex; + position: sticky; + top: 0; + height: env(safe-area-inset-top); + background-color: #f7f7f7; + width: 100%; + z-index: 1; + } + + .flex-column, .navbar-brand { + padding-left: env(safe-area-inset-left); + } +} diff --git a/MewExplorer/wwwroot/css/open-iconic/FONT-LICENSE b/MewExplorer/wwwroot/css/open-iconic/FONT-LICENSE new file mode 100644 index 0000000..a1dc03f --- /dev/null +++ b/MewExplorer/wwwroot/css/open-iconic/FONT-LICENSE @@ -0,0 +1,86 @@ +SIL OPEN FONT LICENSE Version 1.1 + +Copyright (c) 2014 Waybury + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/MewExplorer/wwwroot/css/open-iconic/ICON-LICENSE b/MewExplorer/wwwroot/css/open-iconic/ICON-LICENSE new file mode 100644 index 0000000..2199f4a --- /dev/null +++ b/MewExplorer/wwwroot/css/open-iconic/ICON-LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Waybury + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/MewExplorer/wwwroot/css/open-iconic/README.md b/MewExplorer/wwwroot/css/open-iconic/README.md new file mode 100644 index 0000000..5ac0c17 --- /dev/null +++ b/MewExplorer/wwwroot/css/open-iconic/README.md @@ -0,0 +1,114 @@ +[Open Iconic v1.1.1](https://github.com/iconic/open-iconic) +=========== + +### Open Iconic is the open source sibling of [Iconic](https://github.com/iconic/open-iconic). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](https://github.com/iconic/open-iconic) + + + +## What's in Open Iconic? + +* 223 icons designed to be legible down to 8 pixels +* Super-light SVG files - 61.8 for the entire set +* SVG sprite—the modern replacement for icon fonts +* Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats +* Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats +* PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. + + +## Getting Started + +#### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](https://github.com/iconic/open-iconic) and [Reference](https://github.com/iconic/open-iconic) sections. + +### General Usage + +#### Using Open Iconic's SVGs + +We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). + +``` +icon name +``` + +#### Using Open Iconic's SVG Sprite + +Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. + +Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* + +``` + + + +``` + +Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. + +``` +.icon { + width: 16px; + height: 16px; +} +``` + +Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. + +``` +.icon-account-login { + fill: #f00; +} +``` + +To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). + +#### Using Open Iconic's Icon Font... + + +##### …with Bootstrap + +You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` + + +``` + +``` + + +``` + +``` + +##### …with Foundation + +You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` + +``` + +``` + + +``` + +``` + +##### …on its own + +You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` + +``` + +``` + +``` + +``` + + +## License + +### Icons + +All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). + +### Fonts + +All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). diff --git a/MewExplorer/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css b/MewExplorer/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css new file mode 100644 index 0000000..4664f2e --- /dev/null +++ b/MewExplorer/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css @@ -0,0 +1 @@ +@font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'} \ No newline at end of file diff --git a/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.eot b/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.eot new file mode 100644 index 0000000..f98177d Binary files /dev/null and b/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.eot differ diff --git a/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.otf b/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.otf new file mode 100644 index 0000000..f6bd684 Binary files /dev/null and b/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.otf differ diff --git a/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.svg b/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.svg new file mode 100644 index 0000000..32b2c4e --- /dev/null +++ b/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.svg @@ -0,0 +1,543 @@ + + + + + +Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014 + By P.J. Onori +Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf b/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf new file mode 100644 index 0000000..a894b68 Binary files /dev/null and b/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf differ diff --git a/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.woff b/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.woff new file mode 100644 index 0000000..f930998 Binary files /dev/null and b/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.woff differ diff --git a/MewExplorer/wwwroot/favicon.ico b/MewExplorer/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b Binary files /dev/null and b/MewExplorer/wwwroot/favicon.ico differ diff --git a/MewExplorer/wwwroot/index.html b/MewExplorer/wwwroot/index.html new file mode 100644 index 0000000..661a6ad --- /dev/null +++ b/MewExplorer/wwwroot/index.html @@ -0,0 +1,29 @@ + + + + + + MewExplorer + + + + + + + + +
+ +
Loading...
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + + + \ No newline at end of file diff --git a/MewTerminal/Commands/ClearCommand.cs b/MewTerminal/Commands/ClearCommand.cs new file mode 100644 index 0000000..098f89c --- /dev/null +++ b/MewTerminal/Commands/ClearCommand.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using CommandLine; +using MewtocolNet; +using MewtocolNet.ComCassette; +using Spectre.Console; + +namespace MewTerminal.Commands; + +[Verb("clear", HelpText = "Clears console", Hidden = true)] +internal class ClearCommand : CommandLineExcecuteable { + + public override void Run() { + + Console.Clear(); + + } + +} \ No newline at end of file diff --git a/MewTerminal/Commands/CommandLineExcecuteable.cs b/MewTerminal/Commands/CommandLineExcecuteable.cs new file mode 100644 index 0000000..977b438 --- /dev/null +++ b/MewTerminal/Commands/CommandLineExcecuteable.cs @@ -0,0 +1,41 @@ +using CommandLine.Text; +using CommandLine; +using MewtocolNet.Logging; + +namespace MewTerminal.Commands; + +public abstract class CommandLineExcecuteable { + + static UnParserSettings UnparserSet = new UnParserSettings { + PreferShortName = true, + }; + + [Option('v', "verbosity", HelpText = "Sets the Loglevel verbosity", Default = LogLevel.None)] + public LogLevel LogLevel { get; set; } = LogLevel.None; + + [Usage] + public static IEnumerable Examples { + get { + return new List() { + new Example( + helpText: "Sanning from adapter with ip 127.0.0.1 and logging all critical messages", + formatStyle: UnparserSet, + sample: new ScanCommand { + IPSource = "127.0.0.1", + LogLevel = LogLevel.Critical, + }), + new Example( + helpText: "Scanning from all adapters and logging only errors", + formatStyle: UnparserSet, + sample: new ScanCommand { + LogLevel = LogLevel.Error, + }), + }; + } + } + + public virtual void Run() { } + + public virtual Task RunAsync () => Task.CompletedTask; + +} diff --git a/MewTerminal/Commands/ScanCommand.cs b/MewTerminal/Commands/ScanCommand.cs new file mode 100644 index 0000000..d197c68 --- /dev/null +++ b/MewTerminal/Commands/ScanCommand.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using CommandLine; +using MewtocolNet; +using MewtocolNet.ComCassette; +using MewtocolNet.Logging; +using Spectre.Console; + +namespace MewTerminal.Commands; + +[Verb("scan", HelpText = "Scans all network PLCs")] +internal class ScanCommand : CommandLineExcecuteable { + + [Option("ip", HelpText = "IP of the source adapter" )] + public string? IPSource { get; set; } + + [Option("timeout", Default = 100)] + public int? TimeoutMS { get; set; } + + [Option("plc", Required = false, HelpText = "Gets the PLC types")] + public bool GetPLCTypes { get; set; } + + private class PLCCassetteTypeInfo { + + public CassetteInformation Cassette { get; set; } + + public PLCInfo PLCInf { get; set; } + + } + + public override async Task RunAsync () { + + await AnsiConsole.Status() + .Spinner(Spinner.Known.Dots) + .StartAsync("Scanning...", async ctx => { + + var query = await CassetteFinder.FindClientsAsync(IPSource, TimeoutMS ?? 100); + + var found = query.Select(x => new PLCCassetteTypeInfo { Cassette = x }).ToList(); + + if (found.Count > 0 && GetPLCTypes) { + + foreach (var item in found) { + + ctx.Status($"Getting cassette PLC {item.Cassette.IPAddress}:{item.Cassette.Port}") + .Spinner(Spinner.Known.Dots); + + var dev = Mewtocol.Ethernet(item.Cassette.IPAddress, item.Cassette.Port); + dev.ConnectTimeout = 1000; + await dev.ConnectAsync(); + item.PLCInf = dev.PlcInfo; + + dev.Disconnect(); + + } + + } + + if (found.Count() > 0) { + + AnsiConsole.MarkupLineInterpolated($"✅ Found {found.Count()} devices..."); + + } else { + + AnsiConsole.MarkupLineInterpolated($"❌ Found no devices"); + return; + + } + + if (found.Any(x => x.PLCInf != PLCInfo.None)) { + + AnsiConsole.Write(found.Select(x => new { + x.Cassette.Name, + PLC = x.PLCInf.TypeCode.ToName(), + IsRun = x.PLCInf.OperationMode.HasFlag(OPMode.RunMode), + IP = x.Cassette.IPAddress, + x.Cassette.Port, + DHCP = x.Cassette.UsesDHCP, + MAC = x.Cassette.MacAddress, + Ver = x.Cassette.FirmwareVersion, + x.Cassette.Status, + }).ToTable()); + + } else { + + AnsiConsole.Write(found.Select(x => new { + x.Cassette.Name, + IP = x.Cassette.IPAddress, + x.Cassette.Port, + DHCP = x.Cassette.UsesDHCP, + MAC = x.Cassette.MacAddress, + Ver = x.Cassette.FirmwareVersion, + x.Cassette.Status, + }).ToTable()); + + } + + }); + + } + +} \ No newline at end of file diff --git a/MewTerminal/Helpers/Helpers.cs b/MewTerminal/Helpers/Helpers.cs new file mode 100644 index 0000000..bc03a36 --- /dev/null +++ b/MewTerminal/Helpers/Helpers.cs @@ -0,0 +1,54 @@ +using Spectre.Console; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MewTerminal; + +internal static class Helpers { + + internal static Table ToTable (this IEnumerable data, params string[] markups) { + + // Create a table + var table = new Table(); + var type = typeof(T); + + var props = type.GetProperties(); + bool isFirst = true; + + foreach (var item in data) { + + var rowVals = new List(); + + foreach (var prop in props) { + + if(isFirst) table.AddColumn(prop.Name); + + var propVal = prop.GetValue(item); + + string strVal = propVal?.ToString() ?? "null"; + + if (propVal is byte[] bArr) { + strVal = string.Join(" ", bArr.Select(x => x.ToString("X2"))); + } + + strVal = strVal.Replace("[", ""); + strVal = strVal.Replace("]", ""); + + rowVals.Add(strVal); + + } + + isFirst = false; + + table.AddRow(rowVals.ToArray()); + + } + + return table; + + } + +} diff --git a/MewTerminal/MewTerminal.csproj b/MewTerminal/MewTerminal.csproj new file mode 100644 index 0000000..417a0cf --- /dev/null +++ b/MewTerminal/MewTerminal.csproj @@ -0,0 +1,34 @@ + + + + Exe + net7.0 + enable + enable + true + + + + + + + + + + + + + + x64 + win-x64 + True + None + False + false + none + en + ..\Builds\MewTerminal + + + + diff --git a/MewTerminal/Program.cs b/MewTerminal/Program.cs new file mode 100644 index 0000000..2de046c --- /dev/null +++ b/MewTerminal/Program.cs @@ -0,0 +1,86 @@ +using CommandLine; +using CommandLine.Text; +using MewTerminal.Commands; +using MewtocolNet.Logging; +using Spectre.Console; +using System.Reflection; + +namespace MewTerminal; + +internal class Program { + + static void Main(string[] args) { + + Logger.OnNewLogMessage((dt, lv, msg) => { + + AnsiConsole.WriteLine($"{msg}"); + + }); + + #if DEBUG + + Console.Clear(); + + var firstArg = new string[] { "help" }; + + start: + + if(firstArg == null) { + Console.WriteLine("Enter arguments [DEBUG MODE]"); + args = Console.ReadLine().SplitArgs(); + } + + //print help first time + InitParser(firstArg ?? args); + firstArg = null; + goto start; + + #else + + InitParser(args); + + #endif + + } + + private static Type[] LoadVerbs() { + + var lst = Assembly.GetExecutingAssembly() + .GetTypes() + .Where(t => t.GetCustomAttribute() != null) + .ToArray(); + return lst; + + } + + static void InitParser (string[] args) { + + var types = LoadVerbs(); + + var parseRes = Parser.Default.ParseArguments(args, types); + + var helpText = HelpText.AutoBuild(parseRes, h => { + + h.AddEnumValuesToHelpText = true; + + return h; + + }, e => e); + + parseRes.WithNotParsed(err => { + + }); + + if(parseRes?.Value != null && parseRes.Value is CommandLineExcecuteable exc) { + + Logger.LogLevel = exc.LogLevel; + + exc.Run(); + var task = Task.Run(exc.RunAsync); + task.Wait(); + + } + + } + +} diff --git a/MewtocolNet.sln b/MewtocolNet.sln index 4f0c7eb..8a20ae2 100644 --- a/MewtocolNet.sln +++ b/MewtocolNet.sln @@ -5,9 +5,13 @@ VisualStudioVersion = 17.5.33103.201 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MewtocolNet", "MewtocolNet\MewtocolNet.csproj", "{8B7863E7-5E82-4990-9138-2C0C24629982}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples", "Examples\Examples.csproj", "{D1F2FA26-3752-44BA-9DCB-4BC2436C5957}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MewtocolTests", "MewtocolTests\MewtocolTests.csproj", "{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MewtocolTests", "MewtocolTests\MewtocolTests.csproj", "{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MewExplorer", "MewExplorer\MewExplorer.csproj", "{F243F38A-76D3-4C38-BAE6-C61729479661}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocBuilder", "DocBuilder\DocBuilder.csproj", "{50F2D23F-C875-4006-A0B6-7F5A181BC944}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MewTerminal", "MewTerminal\MewTerminal.csproj", "{D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -31,18 +35,6 @@ Global {8B7863E7-5E82-4990-9138-2C0C24629982}.Release|x64.Build.0 = Release|Any CPU {8B7863E7-5E82-4990-9138-2C0C24629982}.Release|x86.ActiveCfg = Release|Any CPU {8B7863E7-5E82-4990-9138-2C0C24629982}.Release|x86.Build.0 = Release|Any CPU - {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Debug|x64.ActiveCfg = Debug|Any CPU - {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Debug|x64.Build.0 = Debug|Any CPU - {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Debug|x86.ActiveCfg = Debug|Any CPU - {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Debug|x86.Build.0 = Debug|Any CPU - {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Release|Any CPU.Build.0 = Release|Any CPU - {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Release|x64.ActiveCfg = Release|Any CPU - {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Release|x64.Build.0 = Release|Any CPU - {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Release|x86.ActiveCfg = Release|Any CPU - {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Release|x86.Build.0 = Release|Any CPU {C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Debug|Any CPU.Build.0 = Debug|Any CPU {C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -55,8 +47,53 @@ Global {C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Release|x64.Build.0 = Release|Any CPU {C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Release|x86.ActiveCfg = Release|Any CPU {C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Release|x86.Build.0 = Release|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Debug|x64.ActiveCfg = Debug|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Debug|x64.Build.0 = Debug|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Debug|x64.Deploy.0 = Debug|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Debug|x86.ActiveCfg = Debug|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Debug|x86.Build.0 = Debug|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Debug|x86.Deploy.0 = Debug|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|Any CPU.Build.0 = Release|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|Any CPU.Deploy.0 = Release|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|x64.ActiveCfg = Release|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|x64.Build.0 = Release|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|x64.Deploy.0 = Release|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|x86.ActiveCfg = Release|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|x86.Build.0 = Release|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|x86.Deploy.0 = Release|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Debug|x64.ActiveCfg = Debug|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Debug|x64.Build.0 = Debug|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Debug|x86.ActiveCfg = Debug|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Debug|x86.Build.0 = Debug|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Release|Any CPU.Build.0 = Release|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Release|x64.ActiveCfg = Release|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Release|x64.Build.0 = Release|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Release|x86.ActiveCfg = Release|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Release|x86.Build.0 = Release|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Debug|x64.ActiveCfg = Debug|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Debug|x64.Build.0 = Debug|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Debug|x86.ActiveCfg = Debug|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Debug|x86.Build.0 = Debug|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Release|Any CPU.Build.0 = Release|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Release|x64.ActiveCfg = Release|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Release|x64.Build.0 = Release|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Release|x86.ActiveCfg = Release|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {4ABB8137-CD8F-4691-9802-9ED371012F47} + EndGlobalSection EndGlobal diff --git a/MewtocolNet/ComCassette/CassetteFinder.cs b/MewtocolNet/ComCassette/CassetteFinder.cs new file mode 100644 index 0000000..748d689 --- /dev/null +++ b/MewtocolNet/ComCassette/CassetteFinder.cs @@ -0,0 +1,172 @@ +using MewtocolNet.Helpers; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MewtocolNet.ComCassette { + + /// + /// Provides a interface to modify and find PLC network cassettes also known as COM5 + /// + public class CassetteFinder { + + public static async Task> FindClientsAsync (string ipSource = null, int timeoutMs = 100) { + + var from = new IPEndPoint(IPAddress.Any, 0); + + var interfacesTasks = new List>>(); + + var usableInterfaces = GetUseableNetInterfaces(); + + if (ipSource == null) { + + var interfaces = NetworkInterface.GetAllNetworkInterfaces(); + + foreach (NetworkInterface netInterface in usableInterfaces) { + + IPInterfaceProperties ipProps = netInterface.GetIPProperties(); + var unicastInfo = ipProps.UnicastAddresses + .FirstOrDefault(x => x.Address.AddressFamily == AddressFamily.InterNetwork); + + var ep = new IPEndPoint(unicastInfo.Address, 0); + interfacesTasks.Add(FindClientsForEndpoint(ep, timeoutMs, netInterface.Name)); + + } + + } else { + + from = new IPEndPoint(IPAddress.Parse(ipSource), 0); + + var netInterface = usableInterfaces.FirstOrDefault(x => x.GetIPProperties().UnicastAddresses.Any(y => y.Address.ToString() == ipSource)); + + if (netInterface == null) + throw new NotSupportedException($"The host endpoint {ipSource}, is not available"); + + interfacesTasks.Add(FindClientsForEndpoint(from, timeoutMs, netInterface.Name)); + + } + + //run the interface querys + var grouped = await Task.WhenAll(interfacesTasks); + + var decomposed = new List(); + + foreach (var grp in grouped) { + + foreach (var cassette in grp) { + + if (decomposed.Any(x => x.MacAddress.SequenceEqual(cassette.MacAddress))) continue; + + decomposed.Add(cassette); + + } + + } + + return decomposed; + + } + + private static IEnumerable GetUseableNetInterfaces () { + + foreach (NetworkInterface netInterface in NetworkInterface.GetAllNetworkInterfaces()) { + + bool isEthernet = + netInterface.NetworkInterfaceType == NetworkInterfaceType.Ethernet || + netInterface.NetworkInterfaceType == NetworkInterfaceType.Ethernet3Megabit || + netInterface.NetworkInterfaceType == NetworkInterfaceType.FastEthernetFx || + netInterface.NetworkInterfaceType == NetworkInterfaceType.FastEthernetT || + netInterface.NetworkInterfaceType == NetworkInterfaceType.GigabitEthernet; + + bool isWlan = netInterface.NetworkInterfaceType == NetworkInterfaceType.Wireless80211; + + bool isUsable = netInterface.OperationalStatus == OperationalStatus.Up; + + if (!isUsable) continue; + if (!(isWlan || isEthernet)) continue; + + IPInterfaceProperties ipProps = netInterface.GetIPProperties(); + var hasUnicastInfo = ipProps.UnicastAddresses + .Any(x => x.Address.AddressFamily == AddressFamily.InterNetwork); + + if (!hasUnicastInfo) continue; + + yield return netInterface; + + } + + } + + private static async Task> FindClientsForEndpoint (IPEndPoint from, int timeoutMs, string ipEndpointName) { + + var cassettesFound = new List(); + + int plcPort = 9090; + + // Byte msg to request the status transmission of all plcs + byte[] requestCode = new byte[] { 0x88, 0x40, 0x00 }; + + // The start code of the status transmission response + byte[] startCode = new byte[] { 0x88, 0xC0, 0x00 }; + + using(var udpClient = new UdpClient()) { + + udpClient.EnableBroadcast = true; + + udpClient.Client.Bind(from); + + //broadcast packet to all devices (plc specific package) + udpClient.Send(requestCode, requestCode.Length, "255.255.255.255", plcPort); + + //canceling after no new data was read + CancellationTokenSource tSource = new CancellationTokenSource(); + var tm = new System.Timers.Timer(timeoutMs); + tm.Elapsed += (s, e) => { + tSource.Cancel(); + tm.Stop(); + }; + tm.Start(); + + //wait for devices to send response + try { + + byte[] recvBuffer = null; + + while (!tSource.Token.IsCancellationRequested) { + + var res = await udpClient.ReceiveAsync().WithCancellation(tSource.Token); + + if (res.Buffer == null) break; + + recvBuffer = res.Buffer; + + if (recvBuffer.SearchBytePattern(startCode) == 0) { + + tm.Stop(); + tm.Start(); + + var parsed = CassetteInformation.FromBytes(recvBuffer, from, ipEndpointName); + if (parsed != null) cassettesFound.Add(parsed); + + } + + } + + } catch (OperationCanceledException) { } catch (SocketException) { } + + } + + return cassettesFound; + + } + + } + +} diff --git a/MewtocolNet/ComCassette/CassetteInformation.cs b/MewtocolNet/ComCassette/CassetteInformation.cs new file mode 100644 index 0000000..fd5845f --- /dev/null +++ b/MewtocolNet/ComCassette/CassetteInformation.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +//WARNING! The whole UDP protocol was reverse engineered and is not fully implemented.. + +namespace MewtocolNet.ComCassette { + + /// + /// Information about the COM cassette + /// + public class CassetteInformation { + + /// + /// Indicates if the cassette is currently configurating + /// + public bool IsConfigurating { get; private set; } + + /// + /// Name of the COM cassette + /// + public string Name { get; set; } + + /// + /// Indicates the usage of DHCP + /// + public bool UsesDHCP { get; set; } + + /// + /// IP Address of the COM cassette + /// + public IPAddress IPAddress { get; set; } + + /// + /// Subnet mask of the cassette + /// + public IPAddress SubnetMask { get; set; } + + /// + /// Default gateway of the cassette + /// + public IPAddress GatewayAddress { get; set; } + + /// + /// Mac address of the cassette + /// + public byte[] MacAddress { get; private set; } + + /// + /// The source endpoint the cassette is reachable from + /// + public IPEndPoint Endpoint { get; private set; } + + /// + /// The name of the endpoint the device is reachable from, or null if not specifically defined + /// + public string EndpointName { get; private set; } + + /// + /// Firmware version as string + /// + public string FirmwareVersion { get; private set; } + + /// + /// The tcp port of the cassette + /// + public int Port { get; private set; } + + /// + /// Status of the cassette + /// + public CassetteStatus Status { get; private set; } + + internal static CassetteInformation FromBytes(byte[] bytes, IPEndPoint endpoint, string endpointName) { + + // Receive data package explained: + // 0 3 4 8 12 17 22 24 27 29 31 32 + // 88 C0 00 | 00 | C0 A8 73 D4 | FF FF FF 00 | C0 A8 73 3C | 00 | C0 8F 60 53 1C | 01 10 | 23 86 | 00 | 25 | 00 | 00 | 00 | 0D | (byte) * (n) NAME LEN + // Header |DHCP| IPv4 addr. | Subnet Mask | IPv4 Gatwy | | Mac Addr. | Ver. | Port | | | |STAT| | Name LEN | Name + // 1 or 0 Procuct Type? StatusCode Length of Name + + //get ips / mac + var dhcpOn = bytes.Skip(3).First() != 0x00; + var ipAdd = new IPAddress(bytes.Skip(4).Take(4).ToArray()); + var subnetMask = new IPAddress(bytes.Skip(8).Take(4).ToArray()); + var gateWaysAdd = new IPAddress(bytes.Skip(12).Take(4).ToArray()); + var macAdd = bytes.Skip(17).Take(5).ToArray(); + var firmwareV = string.Join(".", bytes.Skip(22).Take(2).Select(x => x.ToString("X1")).ToArray()); + var port = BitConverter.ToUInt16(bytes.Skip(24).Take(2).Reverse().ToArray(), 0); + var status = (CassetteStatus)bytes.Skip(29).First(); + + //missing blocks, later + + //get name + var name = Encoding.ASCII.GetString(bytes.Skip(32).ToArray()); + + return new CassetteInformation { + + Name = name, + UsesDHCP = dhcpOn, + IPAddress = ipAdd, + SubnetMask = subnetMask, + GatewayAddress = gateWaysAdd, + MacAddress = macAdd, + Endpoint = endpoint, + EndpointName = endpointName, + FirmwareVersion = firmwareV, + Port = port, + Status = status, + + }; + + } + + public async Task SendNewConfigAsync () { + + if (IsConfigurating) return; + + // this command gets sent to a specific plc ip address to overwrite the cassette config + // If dhcp is set to 1 the ip is ignored but still must be valid + + // 88 41 00 | 00 | C0 8F 61 07 1B | 05 | 54 65 73 74 31 | 05 | 46 50 58 45 54 | 00 | C0 A8 01 07 | FF FF FF 00 | C0 A8 73 3C + // Header | | | 5 | T e s t 1 | 05 | F P X E T |0||1| 192.168.1.7 | 255.255... | 192.168.115.60 + // Header | | Mac Address |LEN>| ASCII Name |LEN>| Static |DHCP| Target IP | Subnet Mask | Gateway + + IsConfigurating = true; + + List sendBytes = new List(); + + //add cmd header + sendBytes.AddRange(new byte[] { 0x88, 0x41, 0x00, 0x00 }); + + //add mac + sendBytes.AddRange(MacAddress); + + //add name length + sendBytes.Add((byte)Name.Length); + + //add name + sendBytes.AddRange(Encoding.ASCII.GetBytes(Name)); + + //FPXET + var subname = Encoding.ASCII.GetBytes("TESTFP"); + + //add sub name length + sendBytes.Add((byte)subname.Length); + + //add subname + sendBytes.AddRange(subname); + + //add dhcp 0 | 1 + sendBytes.Add((byte)(UsesDHCP ? 0x01 : 0x00)); + + //add ip address + sendBytes.AddRange(IPAddress.GetAddressBytes()); + + //add subnet mask ip address + sendBytes.AddRange(SubnetMask.GetAddressBytes()); + + //add gateway ip + sendBytes.AddRange(GatewayAddress.GetAddressBytes()); + + var sendBytesArr = sendBytes.ToArray(); + + using(var udpClient = new UdpClient()) { + + udpClient.Client.Bind(Endpoint); + + //broadcast packet to all devices (plc specific package) + await udpClient.SendAsync(sendBytesArr, sendBytesArr.Length, "255.255.255.255", 9090); + + } + + } + + } + +} diff --git a/MewtocolNet/ComCassette/CassetteStatus.cs b/MewtocolNet/ComCassette/CassetteStatus.cs new file mode 100644 index 0000000..4f84666 --- /dev/null +++ b/MewtocolNet/ComCassette/CassetteStatus.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet.ComCassette { + + /// + /// Needs a list of all status codes.. hard to reverse engineer + /// + public enum CassetteStatus { + + /// + /// Cassette is running as intended + /// + Normal = 0, + /// + /// Cassette DHCP resolution error + /// + DHCPError = 2, + + } + +} diff --git a/MewtocolNet/DocAttributes/PlcCodeTestedAttribute.cs b/MewtocolNet/DocAttributes/PlcCodeTestedAttribute.cs new file mode 100644 index 0000000..3883f15 --- /dev/null +++ b/MewtocolNet/DocAttributes/PlcCodeTestedAttribute.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet.DocAttributes { + + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] + internal class PlcCodeTestedAttribute : Attribute { + + public PlcCodeTestedAttribute() { } + + } + +} \ No newline at end of file diff --git a/MewtocolNet/DocAttributes/PlcEXRTAttribute.cs b/MewtocolNet/DocAttributes/PlcEXRTAttribute.cs new file mode 100644 index 0000000..1975a52 --- /dev/null +++ b/MewtocolNet/DocAttributes/PlcEXRTAttribute.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet.DocAttributes { + + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] + internal class PlcEXRTAttribute : Attribute { + + public PlcEXRTAttribute() {} + + } + +} diff --git a/MewtocolNet/DocAttributes/PlcLegacyAttribute.cs b/MewtocolNet/DocAttributes/PlcLegacyAttribute.cs new file mode 100644 index 0000000..01367dc --- /dev/null +++ b/MewtocolNet/DocAttributes/PlcLegacyAttribute.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet.DocAttributes { + + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] + internal class PlcLegacyAttribute : Attribute { + + public PlcLegacyAttribute() {} + + } + +} diff --git a/MewtocolNet/Exceptions/MewtocolException.cs b/MewtocolNet/Exceptions/MewtocolException.cs new file mode 100644 index 0000000..46c948e --- /dev/null +++ b/MewtocolNet/Exceptions/MewtocolException.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet.Exceptions { + + [Serializable] + public class MewtocolException : Exception { + + public MewtocolException() { } + + public MewtocolException(string message) : base(message) { } + + public MewtocolException(string message, Exception inner) : base(message, inner) { } + + protected MewtocolException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + internal static MewtocolException DupeRegister (IRegisterInternal register) { + + return new MewtocolException($"The mewtocol interface already contains this register: {register.GetRegisterPLCName()}"); + + } + + internal static MewtocolException DupeNameRegister (IRegisterInternal register) { + + return new MewtocolException($"The mewtocol interface registers already contains a register with the name: {register.GetRegisterPLCName()}"); + + } + + } + +} diff --git a/MewtocolNet/Extensions/AsyncExtensions.cs b/MewtocolNet/Extensions/AsyncExtensions.cs new file mode 100644 index 0000000..120dea0 --- /dev/null +++ b/MewtocolNet/Extensions/AsyncExtensions.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.Threading; +using System.Net.Sockets; + +namespace MewtocolNet { + + internal static class AsyncExtensions { + + public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) { + + var tcs = new TaskCompletionSource(); + using (cancellationToken.Register(s => ((TaskCompletionSource)s).TrySetResult(true), tcs)) { + if (task != await Task.WhenAny(task, tcs.Task)) { + throw new OperationCanceledException(cancellationToken); + } + } + + if(task.IsCanceled) return default(T); + + return task.Result; + + } + + } + +} diff --git a/MewtocolNet/Extensions/SerialPortExtensions.cs b/MewtocolNet/Extensions/SerialPortExtensions.cs new file mode 100644 index 0000000..573f608 --- /dev/null +++ b/MewtocolNet/Extensions/SerialPortExtensions.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.IO.Ports; +using System.Text; +using System.Threading.Tasks; + +namespace MewtocolNet { + + internal static class SerialPortExtensions { + + public async static Task WriteAsync (this SerialPort serialPort, byte[] buffer, int offset, int count) { + + await serialPort.BaseStream.WriteAsync(buffer, 0, buffer.Length); + + } + + public async static Task ReadAsync (this SerialPort serialPort, byte[] buffer, int offset, int count) { + var bytesToRead = count; + var temp = new byte[count]; + + while (bytesToRead > 0) { + var readBytes = await serialPort.BaseStream.ReadAsync(temp, 0, bytesToRead); + Array.Copy(temp, 0, buffer, offset + count - bytesToRead, readBytes); + bytesToRead -= readBytes; + } + } + + public async static Task ReadAsync (this SerialPort serialPort, int count) { + var buffer = new byte[count]; + await serialPort.ReadAsync(buffer, 0, count); + return buffer; + } + + } + +} diff --git a/MewtocolNet/Queue/SerialQueue.cs b/MewtocolNet/Helpers/AsyncQueue.cs similarity index 82% rename from MewtocolNet/Queue/SerialQueue.cs rename to MewtocolNet/Helpers/AsyncQueue.cs index 06001bf..79ad62d 100644 --- a/MewtocolNet/Queue/SerialQueue.cs +++ b/MewtocolNet/Helpers/AsyncQueue.cs @@ -1,15 +1,18 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; +using System.Linq; namespace MewtocolNet.Queue { - internal class SerialQueue { + internal class AsyncQueue { readonly object _locker = new object(); readonly WeakReference _lastTask = new WeakReference(null); - internal Task Enqueue (Func> asyncFunction) { + internal Task Enqueue(Func> asyncFunction) { lock (_locker) { + Task lastTask; Task resultTask; @@ -22,6 +25,7 @@ namespace MewtocolNet.Queue { _lastTask.SetTarget(resultTask); return resultTask; + } } diff --git a/MewtocolNet/Mewtocol/LinkedLists.cs b/MewtocolNet/Helpers/CodeDescriptions.cs similarity index 87% rename from MewtocolNet/Mewtocol/LinkedLists.cs rename to MewtocolNet/Helpers/CodeDescriptions.cs index 468a28a..9407856 100644 --- a/MewtocolNet/Mewtocol/LinkedLists.cs +++ b/MewtocolNet/Helpers/CodeDescriptions.cs @@ -1,11 +1,10 @@ -using System; using System.Collections.Generic; -namespace MewtocolNet.Links { +namespace MewtocolNet { - internal class LinkedData { + internal class CodeDescriptions { - internal static Dictionary ErrorCodes = new Dictionary { + internal static Dictionary Error = new Dictionary { {21, "NACK error"}, {22, "WACK error"}, @@ -40,7 +39,6 @@ namespace MewtocolNet.Links { }; - } } \ No newline at end of file diff --git a/MewtocolNet/Helpers/LinqHelpers.cs b/MewtocolNet/Helpers/LinqHelpers.cs new file mode 100644 index 0000000..03d9595 --- /dev/null +++ b/MewtocolNet/Helpers/LinqHelpers.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet.Helpers { + + internal static class LinqHelpers { + + internal static IEnumerable DistinctBy(this IEnumerable source, Func keySelector) { + return DistinctBy(source, keySelector, null); + } + + internal static IEnumerable DistinctBy + (this IEnumerable source, Func keySelector, IEqualityComparer comparer) { + + if (source == null) throw new ArgumentNullException(nameof(source)); + if (keySelector == null) throw new ArgumentNullException(nameof(keySelector)); + + return _(); IEnumerable _() { + var knownKeys = new HashSet(comparer); + foreach (var element in source) { + if (knownKeys.Add(keySelector(element))) + yield return element; + } + } + + } + + } + +} diff --git a/MewtocolNet/Helpers/MewtocolHelpers.cs b/MewtocolNet/Helpers/MewtocolHelpers.cs new file mode 100644 index 0000000..0633e98 --- /dev/null +++ b/MewtocolNet/Helpers/MewtocolHelpers.cs @@ -0,0 +1,396 @@ +using MewtocolNet.DocAttributes; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace MewtocolNet { + + /// + /// Contains helper methods + /// + public static class MewtocolHelpers { + + #region Value PLC Humanizers + + /// + /// Gets the TimeSpan as a PLC representation string fe. + /// + /// T#1h10m30s20ms + /// + /// + /// + /// + public static string AsPLCTime (this TimeSpan timespan) { + + if (timespan == null || timespan == TimeSpan.Zero) + return $"T#0s"; + + StringBuilder sb = new StringBuilder("T#"); + + int millis = timespan.Milliseconds; + int seconds = timespan.Seconds; + int minutes = timespan.Minutes; + int hours = timespan.Hours; + + if (hours > 0) sb.Append($"{hours}h"); + if (minutes > 0) sb.Append($"{minutes}m"); + if (seconds > 0) sb.Append($"{seconds}s"); + if (millis > 0) sb.Append($"{millis}ms"); + + return sb.ToString(); + + } + + /// + /// Turns a bit array into a 0 and 1 string + /// + public static string ToBitString(this BitArray arr) { + + var bits = new bool[arr.Length]; + arr.CopyTo(bits, 0); + return string.Join("", bits.Select(x => x ? "1" : "0")); + + } + + #endregion + + #region Byte and string operation helpers + + /// + /// Searches a byte array for a pattern + /// + /// + /// + /// The start index of the found pattern or -1 + public static int SearchBytePattern(this byte[] src, byte[] pattern) { + + int maxFirstCharSlot = src.Length - pattern.Length + 1; + for (int i = 0; i < maxFirstCharSlot; i++) { + if (src[i] != pattern[0]) // compare only first byte + continue; + + // found a match on first byte, now try to match rest of the pattern + for (int j = pattern.Length - 1; j >= 1; j--) { + if (src[i + j] != pattern[j]) break; + if (j == 1) return i; + } + } + return -1; + + } + + /// + /// Converts a string (after converting to upper case) to ascii bytes + /// + internal static byte[] BytesFromHexASCIIString(this string _str) { + + ASCIIEncoding ascii = new ASCIIEncoding(); + byte[] bytes = ascii.GetBytes(_str.ToUpper()); + return bytes; + + } + + /// + /// Builds the BCC / Checksum for the mewtocol command + /// + /// The mewtocol command (%01#RCS0001) + /// The mewtocol command with the appended checksum + public static string BuildBCCFrame(this string asciiArr) { + + Encoding ae = Encoding.ASCII; + byte[] b = ae.GetBytes(asciiArr); + byte xorTotalByte = 0; + for (int i = 0; i < b.Length; i++) + xorTotalByte ^= b[i]; + return asciiArr.Insert(asciiArr.Length, xorTotalByte.ToString("X2")); + + } + + /// + /// Parses the byte string from a incoming RD message + /// + internal static string ParseDTByteString(this string _onString, int _blockSize = 4) { + + if (_onString == null) + return null; + + var res = new Regex(@"\%([0-9]{2})\$RD(.{" + _blockSize + "})").Match(_onString); + if (res.Success) { + string val = res.Groups[2].Value; + return val; + } + return null; + + } + + /// + /// Parses a return message as RCS single bit + /// + internal static bool? ParseRCSingleBit(this string _onString) { + + _onString = _onString.Replace("\r", ""); + + var res = new Regex(@"\%([0-9]{2})\$RC(.)").Match(_onString); + if (res.Success) { + string val = res.Groups[2].Value; + return val == "1"; + } + return null; + + } + + /// + /// Parses a return message as RCS multiple bits + /// + internal static BitArray ParseRCMultiBit(this string _onString) { + + _onString = _onString.Replace("\r", ""); + + var res = new Regex(@"\%([0-9]{2})\$RC(?(?:0|1){0,8})(..)").Match(_onString); + if (res.Success) { + + string val = res.Groups["bits"].Value; + + return new BitArray(val.Select(c => c == '1').ToArray()); + + } + return null; + + } + + /// + /// Parses a return string from the PLC as a raw byte array
+ /// Example: + /// + /// ↓Start ↓end + /// %01$RD0100020010\r + /// + /// This will return the byte array: + /// + /// [0x01, 0x00, 0x02, 0x00] + /// + ///
+ /// + /// A or null of failed + internal static byte[] ParseDTRawStringAsBytes (this string _onString) { + + var res = new Regex(@"\%([0-9]{2})\$RD(?.*)(?..)..").Match(_onString); + if (res.Success) { + + string val = res.Groups["data"].Value; + var parts = val.SplitInParts(2).ToArray(); + var bytes = new byte[parts.Length]; + + for (int i = 0; i < bytes.Length; i++) { + + bytes[i] = byte.Parse(parts[i], NumberStyles.HexNumber); + + } + + return bytes; + } + return null; + + } + + /// + /// Splits a string into even parts + /// + internal static IEnumerable SplitInParts(this string s, int partLength) { + + if (s == null) + throw new ArgumentNullException(nameof(s)); + if (partLength <= 0) + throw new ArgumentException("Part length has to be positive.", nameof(partLength)); + + for (var i = 0; i < s.Length; i += partLength) + yield return s.Substring(i, Math.Min(partLength, s.Length - i)); + + } + + /// + /// Converts a hex string (AB01C1) to a byte array + /// + internal static byte[] HexStringToByteArray (this string hex) { + if (hex == null) return null; + return Enumerable.Range(0, hex.Length) + .Where(x => x % 2 == 0) + .Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) + .ToArray(); + } + + /// + /// Converts a byte array to a hexadecimal string + /// + /// Seperator between the hex numbers + /// The byte array + public static string ToHexString (this byte[] arr, string seperator = "") { + + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < arr.Length; i++) { + byte b = arr[i]; + sb.Append(b.ToString("X2")); + if(i < arr.Length - 1) sb.Append(seperator); + } + + return sb.ToString(); + + } + + /// + /// Switches byte order from mixed to big endian + /// + internal static byte[] BigToMixedEndian(this byte[] arr) { + + List oldBL = new List(arr); + + List tempL = new List(); + + //make the input list even + if (arr.Length % 2 != 0) + oldBL.Add((byte)0); + + for (int i = 0; i < oldBL.Count; i += 2) { + byte firstByte = oldBL[i]; + byte lastByte = oldBL[i + 1]; + tempL.Add(lastByte); + tempL.Add(firstByte); + + } + + return tempL.ToArray(); + + } + + #endregion + + #region Comparerers + + /// + /// Checks if the register type is boolean + /// + internal static bool IsBoolean (this RegisterType type) { + + return type == RegisterType.X || type == RegisterType.Y || type == RegisterType.R; + + } + + /// + /// Checks if the register type numeric + /// + internal static bool IsNumericDTDDT (this RegisterType type) { + + return type == RegisterType.DT || type == RegisterType.DDT; + + } + + /// + /// Checks if the register type is an physical in or output of the plc + /// + internal static bool IsPhysicalInOutType(this RegisterType type) { + + return type == RegisterType.X || type == RegisterType.Y; + + } + + internal static bool CompareIsDuplicate (this IRegisterInternal reg1, IRegisterInternal compare) { + + bool valCompare = reg1.RegisterType == compare.RegisterType && + reg1.MemoryAddress == compare.MemoryAddress && + reg1.GetSpecialAddress() == compare.GetSpecialAddress(); + + return valCompare; + + } + + internal static bool CompareIsNameDuplicate(this IRegisterInternal reg1, IRegisterInternal compare) { + + return ( reg1.Name != null || compare.Name != null) && reg1.Name == compare.Name; + + } + + #endregion + + #region PLC Type Enum Parsing + + /// + /// Converts the enum to a plc name string + /// + public static string ToName(this PlcType plcT) { + + if (plcT == 0) return "Unknown"; + + return string.Join(" or ", ParsedPlcName.PlcDeconstruct(plcT).Select(x => x.WholeName)); + + } + + /// + /// Converts the enum to a decomposed struct + /// + public static ParsedPlcName[] ToNameDecompose (this PlcType legacyT) { + + if ((int)legacyT == 0) return Array.Empty(); + + return ParsedPlcName.PlcDeconstruct(legacyT); + + } + + /// + /// Checks if the PLC type is discontinued + /// + public static bool IsDiscontinued (this PlcType plcT) { + + var memberInfos = plcT.GetType().GetMember(plcT.ToString()); + var enumValueMemberInfo = memberInfos.FirstOrDefault(m => m.DeclaringType == plcT.GetType()); + var valueAttributes = enumValueMemberInfo?.GetCustomAttributes(typeof(PlcLegacyAttribute), false); + if (valueAttributes != null) { + var found = valueAttributes.FirstOrDefault(x => x.GetType() == typeof(PlcLegacyAttribute)); + if (found != null) return true; + } + + return false; + + } + + #if DEBUG + + internal static bool WasTestedLive (this PlcType plcT) { + + var memberInfos = plcT.GetType().GetMember(plcT.ToString()); + var enumValueMemberInfo = memberInfos.FirstOrDefault(m => m.DeclaringType == plcT.GetType()); + var valueAttributes = enumValueMemberInfo?.GetCustomAttributes(typeof(PlcCodeTestedAttribute), false); + if (valueAttributes != null) { + var found = valueAttributes.FirstOrDefault(x => x.GetType() == typeof(PlcCodeTestedAttribute)); + if (found != null) return true; + } + + return false; + + } + + internal static bool IsEXRTPLC (this PlcType plcT) { + + var memberInfos = plcT.GetType().GetMember(plcT.ToString()); + var enumValueMemberInfo = memberInfos.FirstOrDefault(m => m.DeclaringType == plcT.GetType()); + var valueAttributes = enumValueMemberInfo?.GetCustomAttributes(typeof(PlcEXRTAttribute), false); + if (valueAttributes != null) { + var found = valueAttributes.FirstOrDefault(x => x.GetType() == typeof(PlcEXRTAttribute)); + if (found != null) return true; + } + + return false; + + } + + #endif + + #endregion + + } + +} \ No newline at end of file diff --git a/MewtocolNet/Helpers/ParsedPlcName.cs b/MewtocolNet/Helpers/ParsedPlcName.cs new file mode 100644 index 0000000..c1e9ea2 --- /dev/null +++ b/MewtocolNet/Helpers/ParsedPlcName.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace MewtocolNet { + + /// + /// A structure containing the PLC name parsed + /// + public struct ParsedPlcName { + + /// + /// Whole name of the PLC + /// + public string WholeName { get; internal set; } + + /// + /// The family group of the PLC + /// + public string Group { get; internal set; } + + /// + /// The Memory size of the PLC + /// + public float Size { get; internal set; } + + /// + /// The subtype strings of the plc + /// + public string[] SubTypes { get; internal set; } + + /// + public override string ToString() => WholeName; + + internal static ParsedPlcName[] PlcDeconstruct (PlcType plcT) { + + string wholeStr = plcT.ToString(); + + var split = wholeStr.Replace("_OR_", "|").Split('|'); + var reg = new Regex(@"(?[A-Za-z0-9]*)_(?[A-Za-z0-9]*)(?:__)?(?.*)"); + + var retList = new List(); + + foreach (var item in split) { + + var match = reg.Match(item); + + if (match.Success) { + + string groupStr = SanitizePlcEncodedString(match.Groups["group"].Value); + string sizeStr = SanitizePlcEncodedString(match.Groups["size"].Value); + float sizeFl = float.Parse(sizeStr.Replace("k", ""), NumberStyles.Float, CultureInfo.InvariantCulture); + string additionalStr = match.Groups["additional"].Value; + string[] subTypes = additionalStr.Split('_').Select(x => SanitizePlcEncodedString(x)).ToArray(); + + string wholeName = $"{groupStr} {sizeFl:0.##}k{(subTypes.Length > 1 ? " " : "")}{string.Join(",", subTypes)}"; + + if (string.IsNullOrEmpty(subTypes[0])) + subTypes = Array.Empty(); + + retList.Add(new ParsedPlcName { + Group = groupStr, + Size = sizeFl, + SubTypes = subTypes, + WholeName = wholeName, + }); + + } else { + + throw new FormatException($"The plc enum was not formatted correctly: {item}"); + + } + + } + + return retList.ToArray(); + + } + + private static string SanitizePlcEncodedString(string input) { + + input = input.Replace("d", "-"); + input = input.Replace("c", "."); + input = input.Replace("s", "/"); + + return input; + + } + + } + +} diff --git a/MewtocolNet/IPlc.cs b/MewtocolNet/IPlc.cs new file mode 100644 index 0000000..1386b93 --- /dev/null +++ b/MewtocolNet/IPlc.cs @@ -0,0 +1,122 @@ +using MewtocolNet.Registers; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MewtocolNet { + + /// + /// Provides a interface for Panasonic PLCs + /// + public interface IPlc : IDisposable { + + /// + /// The current connection state of the interface + /// + bool IsConnected { get; } + + /// + /// The current transmission speed in bytes per second + /// + int BytesPerSecondUpstream { get; } + + /// + /// The current transmission speed in bytes per second + /// + int BytesPerSecondDownstream { get; } + + /// + /// Current poller cycle duration + /// + int PollerCycleDurationMs { get; } + + /// + /// Currently queued message count + /// + int QueuedMessages { get; } + + /// + /// The registered data registers of the PLC + /// + IEnumerable Registers { get; } + + /// + /// Generic information about the connected PLC + /// + PLCInfo PlcInfo { get; } + + /// + /// The station number of the PLC + /// + int StationNumber { get; } + + /// + /// A connection info string + /// + string ConnectionInfo { get; } + + /// + /// The initial connection timeout in milliseconds + /// + int ConnectTimeout { get; set; } + + /// + /// Tries to establish a connection with the device asynchronously + /// + Task ConnectAsync(); + + /// + /// Disconnects the devive from its current connection + /// + void Disconnect(); + + /// + /// Calculates the checksum automatically and sends a command to the PLC then awaits results + /// + /// MEWTOCOL Formatted request string ex: %01#RT + /// Append the checksum and bcc automatically + /// Timout to wait for a response + /// Returns the result + Task SendCommandAsync(string _msg, bool withTerminator = true, int timeoutMs = -1); + + /// + /// Use this to await the first poll iteration after connecting, + /// This also completes if the initial connection fails + /// + Task AwaitFirstDataCycleAsync(); + + /// + /// Runs a single poller cycle manually, + /// useful if you want to use a custom update frequency + /// + /// The number of inidvidual mewtocol commands sent + Task RunPollerCylceManual(); + + /// + /// Gets the connection info string + /// + string GetConnectionInfo(); + + /// + /// Adds a register to the plc + /// + void AddRegister(BaseRegister register); + + /// + /// Adds a register to the plc + /// + void AddRegister(IRegister register); + + /// + /// Gets a register from the plc by name + /// + IRegister GetRegister(string name); + + /// + /// Gets all registers from the plc + /// + IEnumerable GetAllRegisters(); + + } + +} diff --git a/MewtocolNet/IPlcEthernet.cs b/MewtocolNet/IPlcEthernet.cs new file mode 100644 index 0000000..dd95ddf --- /dev/null +++ b/MewtocolNet/IPlcEthernet.cs @@ -0,0 +1,55 @@ +using MewtocolNet.RegisterAttributes; +using System; +using System.Net; +using System.Threading.Tasks; + +namespace MewtocolNet { + + /// + /// Provides a interface for Panasonic PLCs over a ethernet connection + /// + public interface IPlcEthernet : IPlc { + + /// + /// The current IP of the PLC connection + /// + string IpAddress { get; } + + /// + /// The current port of the PLC connection + /// + int Port { get; } + + /// + /// The host ip endpoint, leave it null to use an automatic interface + /// + IPEndPoint HostEndpoint { get; set; } + + /// + /// Tries to establish a connection with the device asynchronously + /// + Task ConnectAsync(); + + /// + /// Configures the serial interface + /// + /// IP adress of the PLC + /// Port of the PLC + /// Station Number of the PLC + void ConfigureConnection(string _ip, int _port = 9094, int _station = 1); + + /// + /// Attaches a poller to the interface + /// + IPlcEthernet WithPoller(); + + /// + /// Attaches a register collection object to + /// the interface that can be updated automatically. + /// + /// The type of the collection base class + IPlcEthernet AddRegisterCollection(RegisterCollection collection); + + } + +} diff --git a/MewtocolNet/IPlcSerial.cs b/MewtocolNet/IPlcSerial.cs new file mode 100644 index 0000000..609bf40 --- /dev/null +++ b/MewtocolNet/IPlcSerial.cs @@ -0,0 +1,75 @@ +using MewtocolNet.RegisterAttributes; +using System; +using System.Collections.Generic; +using System.IO.Ports; +using System.Text; +using System.Threading.Tasks; + +namespace MewtocolNet { + + /// + /// Provides a interface for Panasonic PLCs over a serial port connection + /// + public interface IPlcSerial : IPlc { + + /// + /// Port name of the serial port that this device is configured for + /// + string PortName { get; } + + /// + /// The serial connection baud rate that this device is configured for + /// + int SerialBaudRate { get; } + + /// + /// The serial connection data bits + /// + int SerialDataBits { get; } + + /// + /// The serial connection parity + /// + Parity SerialParity { get; } + + /// + /// The serial connection stop bits + /// + StopBits SerialStopBits { get; } + + /// + /// Sets up the connection settings for the device + /// + /// Port name of COM port + /// The serial connection baud rate + /// The serial connection data bits + /// The serial connection parity + /// The serial connection stop bits + /// The station number of the PLC + void ConfigureConnection(string _portName, int _baudRate = 19200, int _dataBits = 8, Parity _parity = Parity.Odd, StopBits _stopBits = StopBits.One, int _station = 1); + + /// + /// Tries to establish a connection with the device asynchronously + /// + Task ConnectAsync(); + + /// + /// Tries to establish a connection with the device asynchronously + /// + Task ConnectAsync(Action onTryingConfig); + + /// + /// Attaches a poller to the interface + /// + IPlcSerial WithPoller(); + + /// + /// Attaches a register collection object to + /// the interface that can be updated automatically. + /// + /// The type of the collection base class + IPlcSerial AddRegisterCollection(RegisterCollection collection); + + } + +} diff --git a/MewtocolNet/InternalEnums/CommandState.cs b/MewtocolNet/InternalEnums/CommandState.cs new file mode 100644 index 0000000..c755904 --- /dev/null +++ b/MewtocolNet/InternalEnums/CommandState.cs @@ -0,0 +1,12 @@ +namespace MewtocolNet { + + internal enum CommandState { + + Initial, + LineFeed, + RequestedNextFrame, + Complete + + } + +} \ No newline at end of file diff --git a/MewtocolNet/Logging/Logger.cs b/MewtocolNet/Logging/Logger.cs new file mode 100644 index 0000000..7e5ae3b --- /dev/null +++ b/MewtocolNet/Logging/Logger.cs @@ -0,0 +1,41 @@ +using System; + +namespace MewtocolNet.Logging { + + /// + /// Logging module for all PLCs + /// + public static class Logger { + + /// + /// Sets the loglevel for the logger module + /// + public static LogLevel LogLevel { get; set; } + + internal static Action LogInvoked; + + /// + /// Gets invoked whenever a new log message is ready + /// + public static void OnNewLogMessage(Action onMsg) { + + LogInvoked += (t, l, m) => { + onMsg(t, l, m); + }; + + } + + internal static void Log(string message, LogLevel loglevel, MewtocolInterface sender = null) { + + if ((int)loglevel <= (int)LogLevel) { + if (sender == null) { + LogInvoked?.Invoke(DateTime.Now, loglevel, message); + } else { + LogInvoked?.Invoke(DateTime.Now, loglevel, $"[{sender.GetConnectionInfo()}] {message}"); + } + } + + } + + } +} diff --git a/MewtocolNet/Mewtocol/Logging/LoggerEnums.cs b/MewtocolNet/Logging/LoggerEnums.cs similarity index 84% rename from MewtocolNet/Mewtocol/Logging/LoggerEnums.cs rename to MewtocolNet/Logging/LoggerEnums.cs index 385bdae..6bd2d19 100644 --- a/MewtocolNet/Mewtocol/Logging/LoggerEnums.cs +++ b/MewtocolNet/Logging/LoggerEnums.cs @@ -1,14 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace MewtocolNet.Logging { +namespace MewtocolNet.Logging { /// /// The loglevel of the logging module /// public enum LogLevel { + /// + /// Logs nothing + /// + None = -1, /// /// Logs only errors /// diff --git a/MewtocolNet/Mewtocol.cs b/MewtocolNet/Mewtocol.cs new file mode 100644 index 0000000..ca88908 --- /dev/null +++ b/MewtocolNet/Mewtocol.cs @@ -0,0 +1,94 @@ +using MewtocolNet.Exceptions; +using System; +using System.Collections.Generic; +using System.IO.Ports; +using System.Linq; +using System.Net; +using System.Text; + +namespace MewtocolNet { + + /// + /// Builder helper for mewtocol interfaces + /// + public static class Mewtocol { + + /// + /// Builds a ethernet based Mewtocol Interface + /// + /// + /// + /// Plc station number + /// + public static IPlcEthernet Ethernet (string ip, int port = 9094, int station = 1) { + + var instance = new MewtocolInterfaceTcp(); + instance.ConfigureConnection(ip, port, station); + return instance; + + } + + /// + /// Builds a ethernet based Mewtocol Interface + /// + /// + /// + /// Plc station number + /// + public static IPlcEthernet Ethernet(IPAddress ip, int port = 9094, int station = 1) { + + var instance = new MewtocolInterfaceTcp(); + instance.ConfigureConnection(ip, port, station); + return instance; + + } + + /// + /// Builds a serial port based Mewtocol Interface + /// + /// + /// + /// + /// + /// + /// + /// + public static IPlcSerial Serial (string portName, BaudRate baudRate = BaudRate._19200, DataBits dataBits = DataBits.Eight, Parity parity = Parity.Odd, StopBits stopBits = StopBits.One, int station = 1) { + + TestPortName(portName); + + var instance = new MewtocolInterfaceSerial(); + instance.ConfigureConnection(portName, (int)baudRate, (int)dataBits, parity, stopBits, station); + return instance; + + } + + /// + /// Builds a serial mewtocol interface that finds the correct settings for the given port name automatically + /// + /// + /// + /// + public static IPlcSerial SerialAuto (string portName, int station = 1) { + + TestPortName(portName); + + var instance = new MewtocolInterfaceSerial(); + instance.ConfigureConnection(portName, station); + instance.ConfigureConnectionAuto(); + return instance; + + } + + private static void TestPortName (string portName) { + + var portnames = SerialPort.GetPortNames(); + + if (!portnames.Any(x => x == portName)) + throw new MewtocolException($"The port {portName} is no valid port"); + + } + + } + +} diff --git a/MewtocolNet/Mewtocol/CpuInfo.cs b/MewtocolNet/Mewtocol/CpuInfo.cs deleted file mode 100644 index 3fa008b..0000000 --- a/MewtocolNet/Mewtocol/CpuInfo.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; - -namespace MewtocolNet.Registers { - - /// - /// Contains information about the plc and its cpu - /// - public partial class CpuInfo { - - /// - /// The cpu type of the plc - /// - public CpuType Cputype { get; set; } - - /// - /// Program capacity in 1K steps - /// - public int ProgramCapacity { get; set; } - - /// - /// Version of the cpu - /// - public string CpuVersion { get; set; } - - internal static CpuInfo BuildFromHexString (string _cpuType, string _cpuVersion, string _progCapacity) { - - CpuInfo retInf = new CpuInfo(); - - switch (_cpuType) { - case "02": - retInf.Cputype = CpuType.FP5_16K; - break; - case "03": - retInf.Cputype = CpuType.FP3_C_10K; - break; - case "04": - retInf.Cputype = CpuType.FP1_M_0_9K; - break; - case "05": - retInf.Cputype = CpuType.FP0_FP1_2_7K; - break; - case "06": - retInf.Cputype = CpuType.FP0_FP1_5K_10K; - break; - case "12": - retInf.Cputype = CpuType.FP5_24K; - break; - case "13": - retInf.Cputype = CpuType.FP3_C_16K; - break; - case "20": - retInf.Cputype = CpuType.FP_Sigma_X_H_30K_60K_120K; - break; - case "50": - retInf.Cputype = CpuType.FP2_16K_32K; - break; - } - - retInf.ProgramCapacity = Convert.ToInt32(_progCapacity); - retInf.CpuVersion = _cpuVersion.Insert(1, "."); - return retInf; - - } - - } - -} \ No newline at end of file diff --git a/MewtocolNet/Mewtocol/DynamicInterface.cs b/MewtocolNet/Mewtocol/DynamicInterface.cs deleted file mode 100644 index 1fe5054..0000000 --- a/MewtocolNet/Mewtocol/DynamicInterface.cs +++ /dev/null @@ -1,490 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MewtocolNet.Logging; -using MewtocolNet.RegisterAttributes; -using MewtocolNet.Registers; - -namespace MewtocolNet { - - /// - /// The PLC com interface class - /// - public partial class MewtocolInterface { - - /// - /// True if the auto poller is currently paused - /// - public bool PollingPaused => pollerIsPaused; - - /// - /// True if the poller is actvice (can be paused) - /// - public bool PollerActive => !pollerTaskStopped; - - internal event Action PolledCycle; - - internal volatile bool pollerTaskRunning; - internal volatile bool pollerTaskStopped; - internal volatile bool pollerIsPaused; - internal volatile bool pollerFirstCycle = false; - - internal bool usePoller = false; - - #region Register Polling - - /// - /// Kills the poller completely - /// - internal void KillPoller () { - - pollerTaskRunning = false; - pollerTaskStopped = true; - - ClearRegisterVals(); - - } - - /// - /// Pauses the polling and waits for the last message to be sent - /// - /// - public async Task PausePollingAsync () { - - if (!pollerTaskRunning) - return; - - pollerTaskRunning = false; - - while (!pollerIsPaused) { - - if (pollerIsPaused) - break; - - await Task.Delay(10); - - } - - pollerTaskRunning = false; - - } - - /// - /// Resumes the polling - /// - public void ResumePolling () { - - pollerTaskRunning = true; - - } - - /// - /// Attaches a continous reader that reads back the Registers and Contacts - /// - internal void AttachPoller () { - - if (pollerTaskRunning) - return; - - pollerFirstCycle = true; - - Task.Factory.StartNew(async () => { - - Logger.Log("Poller is attaching", LogLevel.Info, this); - - int iteration = 0; - - pollerTaskStopped = false; - pollerTaskRunning = true; - pollerIsPaused = false; - - while (!pollerTaskStopped) { - - while (pollerTaskRunning) { - - if (iteration >= Registers.Count + 1) { - iteration = 0; - //invoke cycle polled event - InvokePolledCycleDone(); - continue; - } - - if (iteration >= Registers.Count) { - await GetPLCInfoAsync(); - iteration++; - continue; - } - - var reg = Registers[iteration]; - - if (reg is NRegister shortReg) { - var lastVal = shortReg.Value; - var readout = (await ReadNumRegister(shortReg)).Register.Value; - if (lastVal != readout) { - InvokeRegisterChanged(shortReg); - } - } - if (reg is NRegister ushortReg) { - var lastVal = ushortReg.Value; - var readout = (await ReadNumRegister(ushortReg)).Register.Value; - if (lastVal != readout) { - InvokeRegisterChanged(ushortReg); - } - } - if (reg is NRegister intReg) { - var lastVal = intReg.Value; - var readout = (await ReadNumRegister(intReg)).Register.Value; - if (lastVal != readout) { - InvokeRegisterChanged(intReg); - } - } - if (reg is NRegister uintReg) { - var lastVal = uintReg.Value; - var readout = (await ReadNumRegister(uintReg)).Register.Value; - if (lastVal != readout) { - InvokeRegisterChanged(uintReg); - } - } - if (reg is NRegister floatReg) { - var lastVal = floatReg.Value; - var readout = (await ReadNumRegister(floatReg)).Register.Value; - if (lastVal != readout) { - InvokeRegisterChanged(floatReg); - } - } - if (reg is NRegister tsReg) { - var lastVal = tsReg.Value; - var readout = (await ReadNumRegister(tsReg)).Register.Value; - if (lastVal != readout) { - InvokeRegisterChanged(tsReg); - } - } - if (reg is BRegister boolReg) { - var lastVal = boolReg.Value; - var readout = (await ReadBoolRegister(boolReg)).Register.Value; - if (lastVal != readout) { - InvokeRegisterChanged(boolReg); - } - } - if (reg is SRegister stringReg) { - var lastVal = stringReg.Value; - var readout = (await ReadStringRegister(stringReg)).Register.Value; - if (lastVal != readout) { - InvokeRegisterChanged(stringReg); - } - } - - iteration++; - pollerFirstCycle = false; - - await Task.Delay(pollerDelayMs); - - } - - pollerIsPaused = !pollerTaskRunning; - - } - - pollerIsPaused = false; - - }); - - } - - internal void PropertyRegisterWasSet (string propName, object value) { - - SetRegister(propName, value); - - } - - #endregion - - #region Register Adding - - - /// - /// Adds a PLC memory register to the watchlist - /// The registers can be read back by attaching - /// - /// The address of the register in the PLCs memory - /// - /// The memory area type - /// X = Physical input area (bool) - /// Y = Physical input area (bool) - /// R = Internal relay area (bool) - /// DT = Internal data area (short/ushort) - /// DDT = Internal relay area (int/uint) - /// - /// A naming definition for QOL, doesn't effect PLC and is optional - public void AddRegister (int _address, RegisterType _type, string _name = null) { - - IRegister toAdd = null; - - //as number registers - if (_type == RegisterType.DT_short) { - toAdd = new NRegister(_address, _name); - } - if (_type == RegisterType.DT_ushort) { - toAdd = new NRegister(_address, _name); - } - if (_type == RegisterType.DDT_int) { - toAdd = new NRegister(_address, _name); - } - if (_type == RegisterType.DDT_uint) { - toAdd = new NRegister(_address, _name); - } - if (_type == RegisterType.DDT_float) { - toAdd = new NRegister(_address, _name); - } - - if(toAdd == null) { - toAdd = new BRegister(_address, _type, _name); - } - - Registers.Add(toAdd); - - } - - internal void AddRegister (Type _colType, int _address, RegisterType _type, string _name = null) { - - IRegister toAdd = null; - - //as number registers - if (_type == RegisterType.DT_short) { - toAdd = new NRegister(_address, _name).WithCollectionType(_colType); - } - if (_type == RegisterType.DT_ushort) { - toAdd = new NRegister(_address, _name).WithCollectionType(_colType); - } - if (_type == RegisterType.DDT_int) { - toAdd = new NRegister(_address, _name).WithCollectionType(_colType); - } - if (_type == RegisterType.DDT_uint) { - toAdd = new NRegister(_address, _name).WithCollectionType(_colType); - } - if (_type == RegisterType.DDT_float) { - toAdd = new NRegister(_address, _name).WithCollectionType(_colType); - } - - if (toAdd == null) { - toAdd = new BRegister(_address, _type, _name).WithCollectionType(_colType); - } - - Registers.Add(toAdd); - - } - - /// - /// Adds a PLC memory register to the watchlist - /// The registers can be read back by attaching - /// - /// The special address of the register in the PLCs memory - /// - /// The memory area type - /// X = Physical input area (bool) - /// Y = Physical input area (bool) - /// R = Internal relay area (bool) - /// DT = Internal data area (short/ushort) - /// DDT = Internal relay area (int/uint) - /// - /// A naming definition for QOL, doesn't effect PLC and is optional - public void AddRegister (SpecialAddress _spAddress, RegisterType _type, string _name = null) { - - //as bool registers - Registers.Add(new BRegister(_spAddress, _type, _name)); - - } - - internal void AddRegister (Type _colType, SpecialAddress _spAddress, RegisterType _type, string _name = null) { - - var reg = new BRegister(_spAddress, _type, _name); - - reg.collectionType = _colType; - - //as bool registers - Registers.Add(reg); - - } - - /// - /// Adds a PLC memory register to the watchlist - /// The registers can be read back by attaching - /// - /// - /// The type of the register translated from C# to IEC 61131-3 types - /// C# ------ IEC - /// short => INT/WORD - /// ushort => UINT - /// int => DOUBLE - /// uint => UDOUBLE - /// float => REAL - /// string => STRING - /// - /// A naming definition for QOL, doesn't effect PLC and is optional - /// The address of the register in the PLCs memory - /// The length of the string (Can be ignored for other types) - public void AddRegister(int _address, int _length = 1, string _name = null) { - - Type regType = typeof(T); - - if (regType != typeof(string) && _length != 1) { - throw new NotSupportedException($"_lenght parameter only allowed for register of type string"); - } - - IRegister toAdd; - - if (regType == typeof(short)) { - toAdd = new NRegister(_address, _name); - } else if (regType == typeof(ushort)) { - toAdd = new NRegister(_address, _name); - } else if (regType == typeof(int)) { - toAdd = new NRegister(_address, _name); - } else if (regType == typeof(uint)) { - toAdd = new NRegister(_address, _name); - } else if (regType == typeof(float)) { - toAdd = new NRegister(_address, _name); - } else if (regType == typeof(string)) { - toAdd = new SRegister(_address, _length, _name); - } else if (regType == typeof(TimeSpan)) { - toAdd = new NRegister(_address, _name); - } else if (regType == typeof(bool)) { - toAdd = new BRegister(_address, RegisterType.R, _name); - } else { - throw new NotSupportedException($"The type {regType} is not allowed for Registers \n" + - $"Allowed are: short, ushort, int, uint, float and string"); - } - - - if (Registers.Any(x => x.GetRegisterPLCName() == toAdd.GetRegisterPLCName())) { - throw new NotSupportedException($"Cannot add a register multiple times, " + - $"make sure that all register attributes or AddRegister assignments have different adresses."); - } - - } - - //Internal register adding for auto register collection building - internal void AddRegister (Type _colType, int _address, PropertyInfo boundProp, int _length = 1, bool _isBitwise = false, Type _enumType = null) { - - Type regType = typeof(T); - - if (regType != typeof(string) && _length != 1) { - throw new NotSupportedException($"_lenght parameter only allowed for register of type string"); - } - - if (Registers.Any(x => x.MemoryAddress == _address) && _isBitwise) { - return; - } - - IRegister reg = null; - - string propName = boundProp.Name; - - //rename the property name to prevent duplicate names in case of a bitwise prop - if(_isBitwise && regType == typeof(short)) - propName = $"Auto_Bitwise_DT{_address}"; - - if (_isBitwise && regType == typeof(int)) - propName = $"Auto_Bitwise_DDT{_address}"; - - if (regType == typeof(short)) { - reg = new NRegister(_address, propName, _isBitwise, _enumType).WithCollectionType(_colType); - } else if (regType == typeof(ushort)) { - reg = new NRegister(_address, propName).WithCollectionType(_colType); - } else if (regType == typeof(int)) { - reg = new NRegister(_address, propName, _isBitwise, _enumType).WithCollectionType(_colType); - } else if (regType == typeof(uint)) { - reg = new NRegister(_address, propName).WithCollectionType(_colType); - } else if (regType == typeof(float)) { - reg = new NRegister(_address, propName).WithCollectionType(_colType); - } else if (regType == typeof(string)) { - reg = new SRegister(_address, _length, propName).WithCollectionType(_colType); - } else if (regType == typeof(TimeSpan)) { - reg = new NRegister(_address, propName).WithCollectionType(_colType); - } else if (regType == typeof(bool)) { - reg = new BRegister(_address, RegisterType.R, propName).WithCollectionType(_colType); - } - - if (reg == null) { - throw new NotSupportedException($"The type {regType} is not allowed for Registers \n" + - $"Allowed are: short, ushort, int, uint, float and string"); - } - - if (Registers.Any(x => x.GetRegisterPLCName() == reg.GetRegisterPLCName()) && !_isBitwise) { - throw new NotSupportedException($"Cannot add a register multiple times, " + - $"make sure that all register attributes or AddRegister assignments have different adresses."); - } - - Registers.Add(reg); - - } - - #endregion - - #region Register accessing - - /// - /// Gets a register that was added by its name - /// - /// - public IRegister GetRegister (string name) { - - return Registers.FirstOrDefault(x => x.Name == name); - - } - - /// - /// Gets a register that was added by its name - /// - /// The type of register - /// A casted register or the default value - public T GetRegister (string name) where T : IRegister { - try { - - var reg = Registers.FirstOrDefault(x => x.Name == name); - return (T)reg; - - } catch (InvalidCastException) { - - return default(T); - - } - - } - - #endregion - - #region Register Reading - - /// - /// Gets a list of all added registers - /// - public List GetAllRegisters () { - - return Registers; - - } - - #endregion - - #region Event Invoking - - internal void InvokeRegisterChanged (IRegister reg) { - - RegisterChanged?.Invoke(reg); - - } - - internal void InvokePolledCycleDone () { - - PolledCycle?.Invoke(); - - } - - #endregion - - } -} diff --git a/MewtocolNet/Mewtocol/IRegister.cs b/MewtocolNet/Mewtocol/IRegister.cs deleted file mode 100644 index 524bf15..0000000 --- a/MewtocolNet/Mewtocol/IRegister.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Text; - -namespace MewtocolNet { - - /// - /// An interface for all register types - /// - public interface IRegister { - - /// - /// Gets called whenever the value was changed - /// - event Action ValueChanged; - - /// - /// The name of the register - /// - string Name { get; } - - /// - /// The current value of the register - /// - object Value { get; } - - /// - /// The plc memory address of the register - /// - int MemoryAddress { get; } - - /// - /// Indicates if the register is processed bitwise - /// - /// True if bitwise - bool IsUsedBitwise(); - - /// - /// Generates a string describing the starting memory address of the register - /// - string GetStartingMemoryArea(); - - /// - /// Gets the current value formatted as a readable string - /// - string GetValueString(); - - /// - /// Builds the identifier for the mewtocol query string - /// - /// - string BuildMewtocolQuery(); - - /// - /// Builds a register string that prepends the memory address fe. DDT or DT, X, Y etc - /// - string GetRegisterString(); - - /// - /// Builds a combined name for the attached property to uniquely identify the property register binding - /// - /// - string GetCombinedName(); - - /// - /// Gets the name of the class that contains the attached property - /// - /// - string GetContainerName(); - - /// - /// Builds a register name after the PLC convention
- /// Example DDT100, XA, X6, Y1, DT3300 - ///
- string GetRegisterPLCName(); - - /// - /// Clears the current value of the register and resets it to default - /// - void ClearValue(); - - /// - /// Triggers a notifychanged update event - /// - void TriggerNotifyChange(); - - /// - /// Gets the type of the class collection its attached property is in or null - /// - /// The class name or null if manually added - Type GetCollectionType(); - - /// - /// Builds a readable string with all important register informations - /// - string ToString(); - - } - -} diff --git a/MewtocolNet/Mewtocol/Logging/Logger.cs b/MewtocolNet/Mewtocol/Logging/Logger.cs deleted file mode 100644 index ee4d311..0000000 --- a/MewtocolNet/Mewtocol/Logging/Logger.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace MewtocolNet.Logging { - - /// - /// Logging module for all PLCs - /// - public static class Logger { - - /// - /// Sets the loglevel for the logger module - /// - public static LogLevel LogLevel { get; set; } - - internal static Action LogInvoked; - - /// - /// Gets invoked whenever a new log message is ready - /// - public static void OnNewLogMessage (Action onMsg) { - - LogInvoked += (t, m) => { - onMsg(t, m); - }; - - } - - internal static void Log (string message, LogLevel loglevel, MewtocolInterface sender = null) { - - if ((int)loglevel <= (int)LogLevel) { - if (sender == null) { - LogInvoked?.Invoke(DateTime.Now, message); - } else { - LogInvoked?.Invoke(DateTime.Now, $"[{sender.GetConnectionPortInfo()}] {message}"); - } - } - - } - - } -} diff --git a/MewtocolNet/Mewtocol/MewtocolHelpers.cs b/MewtocolNet/Mewtocol/MewtocolHelpers.cs deleted file mode 100644 index 1b82ccc..0000000 --- a/MewtocolNet/Mewtocol/MewtocolHelpers.cs +++ /dev/null @@ -1,244 +0,0 @@ -using System; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Collections.Generic; -using MewtocolNet.Registers; -using System.Collections; - -namespace MewtocolNet { - - /// - /// Contains helper methods - /// - public static class MewtocolHelpers { - - /// - /// Turns a bit array into a 0 and 1 string - /// - public static string ToBitString (this BitArray arr) { - - var bits = new bool[arr.Length]; - arr.CopyTo(bits, 0); - return string.Join("", bits.Select(x => x ? "1" : "0")); - - } - - /// - /// Converts a string (after converting to upper case) to ascii bytes - /// - internal static byte[] ToHexASCIIBytes (this string _str) { - - ASCIIEncoding ascii = new ASCIIEncoding(); - byte[] bytes = ascii.GetBytes(_str.ToUpper()); - return bytes; - - } - - internal static string BuildBCCFrame (this string asciiArr) { - - Encoding ae = Encoding.ASCII; - byte[] b = ae.GetBytes(asciiArr); - byte xorTotalByte = 0; - for(int i = 0; i < b.Length; i++) - xorTotalByte ^= b[i]; - return asciiArr.Insert(asciiArr.Length, xorTotalByte.ToString("X2")); - - } - - /// - /// Parses the byte string from a incoming RD message - /// - internal static string ParseDTByteString (this string _onString, int _blockSize = 4) { - - if (_onString == null) - return null; - - var res = new Regex(@"\%([0-9]{2})\$RD(.{" + _blockSize + "})").Match(_onString); - if (res.Success) { - string val = res.Groups[2].Value; - return val; - } - return null; - - } - - internal static bool? ParseRCSingleBit (this string _onString) { - - var res = new Regex(@"\%([0-9]{2})\$RC(.)").Match(_onString); - if (res.Success) { - string val = res.Groups[2].Value; - return val == "1"; - } - return null; - - } - - internal static string ParseDTString (this string _onString) { - - var res = new Regex(@"\%([0-9]{2})\$RD.{8}(.*)...").Match(_onString); - if(res.Success) { - string val = res.Groups[2].Value; - return val.GetStringFromAsciiHex()?.Trim(); - } - return null; - - } - - internal static string ReverseByteOrder (this string _onString) { - - if(_onString == null) return null; - - //split into 2 chars - var stringBytes = _onString.SplitInParts(2).ToList(); - - stringBytes.Reverse(); - - return string.Join("", stringBytes); - - } - - internal static IEnumerable SplitInParts (this string s, int partLength) { - - if (s == null) - throw new ArgumentNullException(nameof(s)); - if (partLength <= 0) - throw new ArgumentException("Part length has to be positive.", nameof(partLength)); - - for (var i = 0; i < s.Length; i += partLength) - yield return s.Substring(i, Math.Min(partLength, s.Length - i)); - - } - - internal static string BuildDTString (this string _inString, short _stringReservedSize) { - - StringBuilder sb = new StringBuilder(); - - //clamp string lenght - if (_inString.Length > _stringReservedSize) { - _inString = _inString.Substring(0, _stringReservedSize); - } - - //actual string content - var hexstring = _inString.GetAsciiHexFromString(); - - var sizeBytes = BitConverter.GetBytes((short)(hexstring.Length / 2)).ToHexString(); - - if (hexstring.Length >= 2) { - - var remainderBytes = (hexstring.Length / 2) % 2; - - if (remainderBytes != 0) { - hexstring += "20"; - } - - } - - var reservedSizeBytes = BitConverter.GetBytes(_stringReservedSize).ToHexString(); - - //reserved string count bytes - sb.Append(reservedSizeBytes); - //string count actual bytes - sb.Append(sizeBytes); - - - sb.Append(hexstring); - - return sb.ToString(); - } - - - internal static string GetStringFromAsciiHex (this string input) { - if (input.Length % 2 != 0) - return null; - byte[] bytes = new byte[input.Length / 2]; - for (int i = 0; i < input.Length; i += 2) { - String hex = input.Substring(i, 2); - bytes[i/2] = Convert.ToByte(hex, 16); - } - return Encoding.ASCII.GetString(bytes); - } - - internal static string GetAsciiHexFromString (this string input) { - var bytes = new ASCIIEncoding().GetBytes(input); - return bytes.ToHexString(); - } - - internal static byte[] HexStringToByteArray(this string hex) { - if (hex == null) - return null; - return Enumerable.Range(0, hex.Length) - .Where(x => x % 2 == 0) - .Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) - .ToArray(); - } - - internal static string ToHexString (this byte[] arr) { - - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < arr.Length; i++) { - byte b = arr[i]; - sb.Append(b.ToString("X2")); - } - return sb.ToString(); - - } - - internal static byte[] BigToMixedEndian (this byte[] arr) { - - List oldBL = new List(arr); - - List tempL = new List(); - - //make the input list even - if(arr.Length % 2 != 0) - oldBL.Add((byte)0); - - for (int i = 0; i < oldBL.Count; i+=2) { - byte firstByte = oldBL[i]; - byte lastByte = oldBL[i + 1]; - tempL.Add(lastByte); - tempL.Add(firstByte); - - } - - return tempL.ToArray(); - - } - - internal static bool IsDoubleNumericRegisterType (this Type type) { - - //Type[] singles = new Type[] { - // typeof(short), - // typeof(ushort), - //}; - - Type[] doubles = new Type[] { - typeof(int), - typeof(uint), - typeof(float), - typeof(TimeSpan), - }; - - return doubles.Contains(type); - - } - - internal static bool IsNumericSupportedType (this Type type) { - - Type[] supported = new Type[] { - typeof(short), - typeof(ushort), - typeof(int), - typeof(uint), - typeof(float), - typeof(TimeSpan), - }; - - return supported.Contains(type); - - } - - } - -} \ No newline at end of file diff --git a/MewtocolNet/Mewtocol/MewtocolInterface.cs b/MewtocolNet/Mewtocol/MewtocolInterface.cs deleted file mode 100644 index 9062077..0000000 --- a/MewtocolNet/Mewtocol/MewtocolInterface.cs +++ /dev/null @@ -1,904 +0,0 @@ -using System; -using System.IO; -using System.Net.Sockets; -using System.Text; -using System.Text.RegularExpressions; -using System.Collections.Generic; -using System.Threading.Tasks; -using System.Linq; -using MewtocolNet.Registers; -using MewtocolNet.RegisterAttributes; -using MewtocolNet.Logging; -using System.Collections; -using System.Diagnostics; -using System.ComponentModel; -using System.Net; -using System.Threading; -using MewtocolNet.Queue; -using System.Reflection; -using System.Timers; - -namespace MewtocolNet -{ - - /// - /// The PLC com interface class - /// - public partial class MewtocolInterface : INotifyPropertyChanged, IDisposable { - - /// - /// Gets triggered when the PLC connection was established - /// - public event Action Connected; - - /// - /// Gets triggered when the PLC connection was closed or lost - /// - public event Action Disconnected; - - /// - /// Gets triggered when a registered data register changes its value - /// - public event Action RegisterChanged; - - /// - /// Gets triggered when a property of the interface changes - /// - public event PropertyChangedEventHandler PropertyChanged; - - private int connectTimeout = 3000; - /// - /// The initial connection timeout in milliseconds - /// - public int ConnectTimeout { - get { return connectTimeout; } - set { connectTimeout = value; } - } - - private volatile int pollerDelayMs = 0; - /// - /// Delay for each poller cycle in milliseconds, default = 0 - /// - public int PollerDelayMs { - get => pollerDelayMs; - set { - pollerDelayMs = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PollerDelayMs))); - } - } - - private volatile int queuedMessages; - /// - /// Currently queued Messages - /// - public int QueuedMessages { - get => queuedMessages; - } - - /// - /// The host ip endpoint, leave it null to use an automatic interface - /// - public IPEndPoint HostEndpoint { get; set; } - - private bool isConnected; - /// - /// The current connection state of the interface - /// - public bool IsConnected { - get => isConnected; - private set { - isConnected = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsConnected))); - } - } - - private bool disposed; - /// - /// True if the current interface was disposed - /// - public bool Disposed { - get { return disposed; } - private set { disposed = value; } - } - - - private PLCInfo plcInfo; - /// - /// Generic information about the connected PLC - /// - public PLCInfo PlcInfo { - get => plcInfo; - private set { - plcInfo = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PlcInfo))); - } - } - - /// - /// The registered data registers of the PLC - /// - public List Registers { get; set; } = new List(); - - private string ip; - private int port; - private int stationNumber; - private int cycleTimeMs = 25; - - private int bytesTotalCountedUpstream = 0; - private int bytesTotalCountedDownstream = 0; - - /// - /// The current IP of the PLC connection - /// - public string IpAddress => ip; - /// - /// The current port of the PLC connection - /// - public int Port => port; - /// - /// The station number of the PLC - /// - public int StationNumber => stationNumber; - - /// - /// The duration of the last message cycle - /// - public int CycleTimeMs { - get { return cycleTimeMs; } - private set { - cycleTimeMs = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CycleTimeMs))); - } - } - - private int bytesPerSecondUpstream = 0; - /// - /// The current transmission speed in bytes per second - /// - public int BytesPerSecondUpstream { - get { return bytesPerSecondUpstream; } - private set { - bytesPerSecondUpstream = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BytesPerSecondUpstream))); - } - } - - private int bytesPerSecondDownstream = 0; - /// - /// The current transmission speed in bytes per second - /// - public int BytesPerSecondDownstream { - get { return bytesPerSecondDownstream; } - private set { - bytesPerSecondDownstream = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BytesPerSecondDownstream))); - } - } - - internal NetworkStream stream; - internal TcpClient client; - internal readonly SerialQueue queue = new SerialQueue(); - private int RecBufferSize = 128; - internal int SendExceptionsInRow = 0; - internal bool ImportantTaskRunning = false; - - private Stopwatch speedStopwatchUpstr; - private Stopwatch speedStopwatchDownstr; - - #region Initialization - - /// - /// Builds a new Interfacer for a PLC - /// - /// IP adress of the PLC - /// Port of the PLC - /// Station Number of the PLC - public MewtocolInterface (string _ip, int _port = 9094, int _station = 1) { - - ip = _ip; - port = _port; - stationNumber = _station; - - Connected += MewtocolInterface_Connected; - - void MewtocolInterface_Connected (PLCInfo obj) { - - if (usePoller) - AttachPoller(); - - IsConnected = true; - - } - - RegisterChanged += (o) => { - - string address = $"{o.GetRegisterString()}{o.GetStartingMemoryArea()}".PadRight(5, (char)32); - - Logger.Log($"{address} " + - $"{(o.Name != null ? $"({o.Name}) " : "")}" + - $"changed to \"{o.GetValueString()}\"", LogLevel.Change, this); - }; - - } - - #endregion - - #region Setup - - /// - /// Trys to connect to the PLC by the IP given in the constructor - /// - /// - /// Gets called when a connection with a PLC was established - /// - /// If is used it waits for the first data receive cycle to complete - /// - /// Gets called when an error or timeout during connection occurs - /// - public async Task ConnectAsync (Action OnConnected = null, Action OnFailed = null) { - - Logger.Log("Connecting to PLC...", LogLevel.Info, this); - - var plcinf = await GetPLCInfoAsync(); - - if (plcinf != null) { - - Logger.Log("Connected", LogLevel.Info, this); - Logger.Log($"\n\n{plcinf.ToString()}\n\n", LogLevel.Verbose, this); - - Connected?.Invoke(plcinf); - - if (OnConnected != null) { - - if (!usePoller) { - OnConnected(plcinf); - return this; - } - - PolledCycle += OnPollCycleDone; - void OnPollCycleDone () { - OnConnected(plcinf); - PolledCycle -= OnPollCycleDone; - } - } - - } else { - - if (OnFailed != null) { - OnFailed(); - Disconnected?.Invoke(); - Logger.Log("Initial connection failed", LogLevel.Info, this); - } - - } - - return this; - - } - - /// - /// Changes the connections parameters of the PLC, only applyable when the connection is offline - /// - /// Ip adress - /// Port number - /// Station number - public void ChangeConnectionSettings (string _ip, int _port, int _station = 1) { - - if (IsConnected) - throw new Exception("Cannot change the connection settings while the PLC is connected"); - - ip = _ip; - port = _port; - stationNumber = _station; - - } - - /// - /// Closes the connection all cyclic polling - /// - public void Disconnect () { - - if (!IsConnected) - return; - - OnMajorSocketExceptionWhileConnected(); - - } - - /// - /// Attaches a poller to the interface that continously - /// polls the registered data registers and writes the values to them - /// - public MewtocolInterface WithPoller () { - - usePoller = true; - - return this; - - } - - #endregion - - #region TCP connection state handling - - private async Task ConnectTCP () { - - if (!IPAddress.TryParse(ip, out var targetIP)) { - throw new ArgumentException("The IP adress of the PLC was no valid format"); - } - - try { - - if(HostEndpoint != null) { - - client = new TcpClient(HostEndpoint) { - ReceiveBufferSize = RecBufferSize, - NoDelay = false, - }; - var ep = (IPEndPoint)client.Client.LocalEndPoint; - Logger.Log($"Connecting [MAN] endpoint: {ep.Address}:{ep.Port}", LogLevel.Verbose, this); - - } else { - - client = new TcpClient() { - ReceiveBufferSize = RecBufferSize, - NoDelay = false, - ExclusiveAddressUse = true, - }; - - } - - var result = client.BeginConnect(targetIP, port, null, null); - var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(ConnectTimeout)); - - if(!success || !client.Connected) { - OnMajorSocketExceptionWhileConnecting(); - return; - } - - if(HostEndpoint == null) { - var ep = (IPEndPoint)client.Client.LocalEndPoint; - Logger.Log($"Connecting [AUTO] endpoint: {ep.Address.MapToIPv4()}:{ep.Port}", LogLevel.Verbose, this); - } - - stream = client.GetStream(); - stream.ReadTimeout = 1000; - - await Task.CompletedTask; - - } catch (SocketException) { - - OnMajorSocketExceptionWhileConnecting(); - - } - - } - - private void OnMajorSocketExceptionWhileConnecting () { - - Logger.Log("The PLC connection timed out", LogLevel.Error, this); - CycleTimeMs = 0; - IsConnected = false; - KillPoller(); - - } - - private void OnMajorSocketExceptionWhileConnected () { - - if (IsConnected) { - - Logger.Log("The PLC connection was closed", LogLevel.Error, this); - CycleTimeMs = 0; - IsConnected = false; - Disconnected?.Invoke(); - KillPoller(); - client.Close(); - - } - - } - - private void ClearRegisterVals () { - - for (int i = 0; i < Registers.Count; i++) { - - var reg = Registers[i]; - reg.ClearValue(); - - } - - } - - #endregion - - #region Register Collection - - /// - /// Attaches a register collection object to - /// the interface that can be updated automatically. - /// - /// Just create a class inheriting from - /// and assert some propertys with the custom . - /// - /// A collection inherting the class - public MewtocolInterface WithRegisterCollection (RegisterCollectionBase collection) { - - collection.PLCInterface = this; - - var props = collection.GetType().GetProperties(); - - foreach (var prop in props) { - - var attributes = prop.GetCustomAttributes(true); - - string propName = prop.Name; - foreach (var attr in attributes) { - - if (attr is RegisterAttribute cAttribute) { - - if (prop.PropertyType == typeof(bool) && cAttribute.AssignedBitIndex == -1) { - if (cAttribute.SpecialAddress == SpecialAddress.None) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, cAttribute.RegisterType, _name: propName); - } else { - AddRegister(collection.GetType(), cAttribute.SpecialAddress, cAttribute.RegisterType, _name: propName); - } - } - - if (prop.PropertyType == typeof(short)) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop); - } - - if (prop.PropertyType == typeof(ushort)) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop); - } - - if (prop.PropertyType == typeof(int)) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop); - } - - if (prop.PropertyType == typeof(uint)) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop); - } - - if (prop.PropertyType == typeof(float)) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop); - } - - if (prop.PropertyType == typeof(string)) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop, cAttribute.StringLength); - } - - if (prop.PropertyType.IsEnum) { - - if (cAttribute.BitCount == BitCount.B16) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop, _enumType: prop.PropertyType); - } else { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop, _enumType: prop.PropertyType); - } - - } - - //read number as bit array - if (prop.PropertyType == typeof(BitArray)) { - - if (cAttribute.BitCount == BitCount.B16) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop, _isBitwise: true); - } else { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop, _isBitwise: true); - } - - } - - //read number as bit array by invdividual properties - if (prop.PropertyType == typeof(bool) && cAttribute.AssignedBitIndex != -1) { - - //var bitwiseCount = Registers.Count(x => x.Value.isUsedBitwise); - - if (cAttribute.BitCount == BitCount.B16) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop, _isBitwise: true); - } else { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop, _isBitwise: true); - } - - } - - if (prop.PropertyType == typeof(TimeSpan)) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop); - } - - } - - } - - } - - RegisterChanged += (reg) => { - - //register is used bitwise - if(reg.IsUsedBitwise()) { - - for (int i = 0; i < props.Length; i++) { - - var prop = props[i]; - var bitWiseFound = prop.GetCustomAttributes(true) - .FirstOrDefault(y => y.GetType() == typeof(RegisterAttribute) && ((RegisterAttribute)y).MemoryArea == reg.MemoryAddress); - - if(bitWiseFound != null) { - - var casted = (RegisterAttribute)bitWiseFound; - var bitIndex = casted.AssignedBitIndex; - - BitArray bitAr = null; - - if (reg is NRegister reg16) { - var bytes = BitConverter.GetBytes((short)reg16.Value); - bitAr = new BitArray(bytes); - } else if(reg is NRegister reg32) { - var bytes = BitConverter.GetBytes((int)reg32.Value); - bitAr = new BitArray(bytes); - } - - if (bitAr != null && bitIndex < bitAr.Length && bitIndex >= 0) { - - //set the specific bit index if needed - prop.SetValue(collection, bitAr[bitIndex]); - collection.TriggerPropertyChanged(prop.Name); - - } else if (bitAr != null) { - - //set the specific bit array if needed - prop.SetValue(collection, bitAr); - collection.TriggerPropertyChanged(prop.Name); - - } - - } - - } - - } - - //updating normal properties - var foundToUpdate = props.FirstOrDefault(x => x.Name == reg.Name); - - if (foundToUpdate != null) { - - var foundAttributes = foundToUpdate.GetCustomAttributes(true); - var foundAttr = foundAttributes.FirstOrDefault(x => x.GetType() == typeof(RegisterAttribute)); - - if (foundAttr == null) - return; - - var registerAttr = (RegisterAttribute)foundAttr; - - //check if bit parse mode - if (registerAttr.AssignedBitIndex == -1) { - - HashSet NumericTypes = new HashSet { - typeof(bool), - typeof(short), - typeof(ushort), - typeof(int), - typeof(uint), - typeof(float), - typeof(TimeSpan), - typeof(string) - }; - - var regValue = ((IRegister)reg).Value; - - if (NumericTypes.Any(x => foundToUpdate.PropertyType == x)) { - foundToUpdate.SetValue(collection, regValue); - } - - if (foundToUpdate.PropertyType.IsEnum) { - foundToUpdate.SetValue(collection, regValue); - } - - } - - collection.TriggerPropertyChanged(foundToUpdate.Name); - - } - - }; - - if (collection != null) - collection.OnInterfaceLinked(this); - - Connected += (i) => { - if (collection != null) - collection.OnInterfaceLinkedAndOnline(this); - }; - - return this; - - } - - #endregion - - #region Register Writing - - /// - /// Sets a register in the PLCs memory - /// - /// The name the register was given to or a property name from the RegisterCollection class - /// The value to write to the register - public void SetRegister (string registerName, object value) { - - var foundRegister = GetAllRegisters().FirstOrDefault(x => x.Name == registerName); - - if (foundRegister == null) { - throw new Exception($"Register with the name {registerName} was not found"); - } - - _ = SetRegisterAsync(registerName, value); - - } - - /// - /// Sets a register in the PLCs memory asynchronously, returns the result status from the PLC - /// - /// The name the register was given to or a property name from the RegisterCollection class - /// The value to write to the register - public async Task SetRegisterAsync (string registerName, object value) { - - var foundRegister = GetAllRegisters().FirstOrDefault(x => x.Name == registerName); - - if (foundRegister == null) { - throw new Exception($"Register with the name {registerName} was not found"); - } - - if (foundRegister.GetType() == typeof(BRegister)) { - - return await WriteBoolRegister((BRegister)foundRegister, (bool)value); - - } - - if (foundRegister.GetType() == typeof(NRegister)) { - - return await WriteNumRegister((NRegister)foundRegister, (short)value); - - } - - if (foundRegister.GetType() == typeof(NRegister)) { - - return await WriteNumRegister((NRegister)foundRegister, (ushort)value); - - } - - if (foundRegister.GetType() == typeof(NRegister)) { - - return await WriteNumRegister((NRegister)foundRegister, (int)value); - - } - - if (foundRegister.GetType() == typeof(NRegister)) { - - return await WriteNumRegister((NRegister)foundRegister, (uint)value); - - } - - if (foundRegister.GetType() == typeof(NRegister)) { - - return await WriteNumRegister((NRegister)foundRegister, (float)value); - - } - - if (foundRegister.GetType() == typeof(NRegister)) { - - return await WriteNumRegister((NRegister)foundRegister, (TimeSpan)value); - - } - - if (foundRegister.GetType() == typeof(SRegister)) { - - return await WriteStringRegister((SRegister)foundRegister, (string)value); - - } - - return false; - - } - - #endregion - - #region Low level command handling - - /// - /// Calculates checksum and sends a command to the PLC then awaits results - /// - /// MEWTOCOL Formatted request string ex: %01#RT - /// Returns the result - public async Task SendCommandAsync (string _msg) { - - _msg = _msg.BuildBCCFrame(); - _msg += "\r"; - - //send request - try { - - queuedMessages++; - - var response = await queue.Enqueue(() => SendSingleBlock(_msg)); - - if (queuedMessages > 0) - queuedMessages--; - - if (response == null) { - return new CommandResult { - Success = false, - Error = "0000", - ErrorDescription = "null result" - }; - } - - //error catching - Regex errorcheck = new Regex(@"\%[0-9]{2}\!([0-9]{2})", RegexOptions.IgnoreCase); - Match m = errorcheck.Match(response.ToString()); - if (m.Success) { - string eCode = m.Groups[1].Value; - string eDes = Links.LinkedData.ErrorCodes[Convert.ToInt32(eCode)]; - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Response is: {response}"); - Logger.Log($"Error on command {_msg.Replace("\r", "")} the PLC returned error code: {eCode}, {eDes}", LogLevel.Error); - Console.ResetColor(); - return new CommandResult { - Success = false, - Error = eCode, - ErrorDescription = eDes - }; - } - - return new CommandResult { - Success = true, - Error = "0000", - Response = response.ToString() - }; - - } catch { - return new CommandResult { - Success = false, - Error = "0000", - ErrorDescription = "null result" - }; - } - - } - - private async Task SendSingleBlock (string _blockString) { - - if (client == null || !client.Connected ) { - await ConnectTCP(); - } - - if (client == null || !client.Connected) - return null; - - var message = _blockString.ToHexASCIIBytes(); - - //time measuring - if(speedStopwatchUpstr == null) { - speedStopwatchUpstr = Stopwatch.StartNew(); - } - - if(speedStopwatchUpstr.Elapsed.TotalSeconds >= 1) { - speedStopwatchUpstr.Restart(); - bytesTotalCountedUpstream = 0; - } - - //send request - using (var sendStream = new MemoryStream(message)) { - await sendStream.CopyToAsync(stream); - Logger.Log($"[--------------------------------]", LogLevel.Critical, this); - Logger.Log($"--> OUT MSG: {_blockString}", LogLevel.Critical, this); - } - - //calc upstream speed - bytesTotalCountedUpstream += message.Length; - - var perSecUpstream = (double)((bytesTotalCountedUpstream / speedStopwatchUpstr.Elapsed.TotalMilliseconds) * 1000); - if (perSecUpstream <= 10000) - BytesPerSecondUpstream = (int)Math.Round(perSecUpstream, MidpointRounding.AwayFromZero); - - //await result - StringBuilder response = new StringBuilder(); - try { - - byte[] responseBuffer = new byte[128 * 16]; - - bool endLineCode = false; - bool startMsgCode = false; - - while (!endLineCode && !startMsgCode) { - - do { - - //time measuring - if (speedStopwatchDownstr == null) { - speedStopwatchDownstr = Stopwatch.StartNew(); - } - - if (speedStopwatchDownstr.Elapsed.TotalSeconds >= 1) { - speedStopwatchDownstr.Restart(); - bytesTotalCountedDownstream = 0; - } - - int bytes = await stream.ReadAsync(responseBuffer, 0, responseBuffer.Length); - - endLineCode = responseBuffer.Any(x => x == 0x0D); - startMsgCode = responseBuffer.Count(x => x == 0x25) > 1; - - if (!endLineCode && !startMsgCode) break; - - response.Append(Encoding.UTF8.GetString(responseBuffer, 0, bytes)); - } - while (stream.DataAvailable); - - } - - } catch (IOException) { - OnMajorSocketExceptionWhileConnected(); - return null; - } catch (SocketException) { - OnMajorSocketExceptionWhileConnected(); - return null; - } - - if(!string.IsNullOrEmpty(response.ToString())) { - - Logger.Log($"<-- IN MSG: {response}", LogLevel.Critical, this); - - bytesTotalCountedDownstream += Encoding.ASCII.GetByteCount(response.ToString()); - - var perSecDownstream = (double)((bytesTotalCountedDownstream / speedStopwatchDownstr.Elapsed.TotalMilliseconds) * 1000); - - if(perSecUpstream <= 10000) - BytesPerSecondDownstream = (int)Math.Round(perSecUpstream, MidpointRounding.AwayFromZero); - - return response.ToString(); - - } else { - return null; - } - - } - - #endregion - - #region Disposing - - /// - /// Disposes the current interface and clears all its members - /// - public void Dispose () { - - if (Disposed) return; - - Disconnect(); - - GC.SuppressFinalize(this); - - Disposed = true; - - } - - #endregion - - #region Accessing Info - - /// - /// Gets the connection info string - /// - public string GetConnectionPortInfo () { - - return $"{IpAddress}:{Port}"; - - } - - #endregion - - } - - -} \ No newline at end of file diff --git a/MewtocolNet/Mewtocol/MewtocolInterfaceRequests.cs b/MewtocolNet/Mewtocol/MewtocolInterfaceRequests.cs deleted file mode 100644 index 375fb73..0000000 --- a/MewtocolNet/Mewtocol/MewtocolInterfaceRequests.cs +++ /dev/null @@ -1,416 +0,0 @@ -using System; -using System.IO; -using System.Net.Sockets; -using System.Text; -using System.Collections.Generic; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using MewtocolNet.Registers; -using System.Linq; -using System.Globalization; -using MewtocolNet.Logging; - -namespace MewtocolNet { - - public partial class MewtocolInterface { - - #region PLC info getters - - /// - /// Gets generic information about the PLC - /// - /// A PLCInfo class - public async Task GetPLCInfoAsync () { - var resu = await SendCommandAsync("%01#RT"); - if(!resu.Success) return null; - - var reg = new Regex(@"\%([0-9]{2})\$RT([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{4})..", RegexOptions.IgnoreCase); - Match m = reg.Match(resu.Response); - - if(m.Success) { - - string station = m.Groups[1].Value; - string cpu = m.Groups[2].Value; - string version = m.Groups[3].Value; - string capacity = m.Groups[4].Value; - string operation = m.Groups[5].Value; - - string errorflag = m.Groups[7].Value; - string error = m.Groups[8].Value; - - PLCInfo retInfo = new PLCInfo { - CpuInformation = CpuInfo.BuildFromHexString(cpu, version, capacity), - OperationMode = PLCMode.BuildFromHex(operation), - ErrorCode = error, - StationNumber = int.Parse(station ?? "0"), - }; - - PlcInfo = retInfo; - return retInfo; - - } - return null; - } - - #endregion - - #region Operation mode changing - - /// - /// Changes the PLCs operation mode to the given one - /// - /// The mode to change to - /// The success state of the write operation - public async Task SetOperationMode (OPMode mode) { - - string modeChar = mode == OPMode.Prog ? "P" : "R"; - - string requeststring = $"%{GetStationNumber()}#RM{modeChar}"; - var result = await SendCommandAsync(requeststring); - - if (result.Success) { - Logger.Log($"operation mode was changed to {mode}", LogLevel.Info, this); - } else { - Logger.Log("Operation mode change failed", LogLevel.Error, this); - } - - return result.Success; - - } - - #endregion - - #region Byte range writingv / reading to registers - - /// - /// Writes a byte array to a span over multiple registers at once, - /// Rembember the plc can only store word so in order to write to a word array - /// your byte array should be double the size - /// - /// /// start address of the array - /// - /// - public async Task WriteByteRange (int start, byte[] byteArr) { - - string byteString = byteArr.BigToMixedEndian().ToHexString(); - var wordLength = byteArr.Length / 2; - if (byteArr.Length % 2 != 0) - wordLength++; - - string startStr = start.ToString().PadLeft(5, '0'); - string endStr = (start + wordLength - 1).ToString().PadLeft(5, '0'); - - string requeststring = $"%{GetStationNumber()}#WDD{startStr}{endStr}{byteString}"; - var result = await SendCommandAsync(requeststring); - - return result.Success; - - } - - /// - /// Reads the bytes from the start adress for counts byte length - /// - /// Start adress - /// Number of bytes to get - /// Gets invoked when the progress changes, contains the progress as a double - /// A byte array or null of there was an error - public async Task ReadByteRange (int start, int count, Action onProgress = null) { - - var byteList = new List(); - - var wordLength = count / 2; - if (count % 2 != 0) - wordLength++; - - - //read blocks of max 4 words per msg - for (int i = 0; i < wordLength; i+=8) { - - int curWordStart = start + i; - int curWordEnd = curWordStart + 7; - - string startStr = curWordStart.ToString().PadLeft(5, '0'); - string endStr = (curWordEnd).ToString().PadLeft(5, '0'); - - string requeststring = $"%{GetStationNumber()}#RDD{startStr}{endStr}"; - var result = await SendCommandAsync(requeststring); - - if (result.Success && !string.IsNullOrEmpty(result.Response)) { - - var bytes = result.Response.ParseDTByteString(8 * 4).HexStringToByteArray(); - - if (bytes == null) { - return null; - } - - byteList.AddRange(bytes.BigToMixedEndian().Take(count).ToArray()); - - } - - if(onProgress != null) - onProgress((double)i / wordLength); - - } - - return byteList.ToArray(); - - } - - #endregion - - #region Bool register reading / writing - - /// - /// Reads the given boolean register from the PLC - /// - /// The register to read - public async Task ReadBoolRegister (BRegister _toRead) { - - string requeststring = $"%{GetStationNumber()}#RCS{_toRead.BuildMewtocolQuery()}"; - var result = await SendCommandAsync(requeststring); - - if(!result.Success) { - return new BRegisterResult { - Result = result, - Register = _toRead - }; - } - - var resultBool = result.Response.ParseRCSingleBit(); - if(resultBool != null) { - _toRead.SetValueFromPLC(resultBool.Value); - } - - var finalRes = new BRegisterResult { - Result = result, - Register = _toRead - }; - - return finalRes; - - } - - /// - /// Writes to the given bool register on the PLC - /// - /// The register to write to - /// The value to write - /// The success state of the write operation - public async Task WriteBoolRegister (BRegister _toWrite, bool value) { - - string requeststring = $"%{GetStationNumber()}#WCS{_toWrite.BuildMewtocolQuery()}{(value ? "1" : "0")}"; - - var result = await SendCommandAsync(requeststring); - - return result.Success && result.Response.StartsWith($"%{ GetStationNumber()}$WC"); - - } - - #endregion - - #region Number register reading / writing - - /// - /// Reads the given numeric register from the PLC - /// - /// Type of number (short, ushort, int, uint, float) - /// The register to read - /// A result with the given NumberRegister containing the readback value and a result struct - public async Task> ReadNumRegister (NRegister _toRead) { - - Type numType = typeof(T); - - string requeststring = $"%{GetStationNumber()}#RD{_toRead.BuildMewtocolQuery()}"; - var result = await SendCommandAsync(requeststring); - - var failedResult = new NRegisterResult { - Result = result, - Register = _toRead - }; - - if (!result.Success || string.IsNullOrEmpty(result.Response)) { - return failedResult; - } - - if (numType == typeof(short)) { - - var resultBytes = result.Response.ParseDTByteString(4).ReverseByteOrder(); - if (resultBytes == null) return failedResult; - var val = short.Parse(resultBytes, NumberStyles.HexNumber); - _toRead.SetValueFromPLC(val); - - } else if (numType == typeof(ushort)) { - - var resultBytes = result.Response.ParseDTByteString(4).ReverseByteOrder(); - if (resultBytes == null) return failedResult; - var val = ushort.Parse(resultBytes, NumberStyles.HexNumber); - _toRead.SetValueFromPLC(val); - - } else if (numType == typeof(int)) { - - var resultBytes = result.Response.ParseDTByteString(8).ReverseByteOrder(); - if (resultBytes == null) return failedResult; - var val = int.Parse(resultBytes, NumberStyles.HexNumber); - _toRead.SetValueFromPLC(val); - - } else if (numType == typeof(uint)) { - - var resultBytes = result.Response.ParseDTByteString(8).ReverseByteOrder(); - if (resultBytes == null) return failedResult; - var val = uint.Parse(resultBytes, NumberStyles.HexNumber); - _toRead.SetValueFromPLC(val); - - } else if (numType == typeof(float)) { - - var resultBytes = result.Response.ParseDTByteString(8).ReverseByteOrder(); - if (resultBytes == null) return failedResult; - //convert to unsigned int first - var val = uint.Parse(resultBytes, NumberStyles.HexNumber); - - byte[] floatVals = BitConverter.GetBytes(val); - float finalFloat = BitConverter.ToSingle(floatVals, 0); - - _toRead.SetValueFromPLC(finalFloat); - - } else if (numType == typeof(TimeSpan)) { - - var resultBytes = result.Response.ParseDTByteString(8).ReverseByteOrder(); - if (resultBytes == null) return failedResult; - //convert to unsigned int first - var vallong = long.Parse(resultBytes, NumberStyles.HexNumber); - var valMillis = vallong * 10; - var ts = TimeSpan.FromMilliseconds(valMillis); - - //minmax writable / readable value is 10ms - _toRead.SetValueFromPLC(ts); - - } - - var finalRes = new NRegisterResult { - Result = result, - Register = _toRead - }; - - return finalRes; - } - - /// - /// Reads the given numeric register from the PLC - /// - /// Type of number (short, ushort, int, uint, float) - /// The register to write - /// The value to write - /// The success state of the write operation - public async Task WriteNumRegister (NRegister _toWrite, T _value) { - - byte[] toWriteVal; - Type numType = typeof(T); - - if (numType == typeof(short)) { - toWriteVal = BitConverter.GetBytes(Convert.ToInt16(_value)); - } else if (numType == typeof(ushort)) { - toWriteVal = BitConverter.GetBytes(Convert.ToUInt16(_value)); - } else if (numType == typeof(int)) { - toWriteVal = BitConverter.GetBytes(Convert.ToInt32(_value)); - } else if (numType == typeof(uint)) { - toWriteVal = BitConverter.GetBytes(Convert.ToUInt32(_value)); - } else if (numType == typeof(float)) { - - var fl = _value as float?; - if (fl == null) - throw new NullReferenceException("Float cannot be null"); - - toWriteVal = BitConverter.GetBytes(fl.Value); - - } else if (numType == typeof(TimeSpan)) { - - var fl = _value as TimeSpan?; - if (fl == null) - throw new NullReferenceException("Timespan cannot be null"); - - var tLong = (uint)(fl.Value.TotalMilliseconds / 10); - toWriteVal = BitConverter.GetBytes(tLong); - - } else { - toWriteVal = null; - } - - string requeststring = $"%{GetStationNumber()}#WD{_toWrite.BuildMewtocolQuery()}{toWriteVal.ToHexString()}"; - - var result = await SendCommandAsync(requeststring); - - return result.Success && result.Response.StartsWith($"%{ GetStationNumber()}$WD"); - - } - - #endregion - - #region String register reading / writing - - //string is build up like this - //04 00 04 00 53 50 33 35 13 - //0, 1 = reserved size - //1, 2 = current size - //3,4,5,6 = ASCII encoded chars (SP35) - //7,8 = checksum - - /// - /// Reads back the value of a string register - /// - /// The register to read - /// The station number of the PLC - /// - public async Task ReadStringRegister (SRegister _toRead, int _stationNumber = 1) { - - string requeststring = $"%{GetStationNumber()}#RD{_toRead.BuildMewtocolQuery()}"; - var result = await SendCommandAsync(requeststring); - if (result.Success) - _toRead.SetValueFromPLC(result.Response.ParseDTString()); - return new SRegisterResult { - Result = result, - Register = _toRead - }; - } - - /// - /// Writes a string to a string register - /// - /// The register to write - /// The value to write, if the strings length is longer than the cap size it gets trimmed to the max char length - /// The station number of the PLC - /// The success state of the write operation - public async Task WriteStringRegister(SRegister _toWrite, string _value, int _stationNumber = 1) { - - if (_value == null) _value = ""; - if(_value.Length > _toWrite.ReservedSize) { - throw new ArgumentException("Write string size cannot be longer than reserved string size"); - } - - string stationNum = GetStationNumber(); - string dataString = _value.BuildDTString(_toWrite.ReservedSize); - string dataArea = _toWrite.BuildCustomIdent(dataString.Length / 4); - - string requeststring = $"%{stationNum}#WD{dataArea}{dataString}"; - - var result = await SendCommandAsync(requeststring); - - - return result.Success && result.Response.StartsWith($"%{ GetStationNumber()}$WD"); - } - - #endregion - - #region Helpers - - internal string GetStationNumber () { - - return StationNumber.ToString().PadLeft(2, '0'); - - - } - - #endregion - - } - -} diff --git a/MewtocolNet/Mewtocol/PLCEnums/CpuType.cs b/MewtocolNet/Mewtocol/PLCEnums/CpuType.cs deleted file mode 100644 index 9312177..0000000 --- a/MewtocolNet/Mewtocol/PLCEnums/CpuType.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace MewtocolNet { - - /// - /// CPU type of the PLC - /// - public enum CpuType { - /// - /// FP 0 / FP 2.7K - /// - FP0_FP1_2_7K, - /// - /// FP0 / FP1, 5K / 10K - /// - FP0_FP1_5K_10K, - /// - /// FP1 M 0.9K - /// - FP1_M_0_9K, - /// - /// FP2 16k / 32k - /// - FP2_16K_32K, - /// - /// FP3 C 10K - /// - FP3_C_10K, - /// - /// FP3 C 16K - /// - FP3_C_16K, - /// - /// FP5 16K - /// - FP5_16K, - /// - /// FP 5 24K - /// - FP5_24K, - /// - /// Includes panasonic FPX, FPX-H, Sigma - /// - FP_Sigma_X_H_30K_60K_120K - - } - -} \ No newline at end of file diff --git a/MewtocolNet/Mewtocol/PLCEnums/OPMode.cs b/MewtocolNet/Mewtocol/PLCEnums/OPMode.cs deleted file mode 100644 index 150338c..0000000 --- a/MewtocolNet/Mewtocol/PLCEnums/OPMode.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace MewtocolNet { - - /// - /// CPU type of the PLC - /// - public enum OPMode { - /// - /// PLC run mode - /// - Run, - /// - /// PLC programming mode - /// - Prog, - } - -} \ No newline at end of file diff --git a/MewtocolNet/Mewtocol/PLCInfo.cs b/MewtocolNet/Mewtocol/PLCInfo.cs deleted file mode 100644 index c89748e..0000000 --- a/MewtocolNet/Mewtocol/PLCInfo.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace MewtocolNet.Registers { - /// - /// Contains generic information about the plc - /// - public class PLCInfo { - - /// - /// Contains information about the PLCs cpu - /// - public CpuInfo CpuInformation {get;set;} - /// - /// Contains information about the PLCs operation modes - /// - public PLCMode OperationMode {get;set;} - /// - /// Current error code of the PLC - /// - public string ErrorCode {get;set;} - - /// - /// Current station number of the PLC - /// - public int StationNumber { get;set;} - - /// - /// Generates a string containing some of the most important informations - /// - /// - public override string ToString () { - - return $"Type: {CpuInformation.Cputype},\n" + - $"Capacity: {CpuInformation.ProgramCapacity}k\n" + - $"CPU v: {CpuInformation.CpuVersion}\n" + - $"Station Num: {StationNumber}\n" + - $"--------------------------------\n" + - $"OP Mode: {(OperationMode.RunMode ? "Run" : "Prog")}\n" + - $"Error Code: {ErrorCode}"; - - } - - } - -} \ No newline at end of file diff --git a/MewtocolNet/Mewtocol/PLCMode.cs b/MewtocolNet/Mewtocol/PLCMode.cs deleted file mode 100644 index e9a10eb..0000000 --- a/MewtocolNet/Mewtocol/PLCMode.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; - -namespace MewtocolNet.Registers { - - /// - /// All modes - /// - public class PLCMode { - - /// - /// PLC is running - /// - public bool RunMode { get; set; } - /// - /// PLC is in test - /// - public bool TestRunMode { get; set; } - /// - /// BreakExcecuting - /// - public bool BreakExcecuting { get; set; } - /// - /// BreakValid - /// - public bool BreakValid { get; set; } - /// - /// PLC output is enabled - /// - public bool OutputEnabled { get; set; } - /// - /// PLC runs step per step - /// - public bool StepRunMode { get; set; } - /// - /// Message executing - /// - public bool MessageExecuting { get; set; } - /// - /// PLC is in remote mode - /// - public bool RemoteMode { get; set; } - - /// - /// Gets operation mode from 2 digit hex number - /// - internal static PLCMode BuildFromHex (string _hexString) { - - string lower = Convert.ToString(Convert.ToInt32(_hexString.Substring(0, 1)), 2).PadLeft(4, '0'); - string higher = Convert.ToString(Convert.ToInt32(_hexString.Substring(1, 1)), 2).PadLeft(4, '0'); - string combined = lower + higher; - - var retMode = new PLCMode(); - - for (int i = 0; i < 8; i++) { - char digit = combined[i]; - bool state = false; - if (digit.ToString() == "1") - state = true; - switch (i) { - case 0: - retMode.RunMode = state; - break; - case 1: - retMode.TestRunMode = state; - break; - case 2: - retMode.BreakExcecuting = state; - break; - case 3: - retMode.BreakValid = state; - break; - case 4: - retMode.OutputEnabled = state; - break; - case 5: - retMode.StepRunMode = state; - break; - case 6: - retMode.MessageExecuting = state; - break; - case 7: - retMode.RemoteMode = state; - break; - } - } - - return retMode; - - } - } - -} \ No newline at end of file diff --git a/MewtocolNet/Mewtocol/RegisterAttributes/RegisterAttribute.cs b/MewtocolNet/Mewtocol/RegisterAttributes/RegisterAttribute.cs deleted file mode 100644 index 382c574..0000000 --- a/MewtocolNet/Mewtocol/RegisterAttributes/RegisterAttribute.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace MewtocolNet.RegisterAttributes { - - /// - /// The size of the bitwise register - /// - public enum BitCount { - /// - /// 16 bit - /// - B16, - /// - /// 32 bit - /// - B32 - } - - /// - /// Defines the behavior of a register property - /// - [AttributeUsage(AttributeTargets.Property)] - public class RegisterAttribute : Attribute { - - internal int MemoryArea; - internal int StringLength; - internal RegisterType RegisterType; - internal SpecialAddress SpecialAddress = SpecialAddress.None; - internal BitCount BitCount; - internal int AssignedBitIndex = -1; - - - /// - /// Attribute for string type or numeric registers - /// - /// The area in the plcs memory - /// The max string length in the plc - public RegisterAttribute (int memoryArea, int stringLength = 1) { - - MemoryArea = memoryArea; - StringLength = stringLength; - - } - - /// - /// Attribute for boolean registers - /// - /// The area in the plcs memory - /// The type of boolean register - public RegisterAttribute (int memoryArea, RegisterType type) { - - if (type.ToString().StartsWith("DT")) - throw new NotSupportedException("DT types are not supported for attribute register setups because the number type is casted automatically"); - - MemoryArea = memoryArea; - RegisterType = type; - SpecialAddress = SpecialAddress.None; - - } - - /// - /// Attribute for boolean registers - /// - /// The special area in the plcs memory - /// The type of boolean register - public RegisterAttribute (RegisterType type, SpecialAddress spAdress) { - - if (type.ToString().StartsWith("DT")) - throw new NotSupportedException("DT types are not supported for attribute register setups because the number type is casted automatically"); - - RegisterType = type; - SpecialAddress = spAdress; - - } - - /// - /// Attribute to read numeric registers as bitwise - /// - /// The area in the plcs memory - /// The number of bits to parse - public RegisterAttribute (int memoryArea, BitCount bitcount) { - - MemoryArea = memoryArea; - StringLength = 0; - BitCount = bitcount; - - } - - /// - /// Attribute to read numeric registers as bitwise - /// - /// The area in the plcs memory - /// The number of bits to parse - /// The index of the bit that gets linked to the bool - public RegisterAttribute (int memoryArea, uint assignBit, BitCount bitcount) { - - if(assignBit > 15 && bitcount == BitCount.B16) { - throw new NotSupportedException("The assignBit parameter cannot be greater than 15 in a 16 bit var"); - } - - if (assignBit > 31 && bitcount == BitCount.B32) { - throw new NotSupportedException("The assignBit parameter cannot be greater than 31 in a 32 bit var"); - } - - MemoryArea = memoryArea; - StringLength = 0; - BitCount = bitcount; - AssignedBitIndex = (int)assignBit; - - } - - } - -} diff --git a/MewtocolNet/Mewtocol/RegisterEnums.cs b/MewtocolNet/Mewtocol/RegisterEnums.cs deleted file mode 100644 index 66f7582..0000000 --- a/MewtocolNet/Mewtocol/RegisterEnums.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace MewtocolNet { - - /// - /// The special register type - /// - public enum RegisterType { - - /// - /// Physical input as a bool (Relay) - /// - X, - /// - /// Physical output as a bool (Relay) - /// - Y, - /// - /// Internal as a bool (Relay) - /// - R, - /// - /// Data area as a short (Register) - /// - DT_short, - /// - /// Data area as an unsigned short (Register) - /// - DT_ushort, - /// - /// Double data area as an integer (Register) - /// - DDT_int, - /// - /// Double data area as an unsigned integer (Register) - /// - DDT_uint, - /// - /// Double data area as an floating point number (Register) - /// - DDT_float, - - } - - /// - /// The special input / output channel address - /// - public enum SpecialAddress { - - #pragma warning disable CS1591 - - /// - /// No defined - /// - None, - A = -10, - B = -11, - C = -12, - D = -13, - E = -14, - F = -15, - - #pragma warning restore - - } - -} diff --git a/MewtocolNet/Mewtocol/Responses.cs b/MewtocolNet/Mewtocol/Responses.cs deleted file mode 100644 index b9b2289..0000000 --- a/MewtocolNet/Mewtocol/Responses.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; -using System.Text.RegularExpressions; -using System.Linq; -using System.Text; -using System.ComponentModel; - -namespace MewtocolNet.Registers { - - /// - /// The formatted result of a ascii command - /// - public struct CommandResult { - - /// - /// Success state of the message - /// - public bool Success {get;set;} - /// - /// Response text of the message - /// - public string Response {get;set;} - /// - /// Error code of the message - /// - public string Error {get;set;} - /// - /// Error text of the message - /// - public string ErrorDescription {get;set;} - - } - -} \ No newline at end of file diff --git a/MewtocolNet/Mewtocol/Subregisters/BRegister.cs b/MewtocolNet/Mewtocol/Subregisters/BRegister.cs deleted file mode 100644 index 92cb068..0000000 --- a/MewtocolNet/Mewtocol/Subregisters/BRegister.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System; -using System.ComponentModel; -using System.Text; -using MewtocolNet; - -namespace MewtocolNet.Registers { - - /// - /// Defines a register containing a boolean - /// - public class BRegister : IRegister, INotifyPropertyChanged { - - /// - /// Gets called whenever the value was changed - /// - public event Action ValueChanged; - - /// - /// Triggers when a property on the register changes - /// - public event PropertyChangedEventHandler PropertyChanged; - - internal RegisterType RegType { get; private set; } - - internal SpecialAddress SpecialAddress { get; private set; } - - internal Type collectionType; - - /// - /// The type of collection the register is in or null of added manually - /// - public Type CollectionType => collectionType; - - internal bool lastValue; - - /// - /// The value of the register - /// - public object Value => lastValue; - - internal string name; - /// - /// The register name or null of not defined - /// - public string Name => name; - - internal int memoryAdress; - /// - /// The registers memory adress if not a special register - /// - public int MemoryAddress => memoryAdress; - - /// - /// Defines a register containing a number - /// - /// Memory start adress max 99999 - /// Type of boolean register - /// Name of the register - public BRegister (int _address, RegisterType _type = RegisterType.R, string _name = null) { - - if (_address > 99999) throw new NotSupportedException("Memory addresses cant be greater than 99999"); - - if (_type != RegisterType.X && _type != RegisterType.Y && _type != RegisterType.R) - throw new NotSupportedException("The register type cant be numeric, use X, Y or R"); - - memoryAdress = _address; - name = _name; - - RegType = _type; - - } - - /// - /// Defines a register containing a number - /// - /// Memory start adress max 99999 - /// Type of boolean register - /// Name of the register - public BRegister (SpecialAddress _address, RegisterType _type = RegisterType.R, string _name = null) { - - if (_address == SpecialAddress.None) - throw new NotSupportedException("Special address cant be none"); - - if (_type != RegisterType.X && _type != RegisterType.Y && _type != RegisterType.R) - throw new NotSupportedException("The register type cant be numeric, use X, Y or R"); - - SpecialAddress = _address; - name = _name; - - RegType = _type; - - } - - internal BRegister WithCollectionType(Type colType) { - - collectionType = colType; - return this; - - } - - /// - /// Builds the register area name - /// - public string BuildMewtocolQuery () { - - //build area code from register type - StringBuilder asciistring = new StringBuilder(RegType.ToString()); - if(SpecialAddress == SpecialAddress.None) { - asciistring.Append(MemoryAddress.ToString().PadLeft(4, '0')); - } else { - asciistring.Append(SpecialAddress.ToString().PadLeft(4, '0')); - } - - return asciistring.ToString(); - - } - - internal void SetValueFromPLC (bool val) { - - lastValue = val; - TriggerChangedEvnt(this); - TriggerNotifyChange(); - - } - - public string GetStartingMemoryArea() { - - if (SpecialAddress != SpecialAddress.None) - return SpecialAddress.ToString(); - - return MemoryAddress.ToString(); - - } - - public bool IsUsedBitwise() => false; - - public Type GetCollectionType() => CollectionType; - - public string GetValueString() => Value.ToString(); - - public void ClearValue() => SetValueFromPLC(false); - - public string GetRegisterString() => RegType.ToString(); - - public string GetCombinedName() => $"{(CollectionType != null ? $"{CollectionType.Name}." : "")}{Name ?? "Unnamed"}"; - - public string GetContainerName() => $"{(CollectionType != null ? $"{CollectionType.Name}" : "")}"; - - public string GetRegisterPLCName() { - - if (SpecialAddress != SpecialAddress.None) { - return $"{GetRegisterString()}{SpecialAddress}"; - } - - return $"{GetRegisterString()}{MemoryAddress}"; - - } - - internal void TriggerChangedEvnt(object changed) => ValueChanged?.Invoke(changed); - - public void TriggerNotifyChange() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value")); - - public override string ToString() => $"{GetRegisterPLCName()} - Value: {GetValueString()}"; - - } - -} diff --git a/MewtocolNet/Mewtocol/Subregisters/BRegisterResult.cs b/MewtocolNet/Mewtocol/Subregisters/BRegisterResult.cs deleted file mode 100644 index 8a39277..0000000 --- a/MewtocolNet/Mewtocol/Subregisters/BRegisterResult.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace MewtocolNet.Registers { - - /// - /// Result for a boolean register - /// - public class BRegisterResult { - - /// - /// The command result - /// - public CommandResult Result { get; set; } - - /// - /// The used register - /// - public BRegister Register { get; set; } - - } - -} diff --git a/MewtocolNet/Mewtocol/Subregisters/NRegister.cs b/MewtocolNet/Mewtocol/Subregisters/NRegister.cs deleted file mode 100644 index 79cf631..0000000 --- a/MewtocolNet/Mewtocol/Subregisters/NRegister.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Reflection; -using System.Text; - -namespace MewtocolNet.Registers { - - /// - /// Defines a register containing a number - /// - /// The type of the numeric value - public class NRegister : IRegister { - - /// - /// Gets called whenever the value was changed - /// - public event Action ValueChanged; - - /// - /// Triggers when a property on the register changes - /// - public event PropertyChangedEventHandler PropertyChanged; - - internal Type collectionType; - - /// - /// The type of collection the register is in or null of added manually - /// - public Type CollectionType => collectionType; - - internal T lastValue; - - /// - /// The value of the register - /// - public object Value => lastValue; - - internal string name; - /// - /// The register name or null of not defined - /// - public string Name => name; - - internal int memoryAdress; - /// - /// The registers memory adress if not a special register - /// - public int MemoryAddress => memoryAdress; - - internal int memoryLength; - /// - /// The rgisters memory length - /// - public int MemoryLength => memoryLength; - - internal bool isUsedBitwise { get; set; } - - internal Type enumType { get; set; } - - /// - /// Defines a register containing a number - /// - /// Memory start adress max 99999 - /// Name of the register - public NRegister (int _adress, string _name = null) { - - if (_adress > 99999) - throw new NotSupportedException("Memory adresses cant be greater than 99999"); - - memoryAdress = _adress; - name = _name; - Type numType = typeof(T); - if (numType == typeof(short)) { - memoryLength = 0; - } else if (numType == typeof(ushort)) { - memoryLength = 0; - } else if (numType == typeof(int)) { - memoryLength = 1; - } else if (numType == typeof(uint)) { - memoryLength = 1; - } else if (numType == typeof(float)) { - memoryLength = 1; - } else if (numType == typeof(TimeSpan)) { - memoryLength = 1; - } else { - throw new NotSupportedException($"The type {numType} is not allowed for Number Registers"); - } - - } - - internal NRegister(int _adress, string _name = null, bool isBitwise = false, Type _enumType = null) { - - if (_adress > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); - memoryAdress = _adress; - name = _name; - Type numType = typeof(T); - if (numType == typeof(short)) { - memoryLength = 0; - } else if (numType == typeof(ushort)) { - memoryLength = 0; - } else if (numType == typeof(int)) { - memoryLength = 1; - } else if (numType == typeof(uint)) { - memoryLength = 1; - } else if (numType == typeof(float)) { - memoryLength = 1; - } else if (numType == typeof(TimeSpan)) { - memoryLength = 1; - } else { - throw new NotSupportedException($"The type {numType} is not allowed for Number Registers"); - } - - isUsedBitwise = isBitwise; - enumType = _enumType; - - } - - internal NRegister WithCollectionType (Type colType) { - - collectionType = colType; - return this; - - } - - internal void SetValueFromPLC (object val) { - - lastValue = (T)val; - TriggerChangedEvnt(this); - TriggerNotifyChange(); - - } - - public string GetStartingMemoryArea () => this.MemoryAddress.ToString(); - - public Type GetCollectionType() => CollectionType; - - public bool IsUsedBitwise() => isUsedBitwise; - - public string GetValueString() { - - //is number or bitwise - if(enumType == null) { - - return $"{Value}{(isUsedBitwise ? $" [{GetBitwise().ToBitString()}]" : "")}"; - - } - - //is enum - var dict = new Dictionary(); - - foreach (var name in Enum.GetNames(enumType)) { - - int enumKey = (int)Enum.Parse(enumType, name); - if (!dict.ContainsKey(enumKey)) { - dict.Add(enumKey, name); - } - - } - - if (enumType != null && Value is short shortVal) { - - if (dict.ContainsKey(shortVal)) { - - return $"{Value} ({dict[shortVal]})"; - - } else { - - return $"{Value} (Missing Enum)"; - - } - - } - - if (enumType != null && Value is int intVal) { - - if (dict.ContainsKey(intVal)) { - - return $"{Value} ({dict[intVal]})"; - - } else { - - return $"{Value} (Missing Enum)"; - - } - - } - - return Value.ToString(); - - } - - /// - /// Gets the register bitwise if its a 16 or 32 bit int - /// - /// A bitarray - public BitArray GetBitwise() { - - if (this is NRegister shortReg) { - - var bytes = BitConverter.GetBytes((short)Value); - BitArray bitAr = new BitArray(bytes); - return bitAr; - - } - - if (this is NRegister intReg) { - - var bytes = BitConverter.GetBytes((int)Value); - BitArray bitAr = new BitArray(bytes); - return bitAr; - - } - - return null; - - } - - public string BuildMewtocolQuery() { - - StringBuilder asciistring = new StringBuilder("D"); - asciistring.Append(MemoryAddress.ToString().PadLeft(5, '0')); - asciistring.Append((MemoryAddress + MemoryLength).ToString().PadLeft(5, '0')); - return asciistring.ToString(); - - } - - public string GetRegisterString() { - - if(Value is short) { - return "DT"; - } - - if (Value is ushort) { - return "DT"; - } - - if (Value is int) { - return "DDT"; - } - - if (Value is uint) { - return "DDT"; - } - - if (Value is float) { - return "DDT"; - } - - if (Value is TimeSpan) { - return "DDT"; - } - - throw new NotSupportedException("Numeric type is not supported"); - - } - - public string GetCombinedName() => $"{(CollectionType != null ? $"{CollectionType.Name}." : "")}{Name ?? "Unnamed"}"; - - public string GetContainerName() => $"{(CollectionType != null ? $"{CollectionType.Name}" : "")}"; - - public string GetRegisterPLCName() => $"{GetRegisterString()}{MemoryAddress}"; - - public void ClearValue() => SetValueFromPLC(default(T)); - - internal void TriggerChangedEvnt(object changed) => ValueChanged?.Invoke(changed); - - public void TriggerNotifyChange() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value")); - - public override string ToString() => $"{GetRegisterPLCName()} - Value: {GetValueString()}"; - - } - -} diff --git a/MewtocolNet/Mewtocol/Subregisters/NRegisterResult.cs b/MewtocolNet/Mewtocol/Subregisters/NRegisterResult.cs deleted file mode 100644 index 74f7214..0000000 --- a/MewtocolNet/Mewtocol/Subregisters/NRegisterResult.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; - -namespace MewtocolNet.Registers { - /// - /// Result for a read/write operation - /// - /// The type of the numeric value - public class NRegisterResult { - - /// - /// Command result - /// - public CommandResult Result { get; set; } - - /// - /// The used register - /// - public NRegister Register { get; set; } - - /// - /// Trys to get the value of there is one - /// - public bool TryGetValue (out T value) { - - if(Result.Success) { - value = (T)Register.Value; - return true; - } - value = default(T); - return false; - - } - - } - - - - -} diff --git a/MewtocolNet/Mewtocol/Subregisters/SRegister.cs b/MewtocolNet/Mewtocol/Subregisters/SRegister.cs deleted file mode 100644 index 49c0d6e..0000000 --- a/MewtocolNet/Mewtocol/Subregisters/SRegister.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.ComponentModel; -using System.Text; - -namespace MewtocolNet.Registers { - /// - /// Defines a register containing a string - /// - public class SRegister : IRegister { - - /// - /// Gets called whenever the value was changed - /// - public event Action ValueChanged; - - /// - /// Triggers when a property on the register changes - /// - public event PropertyChangedEventHandler PropertyChanged; - - internal Type collectionType; - - /// - /// The type of collection the register is in or null of added manually - /// - public Type CollectionType => collectionType; - - internal string lastValue; - - /// - /// The value of the register - /// - public object Value => lastValue; - - internal string name; - /// - /// The register name or null of not defined - /// - public string Name => name; - - internal int memoryAdress; - /// - /// The registers memory adress if not a special register - /// - public int MemoryAddress => memoryAdress; - - internal int memoryLength; - /// - /// The registers memory length - /// - public int MemoryLength => memoryLength; - - internal short ReservedSize { get; set; } - - /// - /// Defines a register containing a string - /// - public SRegister(int _adress, int _reservedStringSize, string _name = null) { - - if (_adress > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); - name = _name; - memoryAdress = _adress; - ReservedSize = (short)_reservedStringSize; - - //calc mem length - var wordsize = (double)_reservedStringSize / 2; - if (wordsize % 2 != 0) { - wordsize++; - } - - memoryLength = (int)Math.Round(wordsize + 1); - } - - internal SRegister WithCollectionType(Type colType) { - - collectionType = colType; - return this; - - } - - /// - /// Builds the register identifier for the mewotocol protocol - /// - public string BuildMewtocolQuery() { - - StringBuilder asciistring = new StringBuilder("D"); - - asciistring.Append(MemoryAddress.ToString().PadLeft(5, '0')); - asciistring.Append((MemoryAddress + MemoryLength).ToString().PadLeft(5, '0')); - - return asciistring.ToString(); - } - - internal string BuildCustomIdent (int overwriteWordLength) { - - if (overwriteWordLength <= 0) - throw new Exception("overwriteWordLength cant be 0 or less"); - - StringBuilder asciistring = new StringBuilder("D"); - - asciistring.Append(MemoryAddress.ToString().PadLeft(5, '0')); - asciistring.Append((MemoryAddress + overwriteWordLength - 1).ToString().PadLeft(5, '0')); - - return asciistring.ToString(); - } - - public Type GetCollectionType() => CollectionType; - - public bool IsUsedBitwise() => false; - - internal void SetValueFromPLC (string val) { - - lastValue = val; - - TriggerChangedEvnt(this); - TriggerNotifyChange(); - - } - - public string GetStartingMemoryArea() => this.MemoryAddress.ToString(); - - public string GetValueString() => Value?.ToString() ?? ""; - - public string GetRegisterString() => "DT"; - - public string GetCombinedName() => $"{(CollectionType != null ? $"{CollectionType.Name}." : "")}{Name ?? "Unnamed"}"; - - public string GetContainerName() => $"{(CollectionType != null ? $"{CollectionType.Name}" : "")}"; - - public string GetRegisterPLCName() => $"{GetRegisterString()}{MemoryAddress}"; - - public void ClearValue() => SetValueFromPLC(null); - - internal void TriggerChangedEvnt(object changed) => ValueChanged?.Invoke(changed); - - public void TriggerNotifyChange() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value")); - - public override string ToString () => $"{GetRegisterPLCName()} - Value: {GetValueString()}"; - - } - -} diff --git a/MewtocolNet/Mewtocol/Subregisters/SRegisterResult.cs b/MewtocolNet/Mewtocol/Subregisters/SRegisterResult.cs deleted file mode 100644 index 45129a6..0000000 --- a/MewtocolNet/Mewtocol/Subregisters/SRegisterResult.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace MewtocolNet.Registers { - - /// - /// The results of a string register operation - /// - public class SRegisterResult { - - /// - /// The command result - /// - public CommandResult Result { get; set; } - /// - /// The register definition used - /// - public SRegister Register { get; set; } - - } - -} diff --git a/MewtocolNet/MewtocolFrameResponse.cs b/MewtocolNet/MewtocolFrameResponse.cs new file mode 100644 index 0000000..0ff4547 --- /dev/null +++ b/MewtocolNet/MewtocolFrameResponse.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet { + + public struct MewtocolFrameResponse { + + public bool Success { get; private set; } + + public string Response { get; private set; } + + public int ErrorCode { get; private set; } + + public string Error { get; private set; } + + public static MewtocolFrameResponse Timeout => new MewtocolFrameResponse(403, "Request timed out"); + + public static MewtocolFrameResponse NotIntialized => new MewtocolFrameResponse(405, "PLC was not initialized"); + + public MewtocolFrameResponse (string response) { + + Success = true; + ErrorCode = 0; + Response = response; + Error = null; + + } + + public MewtocolFrameResponse(int errorCode) { + + Success = false; + Response = null; + ErrorCode = errorCode; + Error = CodeDescriptions.Error[errorCode]; + + } + + public MewtocolFrameResponse(int errorCode, string exceptionMsg) { + + Success = false; + Response = null; + ErrorCode = errorCode; + Error = exceptionMsg; + + } + + /// + public static bool operator == (MewtocolFrameResponse c1, MewtocolFrameResponse c2) { + return c1.Equals(c2); + } + + /// + public static bool operator != (MewtocolFrameResponse c1, MewtocolFrameResponse c2) { + return !c1.Equals(c2); + } + + } + +} diff --git a/MewtocolNet/MewtocolInterface.cs b/MewtocolNet/MewtocolInterface.cs new file mode 100644 index 0000000..3522607 --- /dev/null +++ b/MewtocolNet/MewtocolInterface.cs @@ -0,0 +1,500 @@ +using MewtocolNet.Logging; +using MewtocolNet.Queue; +using MewtocolNet.Registers; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace MewtocolNet { + + public partial class MewtocolInterface : IPlc, INotifyPropertyChanged, IDisposable { + + #region Private fields + + private protected Stream stream; + + private int tcpMessagesSentThisCycle = 0; + private int pollerCycleDurationMs; + private volatile int queuedMessages; + private bool isConnected; + private PLCInfo plcInfo; + private protected int stationNumber; + + private protected int bytesTotalCountedUpstream = 0; + private protected int bytesTotalCountedDownstream = 0; + private protected int cycleTimeMs = 25; + private protected int bytesPerSecondUpstream = 0; + private protected int bytesPerSecondDownstream = 0; + + private protected AsyncQueue queue = new AsyncQueue(); + private protected int RecBufferSize = 128; + private protected Stopwatch speedStopwatchUpstr; + private protected Stopwatch speedStopwatchDownstr; + private protected Task firstPollTask = new Task(() => { }); + + #endregion + + #region Internal fields + + internal event Action PolledCycle; + internal volatile bool pollerTaskStopped = true; + internal volatile bool pollerFirstCycle; + internal bool usePoller = false; + + internal List RegistersUnderlying { get; private set; } = new List(); + internal IEnumerable RegistersInternal => RegistersUnderlying.Cast(); + + #endregion + + #region Public Read Only Properties / Fields + + /// + public event Action Connected; + + /// + public event Action Disconnected; + + /// + public event Action RegisterChanged; + + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + public bool Disposed { get; private set; } + + /// + public int QueuedMessages => queuedMessages; + + /// + public bool IsConnected { + get => isConnected; + private protected set { + isConnected = value; + OnPropChange(); + } + } + + /// + public PLCInfo PlcInfo { + get => plcInfo; + private set { + plcInfo = value; + OnPropChange(); + } + } + + /// + public IEnumerable Registers => RegistersUnderlying.Cast(); + + /// + public int StationNumber => stationNumber; + + /// + public int BytesPerSecondUpstream { + get { return bytesPerSecondUpstream; } + private protected set { + bytesPerSecondUpstream = value; + OnPropChange(); + } + } + + /// + public int BytesPerSecondDownstream { + get { return bytesPerSecondDownstream; } + private protected set { + bytesPerSecondDownstream = value; + OnPropChange(); + } + } + + /// + public string ConnectionInfo => GetConnectionInfo(); + + #endregion + + #region Public read/write Properties / Fields + + /// + public int ConnectTimeout { get; set; } = 3000; + + #endregion + + #region Methods + + private protected MewtocolInterface () { + + Connected += MewtocolInterface_Connected; + + void MewtocolInterface_Connected(PLCInfo obj) { + + if (usePoller) + AttachPoller(); + + IsConnected = true; + + } + + RegisterChanged += (o) => { + + var asInternal = (IRegisterInternal)o; + + string address = $"{asInternal.GetRegisterString()}{asInternal.GetStartingMemoryArea()}".PadRight(5, (char)32); + + Logger.Log($"{address} " + + $"{(o.Name != null ? $"({o.Name}) " : "")}" + + $"changed to \"{asInternal.GetValueString()}\"", LogLevel.Change, this); + }; + + } + + /// + public virtual async Task ConnectAsync() => throw new NotImplementedException(); + + /// + public async Task AwaitFirstDataCycleAsync() => await firstPollTask; + + /// + public void Disconnect() { + + if (!IsConnected) return; + + if(pollCycleTask != null && !pollCycleTask.IsCompleted) + pollCycleTask.Wait(); + + OnMajorSocketExceptionWhileConnected(); + + } + + /// + public void Dispose() { + + if (Disposed) return; + Disconnect(); + //GC.SuppressFinalize(this); + Disposed = true; + + } + + /// + public virtual string GetConnectionInfo() => throw new NotImplementedException(); + + /// + public async Task SendCommandAsync(string _msg, bool withTerminator = true, int timeoutMs = -1) { + + //send request + queuedMessages++; + + var tempResponse = queue.Enqueue(async () => await SendFrameAsync(_msg, withTerminator, withTerminator)); + + if (await Task.WhenAny(tempResponse, Task.Delay(timeoutMs)) != tempResponse) { + // timeout logic + return MewtocolFrameResponse.Timeout; + } + + tcpMessagesSentThisCycle++; + queuedMessages--; + + return tempResponse.Result; + + } + + private protected async Task SendFrameAsync (string frame, bool useBcc = true, bool useCr = true) { + + try { + + if (stream == null) return MewtocolFrameResponse.NotIntialized; + + if (useBcc) + frame = $"{frame.BuildBCCFrame()}"; + + if (useCr) + frame = $"{frame}\r"; + + + SetUpstreamStopWatchStart(); + + //write inital command + byte[] writeBuffer = Encoding.UTF8.GetBytes(frame); + stream.Write(writeBuffer, 0, writeBuffer.Length); + + //calc upstream speed + CalcUpstreamSpeed(writeBuffer.Length); + + Logger.Log($"[---------CMD START--------]", LogLevel.Critical, this); + Logger.Log($"--> OUT MSG: {frame.Replace("\r", "(CR)")}", LogLevel.Critical, this); + + var readResult = await ReadCommandAsync(); + + //did not receive bytes but no errors, the com port was not configured right + if (readResult.Item1.Length == 0) { + + return new MewtocolFrameResponse(402, "Receive buffer was empty"); + + } + + //build final result + string resString = Encoding.UTF8.GetString(readResult.Item1); + + //check if the message had errors + //error response + var gotErrorcode = CheckForErrorMsg(resString); + if (gotErrorcode != 0) { + var errResponse = new MewtocolFrameResponse(gotErrorcode); + Logger.Log($"Command error: {errResponse.Error}", LogLevel.Error, this); + return errResponse; + } + + //was multiframed response + if (readResult.Item2) { + + var split = resString.Split('&'); + + for (int j = 0; j < split.Length; j++) { + + split[j] = split[j].Replace("\r", ""); + split[j] = split[j].Substring(0, split[j].Length - 2); + if (j > 0) split[j] = split[j].Replace($"%{GetStationNumber()}", ""); + + } + + resString = string.Join("", split); + + } + + Logger.Log($"<-- IN MSG: {resString.Replace("\r", "(CR)")}", LogLevel.Critical, this); + Logger.Log($"Total bytes parsed: {resString.Length}", LogLevel.Critical, this); + Logger.Log($"[---------CMD END----------]", LogLevel.Critical, this); + + return new MewtocolFrameResponse(resString); + + } catch (Exception ex) { + + return new MewtocolFrameResponse(400, ex.Message.ToString(System.Globalization.CultureInfo.InvariantCulture)); + + } + + } + + private protected async Task<(byte[], bool)> ReadCommandAsync () { + + //read total + List totalResponse = new List(); + bool wasMultiFramedResponse = false; + + try { + + bool needsRead = false; + + do { + + SetDownstreamStopWatchStart(); + + byte[] buffer = new byte[128]; + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); + + CalcDownstreamSpeed(bytesRead); + + byte[] received = new byte[bytesRead]; + Buffer.BlockCopy(buffer, 0, received, 0, bytesRead); + + var commandRes = ParseBufferFrame(received); + needsRead = commandRes == CommandState.LineFeed || commandRes == CommandState.RequestedNextFrame; + + var tempMsg = Encoding.UTF8.GetString(received).Replace("\r", "(CR)"); + Logger.Log($">> IN PART: {tempMsg}, Command state: {commandRes}", LogLevel.Critical, this); + + //add complete response to collector without empty bytes + totalResponse.AddRange(received.Where(x => x != (byte)0x0)); + + if (commandRes == CommandState.RequestedNextFrame) { + + //request next frame + var writeBuffer = Encoding.UTF8.GetBytes("%01**&\r"); + await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length); + wasMultiFramedResponse = true; + + } + + } while (needsRead); + + } catch (OperationCanceledException) { } + + return (totalResponse.ToArray(), wasMultiFramedResponse); + + } + + private protected CommandState ParseBufferFrame(byte[] received) { + + const char CR = '\r'; + const char DELIMITER = '&'; + + CommandState cmdState; + + bool terminatorReceived = received.Any(x => x == (byte)CR); + var delimiterTerminatorIdx = received.ToArray().SearchBytePattern(new byte[] { (byte)DELIMITER, (byte)CR }); + + if (terminatorReceived && delimiterTerminatorIdx == -1) { + cmdState = CommandState.Complete; + } else if (delimiterTerminatorIdx != -1) { + cmdState = CommandState.RequestedNextFrame; + } else { + cmdState = CommandState.LineFeed; + } + + return cmdState; + + } + + private protected int CheckForErrorMsg (string msg) { + + //error catching + Regex errorcheck = new Regex(@"\%..\!([0-9]{2})", RegexOptions.IgnoreCase); + Match m = errorcheck.Match(msg); + + if (m.Success) { + + string eCode = m.Groups[1].Value; + return Convert.ToInt32(eCode); + + } + + return 0; + + } + + private protected void OnMajorSocketExceptionWhileConnecting() { + + if (IsConnected) { + + Logger.Log("The PLC connection timed out", LogLevel.Error, this); + OnDisconnect(); + + } + + } + + private protected void OnMajorSocketExceptionWhileConnected() { + + if (IsConnected) { + + Logger.Log("The PLC connection was closed", LogLevel.Error, this); + OnDisconnect(); + + } + + } + + private protected virtual void OnConnected (PLCInfo plcinf) { + + Logger.Log("Connected", LogLevel.Info, this); + Logger.Log($"\n\n{plcinf.ToString()}\n\n", LogLevel.Verbose, this); + + IsConnected = true; + + Connected?.Invoke(plcinf); + + if (!usePoller) { + firstPollTask.RunSynchronously(); + } + + PolledCycle += OnPollCycleDone; + void OnPollCycleDone() { + + firstPollTask.RunSynchronously(); + PolledCycle -= OnPollCycleDone; + + } + + } + + private protected virtual void OnDisconnect () { + + BytesPerSecondDownstream = 0; + BytesPerSecondUpstream = 0; + PollerCycleDurationMs = 0; + + IsConnected = false; + ClearRegisterVals(); + + Disconnected?.Invoke(); + KillPoller(); + + } + + private protected void ClearRegisterVals() { + + for (int i = 0; i < RegistersUnderlying.Count; i++) { + + var reg = (IRegisterInternal)RegistersUnderlying[i]; + reg.ClearValue(); + + } + + } + + private void SetUpstreamStopWatchStart () { + + if (speedStopwatchUpstr == null) { + speedStopwatchUpstr = Stopwatch.StartNew(); + } + + if (speedStopwatchUpstr.Elapsed.TotalSeconds >= 1) { + speedStopwatchUpstr.Restart(); + bytesTotalCountedUpstream = 0; + } + + } + + private void SetDownstreamStopWatchStart () { + + if (speedStopwatchDownstr == null) { + speedStopwatchDownstr = Stopwatch.StartNew(); + } + + if (speedStopwatchDownstr.Elapsed.TotalSeconds >= 1) { + speedStopwatchDownstr.Restart(); + bytesTotalCountedDownstream = 0; + } + + } + + private void CalcUpstreamSpeed (int byteCount) { + + bytesTotalCountedUpstream += byteCount; + + var perSecUpstream = (double)((bytesTotalCountedUpstream / speedStopwatchUpstr.Elapsed.TotalMilliseconds) * 1000); + if (perSecUpstream <= 10000) + BytesPerSecondUpstream = (int)Math.Round(perSecUpstream, MidpointRounding.AwayFromZero); + + } + + private void CalcDownstreamSpeed (int byteCount) { + + bytesTotalCountedDownstream += byteCount; + + var perSecDownstream = (double)((bytesTotalCountedDownstream / speedStopwatchDownstr.Elapsed.TotalMilliseconds) * 1000); + + if (perSecDownstream <= 10000) + BytesPerSecondDownstream = (int)Math.Round(perSecDownstream, MidpointRounding.AwayFromZero); + + } + + private protected void OnPropChange([CallerMemberName] string propertyName = null) { + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + + } + + #endregion + + } + +} diff --git a/MewtocolNet/MewtocolInterfaceRegisterHandling.cs b/MewtocolNet/MewtocolInterfaceRegisterHandling.cs new file mode 100644 index 0000000..51bb07f --- /dev/null +++ b/MewtocolNet/MewtocolInterfaceRegisterHandling.cs @@ -0,0 +1,453 @@ +using MewtocolNet.Exceptions; +using MewtocolNet.Logging; +using MewtocolNet.RegisterAttributes; +using MewtocolNet.RegisterBuilding; +using MewtocolNet.Registers; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MewtocolNet { + + /// + /// The PLC com interface class + /// + public partial class MewtocolInterface { + + internal Task pollCycleTask; + + /// + /// True if the poller is actvice (can be paused) + /// + public bool PollerActive => !pollerTaskStopped; + + /// + /// Current poller cycle duration + /// + public int PollerCycleDurationMs { + get => pollerCycleDurationMs; + private set { + pollerCycleDurationMs = value; + OnPropChange(); + } + } + + #region Register Polling + + /// + /// Kills the poller completely + /// + internal void KillPoller() { + + pollerTaskStopped = true; + ClearRegisterVals(); + + } + + /// + /// Attaches a continous reader that reads back the Registers and Contacts + /// + internal void AttachPoller() { + + if (!pollerTaskStopped) + return; + + PollerCycleDurationMs = 0; + pollerFirstCycle = true; + + Task.Run(Poll); + + } + + /// + /// Runs a single poller cycle manually, + /// useful if you want to use a custom update frequency + /// + /// The number of inidvidual mewtocol commands sent + public async Task RunPollerCylceManual () { + + if (!pollerTaskStopped) + throw new NotSupportedException($"The poller is already running, " + + $"please make sure there is no polling active before calling {nameof(RunPollerCylceManual)}"); + + tcpMessagesSentThisCycle = 0; + + pollCycleTask = OnMultiFrameCycle(); + await pollCycleTask; + + return tcpMessagesSentThisCycle; + + } + + //polls all registers one by one (slow) + internal async Task Poll () { + + Logger.Log("Poller is attaching", LogLevel.Info, this); + + pollerTaskStopped = false; + + while (!pollerTaskStopped) { + + tcpMessagesSentThisCycle = 0; + + pollCycleTask = OnMultiFrameCycle(); + await pollCycleTask; + + if (!IsConnected) { + pollerTaskStopped = true; + return; + } + + pollerFirstCycle = false; + InvokePolledCycleDone(); + + } + + } + + private async Task OnMultiFrameCycle () { + + var sw = Stopwatch.StartNew(); + + await UpdateRCPRegisters(); + + await UpdateDTRegisters(); + + await GetPLCInfoAsync(); + + sw.Stop(); + PollerCycleDurationMs = (int)sw.ElapsedMilliseconds; + + } + + #endregion + + #region Smart register polling methods + + private async Task UpdateRCPRegisters () { + + //build booleans + var rcpList = RegistersUnderlying.Where(x => x.GetType() == typeof(BoolRegister)) + .Select(x => (BoolRegister)x) + .ToArray(); + + //one frame can only read 8 registers at a time + int rcpFrameCount = (int)Math.Ceiling((double)rcpList.Length / 8); + int rcpLastFrameRemainder = rcpList.Length <= 8 ? rcpList.Length : rcpList.Length % 8; + + for (int i = 0; i < rcpFrameCount; i++) { + + int toReadRegistersCount = 8; + + if(i == rcpFrameCount - 1) toReadRegistersCount = rcpLastFrameRemainder; + + var rcpString = new StringBuilder($"%{GetStationNumber()}#RCP{toReadRegistersCount}"); + + for (int j = 0; j < toReadRegistersCount; j++) { + + BoolRegister register = rcpList[i + j]; + rcpString.Append(register.BuildMewtocolQuery()); + + } + + string rcpRequest = rcpString.ToString(); + var result = await SendCommandAsync(rcpRequest); + if (!result.Success) return; + + var resultBitArray = result.Response.ParseRCMultiBit(); + + for (int k = 0; k < resultBitArray.Length; k++) { + + var register = rcpList[i + k]; + + if((bool)register.Value != resultBitArray[k]) { + register.SetValueFromPLC(resultBitArray[k]); + InvokeRegisterChanged(register); + } + + } + + } + + } + + private async Task UpdateDTRegisters () { + + foreach (var reg in RegistersUnderlying) { + + var type = reg.GetType(); + + if(reg.RegisterType.IsNumericDTDDT() || reg.RegisterType == RegisterType.DT_BYTE_RANGE) { + + var lastVal = reg.Value; + var rwReg = (IRegisterInternal)reg; + var readout = await rwReg.ReadAsync(); + if (readout == null) return; + + if (lastVal != readout) { + rwReg.SetValueFromPLC(readout); + InvokeRegisterChanged(reg); + } + + } + + } + + } + + #endregion + + #region Register Colleciton adding + + internal MewtocolInterface WithRegisterCollection (RegisterCollection collection) { + + collection.PLCInterface = this; + + var props = collection.GetType().GetProperties(); + + foreach (var prop in props) { + + var attributes = prop.GetCustomAttributes(true); + + string propName = prop.Name; + foreach (var attr in attributes) { + + if (attr is RegisterAttribute cAttribute && prop.PropertyType.IsAllowedPlcCastingType()) { + + var dotnetType = prop.PropertyType; + + AddRegister(new RegisterBuildInfo { + memoryAddress = cAttribute.MemoryArea, + specialAddress = cAttribute.SpecialAddress, + memorySizeBytes = cAttribute.ByteLength, + registerType = cAttribute.RegisterType, + dotnetCastType = dotnetType, + collectionType = collection.GetType(), + name = prop.Name, + }); + + } + + } + + } + + RegisterChanged += (reg) => { + + //register is used bitwise + if (reg.GetType() == typeof(BytesRegister)) { + + for (int i = 0; i < props.Length; i++) { + + var prop = props[i]; + var bitWiseFound = prop.GetCustomAttributes(true) + .FirstOrDefault(y => y.GetType() == typeof(RegisterAttribute) && ((RegisterAttribute)y).MemoryArea == reg.MemoryAddress); + + if (bitWiseFound != null) { + + var casted = (RegisterAttribute)bitWiseFound; + var bitIndex = casted.AssignedBitIndex; + + BitArray bitAr = null; + + if (reg is NumberRegister reg16) { + var bytes = BitConverter.GetBytes((short)reg16.Value); + bitAr = new BitArray(bytes); + } else if (reg is NumberRegister reg32) { + var bytes = BitConverter.GetBytes((int)reg32.Value); + bitAr = new BitArray(bytes); + } + + if (bitAr != null && bitIndex < bitAr.Length && bitIndex >= 0) { + + //set the specific bit index if needed + prop.SetValue(collection, bitAr[bitIndex]); + collection.TriggerPropertyChanged(prop.Name); + + } else if (bitAr != null) { + + //set the specific bit array if needed + prop.SetValue(collection, bitAr); + collection.TriggerPropertyChanged(prop.Name); + + } + + } + + } + + } + + //updating normal properties + var foundToUpdate = props.FirstOrDefault(x => x.Name == reg.Name); + + if (foundToUpdate != null) { + + var foundAttributes = foundToUpdate.GetCustomAttributes(true); + var foundAttr = foundAttributes.FirstOrDefault(x => x.GetType() == typeof(RegisterAttribute)); + + if (foundAttr == null) + return; + + var registerAttr = (RegisterAttribute)foundAttr; + + //check if bit parse mode + if (registerAttr.AssignedBitIndex == -1) { + + HashSet NumericTypes = new HashSet { + typeof(bool), + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(float), + typeof(TimeSpan), + typeof(string) + }; + + var regValue = ((IRegister)reg).Value; + + if (NumericTypes.Any(x => foundToUpdate.PropertyType == x)) { + foundToUpdate.SetValue(collection, regValue); + } + + if (foundToUpdate.PropertyType.IsEnum) { + foundToUpdate.SetValue(collection, regValue); + } + + } + + collection.TriggerPropertyChanged(foundToUpdate.Name); + + } + + }; + + if (collection != null) + collection.OnInterfaceLinked(this); + + Connected += (i) => { + if (collection != null) + collection.OnInterfaceLinkedAndOnline(this); + }; + + return this; + + } + + #endregion + + #region Register Adding + + /// + public void AddRegister(BaseRegister register) { + + if (CheckDuplicateRegister(register)) + throw MewtocolException.DupeRegister(register); + + if (CheckDuplicateNameRegister(register)) + throw MewtocolException.DupeNameRegister(register); + + register.attachedInterface = this; + RegistersUnderlying.Add(register); + + } + + /// + public void AddRegister(IRegister register) => AddRegister(register as BaseRegister); + + internal void AddRegister (RegisterBuildInfo buildInfo) { + + var builtRegister = buildInfo.Build(); + + //is bitwise and the register list already contains that area register + if(builtRegister.GetType() == typeof(BytesRegister) && CheckDuplicateRegister(builtRegister, out var existing)) { + + return; + + } + + if (CheckDuplicateRegister(builtRegister)) + throw MewtocolException.DupeRegister(builtRegister); + + if(CheckDuplicateNameRegister(builtRegister)) + throw MewtocolException.DupeNameRegister(builtRegister); + + builtRegister.attachedInterface = this; + RegistersUnderlying.Add(builtRegister); + + } + + private bool CheckDuplicateRegister (IRegisterInternal instance, out IRegisterInternal foundDupe) { + + foundDupe = RegistersInternal.FirstOrDefault(x => x.CompareIsDuplicate(instance)); + + return RegistersInternal.Contains(instance) || foundDupe != null; + + } + + private bool CheckDuplicateRegister(IRegisterInternal instance) { + + var foundDupe = RegistersInternal.FirstOrDefault(x => x.CompareIsDuplicate(instance)); + + return RegistersInternal.Contains(instance) || foundDupe != null; + + } + + private bool CheckDuplicateNameRegister(IRegisterInternal instance) { + + return RegistersInternal.Any(x => x.CompareIsNameDuplicate(instance)); + + } + + #endregion + + #region Register accessing + + /// > + public IRegister GetRegister(string name) { + + return RegistersUnderlying.FirstOrDefault(x => x.Name == name); + + } + + /// + public IEnumerable GetAllRegisters () { + + return RegistersUnderlying.Cast(); + + } + + #endregion + + #region Event Invoking + + internal void PropertyRegisterWasSet(string propName, object value) { + + _ = SetRegisterAsync(GetRegister(propName), value); + + } + + internal void InvokeRegisterChanged(IRegister reg) { + + RegisterChanged?.Invoke(reg); + + } + + internal void InvokePolledCycleDone() { + + PolledCycle?.Invoke(); + + } + + #endregion + + } + +} diff --git a/MewtocolNet/MewtocolInterfaceRequests.cs b/MewtocolNet/MewtocolInterfaceRequests.cs new file mode 100644 index 0000000..94015f3 --- /dev/null +++ b/MewtocolNet/MewtocolInterfaceRequests.cs @@ -0,0 +1,329 @@ +using MewtocolNet.Exceptions; +using MewtocolNet.Logging; +using MewtocolNet.Registers; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace MewtocolNet { + + public partial class MewtocolInterface { + + #region PLC info getters + + /// + /// Gets generic information about the PLC + /// + /// A PLCInfo class + public async Task GetPLCInfoAsync(int timeout = -1) { + + var resRT = await SendCommandAsync("%EE#RT", timeoutMs: timeout); + + if (!resRT.Success) { + + //timeouts are ok and dont throw + if (resRT == MewtocolFrameResponse.Timeout) return null; + + throw new MewtocolException(resRT.Error); + + } + + var resEXRT = await SendCommandAsync("%EE#EX00RT00", timeoutMs: timeout); + + //timeouts are ok and dont throw + if (!resRT.Success && resRT == MewtocolFrameResponse.Timeout) return null; + + PLCInfo plcInf; + + //dont overwrite, use first + if (!PLCInfo.TryFromRT(resRT.Response, out plcInf)) { + + throw new MewtocolException("The RT message could not be parsed"); + + } + + //overwrite first with EXRT + if (resEXRT.Success && !plcInf.TryExtendFromEXRT(resEXRT.Response)) { + + throw new MewtocolException("The EXRT message could not be parsed"); + + } + + PlcInfo = plcInf; + + return plcInf; + + } + + #endregion + + #region Operation mode changing + + /// + /// Changes the PLCs operation mode to the given one + /// + /// The mode to change to + /// The success state of the write operation + public async Task SetOperationMode (bool setRun) { + + string modeChar = setRun ? "R" : "P"; + + string requeststring = $"%{GetStationNumber()}#RM{modeChar}"; + var result = await SendCommandAsync(requeststring); + + if (result.Success) { + Logger.Log($"operation mode was changed to {(setRun ? "Run" : "Prog")}", LogLevel.Info, this); + } else { + Logger.Log("Operation mode change failed", LogLevel.Error, this); + } + + return result.Success; + + } + + #endregion + + #region Byte range writingv / reading to registers + + /// + /// Writes a byte array to a span over multiple registers at once, + /// Rembember the plc can only store word so in order to write to a word array + /// your byte array should be double the size + /// + /// /// start address of the array + /// + /// + public async Task WriteByteRange (int start, byte[] byteArr) { + + string byteString = byteArr.BigToMixedEndian().ToHexString(); + var wordLength = byteArr.Length / 2; + if (byteArr.Length % 2 != 0) + wordLength++; + + string startStr = start.ToString().PadLeft(5, '0'); + string endStr = (start + wordLength - 1).ToString().PadLeft(5, '0'); + + string requeststring = $"%{GetStationNumber()}#WDD{startStr}{endStr}{byteString}"; + var result = await SendCommandAsync(requeststring); + + return result.Success; + + } + + /// + /// Reads the bytes from the start adress for counts byte length + /// + /// Start adress + /// Number of bytes to get + /// Flips bytes from big to mixed endian + /// Gets invoked when the progress changes, contains the progress as a double + /// A byte array or null of there was an error + public async Task ReadByteRange(int start, int count, bool flipBytes = true, Action onProgress = null) { + + var byteList = new List(); + + var wordLength = count / 2; + if (count % 2 != 0) + wordLength++; + + + //read blocks of max 4 words per msg + for (int i = 0; i < wordLength; i += 8) { + + int curWordStart = start + i; + int curWordEnd = curWordStart + 7; + + string startStr = curWordStart.ToString().PadLeft(5, '0'); + string endStr = (curWordEnd).ToString().PadLeft(5, '0'); + + string requeststring = $"%{GetStationNumber()}#RDD{startStr}{endStr}"; + var result = await SendCommandAsync(requeststring); + + if (result.Success && !string.IsNullOrEmpty(result.Response)) { + + var bytes = result.Response.ParseDTByteString(8 * 4).HexStringToByteArray(); + + if (bytes == null) return null; + + if (flipBytes) { + byteList.AddRange(bytes.BigToMixedEndian().Take(count).ToArray()); + } else { + byteList.AddRange(bytes.Take(count).ToArray()); + } + + } + + if (onProgress != null) + onProgress((double)i / wordLength); + + } + + return byteList.ToArray(); + + } + + #endregion + + #region Raw register reading / writing + + internal async Task ReadRawRegisterAsync (IRegisterInternal _toRead) { + + var toreadType = _toRead.GetType(); + + //returns a byte array 1 long and with the byte beeing 0 or 1 + if (toreadType == typeof(BoolRegister)) { + + string requeststring = $"%{GetStationNumber()}#RCS{_toRead.BuildMewtocolQuery()}"; + var result = await SendCommandAsync(requeststring); + if (!result.Success) return null; + + var resultBool = result.Response.ParseRCSingleBit(); + if (resultBool != null) { + + return resultBool.Value ? new byte[] { 1 } : new byte[] { 0 }; + + } + + } + + //returns a byte array 2 bytes or 4 bytes long depending on the data size + if (toreadType.IsGenericType && _toRead.GetType().GetGenericTypeDefinition() == typeof(NumberRegister<>)) { + + string requeststring = $"%{GetStationNumber()}#RD{_toRead.BuildMewtocolQuery()}"; + var result = await SendCommandAsync(requeststring); + if (!result.Success) return null; + + if(_toRead.RegisterType == RegisterType.DT) { + + return result.Response.ParseDTByteString(4).HexStringToByteArray(); + + } else { + + return result.Response.ParseDTByteString(8).HexStringToByteArray(); + + } + + } + + //returns a byte array with variable size + if (toreadType == typeof(BytesRegister)) { + + string requeststring = $"%{GetStationNumber()}#RD{_toRead.BuildMewtocolQuery()}"; + var result = await SendCommandAsync(requeststring); + if (!result.Success) return null; + + var resBytes = result.Response.ParseDTRawStringAsBytes(); + + return resBytes; + + } + + if (toreadType == typeof(StringRegister)) { + + string requeststring = $"%{GetStationNumber()}#RD{_toRead.BuildMewtocolQuery()}"; + var result = await SendCommandAsync(requeststring); + if (!result.Success) return null; + + var resBytes = result.Response.ParseDTRawStringAsBytes(); + + return resBytes; + + } + + throw new Exception($"Failed to load the byte data for: {_toRead}"); + + } + + internal async Task WriteRawRegisterAsync (IRegisterInternal _toWrite, byte[] data) { + + var toWriteType = _toWrite.GetType(); + + //returns a byte array 1 long and with the byte beeing 0 or 1 + if (toWriteType == typeof(BoolRegister)) { + + string requeststring = $"%{GetStationNumber()}#WCS{_toWrite.BuildMewtocolQuery()}{(data[0] == 1 ? "1" : "0")}"; + var result = await SendCommandAsync(requeststring); + return result.Success; + + } + + //writes a byte array 2 bytes or 4 bytes long depending on the data size + if (toWriteType.IsGenericType && toWriteType.GetGenericTypeDefinition() == typeof(NumberRegister<>)) { + + string requeststring = $"%{GetStationNumber()}#WD{_toWrite.BuildMewtocolQuery()}{data.ToHexString()}"; + var result = await SendCommandAsync(requeststring); + return result.Success; + + } + + //returns a byte array with variable size + if (toWriteType == typeof(BytesRegister)) { + + throw new NotImplementedException("Not imp"); + + } + + //writes to the string area + if (toWriteType == typeof(StringRegister)) { + + string requeststring = $"%{GetStationNumber()}#WD{_toWrite.BuildMewtocolQuery()}{data.ToHexString()}"; + var result = await SendCommandAsync(requeststring); + return result.Success; + + } + + return false; + + } + + #endregion + + #region Register reading / writing + + internal async Task SetRegisterAsync (IRegister register, object value) { + + var internalReg = (IRegisterInternal)register; + + return await internalReg.WriteAsync(value); + + } + + #endregion + + #region Reading / Writing Plc program + + public async Task GetSystemRegister () { + + //the "." means CR or \r + + await SendCommandAsync("%EE#RT"); + + //then get plc status extended? gets polled all time + // %EE#EX00RT00 + await SendCommandAsync("%EE#EX00RT00"); + + + + } + + #endregion + + #region Helpers + + internal string GetStationNumber() { + + return StationNumber.ToString().PadLeft(2, '0'); + + + } + + #endregion + + } + +} diff --git a/MewtocolNet/MewtocolInterfaceSerial.cs b/MewtocolNet/MewtocolInterfaceSerial.cs new file mode 100644 index 0000000..62abbc8 --- /dev/null +++ b/MewtocolNet/MewtocolInterfaceSerial.cs @@ -0,0 +1,293 @@ +using MewtocolNet.Logging; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Ports; +using System.Linq; +using System.Net.Sockets; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using System.Threading; +using MewtocolNet.RegisterAttributes; + +namespace MewtocolNet { + + public class MewtocolInterfaceSerial : MewtocolInterface, IPlcSerial { + + private bool autoSerial; + + private event Action tryingSerialConfig; + + //serial config + + /// + public string PortName { get; private set; } + + /// + public int SerialBaudRate { get; private set; } + + /// + public int SerialDataBits { get; private set; } + + /// + public Parity SerialParity { get; private set; } + + /// + public StopBits SerialStopBits { get; private set; } + + //Serial + internal SerialPort serialClient; + + internal MewtocolInterfaceSerial () : base() { } + + /// + public IPlcSerial WithPoller () { + + usePoller = true; + return this; + + } + + public IPlcSerial AddRegisterCollection (RegisterCollection collection) { + + WithRegisterCollection(collection); + return this; + + } + + /// + public override string GetConnectionInfo() { + + StringBuilder sb = new StringBuilder(); + + sb.Append($"{PortName}, "); + sb.Append($"{SerialBaudRate}, "); + sb.Append($"{SerialDataBits} "); + + sb.Append($"{SerialParity.ToString().Substring(0, 1)} "); + + switch (SerialStopBits) { + case StopBits.None: + sb.Append("0"); + break; + case StopBits.One: + sb.Append("1"); + break; + case StopBits.Two: + sb.Append("2"); + break; + case StopBits.OnePointFive: + sb.Append("1.5"); + break; + } + + return sb.ToString(); + + } + + /// + public void ConfigureConnection (string _portName, int _baudRate = 19200, int _dataBits = 8, Parity _parity = Parity.Odd, StopBits _stopBits = StopBits.One, int _station = 1) { + + PortName = _portName; + SerialBaudRate = _baudRate; + SerialDataBits = _dataBits; + SerialParity = _parity; + SerialStopBits = _stopBits; + stationNumber = _station; + + OnSerialPropsChanged(); + Disconnect(); + + } + + internal void ConfigureConnectionAuto () { + + autoSerial = true; + + } + + public override async Task ConnectAsync() => await ConnectAsync(null); + + /// + public async Task ConnectAsync (Action onTryingConfig = null) { + + void OnTryConfig() { + onTryingConfig(); + } + + if (onTryingConfig != null) + tryingSerialConfig += OnTryConfig; + + try { + + PLCInfo? gotInfo = null; + + if(autoSerial) { + + Logger.Log($"Connecting [AUTO CONFIGURE]: {PortName}", LogLevel.Info, this); + gotInfo = await TryConnectAsyncMulti(); + + } else { + + Logger.Log($"Connecting [MAN]: {PortName}", LogLevel.Info, this); + gotInfo = await TryConnectAsyncSingle(PortName, SerialBaudRate, SerialDataBits, SerialParity, SerialStopBits); + + } + + if(gotInfo != null) { + + OnConnected(gotInfo.Value); + + } else { + + Logger.Log("Initial connection failed", LogLevel.Error, this); + OnMajorSocketExceptionWhileConnecting(); + + } + + await Task.CompletedTask; + + } catch (SocketException) { + + OnMajorSocketExceptionWhileConnecting(); + + } + + tryingSerialConfig -= OnTryConfig; + + } + + private async Task TryConnectAsyncMulti () { + + var baudRates = Enum.GetValues(typeof(BaudRate)).Cast(); + + //ordered by most commonly used + baudRates = new List { + //most common 3 + BaudRate._19200, + BaudRate._115200, + BaudRate._9600, + //others + BaudRate._1200, + BaudRate._2400, + BaudRate._4800, + BaudRate._38400, + BaudRate._57600, + BaudRate._230400, + }; + + var dataBits = Enum.GetValues(typeof(DataBits)).Cast(); + var parities = new List() { Parity.None, Parity.Odd, Parity.Even, Parity.Mark }; + var stopBits = new List { StopBits.One, StopBits.Two }; + + foreach (var baud in baudRates) { + + foreach (var databit in dataBits) { + + foreach (var parity in parities) { + + foreach (var stopBit in stopBits) { + + var res = await TryConnectAsyncSingle(PortName, (int)baud, (int)databit, parity, stopBit); + if(res != null) return res; + + } + + } + + } + + } + + return null; + + } + + private async Task TryConnectAsyncSingle (string port, int baud, int dbits, Parity par, StopBits sbits) { + + try { + + serialClient = new SerialPort() { + PortName = port, + BaudRate = baud, + DataBits = dbits, + Parity = par, + StopBits = sbits, + ReadTimeout = 100, + Handshake = Handshake.None + }; + + PortName = port; + SerialBaudRate = baud; + SerialDataBits = dbits; + SerialParity = par; + SerialStopBits = sbits; + OnSerialPropsChanged(); + tryingSerialConfig?.Invoke(); + + serialClient.Open(); + + if (!serialClient.IsOpen) { + + Logger.Log($"Failed to open [SERIAL]: {GetConnectionInfo()}", LogLevel.Critical, this); + return null; + + } + + stream = serialClient.BaseStream; + + Logger.Log($"Opened [SERIAL]: {GetConnectionInfo()}", LogLevel.Critical, this); + + var plcinf = await GetPLCInfoAsync(100); + + if (plcinf == null) CloseClient(); + + return plcinf; + + } catch (UnauthorizedAccessException) { + + Logger.Log($"The port {serialClient.PortName} is currently in use. Close all accessing applications first", LogLevel.Error, this); + return null; + + } + + } + + private void CloseClient () { + + if(serialClient.IsOpen) { + + serialClient.Close(); + Logger.Log($"Closed [SERIAL]", LogLevel.Verbose, this); + + } + + } + + private protected override void OnDisconnect() { + + if (IsConnected) { + + base.OnDisconnect(); + + CloseClient(); + + } + + } + + private void OnSerialPropsChanged () { + + OnPropChange(nameof(PortName)); + OnPropChange(nameof(SerialBaudRate)); + OnPropChange(nameof(SerialDataBits)); + OnPropChange(nameof(SerialParity)); + OnPropChange(nameof(SerialStopBits)); + + } + + } + +} diff --git a/MewtocolNet/MewtocolInterfaceTcp.cs b/MewtocolNet/MewtocolInterfaceTcp.cs new file mode 100644 index 0000000..af5ffb2 --- /dev/null +++ b/MewtocolNet/MewtocolInterfaceTcp.cs @@ -0,0 +1,179 @@ +using MewtocolNet.Exceptions; +using MewtocolNet.Logging; +using MewtocolNet.Queue; +using MewtocolNet.RegisterAttributes; +using MewtocolNet.Registers; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.Design; +using System.Diagnostics; +using System.IO; +using System.IO.Ports; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace MewtocolNet { + + /// + /// The PLC com interface class + /// + public class MewtocolInterfaceTcp : MewtocolInterface, IPlcEthernet { + + //TCP + internal TcpClient client; + + private IPAddress ipAddr; + + /// + public string IpAddress => ipAddr.ToString(); + + /// + public int Port { get; private set; } + + /// + public IPEndPoint HostEndpoint { get; set; } + + internal MewtocolInterfaceTcp () : base() { } + + /// + public IPlcEthernet WithPoller () { + + usePoller = true; + return this; + + } + + /// + public IPlcEthernet AddRegisterCollection (RegisterCollection collection) { + + WithRegisterCollection(collection); + return this; + + } + + #region TCP connection state handling + + /// + public void ConfigureConnection (string ip, int port = 9094, int station = 1) { + + if (!IPAddress.TryParse(ip, out ipAddr)) + throw new MewtocolException($"The ip: {ip} is no valid ip address"); + + Port = port; + stationNumber = station; + + Disconnect(); + + } + + /// + public void ConfigureConnection(IPAddress ip, int port = 9094, int station = 1) { + + ipAddr = ip; + Port = port; + stationNumber = station; + + Disconnect(); + + } + + /// + public override async Task ConnectAsync () { + + try { + + if (HostEndpoint != null) { + + client = new TcpClient(HostEndpoint) { + ReceiveBufferSize = RecBufferSize, + NoDelay = false, + }; + var ep = (IPEndPoint)client.Client.LocalEndPoint; + Logger.Log($"Connecting [MAN] endpoint: {ep.Address}:{ep.Port}", LogLevel.Info, this); + + } else { + + client = new TcpClient() { + ReceiveBufferSize = RecBufferSize, + NoDelay = false, + //ExclusiveAddressUse = true, + }; + + } + + var result = client.BeginConnect(ipAddr, Port, null, null); + var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(ConnectTimeout)); + + if (!success || !client.Connected) { + + Logger.Log("The PLC connection timed out", LogLevel.Error, this); + OnMajorSocketExceptionWhileConnecting(); + return; + } + + if (HostEndpoint == null) { + var ep = (IPEndPoint)client.Client.LocalEndPoint; + Logger.Log($"Connecting [AUTO] endpoint: {ep.Address.MapToIPv4()}:{ep.Port}", LogLevel.Info, this); + } + + //get the stream + stream = client.GetStream(); + stream.ReadTimeout = 1000; + + //get plc info + var plcinf = await GetPLCInfoAsync(); + + if (plcinf != null) { + + OnConnected(plcinf.Value); + + } else { + + Logger.Log("Initial connection failed", LogLevel.Error, this); + OnDisconnect(); + + } + + await Task.CompletedTask; + + } catch (SocketException) { + + OnMajorSocketExceptionWhileConnecting(); + + } + + } + + /// + /// Gets the connection info string + /// + public override string GetConnectionInfo() { + + return $"{IpAddress}:{Port}"; + + } + + private protected override void OnDisconnect() { + + if (IsConnected) { + + base.OnDisconnect(); + + client.Close(); + + } + + } + + #endregion + + } + +} \ No newline at end of file diff --git a/MewtocolNet/MewtocolNet.csproj b/MewtocolNet/MewtocolNet.csproj index 5f420a3..89d2219 100644 --- a/MewtocolNet/MewtocolNet.csproj +++ b/MewtocolNet/MewtocolNet.csproj @@ -1,8 +1,12 @@  + + + false + netstandard2.0 Mewtocol.NET - 0.7.0 + 0.0.0 Felix Weiss Womed true @@ -13,14 +17,28 @@ plc;panasonic;mewtocol;automation; MIT 2ccdcc9b-94a3-4e76-8827-453ab889ea33 + - - ..\Builds\MewtocolNet.xml - ..\Builds + + + ..\Builds\MewtocolNet\MewtocolNet.xml + ..\Builds\MewtocolNet + - - <_Parameter1>MewtocolTests - - + + <_Parameter1>MewtocolTests + + + + + + <_Parameter1>DocBuilder + + + + + + + diff --git a/MewtocolNet/PLCInfo.cs b/MewtocolNet/PLCInfo.cs new file mode 100644 index 0000000..ce3ae79 --- /dev/null +++ b/MewtocolNet/PLCInfo.cs @@ -0,0 +1,123 @@ +using System.Globalization; +using System.Text.RegularExpressions; + +namespace MewtocolNet { + + /// + /// Holds various informations about the PLC + /// + public struct PLCInfo { + + /// + /// The type of the PLC named by Panasonic + /// + public PlcType TypeCode { get; private set; } + + /// + /// Contains information about the PLCs operation modes as flags + /// + public OPMode OperationMode { get; private set; } + + /// + /// Hardware information flags about the PLC + /// + public HWInformation HardwareInformation { get; private set; } + + /// + /// Program capacity in 1K steps + /// + public int ProgramCapacity { get; private set; } + + /// + /// Version of the cpu + /// + public string CpuVersion { get; private set; } + + /// + /// Current error code of the PLC + /// + public string SelfDiagnosticError { get; internal set; } + + /// + /// Quickcheck for the runmode flag + /// + public bool IsRunMode => OperationMode.HasFlag(OPMode.RunMode); + + internal bool TryExtendFromEXRT (string msg) { + + var regexEXRT = new Regex(@"\%EE\$EX00RT00(?..)(?..)..(?..)(?..)..(?..)(?....)(?..)(?..)(?.)(?....)(?....)(?....).*", RegexOptions.IgnoreCase); + var match = regexEXRT.Match(msg); + if(match.Success) { + + byte typeCodeByte = byte.Parse(match.Groups["mc"].Value, NumberStyles.HexNumber); + + this.TypeCode = (PlcType)typeCodeByte; + this.CpuVersion = match.Groups["ver"].Value; + this.HardwareInformation = (HWInformation)byte.Parse(match.Groups["hwif"].Value, NumberStyles.HexNumber); + + return true; + + } + + return false; + + } + + internal static bool TryFromRT (string msg, out PLCInfo inf) { + + var regexRT = new Regex(@"\%EE\$RT(?..)(?..)(?..)(?..)..(?..)(?....).*", RegexOptions.IgnoreCase); + var match = regexRT.Match(msg); + if (match.Success) { + + byte typeCodeByte = byte.Parse(match.Groups["cputype"].Value, NumberStyles.HexNumber); + + inf = new PLCInfo { + TypeCode = (PlcType)typeCodeByte, + CpuVersion = match.Groups["cpuver"].Value, + ProgramCapacity = int.Parse(match.Groups["cap"].Value), + SelfDiagnosticError = match.Groups["sdiag"].Value, + OperationMode = (OPMode)byte.Parse(match.Groups["op"].Value, NumberStyles.HexNumber), + }; + + return true; + + } + + inf = default(PLCInfo); + return false; + + } + + /// + /// Plc info when its not connected + /// + public static PLCInfo None => new PLCInfo() { + + SelfDiagnosticError = "", + CpuVersion = "", + HardwareInformation = 0, + OperationMode = 0, + ProgramCapacity = 0, + TypeCode = 0, + + }; + + /// + public static bool operator == (PLCInfo c1, PLCInfo c2) { + return c1.Equals(c2); + } + + /// + public static bool operator != (PLCInfo c1, PLCInfo c2) { + return !c1.Equals(c2); + } + + public override string ToString() { + + return $"{TypeCode.ToName()}, OP: {OperationMode}"; + + } + + } + +} \ No newline at end of file diff --git a/MewtocolNet/PublicEnums/BaudRate.cs b/MewtocolNet/PublicEnums/BaudRate.cs new file mode 100644 index 0000000..772eefd --- /dev/null +++ b/MewtocolNet/PublicEnums/BaudRate.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet { + + public enum BaudRate { + + _1200 = 1200, + _2400 = 2400, + _4800 = 4800, + _9600 = 9600, + _19200 = 19200, + _38400 = 38400, + _57600 = 57600, + _115200 = 115200, + _230400 = 230400, + + } + +} diff --git a/MewtocolNet/PublicEnums/BitCount.cs b/MewtocolNet/PublicEnums/BitCount.cs new file mode 100644 index 0000000..fb0478f --- /dev/null +++ b/MewtocolNet/PublicEnums/BitCount.cs @@ -0,0 +1,17 @@ +namespace MewtocolNet { + + /// + /// The size of the bitwise register + /// + public enum BitCount { + /// + /// 16 bit + /// + B16, + /// + /// 32 bit + /// + B32 + } + +} diff --git a/MewtocolNet/PublicEnums/DataBits.cs b/MewtocolNet/PublicEnums/DataBits.cs new file mode 100644 index 0000000..f5f41fd --- /dev/null +++ b/MewtocolNet/PublicEnums/DataBits.cs @@ -0,0 +1,9 @@ +namespace MewtocolNet { + public enum DataBits { + + Seven = 7, + Eight = 8, + + } + +} diff --git a/MewtocolNet/PublicEnums/HWInformation.cs b/MewtocolNet/PublicEnums/HWInformation.cs new file mode 100644 index 0000000..2f9b06d --- /dev/null +++ b/MewtocolNet/PublicEnums/HWInformation.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet { + + /// + /// Contains hardware information about the device as flags + /// + [Flags] + public enum HWInformation : byte { + + /// + /// Has user ROM + /// + UserROM = 1, + /// + /// Has IC card + /// + ICCard = 2, + /// + /// Has general purpose memory + /// + GeneralPurposeMemory = 4, + /// + /// Is CPU ultra high speed type + /// + UltraHighSpeed = 8, + + } + +} diff --git a/MewtocolNet/PublicEnums/IOType.cs b/MewtocolNet/PublicEnums/IOType.cs new file mode 100644 index 0000000..dde6f35 --- /dev/null +++ b/MewtocolNet/PublicEnums/IOType.cs @@ -0,0 +1,26 @@ +namespace MewtocolNet { + + // this is just used as syntactic sugar, + // when creating registers that are R/X/Y typed you dont need the DT types + + /// + /// The type of an input/output register + /// + public enum IOType { + + /// + /// Physical input as a bool (Relay) + /// + X = 0, + /// + /// Physical output as a bool (Relay) + /// + Y = 1, + /// + /// Internal relay + /// + R = 2, + + } + +} diff --git a/MewtocolNet/PublicEnums/OPMode.cs b/MewtocolNet/PublicEnums/OPMode.cs new file mode 100644 index 0000000..73a8467 --- /dev/null +++ b/MewtocolNet/PublicEnums/OPMode.cs @@ -0,0 +1,50 @@ +using System; + +namespace MewtocolNet { + + /// + /// Descibes the operation mode of the device as flags + /// + [Flags] + public enum OPMode : byte { + + /// + /// No operation mode flag active + /// + None = 0, + /// + /// Is in RUN mode, otherwise its PROG Mode + /// + RunMode = 1, + /// + /// Is in test mode, otherwise ok + /// + TestMode = 2, + /// + /// Is BRK/1 step executed + /// + BreakPointPerOneStep = 4, + /// + /// Is BRK command enabled + /// + BreakEnabled = 16, + /// + /// Is outputting to external device + /// + ExternalOutput = 32, + /// + /// Is 1 step exec enabled + /// + OneStepExecEnabled = 64, + /// + /// Is a message displayed? + /// + MessageInstructionDisplayed = 128, + /// + /// Is in remote mode + /// + RemoteMode = 255, + + } + +} diff --git a/MewtocolNet/PublicEnums/PlcType.cs b/MewtocolNet/PublicEnums/PlcType.cs new file mode 100644 index 0000000..b1e2cbf --- /dev/null +++ b/MewtocolNet/PublicEnums/PlcType.cs @@ -0,0 +1,322 @@ +using MewtocolNet.DocAttributes; + +namespace MewtocolNet { + + //this overwrites the CPU code and only comes with EXRT + //special chars: (d = -) (c = .) (s = /) + + //MISSING! FP7 and EcoLogix + + /// + /// The type of the PLC + /// + public enum PlcType { + + #region FP5 Family (Legacy) + + /// + /// FP5 16k + /// + [PlcLegacy] + FP5_16k = 0x02, + + /// + /// FP5 24k + /// + [PlcLegacy] + FP5_24k = 0x12, + + #endregion + + #region FP2 Family (Legacy) + + /// + /// FP2 16k OR FP2 32k + /// + [PlcLegacy] + FP2_16k_OR_FP2_32k = 0x50, + + //misses entry FP2 32k + + #endregion + + #region FP3/FP-C Family (Legacy) + + /// + /// FP3 10k + /// + [PlcLegacy] + FP3_10k = 0x03, + /// + /// FP3 or FP-C 16k + /// + [PlcLegacy] + FP3_16k_OR_FPdC_16k = 0x13, + + #endregion + + #region FP1 / FPM Family (Legacy) + + /// + /// FP1 0.9k C14,C16 or FP-M 0.9k C16T + /// + [PlcLegacy] + FP1_0c9k__C14_C16_OR_FPdM_0c9k__C16T = 0x04, + /// + /// FP1 2.7k C24,C40 or FP-M 2.7k C20R,C20T,C32T + /// + [PlcLegacy] + FP1_2c7k__C24_C40_OR_FPdM_2c7k__C20R_C20T_C32T = 0x05, + /// + /// FP1 5.0k C56,C72 or FPM 5k C20RC,C20TC,C32TC + /// + [PlcLegacy] + FP1_5k__C56_C72_OR_FPdM_5k__C20RC_C20TC_C32TC = 0x06, + + #endregion + + #region FP10 Family (Legacy) + + /// + /// FP10 30k,60k OR FP10S 30k + /// + [PlcLegacy] + FP10_30k_OR_FP10_60k_OR_FP10S_30k = 0x20, + + //misses entry FP10 60k + + #endregion + + #region FP10SH Family (Legacy) + + /// + /// FP10SH 30k, 60k, 120k + /// + [PlcLegacy] + FP10SH_30k_OR_FP10SH_60k_OR_FP10SH_120k = 0x30, + + #endregion + + #region FP0 Family (Legacy) + + /// + /// FP0 2.7k C10,C14,C16 + /// + [PlcLegacy] + FP0_2c7k__C10_C14_C16 = 0x40, + /// + /// FP0 5k + /// + [PlcLegacy] + FP0_5k__C32_SL1 = 0x41, + /// + /// FP0 10k + /// + [PlcLegacy] + FP0_10c0k__T32 = 0x42, + + #endregion + + #region FP-Sigma Family (Legacy) + + /// + /// FP-SIGMA 12k + /// + [PlcLegacy, PlcEXRT] + FPdSIGMA_12k = 0x43, + /// + /// FP-SIGMA 32k + /// + [PlcLegacy, PlcEXRT] + FPdSIGMA_32k = 0x44, + /// + /// FP-SIGMA 16k or FP-SIGMA 40k + /// + [PlcLegacy, PlcEXRT] + FPdSIGMA_16k_OR_FPdSIGMA_40k = 0xE1, + + #endregion + + #region FP-e Family (Legacy) + + /// + /// FP-e 2.7k + /// + [PlcLegacy, PlcEXRT] + FPde_2c7k = 0x45, + + #endregion + + #region FP0R Family + + /// + /// FP0R 16k C10,C14,C16 + /// + [PlcEXRT] + FP0R_16k__C10_C14_C16 = 0x46, + /// + /// FP0R 32k C32 + /// + [PlcEXRT] + FP0R_32k__C32 = 0x47, + /// + /// FP0R 32k T32 + /// + [PlcEXRT] + FP0R_32k__T32 = 0x48, + /// + /// FP0R 32k F32 + /// + [PlcEXRT] + FP0R_32k__F32 = 0x49, + + #endregion + + #region FP2SH Family (Legacy) + + /// + /// FP2SH 60k + /// + [PlcLegacy, PlcEXRT] + FP2SH_60k = 0x60, + /// + /// FP2SH 32k + /// + [PlcLegacy, PlcEXRT] + FP2SH_32k = 0x62, + /// + /// FP2SH 120k + /// + [PlcLegacy, PlcEXRT] + FP2SH_120k = 0xE0, + + #endregion + + #region FP-X Family (Legacy) + + /// + /// FP-X 16k C14R + /// + [PlcLegacy, PlcEXRT, PlcCodeTested] + FPdX_16k__C14R = 0x70, + /// + /// FP-X 32k C30R,C60R + /// + [PlcLegacy, PlcEXRT] + FPdX_32k__C30R_C60R = 0x71, + /// + /// FP-X0 2.5k L14,L30 + /// + [PlcLegacy, PlcEXRT] + FPdX0_2c5k__L14_L30 = 0x72, + /// + /// FP-X 16k L14 + /// + [PlcLegacy, PlcEXRT] + FPdX_16k__L14 = 0x73, + /// + /// FP-X 32k L30,L60 + /// + [PlcLegacy, PlcEXRT] + FPdX_32k__L30_L60 = 0x74, + /// + /// FP-X0 8k L40,L60 + /// + [PlcLegacy, PlcEXRT] + FPdX0_8k__L40_L60 = 0x75, + /// + /// FP-X 16k C14T/P + /// + [PlcLegacy, PlcEXRT] + FPdX_16k__C14TsP = 0x76, + /// + /// FP-X 32k C30T/P,C60T/P,C38AT,C40T + /// + [PlcLegacy, PlcEXRT, PlcCodeTested] + FPdX_32k__C30TsP_C60TsP_C38AT_C40T = 0x77, + /// + /// FP-X 2.5k C40RT0A + /// + [PlcLegacy, PlcEXRT] + FPdX_2c5k__C40RT0A = 0x7A, + /// + /// FP-X0 16k L40,L60 + /// + [PlcLegacy, PlcEXRT] + FPdX0_16k__L40_L60 = 0x7F, + + #endregion + + #region FP-XH Family + + /// + /// FP-XH 16k C14R + /// + [PlcEXRT, PlcCodeTested] + FPdXH_16k__C14R = 0xA0, + /// + /// FP-XH 32k C30R,C40R,C60R + /// + [PlcEXRT] + FPdXH_32k__C30R_C40R_C60R = 0xA1, + /// + /// FP-XH 16k C14T/P + /// + [PlcEXRT] + FPdXH_16k__C14TsP = 0xA4, + /// + /// FP-XH 32k C30T/P,C40T,C60T/P + /// + [PlcEXRT, PlcCodeTested] + FPdXH_32k__C30TsP_C40T_C60TsP = 0xA5, + /// + /// FP-XH 32k C38AT + /// + [PlcEXRT] + FPdXH_32k__C38AT = 0xA7, + /// + /// FP-XH 32k M4T/L + /// + [PlcEXRT] + FPdXH_32k__M4TsL = 0xA8, + /// + /// FP-XH 32k M8N16T/P (RTEX) + /// + [PlcEXRT] + FPdXH_32k__M8N16TsP = 0xAC, + /// + /// FP-XH 32k M8N30T (RTEX) + /// + [PlcEXRT] + FPdXH_32k__M8N30T = 0xAD, + /// + /// FP-XH 32k C40ET,C60ET + /// + [PlcEXRT] + FPdXH_32k__C40ET_C60ET = 0xAE, + /// + /// FP-XH 32k C60ETF + /// + [PlcEXRT] + FPdXH_32k__C60ETF = 0xAF, + + #endregion + + #region FP0H Family + + /// + /// FP0H 32k C32T/P + /// + [PlcEXRT] + FP0H_32k__C32TsP = 0xB0, + /// + /// FP0H 32k C32ET/EP + /// + [PlcEXRT] + FP0H_32k__C32ETsEP = 0xB1, + + #endregion + + } + +} \ No newline at end of file diff --git a/MewtocolNet/PublicEnums/PlcVarType.cs b/MewtocolNet/PublicEnums/PlcVarType.cs new file mode 100644 index 0000000..d7ac5de --- /dev/null +++ b/MewtocolNet/PublicEnums/PlcVarType.cs @@ -0,0 +1,20 @@ +using System.Text; + +namespace MewtocolNet { + + public enum PlcVarType { + + BOOL, + INT, + UINT, + DINT, + UDINT, + REAL, + TIME, + STRING, + WORD, + DWORD + + } + +} diff --git a/MewtocolNet/PublicEnums/RegisterType.cs b/MewtocolNet/PublicEnums/RegisterType.cs new file mode 100644 index 0000000..4dcf5ce --- /dev/null +++ b/MewtocolNet/PublicEnums/RegisterType.cs @@ -0,0 +1,37 @@ +using System; + +namespace MewtocolNet { + + /// + /// The register prefixed type + /// + public enum RegisterType { + + /// + /// Physical input as a bool (Relay) + /// + X = 0, + /// + /// Physical output as a bool (Relay) + /// + Y = 1, + /// + /// Internal as a bool (Relay) + /// + R = 2, + /// + /// Single word area (Register) + /// + DT = 3, + /// + /// Double word area (Register) + /// + DDT = 4, + /// + /// Area of a byte sequence longer than 2 words + /// + DT_BYTE_RANGE = 5, + + } + +} diff --git a/MewtocolNet/RegisterAttributes/RegisterAttribute.cs b/MewtocolNet/RegisterAttributes/RegisterAttribute.cs new file mode 100644 index 0000000..cd79645 --- /dev/null +++ b/MewtocolNet/RegisterAttributes/RegisterAttribute.cs @@ -0,0 +1,81 @@ +using System; + +namespace MewtocolNet.RegisterAttributes { + + /// + /// Defines the behavior of a register property + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + public class RegisterAttribute : Attribute { + + internal RegisterType? RegisterType; + + internal int MemoryArea = 0; + internal int ByteLength = 2; + internal byte SpecialAddress = 0x0; + + internal BitCount BitCount; + internal int AssignedBitIndex = -1; + + /// + /// Attribute for string type or numeric registers + /// + /// The area in the plcs memory + public RegisterAttribute(int memoryArea) { + + MemoryArea = memoryArea; + + } + + public RegisterAttribute(int memoryArea, int byteLength) { + + MemoryArea = memoryArea; + ByteLength = byteLength; + + } + + public RegisterAttribute(int memoryArea, BitCount bitCount) { + + MemoryArea = memoryArea; + BitCount = bitCount; + AssignedBitIndex = 0; + + RegisterType = BitCount == BitCount.B16 ? MewtocolNet.RegisterType.DT : MewtocolNet.RegisterType.DDT; + + } + + public RegisterAttribute(int memoryArea, BitCount bitCount, int bitIndex) { + + MemoryArea = memoryArea; + BitCount = bitCount; + AssignedBitIndex = bitIndex; + + RegisterType = BitCount == BitCount.B16 ? MewtocolNet.RegisterType.DT : MewtocolNet.RegisterType.DDT; + + } + + /// + /// Attribute for boolean registers + /// + public RegisterAttribute(IOType type, byte spAdress = 0x0) { + + MemoryArea = 0; + RegisterType = (RegisterType)(int)type; + SpecialAddress = spAdress; + + } + + /// + /// Attribute for boolean registers + /// + public RegisterAttribute(IOType type, int memoryArea, byte spAdress = 0x0) { + + MemoryArea = memoryArea; + RegisterType = (RegisterType)(int)type; + SpecialAddress = spAdress; + + } + + } + +} diff --git a/MewtocolNet/Mewtocol/RegisterAttributes/RegisterCollectionBase.cs b/MewtocolNet/RegisterAttributes/RegisterCollection.cs similarity index 67% rename from MewtocolNet/Mewtocol/RegisterAttributes/RegisterCollectionBase.cs rename to MewtocolNet/RegisterAttributes/RegisterCollection.cs index 4b5f2a5..497abf1 100644 --- a/MewtocolNet/Mewtocol/RegisterAttributes/RegisterCollectionBase.cs +++ b/MewtocolNet/RegisterAttributes/RegisterCollection.cs @@ -1,25 +1,17 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Linq; +using System.ComponentModel; using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; -namespace MewtocolNet.RegisterAttributes -{ +namespace MewtocolNet.RegisterAttributes { /// /// A register collection base with full auto read and notification support built in /// - public class RegisterCollectionBase : INotifyPropertyChanged { + public class RegisterCollection : INotifyPropertyChanged { /// /// Reference to its bound interface /// - public MewtocolInterface PLCInterface { get; set; } + public MewtocolInterface PLCInterface { get; set; } /// /// Whenever one of its props changes @@ -30,7 +22,7 @@ namespace MewtocolNet.RegisterAttributes /// Triggers a property changed event /// /// Name of the property to trigger for - public void TriggerPropertyChanged (string propertyName = null) { + public void TriggerPropertyChanged(string propertyName = null) { var handler = PropertyChanged; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } @@ -38,11 +30,11 @@ namespace MewtocolNet.RegisterAttributes /// /// Use this on the setter method of a property to enable automatic property register writing /// - public void AutoSetter (object value, ref T privateField, [CallerMemberName] string propName = null) { + public void AutoSetter(object value, ref T privateField, [CallerMemberName] string propName = null) { PLCInterface.PropertyRegisterWasSet(propName, value); - if(value is IRegister reg) { + if (value is IRegister reg) { privateField = (T)reg.Value; return; @@ -57,14 +49,14 @@ namespace MewtocolNet.RegisterAttributes /// Gets called when the register collection base was linked to its parent mewtocol interface /// /// The parent interface - public virtual void OnInterfaceLinked (MewtocolInterface plc) { } + public virtual void OnInterfaceLinked(MewtocolInterface plc) { } /// /// Gets called when the register collection base was linked to its parent mewtocol interface /// and the plc connection is established /// /// The parent interface - public virtual void OnInterfaceLinkedAndOnline (MewtocolInterface plc) { } + public virtual void OnInterfaceLinkedAndOnline(MewtocolInterface plc) { } } diff --git a/MewtocolNet/RegisterBuilding/FinalizerExtensions.cs b/MewtocolNet/RegisterBuilding/FinalizerExtensions.cs new file mode 100644 index 0000000..683232c --- /dev/null +++ b/MewtocolNet/RegisterBuilding/FinalizerExtensions.cs @@ -0,0 +1,78 @@ +using MewtocolNet.Registers; +using System; +using System.Linq; +using System.Reflection; + +namespace MewtocolNet.RegisterBuilding { + + public static class FinalizerExtensions { + + public static IRegister Build(this RegisterBuilderStep step) { + + //if no casting method in builder was called => autocast the type from the RegisterType + if (!step.wasCasted) + step.AutoType(); + + //fallbacks if no casting builder was given + step.GetFallbackDotnetType(); + + var builtReg = new RegisterBuildInfo { + + name = step.Name, + specialAddress = step.SpecialAddress, + memoryAddress = step.MemAddress, + memorySizeBytes = step.MemByteSize, + registerType = step.RegType, + dotnetCastType = step.dotnetVarType, + + }.Build(); + + step.AddToRegisterList(builtReg); + + return builtReg; + + } + + private static void GetFallbackDotnetType(this RegisterBuilderStep step) { + + bool isBoolean = step.RegType.IsBoolean(); + bool isTypeNotDefined = step.plcVarType == null && step.dotnetVarType == null; + + if (isTypeNotDefined && step.RegType == RegisterType.DT) { + + step.dotnetVarType = typeof(short); + + } + if (isTypeNotDefined && step.RegType == RegisterType.DDT) { + + step.dotnetVarType = typeof(int); + + } else if (isTypeNotDefined && isBoolean) { + + step.dotnetVarType = typeof(bool); + + } else if (isTypeNotDefined && step.RegType == RegisterType.DT_BYTE_RANGE) { + + step.dotnetVarType = typeof(string); + + } + + if (step.plcVarType != null) { + + step.dotnetVarType = step.plcVarType.Value.GetDefaultDotnetType(); + + } + + } + + private static void AddToRegisterList(this RegisterBuilderStep step, BaseRegister instance) { + + if (step.forInterface == null) return; + + step.forInterface.AddRegister(instance); + + } + + } + +} diff --git a/MewtocolNet/RegisterBuilding/ParseResult.cs b/MewtocolNet/RegisterBuilding/ParseResult.cs new file mode 100644 index 0000000..b851848 --- /dev/null +++ b/MewtocolNet/RegisterBuilding/ParseResult.cs @@ -0,0 +1,13 @@ +namespace MewtocolNet.RegisterBuilding { + + internal struct ParseResult { + + public ParseResultState state; + + public string hardFailReason; + + public RegisterBuilderStep stepData; + + } + +} diff --git a/MewtocolNet/RegisterBuilding/ParseResultState.cs b/MewtocolNet/RegisterBuilding/ParseResultState.cs new file mode 100644 index 0000000..142ac3c --- /dev/null +++ b/MewtocolNet/RegisterBuilding/ParseResultState.cs @@ -0,0 +1,19 @@ +namespace MewtocolNet.RegisterBuilding { + internal enum ParseResultState { + + /// + /// The parse try failed at the intial regex match + /// + FailedSoft, + /// + /// The parse try failed at the afer- regex match + /// + FailedHard, + /// + /// The parse try did work + /// + Success, + + } + +} diff --git a/MewtocolNet/RegisterBuilding/RegBuilder.cs b/MewtocolNet/RegisterBuilding/RegBuilder.cs new file mode 100644 index 0000000..6994662 --- /dev/null +++ b/MewtocolNet/RegisterBuilding/RegBuilder.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace MewtocolNet.RegisterBuilding { + + /// + /// Contains useful tools for register creation + /// + public class RegBuilder { + + internal MewtocolInterface forInterface = null; + + //methods to test the input string on + private static List> parseMethods = new List>() { + + (x) => TryBuildBoolean(x), + (x) => TryBuildNumericBased(x), + + }; + + public static RegBuilder ForInterface (IPlc interf) { + + var rb = new RegBuilder(); + rb.forInterface = interf as MewtocolInterface; + return rb; + + } + + public static RegBuilder ForInterface(IPlcSerial interf) { + + var rb = new RegBuilder(); + rb.forInterface = interf as MewtocolInterface; + return rb; + + } + + public static RegBuilder Factory { get; private set; } = new RegBuilder(); + + + public RegisterBuilderStep FromPlcRegName (string plcAddrName, string name = null) { + + foreach (var method in parseMethods) { + + var res = method.Invoke(plcAddrName); + + if(res.state == ParseResultState.Success) { + + if (!string.IsNullOrEmpty(name)) + res.stepData.Name = name; + + res.stepData.OriginalInput = plcAddrName; + res.stepData.forInterface = forInterface; + return res.stepData; + + } else if(res.state == ParseResultState.FailedHard) { + + throw new Exception(res.hardFailReason); + + } + + } + + throw new Exception("Wrong input format"); + + } + + //bool registers + private static ParseResult TryBuildBoolean (string plcAddrName) { + + //regex to find special register values + var patternBool = new Regex(@"(?X|Y|R)(?[0-9]{0,3})(?(?:[0-9]|[A-F]){1})?"); + + var match = patternBool.Match(plcAddrName); + + if (!match.Success) + return new ParseResult { + state = ParseResultState.FailedSoft + }; + + string prefix = match.Groups["prefix"].Value; + string area = match.Groups["area"].Value; + string special = match.Groups["special"].Value; + + IOType regType; + int areaAdd = 0; + byte specialAdd = 0x0; + + //try cast the prefix + if(!Enum.TryParse(prefix, out regType)) { + + return new ParseResult { + state = ParseResultState.FailedHard, + hardFailReason = $"Cannot parse '{plcAddrName}', the prefix is not allowed for boolean registers" + }; + + } + + if(!string.IsNullOrEmpty(area) && !int.TryParse(area, out areaAdd) ) { + + return new ParseResult { + state = ParseResultState.FailedHard, + hardFailReason = $"Cannot parse '{plcAddrName}', the area address: '{area}' is wrong" + }; + + } + + //special address not given + if(string.IsNullOrEmpty(special) && !string.IsNullOrEmpty(area)) { + + var isAreaInt = int.TryParse(area, NumberStyles.Number, CultureInfo.InvariantCulture, out var areaInt); + + if (isAreaInt && areaInt >= 0 && areaInt <= 9) { + + //area address is actually meant as special address but 0-9 + specialAdd = (byte)areaInt; + areaAdd = 0; + + + } else if (isAreaInt && areaInt > 9) { + + //area adress is meant to be the actual area address + areaAdd = areaInt; + specialAdd = 0; + + } else { + + return new ParseResult { + state = ParseResultState.FailedHard, + hardFailReason = $"Cannot parse '{plcAddrName}', the special address: '{special}' is wrong 1", + }; + + } + + } else { + + //special address parsed as hex num + if (!byte.TryParse(special, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out specialAdd)) { + + return new ParseResult { + state = ParseResultState.FailedHard, + hardFailReason = $"Cannot parse '{plcAddrName}', the special address: '{special}' is wrong 2", + }; + + } + + } + + return new ParseResult { + state = ParseResultState.Success, + stepData = new RegisterBuilderStep ((RegisterType)(int)regType, areaAdd, specialAdd), + }; + + } + + // one to two word registers + private static ParseResult TryBuildNumericBased (string plcAddrName) { + + var patternByte = new Regex(@"(?DT|DDT)(?[0-9]{1,5})"); + + var match = patternByte.Match(plcAddrName); + + if (!match.Success) + return new ParseResult { + state = ParseResultState.FailedSoft + }; + + + string prefix = match.Groups["prefix"].Value; + string area = match.Groups["area"].Value; + + RegisterType regType; + int areaAdd = 0; + + //try cast the prefix + if (!Enum.TryParse(prefix, out regType)) { + + return new ParseResult { + state = ParseResultState.FailedHard, + hardFailReason = $"Cannot parse '{plcAddrName}', the prefix is not allowed for numeric registers" + }; + + } + + if (!string.IsNullOrEmpty(area) && !int.TryParse(area, out areaAdd)) { + + return new ParseResult { + state = ParseResultState.FailedHard, + hardFailReason = $"Cannot parse '{plcAddrName}', the area address: '{area}' is wrong" + }; + + } + + return new ParseResult { + state = ParseResultState.Success, + stepData = new RegisterBuilderStep(regType, areaAdd), + }; + + } + + } + +} diff --git a/MewtocolNet/RegisterBuilding/RegisterBuildInfo.cs b/MewtocolNet/RegisterBuilding/RegisterBuildInfo.cs new file mode 100644 index 0000000..7e88f4a --- /dev/null +++ b/MewtocolNet/RegisterBuilding/RegisterBuildInfo.cs @@ -0,0 +1,111 @@ +using MewtocolNet.Registers; +using System; +using System.Collections; +using System.Reflection; + +namespace MewtocolNet.RegisterBuilding { + + internal struct RegisterBuildInfo { + + internal string name; + internal int memoryAddress; + internal int memorySizeBytes; + internal byte? specialAddress; + + internal RegisterType? registerType; + internal Type dotnetCastType; + internal Type collectionType; + + internal BaseRegister Build () { + + RegisterType regType = registerType ?? dotnetCastType.ToRegisterTypeDefault(); + + Type registerClassType = dotnetCastType.GetDefaultRegisterHoldingType(); + + bool isBytesRegister = !registerClassType.IsGenericType && registerClassType == typeof(BytesRegister); + bool isStringRegister = !registerClassType.IsGenericType && registerClassType == typeof(StringRegister); + + if (regType.IsNumericDTDDT() && (dotnetCastType == typeof(bool))) { + + //------------------------------------------- + //as numeric register with boolean bit target + //create a new bregister instance + var instance = new BytesRegister(memoryAddress, memorySizeBytes, name); + + if (collectionType != null) + instance.WithCollectionType(collectionType); + + return instance; + + } else if (regType.IsNumericDTDDT() && !isBytesRegister && !isStringRegister) { + + //------------------------------------------- + //as numeric register + + var areaAddr = memoryAddress; + + //create a new bregister instance + var flags = BindingFlags.Public | BindingFlags.Instance; + + //int _adress, Type _enumType = null, string _name = null + var parameters = new object[] { areaAddr, null, name }; + var instance = (BaseRegister)Activator.CreateInstance(registerClassType, flags, null, parameters, null); + + if(collectionType != null) + instance.WithCollectionType(collectionType); + + return instance; + + } + + if(isBytesRegister) { + + //------------------------------------------- + //as byte range register + var instance = new BytesRegister(memoryAddress, memorySizeBytes, name); + + if (collectionType != null) + instance.WithCollectionType(collectionType); + + return instance; + + } + + if (isStringRegister) { + + //------------------------------------------- + //as byte range register + var instance = (BaseRegister)new StringRegister(memoryAddress, name); + + if (collectionType != null) + instance.WithCollectionType(collectionType); + + return instance; + + } + + if (regType.IsBoolean()) { + + //------------------------------------------- + //as boolean register + + var io = (IOType)(int)regType; + var spAddr = specialAddress; + var areaAddr = memoryAddress; + + var instance = new BoolRegister(io, spAddr.Value, areaAddr, name); + + if (collectionType != null) + ((IRegisterInternal)instance).WithCollectionType(collectionType); + + return instance; + + } + + throw new Exception("Failed to build register"); + + } + + } + +} diff --git a/MewtocolNet/RegisterBuilding/RegisterBuilderStep.cs b/MewtocolNet/RegisterBuilding/RegisterBuilderStep.cs new file mode 100644 index 0000000..c334a72 --- /dev/null +++ b/MewtocolNet/RegisterBuilding/RegisterBuilderStep.cs @@ -0,0 +1,114 @@ +using System; + +namespace MewtocolNet.RegisterBuilding { + + public class RegisterBuilderStep { + + internal MewtocolInterface forInterface; + + internal bool wasCasted = false; + + internal string OriginalInput; + + internal string Name; + internal RegisterType RegType; + internal int MemAddress; + internal int MemByteSize; + internal byte? SpecialAddress; + + internal PlcVarType? plcVarType; + internal Type dotnetVarType; + + public RegisterBuilderStep() => throw new NotSupportedException("Cant make a new instance of RegisterBuilderStep, use the builder pattern"); + + internal RegisterBuilderStep(RegisterType regType, int memAddr) { + + RegType = regType; + MemAddress = memAddr; + + } + + internal RegisterBuilderStep(RegisterType regType, int memAddr, byte specialAddr) { + + RegType = regType; + MemAddress = memAddr; + SpecialAddress = specialAddr; + + } + + public RegisterBuilderStep AsPlcType(PlcVarType varType) { + + dotnetVarType = null; + plcVarType = varType; + + wasCasted = true; + + return this; + + } + + public RegisterBuilderStep AsType() { + + if (!typeof(T).IsAllowedPlcCastingType()) { + + throw new NotSupportedException($"The dotnet type {typeof(T)}, is not supported for PLC type casting"); + + } + + dotnetVarType = typeof(T); + plcVarType = null; + + wasCasted = true; + + return this; + + } + + public RegisterBuilderStep AsBytes (int byteLength) { + + if (RegType != RegisterType.DT) { + + throw new NotSupportedException($"Cant use the AsByte converter on a non DT register"); + + } + + MemByteSize = byteLength; + dotnetVarType = typeof(byte[]); + plcVarType = null; + + wasCasted = true; + + return this; + + } + + internal RegisterBuilderStep AutoType() { + + switch (RegType) { + case RegisterType.X: + case RegisterType.Y: + case RegisterType.R: + dotnetVarType = typeof(bool); + break; + case RegisterType.DT: + dotnetVarType = typeof(short); + break; + case RegisterType.DDT: + dotnetVarType = typeof(int); + break; + case RegisterType.DT_BYTE_RANGE: + dotnetVarType = typeof(string); + break; + } + + plcVarType = null; + + wasCasted = true; + + return this; + + } + + } + +} diff --git a/MewtocolNet/Registers/BaseRegister.cs b/MewtocolNet/Registers/BaseRegister.cs new file mode 100644 index 0000000..5a74bad --- /dev/null +++ b/MewtocolNet/Registers/BaseRegister.cs @@ -0,0 +1,124 @@ +using System; +using System.ComponentModel; +using System.Text; +using System.Threading.Tasks; + +namespace MewtocolNet.Registers { + + public abstract class BaseRegister : IRegister, IRegisterInternal, INotifyPropertyChanged { + + /// + /// Gets called whenever the value was changed + /// + public event Action ValueChanged; + + internal MewtocolInterface attachedInterface; + internal object lastValue; + internal Type collectionType; + internal string name; + internal int memoryAddress; + + /// + public MewtocolInterface AttachedInterface => attachedInterface; + + /// + public object Value => lastValue; + + /// + public RegisterType RegisterType { get; protected set; } + + /// + public Type CollectionType => collectionType; + + /// + public string Name => name; + + /// + public string PLCAddressName => GetRegisterPLCName(); + + /// + public int MemoryAddress => memoryAddress; + + #region Trigger update notify + + public event PropertyChangedEventHandler PropertyChanged; + + internal void TriggerChangedEvnt(object changed) => ValueChanged?.Invoke(changed); + + public void TriggerNotifyChange() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value")); + + #endregion + + public virtual void ClearValue() => SetValueFromPLC(null); + + public virtual void SetValueFromPLC(object val) { + + lastValue = val; + TriggerChangedEvnt(this); + TriggerNotifyChange(); + + } + + public void WithCollectionType(Type colType) => collectionType = colType; + + #region Default accessors + + public Type GetCollectionType() => CollectionType; + + public RegisterType GetRegisterType() => RegisterType; + + public virtual string BuildMewtocolQuery() { + + StringBuilder asciistring = new StringBuilder("D"); + asciistring.Append(MemoryAddress.ToString().PadLeft(5, '0')); + asciistring.Append(MemoryAddress.ToString().PadLeft(5, '0')); + return asciistring.ToString(); + + } + + public virtual string GetStartingMemoryArea() => MemoryAddress.ToString(); + + public virtual byte? GetSpecialAddress() => null; + + public virtual string GetValueString() => Value?.ToString() ?? "null"; + + public virtual string GetAsPLC () => Value?.ToString() ?? "null"; + + public virtual string GetRegisterString() => RegisterType.ToString(); + + public virtual string GetCombinedName() => $"{(CollectionType != null ? $"{CollectionType.Name}." : "")}{Name ?? "Unnamed"}"; + + public virtual string GetContainerName() => $"{(CollectionType != null ? $"{CollectionType.Name}" : "")}"; + + public virtual string GetRegisterPLCName() => $"{GetRegisterString()}{MemoryAddress}"; + + #endregion + + #region Read / Write + + public virtual async Task ReadAsync() => throw new NotImplementedException(); + + public virtual async Task WriteAsync(object data) => throw new NotImplementedException(); + + #endregion + + public override string ToString() => $"{GetRegisterPLCName()}{(Name != null ? $" ({Name})" : "")} - Value: {GetValueString()}"; + + public virtual string ToString(bool additional) { + + if (!additional) return this.ToString(); + + StringBuilder sb = new StringBuilder(); + sb.AppendLine($"PLC Naming: {GetRegisterPLCName()}"); + sb.AppendLine($"Name: {Name ?? "Not named"}"); + sb.AppendLine($"Value: {GetValueString()}"); + sb.AppendLine($"Register Type: {RegisterType}"); + sb.AppendLine($"Memory Address: {MemoryAddress}"); + + return sb.ToString(); + + } + + } + +} diff --git a/MewtocolNet/Registers/BoolRegister.cs b/MewtocolNet/Registers/BoolRegister.cs new file mode 100644 index 0000000..cf1af8c --- /dev/null +++ b/MewtocolNet/Registers/BoolRegister.cs @@ -0,0 +1,153 @@ +using System; +using System.ComponentModel; +using System.Text; +using System.Threading.Tasks; + +namespace MewtocolNet.Registers { + + /// + /// Defines a register containing a boolean + /// + public class BoolRegister : BaseRegister { + + internal byte specialAddress; + /// + /// The registers memory adress if not a special register + /// + public byte SpecialAddress => specialAddress; + + /// + /// Creates a new boolean register + /// + /// The io type prefix + /// The special address + /// The area special address + /// The custom name + /// + /// + public BoolRegister(IOType _io, byte _spAddress = 0x0, int _areaAdress = 0, string _name = null) { + + if (_areaAdress < 0) + throw new NotSupportedException("The area address cant be negative"); + + if (_io == IOType.R && _areaAdress >= 512) + throw new NotSupportedException("R area addresses cant be greater than 511"); + + if ((_io == IOType.X || _io == IOType.Y) && _areaAdress >= 110) + throw new NotSupportedException("XY area addresses cant be greater than 110"); + + if (_spAddress > 0xF) + throw new NotSupportedException("Special address cant be greater 15 or 0xF"); + + lastValue = false; + + memoryAddress = _areaAdress; + specialAddress = _spAddress; + name = _name; + + RegisterType = (RegisterType)(int)_io; + + } + + #region Read / Write + + public override void SetValueFromPLC(object val) { + + lastValue = (bool)val; + TriggerChangedEvnt(this); + TriggerNotifyChange(); + + } + + /// + public override async Task ReadAsync() { + + if (!attachedInterface.IsConnected) return null; + + var read = await attachedInterface.ReadRawRegisterAsync(this); + if(read == null) return null; + var parsed = PlcValueParser.Parse(this, read); + + SetValueFromPLC(parsed); + return parsed; + + } + + /// + public override async Task WriteAsync(object data) { + + if (!attachedInterface.IsConnected) return false; + + var res = await attachedInterface.WriteRawRegisterAsync(this, PlcValueParser.Encode(this, (bool)data)); + if (res) SetValueFromPLC(data); + return res; + + } + + #endregion + + /// + public override byte? GetSpecialAddress() => SpecialAddress; + + /// + public override string BuildMewtocolQuery() { + + //(R|X|Y)(area add [3] + special add [1]) + StringBuilder asciistring = new StringBuilder(); + + string prefix = RegisterType.ToString(); + string mem = MemoryAddress.ToString(); + string sp = SpecialAddress.ToString("X1"); + + asciistring.Append(prefix); + asciistring.Append(mem.PadLeft(3, '0')); + asciistring.Append(sp); + + return asciistring.ToString(); + + } + + /// + public override void ClearValue() => SetValueFromPLC(false); + + /// + public override string GetRegisterPLCName() { + + var spAdressEnd = SpecialAddress.ToString("X1"); + + if (MemoryAddress == 0) { + + return $"{GetRegisterString()}{spAdressEnd}"; + + } + + if (MemoryAddress > 0 && SpecialAddress != 0) { + + return $"{GetRegisterString()}{MemoryAddress}{spAdressEnd}"; + + } + + return $"{GetRegisterString()}{MemoryAddress}"; + + } + + /// + public override string ToString(bool additional) { + + if (!additional) return this.ToString(); + + StringBuilder sb = new StringBuilder(); + sb.AppendLine($"PLC Naming: {GetRegisterPLCName()}"); + sb.AppendLine($"Name: {Name ?? "Not named"}"); + sb.AppendLine($"Value: {GetValueString()}"); + sb.AppendLine($"Register Type: {RegisterType}"); + sb.AppendLine($"Memory Address: {MemoryAddress}"); + sb.AppendLine($"Special Address: {SpecialAddress:X1}"); + + return sb.ToString(); + + } + + } + +} diff --git a/MewtocolNet/Registers/BytesRegister.cs b/MewtocolNet/Registers/BytesRegister.cs new file mode 100644 index 0000000..bd2483f --- /dev/null +++ b/MewtocolNet/Registers/BytesRegister.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MewtocolNet.Registers { + + /// + /// Defines a register containing a string + /// + public class BytesRegister : BaseRegister { + + internal int addressLength; + /// + /// The rgisters memory length + /// + public int AddressLength => addressLength; + + internal short ReservedSize { get; set; } + + /// + /// Defines a register containing a string + /// + public BytesRegister(int _address, int _reservedByteSize, string _name = null) { + + if (_address > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); + name = _name; + memoryAddress = _address; + ReservedSize = (short)_reservedByteSize; + + //calc mem length + //because one register is always 1 word (2 bytes) long, if the bytecount is uneven we get the trailing word too + var byteSize = _reservedByteSize; + if (_reservedByteSize % 2 != 0) byteSize++; + + RegisterType = RegisterType.DT_BYTE_RANGE; + addressLength = (byteSize / 2) - 1; + + } + + public override string GetValueString() => Value == null ? "null" : ((byte[])Value).ToHexString("-"); + + /// + public override string BuildMewtocolQuery() { + + StringBuilder asciistring = new StringBuilder("D"); + + asciistring.Append(MemoryAddress.ToString().PadLeft(5, '0')); + asciistring.Append((MemoryAddress + AddressLength).ToString().PadLeft(5, '0')); + + return asciistring.ToString(); + } + + /// + public override void SetValueFromPLC (object val) { + + lastValue = (byte[])val; + + TriggerChangedEvnt(this); + TriggerNotifyChange(); + + } + + /// + public override string GetRegisterString() => "DT"; + + /// + public override void ClearValue() => SetValueFromPLC(null); + + /// + public override async Task ReadAsync() { + + if (!attachedInterface.IsConnected) return null; + + var read = await attachedInterface.ReadRawRegisterAsync(this); + if (read == null) return null; + + var parsed = PlcValueParser.Parse(this, read); + + SetValueFromPLC(parsed); + return parsed; + + } + + /// + public override async Task WriteAsync(object data) { + + if (!attachedInterface.IsConnected) return false; + + var res = await attachedInterface.WriteRawRegisterAsync(this, (byte[])data); + if (res) SetValueFromPLC(data); + return res; + + } + + } + +} diff --git a/MewtocolNet/Registers/Interfaces/IRegister.cs b/MewtocolNet/Registers/Interfaces/IRegister.cs new file mode 100644 index 0000000..18aa8d8 --- /dev/null +++ b/MewtocolNet/Registers/Interfaces/IRegister.cs @@ -0,0 +1,73 @@ +using System; +using System.Text; +using System.Threading.Tasks; + +namespace MewtocolNet { + + /// + /// An interface for all register types + /// + public interface IRegister { + + /// + /// Gets called whenever the value was changed + /// + event Action ValueChanged; + + /// + /// Type of the underlying register + /// + RegisterType RegisterType { get; } + + /// + /// The name of the register + /// + string Name { get; } + + /// + /// Gets the register address name as in the plc + /// + string PLCAddressName { get; } + + /// + /// The current value of the register + /// + object Value { get; } + + /// + /// The plc memory address of the register + /// + int MemoryAddress { get; } + + /// + /// Gets the value of the register as the plc representation string + /// + /// + string GetAsPLC(); + + /// + /// Builds a readable string with all important register informations + /// + string ToString(); + + /// + /// Builds a readable string with all important register informations and additional infos + /// + string ToString(bool detailed); + + /// + /// Reads the register value async from the plc + /// + /// The register value + Task ReadAsync(); + + /// + /// Writes the register content async to the plc + /// + /// True if successfully set + Task WriteAsync(object data); + + + } + +} diff --git a/MewtocolNet/Registers/Interfaces/IRegisterInternal.cs b/MewtocolNet/Registers/Interfaces/IRegisterInternal.cs new file mode 100644 index 0000000..e5b7a3a --- /dev/null +++ b/MewtocolNet/Registers/Interfaces/IRegisterInternal.cs @@ -0,0 +1,65 @@ +using MewtocolNet.Registers; +using System; +using System.Threading.Tasks; + +namespace MewtocolNet { + internal interface IRegisterInternal { + + event Action ValueChanged; + + //props + + MewtocolInterface AttachedInterface { get; } + + RegisterType RegisterType { get; } + + string Name { get; } + + object Value { get; } + + int MemoryAddress { get; } + + // setters + + void WithCollectionType(Type colType); + + void SetValueFromPLC(object value); + + void ClearValue(); + + // Accessors + + Type GetCollectionType(); + + string GetRegisterString(); + + string GetCombinedName(); + + string GetContainerName(); + + string GetRegisterPLCName(); + + byte? GetSpecialAddress(); + + string GetStartingMemoryArea(); + + string GetValueString(); + + string BuildMewtocolQuery(); + + + //others + + void TriggerNotifyChange(); + + Task ReadAsync(); + + Task WriteAsync(object data); + + string ToString(); + + string ToString(bool detailed); + + } + +} diff --git a/MewtocolNet/Registers/NumberRegister.cs b/MewtocolNet/Registers/NumberRegister.cs new file mode 100644 index 0000000..2f8a4ab --- /dev/null +++ b/MewtocolNet/Registers/NumberRegister.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace MewtocolNet.Registers { + + /// + /// Defines a register containing a number + /// + /// The type of the numeric value + public class NumberRegister : BaseRegister { + + internal Type enumType { get; set; } + + /// + /// Defines a register containing a number + /// + /// Memory start adress max 99999 + /// Name of the register + public NumberRegister (int _address, string _name = null) { + + if (_address > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); + + memoryAddress = _address; + name = _name; + + Type numType = typeof(T); + + var allowedTypes = PlcValueParser.GetAllowDotnetTypes(); + if (!allowedTypes.Contains(numType)) + throw new NotSupportedException($"The type {numType} is not allowed for Number Registers"); + + var areaLen = (Marshal.SizeOf(numType) / 2) - 1; + RegisterType = areaLen >= 1 ? RegisterType.DDT : RegisterType.DT; + + lastValue = default(T); + + } + + /// + /// Defines a register containing a number + /// + /// Memory start adress max 99999 + /// Enum type to parse as + /// Name of the register + public NumberRegister(int _address, Type _enumType, string _name = null) { + + if (_address > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); + + memoryAddress = _address; + name = _name; + + Type numType = typeof(T); + + var allowedTypes = PlcValueParser.GetAllowDotnetTypes(); + if (!allowedTypes.Contains(numType)) + throw new NotSupportedException($"The type {numType} is not allowed for Number Registers"); + + var areaLen = (Marshal.SizeOf(numType) / 2) - 1; + RegisterType = areaLen >= 1 ? RegisterType.DDT : RegisterType.DT; + + enumType = _enumType; + lastValue = default(T); + + } + + /// + public override void SetValueFromPLC(object val) { + + lastValue = (T)val; + TriggerChangedEvnt(this); + TriggerNotifyChange(); + + } + + /// + public override string BuildMewtocolQuery() { + + StringBuilder asciistring = new StringBuilder("D"); + asciistring.Append(MemoryAddress.ToString().PadLeft(5, '0')); + + int offsetAddress = 0; + if(RegisterType == RegisterType.DDT) + offsetAddress = 1; + + asciistring.Append((MemoryAddress + offsetAddress).ToString().PadLeft(5, '0')); + return asciistring.ToString(); + + } + + /// + public override string GetAsPLC() => ((TimeSpan)Value).AsPLCTime(); + + /// + public override string GetValueString() { + + if(typeof(T) == typeof(TimeSpan)) { + + return $"{Value} [{((TimeSpan)Value).AsPLCTime()}]"; + + } + + //is number or bitwise + if (enumType == null) { + + return $"{Value}"; + + } + + //is enum + var dict = new Dictionary(); + + foreach (var name in Enum.GetNames(enumType)) { + + int enumKey = (int)Enum.Parse(enumType, name); + if (!dict.ContainsKey(enumKey)) { + dict.Add(enumKey, name); + } + + } + + if (enumType != null && Value is short shortVal) { + + if (dict.ContainsKey(shortVal)) { + + return $"{Value} ({dict[shortVal]})"; + + } else { + + return $"{Value} (Missing Enum)"; + + } + + } + + if (enumType != null && Value is int intVal) { + + if (dict.ContainsKey(intVal)) { + + return $"{Value} ({dict[intVal]})"; + + } else { + + return $"{Value} (Missing Enum)"; + + } + + } + + return Value.ToString(); + + } + + /// + public override void ClearValue() => SetValueFromPLC(default(T)); + + /// + public override async Task ReadAsync() { + + if (!attachedInterface.IsConnected) return null; + + var read = await attachedInterface.ReadRawRegisterAsync(this); + if (read == null) return null; + + var parsed = PlcValueParser.Parse(this, read); + + SetValueFromPLC(parsed); + return parsed; + + } + + /// + public override async Task WriteAsync(object data) { + + if (!attachedInterface.IsConnected) return false; + + var res = await attachedInterface.WriteRawRegisterAsync(this, PlcValueParser.Encode(this, (T)data)); + if (res) SetValueFromPLC(data); + return res; + + } + + /// + /// Gets the register bitwise if its a 16 or 32 bit int + /// + /// A bitarray + public BitArray GetBitwise() { + + if (this is NumberRegister shortReg) { + + var bytes = BitConverter.GetBytes((short)Value); + BitArray bitAr = new BitArray(bytes); + return bitAr; + + } + + if (this is NumberRegister intReg) { + + var bytes = BitConverter.GetBytes((int)Value); + BitArray bitAr = new BitArray(bytes); + return bitAr; + + } + + return null; + + } + + } + +} diff --git a/MewtocolNet/Registers/StringRegister.cs b/MewtocolNet/Registers/StringRegister.cs new file mode 100644 index 0000000..9147049 --- /dev/null +++ b/MewtocolNet/Registers/StringRegister.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace MewtocolNet.Registers { + + /// + /// Defines a register containing a string + /// + public class StringRegister : BaseRegister { + + internal short ReservedSize { get; set; } + + internal short UsedSize { get; set; } + + internal int WordsSize { get; set; } + + private bool isCalibrated = false; + + /// + /// Defines a register containing a string + /// + public StringRegister (int _address, string _name = null) { + + if (_address > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); + name = _name; + memoryAddress = _address; + RegisterType = RegisterType.DT_BYTE_RANGE; + + } + + /// + public override string BuildMewtocolQuery() { + + StringBuilder asciistring = new StringBuilder("D"); + + asciistring.Append(MemoryAddress.ToString().PadLeft(5, '0')); + asciistring.Append((MemoryAddress + WordsSize - 1).ToString().PadLeft(5, '0')); + + return asciistring.ToString(); + } + + /// + public override string GetValueString() => $"'{Value}'"; + + /// + public override void SetValueFromPLC (object val) { + + lastValue = (string)val; + + TriggerChangedEvnt(this); + TriggerNotifyChange(); + + } + + /// + public override string GetRegisterString() => "DT"; + + /// + public override void ClearValue() => SetValueFromPLC(""); + + /// + public override async Task ReadAsync() { + + if (!attachedInterface.IsConnected) return null; + + //get the string params first + + if(!isCalibrated) await Calibrate(); + + var read = await attachedInterface.ReadRawRegisterAsync(this); + if (read == null) return null; + + return PlcValueParser.Parse(this, read); + + } + + private async Task Calibrate () { + + //get the string describer bytes + var bytes = await attachedInterface.ReadByteRange(MemoryAddress, 4, false); + + ReservedSize = BitConverter.ToInt16(bytes, 0); + UsedSize = BitConverter.ToInt16(bytes, 2); + WordsSize = 2 + (ReservedSize + 1) / 2; + + isCalibrated = true; + + } + + /// + public override async Task WriteAsync(object data) { + + if (!attachedInterface.IsConnected) return false; + + var res = await attachedInterface.WriteRawRegisterAsync(this, PlcValueParser.Encode(this, (string)data)); + if (res) { + UsedSize = (short)((string)Value).Length; + SetValueFromPLC(data); + } + + return res; + + } + + } + +} diff --git a/MewtocolNet/TypeConversion/Conversions.cs b/MewtocolNet/TypeConversion/Conversions.cs new file mode 100644 index 0000000..53d62e4 --- /dev/null +++ b/MewtocolNet/TypeConversion/Conversions.cs @@ -0,0 +1,220 @@ +using MewtocolNet.Registers; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace MewtocolNet.TypeConversion { + + internal static class Conversions { + + internal static Dictionary dictPlcTypeToRegisterType = new Dictionary { + + { PlcVarType.BOOL, RegisterType.R }, + { PlcVarType.INT, RegisterType.DT }, + { PlcVarType.UINT, RegisterType.DT }, + { PlcVarType.DINT, RegisterType.DDT }, + { PlcVarType.UDINT, RegisterType.DDT }, + { PlcVarType.REAL, RegisterType.DDT }, + { PlcVarType.TIME, RegisterType.DDT }, + { PlcVarType.WORD, RegisterType.DT }, + { PlcVarType.DWORD, RegisterType.DDT }, + { PlcVarType.STRING, RegisterType.DT_BYTE_RANGE }, + + }; + + /// + /// All conversions for reading dataf from and to the plc + /// + internal static List items = new List { + + new PlcTypeConversion(RegisterType.R) { + HoldingRegisterType = typeof(BoolRegister), + PlcVarType = PlcVarType.BOOL, + FromRaw = (reg, bytes) => { + + return (bool)(bytes[0] == 1); + }, + ToRaw = (reg, value) => { + + return new byte[] { (byte)(value ? 1 : 0) }; + + }, + }, + new PlcTypeConversion(RegisterType.X) { + HoldingRegisterType = typeof(BoolRegister), + PlcVarType = PlcVarType.BOOL, + FromRaw = (reg, bytes) => { + + return bytes[0] == 1; + }, + ToRaw = (reg, value) => { + + return new byte[] { (byte)(value ? 1 : 0) }; + + }, + }, + new PlcTypeConversion(RegisterType.Y) { + HoldingRegisterType = typeof(BoolRegister), + PlcVarType = PlcVarType.BOOL, + FromRaw = (reg, bytes) => { + + return bytes[0] == 1; + }, + ToRaw = (reg, value) => { + + return new byte[] { (byte)(value ? 1 : 0) }; + + }, + }, + new PlcTypeConversion(RegisterType.DT) { + HoldingRegisterType = typeof(NumberRegister), + PlcVarType = PlcVarType.INT, + FromRaw = (reg, bytes) => { + + return BitConverter.ToInt16(bytes, 0); + }, + ToRaw = (reg, value) => { + + return BitConverter.GetBytes(value); + + }, + }, + new PlcTypeConversion(RegisterType.DT) { + HoldingRegisterType = typeof(NumberRegister), + PlcVarType = PlcVarType.UINT, + FromRaw = (reg, bytes) => { + + return BitConverter.ToUInt16(bytes, 0); + }, + ToRaw = (reg, value) => { + + return BitConverter.GetBytes(value); + + }, + }, + new PlcTypeConversion(RegisterType.DDT) { + HoldingRegisterType = typeof(NumberRegister), + PlcVarType = PlcVarType.DINT, + FromRaw = (reg, bytes) => { + + return BitConverter.ToInt32(bytes, 0); + }, + ToRaw = (reg, value) => { + + return BitConverter.GetBytes(value); + + }, + }, + new PlcTypeConversion(RegisterType.DDT) { + HoldingRegisterType = typeof(NumberRegister), + PlcVarType = PlcVarType.UDINT, + FromRaw = (reg, bytes) => { + + return BitConverter.ToUInt32(bytes, 0); + }, + ToRaw = (reg, value) => { + + return BitConverter.GetBytes(value); + + }, + }, + new PlcTypeConversion(RegisterType.DDT) { + HoldingRegisterType = typeof(NumberRegister), + PlcVarType = PlcVarType.REAL, + FromRaw = (reg, bytes) => { + + var val = BitConverter.ToUInt32(bytes, 0); + byte[] floatVals = BitConverter.GetBytes(val); + float finalFloat = BitConverter.ToSingle(floatVals, 0); + + return finalFloat; + + }, + ToRaw = (reg, value) => { + + return BitConverter.GetBytes(value); + + }, + }, + new PlcTypeConversion(RegisterType.DDT) { + HoldingRegisterType = typeof(NumberRegister), + PlcVarType = PlcVarType.TIME, + FromRaw = (reg, bytes) => { + + var vallong = BitConverter.ToUInt32(bytes, 0); + var valMillis = vallong * 10; + var ts = TimeSpan.FromMilliseconds(valMillis); + return ts; + + }, + ToRaw = (reg, value) => { + + var tLong = (uint)(value.TotalMilliseconds / 10); + return BitConverter.GetBytes(tLong); + + }, + }, + new PlcTypeConversion(RegisterType.DT) { + HoldingRegisterType = typeof(BytesRegister), + FromRaw = (reg, bytes) => bytes, + ToRaw = (reg, value) => value, + }, + new PlcTypeConversion(RegisterType.DT_BYTE_RANGE) { + HoldingRegisterType = typeof(StringRegister), + PlcVarType = PlcVarType.STRING, + FromRaw = (reg, bytes) => { + + //get actual showed size + short actualLen = BitConverter.ToInt16(bytes, 2); + + //skip 4 bytes because they only describe the length + return Encoding.UTF8.GetString(bytes.Skip(4).Take(actualLen).ToArray()); + + }, + ToRaw = (reg, value) => { + + var sReg = (StringRegister)reg; + + if(value.Length > sReg.ReservedSize) + value = value.Substring(0, sReg.ReservedSize); + + int padLen = sReg.ReservedSize; + if(sReg.ReservedSize % 2 != 0) padLen++; + + var strBytes = Encoding.UTF8.GetBytes(value.PadRight(padLen, '\0')); + + List finalBytes = new List(); + finalBytes.AddRange(BitConverter.GetBytes(sReg.ReservedSize)); + finalBytes.AddRange(BitConverter.GetBytes((short)value.Length)); + finalBytes.AddRange(strBytes); + + return finalBytes.ToArray(); + + }, + }, + new PlcTypeConversion(RegisterType.DT) { + HoldingRegisterType = typeof(BytesRegister), + PlcVarType = PlcVarType.WORD, + FromRaw = (reg, bytes) => { + + BitArray bitAr = new BitArray(bytes); + return bitAr; + + }, + ToRaw = (reg, value) => { + + byte[] ret = new byte[(value.Length - 1) / 8 + 1]; + value.CopyTo(ret, 0); + return ret; + + }, + }, + + }; + + } + +} diff --git a/MewtocolNet/TypeConversion/IPlcTypeConverter.cs b/MewtocolNet/TypeConversion/IPlcTypeConverter.cs new file mode 100644 index 0000000..cdbffb5 --- /dev/null +++ b/MewtocolNet/TypeConversion/IPlcTypeConverter.cs @@ -0,0 +1,21 @@ +using System; + +namespace MewtocolNet { + + internal interface IPlcTypeConverter { + + object FromRawData(IRegister register, byte[] data); + + byte[] ToRawData(IRegister register, object value); + + Type GetDotnetType(); + + Type GetHoldingRegisterType(); + + RegisterType GetPlcRegisterType(); + + PlcVarType GetPlcVarType(); + + } + +} diff --git a/MewtocolNet/TypeConversion/PlcTypeConversion.cs b/MewtocolNet/TypeConversion/PlcTypeConversion.cs new file mode 100644 index 0000000..b65f961 --- /dev/null +++ b/MewtocolNet/TypeConversion/PlcTypeConversion.cs @@ -0,0 +1,42 @@ +using System; +using System.ComponentModel; + +namespace MewtocolNet { + + internal class PlcTypeConversion : IPlcTypeConverter { + + public Type MainType { get; private set; } + + public RegisterType PlcType { get; private set; } + + public PlcVarType PlcVarType { get; set; } + + public Type HoldingRegisterType { get; set; } + + public Func FromRaw { get; set; } + + public Func ToRaw { get; set; } + + public PlcTypeConversion(RegisterType plcType) { + + MainType = typeof(T); + PlcType = plcType; + + } + + public Type GetDotnetType() => MainType; + + public Type GetHoldingRegisterType() => HoldingRegisterType; + + public RegisterType GetPlcRegisterType() => PlcType; + + public PlcVarType GetPlcVarType() => PlcVarType; + + public object FromRawData(IRegister register, byte[] data) => FromRaw.Invoke(register, data); + + public byte[] ToRawData(IRegister register, object value) => ToRaw.Invoke(register, (T)value); + + + } + +} diff --git a/MewtocolNet/TypeConversion/PlcValueParser.cs b/MewtocolNet/TypeConversion/PlcValueParser.cs new file mode 100644 index 0000000..ab873d8 --- /dev/null +++ b/MewtocolNet/TypeConversion/PlcValueParser.cs @@ -0,0 +1,58 @@ +using MewtocolNet.Exceptions; +using MewtocolNet.Registers; +using MewtocolNet.TypeConversion; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; + +namespace MewtocolNet { + + internal static class PlcValueParser { + + private static List conversions => Conversions.items; + + public static T Parse(IRegister register, byte[] bytes) { + + var converter = conversions.FirstOrDefault(x => x.GetDotnetType() == typeof(T)); + + if (converter == null) + throw new MewtocolException($"A converter for the dotnet type {typeof(T)} doesn't exist"); + + return (T)converter.FromRawData(register, bytes); + + } + + public static byte[] Encode (IRegister register, T value) { + + var converter = conversions.FirstOrDefault(x => x.GetDotnetType() == typeof(T)); + + if (converter == null) + throw new MewtocolException($"A converter for the dotnet type {typeof(T)} doesn't exist"); + + return converter.ToRawData(register, value); + + } + + public static List GetAllowDotnetTypes () => conversions.Select(x => x.GetDotnetType()).ToList(); + + public static List GetAllowRegisterTypes () => conversions.Select(x => x.GetHoldingRegisterType()).ToList(); + + public static RegisterType? GetDefaultRegisterType (Type type) => + conversions.FirstOrDefault(x => x.GetDotnetType() == type)?.GetPlcRegisterType(); + + public static Type GetDefaultRegisterHoldingType (this PlcVarType type) => + conversions.FirstOrDefault(x => x.GetPlcVarType() == type)?.GetHoldingRegisterType(); + + public static Type GetDefaultRegisterHoldingType (this Type type) => + conversions.FirstOrDefault(x => x.GetDotnetType() == type)?.GetHoldingRegisterType(); + + public static Type GetDefaultDotnetType (this PlcVarType type) => + conversions.FirstOrDefault(x => x.GetPlcVarType() == type)?.GetDotnetType(); + + public static PlcVarType? GetDefaultPlcVarType (this Type type) => + conversions.FirstOrDefault(x => x.GetDotnetType() == type)?.GetPlcVarType(); + + } + +} diff --git a/MewtocolNet/TypeConversion/PlcVarTypeConversions.cs b/MewtocolNet/TypeConversion/PlcVarTypeConversions.cs new file mode 100644 index 0000000..2ff6817 --- /dev/null +++ b/MewtocolNet/TypeConversion/PlcVarTypeConversions.cs @@ -0,0 +1,65 @@ +using MewtocolNet.Exceptions; +using MewtocolNet.Registers; +using MewtocolNet.TypeConversion; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace MewtocolNet { + + internal static class PlcVarTypeConversions { + + static List allowedCastingTypes = PlcValueParser.GetAllowDotnetTypes(); + + static List allowedGenericRegisters = PlcValueParser.GetAllowRegisterTypes(); + + internal static bool IsAllowedRegisterGenericType(this IRegister register) { + + return allowedGenericRegisters.Contains(register.GetType()); + + } + + internal static bool IsAllowedPlcCastingType() { + + return allowedCastingTypes.Contains(typeof(T)); + + } + + internal static bool IsAllowedPlcCastingType(this Type type) { + + return allowedCastingTypes.Contains(type); + + } + + internal static RegisterType ToRegisterTypeDefault(this Type type) { + + var found = PlcValueParser.GetDefaultRegisterType(type); + + if (found != null) { + + return found.Value; + + } + + throw new MewtocolException("No default register type found"); + + } + + internal static PlcVarType ToPlcVarType (this Type type) { + + var found = type.GetDefaultPlcVarType().Value; + + if (found != null) { + + return found; + + } + + throw new MewtocolException("No default plcvar type found"); + + } + + } + +} diff --git a/MewtocolTests/AutomatedPropertyRegisters.cs b/MewtocolTests/AutomatedPropertyRegisters.cs index 30e902d..9a30f15 100644 --- a/MewtocolTests/AutomatedPropertyRegisters.cs +++ b/MewtocolTests/AutomatedPropertyRegisters.cs @@ -1,12 +1,9 @@ -using Xunit; - using MewtocolNet; -using MewtocolNet.Registers; -using System.Diagnostics; -using Xunit.Abstractions; -using System.Collections; using MewtocolNet.RegisterAttributes; -using Microsoft.Win32; +using MewtocolNet.Registers; +using System.Collections; +using Xunit; +using Xunit.Abstractions; namespace MewtocolTests { @@ -18,14 +15,15 @@ namespace MewtocolTests { this.output = output; } - public class TestRegisterCollection : RegisterCollectionBase { + public class TestRegisterCollection : RegisterCollection { //corresponds to a R100 boolean register in the PLC - [Register(1000, RegisterType.R)] + //can also be written as R1000 because the last one is a special address + [Register(IOType.R, memoryArea: 85, spAdress: 0)] public bool TestBool1 { get; private set; } //corresponds to a XD input of the PLC - [Register(RegisterType.X, SpecialAddress.D)] + [Register(IOType.X, (byte)0xD)] public bool TestBoolInputXD { get; private set; } //corresponds to a DT1101 - DT1104 string register in the PLC with (STRING[4]) @@ -62,10 +60,10 @@ namespace MewtocolTests { public BitArray TestBitRegister32 { get; private set; } //corresponds to a DT1204 as a 16bit word/int takes the bit at index 9 and writes it back as a boolean - [Register(1204, 9, BitCount.B16)] + [Register(1204, BitCount.B16, 9)] public bool BitValue { get; private set; } - [Register(1204, 5, BitCount.B16)] + [Register(1204, BitCount.B32, 5)] public bool FillTest { get; private set; } //corresponds to a DT7012 - DT7013 as a 32bit time value that gets parsed as a timespan (TIME) @@ -92,8 +90,8 @@ namespace MewtocolTests { public CurrentState TestEnum32 { get; private set; } } - - private void TestBasicGeneration(IRegister reg, string propName, object expectValue, int expectAddr, string expectPlcName) { + + private void TestBasicGeneration(IRegisterInternal reg, string propName, object expectValue, int expectAddr, string expectPlcName) { Assert.NotNull(reg); Assert.Equal(propName, reg.Name); @@ -109,195 +107,106 @@ namespace MewtocolTests { //actual tests [Fact(DisplayName = "Boolean R generation")] - public void BooleanGen () { + public void BooleanGen() { - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); + var interf = Mewtocol.Ethernet("192.168.0.1"); + interf.AddRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestBool1)); //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestBool1), false, 1000, "R1000"); + TestBasicGeneration((IRegisterInternal)register, nameof(TestRegisterCollection.TestBool1), false, 85, "R85"); } [Fact(DisplayName = "Boolean input XD generation")] - public void BooleanInputGen () { + public void BooleanInputGen() { - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); + var interf = Mewtocol.Ethernet("192.168.0.1"); + interf.AddRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestBoolInputXD)); //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestBoolInputXD), false, 0, "XD"); + TestBasicGeneration((IRegisterInternal)register, nameof(TestRegisterCollection.TestBoolInputXD), false, 0, "XD"); } [Fact(DisplayName = "Int16 generation")] - public void Int16Gen () { + public void Int16Gen() { - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); + var interf = Mewtocol.Ethernet("192.168.0.1"); + interf.AddRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestInt16)); //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestInt16), (short)0, 899, "DT899"); + TestBasicGeneration((IRegisterInternal)register, nameof(TestRegisterCollection.TestInt16), (short)0, 899, "DT899"); } [Fact(DisplayName = "UInt16 generation")] - public void UInt16Gen () { + public void UInt16Gen() { - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); + var interf = Mewtocol.Ethernet("192.168.0.1"); + interf.AddRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestUInt16)); //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestUInt16), (ushort)0, 342, "DT342"); + TestBasicGeneration((IRegisterInternal)register, nameof(TestRegisterCollection.TestUInt16), (ushort)0, 342, "DT342"); } [Fact(DisplayName = "Int32 generation")] - public void Int32Gen () { + public void Int32Gen() { - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); + var interf = Mewtocol.Ethernet("192.168.0.1"); + interf.AddRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestInt32)); //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestInt32), (int)0, 7001, "DDT7001"); + TestBasicGeneration((IRegisterInternal)register, nameof(TestRegisterCollection.TestInt32), (int)0, 7001, "DDT7001"); } [Fact(DisplayName = "UInt32 generation")] - public void UInt32Gen () { + public void UInt32Gen() { - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); + var interf = Mewtocol.Ethernet("192.168.0.1"); + interf.AddRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestUInt32)); //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestUInt32), (uint)0, 765, "DDT765"); + TestBasicGeneration((IRegisterInternal)register, nameof(TestRegisterCollection.TestUInt32), (uint)0, 765, "DDT765"); } [Fact(DisplayName = "Float32 generation")] - public void Float32Gen () { + public void Float32Gen() { - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); + var interf = Mewtocol.Ethernet("192.168.0.1"); + interf.AddRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestFloat32)); //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestFloat32), (float)0, 7003, "DDT7003"); - - } - - [Fact(DisplayName = "String generation")] - public void StringGen () { - - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); - - var register = interf.GetRegister(nameof(TestRegisterCollection.TestString2)); - - //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestString2), null!, 7005, "DT7005"); - - Assert.Equal(5, ((SRegister)register).ReservedSize); - Assert.Equal(4, ((SRegister)register).MemoryLength); - - } - - [Fact(DisplayName = "BitArray 16bit generation")] - public void BitArray16Gen () { - - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); - - var register = interf.GetRegister($"Auto_Bitwise_DT7010"); - - //test generic properties - TestBasicGeneration(register, "Auto_Bitwise_DT7010", (short)0, 7010, "DT7010"); - - Assert.True(((NRegister)register).isUsedBitwise); - Assert.Equal(0, ((NRegister)register).MemoryLength); - - } - - [Fact(DisplayName = "BitArray 32bit generation")] - public void BitArray32Gen () { - - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); - - var register = interf.GetRegister($"Auto_Bitwise_DDT8010"); - - //test generic properties - TestBasicGeneration(register, "Auto_Bitwise_DDT8010", (int)0, 8010, "DDT8010"); - - Assert.True(((NRegister)register).isUsedBitwise); - Assert.Equal(1, ((NRegister)register).MemoryLength); - - } - - [Fact(DisplayName = "BitArray single bool generation")] - public void BitArraySingleBool16Gen () { - - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); - - var register = interf.GetRegister($"Auto_Bitwise_DT1204"); - - //test generic properties - TestBasicGeneration(register, "Auto_Bitwise_DT1204", (short)0, 1204, "DT1204"); - - Assert.True(((NRegister)register).isUsedBitwise); + TestBasicGeneration((IRegisterInternal)register, nameof(TestRegisterCollection.TestFloat32), (float)0, 7003, "DDT7003"); } [Fact(DisplayName = "TimeSpan generation")] - public void TimespanGen () { + public void TimespanGen() { - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); + var interf = Mewtocol.Ethernet("192.168.0.1"); + interf.AddRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestTime)); //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestTime), TimeSpan.Zero, 7012, "DDT7012"); - - } - - [Fact(DisplayName = "Enum16 generation")] - public void Enum16Gen () { - - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); - - var register = interf.GetRegister(nameof(TestRegisterCollection.TestEnum16)); - - //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestEnum16), (short)TestRegisterCollection.CurrentState.Undefined, 50, "DT50"); - - } - - [Fact(DisplayName = "Enum32 generation")] - public void Enum32Gen () { - - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); - - var register = interf.GetRegister(nameof(TestRegisterCollection.TestEnum32)); - - //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestEnum32), (int)TestRegisterCollection.CurrentState.Undefined, 51, "DDT51"); + TestBasicGeneration((IRegisterInternal)register, nameof(TestRegisterCollection.TestTime), TimeSpan.Zero, 7012, "DDT7012"); } diff --git a/MewtocolTests/EncapsulatedTests/ExpectedPlcInformationData.cs b/MewtocolTests/EncapsulatedTests/ExpectedPlcInformationData.cs new file mode 100644 index 0000000..0f35705 --- /dev/null +++ b/MewtocolTests/EncapsulatedTests/ExpectedPlcInformationData.cs @@ -0,0 +1,17 @@ +using MewtocolNet; + +namespace MewtocolTests.EncapsulatedTests; + +public class ExpectedPlcInformationData { + + public string PLCName { get; set; } + + public string PLCIP { get; set; } + + public int PLCPort { get; set; } + + public PlcType Type { get; set; } + + public int ProgCapacity { get; set; } + +} diff --git a/MewtocolTests/EncapsulatedTests/RegisterReadWriteTest.cs b/MewtocolTests/EncapsulatedTests/RegisterReadWriteTest.cs new file mode 100644 index 0000000..529cc84 --- /dev/null +++ b/MewtocolTests/EncapsulatedTests/RegisterReadWriteTest.cs @@ -0,0 +1,20 @@ +using MewtocolNet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MewtocolTests.EncapsulatedTests; + +internal class RegisterReadWriteTest { + + public IRegister TargetRegister { get; set; } + + public object IntialValue { get; set; } + + public object AfterWriteValue { get; set; } + + public string RegisterPlcAddressName { get; set; } + +} diff --git a/MewtocolTests/MewtocolTests.csproj b/MewtocolTests/MewtocolTests.csproj index 9d3002f..7c619ff 100644 --- a/MewtocolTests/MewtocolTests.csproj +++ b/MewtocolTests/MewtocolTests.csproj @@ -1,13 +1,16 @@ - net6.0 + + false + false + + net6.0 enable enable - - false + - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/MewtocolTests/TestHelperExtensions.cs b/MewtocolTests/TestHelperExtensions.cs index b39e65e..7c58817 100644 --- a/MewtocolTests/TestHelperExtensions.cs +++ b/MewtocolTests/TestHelperExtensions.cs @@ -1,10 +1,7 @@ -using Xunit; - using MewtocolNet; -using MewtocolNet.Registers; -using Xunit.Abstractions; -using MewtocolNet.Links; using System.Collections; +using Xunit; +using Xunit.Abstractions; namespace MewtocolTests { @@ -12,12 +9,12 @@ namespace MewtocolTests { private readonly ITestOutputHelper output; - public TestHelperExtensions (ITestOutputHelper output) { + public TestHelperExtensions(ITestOutputHelper output) { this.output = output; } [Fact(DisplayName = nameof(MewtocolHelpers.ToBitString))] - public void ToBitStringGeneration () { + public void ToBitStringGeneration() { var bitarr = new BitArray(16); bitarr[2] = true; @@ -31,7 +28,7 @@ namespace MewtocolTests { } [Fact(DisplayName = nameof(MewtocolHelpers.ToHexString))] - public void ToHexStringGeneration () { + public void ToHexStringGeneration() { var bytes = new byte[6] { 0x10, @@ -46,8 +43,8 @@ namespace MewtocolTests { } - [Fact(DisplayName = nameof(MewtocolHelpers.ToHexASCIIBytes))] - public void ToHexASCIIBytesGeneration () { + [Fact(DisplayName = nameof(MewtocolHelpers.BytesFromHexASCIIString))] + public void ToHexASCIIBytesGeneration() { string test = "Hello, world!"; @@ -65,12 +62,12 @@ namespace MewtocolTests { 0x4C, 0x44, 0x21 - }, test.ToHexASCIIBytes()); + }, test.BytesFromHexASCIIString()); } [Fact(DisplayName = nameof(MewtocolHelpers.BuildBCCFrame))] - public void BuildBCCFrameGeneration () { + public void BuildBCCFrameGeneration() { string test = "%01#RCSX0000"; string expect = "%01#RCSX00001D"; @@ -80,7 +77,7 @@ namespace MewtocolTests { } [Fact(DisplayName = nameof(MewtocolHelpers.ParseDTByteString))] - public void ParseDTByteStringGeneration () { + public void ParseDTByteStringGeneration() { var testList = new List() { "1112", @@ -97,7 +94,7 @@ namespace MewtocolTests { } [Fact(DisplayName = nameof(MewtocolHelpers.ParseRCSingleBit))] - public void ParseRCSingleBitGeneration () { + public void ParseRCSingleBitGeneration() { Assert.True($"%01$RC1".BuildBCCFrame().ParseRCSingleBit()); Assert.False($"%01$RC0".BuildBCCFrame().ParseRCSingleBit()); diff --git a/MewtocolTests/TestLinkedLists.cs b/MewtocolTests/TestLinkedLists.cs index cef48f8..0f24484 100644 --- a/MewtocolTests/TestLinkedLists.cs +++ b/MewtocolTests/TestLinkedLists.cs @@ -1,9 +1,6 @@ -using Xunit; - using MewtocolNet; -using MewtocolNet.Registers; +using Xunit; using Xunit.Abstractions; -using MewtocolNet.Links; namespace MewtocolTests { @@ -11,12 +8,12 @@ namespace MewtocolTests { private readonly ITestOutputHelper output; - public TestLinkedLists (ITestOutputHelper output) { + public TestLinkedLists(ITestOutputHelper output) { this.output = output; } [Fact(DisplayName = "Linked error list")] - public void NumericRegisterMewtocolIdentifiers () { + public void NumericRegisterMewtocolIdentifiers() { var expectedData = new Dictionary { @@ -53,7 +50,7 @@ namespace MewtocolTests { }; - Assert.Equal(expectedData, LinkedData.ErrorCodes); + Assert.Equal(expectedData, CodeDescriptions.Error); } diff --git a/MewtocolTests/TestLivePLC.cs b/MewtocolTests/TestLivePLC.cs index eea5f52..1a94956 100644 --- a/MewtocolTests/TestLivePLC.cs +++ b/MewtocolTests/TestLivePLC.cs @@ -1,63 +1,79 @@ using MewtocolNet; using MewtocolNet.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using MewtocolNet.RegisterBuilding; +using MewtocolNet.Registers; +using MewtocolTests.EncapsulatedTests; using Xunit; using Xunit.Abstractions; -namespace MewtocolTests { +namespace MewtocolTests +{ public class TestLivePLC { private readonly ITestOutputHelper output; - private List testData = new() { + private List testPlcInformationData = new() { - new ExpectedTestData { + new ExpectedPlcInformationData { PLCName = "FPX-H C30T", PLCIP = "192.168.115.210", PLCPort = 9094, - Type = CpuType.FP_Sigma_X_H_30K_60K_120K, + Type = PlcType.FPdXH_32k__C30TsP_C40T_C60TsP, ProgCapacity = 32, }, - new ExpectedTestData { + new ExpectedPlcInformationData { PLCName = "FPX-H C14R", PLCIP = "192.168.115.212", PLCPort = 9094, - Type = CpuType.FP_Sigma_X_H_30K_60K_120K, + Type = PlcType.FPdXH_16k__C14R, ProgCapacity = 16, }, }; - public TestLivePLC (ITestOutputHelper output) { + private List testRegisterRW = new() { + + new RegisterReadWriteTest { + TargetRegister = new BoolRegister(IOType.R, 0xA, 10), + RegisterPlcAddressName = "R10A", + IntialValue = false, + AfterWriteValue = true, + }, + new RegisterReadWriteTest { + TargetRegister = new NumberRegister(3000), + RegisterPlcAddressName = "DT3000", + IntialValue = (short)0, + AfterWriteValue = (short)-513, + }, + + }; + + public TestLivePLC(ITestOutputHelper output) { this.output = output; - Logger.LogLevel = LogLevel.Verbose; - Logger.OnNewLogMessage((d,m) => { + Logger.LogLevel = LogLevel.Critical; + Logger.OnNewLogMessage((d, l, m) => { output.WriteLine($"Mewtocol Logger: {d} {m}"); }); - + } [Fact(DisplayName = "Connection cycle client to PLC")] - public async void TestClientConnection () { + public async void TestClientConnection() { - foreach (var plc in testData) { + foreach (var plc in testPlcInformationData) { output.WriteLine($"Testing: {plc.PLCName}"); - var cycleClient = new MewtocolInterface(plc.PLCIP, plc.PLCPort); + var cycleClient = Mewtocol.Ethernet(plc.PLCIP, plc.PLCPort); await cycleClient.ConnectAsync(); @@ -72,13 +88,13 @@ namespace MewtocolTests { } [Fact(DisplayName = "Reading basic information from PLC")] - public async void TestClientReadPLCStatus () { + public async void TestClientReadPLCStatus() { - foreach (var plc in testData) { + foreach (var plc in testPlcInformationData) { output.WriteLine($"Testing: {plc.PLCName}\n"); - var client = new MewtocolInterface(plc.PLCIP, plc.PLCPort); + var client = Mewtocol.Ethernet(plc.PLCIP, plc.PLCPort); await client.ConnectAsync(); @@ -86,8 +102,46 @@ namespace MewtocolTests { Assert.True(client.IsConnected); - Assert.Equal(client.PlcInfo.CpuInformation.Cputype, plc.Type); - Assert.Equal(client.PlcInfo.CpuInformation.ProgramCapacity, plc.ProgCapacity); + Assert.Equal(client.PlcInfo.TypeCode, plc.Type); + Assert.Equal(client.PlcInfo.ProgramCapacity, plc.ProgCapacity); + + client.Disconnect(); + + } + + } + + [Fact(DisplayName = "Reading basic information from PLC")] + public async void TestRegisterReadWriteAsync() { + + foreach (var plc in testPlcInformationData) { + + output.WriteLine($"Testing: {plc.PLCName}\n"); + + var client = Mewtocol.Ethernet(plc.PLCIP, plc.PLCPort); + + foreach (var testRW in testRegisterRW) { + + client.AddRegister(testRW.TargetRegister); + + } + + await client.ConnectAsync(); + Assert.True(client.IsConnected); + + foreach (var testRW in testRegisterRW) { + + var testRegister = client.Registers.First(x => x.PLCAddressName == testRW.RegisterPlcAddressName); + + //test inital val + Assert.Equal(testRW.IntialValue, testRegister.Value); + + await testRegister.WriteAsync(testRW.AfterWriteValue); + + //test after write val + Assert.Equal(testRW.AfterWriteValue, testRegister.Value); + + } client.Disconnect(); @@ -97,18 +151,4 @@ namespace MewtocolTests { } - public class ExpectedTestData { - - public string PLCName { get; set; } - - public string PLCIP { get; set; } - - public int PLCPort { get; set; } - - public CpuType Type { get; set; } - - public int ProgCapacity { get; set; } - - } - } diff --git a/MewtocolTests/TestRegisterBuilder.cs b/MewtocolTests/TestRegisterBuilder.cs new file mode 100644 index 0000000..4be5918 --- /dev/null +++ b/MewtocolTests/TestRegisterBuilder.cs @@ -0,0 +1,206 @@ +using MewtocolNet; +using MewtocolNet.RegisterBuilding; +using MewtocolNet.Registers; +using System.Collections; +using Xunit; +using Xunit.Abstractions; +using static System.Net.Mime.MediaTypeNames; + +namespace MewtocolTests; + +public class TestRegisterBuilder { + + private readonly ITestOutputHelper output; + + public TestRegisterBuilder(ITestOutputHelper output) { + this.output = output; + } + + [Fact(DisplayName = "Parsing as Bool Register List (Phyiscal Outputs)")] + public void TestParsingBRegisterY() { + + var tests = new Dictionary() { + + {"Y0", new BoolRegister(IOType.Y)}, + {"Y1", new BoolRegister(IOType.Y, 0x1)}, + {"Y2", new BoolRegister(IOType.Y, 0x2)}, + {"Y3", new BoolRegister(IOType.Y, 0x3)}, + {"Y4", new BoolRegister(IOType.Y, 0x4)}, + {"Y5", new BoolRegister(IOType.Y, 0x5)}, + {"Y6", new BoolRegister(IOType.Y, 0x6)}, + {"Y7", new BoolRegister(IOType.Y, 0x7)}, + {"Y8", new BoolRegister(IOType.Y, 0x8)}, + {"Y9", new BoolRegister(IOType.Y, 0x9)}, + + {"YA", new BoolRegister(IOType.Y, 0xA)}, + {"YB", new BoolRegister(IOType.Y, 0xB)}, + {"YC", new BoolRegister(IOType.Y, 0xC)}, + {"YD", new BoolRegister(IOType.Y, 0xD)}, + {"YE", new BoolRegister(IOType.Y, 0xE)}, + {"YF", new BoolRegister(IOType.Y, 0xF)}, + + {"Y1A", new BoolRegister(IOType.Y, 0xA, 1)}, + {"Y10B", new BoolRegister(IOType.Y, 0xB, 10)}, + {"Y109C", new BoolRegister(IOType.Y, 0xC, 109)}, + + }; + + TestBoolDict(tests); + + } + + [Fact(DisplayName = "Parsing as Bool Register List (Phyiscal Inputs)")] + public void TestParsingBRegisterX() { + + var tests = new Dictionary() { + + {"X0", new BoolRegister(IOType.X)}, + {"X1", new BoolRegister(IOType.X, 0x1)}, + {"X2", new BoolRegister(IOType.X, 0x2)}, + {"X3", new BoolRegister(IOType.X, 0x3)}, + {"X4", new BoolRegister(IOType.X, 0x4)}, + {"X5", new BoolRegister(IOType.X, 0x5)}, + {"X6", new BoolRegister(IOType.X, 0x6)}, + {"X7", new BoolRegister(IOType.X, 0x7)}, + {"X8", new BoolRegister(IOType.X, 0x8)}, + {"X9", new BoolRegister(IOType.X, 0x9)}, + + {"XA", new BoolRegister(IOType.X, 0xA)}, + {"XB", new BoolRegister(IOType.X, 0xB)}, + {"XC", new BoolRegister(IOType.X, 0xC)}, + {"XD", new BoolRegister(IOType.X, 0xD)}, + {"XE", new BoolRegister(IOType.X, 0xE)}, + {"XF", new BoolRegister(IOType.X, 0xF)}, + + {"X1A", new BoolRegister(IOType.X, 0xA, 1)}, + {"X10B", new BoolRegister(IOType.X, 0xB, 10)}, + {"X109C", new BoolRegister(IOType.X, 0xC, 109)}, + + }; + + TestBoolDict(tests); + + } + + [Fact(DisplayName = "Parsing as Bool Register List (Internal Relay)")] + public void TestParsingBRegisterR() { + + var tests = new Dictionary() { + + {"R0", new BoolRegister(IOType.R)}, + {"R1", new BoolRegister(IOType.R, 0x1)}, + {"R2", new BoolRegister(IOType.R, 0x2)}, + {"R3", new BoolRegister(IOType.R, 0x3)}, + {"R4", new BoolRegister(IOType.R, 0x4)}, + {"R5", new BoolRegister(IOType.R, 0x5)}, + {"R6", new BoolRegister(IOType.R, 0x6)}, + {"R7", new BoolRegister(IOType.R, 0x7)}, + {"R8", new BoolRegister(IOType.R, 0x8)}, + {"R9", new BoolRegister(IOType.R, 0x9)}, + + {"RA", new BoolRegister(IOType.R, 0xA)}, + {"RB", new BoolRegister(IOType.R, 0xB)}, + {"RC", new BoolRegister(IOType.R, 0xC)}, + {"RD", new BoolRegister(IOType.R, 0xD)}, + {"RE", new BoolRegister(IOType.R, 0xE)}, + {"RF", new BoolRegister(IOType.R, 0xF)}, + + {"R1A", new BoolRegister(IOType.R, 0xA, 1)}, + {"R10B", new BoolRegister(IOType.R, 0xB, 10)}, + {"R109C", new BoolRegister(IOType.R, 0xC, 109)}, + {"R1000", new BoolRegister(IOType.R, 0x0, 100)}, + {"R511", new BoolRegister(IOType.R, 0x0, 511)}, + {"R511A", new BoolRegister(IOType.R, 0xA, 511)}, + + }; + + TestBoolDict(tests); + + } + + private void TestBoolDict (Dictionary dict) { + + foreach (var item in dict) { + + output.WriteLine($"Expected: {item.Key}"); + + var built = RegBuilder.Factory.FromPlcRegName(item.Key).AsPlcType(PlcVarType.BOOL).Build(); + + output.WriteLine($"{(built?.ToString(true) ?? "null")}\n"); + Assert.Equivalent(item.Value, built); + + } + + } + + [Fact(DisplayName = "Parsing as Bool Register (Casted)")] + public void TestRegisterBuildingBoolCasted () { + + var expect = new BoolRegister(IOType.R, 0x1, 0); + var expect2 = new BoolRegister(IOType.Y, 0xA, 103); + + Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("R1").AsPlcType(PlcVarType.BOOL).Build()); + Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("R1").AsType().Build()); + + Assert.Equivalent(expect2, RegBuilder.Factory.FromPlcRegName("Y103A").AsPlcType(PlcVarType.BOOL).Build()); + Assert.Equivalent(expect2, RegBuilder.Factory.FromPlcRegName("Y103A").AsType().Build()); + + } + + [Fact(DisplayName = "Parsing as Bool Register (Auto)")] + public void TestRegisterBuildingBoolAuto () { + + var expect = new BoolRegister(IOType.R, 0x1, 0); + var expect2 = new BoolRegister(IOType.Y, 0xA, 103); + + Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("R1").Build()); + Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("R1").Build()); + + Assert.Equivalent(expect2, RegBuilder.Factory.FromPlcRegName("Y103A").Build()); + Assert.Equivalent(expect2, RegBuilder.Factory.FromPlcRegName("Y103A").Build()); + + } + + [Fact(DisplayName = "Parsing as Number Register (Casted)")] + public void TestRegisterBuildingNumericCasted() { + + var expect = new NumberRegister(303, null); + var expect2 = new NumberRegister(10002, null); + var expect3 = new NumberRegister(400, null); + //var expect4 = new NRegister(103, null, true); + + Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("DT303").AsPlcType(PlcVarType.INT).Build()); + Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("DT303").AsType().Build()); + + Assert.Equivalent(expect2, RegBuilder.Factory.FromPlcRegName("DDT10002").AsPlcType(PlcVarType.DINT).Build()); + Assert.Equivalent(expect2, RegBuilder.Factory.FromPlcRegName("DDT10002").AsType().Build()); + + Assert.Equivalent(expect3, RegBuilder.Factory.FromPlcRegName("DDT400").AsPlcType(PlcVarType.TIME).Build()); + Assert.Equivalent(expect3, RegBuilder.Factory.FromPlcRegName("DDT400").AsType().Build()); + + //Assert.Equivalent(expect4, RegBuilder.FromPlcRegName("DT103").AsType().Build()); + + } + + [Fact(DisplayName = "Parsing as Number Register (Auto)")] + public void TestRegisterBuildingNumericAuto() { + + var expect = new NumberRegister(303, null); + var expect2 = new NumberRegister(10002, null); + + Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("DT303").Build()); + Assert.Equivalent(expect2, RegBuilder.Factory.FromPlcRegName("DDT10002").Build()); + + } + + [Fact(DisplayName = "Parsing as Bytes Register (Casted)")] + public void TestRegisterBuildingByteRangeCasted() { + + var expect = new BytesRegister(303, 5); + + Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("DT303").AsBytes(5).Build()); + + + } + +} diff --git a/MewtocolTests/TestRegisterInterface.cs b/MewtocolTests/TestRegisterInterface.cs index f3acba8..b2b8b50 100644 --- a/MewtocolTests/TestRegisterInterface.cs +++ b/MewtocolTests/TestRegisterInterface.cs @@ -1,7 +1,6 @@ -using Xunit; - using MewtocolNet; using MewtocolNet.Registers; +using Xunit; using Xunit.Abstractions; namespace MewtocolTests { @@ -10,36 +9,40 @@ namespace MewtocolTests { private readonly ITestOutputHelper output; - public TestRegisterInterface (ITestOutputHelper output) { + public TestRegisterInterface(ITestOutputHelper output) { this.output = output; } [Fact(DisplayName = "Numeric mewtocol query building")] - public void NumericRegisterMewtocolIdentifiers () { + public void NumericRegisterMewtocolIdentifiers() { - List registers = new List { - new NRegister(50, _name: null), - new NRegister(50, _name: null), - new NRegister(50, _name : null), - new NRegister(50, _name : null), - new NRegister(50, _name : null), - new NRegister(50, _name : null), + List registers = new List { + new NumberRegister(50), + new NumberRegister(50), + new NumberRegister(50), + new NumberRegister(50), + new NumberRegister(50), + new NumberRegister(50), + new BytesRegister(50, 30), + new BytesRegister(50, 31), }; - List expcectedIdents = new List { + List expectedIdents = new List { "D0005000050", //single word register "D0005000050", //single word register "D0005000051", //double word register "D0005000051", //double word register "D0005000051", //double word register "D0005000051", //double word register + "D0005000064", //variable len register even bytes + "D0005000065", //variable len register odd bytes }; //test mewtocol idents for (int i = 0; i < registers.Count; i++) { - IRegister? reg = registers[i]; - string expect = expcectedIdents[i]; + IRegisterInternal? reg = registers[i]; + string expect = expectedIdents[i]; Assert.Equal(expect, reg.BuildMewtocolQuery()); @@ -48,41 +51,56 @@ namespace MewtocolTests { } [Fact(DisplayName = "PLC register naming convention test")] - public void PLCRegisterIdentifiers () { + public void PLCRegisterIdentifiers() { - List registers = new List { + List registers = new List { //numeric ones - new NRegister(50, _name: null), - new NRegister(60, _name : null), - new NRegister(70, _name : null), - new NRegister(80, _name : null), - new NRegister(90, _name : null), - new NRegister(100, _name : null), + new NumberRegister(50, _name: null), + new NumberRegister(60, _name : null), + new NumberRegister(70, _name : null), + new NumberRegister(80, _name : null), + new NumberRegister(90, _name : null), + new NumberRegister(100, _name : null), + //boolean - new BRegister(100), - new BRegister(5, RegisterType.X), - new BRegister(SpecialAddress.A, RegisterType.X), + new BoolRegister(IOType.R, 0, 100), + new BoolRegister(IOType.R, 0, 0), + new BoolRegister(IOType.X, 5), + new BoolRegister(IOType.X, 0xA), + new BoolRegister(IOType.X, 0xF, 109), + new BoolRegister(IOType.Y, 0xC, 75), + //string - new SRegister(999, 5), + new BytesRegister(999, 5), }; List expcectedIdents = new List { + + //numeric ones "DT50", "DT60", "DDT70", - "DDT80", - "DDT90", + "DDT80", + "DDT90", "DDT100", + + //boolean "R100", + "R0", "X5", "XA", + "X109F", + "Y75C", + + //string "DT999" + }; //test mewtocol idents for (int i = 0; i < registers.Count; i++) { - IRegister? reg = registers[i]; + IRegisterInternal? reg = registers[i]; string expect = expcectedIdents[i]; Assert.Equal(expect, reg.GetRegisterPLCName()); @@ -92,11 +110,11 @@ namespace MewtocolTests { } [Fact(DisplayName = "Non allowed (Overflow address)")] - public void OverFlowRegisterAddress () { + public void OverFlowRegisterAddress() { var ex = Assert.Throws(() => { - new NRegister(100000, _name: null); + new NumberRegister(100000, _name: null); }); @@ -104,7 +122,7 @@ namespace MewtocolTests { var ex1 = Assert.Throws(() => { - new BRegister(100000); + new BoolRegister(IOType.R, _areaAdress: 512); }); @@ -112,46 +130,28 @@ namespace MewtocolTests { var ex2 = Assert.Throws(() => { - new SRegister(100000, 5); + new BoolRegister(IOType.X, _areaAdress: 110); }); output.WriteLine(ex2.Message.ToString()); + var ex3 = Assert.Throws(() => { + + new BytesRegister(100000, 5); + + }); + + output.WriteLine(ex3.Message.ToString()); + } [Fact(DisplayName = "Non allowed (Wrong data type)")] - public void WrongDataTypeRegister () { + public void WrongDataTypeRegister() { var ex = Assert.Throws(() => { - new NRegister(100, _name: null); - - }); - - output.WriteLine(ex.Message.ToString()); - - } - - [Fact(DisplayName = "Non allowed (Wrong bool type address)")] - public void WrongDataTypeRegisterBool1 () { - - var ex = Assert.Throws(() => { - - new BRegister(100, RegisterType.DDT_int); - - }); - - output.WriteLine(ex.Message.ToString()); - - } - - [Fact(DisplayName = "Non allowed (Wrong bool special address)")] - public void WrongDataTypeRegisterBool2 () { - - var ex = Assert.Throws(() => { - - new BRegister(SpecialAddress.None, RegisterType.X); + new NumberRegister(100, _name: null); }); diff --git a/PLC_Test/test_c30_fpx_h.ini b/PLC_Test/test_c30_fpx_h.ini index e810e50..90aaa53 100644 Binary files a/PLC_Test/test_c30_fpx_h.ini and b/PLC_Test/test_c30_fpx_h.ini differ diff --git a/PLC_Test/test_c30_fpx_h.pro b/PLC_Test/test_c30_fpx_h.pro index fb08732..098bb1c 100644 Binary files a/PLC_Test/test_c30_fpx_h.pro and b/PLC_Test/test_c30_fpx_h.pro differ diff --git a/PLC_Test/test_c30_fpx_h.xml b/PLC_Test/test_c30_fpx_h.xml index 4109899..8929a5a 100644 --- a/PLC_Test/test_c30_fpx_h.xml +++ b/PLC_Test/test_c30_fpx_h.xml @@ -2,10 +2,20 @@ - + - + + + + + + + + + + + diff --git a/build_order.md b/build_order.md new file mode 100644 index 0000000..9cbbabe --- /dev/null +++ b/build_order.md @@ -0,0 +1,15 @@ +# On commit pipeline + +## 1. Run the tests + +`dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=../Builds/TestResults/coverage.opencover.xml` + +## 2. Run the docs Autobuilder + +`dotnet run --project "./DocBuilder/DocBuilder.csproj"` + +# On publish pipeline + +## 3. Publish + +`dotnet publish -c:Release --property WarningLevel=0` \ No newline at end of file diff --git a/formatting_settings b/formatting_settings new file mode 100644 index 0000000..e140ae8 --- /dev/null +++ b/formatting_settings @@ -0,0 +1,229 @@ +# Entfernen Sie die folgende Zeile, wenn Sie EDITORCONFIG-Einstellungen von höheren Verzeichnissen vererben möchten. +root = true + +# C#-Dateien +[*.cs] + +#### Wichtige EditorConfig-Optionen #### + +# Einzüge und Abstände +indent_size = 4 +indent_style = space +tab_width = 4 + +# Einstellungen für neue Zeilen +end_of_line = crlf +insert_final_newline = false + +#### .NET-Codierungskonventionen #### + +# Using-Direktiven organisieren +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this.- und Me.-Einstellungen +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Einstellungen für Sprachschlüsselwörter und BCL-Typen +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Einstellungen für Klammern +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Einstellungen für Modifizierer +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Einstellungen für Ausdrucksebene +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Einstellungen für Felder +dotnet_style_readonly_field = true + +# Einstellungen für Parameter +dotnet_code_quality_unused_parameters = all + +# Unterdrückungseinstellungen +dotnet_remove_unnecessary_suppression_exclusions = none + +# Einstellungen für neue Zeilen +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C#-Codierungskonventionen #### + +# Var-Einstellungen +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Ausdruckskörpermember +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = false +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# Einstellungen für den Musterabgleich +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Einstellungen für NULL-Überprüfung +csharp_style_conditional_delegate_call = true + +# Einstellungen für Modifizierer +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Einstellungen für Codeblöcke +csharp_prefer_braces = true +csharp_prefer_simple_using_statement = true +csharp_style_namespace_declarations = file_scoped +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_top_level_statements = true + +# Einstellungen für Ausdrucksebene +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# Einstellungen für using-Anweisungen +csharp_using_directive_placement = outside_namespace + +# Einstellungen für neue Zeilen +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C#-Formatierungsregeln #### + +# Einstellungen für neue Zeilen +csharp_new_line_before_catch = false +csharp_new_line_before_else = false +csharp_new_line_before_finally = false +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = none +csharp_new_line_between_query_expression_clauses = true + +# Einstellungen für Einrückung +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = false +csharp_indent_case_contents_when_block = false +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Einstellungen für Abstände +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Umbrucheinstellungen +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Benennungsstile #### + +# Benennungsregeln + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbolspezifikationen + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Benennungsstile + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case