mirror of
https://github.com/OpenLogics/MewtocolNet.git
synced 2025-12-06 11:11:23 +00:00
Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18d5118b43 | ||
|
|
48b04dfb7f | ||
|
|
9ca31fe748 | ||
|
|
3bc790238e | ||
|
|
df0de8830e | ||
|
|
19db0a9daa | ||
|
|
6dd2a1688a | ||
|
|
8bd1ce6b4d | ||
|
|
79d6f7fa54 | ||
|
|
a58e60d4f9 | ||
|
|
c6bddc61fb | ||
|
|
300d84daf2 | ||
|
|
78b12afbc3 | ||
|
|
30fec39731 | ||
|
|
372043f18e | ||
|
|
5e8baaddfd | ||
|
|
97b1a665d7 | ||
|
|
2afcea2767 | ||
|
|
5d4ef322ee | ||
|
|
590a3a3723 | ||
|
|
19944f9fd9 | ||
|
|
993c3cba7e | ||
|
|
47e50078b0 | ||
|
|
42d6ee607d | ||
|
|
bd907b80df | ||
|
|
b17fcfeff9 | ||
|
|
b3909e3f4b | ||
|
|
40016a5fa5 | ||
|
|
4551d24128 | ||
|
|
aed1690a7c | ||
|
|
806b45beef | ||
|
|
5b4a9966b1 | ||
|
|
28752f4396 | ||
|
|
8e5051ed83 | ||
|
|
938918e4df | ||
|
|
642aed666c | ||
|
|
a8ee01dfd7 | ||
|
|
a0e54bf8e1 | ||
|
|
eca2798859 | ||
|
|
7677ff5015 | ||
|
|
aff5ca9fda | ||
|
|
5ff29e3ffc | ||
|
|
dba8de159b | ||
|
|
5221664071 | ||
|
|
81676ec2e5 | ||
|
|
7f3db93f94 | ||
|
|
0668ad2f86 | ||
|
|
354c4d6428 | ||
|
|
a00f56074f | ||
|
|
4fb9910d54 | ||
|
|
111eacb785 | ||
|
|
a06a69be3f | ||
|
|
5ed2580bb6 | ||
|
|
c41b0cef16 | ||
|
|
ec78fad881 | ||
|
|
82d9aa8b16 | ||
|
|
74cb3bd59f | ||
|
|
b62e0b06e6 | ||
|
|
e5020253c9 | ||
|
|
aa8441fa33 | ||
|
|
b64a82d5bb | ||
|
|
3f3e3abe15 | ||
|
|
f4af3dd05e | ||
|
|
d695108fd4 | ||
|
|
38b83affd7 | ||
|
|
1f33ebb8d3 | ||
|
|
4d6eee5585 | ||
|
|
d24201b5d9 | ||
|
|
bf78156a9d | ||
|
|
eec1479406 | ||
|
|
fa2762c082 | ||
|
|
9d3e895c3b | ||
|
|
c1ce66c253 | ||
|
|
a399e0fa16 | ||
|
|
9bcffad77b | ||
|
|
3a7b787949 | ||
|
|
8fdff367f6 | ||
|
|
eb70dac5a8 | ||
|
|
d6c00097bc | ||
|
|
4666d3071b | ||
|
|
32c20e7360 | ||
|
|
daecd73a6d | ||
|
|
82821e773d | ||
|
|
6c7e91f648 | ||
|
|
fbd53c850f | ||
|
|
d8c18bbf34 | ||
|
|
88cdc1a36c | ||
|
|
864389a4ad | ||
|
|
5d6cef91ec | ||
|
|
2615f8c428 | ||
|
|
03341d3663 | ||
|
|
6c324bcdff | ||
|
|
befadb67a5 | ||
|
|
6cb45ac27d | ||
|
|
fcb62960ec | ||
|
|
fd85a87f5d | ||
|
|
bd6a6cf177 | ||
|
|
638b173847 | ||
|
|
aa282fe156 | ||
|
|
fe3c849934 | ||
|
|
953e22503a | ||
|
|
d956525538 | ||
|
|
616d102dea | ||
|
|
6d3b5adf7d | ||
|
|
a0da9e77fe | ||
|
|
ab7f5ca302 | ||
|
|
f338acfe8a | ||
|
|
8c2ba1f68f | ||
|
|
c332cd9f86 | ||
|
|
15cc2e245d | ||
|
|
bc765b870a | ||
|
|
a9bd2962b4 | ||
|
|
7be52efb7e | ||
|
|
b48f86d23d | ||
|
|
f5f1e3bea9 | ||
|
|
88ad175145 | ||
|
|
ebee8a0623 | ||
|
|
50a11a3497 | ||
|
|
2f9c86d1a4 | ||
|
|
62bb1b422a | ||
|
|
431c5895f0 | ||
|
|
ebde5421ab | ||
|
|
c1678ae6b4 | ||
|
|
e2f53f9c4a | ||
|
|
844fcfc76d | ||
|
|
111482ec2d | ||
|
|
b72771c13e | ||
|
|
caa138c130 | ||
|
|
e3b6fdf780 | ||
|
|
5dfa48a89a | ||
|
|
128b9f9705 | ||
|
|
69d728f345 | ||
|
|
c01204ce57 | ||
|
|
da2cb6b58a | ||
|
|
96827f3baf | ||
|
|
2df944da8c | ||
|
|
f5e6d7d6af | ||
|
|
ff3cc41aea | ||
|
|
d5b701186e | ||
|
|
0702ab7d0a | ||
|
|
5adadbfc46 | ||
|
|
739f83bf0c | ||
|
|
a9bd792af1 | ||
|
|
e6ffc92db6 |
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
159
.github/workflows/publish-pipeline.yml
vendored
159
.github/workflows/publish-pipeline.yml
vendored
@@ -2,8 +2,18 @@ name: Publish pipeline
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
inputs:
|
||||
build_pre_release:
|
||||
description: 'Mark as pre-release'
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
version_tag:
|
||||
description: 'The version number formatted as X.X.X'
|
||||
default: '0.1.0'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
|
||||
permissions: write-all
|
||||
jobs:
|
||||
@@ -17,19 +27,108 @@ jobs:
|
||||
publish-package:
|
||||
name: 'Build and publish package'
|
||||
needs: test-pipeline
|
||||
runs-on: [self-hosted, linux, x64, womed-local-linux]
|
||||
runs-on: [self-hosted, linux, ARM64]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Parse version tag
|
||||
- name: 'RenameVersionTag'
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION=${{ github.ref_name }}
|
||||
echo "VERSION=${VERSION:1}" >> $GITHUB_ENV
|
||||
PRE_REL_STR='-pre'
|
||||
EMPTY_STR=
|
||||
if ${{ github.event.inputs.build_pre_release }}
|
||||
then
|
||||
echo "prerelease_append=$PRE_REL_STR" >> $GITHUB_ENV
|
||||
else
|
||||
echo "prerelease_append=$EMPTY_STR" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Set .csproj version to ${{ env.VERSION }}
|
||||
- name: Set .csproj version to ${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}
|
||||
run: |
|
||||
sed -i 's/<Version>[0-9].[0-9].[0-9]<\/Version>/<Version>${{ env.VERSION }}<\/Version>/g' MewtocolNet/MewtocolNet.csproj
|
||||
sed -i 's/<Version>[0-9].[0-9].[0-9]<\/Version>/<Version>${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}<\/Version>/g' MewtocolNet/MewtocolNet.csproj
|
||||
less MewtocolNet/MewtocolNet.csproj
|
||||
|
||||
- name: 'Bump version and push tag v${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}'
|
||||
id: tag_version
|
||||
uses: mathieudutour/github-tag-action@v6.1
|
||||
with:
|
||||
tag_prefix: v
|
||||
custom_tag: ${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 'Build Changelog'
|
||||
uses: mikepenz/release-changelog-builder-action@v3
|
||||
id: github_release_log
|
||||
with:
|
||||
commitMode: true
|
||||
configurationJson: |
|
||||
{
|
||||
"template": "#{{CHANGELOG}}\n\n<details>\n<summary>Uncategorized</summary>\n\n#{{UNCATEGORIZED}}\n</details>",
|
||||
"pr_template": "- #{{TITLE}} #{{MERGE_SHA}}",
|
||||
"trim_values" : true,
|
||||
"ignore_labels": [ "ignore" ],
|
||||
"categories": [
|
||||
{
|
||||
"title": "## 🚀 Added Features",
|
||||
"labels": ["feature", "features", "add", "added", "implemented", "impl", "new"]
|
||||
},
|
||||
{
|
||||
"title": "## 🔃 Changed Features",
|
||||
"labels": ["change", "changed"]
|
||||
},
|
||||
{
|
||||
"title": "## ❌ Removed Features",
|
||||
"labels": ["remove", "removed"]
|
||||
},
|
||||
{
|
||||
"title": "## 🪲 Fixes",
|
||||
"labels": ["fix", "fixed", "fixes"]
|
||||
},
|
||||
{
|
||||
"title": "## 📖 Documentation",
|
||||
"labels": ["doc", "docs", "documentation"]
|
||||
},
|
||||
{
|
||||
"title": "## 🧪 Tests",
|
||||
"labels": ["test", "tests", "unittests"]
|
||||
},
|
||||
{
|
||||
"title": "## 📦 Dependencies",
|
||||
"labels": ["dependencies", "package", "nuget"]
|
||||
},
|
||||
{
|
||||
"title": "## 🏠 Householding",
|
||||
"labels": ["upgraded", "upgrade", "update", "clear", "delete", "deleted", "version"]
|
||||
},
|
||||
{
|
||||
"title": "## 🌎 Localization",
|
||||
"labels": ["lang", "language", "localization", "locale", "translation", "translations"]
|
||||
},
|
||||
{
|
||||
"title": "## 💬 Other",
|
||||
"labels": ["other"]
|
||||
}
|
||||
],
|
||||
"duplicate_filter": {
|
||||
"pattern": ".*",
|
||||
"on_property": "title",
|
||||
"method": "match"
|
||||
},
|
||||
"transformers": [
|
||||
{
|
||||
"pattern": "^.*?\\s*\\(?(\\w+)\\)? (.*)",
|
||||
"target": "- $1 $2"
|
||||
}
|
||||
],
|
||||
"label_extractor": [
|
||||
{
|
||||
"pattern": "^.*?\\s*\\(?(\\w{3,})\\)? (.*)",
|
||||
"target": "$1",
|
||||
"flags": "gis"
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v2
|
||||
@@ -37,27 +136,43 @@ jobs:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
run: dotnet restore ./MewtocolNet/MewtocolNet.csproj
|
||||
|
||||
- name: Build as ${{ env.VERSION }}
|
||||
run: dotnet build "MewtocolNet" --no-incremental
|
||||
- name: Build as ${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}
|
||||
run: dotnet build "MewtocolNet" --no-incremental -c:Release
|
||||
|
||||
- name: Pack as ${{ env.VERSION }}
|
||||
run: dotnet pack "MewtocolNet"
|
||||
- name: Pack as ${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}
|
||||
run: dotnet pack "MewtocolNet" -c:Release
|
||||
|
||||
- name: Publish as ${{ env.VERSION }}
|
||||
- name: Publish as ${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}
|
||||
run: |
|
||||
cd '${{ github.workspace }}/Builds'
|
||||
cd '${{ github.workspace }}/Builds/MewtocolNet'
|
||||
ls -l
|
||||
dotnet nuget push "*.nupkg" --skip-duplicate --api-key ${{ secrets.GITHUB_TOKEN }} --source "https://nuget.pkg.github.com/WOmed/index.json"
|
||||
dotnet nuget push "*.nupkg" --skip-duplicate --api-key ${{ secrets.GITHUB_TOKEN }} --source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json"
|
||||
dotnet nuget push "*.nupkg" --skip-duplicate --api-key ${{ secrets.NUGET_TOKEN }} --source "https://api.nuget.org/v3/index.json"
|
||||
|
||||
- name: 'Upload artifacts to latest release'
|
||||
- name: 'Create Release v${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}'
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: v${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}
|
||||
release_name: v${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}
|
||||
body: |
|
||||
## Changelog
|
||||
${{ steps.github_release_log.outputs.changelog }}
|
||||
|
||||
**Total commits:** ${{ steps.github_release_log.outputs.commits }}
|
||||
draft: false
|
||||
prerelease: ${{ github.event.inputs.build_pre_release }}
|
||||
|
||||
- name: 'Upload package to latest release'
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: "${{ github.event.release.upload_url }}"
|
||||
asset_path: ${{ github.workspace }}/Builds/Mewtocol.NET.${{ env.VERSION }}.nupkg
|
||||
asset_name: Mewtocol.NET.${{ env.VERSION }}.nupkg
|
||||
upload_url: "${{ steps.create_release.outputs.upload_url }}"
|
||||
asset_path: ${{ github.workspace }}/Builds/MewtocolNet/Mewtocol.NET.${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}.nupkg
|
||||
asset_name: Mewtocol.NET.${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}.nupkg
|
||||
asset_content_type: application/zip
|
||||
|
||||
119
.github/workflows/test-pipeline.yml
vendored
119
.github/workflows/test-pipeline.yml
vendored
@@ -3,48 +3,109 @@ name: Test pipeline
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
inputs:
|
||||
cache-id:
|
||||
default: 'test-results'
|
||||
required: false
|
||||
type: string
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
branches-ignore:
|
||||
- badges
|
||||
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
|
||||
runs-on: [self-hosted, linux, x64, womed-local-linux]
|
||||
name: 'Run tests and documentation'
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
|
||||
- name: 'Setup dotnet'
|
||||
uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
dotnet-version: |
|
||||
6.0.x
|
||||
7.0.x
|
||||
|
||||
- name: 'Checkout'
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 'Extract branch name'
|
||||
shell: bash
|
||||
run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
|
||||
id: extract_branch
|
||||
|
||||
- name: 'Run tests'
|
||||
run: |
|
||||
cd '${{ github.workspace }}/MewtocolTests'
|
||||
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
|
||||
run: dotnet test "./MewtocolTests" /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=../Builds/TestResults/coverage.opencover.xml
|
||||
|
||||
- name: 'Run docbuilder'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
dotnet run --project "./AutoTools.DocBuilder/AutoTools.DocBuilder.csproj" "~/plctypes.md"
|
||||
|
||||
#Upload to codecov
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: 'Switch and Commit to docs branch'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
git fetch
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git checkout --orphan ${{ steps.extract_branch.outputs.branch }}_auto_docs
|
||||
git rm -rf .
|
||||
cp ~/plctypes.md ./plctypes.md &&
|
||||
git add "./plctypes.md" -f &&
|
||||
git commit -m "Update documentation"
|
||||
|
||||
- name: 'Push docs commit'
|
||||
uses: ad-m/github-push-action@master
|
||||
continue-on-error: true
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: ${{ steps.extract_branch.outputs.branch }}_auto_docs
|
||||
|
||||
- name: Report Generator
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@5.1.22
|
||||
with:
|
||||
reports: './Builds/TestResults/coverage.opencover.xml'
|
||||
targetdir: './Builds/TestResults'
|
||||
reporttypes: HtmlSummary;MarkdownSummaryGithub;Badges
|
||||
historydir: './Builds/Hist'
|
||||
title: Report
|
||||
|
||||
- name: Markdown report and copy for badges branch
|
||||
run: |
|
||||
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 ~
|
||||
|
||||
- name: Cache test results
|
||||
if: ${{ github.event_name == 'workflow_call' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
key: ${{ inputs.cache-id }}-${{ steps.extract_branch.outputs.branch }}
|
||||
path: |
|
||||
${{ github.workspace }}/Builds/TestResults
|
||||
|
||||
- name: Commit badge
|
||||
continue-on-error: true
|
||||
run: |
|
||||
git fetch
|
||||
git checkout badges
|
||||
cp ~/summary.html ./Builds/TestResults/summary_${{ steps.extract_branch.outputs.branch }}.html
|
||||
cp ~/badge_combined.svg ./Builds/TestResults/badge_combined_${{ steps.extract_branch.outputs.branch }}.svg
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add "./Builds/TestResults/badge_combined_${{ steps.extract_branch.outputs.branch }}.svg" -f
|
||||
git add "./Builds/TestResults/summary_${{ steps.extract_branch.outputs.branch }}.html" -f
|
||||
git commit -m "Add/Update badge for branch ${{ steps.extract_branch.outputs.branch }}"
|
||||
|
||||
- name: 'Push badge commit'
|
||||
uses: ad-m/github-push-action@master
|
||||
if: ${{ success() }}
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: badges
|
||||
|
||||
|
||||
17
AutoTools.ChmDataExtract/AutoTools.ChmDataExtract.csproj
Normal file
17
AutoTools.ChmDataExtract/AutoTools.ChmDataExtract.csproj
Normal file
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPublishable>false</IsPublishable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MewtocolNet\MewtocolNet.csproj"/>
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.50" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
510
AutoTools.ChmDataExtract/Program.cs
Normal file
510
AutoTools.ChmDataExtract/Program.cs
Normal file
@@ -0,0 +1,510 @@
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using HtmlAgilityPack;
|
||||
using MewtocolNet;
|
||||
using MewtocolNet.Helpers;
|
||||
|
||||
namespace AutoTools.ChmDataExtract;
|
||||
|
||||
internal class Program {
|
||||
|
||||
const string sysVarsLoc = @"Panasonic-ID SUNX Control\Control FPWIN Pro 7\Mak\Res_Eng\SysVars.chm";
|
||||
const string funcNamesLoc = @"Panasonic-ID SUNX Control\Control FPWIN Pro 7\Mak\Res_Eng\FPWINPro.chm";
|
||||
|
||||
static Dictionary<string, List<PlcType>> plcGroups = new() {
|
||||
{ "FP7 CPS41/31 E/ES", new List<PlcType> {
|
||||
PlcType.FP7_120k__CPS31E,
|
||||
PlcType.FP7_196k__CPS41E,
|
||||
PlcType.FP7_120k__CPS31ES,
|
||||
PlcType.FP7_196k__CPS41ES,
|
||||
}},
|
||||
{ "FP7 CPS31/31S", new List<PlcType> {
|
||||
PlcType.FP7_120k__CPS31,
|
||||
PlcType.FP7_120k__CPS31S,
|
||||
}},
|
||||
{ "FP7 CPS21", new List<PlcType> {
|
||||
PlcType.FP7_64k__CPS21,
|
||||
}},
|
||||
{ "ELC500", new List<PlcType> {
|
||||
PlcType.ECOLOGIX_0k__ELC500,
|
||||
}},
|
||||
{ "FP-SIGMA 12k", new List<PlcType> {
|
||||
PlcType.FPdSIGMA_12k,
|
||||
PlcType.FPdSIGMA_16k,
|
||||
}},
|
||||
{ "FP-SIGMA 32k", new List<PlcType> {
|
||||
PlcType.FPdSIGMA_32k,
|
||||
PlcType.FPdSIGMA_40k,
|
||||
}},
|
||||
{ "FP0R 16k/32k C types", new List<PlcType> {
|
||||
PlcType.FP0R_16k__C10_C14_C16,
|
||||
PlcType.FP0R_32k__C32,
|
||||
}},
|
||||
{ "FP0R 32k T32", new List<PlcType> {
|
||||
PlcType.FP0R_32k__T32,
|
||||
PlcType.FP0R_32k__F32,
|
||||
}},
|
||||
{ "FP2 16k", new List<PlcType> {
|
||||
PlcType.FP2_16k,
|
||||
}},
|
||||
{ "FP2 32k", new List<PlcType> {
|
||||
PlcType.FP2_32k,
|
||||
}},
|
||||
{ "FP2SH 32k/60k/120k", new List<PlcType> {
|
||||
PlcType.FP2SH_60k,
|
||||
PlcType.FP2SH_60k,
|
||||
PlcType.FP2SH_120k,
|
||||
}},
|
||||
{ "FP-X 16k/32k R-types", new List<PlcType> {
|
||||
PlcType.FPdX_16k__C14R,
|
||||
PlcType.FPdX_32k__C30R_C60R,
|
||||
}},
|
||||
{ "FP-X 16k/32k T-types", new List<PlcType> {
|
||||
PlcType.FPdX_16k__C14TsP,
|
||||
PlcType.FPdX_32k__C30TsP_C60TsP_C38AT_C40T,
|
||||
}},
|
||||
{"FP0H C32T/P ET/EP", new List<PlcType> {
|
||||
PlcType.FP0H_32k__C32TsP,
|
||||
PlcType.FP0H_32k__C32ETsEP,
|
||||
}},
|
||||
{ "FP-X 16k/32k L-types", new List<PlcType> {
|
||||
PlcType.FPdX_16k__L14,
|
||||
PlcType.FPdX_32k__L30_L60,
|
||||
}},
|
||||
{ "FP-X 2.5k C40RT0A", new List<PlcType> {
|
||||
PlcType.FPdX_2c5k__C40RT0A,
|
||||
}},
|
||||
{ "FP-X0 2.5k L14,L30", new List<PlcType> {
|
||||
PlcType.FPdX0_2c5k__L14_L30,
|
||||
}},
|
||||
{ "FP-X0 8k L40,L60", new List<PlcType> {
|
||||
PlcType.FPdX0_8k__L40_L60,
|
||||
}},
|
||||
{ "FP-e 2.7k", new List<PlcType> {
|
||||
PlcType.FPde_2c7k,
|
||||
}},
|
||||
{ "FP-XH 16k/32k R-types", new List<PlcType> {
|
||||
PlcType.FPdXH_16k__C14R,
|
||||
PlcType.FPdXH_32k__C30R_C40R_C60R,
|
||||
}},
|
||||
{ "FP-XH 16k/32k T-types", new List<PlcType> {
|
||||
PlcType.FPdXH_16k__C14TsP,
|
||||
PlcType.FPdXH_32k__C30TsP_C40T_C60TsP,
|
||||
PlcType.FPdXH_32k__C30TsP_C40T_C60TsP,
|
||||
PlcType.FPdXH_32k__C38AT,
|
||||
PlcType.FPdXH_32k__C40ET_C60ET,
|
||||
PlcType.FPdXH_32k__C60ETF,
|
||||
}},
|
||||
{ "FP-XH M4/M8 types", new List<PlcType> {
|
||||
PlcType.FPdXH_32k__M4TsL,
|
||||
PlcType.FPdXH_32k__M8N16TsP,
|
||||
PlcType.FPdXH_32k__M8N30T,
|
||||
}},
|
||||
};
|
||||
|
||||
class AddressException {
|
||||
|
||||
public string ExceptionTitle;
|
||||
|
||||
public string ForSysRegister;
|
||||
|
||||
}
|
||||
|
||||
internal class FPFunction {
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string RedundantName { get; set; } = null!;
|
||||
|
||||
public string Description { get; set; } = null!;
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public Dictionary<string, string[]>? ParametersIn { get; set; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public Dictionary<string, string[]>? ParametersOut { get; set; }
|
||||
|
||||
}
|
||||
|
||||
static void Main(string[] args) => Task.Run(AsyncMain).Wait();
|
||||
|
||||
static async Task AsyncMain () {
|
||||
|
||||
GetFunctionNames();
|
||||
//await GetSystemRegisters();
|
||||
|
||||
}
|
||||
|
||||
static void GetFunctionNames () {
|
||||
|
||||
var functions = new Dictionary<string, FPFunction>();
|
||||
|
||||
var progLoc = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
|
||||
var progFilesPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
|
||||
var sysVarsPath = Path.Combine(progFilesPath, funcNamesLoc);
|
||||
|
||||
Directory.SetCurrentDirectory(progLoc);
|
||||
File.Copy(sysVarsPath, "./FPWINPro.chm", true);
|
||||
|
||||
var startInfo = new ProcessStartInfo {
|
||||
WorkingDirectory = progLoc,
|
||||
FileName = "hh.exe",
|
||||
Arguments = $"-decompile ./DecompFuncs ./FPWINPro.chm",
|
||||
};
|
||||
|
||||
//call the hh.exe decompiler for chm
|
||||
if (!File.Exists("./DecompFuncs/topics/availability.html")) {
|
||||
var proc = Process.Start(startInfo)!;
|
||||
proc.WaitForExit();
|
||||
}
|
||||
|
||||
var doc = new HtmlDocument();
|
||||
doc.Load("./DecompFuncs/topics/availability.html");
|
||||
|
||||
//[contains(@class, 'table mainbody')]
|
||||
foreach (HtmlNode table in doc.DocumentNode.SelectNodes("//table[1]")) {
|
||||
|
||||
var rows = table?.SelectSingleNode("tbody")?.SelectNodes("tr");
|
||||
if (rows == null) continue;
|
||||
|
||||
foreach (var row in rows) {
|
||||
|
||||
var columns = row.SelectNodes("td");
|
||||
if (columns == null) continue;
|
||||
|
||||
var itemRow = columns?.FirstOrDefault()?.SelectSingleNode("p/a[contains(@class,'xref')]");
|
||||
|
||||
string rowName = itemRow?.InnerText ?? "Unnamed";
|
||||
|
||||
if (!Regex.IsMatch(rowName, @"^F[0-9]{1,3}_.*$")) continue;
|
||||
|
||||
FPFunction functionIns = new FPFunction();
|
||||
|
||||
Console.Write($"Var: {rowName, -20}");
|
||||
|
||||
var href = itemRow?.GetAttributeValue("href", null);
|
||||
|
||||
if (href != null) {
|
||||
|
||||
//Console.Write($" {href}");
|
||||
|
||||
var docSub = new HtmlDocument();
|
||||
docSub.Load($"./DecompFuncs{href}");
|
||||
|
||||
var noteSection = docSub.DocumentNode.SelectSingleNode("//section/div[contains(@class,'note note')]");
|
||||
var xrefRedundant = noteSection?.SelectSingleNode("p/a[contains(@class,'xref')]");
|
||||
var xrefNodeContent = noteSection?.SelectSingleNode("p/span");
|
||||
|
||||
//get params in / out
|
||||
var inOutDefinitionNodes = docSub.DocumentNode.SelectNodes("//p[contains(@class,'p inoutput')]");
|
||||
|
||||
if (inOutDefinitionNodes != null) {
|
||||
|
||||
foreach (var ioTypeNode in inOutDefinitionNodes) {
|
||||
|
||||
var nodeInOutType = ioTypeNode.InnerText.SanitizeLinebreakFormatting();
|
||||
|
||||
Console.Write($"{nodeInOutType}: ");
|
||||
|
||||
var currentSibling = ioTypeNode;
|
||||
|
||||
while (true) {
|
||||
|
||||
if (currentSibling.NextSibling == null) break;
|
||||
currentSibling = currentSibling.NextSibling;
|
||||
|
||||
if (currentSibling.HasClass("inoutput")) {
|
||||
break;
|
||||
}
|
||||
|
||||
var paramNodes = currentSibling.SelectNodes("dt");
|
||||
|
||||
if (paramNodes == null) continue;
|
||||
|
||||
foreach (var paramNode in paramNodes) {
|
||||
|
||||
var paramName = paramNode.SelectSingleNode("span[1]")?.InnerText?.SanitizeBracketFormatting();
|
||||
var paramTypes = paramNode.SelectSingleNode("span[2]")?.InnerText?.SanitizeBracketFormatting();
|
||||
|
||||
if (paramName != null && paramTypes != null) {
|
||||
|
||||
if (functionIns.ParametersIn == null)
|
||||
functionIns.ParametersIn = new Dictionary<string, string[]>();
|
||||
|
||||
if (functionIns.ParametersOut == null)
|
||||
functionIns.ParametersOut = new Dictionary<string, string[]>();
|
||||
|
||||
Console.Write($"{paramName} {paramTypes}");
|
||||
|
||||
var splitParamNames = paramName.Split(", ");
|
||||
|
||||
foreach (var splitName in splitParamNames) {
|
||||
|
||||
if (nodeInOutType == "Input") {
|
||||
|
||||
if (functionIns.ParametersIn.ContainsKey(splitName)) break;
|
||||
functionIns.ParametersIn.Add(splitName, paramTypes.SanitizeBracketFormatting().Split(", "));
|
||||
|
||||
} else {
|
||||
|
||||
if (functionIns.ParametersOut.ContainsKey(splitName)) break;
|
||||
functionIns.ParametersOut.Add(splitName, paramTypes.SanitizeBracketFormatting().Split(", "));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
HtmlNode? descrSection = null;
|
||||
|
||||
if (xrefRedundant != null && xrefNodeContent != null && xrefNodeContent.InnerText.StartsWith("This is a redundant F instruction")) {
|
||||
|
||||
descrSection = docSub.DocumentNode.SelectSingleNode("//section[2]");
|
||||
|
||||
functionIns.RedundantName = xrefRedundant.InnerText;
|
||||
|
||||
//Console.Write($"{xrefRedundant.InnerText}");
|
||||
|
||||
} else {
|
||||
|
||||
descrSection = docSub.DocumentNode.SelectSingleNode("//section[1]");
|
||||
|
||||
}
|
||||
|
||||
if (descrSection != null) {
|
||||
|
||||
var descrText = descrSection?.InnerText;
|
||||
|
||||
if(descrText != null) {
|
||||
|
||||
descrText = descrText.SanitizeLinebreakFormatting().Replace("\"", "\\\"");
|
||||
|
||||
functionIns.Description = descrText;
|
||||
|
||||
//Console.Write($" {descrText}");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
functions.Add(rowName, functionIns);
|
||||
Console.WriteLine();
|
||||
|
||||
//compatibility matrix
|
||||
//for (int i = 1; i < columns?.Count - 1; i++) {
|
||||
|
||||
// bool isChecked = columns?.ElementAtOrDefault(i)?.SelectSingleNode("p")?.InnerHtml != "";
|
||||
|
||||
// Console.Write($"{(isChecked ? "1" : "0")}, ");
|
||||
|
||||
//}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BuildFunctionNamesDictFile(functions);
|
||||
|
||||
}
|
||||
|
||||
static void BuildFunctionNamesDictFile (Dictionary<string, FPFunction> dict) {
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("using System.Collections.Generic;\n");
|
||||
sb.AppendLine("namespace MewtocolNet.AutoGeneratedData {\n");
|
||||
|
||||
sb.AppendLine("\tpublic class FPFunction {\n");
|
||||
|
||||
sb.AppendLine("\t\tpublic string RedundantName { get; private set; }\n");
|
||||
sb.AppendLine("\t\tpublic string Description { get; private set; }\n");
|
||||
sb.AppendLine("\t\tpublic Dictionary<string, string[]> ParametersIn { get; private set; }\n");
|
||||
sb.AppendLine("\t\tpublic Dictionary<string, string[]> ParametersOut { get; private set; }\n");
|
||||
|
||||
sb.AppendLine("\t\tpublic static readonly Dictionary<string, FPFunction> functions = new Dictionary<string, FPFunction> {\n");
|
||||
|
||||
foreach (var item in dict) {
|
||||
|
||||
sb.AppendLine($"\t\t\t{{ \"{item.Key}\", new FPFunction {{");
|
||||
|
||||
if(item.Value.RedundantName != null)
|
||||
sb.AppendLine($"\t\t\t\tRedundantName = \"{item.Value.RedundantName}\",");
|
||||
|
||||
if(item.Value.Description != null)
|
||||
sb.AppendLine($"\t\t\t\tDescription = \"{item.Value.Description}\",");
|
||||
|
||||
if (item.Value.ParametersIn != null) {
|
||||
|
||||
sb.AppendLine("\t\t\t\tParametersIn = new Dictionary<string, string[]> {");
|
||||
|
||||
foreach (var paramIn in item.Value.ParametersIn) {
|
||||
|
||||
sb.AppendLine($"\t\t\t\t\t{{ \"{paramIn.Key}\", new string[] {{");
|
||||
|
||||
foreach (var paramType in paramIn.Value) {
|
||||
|
||||
sb.AppendLine($"\t\t\t\t\t\t\"{paramType}\",");
|
||||
|
||||
}
|
||||
|
||||
sb.AppendLine("\t\t\t\t\t}},");
|
||||
|
||||
}
|
||||
|
||||
sb.AppendLine("\t\t\t\t},");
|
||||
|
||||
}
|
||||
|
||||
if (item.Value.ParametersOut != null) {
|
||||
|
||||
sb.AppendLine("\t\t\t\tParametersOut = new Dictionary<string, string[]> {");
|
||||
|
||||
foreach (var paramOut in item.Value.ParametersOut) {
|
||||
|
||||
sb.AppendLine($"\t\t\t\t\t{{ \"{paramOut.Key}\", new string[] {{");
|
||||
|
||||
foreach (var paramType in paramOut.Value) {
|
||||
|
||||
sb.AppendLine($"\t\t\t\t\t\t\"{paramType}\",");
|
||||
|
||||
}
|
||||
|
||||
sb.AppendLine("\t\t\t\t\t}},");
|
||||
|
||||
}
|
||||
|
||||
sb.AppendLine("\t\t\t\t},");
|
||||
|
||||
}
|
||||
|
||||
sb.AppendLine($"\t\t\t}}}},");
|
||||
|
||||
}
|
||||
|
||||
sb.AppendLine("\t\t};\n");
|
||||
sb.AppendLine("\t}\n");
|
||||
sb.AppendLine("}");
|
||||
|
||||
File.WriteAllText("../../../../MewtocolNet/AutoGeneratedData/FPFunction.cs", sb.ToString());
|
||||
|
||||
}
|
||||
|
||||
static async Task GetSystemRegisters () {
|
||||
|
||||
var addressExceptions = new List<AddressException>();
|
||||
|
||||
var progLoc = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
|
||||
var progFilesPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
|
||||
var sysVarsPath = Path.Combine(progFilesPath, sysVarsLoc);
|
||||
|
||||
Directory.SetCurrentDirectory(progLoc);
|
||||
File.Copy(sysVarsPath, "./SysVars.chm", true);
|
||||
|
||||
var startInfo = new ProcessStartInfo {
|
||||
WorkingDirectory = progLoc,
|
||||
FileName = "hh.exe",
|
||||
Arguments = $"-decompile ./Decomp ./SysVars.chm",
|
||||
};
|
||||
|
||||
//call the hh.exe decompiler for chm
|
||||
if (!File.Exists("./Decomp/topics/availability.html")) {
|
||||
var proc = Process.Start(startInfo)!;
|
||||
proc.WaitForExit();
|
||||
}
|
||||
|
||||
var doc = new HtmlDocument();
|
||||
doc.Load("./Decomp/topics/availability.html");
|
||||
|
||||
//[contains(@class, 'table mainbody')]
|
||||
foreach (HtmlNode table in doc.DocumentNode.SelectNodes("//table[1]")) {
|
||||
|
||||
var rows = table?.SelectSingleNode("tbody")?.SelectNodes("tr");
|
||||
if (rows == null) continue;
|
||||
|
||||
string lastRegisterName = "Name";
|
||||
|
||||
int iSystemRegister = 0;
|
||||
|
||||
foreach (var row in rows) {
|
||||
|
||||
var columns = row.SelectNodes("td");
|
||||
if (columns == null) continue;
|
||||
|
||||
//get var name
|
||||
var varNameNode = columns?.FirstOrDefault()?.SelectSingleNode("p/a[contains(@class,'xref')]");
|
||||
|
||||
string registerAddress;
|
||||
int iterateStart;
|
||||
|
||||
if (varNameNode != null) {
|
||||
|
||||
lastRegisterName = varNameNode.InnerText;
|
||||
|
||||
//get second col
|
||||
var regAddressNode = columns?.ElementAtOrDefault(1)?.SelectSingleNode("p");
|
||||
registerAddress = regAddressNode?.InnerText ?? "Null";
|
||||
iterateStart = 2;
|
||||
|
||||
} else {
|
||||
|
||||
//get first col
|
||||
var regAddressNode = columns?.ElementAtOrDefault(0)?.SelectSingleNode("p");
|
||||
registerAddress = regAddressNode?.InnerText ?? "Null";
|
||||
iterateStart = 1;
|
||||
}
|
||||
|
||||
//filter the address for annotations
|
||||
var regexAnnotation = new Regex(@"\(.*\)");
|
||||
var matchAnnotation = regexAnnotation.Match(registerAddress);
|
||||
if (matchAnnotation.Success) {
|
||||
|
||||
registerAddress = regexAnnotation.Replace(registerAddress, "");
|
||||
|
||||
addressExceptions.Add(new AddressException {
|
||||
ForSysRegister = lastRegisterName,
|
||||
ExceptionTitle = matchAnnotation.Value,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
Console.Write($"Var: {lastRegisterName} | {registerAddress} ".PadRight(100, ' '));
|
||||
|
||||
for (int i = iterateStart, j = 0; i < columns?.Count + 1; i++) {
|
||||
|
||||
if (j >= plcGroups.Count - 1) continue;
|
||||
|
||||
var group = plcGroups.Keys.ToList()[j];
|
||||
|
||||
bool isChecked = columns?.ElementAtOrDefault(i)?.SelectSingleNode("p")?.InnerHtml != "";
|
||||
|
||||
Console.Write($"{(isChecked ? "1" : "0")}, ");
|
||||
|
||||
j++;
|
||||
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
16
AutoTools.DocBuilder/AutoTools.DocBuilder.csproj
Normal file
16
AutoTools.DocBuilder/AutoTools.DocBuilder.csproj
Normal file
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPublishable>false</IsPublishable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MewtocolNet\MewtocolNet.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
2
AutoTools.DocBuilder/Docs/program-read-write.md
Normal file
2
AutoTools.DocBuilder/Docs/program-read-write.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Reading and writing programs
|
||||
|
||||
291
AutoTools.DocBuilder/Docs/reverse_engineering_data.md
Normal file
291
AutoTools.DocBuilder/Docs/reverse_engineering_data.md
Normal file
@@ -0,0 +1,291 @@
|
||||
# Open Questions
|
||||
|
||||
- What is the HW information byte for?
|
||||
- What are the last bytes of the EXRT message for?
|
||||
|
||||
# Mewtocol Init
|
||||
|
||||
| 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 | ?
|
||||
|
||||
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 |
|
||||
|
||||
## FP-X0 2.5k L14,L30
|
||||
|
||||
### %EE$RT
|
||||
|
||||
|Reponse Byte|Description|
|
||||
|------------|-----------|
|
||||
| 72 | Model code |
|
||||
| 12 | Version |
|
||||
| 02 | Prog capacity |
|
||||
| 82 | Op mode |
|
||||
| 00 | Link unit |
|
||||
| 00 | Error flag |
|
||||
| 0000 | Self diag error |
|
||||
|
||||
### %EE$EX00RT
|
||||
|
||||
|Reponse Byte|Description|
|
||||
|------------|-----------|
|
||||
| 00 | Extended mode
|
||||
| 32 | Data item count
|
||||
| 72 | Machine type
|
||||
| 00 | Version (Fixed to 00)
|
||||
| 02 | Prog capacity in K
|
||||
| 82 | Operation mode / status
|
||||
| 00 | Link unit
|
||||
| 00 | Error flag
|
||||
| 0000 | Self diag error
|
||||
| 23 | Version
|
||||
| 01 | Hardware information
|
||||
| 0 | Number of programs
|
||||
| 4100 | Program size BCD
|
||||
| 0301 | Header size (no. of words) bcd
|
||||
| 2819 | System register size
|
||||
| 0000001480004 | ?
|
||||
|
||||
## FP0 2.7k C10,C14
|
||||
|
||||
### %EE$RT
|
||||
|
||||
|Reponse Byte|Description|
|
||||
|------------|-----------|
|
||||
| 05 | Model code |
|
||||
| 12 | Version |
|
||||
| 03 | Prog capacity |
|
||||
| 82 | Op mode |
|
||||
| 00 | Link unit |
|
||||
| 00 | Error flag |
|
||||
| 0000 | Self diag error |
|
||||
|
||||
### %EE$EX00RT
|
||||
|
||||
|Reponse Byte|Description|
|
||||
|------------|-----------|
|
||||
| 00 | Extended mode
|
||||
| 32 | Data item count
|
||||
| 40 | Machine type
|
||||
| 00 | Version (Fixed to 00)
|
||||
| 03 | Prog capacity in K
|
||||
| 82 | Operation mode / status
|
||||
| 00 | Link unit
|
||||
| 00 | Error flag
|
||||
| 0000 | Self diag error
|
||||
| 23 | Version
|
||||
| 01 | Hardware information
|
||||
| 0 | Number of programs
|
||||
| 4100 | Program size BCD
|
||||
| 0301 | Header size (no. of words) bcd
|
||||
| 2819 | System register size
|
||||
| 20130000080070004 | ?
|
||||
|
||||
## FP0 5k C32,SL1
|
||||
|
||||
### %EE$RT
|
||||
|
||||
|Reponse Byte|Description|
|
||||
|------------|-----------|
|
||||
| 06 | Model code |
|
||||
| 12 | Version |
|
||||
| 05 | Prog capacity |
|
||||
| 82 | Op mode |
|
||||
| 00 | Link unit |
|
||||
| 00 | Error flag |
|
||||
| 0000 | Self diag error |
|
||||
|
||||
### %EE$EX00RT
|
||||
|
||||
|Reponse Byte|Description|
|
||||
|------------|-----------|
|
||||
| 00 | Extended mode
|
||||
| 32 | Data item count
|
||||
| 41 | Machine type
|
||||
| 00 | Version (Fixed to 00)
|
||||
| 03 | Prog capacity in K
|
||||
| 82 | Operation mode / status
|
||||
| 00 | Link unit
|
||||
| 00 | Error flag
|
||||
| 0000 | Self diag error
|
||||
| 23 | Version
|
||||
| 01 | Hardware information
|
||||
| 0 | Number of programs
|
||||
| 4100 | Program size BCD
|
||||
| 0501 | Header size (no. of words) bcd
|
||||
| 2819 | System register size
|
||||
| 20130000080070004 | ?
|
||||
|
||||
## FP0 10k
|
||||
|
||||
### %EE$RT
|
||||
|
||||
|Reponse Byte|Description|
|
||||
|------------|-----------|
|
||||
| 42 | Model code |
|
||||
| 12 | Version |
|
||||
| 10 | Prog capacity |
|
||||
| 82 | Op mode |
|
||||
| 00 | Link unit |
|
||||
| 00 | Error flag |
|
||||
| 0000 | Self diag error |
|
||||
|
||||
### %EE$EX00RT
|
||||
|
||||
|Reponse Byte|Description|
|
||||
|------------|-----------|
|
||||
| 00 | Extended mode
|
||||
| 32 | Data item count
|
||||
| 42 | Machine type
|
||||
| 00 | Version (Fixed to 00)
|
||||
| 10 | Prog capacity in K
|
||||
| 82 | Operation mode / status
|
||||
| 00 | Link unit
|
||||
| 00 | Error flag
|
||||
| 0000 | Self diag error
|
||||
| 23 | Version
|
||||
| 01 | Hardware information
|
||||
| 0 | Number of programs
|
||||
| 4100 | Program size BCD
|
||||
| 1001 | Header size (no. of words) bcd
|
||||
| 2819 | System register size
|
||||
| 20130000080070004 | ?
|
||||
|
||||
## FP2SH 60k
|
||||
|
||||
### %EE$RT
|
||||
|
||||
|Reponse Byte|Description|
|
||||
|------------|-----------|
|
||||
| 60 | Model code |
|
||||
| 12 | Version |
|
||||
| 00 | Prog capacity |
|
||||
| 82 | Op mode |
|
||||
| 00 | Link unit |
|
||||
| 00 | Error flag |
|
||||
| 0000 | Self diag error |
|
||||
|
||||
### %EE$EX00RT
|
||||
|
||||
|Reponse Byte|Description|
|
||||
|------------|-----------|
|
||||
| 00 | Extended mode
|
||||
| 32 | Data item count
|
||||
| 60 | Machine type
|
||||
| 00 | Version (Fixed to 00)
|
||||
| 00 | Prog capacity in K
|
||||
| 82 | Operation mode / status
|
||||
| 00 | Link unit
|
||||
| 00 | Error flag
|
||||
| 0000 | Self diag error
|
||||
| 23 | Version
|
||||
| 01 | Hardware information
|
||||
| 0 | Number of programs
|
||||
| 4100 | Program size BCD
|
||||
| 6001 | Header size (no. of words) bcd
|
||||
| 2819 | System register size
|
||||
| 20130000000080004 | ?
|
||||
|
||||
# Mewtocol-7 Com
|
||||
|
||||
## Getting the status of the plc
|
||||
|
||||
`%EE#RT`
|
||||
|
||||
Normal plc would return the default, see above.
|
||||
|
||||
The FP7 series returns:
|
||||
|
||||
`%EE$RT0000000100000000`
|
||||
|
||||
> This indicates that the device uses the Mewtocol 7 format
|
||||
|
||||
When trying to use EXRT the FP7 returns not supported
|
||||
|
||||
[R] `%EE#EX00RT00`
|
||||
|
||||
[S] `%EE!42`
|
||||
|
||||
Then request the status from the PLC using Mewtocol 7
|
||||
|
||||
[R] `>@EEE00#30STRD00`
|
||||
|
||||
[O] `>@EEE00$30STRD`
|
||||
|
||||
|Reponse Byte|Description|
|
||||
|-------------|-----------|
|
||||
| 00 | Read status (always 0) |
|
||||
| 07 | series code: 7 = FP7 |
|
||||
| 10 | model code: 3 = CPS41E, 4 = CPS31E, 5 = CPS31, 6 = CPS41ES, 7 = CPS31ES, 8 = CPS31S, 9 = CPS21, 10 = ElC500 |
|
||||
| 0000 | user special order code (Not important) |
|
||||
| 0453 | latest cpu version = 4.53 |
|
||||
| 0453 | communication cpu version = 4.53 |
|
||||
| 0453 | operation cpu version = 4.53 |
|
||||
| 01 | op mode = 4.53 |
|
||||
| 00 | error flag |
|
||||
| 0000 | self diag error |
|
||||
| 00 | sd card information |
|
||||
16
AutoTools.DocBuilder/Docs/system-registers.md
Normal file
16
AutoTools.DocBuilder/Docs/system-registers.md
Normal file
@@ -0,0 +1,16 @@
|
||||
3 byte system registers, read with RR
|
||||
|
||||
|RR Adress|Interpreting type|Description|
|
||||
|-|-|-|
|
||||
|RR000|uint16|Program size steps capacity|
|
||||
|RR005|uint16|Start address counter|
|
||||
|RR006|uint16|Start address timer/counter|
|
||||
|RR007|uint16|Start WR area (self reliant)|
|
||||
|RR008 - RR009|uint32|Start DT area (self reliant)|
|
||||
|
||||
4 byte / 1 word system registers read with R
|
||||
|
||||
|WR Adress|Interpreting type|Description|
|
||||
|-|-|-|
|
||||
|R900|uint16|Self diag error|
|
||||
|R902|uint16|Mode info|
|
||||
152
AutoTools.DocBuilder/Program.cs
Normal file
152
AutoTools.DocBuilder/Program.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
//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;
|
||||
|
||||
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 = null!;
|
||||
|
||||
if (args.Length == 0) {
|
||||
|
||||
filePath = Path.Combine(entryLoc.Location, @"..\..\..\..\Docs\plctypes.md");
|
||||
|
||||
} else {
|
||||
|
||||
var userPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
|
||||
filePath = args[0].Replace("~/", $"{userPath}/");
|
||||
|
||||
}
|
||||
|
||||
Console.WriteLine($"{filePath}");
|
||||
|
||||
StringBuilder markdownBuilder = new StringBuilder();
|
||||
|
||||
var plcNames = Enum.GetNames<PlcType>().Where(x => x != PlcType.Unknown.ToString()).OrderBy(x => x).ToArray();
|
||||
|
||||
void WritePlcTypeTable(string[] names) {
|
||||
|
||||
var groups = names.Select(x => x.ToNameDecompose())
|
||||
.GroupBy(x => x.Group)
|
||||
.SelectMany(g => g.OrderBy(x => x.Size))
|
||||
.GroupBy(x => x.Group);
|
||||
|
||||
markdownBuilder.AppendLine("<table>");
|
||||
|
||||
bool isFirstIt = true;
|
||||
|
||||
foreach (var group in groups) {
|
||||
|
||||
group.OrderBy(x => x.TypeCode);
|
||||
|
||||
bool isFirstGroup = true;
|
||||
|
||||
foreach (var enu in group) {
|
||||
|
||||
string cpuOrMachCode = null!;
|
||||
|
||||
cpuOrMachCode = enu.TypeCode.ToString("X6");
|
||||
ArgumentNullException.ThrowIfNull(enu);
|
||||
|
||||
//first iteration
|
||||
if (isFirstIt) {
|
||||
|
||||
markdownBuilder.AppendLine("<tr>");
|
||||
|
||||
markdownBuilder.AppendLine($"<th>Type</th>");
|
||||
markdownBuilder.AppendLine($"<th>Capacity</th>");
|
||||
markdownBuilder.AppendLine($"<th>Code</th>");
|
||||
markdownBuilder.AppendLine($"<th>Enum</th>");
|
||||
markdownBuilder.AppendLine($"<th>DCNT</th>");
|
||||
markdownBuilder.AppendLine($"<th>EXRT</th>");
|
||||
markdownBuilder.AppendLine($"<th>Tested</th>");
|
||||
|
||||
markdownBuilder.AppendLine("</tr>");
|
||||
|
||||
isFirstIt = false;
|
||||
|
||||
}
|
||||
|
||||
if (isFirstGroup) {
|
||||
|
||||
markdownBuilder.AppendLine("<tr>");
|
||||
|
||||
markdownBuilder.AppendLine($"<td colspan=\"7\" height=50>📟 <b>{group.Key}</b> </td>");
|
||||
|
||||
markdownBuilder.AppendLine("</tr>");
|
||||
|
||||
isFirstGroup = false;
|
||||
|
||||
}
|
||||
|
||||
markdownBuilder.AppendLine("<tr>");
|
||||
|
||||
markdownBuilder.AppendLine($"<td> {(enu.SubTypes.Length == 0 ? "-" : string.Join(", ", enu.SubTypes))} </td>");
|
||||
markdownBuilder.AppendLine($"<td> {enu.Size}k </td>");
|
||||
markdownBuilder.AppendLine($"<td><code>0x{cpuOrMachCode}</code></td>");
|
||||
|
||||
if (enu.IsDiscontinuedModel) {
|
||||
|
||||
markdownBuilder.AppendLine($"<td><i>{enu.EncodedName}</i></td>");
|
||||
markdownBuilder.AppendLine($"<td align=center>⚠️</td>");
|
||||
|
||||
} else {
|
||||
|
||||
markdownBuilder.AppendLine($"<td colspan=\"2\"><i>{enu.EncodedName}</i></td>");
|
||||
|
||||
}
|
||||
|
||||
markdownBuilder.AppendLine($"<td align=center> {(enu.UsesEXRT ? "✅" : "❌")} </td>");
|
||||
markdownBuilder.AppendLine($"<td align=center> {(enu.WasTestedLive ? "✅" : "❌")} </td>");
|
||||
|
||||
markdownBuilder.AppendLine("</tr>");
|
||||
|
||||
|
||||
}
|
||||
|
||||
isFirstIt = false;
|
||||
|
||||
}
|
||||
|
||||
markdownBuilder.AppendLine("</table>");
|
||||
markdownBuilder.AppendLine("\n");
|
||||
|
||||
}
|
||||
|
||||
markdownBuilder.AppendLine($"# PLC Type Table");
|
||||
markdownBuilder.AppendLine($"Auto Generated @ **{DateTime.Now.ToUniversalTime().ToString("u", CultureInfo.InvariantCulture)}**\n");
|
||||
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. " +
|
||||
$"Some models are never uniquely identifiable by their typecode and need extra hints like Prog Capacity in EXRT or RT. \n\n" +
|
||||
$"Typecode explained:\n" +
|
||||
$"```\n" +
|
||||
$"From left to right\n" +
|
||||
$"0x\n" +
|
||||
$"07 <= extended code (00 for non Mewtocol 7 devices)\n" +
|
||||
$"0120 <= for 120k (Prog capacity), with RT/EXRT/MEW7 override order\n" +
|
||||
$"A5 <= Is the actual typecode, with RT/EXRT/MEW7 override order\n" +
|
||||
$"```"
|
||||
);
|
||||
|
||||
markdownBuilder.AppendLine($"> <b>Discontinued PLCs</b><br>");
|
||||
markdownBuilder.AppendLine($"> These are PLCs that are no longer sold by Panasonic. Marked with ⚠️\n");
|
||||
|
||||
markdownBuilder.AppendLine($"> <b>EXRT PLCs</b><br>");
|
||||
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(plcNames);
|
||||
|
||||
File.WriteAllText(filePath, markdownBuilder.ToString());
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPublishable>false</IsPublishable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\MewtocolNet\MewtocolNet.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
82
Examples/Examples.BasicEthernet/Program.cs
Normal file
82
Examples/Examples.BasicEthernet/Program.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using MewtocolNet;
|
||||
using MewtocolNet.Logging;
|
||||
|
||||
namespace Examples.BasicUsage;
|
||||
|
||||
internal class Program {
|
||||
|
||||
const string IP = "192.168.115.210";
|
||||
const int PORT = 9094;
|
||||
|
||||
static void Main(string[] args) => Task.Run(AsyncMain).Wait();
|
||||
|
||||
static async Task AsyncMain () {
|
||||
|
||||
//the library provides a logging tool, comment this out if needed
|
||||
Logger.LogLevel = LogLevel.Critical;
|
||||
Logger.OnNewLogMessage((t, l, m) => { Console.WriteLine(m); });
|
||||
|
||||
//create a new interface to the plc using ethernet / tcp ip
|
||||
//the using keyword is optional, if you want to use your PLC instance
|
||||
//globally leave it
|
||||
|
||||
//you can also specify the source ip with:
|
||||
//Mewtocol.Ethernet(IP, PORT).FromSource("192.168.113.2", 46040).Build()
|
||||
|
||||
using (var plc = Mewtocol.Ethernet(IP, PORT).Build()) {
|
||||
|
||||
//connect async to the plc
|
||||
await plc.ConnectAsync();
|
||||
|
||||
//check if the connection was established
|
||||
if (!plc.IsConnected) {
|
||||
Console.WriteLine("Failed to connect to the plc...");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
//print basic plc info
|
||||
Console.WriteLine(plc.PlcInfo);
|
||||
|
||||
//check if the plc is not in RUN mode, change to run
|
||||
if(!plc.PlcInfo.IsRunMode) await plc.SetOperationModeAsync(true);
|
||||
|
||||
//get information about the plc
|
||||
Console.WriteLine($"PLC type: {plc.PlcInfo.TypeName}");
|
||||
Console.WriteLine($"Capacity: {plc.PlcInfo.ProgramCapacity}k");
|
||||
Console.WriteLine($"Error: {plc.PlcInfo.SelfDiagnosticError}k");
|
||||
|
||||
//set the plc to prog mode
|
||||
//await plc.SetOperationModeAsync(false);
|
||||
|
||||
//do disconnect use
|
||||
plc.Disconnect();
|
||||
|
||||
//or
|
||||
//await plc.DisconnectAsync();
|
||||
|
||||
//you can then change the connection settings for example to another PLC
|
||||
plc.ConfigureConnection("192.168.115.212", 9094);
|
||||
await plc.ConnectAsync();
|
||||
|
||||
plc.Disconnect();
|
||||
|
||||
plc.ConfigureConnection("192.168.115.214", 9094);
|
||||
await plc.ConnectAsync();
|
||||
|
||||
plc.Disconnect();
|
||||
|
||||
plc.ConfigureConnection("192.168.178.55", 9094);
|
||||
await plc.ConnectAsync();
|
||||
|
||||
}
|
||||
|
||||
//you can also find any applicable source endpoints by using:
|
||||
foreach (var endpoint in Mewtocol.GetSourceEndpoints()) {
|
||||
|
||||
Console.WriteLine($"Usable endpoint: {endpoint}");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPublishable>false</IsPublishable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\MewtocolNet\MewtocolNet.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
148
Examples/Examples.BasicRegisterReadWrite/Program.cs
Normal file
148
Examples/Examples.BasicRegisterReadWrite/Program.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using MewtocolNet;
|
||||
using MewtocolNet.Logging;
|
||||
using MewtocolNet.Registers;
|
||||
|
||||
namespace Examples.BasicRegisterReadWrite;
|
||||
|
||||
internal class Program {
|
||||
|
||||
//const string PLC_IP = "192.168.178.55";
|
||||
const string PLC_IP = "192.168.115.210";
|
||||
|
||||
static void Main(string[] args) => Task.Run(AsyncMain).Wait();
|
||||
|
||||
static async Task AsyncMain() {
|
||||
|
||||
Console.Clear();
|
||||
|
||||
//the library provides a logging tool, comment this out if needed
|
||||
Logger.LogLevel = LogLevel.Critical;
|
||||
|
||||
//here we collect our built registers
|
||||
IRegister<short> simpleNumberRegister = null!;
|
||||
IRegister<ushort> simpleNumberRegister2 = null!;
|
||||
|
||||
IRegister<Word> simpleWordRegister = null!;
|
||||
IArrayRegister<Word> overlapWordRegister = null!;
|
||||
|
||||
IStringRegister stringRegister = null!;
|
||||
IArrayRegister<string> stringArrayRegister = null!;
|
||||
|
||||
IArrayRegister2D<short> simpleNumberRegister2Dim = null!;
|
||||
|
||||
//create a new interface to the plc using ethernet / tcp ip
|
||||
using var plc = Mewtocol.Ethernet(PLC_IP)
|
||||
.WithRegisters(b => {
|
||||
|
||||
//a simple INT at register address DT1000
|
||||
b.Struct<short>("DT1000").Build(out simpleNumberRegister);
|
||||
b.Struct<ushort>("DT1001").Build(out simpleNumberRegister2);
|
||||
|
||||
//you can also read the same array as an other data type
|
||||
//not how they are at the same address, that means their values are linked
|
||||
b.Struct<Word>("DT1000").Build(out simpleWordRegister);
|
||||
|
||||
//same link feature is also true for arrays that overlap their addresses
|
||||
//this will go from DT999 to DT1001 because its a 3 Word array
|
||||
b.Struct<Word>("DT999").AsArray(3).Build(out overlapWordRegister);
|
||||
|
||||
//strings area not stacially sized, they use a different constructor
|
||||
b.String("DT1024", 32).Build(out stringRegister);
|
||||
|
||||
//string can also be arrayed
|
||||
b.String("DT2030", 5).AsArray(3).Build(out stringArrayRegister);
|
||||
|
||||
//a simple 2 dimensional ARRAY [0..2, 0..2] OF INT at DT2003
|
||||
b.Struct<short>("DT2003").AsArray(3, 3).Build(out simpleNumberRegister2Dim);
|
||||
|
||||
b.Struct<bool>("R19A").Build();
|
||||
|
||||
})
|
||||
.Build();
|
||||
|
||||
//you can explain the internal register layout and which ones are linked by
|
||||
Console.WriteLine(plc.Explain());
|
||||
|
||||
//connect async to the plc
|
||||
await plc.ConnectAsync();
|
||||
|
||||
//check if the connection was established
|
||||
if (!plc.IsConnected) {
|
||||
Console.WriteLine("Failed to connect to the plc...");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
//restarts the program, this will make sure the global registers of the plc get reset each run
|
||||
await plc.RestartProgramAsync();
|
||||
|
||||
//from this point on we can modify our registers
|
||||
|
||||
//read the value of the the register
|
||||
short readValue = await simpleNumberRegister.ReadAsync();
|
||||
ushort readValue2 = await simpleNumberRegister2.ReadAsync();
|
||||
|
||||
//show the value
|
||||
Console.WriteLine($"Read value for {nameof(simpleNumberRegister)}: {readValue}");
|
||||
Console.WriteLine($"Read value for {nameof(simpleNumberRegister2)}: {readValue2}");
|
||||
|
||||
//write the value
|
||||
await simpleNumberRegister.WriteAsync(30);
|
||||
|
||||
//show the value
|
||||
Console.WriteLine($"Value of {nameof(simpleNumberRegister)}: {simpleNumberRegister.Value}");
|
||||
|
||||
//because the two registers at DT1000 are linked by their memory address in the plc,
|
||||
//both of their values got updated
|
||||
Console.WriteLine($"Value of {nameof(simpleWordRegister)}: {simpleWordRegister.Value}");
|
||||
|
||||
//also the overlapping word array register value got updated, but only at the DT positions that were read
|
||||
int i = 0;
|
||||
overlapWordRegister.Value.ToList().ForEach(x => {
|
||||
Console.WriteLine($"Value of {nameof(overlapWordRegister)}[{i}]: {x}");
|
||||
i++;
|
||||
});
|
||||
|
||||
//you can even read out a word bitwise
|
||||
Console.WriteLine($"Bits of {nameof(simpleWordRegister)}: {simpleWordRegister.Value?.ToStringBits()}");
|
||||
//or as a single bit
|
||||
Console.WriteLine($"Bit at index 3 of {nameof(simpleWordRegister)}: {simpleWordRegister.Value?[3]}");
|
||||
|
||||
//reading / writing the string register
|
||||
//await stringRegister.WriteAsync("Lorem ipsum dolor sit amet, cons");
|
||||
await stringRegister.ReadAsync();
|
||||
Console.WriteLine($"Value of {nameof(stringRegister)}: {stringRegister.Value}");
|
||||
|
||||
//reading writing a string array register
|
||||
await stringArrayRegister.ReadAsync();
|
||||
|
||||
i = 0;
|
||||
stringArrayRegister.Value.ToList().ForEach(x => {
|
||||
Console.WriteLine($"Value of {nameof(stringArrayRegister)}[{i}]: {x}");
|
||||
i++;
|
||||
});
|
||||
|
||||
//you can either set the whole array at once,
|
||||
//this will be slow if you only want to update one item
|
||||
await stringArrayRegister.WriteAsync(new string[] {
|
||||
"Test1",
|
||||
"Test2",
|
||||
"Test3",
|
||||
});
|
||||
|
||||
//or update just one
|
||||
|
||||
//COMING LATER
|
||||
|
||||
//same thing also works for 2 dim arrays
|
||||
await simpleNumberRegister2Dim.ReadAsync();
|
||||
|
||||
//the array is multi dimensional but can also be iterated per element
|
||||
foreach (var item in simpleNumberRegister2Dim)
|
||||
Console.WriteLine($"Element of {nameof(simpleNumberRegister2Dim)}: {item}");
|
||||
|
||||
//you can also use the array indexer accessors
|
||||
Console.WriteLine($"Element [1, 2] of {nameof(simpleNumberRegister2Dim)}: {simpleNumberRegister2Dim[1, 2]}");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
16
Examples/Examples.Polling/Examples.Polling.csproj
Normal file
16
Examples/Examples.Polling/Examples.Polling.csproj
Normal file
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPublishable>false</IsPublishable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\MewtocolNet\MewtocolNet.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
90
Examples/Examples.Polling/Program.cs
Normal file
90
Examples/Examples.Polling/Program.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using MewtocolNet.Logging;
|
||||
using MewtocolNet.Registers;
|
||||
using MewtocolNet;
|
||||
|
||||
namespace Examples.Polling;
|
||||
|
||||
internal class Program {
|
||||
|
||||
const string PLC_IP = "192.168.178.55";
|
||||
|
||||
static void Main(string[] args) => Task.Run(AsyncMain).Wait();
|
||||
|
||||
static async Task AsyncMain() {
|
||||
|
||||
Console.Clear();
|
||||
|
||||
//the library provides a logging tool, comment this out if needed
|
||||
Logger.LogLevel = LogLevel.Change;
|
||||
|
||||
//create a new interface to the plc using ethernet / tcp ip
|
||||
using var plc = Mewtocol.Ethernet(PLC_IP)
|
||||
.WithPoller() // <= use this in the builder pattern to automatically poll registers
|
||||
.WithInterfaceSettings(s => {
|
||||
|
||||
//this means registers at the same address
|
||||
//or that are contained by overlapping arrays
|
||||
//always get assigned the same poll level
|
||||
s.PollLevelOverwriteMode = PollLevelOverwriteMode.Highest;
|
||||
|
||||
//this means combine all registers that are not farther
|
||||
//than 8 words apart from another into one tcp message,
|
||||
//this safes message frames but a to large number can block read write traffic
|
||||
s.MaxOptimizationDistance = 8;
|
||||
|
||||
})
|
||||
.WithCustomPollLevels(l => {
|
||||
|
||||
//this makes registers at polllevel 2 only run all 3 iterations
|
||||
l.SetLevel(2, 3);
|
||||
|
||||
//this makes registers at polllevel 3 only run all 5 seconds
|
||||
l.SetLevel(3, TimeSpan.FromSeconds(5));
|
||||
|
||||
})
|
||||
.WithRegisters(b => {
|
||||
|
||||
b.Struct<short>("DT1000").Build();
|
||||
b.Struct<Word>("DT1000").Build();
|
||||
b.Struct<ushort>("DT1001").Build();
|
||||
b.Struct<Word>("DT999").AsArray(3).Build();
|
||||
|
||||
//we dont want to poll the string register as fast as we can
|
||||
//so we assign it to level 2 to run only all 3 iterations
|
||||
b.String("DT1024", 32).PollLevel(2).Build();
|
||||
|
||||
//we want to poll this array only at the first iteration,
|
||||
//and after this only if we need the data
|
||||
b.Struct<Word>("DT2000").AsArray(3).PollLevel(PollLevel.FirstIteration).Build();
|
||||
|
||||
//we want to poll this string array all 5 seconds
|
||||
b.String("DT2030", 5).AsArray(3).PollLevel(3).Build();
|
||||
|
||||
//this is a fairly large array, so we never poll it automatically
|
||||
//only if we need the data
|
||||
b.Struct<short>("DT2003").AsArray(3, 3).PollLevel(PollLevel.Never).Build();
|
||||
|
||||
})
|
||||
.Build();
|
||||
|
||||
//this explains the underlying data structure for the poller
|
||||
Console.WriteLine(plc.Explain());
|
||||
|
||||
await plc.ConnectAsync(async () => {
|
||||
|
||||
//we want to restart the program before the poller starts
|
||||
await plc.RestartProgramAsync();
|
||||
|
||||
});
|
||||
|
||||
if (!plc.IsConnected) {
|
||||
Console.WriteLine("Failed to connect to the plc...");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
Console.ReadLine();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPublishable>false</IsPublishable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\MewtocolNet\MewtocolNet.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
31
Examples/Examples.ProgramReadWrite/Program.cs
Normal file
31
Examples/Examples.ProgramReadWrite/Program.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using MewtocolNet;
|
||||
using MewtocolNet.Logging;
|
||||
|
||||
namespace Examples.ProgramReadWrite;
|
||||
|
||||
internal class Program {
|
||||
|
||||
static void Main(string[] args) => Task.Run(AsyncMain).Wait();
|
||||
|
||||
//MewtocolNet.ProgramParsing.PlcBinaryProgram.ParseFromFile(@"C:\Users\fwe\Documents\sps\FPXH_C30_Test1.fp").AnalyzeProgram();
|
||||
|
||||
static async Task AsyncMain () {
|
||||
|
||||
Logger.LogLevel = LogLevel.Error;
|
||||
|
||||
using (var plc = Mewtocol.Ethernet("192.168.178.55").Build()) {
|
||||
|
||||
await plc.ConnectAsync();
|
||||
var prog = await plc.ReadProgramAsync();
|
||||
|
||||
if (prog != null) {
|
||||
|
||||
prog.AnalyzeProgram();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
11
Examples/Examples.WPF/App.xaml
Normal file
11
Examples/Examples.WPF/App.xaml
Normal file
@@ -0,0 +1,11 @@
|
||||
<Application x:Class="Examples.WPF.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:Examples.WPF"
|
||||
xmlns:conv="clr-namespace:Examples.WPF.Converters"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
<conv:NegationConverter x:Key="bInv"/>
|
||||
<conv:ColorHashConverter x:Key="hashColor"/>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
85
Examples/Examples.WPF/App.xaml.cs
Normal file
85
Examples/Examples.WPF/App.xaml.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using Examples.WPF.ViewModels;
|
||||
using MewtocolNet;
|
||||
using MewtocolNet.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Navigation;
|
||||
|
||||
namespace Examples.WPF;
|
||||
|
||||
public partial class App : Application {
|
||||
|
||||
public static AppViewModel ViewModel { get; private set; } = null!;
|
||||
|
||||
public static new App Current = null!;
|
||||
|
||||
public static new MainWindow MainWindow = null!;
|
||||
|
||||
public static ObservableCollection<TextBlock> LoggerItems = null!;
|
||||
|
||||
internal static event Action? LogEventProcessed;
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e) {
|
||||
|
||||
ViewModel = new AppViewModel();
|
||||
|
||||
Current = this;
|
||||
LoggerItems = new();
|
||||
|
||||
Logger.LogLevel = LogLevel.Verbose;
|
||||
Logger.DefaultTargets = LoggerTargets.Trace;
|
||||
|
||||
Logger.OnNewLogMessage((d, l, m) => {
|
||||
|
||||
Application.Current.Dispatcher.BeginInvoke(() => {
|
||||
|
||||
Brush msgColor = null!;
|
||||
|
||||
switch (l) {
|
||||
case LogLevel.Error:
|
||||
msgColor = Brushes.Red;
|
||||
break;
|
||||
case LogLevel.Change:
|
||||
msgColor = Brushes.Blue;
|
||||
break;
|
||||
case LogLevel.Verbose:
|
||||
msgColor = Brushes.Gold;
|
||||
break;
|
||||
case LogLevel.Critical:
|
||||
msgColor = Brushes.Gray;
|
||||
break;
|
||||
}
|
||||
|
||||
if (LoggerItems.Count > 1000) LoggerItems.RemoveAt(0);
|
||||
|
||||
var contRun = msgColor == null ? new Run(m) : new Run(m) {
|
||||
Foreground = msgColor,
|
||||
};
|
||||
|
||||
LoggerItems.Add(new TextBlock {
|
||||
Inlines = {
|
||||
new Run($"[{d:hh:mm:ss:ff}] ") {
|
||||
Foreground = Brushes.LimeGreen,
|
||||
},
|
||||
contRun
|
||||
}
|
||||
});
|
||||
|
||||
LogEventProcessed?.Invoke();
|
||||
|
||||
}, System.Windows.Threading.DispatcherPriority.Background);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
10
Examples/Examples.WPF/AssemblyInfo.cs
Normal file
10
Examples/Examples.WPF/AssemblyInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
62
Examples/Examples.WPF/Converters/ColorHashConverter.cs
Normal file
62
Examples/Examples.WPF/Converters/ColorHashConverter.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
|
||||
namespace Examples.WPF.Converters;
|
||||
|
||||
[ValueConversion(typeof(bool), typeof(bool))]
|
||||
public class ColorHashConverter : IValueConverter {
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
|
||||
var hashCode = value.GetHashCode();
|
||||
var randColor = GenerateRandomVibrantColor(new Random(hashCode));
|
||||
|
||||
System.Windows.Media.Brush outBrush = new System.Windows.Media.SolidColorBrush(new System.Windows.Media.Color {
|
||||
R = randColor.R,
|
||||
G = randColor.G,
|
||||
B = randColor.B,
|
||||
A = 255,
|
||||
});
|
||||
|
||||
return outBrush;
|
||||
|
||||
}
|
||||
|
||||
private Color GenerateRandomVibrantColor(Random random) {
|
||||
|
||||
byte red = (byte)random.Next(256);
|
||||
byte green = (byte)random.Next(256);
|
||||
byte blue = (byte)random.Next(256);
|
||||
|
||||
Color color = Color.FromArgb(255, red, green, blue);
|
||||
|
||||
// Ensure the color is vibrant and colorful
|
||||
while (!IsVibrantColor(color)) {
|
||||
red = (byte)random.Next(256);
|
||||
green = (byte)random.Next(256);
|
||||
blue = (byte)random.Next(256);
|
||||
color = Color.FromArgb(255,red, green, blue);
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
private bool IsVibrantColor(Color color) {
|
||||
|
||||
int minBrightness = 100;
|
||||
int maxBrightness = 200;
|
||||
int minSaturation = 150;
|
||||
|
||||
int brightness = (int)(color.GetBrightness() * 255);
|
||||
int saturation = (int)(color.GetSaturation() * 255);
|
||||
|
||||
return brightness >= minBrightness && brightness <= maxBrightness && saturation >= minSaturation;
|
||||
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
|
||||
|
||||
}
|
||||
14
Examples/Examples.WPF/Converters/NegationConverter.cs
Normal file
14
Examples/Examples.WPF/Converters/NegationConverter.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Examples.WPF.Converters;
|
||||
|
||||
[ValueConversion(typeof(bool), typeof(bool))]
|
||||
public class NegationConverter : IValueConverter {
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => value != null ? !(bool)value : false;
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => value != null ? !(bool)value : false;
|
||||
|
||||
}
|
||||
16
Examples/Examples.WPF/Examples.WPF.csproj
Normal file
16
Examples/Examples.WPF/Examples.WPF.csproj
Normal file
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPublishable>false</IsPublishable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\MewtocolNet\MewtocolNet.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
169
Examples/Examples.WPF/MainWindow.xaml
Normal file
169
Examples/Examples.WPF/MainWindow.xaml
Normal file
@@ -0,0 +1,169 @@
|
||||
<Window x:Class="Examples.WPF.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Examples.WPF"
|
||||
d:DataContext="{d:DesignInstance local:MainWindow, IsDesignTimeCreatable=True}"
|
||||
mc:Ignorable="d"
|
||||
MinWidth="500"
|
||||
MinHeight="400"
|
||||
Height="850"
|
||||
Width="1200"
|
||||
Title="MewtocolNet WPF Demo">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition MinHeight="30" Height="150"/>
|
||||
</Grid.RowDefinitions>
|
||||
<ContentControl x:Name="mainContent"/>
|
||||
<GridSplitter Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="Gray"
|
||||
ShowsPreview="true"
|
||||
Height="5">
|
||||
<GridSplitter.Template>
|
||||
<ControlTemplate>
|
||||
<Separator/>
|
||||
</ControlTemplate>
|
||||
</GridSplitter.Template>
|
||||
</GridSplitter>
|
||||
<Grid Grid.Row="2">
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="20"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Background="LightGray"/>
|
||||
|
||||
<StackPanel HorizontalAlignment="Left"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<TextBlock Text="Logger"
|
||||
Margin="5"/>
|
||||
|
||||
<Border Width="1"
|
||||
Margin="5"
|
||||
Background="Gray"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal">
|
||||
|
||||
<TextBlock Text="Rx"
|
||||
Margin="5"/>
|
||||
|
||||
<Ellipse IsEnabled="{Binding AppViewModel.Plc.IsReceiving, Mode=OneWay}"
|
||||
Fill="Lime"
|
||||
Width="10"
|
||||
Height="10">
|
||||
<Ellipse.Style>
|
||||
<Style TargetType="Ellipse">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value=".1"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Ellipse.Style>
|
||||
</Ellipse>
|
||||
|
||||
<TextBlock Text="Tx"
|
||||
Margin="5"/>
|
||||
|
||||
<Ellipse Fill="Orange"
|
||||
IsEnabled="{Binding AppViewModel.Plc.IsSending, Mode=OneWay}"
|
||||
Width="10"
|
||||
Height="10">
|
||||
<Ellipse.Style>
|
||||
<Style TargetType="Ellipse">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value=".1"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Ellipse.Style>
|
||||
</Ellipse>
|
||||
|
||||
<Border Width="1"
|
||||
Margin="5"
|
||||
Background="Gray"/>
|
||||
|
||||
<TextBlock Text="{Binding AppViewModel.Plc.BytesPerSecondDownstream, Mode=OneWay}"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<Border Width="1"
|
||||
Margin="5"
|
||||
Background="Gray"/>
|
||||
|
||||
<TextBlock Text="{Binding AppViewModel.Plc.BytesPerSecondUpstream, Mode=OneWay}"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<Border Width="1"
|
||||
Margin="5"
|
||||
Background="Gray"/>
|
||||
|
||||
<TextBlock Text="{Binding AppViewModel.Plc.QueuedMessages, StringFormat='{}Q: {0}', Mode=OneWay}"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<StackPanel.Style>
|
||||
<Style TargetType="StackPanel">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding AppViewModel.PlcIsNull}" Value="True">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</StackPanel.Style>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Right">
|
||||
|
||||
<Button Content="Expand"
|
||||
Margin="2"/>
|
||||
|
||||
<ToggleButton Content="Autoscroll"
|
||||
IsChecked="True"
|
||||
Margin="2"
|
||||
x:Name="autoScrollBtn"/>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<ListBox Grid.Row="1"
|
||||
Background="Black"
|
||||
Foreground="White"
|
||||
BorderThickness="0"
|
||||
VirtualizingPanel.IsVirtualizing="true"
|
||||
VirtualizingPanel.VirtualizationMode="Recycling"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Disabled"
|
||||
ItemsSource="{Binding LoggerItems, Mode=OneWay}"
|
||||
x:Name="loggerList">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Vertical"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<ContentPresenter Content="{Binding}"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<Border Background="Black"
|
||||
Grid.Row="2">
|
||||
<TextBlock Text=">"
|
||||
Foreground="White"
|
||||
Margin="5,0,0,0"/>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
57
Examples/Examples.WPF/MainWindow.xaml.cs
Normal file
57
Examples/Examples.WPF/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using Examples.WPF.ViewModels;
|
||||
using Examples.WPF.Views;
|
||||
using MewtocolNet;
|
||||
using MewtocolNet.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace Examples.WPF;
|
||||
|
||||
public partial class MainWindow : Window {
|
||||
|
||||
public ObservableCollection<TextBlock> LoggerItems => App.LoggerItems;
|
||||
|
||||
public AppViewModel AppViewModel => App.ViewModel;
|
||||
|
||||
public MainWindow() {
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
this.DataContext = this;
|
||||
App.MainWindow = this;
|
||||
|
||||
mainContent.Content = new ConnectView();
|
||||
|
||||
loggerList.PreviewMouseWheel += (s, e) => {
|
||||
|
||||
autoScrollBtn.IsChecked = false;
|
||||
|
||||
};
|
||||
|
||||
App.LogEventProcessed += () => {
|
||||
|
||||
Application.Current.Dispatcher.BeginInvoke(() => {
|
||||
|
||||
if (autoScrollBtn?.IsChecked != null && autoScrollBtn.IsChecked.Value)
|
||||
loggerList.ScrollIntoView(App.LoggerItems.Last());
|
||||
|
||||
}, System.Windows.Threading.DispatcherPriority.Send);
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using MewtocolNet;
|
||||
using MewtocolNet.RegisterAttributes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Examples.WPF.RegisterCollections;
|
||||
|
||||
public class TestRegisterCollection : RegisterCollection {
|
||||
|
||||
[Register("R11A")]
|
||||
public bool? TestR11A { get; set; }
|
||||
|
||||
[Register("R11A")]
|
||||
public bool TestR11A_Duplicate_NonNullable { get; set; }
|
||||
|
||||
[Register("R16B")]
|
||||
public bool TestR16B { get; set; }
|
||||
|
||||
[Register("R902")]
|
||||
public bool Test { get; set; }
|
||||
|
||||
[BitRegister("DT1000", 0), PollLevel(3)]
|
||||
public bool? TestDT100_Word_Duplicate_SingleBit { get; set; }
|
||||
|
||||
[Register("DT1000")]
|
||||
public Word TestDT100_Word_Duplicate { get; set; }
|
||||
|
||||
[BitRegister("DDT1010", 1)]
|
||||
public bool? TestDDT1010_DWord_Duplicate_SingleBit { get; set; }
|
||||
|
||||
}
|
||||
40
Examples/Examples.WPF/ViewModels/AppViewModel.cs
Normal file
40
Examples/Examples.WPF/ViewModels/AppViewModel.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Examples.WPF.RegisterCollections;
|
||||
using MewtocolNet;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Examples.WPF.ViewModels {
|
||||
|
||||
public class AppViewModel : ViewModelBase {
|
||||
|
||||
private IPlc? plc;
|
||||
private TestRegisterCollection testRegCollection = null!;
|
||||
|
||||
public bool PlcIsNull => plc == null;
|
||||
|
||||
public bool PlcIsNotNull => plc != null;
|
||||
|
||||
public IPlc? Plc {
|
||||
get => plc;
|
||||
set {
|
||||
plc = value;
|
||||
OnPropChange();
|
||||
OnPropChange(nameof(PlcIsNull));
|
||||
OnPropChange(nameof(PlcIsNotNull));
|
||||
}
|
||||
}
|
||||
|
||||
public TestRegisterCollection TestRegCollection {
|
||||
get => testRegCollection;
|
||||
set {
|
||||
testRegCollection = value;
|
||||
OnPropChange();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
117
Examples/Examples.WPF/ViewModels/ConnectViewViewModel.cs
Normal file
117
Examples/Examples.WPF/ViewModels/ConnectViewViewModel.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration.Internal;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Threading;
|
||||
using MewtocolNet;
|
||||
using MewtocolNet.ComCassette;
|
||||
|
||||
namespace Examples.WPF.ViewModels;
|
||||
|
||||
internal class ConnectViewViewModel : ViewModelBase {
|
||||
|
||||
private bool hasComports = false;
|
||||
private IEnumerable<int> baudRates = null!;
|
||||
private IEnumerable<string> comPorts = null!;
|
||||
private IEnumerable<CassetteInformation> foundCassettes = null!;
|
||||
|
||||
private string selectedIP = "192.168.115.210";
|
||||
private string selectedPort = "9094";
|
||||
private bool isConnecting;
|
||||
|
||||
public IEnumerable<int> BaudRates {
|
||||
get => baudRates;
|
||||
set {
|
||||
baudRates = value;
|
||||
OnPropChange();
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> ComPorts {
|
||||
get => comPorts;
|
||||
set {
|
||||
comPorts = value;
|
||||
OnPropChange();
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<CassetteInformation> FoundCassettes {
|
||||
get => foundCassettes;
|
||||
set {
|
||||
foundCassettes = value;
|
||||
OnPropChange();
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasComports {
|
||||
get => hasComports;
|
||||
set {
|
||||
hasComports = value;
|
||||
OnPropChange();
|
||||
}
|
||||
}
|
||||
|
||||
public string SelectedIP {
|
||||
get { return selectedIP; }
|
||||
set {
|
||||
selectedIP = value;
|
||||
OnPropChange();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public string SelectedPort {
|
||||
get { return selectedPort; }
|
||||
set {
|
||||
selectedPort = value;
|
||||
OnPropChange();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsConnecting {
|
||||
get { return isConnecting; }
|
||||
set {
|
||||
isConnecting = value;
|
||||
OnPropChange();
|
||||
}
|
||||
}
|
||||
|
||||
private DispatcherTimer tm;
|
||||
|
||||
public ConnectViewViewModel() {
|
||||
|
||||
BaudRates = Mewtocol.GetUseableBaudRates();
|
||||
ScanTimerTick(null, null!);
|
||||
|
||||
tm = new DispatcherTimer {
|
||||
Interval = TimeSpan.FromSeconds(3),
|
||||
};
|
||||
tm.Tick += ScanTimerTick;
|
||||
|
||||
tm.Start();
|
||||
|
||||
}
|
||||
|
||||
private async void ScanTimerTick(object? sender, EventArgs e) {
|
||||
|
||||
ComPorts = Mewtocol.GetSerialPortNames();
|
||||
HasComports = ComPorts != null && ComPorts.Count() > 0;
|
||||
|
||||
var found = await CassetteFinder.FindClientsAsync(timeoutMs: 1000);
|
||||
if (FoundCassettes == null || !Enumerable.SequenceEqual(found, FoundCassettes))
|
||||
FoundCassettes = found;
|
||||
|
||||
}
|
||||
|
||||
internal void SelectedCassette (CassetteInformation cassette) {
|
||||
|
||||
SelectedIP = cassette.IPAddress.ToString();
|
||||
SelectedPort = cassette.Port.ToString();
|
||||
|
||||
}
|
||||
|
||||
internal void EndTimer() => tm.Stop();
|
||||
|
||||
}
|
||||
36
Examples/Examples.WPF/ViewModels/PlcDataViewViewModel.cs
Normal file
36
Examples/Examples.WPF/ViewModels/PlcDataViewViewModel.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Examples.WPF.RegisterCollections;
|
||||
using MewtocolNet;
|
||||
using MewtocolNet.Events;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Examples.WPF.ViewModels;
|
||||
|
||||
public class PlcDataViewViewModel : ViewModelBase {
|
||||
|
||||
private ReconnectArgs plcCurrentReconnectArgs = null!;
|
||||
|
||||
public IPlc Plc => App.ViewModel.Plc!;
|
||||
|
||||
public TestRegisterCollection RegCollection => App.ViewModel.TestRegCollection;
|
||||
|
||||
public ReconnectArgs PlcCurrentReconnectArgs {
|
||||
get => plcCurrentReconnectArgs;
|
||||
set {
|
||||
plcCurrentReconnectArgs = value;
|
||||
OnPropChange();
|
||||
}
|
||||
}
|
||||
|
||||
public PlcDataViewViewModel () {
|
||||
|
||||
Plc.ReconnectTryStarted += (s, e) => PlcCurrentReconnectArgs = e;
|
||||
Plc.Reconnected += (s, e) => PlcCurrentReconnectArgs = null!;
|
||||
Plc.Disconnected += (s, e) => PlcCurrentReconnectArgs = null!;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
18
Examples/Examples.WPF/ViewModels/ViewModelBase.cs
Normal file
18
Examples/Examples.WPF/ViewModels/ViewModelBase.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Examples.WPF.ViewModels;
|
||||
|
||||
public abstract class ViewModelBase : INotifyPropertyChanged {
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public void PropChange(string _name) {
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(_name));
|
||||
}
|
||||
|
||||
protected void OnPropChange([CallerMemberName] string propertyName = null) {
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
}
|
||||
129
Examples/Examples.WPF/Views/ConnectView.xaml
Normal file
129
Examples/Examples.WPF/Views/ConnectView.xaml
Normal file
@@ -0,0 +1,129 @@
|
||||
<UserControl x:Class="Examples.WPF.Views.ConnectView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Examples.WPF.Views"
|
||||
xmlns:vm="clr-namespace:Examples.WPF.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance vm:ConnectViewViewModel, IsDesignTimeCreatable=True}"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<Grid Margin="10">
|
||||
<StackPanel>
|
||||
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="Padding" Value="5"/>
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
|
||||
<StackPanel>
|
||||
<TextBlock Text="Connect to a PLC"
|
||||
FontSize="24"/>
|
||||
<Label Content="Set your connection type"/>
|
||||
<ComboBox SelectedIndex="0" x:Name="conTypeCombo">
|
||||
<ComboBoxItem>Ethernet</ComboBoxItem>
|
||||
<ComboBoxItem IsEnabled="{Binding HasComports}">Serial</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<Separator/>
|
||||
|
||||
<StackPanel MinWidth="200">
|
||||
|
||||
<TextBlock Text="Cassettes"/>
|
||||
|
||||
<DataGrid ItemsSource="{Binding FoundCassettes}"
|
||||
SelectionChanged="SelectedCassette"
|
||||
AutoGenerateColumns="False"
|
||||
MinHeight="150"
|
||||
MaxHeight="200"
|
||||
IsReadOnly="True">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width=".5*"/>
|
||||
<DataGridTextColumn Header="IP" Binding="{Binding IPAddress}" Width=".3*"/>
|
||||
<DataGridTextColumn Header="Port" Binding="{Binding Port}" Width=".3*"/>
|
||||
<DataGridCheckBoxColumn Header="DHCP" Binding="{Binding UsesDHCP}" Width="auto"/>
|
||||
<DataGridTextColumn Header="MAC" Binding="{Binding MacAddressStr, Mode=OneWay}" Width="auto"/>
|
||||
<DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="auto"/>
|
||||
</DataGrid.Columns>
|
||||
<DataGrid.RowStyle>
|
||||
<Style TargetType="{x:Type DataGridRow}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Port}" Value="0">
|
||||
<Setter Property="IsEnabled" Value="False"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</DataGrid.RowStyle>
|
||||
</DataGrid>
|
||||
|
||||
<TextBlock Text="Connection"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Label Content="IP Address"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBox Text="{Binding SelectedIP}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Label Content="Port"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBox Text="{Binding SelectedPort}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button Content="Connect"
|
||||
Click="ClickedConnectEth"
|
||||
VerticalAlignment="Center"
|
||||
Padding="5"
|
||||
Margin="10,0,0,0">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnecting}">
|
||||
<Setter Property="IsEnabled" Value="False"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel.Style>
|
||||
<Style TargetType="StackPanel">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding SelectedIndex, ElementName=conTypeCombo}" Value="0">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</StackPanel.Style>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel MinWidth="200"
|
||||
HorizontalAlignment="Left">
|
||||
|
||||
<StackPanel Orientation="Vertical">
|
||||
<Label Content="COM Port"/>
|
||||
<ComboBox ItemsSource="{Binding ComPorts}"
|
||||
SelectedIndex="0"/>
|
||||
<Label Content="BaudRate"/>
|
||||
<ComboBox ItemsSource="{Binding BaudRates}"
|
||||
SelectedIndex="0"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel.Style>
|
||||
<Style TargetType="StackPanel">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding SelectedIndex, ElementName=conTypeCombo}" Value="1">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</StackPanel.Style>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
161
Examples/Examples.WPF/Views/ConnectView.xaml.cs
Normal file
161
Examples/Examples.WPF/Views/ConnectView.xaml.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
using Examples.WPF.RegisterCollections;
|
||||
using Examples.WPF.ViewModels;
|
||||
using MewtocolNet;
|
||||
using MewtocolNet.ComCassette;
|
||||
using MewtocolNet.Logging;
|
||||
using MewtocolNet.Registers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace Examples.WPF.Views;
|
||||
|
||||
/// <summary>
|
||||
/// Interaktionslogik für ConnectView.xaml
|
||||
/// </summary>
|
||||
public partial class ConnectView : UserControl {
|
||||
|
||||
private ConnectViewViewModel viewModel;
|
||||
|
||||
public ConnectView() {
|
||||
|
||||
InitializeComponent();
|
||||
viewModel = new ConnectViewViewModel();
|
||||
this.DataContext = viewModel;
|
||||
|
||||
Unloaded += (s, e) => viewModel.EndTimer();
|
||||
|
||||
}
|
||||
|
||||
private void SelectedCassette(object sender, SelectionChangedEventArgs e) {
|
||||
|
||||
var cassette = (CassetteInformation)((DataGrid)sender).SelectedItem;
|
||||
if (cassette == null) return;
|
||||
|
||||
viewModel.SelectedCassette(cassette);
|
||||
|
||||
}
|
||||
|
||||
private void ClickedConnectEth(object sender, RoutedEventArgs e) {
|
||||
|
||||
Application.Current.Dispatcher.BeginInvoke(async () => {
|
||||
|
||||
viewModel.IsConnecting = true;
|
||||
|
||||
var parsedInt = int.Parse(viewModel.SelectedPort);
|
||||
|
||||
IRegister<short> heartbeatSetter = null!;
|
||||
IRegister<bool> outputContactReference = null!;
|
||||
IRegister<bool> testBoolReference = null!;
|
||||
IRegister<Word> wordRefTest = null!;
|
||||
|
||||
//build a new interface
|
||||
App.ViewModel.Plc = Mewtocol.Ethernet(viewModel.SelectedIP, parsedInt)
|
||||
.WithPoller()
|
||||
.WithInterfaceSettings(setting => {
|
||||
|
||||
setting.TryReconnectAttempts = 10;
|
||||
setting.TryReconnectDelayMs = 2000;
|
||||
setting.SendReceiveTimeoutMs = 1000;
|
||||
setting.HeartbeatIntervalMs = 3000;
|
||||
setting.MaxDataBlocksPerWrite = 128;
|
||||
setting.MaxOptimizationDistance = 20;
|
||||
|
||||
})
|
||||
.WithCustomPollLevels(lvl => {
|
||||
|
||||
lvl.SetLevel(2, 3);
|
||||
lvl.SetLevel(3, TimeSpan.FromSeconds(5));
|
||||
lvl.SetLevel(4, TimeSpan.FromSeconds(10));
|
||||
|
||||
})
|
||||
.WithRegisterCollections(collector => {
|
||||
|
||||
App.ViewModel.TestRegCollection = collector.AddCollection<TestRegisterCollection>();
|
||||
|
||||
})
|
||||
.WithRegisters(b => {
|
||||
|
||||
b.Bool("X4").Build();
|
||||
b.Bool("Y4").Build(out outputContactReference);
|
||||
b.Bool("R10A").PollLevel(PollLevel.FirstIteration).Build(out testBoolReference);
|
||||
|
||||
b.Struct<short>("DT1000").Build(out heartbeatSetter);
|
||||
|
||||
//these will be merged into one
|
||||
b.Struct<Word>("DT1000").Build(out wordRefTest);
|
||||
b.Struct<Word>("DT1000").Build(out wordRefTest);
|
||||
|
||||
b.Struct<ushort>("DT1001").PollLevel(2).Build();
|
||||
b.Struct<Word>("DT1002").PollLevel(2).Build();
|
||||
|
||||
b.Struct<int>("DDT1010").PollLevel(2).Build();
|
||||
b.Struct<uint>("DDT1012").PollLevel(2).Build();
|
||||
b.Struct<DWord>("DDT1014").PollLevel(2).Build();
|
||||
b.Struct<float>("DDT1016").PollLevel(2).Build();
|
||||
b.Struct<TimeSpan>("DDT1018").PollLevel(2).Build();
|
||||
|
||||
b.Struct<DateAndTime>("DDT1020").PollLevel(2).Build();
|
||||
b.Struct<DateAndTime>("DDT1022").PollLevel(2).Build();
|
||||
|
||||
b.String("DT1028", 32).PollLevel(3).Build();
|
||||
b.String("DT1046", 5).PollLevel(4).Build();
|
||||
|
||||
b.Struct<Word>("DT1000").AsArray(5).PollLevel(1).Build();
|
||||
|
||||
})
|
||||
.WithHeartbeatTask(async (plc) => {
|
||||
|
||||
var randShort = (short)new Random().Next(short.MinValue, short.MaxValue);
|
||||
|
||||
//write direct
|
||||
//await heartbeatSetter.WriteAsync(randShort);
|
||||
//or by anonymous
|
||||
await plc.Register.Struct<short>("DT1000").WriteAsync(randShort);
|
||||
|
||||
//write a register without a reference
|
||||
bool randBool = new Random().Next(0, 2) == 1;
|
||||
await plc.Register.Bool("Y4").WriteAsync(randBool);
|
||||
|
||||
if (testBoolReference.Value != null)
|
||||
await testBoolReference.WriteAsync(!testBoolReference.Value.Value);
|
||||
|
||||
await plc.Register.Struct<DateAndTime>("DDT1022").WriteAsync(DateAndTime.FromDateTime(DateTime.UtcNow));
|
||||
|
||||
})
|
||||
.Build();
|
||||
|
||||
//connect to it
|
||||
await App.ViewModel.Plc.ConnectAsync(async () => {
|
||||
|
||||
await App.ViewModel.Plc.RestartProgramAsync();
|
||||
|
||||
});
|
||||
|
||||
await App.ViewModel.Plc.AwaitFirstDataCycleAsync();
|
||||
|
||||
if (App.ViewModel.Plc.IsConnected) {
|
||||
|
||||
App.MainWindow.mainContent.Content = new PlcDataView();
|
||||
|
||||
}
|
||||
|
||||
viewModel.IsConnecting = false;
|
||||
|
||||
}, System.Windows.Threading.DispatcherPriority.Send);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
350
Examples/Examples.WPF/Views/PlcDataView.xaml
Normal file
350
Examples/Examples.WPF/Views/PlcDataView.xaml
Normal file
@@ -0,0 +1,350 @@
|
||||
<UserControl x:Class="Examples.WPF.Views.PlcDataView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:vm="clr-namespace:Examples.WPF.ViewModels"
|
||||
xmlns:local="clr-namespace:Examples.WPF.Views"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance vm:PlcDataViewViewModel, IsDesignTimeCreatable=True}"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800">
|
||||
<Grid>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Menu>
|
||||
<MenuItem Header="PLC">
|
||||
<MenuItem Header="Disconnect" IsEnabled="{Binding Plc.IsConnected}"
|
||||
Click="ClickedDisconnect"/>
|
||||
<MenuItem Header="Connect" IsEnabled="{Binding Plc.IsConnected, Converter={StaticResource bInv}}"
|
||||
Click="ClickedConnect"/>
|
||||
<MenuItem Header="Set Random DT1000" IsEnabled="{Binding Plc.IsConnected}"
|
||||
Click="ClickedSetRandom"/>
|
||||
<MenuItem Header="Queue test" IsEnabled="{Binding Plc.IsConnected}"
|
||||
Click="ClickedAddQueueTest"/>
|
||||
<MenuItem Header="Toggle OP mode" IsEnabled="{Binding Plc.IsConnected}"
|
||||
Click="ClickedToggleRunMode"/>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
<StackPanel Margin="10"
|
||||
Grid.Row="1">
|
||||
|
||||
<TextBlock IsEnabled="{Binding Plc.IsConnected}">
|
||||
|
||||
<Run Text="{Binding Plc.PlcInfo.TypeName, Mode=OneWay}"
|
||||
FontSize="24"
|
||||
BaselineAlignment="Center"
|
||||
FontWeight="SemiBold"/>
|
||||
|
||||
<Run Text="{Binding Plc.PlcInfo.CpuVersion, StringFormat='v{0}', Mode=OneWay}"
|
||||
FontSize="24"
|
||||
FontWeight="Light"/>
|
||||
|
||||
<Ellipse Width="10"
|
||||
Height="10"
|
||||
Fill="Lime"
|
||||
IsEnabled="{Binding Plc.IsConnected}"/>
|
||||
|
||||
<Run>
|
||||
<Run.Style>
|
||||
<Style TargetType="Run">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Plc.IsRunMode, Mode=OneWay}" Value="True">
|
||||
<Setter Property="Text" Value="RUN MODE"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Plc.IsRunMode, Mode=OneWay}" Value="False">
|
||||
<Setter Property="Text" Value="NO RUN MODE"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Run.Style>
|
||||
</Run>
|
||||
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock IsEnabled="{Binding Plc.IsConnected, Converter={StaticResource bInv}}">
|
||||
|
||||
<Run Text="Disconnected"
|
||||
FontSize="24"
|
||||
BaselineAlignment="Center"
|
||||
FontWeight="SemiBold"/>
|
||||
|
||||
<Ellipse Width="10"
|
||||
Height="10"
|
||||
Fill="Red"
|
||||
IsEnabled="{Binding Plc.IsConnected}"/>
|
||||
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock Text="{Binding Plc.PlcInfo.TypeCode, StringFormat='#{0:X}', Mode=OneWay}"
|
||||
FontSize="18"/>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding Plc.PlcInfo.OperationModeTags, Mode=OneWay}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="LightGray"
|
||||
CornerRadius="5"
|
||||
Margin="2"
|
||||
Padding="5">
|
||||
<TextBlock Text="{Binding}"/>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding Plc.PlcInfo.HardwareInformationTags, Mode=OneWay}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="LightGray"
|
||||
CornerRadius="5"
|
||||
Margin="2"
|
||||
Padding="5">
|
||||
<TextBlock Text="{Binding}"/>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock>
|
||||
|
||||
<Run Text="Reconnecting..."/>
|
||||
<Run Text="{Binding PlcCurrentReconnectArgs.ReconnectTry, Mode=OneWay, StringFormat='{}{0}/'}"/>
|
||||
<Run Text="{Binding PlcCurrentReconnectArgs.MaxAttempts, Mode=OneWay}"/> in
|
||||
<Run Text="{Binding PlcCurrentReconnectArgs.RetryCountDownRemaining, Mode=OneWay}"/>
|
||||
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding PlcCurrentReconnectArgs, Mode=OneWay}" Value="{x:Null}">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
|
||||
</TextBlock>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Row="2">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1.5*"/>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<GridSplitter Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="3"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="Gray"
|
||||
ShowsPreview="true"
|
||||
Width="10">
|
||||
<GridSplitter.Template>
|
||||
<ControlTemplate>
|
||||
<Rectangle Fill="Gray"
|
||||
Width="2"
|
||||
Margin="2"/>
|
||||
</ControlTemplate>
|
||||
</GridSplitter.Template>
|
||||
</GridSplitter>
|
||||
|
||||
<TextBlock Text="Underlying Registers"
|
||||
FontSize="18"
|
||||
Margin="10"/>
|
||||
|
||||
<DataGrid Grid.Row="1"
|
||||
Grid.RowSpan="3"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
ItemsSource="{Binding Plc.Registers, Mode=OneWay}">
|
||||
<DataGrid.RowStyle>
|
||||
<Style TargetType="DataGridRow">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsAutoGenerated}" Value="True">
|
||||
<Setter Property="IsEnabled" Value="False"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</DataGrid.RowStyle>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="FP Address" Binding="{Binding PLCAddressName}"/>
|
||||
<DataGridTextColumn Header="Type" Binding="{Binding UnderlyingSystemType.Name}"/>
|
||||
<DataGridTemplateColumn Header="Value" Width="*">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Border HorizontalAlignment="Left"
|
||||
Padding="2,0">
|
||||
<TextBlock Text="{Binding ValueStr}"/>
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ValueStr}" Value="True">
|
||||
<Setter Property="Background" Value="Blue"/>
|
||||
<Setter Property="TextElement.Foreground" Value="White"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn Header="Poll Level" Binding="{Binding PollLevel, Mode=OneWay}"/>
|
||||
<DataGridTextColumn Header="Update Frequency" Binding="{Binding UpdateFreqHz, StringFormat='{}{0} Hz',Mode=OneWay}"/>
|
||||
<DataGridTemplateColumn Width="15">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="{Binding MemoryArea, Mode=OneWay, Converter={StaticResource hashColor}}"/>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<TextBlock Text="Memory Areas"
|
||||
Grid.Column="2"
|
||||
FontSize="18"
|
||||
Margin="10"/>
|
||||
|
||||
<Border Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch"
|
||||
BorderBrush="LightBlue"
|
||||
BorderThickness="1.5">
|
||||
|
||||
<DataGrid IsReadOnly="True"
|
||||
AutoGenerateColumns="False"
|
||||
ItemsSource="{Binding Plc.MemoryAreas, Mode=OneWay}">
|
||||
|
||||
<DataGrid.Columns>
|
||||
|
||||
<DataGridTextColumn Header="Address Range" Binding="{Binding AddressRange, Mode=OneWay}"/>
|
||||
|
||||
<DataGridTemplateColumn Width="15">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="{Binding Converter={StaticResource hashColor}}"/>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTextColumn Header="Words" Binding="{Binding UnderlyingWordsString, Mode=OneWay}"/>
|
||||
|
||||
</DataGrid.Columns>
|
||||
|
||||
</DataGrid>
|
||||
|
||||
</Border>
|
||||
|
||||
<TextBlock Text="Property Bindings"
|
||||
Grid.Column="2"
|
||||
Grid.Row="2"
|
||||
FontSize="18"
|
||||
Margin="10"/>
|
||||
|
||||
<Border Grid.Column="2"
|
||||
Grid.Row="3"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch"
|
||||
BorderBrush="LightBlue"
|
||||
BorderThickness="1.5">
|
||||
|
||||
<ScrollViewer>
|
||||
|
||||
<StackPanel>
|
||||
|
||||
<TextBlock Text="Boolean internal relays"/>
|
||||
|
||||
<TextBlock>
|
||||
<Run Text="R11A Nullable property: "/>
|
||||
<Run Text="{Binding RegCollection.TestR11A}"
|
||||
FontWeight="Bold"/>
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock>
|
||||
<Run Text="R11A Duplicate non nullable property: "/>
|
||||
<Run Text="{Binding RegCollection.TestR11A_Duplicate_NonNullable}"
|
||||
FontWeight="Bold"/>
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock>
|
||||
<Run Text="R16B"/>
|
||||
<Run Text="{Binding RegCollection.TestR16B}"
|
||||
FontWeight="Bold"/>
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock>
|
||||
<Run Text="DT1000 Word duplicate: "/>
|
||||
<Run Text="{Binding RegCollection.TestDT100_Word_Duplicate, Mode=OneWay}"
|
||||
FontWeight="Bold"/>
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock>
|
||||
<Run Text="DT1000 Word direct bit read (Idx 0): "/>
|
||||
<Run Text="{Binding RegCollection.TestDT100_Word_Duplicate_SingleBit, Mode=OneWay}"
|
||||
FontWeight="Bold"/>
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock>
|
||||
<Run Text="DDT1010 DWord direct bit read (Idx 1): "/>
|
||||
<Run Text="{Binding RegCollection.TestDDT1010_DWord_Duplicate_SingleBit, Mode=OneWay}"
|
||||
FontWeight="Bold"/>
|
||||
</TextBlock>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</ScrollViewer>
|
||||
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
81
Examples/Examples.WPF/Views/PlcDataView.xaml.cs
Normal file
81
Examples/Examples.WPF/Views/PlcDataView.xaml.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using Examples.WPF.ViewModels;
|
||||
using MewtocolNet.Registers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace Examples.WPF.Views;
|
||||
|
||||
public partial class PlcDataView : UserControl {
|
||||
|
||||
private PlcDataViewViewModel viewModel;
|
||||
|
||||
public PlcDataView() {
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
viewModel = new PlcDataViewViewModel();
|
||||
this.DataContext = viewModel;
|
||||
|
||||
}
|
||||
|
||||
private void ClickedDisconnect(object sender, RoutedEventArgs e) {
|
||||
|
||||
viewModel.Plc.Disconnect();
|
||||
|
||||
}
|
||||
|
||||
private async void ClickedConnect(object sender, RoutedEventArgs e) {
|
||||
|
||||
await viewModel.Plc.ConnectAsync();
|
||||
|
||||
}
|
||||
|
||||
private async void ClickedSetRandom(object sender, RoutedEventArgs e) {
|
||||
|
||||
var reg = (IRegister<ushort>?)viewModel.Plc.GetAllRegisters()?.FirstOrDefault(x => x.PLCAddressName == "DT1001");
|
||||
|
||||
if(reg != null) {
|
||||
|
||||
await reg.WriteAsync((ushort)new Random().Next(ushort.MinValue, ushort.MaxValue));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async void ClickedAddQueueTest(object sender, RoutedEventArgs e) {
|
||||
|
||||
var tasks = new List<Task<short>>();
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
|
||||
var t = viewModel.Plc.Register.Struct<short>("DT1000").ReadAsync();
|
||||
|
||||
tasks.Add(t);
|
||||
|
||||
}
|
||||
|
||||
var list = await Task.WhenAll(tasks);
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
}
|
||||
|
||||
private async void ClickedToggleRunMode(object sender, RoutedEventArgs e) {
|
||||
|
||||
await viewModel.Plc.ToggleOperationModeAsync();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MewtocolNet\MewtocolNet.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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<MethodInfo>();
|
||||
|
||||
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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Examples;
|
||||
|
||||
public class ScenarioAttribute : Attribute {
|
||||
|
||||
public string Description { get; private set; }
|
||||
|
||||
public ScenarioAttribute(string description) {
|
||||
|
||||
Description = description;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
19
MewTerminal/Commands/ClearCommand.cs
Normal file
19
MewTerminal/Commands/ClearCommand.cs
Normal file
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
20
MewTerminal/Commands/CommandLineExcecuteable.cs
Normal file
20
MewTerminal/Commands/CommandLineExcecuteable.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
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;
|
||||
|
||||
public virtual void Run() { }
|
||||
|
||||
public virtual Task RunAsync () => Task.CompletedTask;
|
||||
|
||||
}
|
||||
35
MewTerminal/Commands/ListSupportCommand.cs
Normal file
35
MewTerminal/Commands/ListSupportCommand.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
using CommandLine;
|
||||
using MewtocolNet;
|
||||
using MewtocolNet.ComCassette;
|
||||
using MewtocolNet.Logging;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace MewTerminal.Commands;
|
||||
|
||||
[Verb("support-list", HelpText = "Lists all supported PLC types")]
|
||||
internal class ListSupportCommand : CommandLineExcecuteable {
|
||||
|
||||
public override void Run () {
|
||||
|
||||
var plcs = Enum.GetValues<PlcType>().Cast<PlcType>();
|
||||
|
||||
var lst = new List<ParsedPlcName>();
|
||||
|
||||
foreach (var plcT in plcs) {
|
||||
|
||||
var decomp = plcT.ToNameDecompose();
|
||||
|
||||
if (decomp == null) continue;
|
||||
|
||||
lst.Add(decomp);
|
||||
|
||||
}
|
||||
|
||||
AnsiConsole.Write(lst.ToTable());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
46
MewTerminal/Commands/OnlineCommands/OnlineCommand.cs
Normal file
46
MewTerminal/Commands/OnlineCommands/OnlineCommand.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using CommandLine;
|
||||
using MewtocolNet;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace MewTerminal.Commands.OnlineCommands;
|
||||
|
||||
internal class OnlineCommand : CommandLineExcecuteable {
|
||||
|
||||
[Option('e', "ethernet", Default = "127.0.0.1:9094", HelpText = "Ethernet config")]
|
||||
public string? EthernetStr { get; set; }
|
||||
|
||||
[Option('s', "serial", Default = null, HelpText = "Serial port config")]
|
||||
public string? SerialStr { get; set; }
|
||||
|
||||
public override async Task RunAsync() {
|
||||
|
||||
try {
|
||||
|
||||
if (!string.IsNullOrEmpty(SerialStr)) {
|
||||
|
||||
} else {
|
||||
|
||||
var split = EthernetStr.Split(":");
|
||||
|
||||
string ip = split[0];
|
||||
int port = int.Parse(split[1]);
|
||||
|
||||
using (var plc = Mewtocol.Ethernet(ip, port).Build()) {
|
||||
|
||||
await AfterSetup(plc);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} catch (Exception ex) {
|
||||
|
||||
AnsiConsole.WriteLine($"[red]{ex.Message.ToString()}[/]");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal virtual async Task AfterSetup(IPlc plc) => throw new NotImplementedException();
|
||||
|
||||
}
|
||||
102
MewTerminal/Commands/ScanCommand.cs
Normal file
102
MewTerminal/Commands/ScanCommand.cs
Normal file
@@ -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).Build();
|
||||
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());
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
65
MewTerminal/Helpers/Helpers.cs
Normal file
65
MewTerminal/Helpers/Helpers.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using Spectre.Console;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MewTerminal;
|
||||
|
||||
internal static class Helpers {
|
||||
|
||||
internal static Table ToTable<T> (this IEnumerable<T> 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<string>();
|
||||
|
||||
foreach (var prop in props) {
|
||||
|
||||
if(isFirst) table.AddColumn(prop.Name.SplitCamelCase());
|
||||
|
||||
var propVal = prop.GetValue(item);
|
||||
|
||||
string strVal = propVal?.ToString() ?? "null";
|
||||
|
||||
if (propVal is byte[] bArr) {
|
||||
strVal = string.Join(" ", bArr.Select(x => x.ToString("X2")));
|
||||
}
|
||||
|
||||
if (propVal is string[] sArr) {
|
||||
strVal = string.Join(", ", sArr);
|
||||
}
|
||||
|
||||
strVal = strVal.Replace("[", "");
|
||||
strVal = strVal.Replace("]", "");
|
||||
|
||||
rowVals.Add(strVal);
|
||||
|
||||
}
|
||||
|
||||
isFirst = false;
|
||||
|
||||
table.AddRow(rowVals.ToArray());
|
||||
|
||||
}
|
||||
|
||||
return table;
|
||||
|
||||
}
|
||||
|
||||
private static string SplitCamelCase (this string str) {
|
||||
|
||||
return Regex.Replace(Regex.Replace(str, @"(\P{Ll})(\P{Ll}\p{Ll})", "$1 $2"), @"(\p{Ll})(\P{Ll})", "$1 $2");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
39
MewTerminal/MewTerminal.csproj
Normal file
39
MewTerminal/MewTerminal.csproj
Normal file
@@ -0,0 +1,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PublishAot>true</PublishAot>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<NoWarn>IL2026,IL2027,IL2090</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageReference Include="Spectre.Console" Version="0.47.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MewtocolNet\MewtocolNet.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||
|
||||
<AssemblyName>mewcmd</AssemblyName>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
|
||||
<TrimMode>partial</TrimMode>
|
||||
<SelfContained>True</SelfContained>
|
||||
<DebugType>None</DebugType>
|
||||
<DebugSymbols>False</DebugSymbols>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
<AllowedReferenceRelatedFileExtensions>none</AllowedReferenceRelatedFileExtensions>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
<OutputPath>..\Builds\MewTerminal</OutputPath>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
93
MewTerminal/Program.cs
Normal file
93
MewTerminal/Program.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using CommandLine;
|
||||
using CommandLine.Text;
|
||||
using MewTerminal.Commands;
|
||||
using MewTerminal.Commands.OnlineCommands;
|
||||
using MewtocolNet.Logging;
|
||||
using Spectre.Console;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MewTerminal;
|
||||
|
||||
internal class Program {
|
||||
|
||||
static void Main(string[] args) {
|
||||
|
||||
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
|
||||
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
|
||||
|
||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
|
||||
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<VerbAttribute>() != 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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
18
MewTerminal/app.manifest
Normal file
18
MewTerminal/app.manifest
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="mewterminal.app"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
</assembly>
|
||||
142
MewtocolNet.sln
142
MewtocolNet.sln
@@ -5,9 +5,27 @@ 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}") = "MewTerminal", "MewTerminal\MewTerminal.csproj", "{D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AutoTools", "AutoTools", "{BAEF983A-EFF2-48DF-A74E-57084166BB4D}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoTools.ChmDataExtract", "AutoTools.ChmDataExtract\AutoTools.ChmDataExtract.csproj", "{5A9DE453-AD64-4F8D-8215-3BB26674D164}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoTools.DocBuilder", "AutoTools.DocBuilder\AutoTools.DocBuilder.csproj", "{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.BasicEthernet", "Examples\Examples.BasicEthernet\Examples.BasicEthernet.csproj", "{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{FDBC39C6-5C06-4249-BDBF-068562D02AD6}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.BasicRegisterReadWrite", "Examples\Examples.BasicRegisterReadWrite\Examples.BasicRegisterReadWrite.csproj", "{C630B87F-0DFD-4B3B-9845-B5627E9612D5}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.Polling", "Examples\Examples.Polling\Examples.Polling.csproj", "{6615556F-EF18-442F-8FF7-A7943A503C92}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.ProgramReadWrite", "Examples\Examples.ProgramReadWrite\Examples.ProgramReadWrite.csproj", "{8311E099-F164-4700-BA1A-F5214B38E0FA}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.WPF", "Examples\Examples.WPF\Examples.WPF.csproj", "{339CC94E-68AB-4D2E-BCC1-84D81082EC93}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -31,18 +49,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 +61,116 @@ 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
|
||||
{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
|
||||
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Release|x64.Build.0 = Release|Any CPU
|
||||
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Release|x86.Build.0 = Release|Any CPU
|
||||
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Release|x64.Build.0 = Release|Any CPU
|
||||
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Release|x86.Build.0 = Release|Any CPU
|
||||
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Release|x64.Build.0 = Release|Any CPU
|
||||
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Release|x86.Build.0 = Release|Any CPU
|
||||
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Release|x86.Build.0 = Release|Any CPU
|
||||
{6615556F-EF18-442F-8FF7-A7943A503C92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6615556F-EF18-442F-8FF7-A7943A503C92}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6615556F-EF18-442F-8FF7-A7943A503C92}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{6615556F-EF18-442F-8FF7-A7943A503C92}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{6615556F-EF18-442F-8FF7-A7943A503C92}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{6615556F-EF18-442F-8FF7-A7943A503C92}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{6615556F-EF18-442F-8FF7-A7943A503C92}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6615556F-EF18-442F-8FF7-A7943A503C92}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6615556F-EF18-442F-8FF7-A7943A503C92}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{6615556F-EF18-442F-8FF7-A7943A503C92}.Release|x64.Build.0 = Release|Any CPU
|
||||
{6615556F-EF18-442F-8FF7-A7943A503C92}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{6615556F-EF18-442F-8FF7-A7943A503C92}.Release|x86.Build.0 = Release|Any CPU
|
||||
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Release|x64.Build.0 = Release|Any CPU
|
||||
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Release|x86.Build.0 = Release|Any CPU
|
||||
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Release|x64.Build.0 = Release|Any CPU
|
||||
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{5A9DE453-AD64-4F8D-8215-3BB26674D164} = {BAEF983A-EFF2-48DF-A74E-57084166BB4D}
|
||||
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA} = {BAEF983A-EFF2-48DF-A74E-57084166BB4D}
|
||||
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E} = {FDBC39C6-5C06-4249-BDBF-068562D02AD6}
|
||||
{C630B87F-0DFD-4B3B-9845-B5627E9612D5} = {FDBC39C6-5C06-4249-BDBF-068562D02AD6}
|
||||
{6615556F-EF18-442F-8FF7-A7943A503C92} = {FDBC39C6-5C06-4249-BDBF-068562D02AD6}
|
||||
{8311E099-F164-4700-BA1A-F5214B38E0FA} = {FDBC39C6-5C06-4249-BDBF-068562D02AD6}
|
||||
{339CC94E-68AB-4D2E-BCC1-84D81082EC93} = {FDBC39C6-5C06-4249-BDBF-068562D02AD6}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {4ABB8137-CD8F-4691-9802-9ED371012F47}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
5326
MewtocolNet/AutoGeneratedData/FPFunction.cs
Normal file
5326
MewtocolNet/AutoGeneratedData/FPFunction.cs
Normal file
File diff suppressed because it is too large
Load Diff
148
MewtocolNet/ComCassette/CassetteFinder.cs
Normal file
148
MewtocolNet/ComCassette/CassetteFinder.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using MewtocolNet.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MewtocolNet.ComCassette {
|
||||
|
||||
/// <summary>
|
||||
/// Provides a interface to modify and find PLC network cassettes also known as COM5
|
||||
/// </summary>
|
||||
public class CassetteFinder {
|
||||
|
||||
public static async Task<IEnumerable<CassetteInformation>> FindClientsAsync(string ipSource = null, int timeoutMs = 100) {
|
||||
|
||||
Logger.Log("Scanning for cassettes over UDP");
|
||||
|
||||
var from = new IPEndPoint(IPAddress.Any, 0);
|
||||
|
||||
var interfacesTasks = new List<Task<List<CassetteInformation>>>();
|
||||
|
||||
var usableInterfaces = Mewtocol.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<CassetteInformation>();
|
||||
|
||||
foreach (var grp in grouped) {
|
||||
|
||||
foreach (var cassette in grp) {
|
||||
|
||||
if (decomposed.Any(x => x.MacAddress.SequenceEqual(cassette.MacAddress))) continue;
|
||||
|
||||
decomposed.Add(cassette);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Logger.Log($"Found {decomposed.Count} cassettes");
|
||||
|
||||
return decomposed.OrderBy(x => x.IPAddress.ToString());
|
||||
|
||||
}
|
||||
|
||||
private static async Task<List<CassetteInformation>> FindClientsForEndpoint(IPEndPoint from, int timeoutMs, string ipEndpointName) {
|
||||
|
||||
var cassettesFound = new List<CassetteInformation>();
|
||||
|
||||
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) {
|
||||
|
||||
if (tSource.Token.IsCancellationRequested) break;
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
219
MewtocolNet/ComCassette/CassetteInformation.cs
Normal file
219
MewtocolNet/ComCassette/CassetteInformation.cs
Normal file
@@ -0,0 +1,219 @@
|
||||
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 {
|
||||
|
||||
/// <summary>
|
||||
/// Information about the COM cassette
|
||||
/// </summary>
|
||||
public class CassetteInformation {
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the cassette is currently configurating
|
||||
/// </summary>
|
||||
public bool IsConfigurating { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the COM cassette
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the usage of DHCP
|
||||
/// </summary>
|
||||
public bool UsesDHCP { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// IP Address of the COM cassette
|
||||
/// </summary>
|
||||
public IPAddress IPAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Subnet mask of the cassette
|
||||
/// </summary>
|
||||
public IPAddress SubnetMask { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default gateway of the cassette
|
||||
/// </summary>
|
||||
public IPAddress GatewayAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Mac address of the cassette
|
||||
/// </summary>
|
||||
public byte[] MacAddress { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Mac address of the cassette formatted as a MAC string (XX:XX:XX:XX:XX)
|
||||
/// </summary>
|
||||
public string MacAddressStr => MacAddress.ToHexString(":");
|
||||
|
||||
/// <summary>
|
||||
/// The source endpoint the cassette is reachable from
|
||||
/// </summary>
|
||||
public IPEndPoint Endpoint { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the endpoint the device is reachable from, or null if not specifically defined
|
||||
/// </summary>
|
||||
public string EndpointName { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Firmware version as string
|
||||
/// </summary>
|
||||
public string FirmwareVersion { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tcp port of the cassette
|
||||
/// </summary>
|
||||
public int Port { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Status of the cassette
|
||||
/// </summary>
|
||||
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<byte> sendBytes = new List<byte>();
|
||||
|
||||
//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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static bool operator ==(CassetteInformation a, CassetteInformation b) => EqualProps(a, b);
|
||||
|
||||
public static bool operator !=(CassetteInformation a, CassetteInformation b) => !EqualProps(a, b);
|
||||
|
||||
private static bool EqualProps (CassetteInformation a, CassetteInformation b) {
|
||||
|
||||
if (a is null && b is null) return true;
|
||||
if (!(a is null) && b is null) return false;
|
||||
if (!(b is null) && a is null) return false;
|
||||
|
||||
return a.Name == b.Name &&
|
||||
a.UsesDHCP == b.UsesDHCP &&
|
||||
a.IPAddress.ToString() == b.IPAddress.ToString() &&
|
||||
a.SubnetMask.ToString() == b.SubnetMask.ToString() &&
|
||||
a.GatewayAddress.ToString() == b.GatewayAddress.ToString() &&
|
||||
a.MacAddressStr == b.MacAddressStr &&
|
||||
a.FirmwareVersion == b.FirmwareVersion &&
|
||||
a.Port == b.Port &&
|
||||
a.Status == b.Status;
|
||||
|
||||
}
|
||||
|
||||
public override bool Equals(object obj) {
|
||||
|
||||
if ((obj == null) || !this.GetType().Equals(obj.GetType())) {
|
||||
return false;
|
||||
} else {
|
||||
return (CassetteInformation)obj == this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
19
MewtocolNet/ComCassette/CassetteStatus.cs
Normal file
19
MewtocolNet/ComCassette/CassetteStatus.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace MewtocolNet.ComCassette {
|
||||
|
||||
/// <summary>
|
||||
/// Needs a list of all status codes.. hard to reverse engineer
|
||||
/// </summary>
|
||||
public enum CassetteStatus {
|
||||
|
||||
/// <summary>
|
||||
/// Cassette is running as intended
|
||||
/// </summary>
|
||||
Normal = 0,
|
||||
/// <summary>
|
||||
/// Cassette DHCP resolution error
|
||||
/// </summary>
|
||||
DHCPError = 2,
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
103
MewtocolNet/CustomTypes/DWord.cs
Normal file
103
MewtocolNet/CustomTypes/DWord.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MewtocolNet {
|
||||
|
||||
/// <summary>
|
||||
/// A DWord is a 16 bit value of 2 bytes
|
||||
/// </summary>
|
||||
public struct DWord : MewtocolExtTypeInit2Word {
|
||||
|
||||
private int bitLength;
|
||||
|
||||
internal uint value;
|
||||
|
||||
public uint Value => value;
|
||||
|
||||
public DWord(uint bytes) {
|
||||
value = bytes;
|
||||
bitLength = Marshal.SizeOf(value) * 8;
|
||||
}
|
||||
|
||||
public DWord(byte[] bytes) {
|
||||
bytes = bytes.Take(4).ToArray();
|
||||
value = BitConverter.ToUInt32(bytes, 0);
|
||||
bitLength = Marshal.SizeOf(value) * 8;
|
||||
}
|
||||
|
||||
//operations
|
||||
|
||||
public static DWord operator -(DWord a, DWord b) => new DWord() {
|
||||
value = (ushort)(a.value - b.value)
|
||||
};
|
||||
|
||||
public static DWord operator +(DWord a, DWord b) => new DWord() {
|
||||
value = (ushort)(a.value + b.value)
|
||||
};
|
||||
|
||||
public static DWord operator *(DWord a, DWord b) => new DWord() {
|
||||
value = (ushort)(a.value * b.value)
|
||||
};
|
||||
|
||||
public static DWord operator /(DWord a, DWord b) => new DWord() {
|
||||
value = (ushort)(a.value / b.value)
|
||||
};
|
||||
|
||||
public static bool operator ==(DWord a, DWord b) => a.value == b.value;
|
||||
|
||||
public static bool operator !=(DWord a, DWord b) => a.value != b.value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bit value at the given position
|
||||
/// </summary>
|
||||
public bool this[int bitIndex] {
|
||||
get {
|
||||
|
||||
if (bitIndex > bitLength - 1 && bitLength != 0)
|
||||
throw new IndexOutOfRangeException($"The word bit index was out of range ({bitIndex}/{bitLength - 1})");
|
||||
|
||||
if (bitLength == 0) return false;
|
||||
|
||||
return (value & (1 << bitIndex)) != 0;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Equals(object obj) {
|
||||
|
||||
if ((obj == null) || !this.GetType().Equals(obj.GetType())) {
|
||||
return false;
|
||||
} else {
|
||||
return (DWord)obj == this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public override int GetHashCode() => (int)value;
|
||||
|
||||
public byte[] ToByteArray() => BitConverter.GetBytes(value);
|
||||
|
||||
//string ops
|
||||
|
||||
public override string ToString() => $"0x{value.ToString("X8")}";
|
||||
|
||||
public string ToStringBits() {
|
||||
|
||||
return Convert.ToString(value, 2).PadLeft(bitLength, '0');
|
||||
|
||||
}
|
||||
|
||||
public string ToStringBitsPlc() {
|
||||
|
||||
var parts = Convert.ToString(value, 2)
|
||||
.PadLeft(Marshal.SizeOf(value) * 8, '0')
|
||||
.SplitInParts(4);
|
||||
|
||||
return string.Join("_", parts);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
92
MewtocolNet/CustomTypes/DateAndTime.cs
Normal file
92
MewtocolNet/CustomTypes/DateAndTime.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MewtocolNet {
|
||||
|
||||
/// <summary>
|
||||
/// A DateAndTime struct of 4 bytes represented as seconds from 2001-01-01 in the PLC<br/>
|
||||
/// This also works for the PLC type TIME_OF_DAY and DATE
|
||||
/// </summary>
|
||||
public struct DateAndTime : MewtocolExtTypeInit2Word {
|
||||
|
||||
internal DateTime value;
|
||||
|
||||
public DateAndTime(int year = 2001, int month = 1, int day = 1, int hour = 0, int minute = 0, int second = 0) {
|
||||
|
||||
var minDate = MinDate;
|
||||
var maxDate = MaxDate;
|
||||
|
||||
if (year < 2001 || year > 2099)
|
||||
throw new NotSupportedException("Year must be between 2001 and 2099");
|
||||
|
||||
if (month < 1 || month > 12)
|
||||
throw new NotSupportedException("Month must be between 1 and 12");
|
||||
|
||||
if (day < 1 || day > 32)
|
||||
throw new NotSupportedException("Day must be between 1 and 32");
|
||||
|
||||
if (day < 1 || day > 32)
|
||||
throw new NotSupportedException("Month must be between 1 and 32");
|
||||
|
||||
var dt = new DateTime(year, month, day, hour, minute, second);
|
||||
|
||||
if (dt < minDate)
|
||||
throw new Exception($"The minimal DATE_AND_TIME repesentation is {minDate}");
|
||||
|
||||
if (dt > maxDate)
|
||||
throw new Exception($"The maximal DATE_AND_TIME repesentation is {maxDate}");
|
||||
|
||||
value = dt;
|
||||
|
||||
}
|
||||
|
||||
public static DateAndTime FromBytes(byte[] bytes) {
|
||||
|
||||
var secondsFrom = BitConverter.ToUInt32(bytes, 0);
|
||||
|
||||
return FromDateTime(MinDate + TimeSpan.FromSeconds(secondsFrom));
|
||||
|
||||
}
|
||||
|
||||
public static DateAndTime FromDateTime(DateTime time) => new DateAndTime(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second);
|
||||
|
||||
//operations
|
||||
|
||||
public static TimeSpan operator -(DateAndTime a, DateAndTime b) => a.value - b.value;
|
||||
|
||||
public static DateAndTime operator +(DateAndTime a, TimeSpan b) => FromDateTime(a.value + b);
|
||||
|
||||
public static bool operator ==(DateAndTime a, DateAndTime b) => a.value == b.value;
|
||||
|
||||
public static bool operator !=(DateAndTime a, DateAndTime b) => a.value != b.value;
|
||||
|
||||
public override bool Equals(object obj) {
|
||||
|
||||
if ((obj == null) || !this.GetType().Equals(obj.GetType())) {
|
||||
return false;
|
||||
} else {
|
||||
return (DateAndTime)obj == this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public override int GetHashCode() => value.GetHashCode();
|
||||
|
||||
public byte[] ToByteArray() => BitConverter.GetBytes(SecondsSinceStart());
|
||||
|
||||
public DateTime ToDateTime() => value;
|
||||
|
||||
private uint SecondsSinceStart() => (uint)(value - MinDate).TotalSeconds;
|
||||
|
||||
private static DateTime MinDate => new DateTime(2001, 01, 01, 0, 0, 0);
|
||||
|
||||
private static DateTime MaxDate => new DateTime(2099, 12, 31, 23, 59, 59);
|
||||
|
||||
//string ops
|
||||
|
||||
public override string ToString() => $"DT#{value:yyyy-MM-dd-HH:mm:ss}";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
7
MewtocolNet/CustomTypes/MewtocolExtensionType.cs
Normal file
7
MewtocolNet/CustomTypes/MewtocolExtensionType.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace MewtocolNet {
|
||||
|
||||
internal interface MewtocolExtTypeInit1Word { }
|
||||
|
||||
internal interface MewtocolExtTypeInit2Word { }
|
||||
|
||||
}
|
||||
103
MewtocolNet/CustomTypes/Word.cs
Normal file
103
MewtocolNet/CustomTypes/Word.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MewtocolNet {
|
||||
|
||||
/// <summary>
|
||||
/// A word is a 16 bit value of 2 bytes
|
||||
/// </summary>
|
||||
public struct Word : MewtocolExtTypeInit1Word {
|
||||
|
||||
private int bitLength;
|
||||
|
||||
internal ushort value;
|
||||
|
||||
public ushort Value => value;
|
||||
|
||||
public Word(ushort bytes) {
|
||||
value = bytes;
|
||||
bitLength = Marshal.SizeOf(value) * 8;
|
||||
}
|
||||
|
||||
public Word(byte[] bytes) {
|
||||
bytes = bytes.Take(2).ToArray();
|
||||
value = BitConverter.ToUInt16(bytes, 0);
|
||||
bitLength = Marshal.SizeOf(value) * 8;
|
||||
}
|
||||
|
||||
//operations
|
||||
|
||||
public static Word operator -(Word a, Word b) => new Word() {
|
||||
value = (ushort)(a.value - b.value)
|
||||
};
|
||||
|
||||
public static Word operator +(Word a, Word b) => new Word() {
|
||||
value = (ushort)(a.value + b.value)
|
||||
};
|
||||
|
||||
public static Word operator *(Word a, Word b) => new Word() {
|
||||
value = (ushort)(a.value * b.value)
|
||||
};
|
||||
|
||||
public static Word operator /(Word a, Word b) => new Word() {
|
||||
value = (ushort)(a.value / b.value)
|
||||
};
|
||||
|
||||
public static bool operator ==(Word a, Word b) => a.value == b.value;
|
||||
|
||||
public static bool operator !=(Word a, Word b) => a.value != b.value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bit value at the given position
|
||||
/// </summary>
|
||||
public bool this[int bitIndex] {
|
||||
get {
|
||||
|
||||
if (bitIndex > bitLength - 1 && bitLength != 0)
|
||||
throw new IndexOutOfRangeException($"The word bit index was out of range ({bitIndex}/{bitLength - 1})");
|
||||
|
||||
if (bitLength == 0) return false;
|
||||
|
||||
return (value & (1 << bitIndex)) != 0;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Equals(object obj) {
|
||||
|
||||
if ((obj == null) || !this.GetType().Equals(obj.GetType())) {
|
||||
return false;
|
||||
} else {
|
||||
return (Word)obj == this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public override int GetHashCode() => (int)value;
|
||||
|
||||
public byte[] ToByteArray() => BitConverter.GetBytes(value);
|
||||
|
||||
//string ops
|
||||
|
||||
public override string ToString() => $"0x{value.ToString("X4")}";
|
||||
|
||||
public string ToStringBits() {
|
||||
|
||||
return Convert.ToString(value, 2).PadLeft(bitLength, '0');
|
||||
|
||||
}
|
||||
|
||||
public string ToStringBitsPlc() {
|
||||
|
||||
var parts = Convert.ToString(value, 2)
|
||||
.PadLeft(Marshal.SizeOf(value) * 8, '0')
|
||||
.SplitInParts(4);
|
||||
|
||||
return string.Join("_", parts);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MewtocolNet.Links {
|
||||
namespace MewtocolNet.DataLists {
|
||||
|
||||
internal class LinkedData {
|
||||
internal class CodeDescriptions {
|
||||
|
||||
internal static Dictionary<int, string> ErrorCodes = new System.Collections.Generic.Dictionary<int, string> {
|
||||
internal static Dictionary<int, string> Error = new Dictionary<int, string> {
|
||||
|
||||
{21, "NACK error"},
|
||||
{22, "WACK error"},
|
||||
@@ -40,7 +39,6 @@ namespace MewtocolNet.Links {
|
||||
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
12
MewtocolNet/Documentation/PlcCodeTestedAttribute.cs
Normal file
12
MewtocolNet/Documentation/PlcCodeTestedAttribute.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace MewtocolNet.Documentation {
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
|
||||
internal class PlcCodeTestedAttribute : Attribute {
|
||||
|
||||
public PlcCodeTestedAttribute() { }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
12
MewtocolNet/Documentation/PlcEXRTAttribute.cs
Normal file
12
MewtocolNet/Documentation/PlcEXRTAttribute.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace MewtocolNet.Documentation {
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
|
||||
internal class PlcEXRTAttribute : Attribute {
|
||||
|
||||
public PlcEXRTAttribute() { }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
12
MewtocolNet/Documentation/PlcLegacyAttribute.cs
Normal file
12
MewtocolNet/Documentation/PlcLegacyAttribute.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace MewtocolNet.Documentation {
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
|
||||
internal class PlcLegacyAttribute : Attribute {
|
||||
|
||||
public PlcLegacyAttribute() { }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
75
MewtocolNet/Documentation/docs.xml
Normal file
75
MewtocolNet/Documentation/docs.xml
Normal file
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<extradoc>
|
||||
<class name="support-conv-types">
|
||||
<list type="bullet">
|
||||
<item>
|
||||
<term>
|
||||
<see cref="bool"/>
|
||||
</term>
|
||||
<description>Boolean R/X/Y registers</description>
|
||||
</item>
|
||||
<item>
|
||||
<term>
|
||||
<see cref="short"/>
|
||||
</term>
|
||||
<description>16 bit signed integer</description>
|
||||
</item>
|
||||
<item>
|
||||
<term>
|
||||
<see cref="ushort"/>
|
||||
</term>
|
||||
<description>16 bit un-signed integer</description>
|
||||
</item>
|
||||
<item>
|
||||
<term>
|
||||
<see cref="Word"/>
|
||||
</term>
|
||||
<description>16 bit word (2 bytes)</description>
|
||||
</item>
|
||||
<item>
|
||||
<term>
|
||||
<see cref="int"/>
|
||||
</term>
|
||||
<description>32 bit signed integer</description>
|
||||
</item>
|
||||
<item>
|
||||
<term>
|
||||
<see cref="uint"/>
|
||||
</term>
|
||||
<description>32 bit un-signed integer</description>
|
||||
</item>
|
||||
<item>
|
||||
<term>
|
||||
<see cref="DWord"/>
|
||||
</term>
|
||||
<description>32 bit word (4 bytes)</description>
|
||||
</item>
|
||||
<item>
|
||||
<term>
|
||||
<see cref="float"/>
|
||||
</term>
|
||||
<description>32 bit floating point</description>
|
||||
</item>
|
||||
<item>
|
||||
<term>
|
||||
<see cref="TimeSpan"/>
|
||||
</term>
|
||||
<description>
|
||||
32 bit time from <see cref="PlcVarType.TIME"/> interpreted as <see cref="TimeSpan"/>
|
||||
</description>
|
||||
</item>
|
||||
<item>
|
||||
<term>
|
||||
<see cref="Enum"/>
|
||||
</term>
|
||||
<description>16 or 32 bit enums, also supports flags</description>
|
||||
</item>
|
||||
<item>
|
||||
<term>
|
||||
<see cref="string"/>
|
||||
</term>
|
||||
<description>String of chars, the interface will automatically get the length</description>
|
||||
</item>
|
||||
</list>
|
||||
</class>
|
||||
</extradoc>
|
||||
9
MewtocolNet/Events/PlcConnectionArgs.cs
Normal file
9
MewtocolNet/Events/PlcConnectionArgs.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace MewtocolNet.Events {
|
||||
|
||||
public delegate void PlcConnectionEventHandler(object sender, PlcConnectionArgs e);
|
||||
|
||||
public class PlcConnectionArgs : EventArgs { }
|
||||
|
||||
}
|
||||
19
MewtocolNet/Events/PlcModeArgs.cs
Normal file
19
MewtocolNet/Events/PlcModeArgs.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace MewtocolNet.Events {
|
||||
|
||||
public delegate void PlcModeChangedEventHandler(object sender, PlcModeArgs e);
|
||||
|
||||
public class PlcModeArgs : EventArgs {
|
||||
|
||||
public OPMode LastMode { get; internal set; }
|
||||
|
||||
public OPMode NowMode { get; internal set; }
|
||||
|
||||
public bool ProgToRun { get; internal set; }
|
||||
|
||||
public bool RunToProg { get; internal set; }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
98
MewtocolNet/Events/ReconnectArgs.cs
Normal file
98
MewtocolNet/Events/ReconnectArgs.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using MewtocolNet.Registers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace MewtocolNet.Events {
|
||||
|
||||
public delegate void PlcReconnectEventHandler(object sender, ReconnectArgs e);
|
||||
|
||||
public class ReconnectArgs : EventArgs, INotifyPropertyChanged {
|
||||
|
||||
private TimeSpan retryCountDownRemaining;
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public event Action Reconnected;
|
||||
|
||||
public int ReconnectTry { get; internal set; }
|
||||
|
||||
public int MaxAttempts { get; internal set; }
|
||||
|
||||
public TimeSpan RetryCountDownTime { get; internal set; }
|
||||
|
||||
public TimeSpan RetryCountDownRemaining {
|
||||
get => retryCountDownRemaining;
|
||||
private set {
|
||||
retryCountDownRemaining = value;
|
||||
OnPropChange();
|
||||
}
|
||||
}
|
||||
|
||||
private bool isReconnected;
|
||||
|
||||
public bool IsReconnected {
|
||||
get { return isReconnected; }
|
||||
set {
|
||||
isReconnected = value;
|
||||
OnPropChange();
|
||||
}
|
||||
}
|
||||
|
||||
private System.Timers.Timer countDownTimer;
|
||||
|
||||
internal ReconnectArgs(int currentAttempt, int totalAttempts, TimeSpan delayBetween) {
|
||||
|
||||
ReconnectTry = currentAttempt;
|
||||
MaxAttempts = totalAttempts;
|
||||
RetryCountDownTime = delayBetween;
|
||||
|
||||
//start countdown timer
|
||||
RetryCountDownRemaining = RetryCountDownTime;
|
||||
|
||||
var interval = 100;
|
||||
var intervalTS = TimeSpan.FromMilliseconds(interval);
|
||||
|
||||
countDownTimer = new System.Timers.Timer(100);
|
||||
|
||||
countDownTimer.Elapsed += (s, e) => {
|
||||
|
||||
if (RetryCountDownRemaining <= TimeSpan.Zero) {
|
||||
StopTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
RetryCountDownRemaining -= intervalTS;
|
||||
|
||||
};
|
||||
|
||||
countDownTimer.Start();
|
||||
|
||||
}
|
||||
|
||||
internal void ConnectionSuccess () {
|
||||
|
||||
IsReconnected = true;
|
||||
StopTimer();
|
||||
Reconnected?.Invoke();
|
||||
|
||||
}
|
||||
|
||||
private void StopTimer () {
|
||||
|
||||
countDownTimer?.Stop();
|
||||
RetryCountDownRemaining = TimeSpan.Zero;
|
||||
|
||||
}
|
||||
|
||||
private void OnPropChange([CallerMemberName] string propertyName = null) {
|
||||
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
22
MewtocolNet/Events/RegisterChanged.cs
Normal file
22
MewtocolNet/Events/RegisterChanged.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using MewtocolNet.Registers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace MewtocolNet.Events {
|
||||
|
||||
public delegate void RegisterChangedEventHandler(object sender, RegisterChangedArgs e);
|
||||
|
||||
public class RegisterChangedArgs : EventArgs {
|
||||
|
||||
public IRegister Register { get; internal set; }
|
||||
|
||||
public object Value { get; internal set; }
|
||||
|
||||
public object PreviousValue { get; internal set; }
|
||||
|
||||
public string PreviousValueString { get; internal set; }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
26
MewtocolNet/Extensions/AsyncExtensions.cs
Normal file
26
MewtocolNet/Extensions/AsyncExtensions.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MewtocolNet {
|
||||
|
||||
internal static class AsyncExtensions {
|
||||
|
||||
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken) {
|
||||
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs)) {
|
||||
if (task != await Task.WhenAny(task, tcs.Task)) {
|
||||
return default(T);
|
||||
}
|
||||
}
|
||||
|
||||
if (task.IsCanceled) return default(T);
|
||||
|
||||
return task.Result;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
34
MewtocolNet/Extensions/SerialPortExtensions.cs
Normal file
34
MewtocolNet/Extensions/SerialPortExtensions.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.IO.Ports;
|
||||
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<byte[]> ReadAsync(this SerialPort serialPort, int count) {
|
||||
var buffer = new byte[count];
|
||||
await serialPort.ReadAsync(buffer, 0, count);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
88
MewtocolNet/Helpers/CRCCalculator.cs
Normal file
88
MewtocolNet/Helpers/CRCCalculator.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace MewtocolNet {
|
||||
|
||||
internal static class CRCCalculator {
|
||||
|
||||
private static readonly ushort[] crcTable_CRC16_MCRF4XX = {
|
||||
0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf,
|
||||
0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7,
|
||||
0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e,
|
||||
0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876,
|
||||
0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd,
|
||||
0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5,
|
||||
0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c,
|
||||
0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974,
|
||||
0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb,
|
||||
0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3,
|
||||
0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a,
|
||||
0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72,
|
||||
0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9,
|
||||
0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1,
|
||||
0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738,
|
||||
0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70,
|
||||
0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7,
|
||||
0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff,
|
||||
0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036,
|
||||
0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e,
|
||||
0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5,
|
||||
0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd,
|
||||
0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134,
|
||||
0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c,
|
||||
0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3,
|
||||
0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb,
|
||||
0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232,
|
||||
0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a,
|
||||
0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1,
|
||||
0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9,
|
||||
0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330,
|
||||
0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78,
|
||||
};
|
||||
|
||||
internal static string BCC_Mew(this string msg) {
|
||||
|
||||
byte[] bytes = Encoding.ASCII.GetBytes(msg);
|
||||
byte crc = CRC8(bytes);
|
||||
|
||||
return msg.Insert(msg.Length, crc.ToString("X2"));
|
||||
|
||||
}
|
||||
|
||||
internal static string BCC_Mew7(this string msg) {
|
||||
|
||||
byte[] bytes = Encoding.ASCII.GetBytes(msg);
|
||||
ushort crc = CRC16_MCRF4XX(bytes);
|
||||
|
||||
return msg.Insert(msg.Length, crc.ToString("X4"));
|
||||
|
||||
}
|
||||
|
||||
private static ushort CRC16_MCRF4XX(byte[] bytes) {
|
||||
|
||||
int icrc = 0xFFFF;
|
||||
|
||||
for (int i = 0; i < bytes.Length; i++) {
|
||||
icrc = (icrc >> 8) ^ crcTable_CRC16_MCRF4XX[(icrc ^ bytes[i]) & 0xff];
|
||||
}
|
||||
|
||||
byte[] ret = BitConverter.GetBytes(icrc);
|
||||
|
||||
return BitConverter.ToUInt16(ret, 0);
|
||||
|
||||
}
|
||||
|
||||
private static byte CRC8(byte[] bytes) {
|
||||
|
||||
byte xorTotalByte = 0;
|
||||
|
||||
for (int i = 0; i < bytes.Length; i++)
|
||||
xorTotalByte ^= bytes[i];
|
||||
|
||||
return xorTotalByte;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
30
MewtocolNet/Helpers/LinqHelpers.cs
Normal file
30
MewtocolNet/Helpers/LinqHelpers.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MewtocolNet.Helpers {
|
||||
|
||||
internal static class LinqHelpers {
|
||||
|
||||
internal static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) {
|
||||
return DistinctBy(source, keySelector, null);
|
||||
}
|
||||
|
||||
internal static IEnumerable<TSource> DistinctBy<TSource, TKey>
|
||||
(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer) {
|
||||
|
||||
if (source == null) throw new ArgumentNullException(nameof(source));
|
||||
if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
|
||||
|
||||
return _(); IEnumerable<TSource> _() {
|
||||
var knownKeys = new HashSet<TKey>(comparer);
|
||||
foreach (var element in source) {
|
||||
if (knownKeys.Add(keySelector(element)))
|
||||
yield return element;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
514
MewtocolNet/Helpers/MewtocolHelpers.cs
Normal file
514
MewtocolNet/Helpers/MewtocolHelpers.cs
Normal file
@@ -0,0 +1,514 @@
|
||||
using MewtocolNet.Documentation;
|
||||
using MewtocolNet.Registers;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace MewtocolNet {
|
||||
|
||||
/// <summary>
|
||||
/// Contains helper methods
|
||||
/// </summary>
|
||||
public static class MewtocolHelpers {
|
||||
|
||||
#region Async extensions
|
||||
|
||||
internal static Task WhenCanceled(this CancellationToken cancellationToken) {
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).SetResult(true), tcs);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Byte and string operation helpers
|
||||
|
||||
public static T SetFlag<T>(this Enum value, T flag, bool set) {
|
||||
|
||||
Type underlyingType = Enum.GetUnderlyingType(value.GetType());
|
||||
|
||||
dynamic valueAsInt = Convert.ChangeType(value, underlyingType);
|
||||
dynamic flagAsInt = Convert.ChangeType(flag, underlyingType);
|
||||
|
||||
if (set) {
|
||||
valueAsInt |= flagAsInt;
|
||||
} else {
|
||||
valueAsInt &= ~flagAsInt;
|
||||
}
|
||||
|
||||
return (T)valueAsInt;
|
||||
|
||||
}
|
||||
|
||||
public static int DetermineTypeByteIntialSize(this Type type) {
|
||||
|
||||
//enums can only be of numeric types
|
||||
if (type.IsEnum) return Marshal.SizeOf(Enum.GetUnderlyingType(type));
|
||||
|
||||
//strings get always set with 4 bytes because the first 4 bytes contain the length
|
||||
if (type == typeof(string)) return 4;
|
||||
if (type == typeof(TimeSpan)) return 4;
|
||||
if (type == typeof(DateTime)) return 4;
|
||||
|
||||
if (type.Namespace.StartsWith("System")) return Marshal.SizeOf(type);
|
||||
|
||||
if (typeof(MewtocolExtTypeInit1Word).IsAssignableFrom(type)) return 2;
|
||||
if (typeof(MewtocolExtTypeInit2Word).IsAssignableFrom(type)) return 4;
|
||||
|
||||
throw new Exception("Type not supported");
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches a byte array for a pattern
|
||||
/// </summary>
|
||||
/// <param name="src"></param>
|
||||
/// <param name="pattern"></param>
|
||||
/// <returns>The start index of the found pattern or -1</returns>
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a string (after converting to upper case) to ascii bytes
|
||||
/// </summary>
|
||||
internal static byte[] BytesFromHexASCIIString(this string _str) {
|
||||
|
||||
ASCIIEncoding ascii = new ASCIIEncoding();
|
||||
byte[] bytes = ascii.GetBytes(_str.ToUpper());
|
||||
return bytes;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a return message as RCS single bit
|
||||
/// </summary>
|
||||
internal static bool? ParseRCSingleBit(this string _onString) {
|
||||
|
||||
_onString = _onString.Replace("\r", "");
|
||||
|
||||
var res = new Regex(@"\%([0-9a-fA-F]{2})\$RC(.)").Match(_onString);
|
||||
if (res.Success) {
|
||||
string val = res.Groups[2].Value;
|
||||
return val == "1";
|
||||
}
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a return message as RCS multiple bits
|
||||
/// </summary>
|
||||
internal static BitArray ParseRCMultiBit(this string _onString) {
|
||||
|
||||
_onString = _onString.Replace("\r", "");
|
||||
|
||||
var res = new Regex(@"\%([0-9a-fA-F]{2})\$RC(?<bits>(?: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;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a return string from the PLC as a raw byte array <br/>
|
||||
/// Example:
|
||||
/// <code>
|
||||
/// ↓Start ↓end
|
||||
/// %01$RD0100020010\r
|
||||
/// </code>
|
||||
/// This will return the byte array:
|
||||
/// <code>
|
||||
/// [0x01, 0x00, 0x02, 0x00]
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <param name="_onString"></param>
|
||||
/// <returns>A <see cref="T:byte[]"/> or null of failed</returns>
|
||||
internal static byte[] ParseResponseStringAsBytes(this string _onString) {
|
||||
|
||||
_onString = _onString.Replace("\r", "");
|
||||
|
||||
var res = new Regex(@"(?:\%|\<)([0-9a-fA-F]{2})\$(?:RD|RP|RC)(?<data>.*)(?<csum>..)").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;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a string into even parts
|
||||
/// </summary>
|
||||
internal static IEnumerable<string> 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));
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a string by uppercase words
|
||||
/// </summary>
|
||||
internal static IEnumerable<string> SplitByAlternatingCase(this string str) {
|
||||
|
||||
var words = new List<string>();
|
||||
var result = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < str.Length; i++) {
|
||||
|
||||
char lastCh = str[Math.Max(0, i - 1)];
|
||||
char ch = str[i];
|
||||
|
||||
if (char.IsUpper(ch) && char.IsLower(lastCh) && result.Length > 0) {
|
||||
words.Add(result.ToString().Trim());
|
||||
result.Clear();
|
||||
}
|
||||
|
||||
result.Append(ch);
|
||||
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(result.ToString()))
|
||||
words.Add(result.ToString().Trim());
|
||||
|
||||
return words;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a string by uppercase words and joins them with the given seperator
|
||||
/// </summary>
|
||||
internal static string JoinSplitByUpperCase(this string str, string seperator = " ") => string.Join(seperator, str.SplitByAlternatingCase());
|
||||
|
||||
internal static string Ellipsis(this string str, int maxLength) {
|
||||
|
||||
if (string.IsNullOrEmpty(str) || str.Length <= maxLength)
|
||||
return str;
|
||||
|
||||
return $"{str.Substring(0, maxLength - 3)}...";
|
||||
|
||||
}
|
||||
|
||||
internal static string SanitizeLinebreakFormatting (this string str) {
|
||||
|
||||
str = str.Replace("\r", "").Replace("\n", "").Trim();
|
||||
|
||||
return Regex.Replace(str, @"\s+", " ");
|
||||
|
||||
}
|
||||
|
||||
internal static string SanitizeBracketFormatting(this string str) {
|
||||
|
||||
return str.Replace("(", "").Replace(")", "").Trim();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a hex string (AB01C1) to a byte array
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a byte array to a hexadecimal string
|
||||
/// </summary>
|
||||
/// <param name="seperator">Seperator between the hex numbers</param>
|
||||
/// <param name="arr">The byte array</param>
|
||||
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();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches byte order from mixed to big endian
|
||||
/// </summary>
|
||||
internal static byte[] BigToMixedEndian(this byte[] arr) {
|
||||
|
||||
List<byte> oldBL = new List<byte>(arr);
|
||||
|
||||
List<byte> tempL = new List<byte>();
|
||||
|
||||
//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
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the register type is boolean
|
||||
/// </summary>
|
||||
internal static bool IsBoolean(this RegisterPrefix type) {
|
||||
|
||||
return type == RegisterPrefix.X || type == RegisterPrefix.Y || type == RegisterPrefix.R;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the register type numeric
|
||||
/// </summary>
|
||||
internal static bool IsNumericDTDDT(this RegisterPrefix type) {
|
||||
|
||||
return type == RegisterPrefix.DT || type == RegisterPrefix.DDT;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the register type is an physical in or output of the plc
|
||||
/// </summary>
|
||||
internal static bool IsPhysicalInOutType(this RegisterPrefix type) {
|
||||
|
||||
return type == RegisterPrefix.X || type == RegisterPrefix.Y;
|
||||
|
||||
}
|
||||
|
||||
internal static bool CompareIsDuplicate(this Register reg1, Register compare) {
|
||||
|
||||
bool valCompare = reg1.RegisterType == compare.RegisterType &&
|
||||
reg1.MemoryAddress == compare.MemoryAddress &&
|
||||
reg1.GetRegisterAddressLen() == compare.GetRegisterAddressLen() &&
|
||||
reg1.GetSpecialAddress() == compare.GetSpecialAddress();
|
||||
|
||||
return valCompare;
|
||||
|
||||
}
|
||||
|
||||
internal static bool CompareIsDuplicateNonCast(this Register toInsert, Register compare, List<Type> allowOverlappingTypes) {
|
||||
|
||||
foreach (var type in allowOverlappingTypes) {
|
||||
|
||||
if (toInsert.GetType() == type) return false;
|
||||
|
||||
}
|
||||
|
||||
bool valCompare = toInsert.GetType() != compare.GetType() &&
|
||||
toInsert.MemoryAddress == compare.MemoryAddress &&
|
||||
toInsert.GetRegisterAddressLen() == compare.GetRegisterAddressLen() &&
|
||||
toInsert.GetSpecialAddress() == compare.GetSpecialAddress();
|
||||
|
||||
return valCompare;
|
||||
|
||||
}
|
||||
|
||||
internal static bool CompareIsNameDuplicate(this Register reg1, Register compare) {
|
||||
|
||||
return (reg1.Name != null || compare.Name != null) && reg1.Name == compare.Name;
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PLC Type Enum Parsing
|
||||
|
||||
/// <summary>
|
||||
/// Gets synonim names for a plc type enum
|
||||
/// </summary>
|
||||
/// <returns>All or just one of there are no synonims for the same <see cref="PlcType"/></returns>
|
||||
public static string[] GetSynonims (this PlcType plcType) {
|
||||
|
||||
return Enum.GetNames(typeof(PlcType)).Where(n => Enum.Parse(typeof(PlcType), n).Equals(plcType)).ToArray();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the enum to a plc name string
|
||||
/// </summary>
|
||||
public static string ToName(this PlcType plcT) {
|
||||
|
||||
if (plcT == 0) return "Unknown";
|
||||
|
||||
if(!Enum.IsDefined(typeof(PlcType), plcT)) return "Unknown";
|
||||
|
||||
return ParsedPlcName.PlcDeconstruct(plcT.ToString()).ToString();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the enum to a decomposed <see cref="ParsedPlcName"/> struct
|
||||
/// </summary>
|
||||
public static ParsedPlcName ToNameDecompose(this PlcType plcT) {
|
||||
|
||||
if ((int)plcT == 0)
|
||||
throw new NotSupportedException("No plc type found");
|
||||
|
||||
if (!Enum.IsDefined(typeof(PlcType), plcT)) return null;
|
||||
|
||||
return ParsedPlcName.PlcDeconstruct(plcT.ToString());
|
||||
|
||||
}
|
||||
|
||||
internal static ParsedPlcName ToNameDecompose(this string plcEnumString) {
|
||||
|
||||
return ParsedPlcName.PlcDeconstruct(plcEnumString);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the PLC type is discontinued
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
#region Mapping
|
||||
|
||||
/// <summary>
|
||||
/// Maps the source object to target object.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of target object.</typeparam>
|
||||
/// <typeparam name="TU">Type of source object.</typeparam>
|
||||
/// <param name="target">Target object.</param>
|
||||
/// <param name="source">Source object.</param>
|
||||
/// <returns>Updated target object.</returns>
|
||||
internal static T Map<T, TU>(this T target, TU source) {
|
||||
|
||||
var flags = BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
|
||||
var tprops = target.GetType().GetProperties();
|
||||
|
||||
tprops.Where(x => x.CanWrite == true).ToList().ForEach(prop => {
|
||||
// check whether source object has the the property
|
||||
var sp = source.GetType().GetProperty(prop.Name);
|
||||
if (sp != null) {
|
||||
// if yes, copy the value to the matching property
|
||||
var value = sp.GetValue(source, null);
|
||||
target.GetType().GetProperty(prop.Name).SetValue(target, value, null);
|
||||
}
|
||||
});
|
||||
|
||||
var tfields = target.GetType().GetFields(flags);
|
||||
tfields.ToList().ForEach(field => {
|
||||
|
||||
var sp = source.GetType().GetField(field.Name, flags);
|
||||
|
||||
if (sp != null) {
|
||||
// if yes, copy the value to the matching property
|
||||
var value = sp.GetValue(source);
|
||||
target.GetType().GetField(field.Name, flags).SetValue(target, value);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
125
MewtocolNet/Helpers/ParsedPlcName.cs
Normal file
125
MewtocolNet/Helpers/ParsedPlcName.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace MewtocolNet {
|
||||
|
||||
/// <summary>
|
||||
/// A structure containing the PLC name parsed
|
||||
/// </summary>
|
||||
public class ParsedPlcName {
|
||||
|
||||
/// <summary>
|
||||
/// Whole name of the PLC
|
||||
/// </summary>
|
||||
public string WholeName { get; internal set; } = "Unknown PLC";
|
||||
|
||||
/// <summary>
|
||||
/// The family group of the PLC
|
||||
/// </summary>
|
||||
public string Group { get; internal set; } = "Unknown Group";
|
||||
|
||||
/// <summary>
|
||||
/// The Memory size of the PLC
|
||||
/// </summary>
|
||||
public float Size { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The subtype strings of the plc
|
||||
/// </summary>
|
||||
public string[] SubTypes { get; internal set; } = new string[] { "N/A" };
|
||||
|
||||
/// <summary>
|
||||
/// Typecode of the parsed string
|
||||
/// </summary>
|
||||
public int TypeCode { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The encoded name, same as enum name
|
||||
/// </summary>
|
||||
public string EncodedName { get; internal set; } = "Unknown";
|
||||
|
||||
/// <summary>
|
||||
/// True if the model is discontinued
|
||||
/// </summary>
|
||||
public bool IsDiscontinuedModel { get; internal set; }
|
||||
|
||||
internal bool WasTestedLive { get; set; }
|
||||
|
||||
internal bool UsesEXRT { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => WholeName;
|
||||
|
||||
internal static ParsedPlcName PlcDeconstruct(string wholeStr) {
|
||||
|
||||
if(wholeStr == "Unknown") {
|
||||
return new ParsedPlcName();
|
||||
}
|
||||
|
||||
var reg = new Regex(@"(?<group>[A-Za-z0-9]*)_(?<size>[A-Za-z0-9]*)(?:__)?(?<additional>.*)");
|
||||
var match = reg.Match(wholeStr);
|
||||
|
||||
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 > 0 ? " " : "")}{string.Join(",", subTypes)}";
|
||||
|
||||
if (string.IsNullOrEmpty(subTypes[0]))
|
||||
subTypes = Array.Empty<string>();
|
||||
|
||||
int typeCode = 999;
|
||||
bool discontinued = false, exrt = false, tested = false;
|
||||
|
||||
if(Enum.TryParse(wholeStr, out PlcType t)) {
|
||||
|
||||
typeCode = (int)t;
|
||||
discontinued = t.IsDiscontinued();
|
||||
|
||||
#if Debug
|
||||
exrt = t.IsEXRTPLC();
|
||||
tested = t.WasTestedLive();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
return new ParsedPlcName {
|
||||
Group = groupStr,
|
||||
Size = sizeFl,
|
||||
SubTypes = subTypes,
|
||||
WholeName = wholeName,
|
||||
EncodedName = wholeStr,
|
||||
TypeCode = typeCode,
|
||||
IsDiscontinuedModel = discontinued,
|
||||
UsesEXRT = exrt,
|
||||
WasTestedLive = tested,
|
||||
};
|
||||
|
||||
} else {
|
||||
|
||||
throw new FormatException($"The plc enum was not formatted correctly: {wholeStr}");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static string SanitizePlcEncodedString(string input) {
|
||||
|
||||
input = input.Replace("d", "-");
|
||||
input = input.Replace("c", ".");
|
||||
input = input.Replace("s", "/");
|
||||
|
||||
return input;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
28
MewtocolNet/Helpers/PlcBitConverter.cs
Normal file
28
MewtocolNet/Helpers/PlcBitConverter.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace MewtocolNet.Helpers {
|
||||
|
||||
internal static class PlcBitConverter {
|
||||
|
||||
internal static string ToVersionNumber(IEnumerable<byte> inBytes, int startIndex = 0) {
|
||||
|
||||
return string.Join(".", inBytes.Skip(startIndex).Take(4).Reverse().Select(x => x.ToString()));
|
||||
|
||||
}
|
||||
|
||||
internal static DateTime ToDateTime(IEnumerable<byte> inBytes, int startIndex = 0) {
|
||||
|
||||
var offDate = new DateTime(2001, 01, 01);
|
||||
|
||||
var secondsOff = BitConverter.ToUInt32(inBytes.ToArray(), startIndex);
|
||||
|
||||
return offDate + TimeSpan.FromSeconds(secondsOff);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
87
MewtocolNet/Helpers/PlcFormat.cs
Normal file
87
MewtocolNet/Helpers/PlcFormat.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace MewtocolNet {
|
||||
|
||||
public static class PlcFormat {
|
||||
|
||||
/// <summary>
|
||||
/// Gets the TimeSpan as a PLC representation string fe.
|
||||
/// <code>
|
||||
/// T#1h10m30s20ms
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <param name="timespan"></param>
|
||||
/// <returns></returns>
|
||||
public static string ToPlcTime(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;
|
||||
int days = timespan.Days;
|
||||
|
||||
if (days > 0) sb.Append($"{days}d");
|
||||
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();
|
||||
|
||||
}
|
||||
|
||||
public static TimeSpan ParsePlcTime(string plcTimeFormat) {
|
||||
|
||||
var reg = new Regex(@"(?:T|t)#(?:(?<d>[0-9]{1,2})d)?(?:(?<h>[0-9]{1,2})h)?(?:(?<m>[0-9]{1,2})m)?(?:(?<s>[0-9]{1,2})s)?(?:(?<ms>[0-9]{1,3})ms)?");
|
||||
var match = reg.Match(plcTimeFormat);
|
||||
|
||||
if (match.Success) {
|
||||
|
||||
var days = match.Groups["d"].Value;
|
||||
var hours = match.Groups["h"].Value;
|
||||
var minutes = match.Groups["m"].Value;
|
||||
var seconds = match.Groups["s"].Value;
|
||||
var milliseconds = match.Groups["ms"].Value;
|
||||
|
||||
TimeSpan retTime = TimeSpan.Zero;
|
||||
|
||||
if (!string.IsNullOrEmpty(days)) retTime += TimeSpan.FromDays(int.Parse(days));
|
||||
if (!string.IsNullOrEmpty(hours)) retTime += TimeSpan.FromHours(int.Parse(hours));
|
||||
if (!string.IsNullOrEmpty(minutes)) retTime += TimeSpan.FromMinutes(int.Parse(minutes));
|
||||
if (!string.IsNullOrEmpty(seconds)) retTime += TimeSpan.FromSeconds(int.Parse(seconds));
|
||||
if (!string.IsNullOrEmpty(milliseconds)) retTime += TimeSpan.FromMilliseconds(int.Parse(milliseconds));
|
||||
|
||||
if ((retTime.TotalMilliseconds % 10) != 0)
|
||||
throw new NotSupportedException("Plc times can't have a millisecond component lower than 10ms");
|
||||
|
||||
return retTime;
|
||||
|
||||
}
|
||||
|
||||
return TimeSpan.Zero;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turns a bit array into a 0 and 1 string
|
||||
/// </summary>
|
||||
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"));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
224
MewtocolNet/IPlc.cs
Normal file
224
MewtocolNet/IPlc.cs
Normal file
@@ -0,0 +1,224 @@
|
||||
using MewtocolNet.Events;
|
||||
using MewtocolNet.ProgramParsing;
|
||||
using MewtocolNet.RegisterBuilding;
|
||||
using MewtocolNet.RegisterBuilding.BuilderPatterns;
|
||||
using MewtocolNet.Registers;
|
||||
using MewtocolNet.UnderlyingRegisters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MewtocolNet {
|
||||
|
||||
/// <summary>
|
||||
/// Provides a interface for Panasonic PLCs
|
||||
/// </summary>
|
||||
public interface IPlc : IDisposable, INotifyPropertyChanged {
|
||||
|
||||
/// <summary>
|
||||
/// Fires when the interface is fully connected to a PLC
|
||||
/// </summary>
|
||||
event PlcConnectionEventHandler Connected;
|
||||
|
||||
/// <summary>
|
||||
/// Fires when a reconnect attempt was successfull
|
||||
/// </summary>
|
||||
event PlcConnectionEventHandler Reconnected;
|
||||
|
||||
/// <summary>
|
||||
/// Fires when the interfaces makes a reconnect try to the PLC
|
||||
/// </summary>
|
||||
event PlcReconnectEventHandler ReconnectTryStarted;
|
||||
|
||||
/// <summary>
|
||||
/// Fires when the plc/interface connection was fully closed
|
||||
/// </summary>
|
||||
event PlcConnectionEventHandler Disconnected;
|
||||
|
||||
/// <summary>
|
||||
/// Fires when the value of a register changes
|
||||
/// </summary>
|
||||
event RegisterChangedEventHandler RegisterChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Plc mode was changed
|
||||
/// </summary>
|
||||
event PlcModeChangedEventHandler ModeChanged;
|
||||
|
||||
/// <summary>
|
||||
/// The current connection state of the interface
|
||||
/// </summary>
|
||||
bool IsConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// This device is sending a message to the plc
|
||||
/// </summary>
|
||||
bool IsSending { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The current transmission speed in bytes per second
|
||||
/// </summary>
|
||||
int BytesPerSecondUpstream { get; }
|
||||
|
||||
/// <summary>
|
||||
/// This device is receiving a message from the plc
|
||||
/// </summary>
|
||||
bool IsReceiving { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The current transmission speed in bytes per second
|
||||
/// </summary>
|
||||
int BytesPerSecondDownstream { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Current poller cycle duration
|
||||
/// </summary>
|
||||
int PollerCycleDurationMs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Shorthand indicator if the plc is in RUN mode
|
||||
/// </summary>
|
||||
bool IsRunMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Currently queued message count
|
||||
/// </summary>
|
||||
int QueuedMessages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Generic information about the connected PLC
|
||||
/// </summary>
|
||||
PLCInfo PlcInfo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The station number of the PLC
|
||||
/// </summary>
|
||||
int StationNumber { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A connection info string
|
||||
/// </summary>
|
||||
string ConnectionInfo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The initial connection timeout in milliseconds
|
||||
/// </summary>
|
||||
int ConnectTimeout { get; set; }
|
||||
|
||||
IEnumerable<IRegister> Registers { get; }
|
||||
|
||||
RBuildAnon Register { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tries to establish a connection with the device asynchronously
|
||||
/// </summary>
|
||||
/// <param name="onConnected">A callback for excecuting something inside the plc connetion process</param>
|
||||
/// <returns></returns>
|
||||
Task<ConnectResult> ConnectAsync(Func<Task> onConnected = null);
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects the device from its current plc connection
|
||||
/// and awaits the end of all asociated tasks
|
||||
/// </summary>
|
||||
Task DisconnectAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects the device from its current plc connection
|
||||
/// </summary>
|
||||
void Disconnect();
|
||||
|
||||
/// <summary>
|
||||
/// Stops a running reconnect task
|
||||
/// </summary>
|
||||
void StopReconnecting();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a task to each reconnect cycle that is run before each individual try
|
||||
/// </summary>
|
||||
void WithReconnectTask(Func<int, Task> callback);
|
||||
|
||||
/// <summary>
|
||||
/// Changes the PLCs operation mode to the given one
|
||||
/// </summary>
|
||||
/// <param name="setRun">True for RUN mode, false for PROG mode</param>
|
||||
/// <returns>The success state of the write operation</returns>
|
||||
Task<bool> SetOperationModeAsync(bool setRun);
|
||||
|
||||
/// <summary>
|
||||
/// Toggles between RUN and PROG mode
|
||||
/// </summary>
|
||||
/// <returns>The success state of the write operation</returns>
|
||||
Task<bool> ToggleOperationModeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Restarts the plc program
|
||||
/// </summary>
|
||||
/// <returns>The success state of the write operation</returns>
|
||||
Task<bool> RestartProgramAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Reads the program from the connected plc
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<PlcBinaryProgram> ReadProgramAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Factory resets the PLC, this includes the current program
|
||||
/// and data in the EEPROM
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task FactoryResetAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Use this to await the first poll iteration after connecting,
|
||||
/// This also completes if the initial connection fails
|
||||
/// </summary>
|
||||
Task AwaitFirstDataCycleAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Runs a single poller cycle manually,
|
||||
/// useful if you want to use a custom update frequency
|
||||
/// </summary>
|
||||
/// <returns>The number of inidvidual mewtocol commands sent</returns>
|
||||
Task<int> UpdateAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connection info string
|
||||
/// </summary>
|
||||
string GetConnectionInfo();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a register from the plc by name
|
||||
/// </summary>
|
||||
IRegister GetRegister(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all registers from the plc
|
||||
/// </summary>
|
||||
IEnumerable<IRegister> GetAllRegisters();
|
||||
|
||||
/// <summary>
|
||||
/// Builds and adds registers to the device
|
||||
/// </summary>
|
||||
/// <param name="builder"></param>
|
||||
void BuildRegisters(Action<RBuildMulti> builder);
|
||||
|
||||
/// <summary>
|
||||
/// Clears all registers atached to the interface
|
||||
/// </summary>
|
||||
void ClearAllRegisters();
|
||||
|
||||
/// <summary>
|
||||
/// Explains the register internal layout at this moment in time
|
||||
/// </summary>
|
||||
string Explain();
|
||||
|
||||
/// <summary>
|
||||
/// A readonly list of the underlying memory areas
|
||||
/// </summary>
|
||||
IReadOnlyList<IMemoryArea> MemoryAreas { get; }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
37
MewtocolNet/IPlcEthernet.cs
Normal file
37
MewtocolNet/IPlcEthernet.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MewtocolNet {
|
||||
|
||||
/// <summary>
|
||||
/// Provides a interface for Panasonic PLCs over a ethernet connection
|
||||
/// </summary>
|
||||
public interface IPlcEthernet : IPlc {
|
||||
|
||||
/// <summary>
|
||||
/// The current IP of the PLC connection
|
||||
/// </summary>
|
||||
string IpAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The current port of the PLC connection
|
||||
/// </summary>
|
||||
int Port { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The host ip endpoint, leave it null to use an automatic interface
|
||||
/// </summary>
|
||||
IPEndPoint HostEndpoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Configures the serial interface
|
||||
/// </summary>
|
||||
/// <param name="_ip">IP adress of the PLC</param>
|
||||
/// <param name="_port">Port of the PLC</param>
|
||||
/// <param name="_station">Station Number of the PLC</param>
|
||||
void ConfigureConnection(string _ip, int _port = 9094, int _station = 1);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
61
MewtocolNet/IPlcSerial.cs
Normal file
61
MewtocolNet/IPlcSerial.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.IO.Ports;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MewtocolNet {
|
||||
|
||||
/// <summary>
|
||||
/// Provides a interface for Panasonic PLCs over a serial port connection
|
||||
/// </summary>
|
||||
public interface IPlcSerial : IPlc {
|
||||
|
||||
/// <summary>
|
||||
/// Port name of the serial port that this device is configured for
|
||||
/// </summary>
|
||||
string PortName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The serial connection baud rate that this device is configured for
|
||||
/// </summary>
|
||||
int SerialBaudRate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The serial connection data bits
|
||||
/// </summary>
|
||||
int SerialDataBits { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The serial connection parity
|
||||
/// </summary>
|
||||
Parity SerialParity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The serial connection stop bits
|
||||
/// </summary>
|
||||
StopBits SerialStopBits { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Is RTS (Request to send) enabled?
|
||||
/// </summary>
|
||||
bool RtsEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the connection settings for the device
|
||||
/// </summary>
|
||||
/// <param name="_portName">Port name of COM port</param>
|
||||
/// <param name="_baudRate">The serial connection baud rate</param>
|
||||
/// <param name="_dataBits">The serial connection data bits</param>
|
||||
/// <param name="_parity">The serial connection parity</param>
|
||||
/// <param name="_stopBits">The serial connection stop bits</param>
|
||||
/// <param name="_rtsEnable">Is RTS (Request to send) enabled?</param>
|
||||
/// <param name="_station">The station number of the PLC</param>
|
||||
void ConfigureConnection(string _portName, int _baudRate = 19200, int _dataBits = 8, Parity _parity = Parity.Odd, StopBits _stopBits = StopBits.One, bool _rtsEnable = true, int _station = 1);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to establish a connection with the device asynchronously
|
||||
/// </summary>
|
||||
Task<ConnectResult> ConnectAsync(Func<Task> callBack, Action onTryingConfig);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
12
MewtocolNet/InternalEnums/CommandState.cs
Normal file
12
MewtocolNet/InternalEnums/CommandState.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace MewtocolNet {
|
||||
|
||||
internal enum CommandState {
|
||||
|
||||
Initial,
|
||||
LineFeed,
|
||||
RequestedNextFrame,
|
||||
Complete
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
112
MewtocolNet/Logging/Logger.cs
Normal file
112
MewtocolNet/Logging/Logger.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace MewtocolNet.Logging {
|
||||
|
||||
/// <summary>
|
||||
/// Logging module for all PLCs
|
||||
/// </summary>
|
||||
public static class Logger {
|
||||
|
||||
/// <summary>
|
||||
/// Sets the loglevel for the global logging module
|
||||
/// </summary>
|
||||
public static LogLevel LogLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines the default output logger targets
|
||||
/// </summary>
|
||||
public static LoggerTargets DefaultTargets { get; set; } = LoggerTargets.Console;
|
||||
|
||||
internal static Action<DateTime, LogLevel, string> LogInvoked;
|
||||
|
||||
static Logger () {
|
||||
|
||||
var isConsoleApplication = Console.LargestWindowWidth != 0;
|
||||
|
||||
OnNewLogMessage((d, l, m) => {
|
||||
|
||||
if(isConsoleApplication && DefaultTargets.HasFlag(LoggerTargets.Console)) {
|
||||
|
||||
switch (l) {
|
||||
case LogLevel.Error:
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"{d:hh:mm:ss:ff} {m}");
|
||||
break;
|
||||
case LogLevel.Info:
|
||||
Console.WriteLine($"{d:hh:mm:ss:ff} {m}");
|
||||
break;
|
||||
case LogLevel.Change:
|
||||
Console.ForegroundColor = ConsoleColor.DarkBlue;
|
||||
Console.WriteLine($"{d:hh:mm:ss:ff} {m}");
|
||||
break;
|
||||
case LogLevel.Verbose:
|
||||
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
||||
Console.WriteLine($"{d:hh:mm:ss:ff} {m}");
|
||||
break;
|
||||
case LogLevel.Critical:
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($"{d:hh:mm:ss:ff} {m}");
|
||||
break;
|
||||
}
|
||||
|
||||
Console.ResetColor();
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
LogInvoked += (d, l, m) => {
|
||||
|
||||
if (DefaultTargets.HasFlag(LoggerTargets.Trace)) {
|
||||
|
||||
Trace.WriteLine($"{d:hh:mm:ss:ff} {m}");
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
//for static calling purposes only
|
||||
internal static void Start() { }
|
||||
|
||||
/// <summary>
|
||||
/// Gets invoked whenever a new log message is ready
|
||||
/// </summary>
|
||||
public static void OnNewLogMessage(Action<DateTime, LogLevel, string> onMsg, LogLevel? maxLevel = null) {
|
||||
|
||||
if (maxLevel == null) maxLevel = LogLevel;
|
||||
|
||||
LogInvoked += (t, l, m) => {
|
||||
|
||||
if ((int)l <= (int)maxLevel) {
|
||||
onMsg(t, l, m);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
internal static void Log(string message, LogLevel loglevel, MewtocolInterface sender = null) {
|
||||
|
||||
if (sender == null) {
|
||||
LogInvoked?.Invoke(DateTime.Now, loglevel, message);
|
||||
} else {
|
||||
LogInvoked?.Invoke(DateTime.Now, loglevel, $"[{sender.GetConnectionInfo()}] {message}");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal static void LogError (string message, MewtocolInterface sender = null) => Log(message, LogLevel.Error, sender);
|
||||
|
||||
internal static void Log (string message, MewtocolInterface sender = null) => Log(message, LogLevel.Info, sender);
|
||||
|
||||
internal static void LogChange (string message, MewtocolInterface sender = null) => Log(message, LogLevel.Change, sender);
|
||||
|
||||
internal static void LogVerbose (string message, MewtocolInterface sender = null) => Log(message, LogLevel.Verbose, sender);
|
||||
|
||||
internal static void LogCritical (string message, MewtocolInterface sender = null) => Log(message, LogLevel.Critical, sender);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace MewtocolNet.Logging {
|
||||
namespace MewtocolNet.Logging {
|
||||
|
||||
/// <summary>
|
||||
/// The loglevel of the logging module
|
||||
/// </summary>
|
||||
public enum LogLevel {
|
||||
|
||||
/// <summary>
|
||||
/// Logs nothing
|
||||
/// </summary>
|
||||
None = -1,
|
||||
/// <summary>
|
||||
/// Logs only errors
|
||||
/// </summary>
|
||||
12
MewtocolNet/Logging/LoggerTargets.cs
Normal file
12
MewtocolNet/Logging/LoggerTargets.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace MewtocolNet.Logging {
|
||||
[Flags]
|
||||
public enum LoggerTargets {
|
||||
|
||||
None = 0,
|
||||
Console = 1,
|
||||
Trace = 2,
|
||||
|
||||
}
|
||||
}
|
||||
498
MewtocolNet/Mewtocol.cs
Normal file
498
MewtocolNet/Mewtocol.cs
Normal file
@@ -0,0 +1,498 @@
|
||||
using MewtocolNet.RegisterAttributes;
|
||||
using MewtocolNet.RegisterBuilding;
|
||||
using MewtocolNet.RegisterBuilding.BuilderPatterns;
|
||||
using MewtocolNet.SetupClasses;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Ports;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using static MewtocolNet.Mewtocol;
|
||||
|
||||
namespace MewtocolNet {
|
||||
|
||||
/// <summary>
|
||||
/// Builder helper for mewtocol interfaces
|
||||
/// </summary>
|
||||
public static class Mewtocol {
|
||||
|
||||
#region Data Access
|
||||
|
||||
/// <summary>
|
||||
/// Lists all usable COM port names
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static IEnumerable<string> GetSerialPortNames () => SerialPort.GetPortNames();
|
||||
|
||||
/// <summary>
|
||||
/// Lists all usable serial baud rates
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static IEnumerable<int> GetUseableBaudRates() => Enum.GetValues(typeof(BaudRate)).Cast<BaudRate>().Select(x => (int)x);
|
||||
|
||||
/// <summary>
|
||||
/// Lists all useable source endpoints of the device this is running on for usage with PLCs
|
||||
/// </summary>
|
||||
public static IEnumerable<IPEndPoint> GetSourceEndpoints() {
|
||||
|
||||
foreach (var netIf in GetUseableNetInterfaces()) {
|
||||
|
||||
var addressInfo = netIf.GetIPProperties().UnicastAddresses
|
||||
.FirstOrDefault(x => x.Address.AddressFamily == AddressFamily.InterNetwork);
|
||||
|
||||
yield return new IPEndPoint(addressInfo.Address, 9094);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists all useable network interfaces of the device this is running on for usage with PLCs
|
||||
/// </summary>
|
||||
public static IEnumerable<NetworkInterface> 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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Interface building step 1
|
||||
|
||||
/// <summary>
|
||||
/// Builds a ethernet based Mewtocol Interface
|
||||
/// </summary>
|
||||
/// <param name="ip"></param>
|
||||
/// <param name="port"></param>
|
||||
/// <param name="station">Plc station number 0xEE for direct communication</param>
|
||||
/// <returns></returns>
|
||||
public static PostInitEth<IPlcEthernet> Ethernet(string ip, int port = 9094, int station = 0xEE) {
|
||||
|
||||
var instance = new MewtocolInterfaceTcp();
|
||||
instance.ConfigureConnection(ip, port, station);
|
||||
return new PostInitEth<IPlcEthernet> {
|
||||
intf = instance
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a ethernet based Mewtocol Interface
|
||||
/// </summary>
|
||||
/// <param name="ip"></param>
|
||||
/// <param name="port"></param>
|
||||
/// <param name="station">Plc station number 0xEE for direct communication</param>
|
||||
/// <returns></returns>
|
||||
public static PostInitEth<IPlcEthernet> Ethernet(IPAddress ip, int port = 9094, int station = 0xEE) {
|
||||
|
||||
var instance = new MewtocolInterfaceTcp();
|
||||
instance.ConfigureConnection(ip, port, station);
|
||||
return new PostInitEth<IPlcEthernet> {
|
||||
intf = instance
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a serial port based Mewtocol Interface
|
||||
/// </summary>
|
||||
/// <param name="portName">System port name</param>
|
||||
/// <param name="baudRate">Baud rate of the plc toolport</param>
|
||||
/// <param name="dataBits">DataBits of the plc toolport</param>
|
||||
/// <param name="parity">Parity rate of the plc toolport</param>
|
||||
/// <param name="stopBits">Stop bits of the plc toolport</param>
|
||||
/// <param name="rtsEnabled">Is RTS (Request to send) enabled?</param>
|
||||
/// <param name="station">Plc station number 0xEE for direct communication</param>
|
||||
/// <returns></returns>
|
||||
public static PostInit<IPlcSerial> Serial(string portName, BaudRate baudRate = BaudRate._19200, DataBits dataBits = DataBits.Eight, Parity parity = Parity.Odd, StopBits stopBits = StopBits.One, bool rtsEnabled = true, int station = 0xEE) {
|
||||
|
||||
var instance = new MewtocolInterfaceSerial();
|
||||
instance.ConfigureConnection(portName, (int)baudRate, (int)dataBits, parity, stopBits, rtsEnabled, station);
|
||||
return new PostInit<IPlcSerial> {
|
||||
intf = instance
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a serial mewtocol interface that finds the correct settings for the given port name automatically
|
||||
/// </summary>
|
||||
/// <param name="portName"></param>
|
||||
/// <param name="rtsEnabled">Is RTS (Request to send) enabled?</param>
|
||||
/// <param name="station">Plc station number 0xEE for direct communication</param>
|
||||
/// <returns></returns>
|
||||
public static PostInit<IPlcSerial> SerialAuto(string portName, bool rtsEnabled = true, int station = 0xEE) {
|
||||
|
||||
var instance = new MewtocolInterfaceSerial();
|
||||
instance.ConfigureConnection(portName, station, rtsEnable: rtsEnabled);
|
||||
instance.ConfigureConnectionAuto();
|
||||
return new PostInit<IPlcSerial> {
|
||||
intf = instance
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Interface building step 2
|
||||
|
||||
public class PollLevelConfigurator {
|
||||
|
||||
internal Dictionary<int, PollLevelConfig> levelConfigs = new Dictionary<int, PollLevelConfig>();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the poll level for the given key
|
||||
/// </summary>
|
||||
/// <param name="level">The level to reference</param>
|
||||
/// <param name="interval">Delay between poll requests</param>
|
||||
public PollLevelConfigurator SetLevel(int level, TimeSpan interval) {
|
||||
|
||||
if (level == PollLevel.Always || level == PollLevel.Never || level == PollLevel.FirstIteration)
|
||||
throw new NotSupportedException("The poll level is reserved for the library");
|
||||
|
||||
if (!levelConfigs.ContainsKey(level)) {
|
||||
levelConfigs.Add(level, new PollLevelConfig {
|
||||
delay = interval,
|
||||
});
|
||||
} else {
|
||||
throw new NotSupportedException("Can't set poll levels multiple times");
|
||||
}
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
public PollLevelConfigurator SetLevel(int level, int skipNth) {
|
||||
|
||||
if (level == PollLevel.Always || level == PollLevel.Never || level == PollLevel.FirstIteration)
|
||||
throw new NotSupportedException("The poll level is reserved for the library");
|
||||
|
||||
if (!levelConfigs.ContainsKey(level)) {
|
||||
levelConfigs.Add(level, new PollLevelConfig {
|
||||
skipNth = skipNth,
|
||||
});
|
||||
} else {
|
||||
throw new NotSupportedException("Can't set poll levels multiple times");
|
||||
}
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class RegCollector {
|
||||
|
||||
internal List<RegisterCollection> collections = new List<RegisterCollection>();
|
||||
|
||||
public T AddCollection<T>(T collection) where T : RegisterCollection {
|
||||
|
||||
collections.Add(collection);
|
||||
|
||||
return collection;
|
||||
|
||||
}
|
||||
|
||||
public T AddCollection<T>() where T : RegisterCollection {
|
||||
|
||||
var instance = (RegisterCollection)Activator.CreateInstance(typeof(T));
|
||||
|
||||
collections.Add(instance);
|
||||
|
||||
return (T)instance;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class PostInitEth<T> : PostInit<T> {
|
||||
|
||||
/// <summary>
|
||||
/// Sets the source of the outgoing ethernet connection
|
||||
/// </summary>
|
||||
public PostInit<T> FromSource (IPEndPoint endpoint) {
|
||||
|
||||
if(endpoint == null)
|
||||
throw new ArgumentNullException("Endpoint can't be null", nameof(endpoint));
|
||||
|
||||
if(intf is MewtocolInterfaceTcp imew) {
|
||||
|
||||
imew.HostEndpoint = endpoint;
|
||||
|
||||
}
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the source of the outgoing ethernet connection
|
||||
/// </summary>
|
||||
/// <param name="ip">IP address of the source interface (Format: 127.0.0.1)</param>
|
||||
/// <param name="port">Port of the source interface</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public PostInit<T> FromSource(string ip, int port) {
|
||||
|
||||
if (intf is MewtocolInterfaceTcp imew) {
|
||||
|
||||
if(port < IPEndPoint.MinPort)
|
||||
throw new ArgumentException($"Source port cant be smaller than {IPEndPoint.MinPort}", nameof(port));
|
||||
|
||||
if (port > IPEndPoint.MaxPort)
|
||||
throw new ArgumentException($"Source port cant be larger than {IPEndPoint.MaxPort}", nameof(port));
|
||||
|
||||
if (!IPAddress.TryParse(ip, out var ipParsed))
|
||||
throw new ArgumentException("Failed to parse the source IP", nameof(ip));
|
||||
|
||||
imew.HostEndpoint = new IPEndPoint(ipParsed, port);
|
||||
|
||||
}
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class PostInit<T> {
|
||||
|
||||
internal T intf;
|
||||
|
||||
/// <summary>
|
||||
/// Attaches a auto poller to the interface that reads all registers
|
||||
/// cyclic
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public PostInit<T> WithPoller() {
|
||||
|
||||
if (intf is MewtocolInterface imew) {
|
||||
imew.usePoller = true;
|
||||
}
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// General setting for the memory manager
|
||||
/// </summary>
|
||||
public PostInit<T> WithInterfaceSettings(Action<InterfaceSettings> settings) {
|
||||
|
||||
var res = new InterfaceSettings();
|
||||
settings.Invoke(res);
|
||||
|
||||
if (res.MaxOptimizationDistance < 0)
|
||||
throw new NotSupportedException($"A value lower than 0 is not allowed for " +
|
||||
$"{nameof(InterfaceSettings.MaxOptimizationDistance)}");
|
||||
|
||||
if (res.MaxDataBlocksPerWrite < 1)
|
||||
throw new NotSupportedException($"A value lower than 1 is not allowed for " +
|
||||
$"{nameof(InterfaceSettings.MaxDataBlocksPerWrite)}");
|
||||
|
||||
if (intf is MewtocolInterface imew) {
|
||||
|
||||
imew.memoryManager.maxOptimizationDistance = res.MaxOptimizationDistance;
|
||||
imew.memoryManager.pollLevelOrMode = res.PollLevelOverwriteMode;
|
||||
|
||||
imew.maxDataBlocksPerWrite = res.MaxDataBlocksPerWrite;
|
||||
imew.heartbeatIntervalMs = res.HeartbeatIntervalMs;
|
||||
imew.tryReconnectAttempts = res.TryReconnectAttempts;
|
||||
imew.tryReconnectDelayMs = res.TryReconnectDelayMs;
|
||||
|
||||
imew.alwaysGetMetadata = res.AlwaysGetMetadata;
|
||||
|
||||
}
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A builder for poll custom levels
|
||||
/// </summary>
|
||||
public PostInit<T> WithCustomPollLevels(Action<PollLevelConfigurator> levels) {
|
||||
|
||||
var res = new PollLevelConfigurator();
|
||||
levels.Invoke(res);
|
||||
|
||||
if (intf is MewtocolInterface imew) {
|
||||
|
||||
imew.memoryManager.pollLevelConfigs = res.levelConfigs;
|
||||
|
||||
}
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A builder for attaching register collections
|
||||
/// </summary>
|
||||
public PostInit<T> WithRegisterCollections(Action<RegCollector> collector) {
|
||||
|
||||
try {
|
||||
|
||||
var res = new RegCollector();
|
||||
collector.Invoke(res);
|
||||
|
||||
if (intf is MewtocolInterface imew) {
|
||||
imew.WithRegisterCollections(res.collections);
|
||||
}
|
||||
|
||||
return this;
|
||||
|
||||
} catch {
|
||||
|
||||
throw;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A builder for attaching register collections
|
||||
/// </summary>
|
||||
public PostInit<T> WithRegisters(Action<RBuildMulti> builder) {
|
||||
|
||||
try {
|
||||
|
||||
var plc = (MewtocolInterface)(object)intf;
|
||||
var regBuilder = new RBuildMulti(plc);
|
||||
|
||||
builder.Invoke(regBuilder);
|
||||
|
||||
plc.AddRegisters(regBuilder.assembler.assembled.ToArray());
|
||||
|
||||
return this;
|
||||
|
||||
} catch {
|
||||
|
||||
throw;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a task to each reconnect cycle that is run before each individual try
|
||||
/// </summary>
|
||||
public PostInit<T> WithReconnectTask(Func<int, Task> callback) {
|
||||
|
||||
var plc = (MewtocolInterface)(object)intf;
|
||||
|
||||
plc.onBeforeReconnectTryTask = callback;
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a task to each heartbeat cycle that is run before each individual cycle request
|
||||
/// </summary>
|
||||
public EndInitSetup<T> WithHeartbeatTask(Func<IPlc,Task> heartBeatAsync, bool executeInProg = false) {
|
||||
try {
|
||||
|
||||
var plc = (MewtocolInterface)(object)this.intf;
|
||||
|
||||
plc.heartbeatCallbackTask = heartBeatAsync;
|
||||
plc.execHeartBeatCallbackTaskInProg = executeInProg;
|
||||
|
||||
return new EndInitSetup<T> {
|
||||
postInit = this,
|
||||
};
|
||||
|
||||
} catch {
|
||||
|
||||
throw;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Repeats the passed method each time the hearbeat is triggered,
|
||||
/// use
|
||||
/// </summary>
|
||||
/// <param name="heartBeatAsync"></param>
|
||||
/// <returns></returns>
|
||||
public EndInitSetup<T> WithHeartbeatTask(Func<Task> heartBeatAsync, bool executeInProg = false) => WithHeartbeatTask(heartBeatAsync, executeInProg);
|
||||
|
||||
/// <summary>
|
||||
/// Disables all heartbeat tasks
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public EndInitSetup<T> DisableHeartBeat() {
|
||||
|
||||
try {
|
||||
|
||||
var plc = (MewtocolInterface)(object)this.intf;
|
||||
|
||||
plc.disableHeartbeat = true;
|
||||
|
||||
return new EndInitSetup<T> {
|
||||
postInit = this,
|
||||
};
|
||||
|
||||
} catch {
|
||||
|
||||
throw;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds and returns the final plc interface
|
||||
/// </summary>
|
||||
public T Build() => (T)(object)((MewtocolInterface)(object)intf).Build();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Interface building step 4
|
||||
|
||||
public class EndInitSetup<T> {
|
||||
|
||||
internal PostInit<T> postInit;
|
||||
|
||||
/// <summary>
|
||||
/// Builds and returns the final plc interface
|
||||
/// </summary>
|
||||
public T Build() => (T)(object)((MewtocolInterface)(object)postInit.intf).Build();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace MewtocolNet.Registers {
|
||||
|
||||
/// <summary>
|
||||
/// Contains information about the plc and its cpu
|
||||
/// </summary>
|
||||
public partial class CpuInfo {
|
||||
|
||||
/// <summary>
|
||||
/// The cpu type of the plc
|
||||
/// </summary>
|
||||
public CpuType Cputype { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Program capacity in 1K steps
|
||||
/// </summary>
|
||||
public int ProgramCapacity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Version of the cpu
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
/// <summary>
|
||||
/// The PLC com interface class
|
||||
/// </summary>
|
||||
public partial class MewtocolInterface {
|
||||
|
||||
/// <summary>
|
||||
/// True if the auto poller is currently paused
|
||||
/// </summary>
|
||||
public bool PollingPaused => pollerIsPaused;
|
||||
|
||||
/// <summary>
|
||||
/// True if the poller is actvice (can be paused)
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Kills the poller completely
|
||||
/// </summary>
|
||||
internal void KillPoller () {
|
||||
|
||||
pollerTaskRunning = false;
|
||||
pollerTaskStopped = true;
|
||||
|
||||
ClearRegisterVals();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pauses the polling and waits for the last message to be sent
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task PausePollingAsync () {
|
||||
|
||||
if (!pollerTaskRunning)
|
||||
return;
|
||||
|
||||
pollerTaskRunning = false;
|
||||
|
||||
while (!pollerIsPaused) {
|
||||
|
||||
if (pollerIsPaused)
|
||||
break;
|
||||
|
||||
await Task.Delay(10);
|
||||
|
||||
}
|
||||
|
||||
pollerTaskRunning = false;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resumes the polling
|
||||
/// </summary>
|
||||
public void ResumePolling () {
|
||||
|
||||
pollerTaskRunning = true;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches a continous reader that reads back the Registers and Contacts
|
||||
/// </summary>
|
||||
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<short> shortReg) {
|
||||
var lastVal = shortReg.Value;
|
||||
var readout = (await ReadNumRegister(shortReg)).Register.Value;
|
||||
if (lastVal != readout) {
|
||||
InvokeRegisterChanged(shortReg);
|
||||
}
|
||||
}
|
||||
if (reg is NRegister<ushort> ushortReg) {
|
||||
var lastVal = ushortReg.Value;
|
||||
var readout = (await ReadNumRegister(ushortReg)).Register.Value;
|
||||
if (lastVal != readout) {
|
||||
InvokeRegisterChanged(ushortReg);
|
||||
}
|
||||
}
|
||||
if (reg is NRegister<int> intReg) {
|
||||
var lastVal = intReg.Value;
|
||||
var readout = (await ReadNumRegister(intReg)).Register.Value;
|
||||
if (lastVal != readout) {
|
||||
InvokeRegisterChanged(intReg);
|
||||
}
|
||||
}
|
||||
if (reg is NRegister<uint> uintReg) {
|
||||
var lastVal = uintReg.Value;
|
||||
var readout = (await ReadNumRegister(uintReg)).Register.Value;
|
||||
if (lastVal != readout) {
|
||||
InvokeRegisterChanged(uintReg);
|
||||
}
|
||||
}
|
||||
if (reg is NRegister<float> floatReg) {
|
||||
var lastVal = floatReg.Value;
|
||||
var readout = (await ReadNumRegister(floatReg)).Register.Value;
|
||||
if (lastVal != readout) {
|
||||
InvokeRegisterChanged(floatReg);
|
||||
}
|
||||
}
|
||||
if (reg is NRegister<TimeSpan> 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
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds a PLC memory register to the watchlist <para/>
|
||||
/// The registers can be read back by attaching <see cref="WithPoller"/>
|
||||
/// </summary>
|
||||
/// <param name="_address">The address of the register in the PLCs memory</param>
|
||||
/// <param name="_type">
|
||||
/// The memory area type
|
||||
/// <para>X = Physical input area (bool)</para>
|
||||
/// <para>Y = Physical input area (bool)</para>
|
||||
/// <para>R = Internal relay area (bool)</para>
|
||||
/// <para>DT = Internal data area (short/ushort)</para>
|
||||
/// <para>DDT = Internal relay area (int/uint)</para>
|
||||
/// </param>
|
||||
/// <param name="_name">A naming definition for QOL, doesn't effect PLC and is optional</param>
|
||||
public void AddRegister (int _address, RegisterType _type, string _name = null) {
|
||||
|
||||
IRegister toAdd = null;
|
||||
|
||||
//as number registers
|
||||
if (_type == RegisterType.DT_short) {
|
||||
toAdd = new NRegister<short>(_address, _name);
|
||||
}
|
||||
if (_type == RegisterType.DT_ushort) {
|
||||
toAdd = new NRegister<ushort>(_address, _name);
|
||||
}
|
||||
if (_type == RegisterType.DDT_int) {
|
||||
toAdd = new NRegister<int>(_address, _name);
|
||||
}
|
||||
if (_type == RegisterType.DDT_uint) {
|
||||
toAdd = new NRegister<uint>(_address, _name);
|
||||
}
|
||||
if (_type == RegisterType.DDT_float) {
|
||||
toAdd = new NRegister<float>(_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<short>(_address, _name).WithCollectionType(_colType);
|
||||
}
|
||||
if (_type == RegisterType.DT_ushort) {
|
||||
toAdd = new NRegister<ushort>(_address, _name).WithCollectionType(_colType);
|
||||
}
|
||||
if (_type == RegisterType.DDT_int) {
|
||||
toAdd = new NRegister<int>(_address, _name).WithCollectionType(_colType);
|
||||
}
|
||||
if (_type == RegisterType.DDT_uint) {
|
||||
toAdd = new NRegister<uint>(_address, _name).WithCollectionType(_colType);
|
||||
}
|
||||
if (_type == RegisterType.DDT_float) {
|
||||
toAdd = new NRegister<float>(_address, _name).WithCollectionType(_colType);
|
||||
}
|
||||
|
||||
if (toAdd == null) {
|
||||
toAdd = new BRegister(_address, _type, _name).WithCollectionType(_colType);
|
||||
}
|
||||
|
||||
Registers.Add(toAdd);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a PLC memory register to the watchlist <para/>
|
||||
/// The registers can be read back by attaching <see cref="WithPoller"/>
|
||||
/// </summary>
|
||||
/// <param name="_spAddress">The special address of the register in the PLCs memory</param>
|
||||
/// <param name="_type">
|
||||
/// The memory area type
|
||||
/// <para>X = Physical input area (bool)</para>
|
||||
/// <para>Y = Physical input area (bool)</para>
|
||||
/// <para>R = Internal relay area (bool)</para>
|
||||
/// <para>DT = Internal data area (short/ushort)</para>
|
||||
/// <para>DDT = Internal relay area (int/uint)</para>
|
||||
/// </param>
|
||||
/// <param name="_name">A naming definition for QOL, doesn't effect PLC and is optional</param>
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a PLC memory register to the watchlist <para/>
|
||||
/// The registers can be read back by attaching <see cref="WithPoller"/>
|
||||
/// </summary>
|
||||
/// <typeparam name="T">
|
||||
/// The type of the register translated from C# to IEC 61131-3 types
|
||||
/// <para>C# ------ IEC</para>
|
||||
/// <para>short => INT/WORD</para>
|
||||
/// <para>ushort => UINT</para>
|
||||
/// <para>int => DOUBLE</para>
|
||||
/// <para>uint => UDOUBLE</para>
|
||||
/// <para>float => REAL</para>
|
||||
/// <para>string => STRING</para>
|
||||
/// </typeparam>
|
||||
/// <param name="_name">A naming definition for QOL, doesn't effect PLC and is optional</param>
|
||||
/// <param name="_address">The address of the register in the PLCs memory</param>
|
||||
/// <param name="_length">The length of the string (Can be ignored for other types)</param>
|
||||
public void AddRegister<T>(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<short>(_address, _name);
|
||||
} else if (regType == typeof(ushort)) {
|
||||
toAdd = new NRegister<ushort>(_address, _name);
|
||||
} else if (regType == typeof(int)) {
|
||||
toAdd = new NRegister<int>(_address, _name);
|
||||
} else if (regType == typeof(uint)) {
|
||||
toAdd = new NRegister<uint>(_address, _name);
|
||||
} else if (regType == typeof(float)) {
|
||||
toAdd = new NRegister<float>(_address, _name);
|
||||
} else if (regType == typeof(string)) {
|
||||
toAdd = new SRegister(_address, _length, _name);
|
||||
} else if (regType == typeof(TimeSpan)) {
|
||||
toAdd = new NRegister<TimeSpan>(_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<T> (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<short>(_address, propName, _isBitwise, _enumType).WithCollectionType(_colType);
|
||||
} else if (regType == typeof(ushort)) {
|
||||
reg = new NRegister<ushort>(_address, propName).WithCollectionType(_colType);
|
||||
} else if (regType == typeof(int)) {
|
||||
reg = new NRegister<int>(_address, propName, _isBitwise, _enumType).WithCollectionType(_colType);
|
||||
} else if (regType == typeof(uint)) {
|
||||
reg = new NRegister<uint>(_address, propName).WithCollectionType(_colType);
|
||||
} else if (regType == typeof(float)) {
|
||||
reg = new NRegister<float>(_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<TimeSpan>(_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
|
||||
|
||||
/// <summary>
|
||||
/// Gets a register that was added by its name
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IRegister GetRegister (string name) {
|
||||
|
||||
return Registers.FirstOrDefault(x => x.Name == name);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a register that was added by its name
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of register</typeparam>
|
||||
/// <returns>A casted register or the <code>default</code> value</returns>
|
||||
public T GetRegister<T> (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
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of all added registers
|
||||
/// </summary>
|
||||
public List<IRegister> GetAllRegisters () {
|
||||
|
||||
return Registers;
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Invoking
|
||||
|
||||
internal void InvokeRegisterChanged (IRegister reg) {
|
||||
|
||||
RegisterChanged?.Invoke(reg);
|
||||
|
||||
}
|
||||
|
||||
internal void InvokePolledCycleDone () {
|
||||
|
||||
PolledCycle?.Invoke();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace MewtocolNet {
|
||||
|
||||
/// <summary>
|
||||
/// An interface for all register types
|
||||
/// </summary>
|
||||
public interface IRegister {
|
||||
|
||||
/// <summary>
|
||||
/// Gets called whenever the value was changed
|
||||
/// </summary>
|
||||
event Action<object> ValueChanged;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the register
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The current value of the register
|
||||
/// </summary>
|
||||
object Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The plc memory address of the register
|
||||
/// </summary>
|
||||
int MemoryAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the register is processed bitwise
|
||||
/// </summary>
|
||||
/// <returns>True if bitwise</returns>
|
||||
bool IsUsedBitwise();
|
||||
|
||||
/// <summary>
|
||||
/// Generates a string describing the starting memory address of the register
|
||||
/// </summary>
|
||||
string GetStartingMemoryArea();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value formatted as a readable string
|
||||
/// </summary>
|
||||
string GetValueString();
|
||||
|
||||
/// <summary>
|
||||
/// Builds the identifier for the mewtocol query string
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
string BuildMewtocolQuery();
|
||||
|
||||
/// <summary>
|
||||
/// Builds a register string that prepends the memory address fe. DDT or DT, X, Y etc
|
||||
/// </summary>
|
||||
string GetRegisterString();
|
||||
|
||||
/// <summary>
|
||||
/// Builds a combined name for the attached property to uniquely identify the property register binding
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
string GetCombinedName();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the class that contains the attached property
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
string GetContainerName();
|
||||
|
||||
/// <summary>
|
||||
/// Builds a register name after the PLC convention <br/>
|
||||
/// Example <code>DDT100, XA, X6, Y1, DT3300</code>
|
||||
/// </summary>
|
||||
string GetRegisterPLCName();
|
||||
|
||||
/// <summary>
|
||||
/// Clears the current value of the register and resets it to default
|
||||
/// </summary>
|
||||
void ClearValue();
|
||||
|
||||
/// <summary>
|
||||
/// Triggers a notifychanged update event
|
||||
/// </summary>
|
||||
void TriggerNotifyChange();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the class collection its attached property is in or null
|
||||
/// </summary>
|
||||
/// <returns>The class name or null if manually added</returns>
|
||||
Type GetCollectionType();
|
||||
|
||||
/// <summary>
|
||||
/// Builds a readable string with all important register informations
|
||||
/// </summary>
|
||||
string ToString();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace MewtocolNet.Logging {
|
||||
|
||||
/// <summary>
|
||||
/// Logging module for all PLCs
|
||||
/// </summary>
|
||||
public static class Logger {
|
||||
|
||||
/// <summary>
|
||||
/// Sets the loglevel for the logger module
|
||||
/// </summary>
|
||||
public static LogLevel LogLevel { get; set; }
|
||||
|
||||
internal static Action<DateTime, string> LogInvoked;
|
||||
|
||||
/// <summary>
|
||||
/// Gets invoked whenever a new log message is ready
|
||||
/// </summary>
|
||||
public static void OnNewLogMessage (Action<DateTime, string> 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}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,235 +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 {
|
||||
|
||||
/// <summary>
|
||||
/// Contains helper methods
|
||||
/// </summary>
|
||||
public static class MewtocolHelpers {
|
||||
|
||||
/// <summary>
|
||||
/// Turns a bit array into a 0 and 1 string
|
||||
/// </summary>
|
||||
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"));
|
||||
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
internal static byte[] ParseDTBytes (this string _onString ,int _blockSize = 4) {
|
||||
var res = new Regex(@"\%([0-9]{2})\$RD(.{"+_blockSize+"})").Match(_onString);
|
||||
if(res.Success) {
|
||||
string val = res.Groups[2].Value;
|
||||
return val.HexStringToByteArray();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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, int _blockSize = 4) {
|
||||
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<String> 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<byte> oldBL = new List<byte>(arr);
|
||||
|
||||
List<byte> tempL = new List<byte>();
|
||||
|
||||
//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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The PLC com interface class
|
||||
/// </summary>
|
||||
public partial class MewtocolInterface : INotifyPropertyChanged, IDisposable {
|
||||
|
||||
/// <summary>
|
||||
/// Gets triggered when the PLC connection was established
|
||||
/// </summary>
|
||||
public event Action<PLCInfo> Connected;
|
||||
|
||||
/// <summary>
|
||||
/// Gets triggered when the PLC connection was closed or lost
|
||||
/// </summary>
|
||||
public event Action Disconnected;
|
||||
|
||||
/// <summary>
|
||||
/// Gets triggered when a registered data register changes its value
|
||||
/// </summary>
|
||||
public event Action<IRegister> RegisterChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets triggered when a property of the interface changes
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
private int connectTimeout = 3000;
|
||||
/// <summary>
|
||||
/// The initial connection timeout in milliseconds
|
||||
/// </summary>
|
||||
public int ConnectTimeout {
|
||||
get { return connectTimeout; }
|
||||
set { connectTimeout = value; }
|
||||
}
|
||||
|
||||
private volatile int pollerDelayMs = 0;
|
||||
/// <summary>
|
||||
/// Delay for each poller cycle in milliseconds, default = 0
|
||||
/// </summary>
|
||||
public int PollerDelayMs {
|
||||
get => pollerDelayMs;
|
||||
set {
|
||||
pollerDelayMs = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PollerDelayMs)));
|
||||
}
|
||||
}
|
||||
|
||||
private volatile int queuedMessages;
|
||||
/// <summary>
|
||||
/// Currently queued Messages
|
||||
/// </summary>
|
||||
public int QueuedMessages {
|
||||
get => queuedMessages;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The host ip endpoint, leave it null to use an automatic interface
|
||||
/// </summary>
|
||||
public IPEndPoint HostEndpoint { get; set; }
|
||||
|
||||
private bool isConnected;
|
||||
/// <summary>
|
||||
/// The current connection state of the interface
|
||||
/// </summary>
|
||||
public bool IsConnected {
|
||||
get => isConnected;
|
||||
private set {
|
||||
isConnected = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsConnected)));
|
||||
}
|
||||
}
|
||||
|
||||
private bool disposed;
|
||||
/// <summary>
|
||||
/// True if the current interface was disposed
|
||||
/// </summary>
|
||||
public bool Disposed {
|
||||
get { return disposed; }
|
||||
private set { disposed = value; }
|
||||
}
|
||||
|
||||
|
||||
private PLCInfo plcInfo;
|
||||
/// <summary>
|
||||
/// Generic information about the connected PLC
|
||||
/// </summary>
|
||||
public PLCInfo PlcInfo {
|
||||
get => plcInfo;
|
||||
private set {
|
||||
plcInfo = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PlcInfo)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The registered data registers of the PLC
|
||||
/// </summary>
|
||||
public List<IRegister> Registers { get; set; } = new List<IRegister>();
|
||||
|
||||
private string ip;
|
||||
private int port;
|
||||
private int stationNumber;
|
||||
private int cycleTimeMs = 25;
|
||||
|
||||
private int bytesTotalCountedUpstream = 0;
|
||||
private int bytesTotalCountedDownstream = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The current IP of the PLC connection
|
||||
/// </summary>
|
||||
public string IpAddress => ip;
|
||||
/// <summary>
|
||||
/// The current port of the PLC connection
|
||||
/// </summary>
|
||||
public int Port => port;
|
||||
/// <summary>
|
||||
/// The station number of the PLC
|
||||
/// </summary>
|
||||
public int StationNumber => stationNumber;
|
||||
|
||||
/// <summary>
|
||||
/// The duration of the last message cycle
|
||||
/// </summary>
|
||||
public int CycleTimeMs {
|
||||
get { return cycleTimeMs; }
|
||||
private set {
|
||||
cycleTimeMs = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CycleTimeMs)));
|
||||
}
|
||||
}
|
||||
|
||||
private int bytesPerSecondUpstream = 0;
|
||||
/// <summary>
|
||||
/// The current transmission speed in bytes per second
|
||||
/// </summary>
|
||||
public int BytesPerSecondUpstream {
|
||||
get { return bytesPerSecondUpstream; }
|
||||
private set {
|
||||
bytesPerSecondUpstream = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BytesPerSecondUpstream)));
|
||||
}
|
||||
}
|
||||
|
||||
private int bytesPerSecondDownstream = 0;
|
||||
/// <summary>
|
||||
/// The current transmission speed in bytes per second
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Builds a new Interfacer for a PLC
|
||||
/// </summary>
|
||||
/// <param name="_ip">IP adress of the PLC</param>
|
||||
/// <param name="_port">Port of the PLC</param>
|
||||
/// <param name="_station">Station Number of the PLC</param>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Trys to connect to the PLC by the IP given in the constructor
|
||||
/// </summary>
|
||||
/// <param name="OnConnected">
|
||||
/// Gets called when a connection with a PLC was established
|
||||
/// <para/>
|
||||
/// If <see cref="WithPoller"/> is used it waits for the first data receive cycle to complete
|
||||
/// </param>
|
||||
/// <param name="OnFailed">Gets called when an error or timeout during connection occurs</param>
|
||||
/// <returns></returns>
|
||||
public async Task<MewtocolInterface> ConnectAsync (Action<PLCInfo> 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;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the connections parameters of the PLC, only applyable when the connection is offline
|
||||
/// </summary>
|
||||
/// <param name="_ip">Ip adress</param>
|
||||
/// <param name="_port">Port number</param>
|
||||
/// <param name="_station">Station number</param>
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the connection all cyclic polling
|
||||
/// </summary>
|
||||
public void Disconnect () {
|
||||
|
||||
if (!IsConnected)
|
||||
return;
|
||||
|
||||
OnMajorSocketExceptionWhileConnected();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches a poller to the interface that continously
|
||||
/// polls the registered data registers and writes the values to them
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Attaches a register collection object to
|
||||
/// the interface that can be updated automatically.
|
||||
/// <para/>
|
||||
/// Just create a class inheriting from <see cref="RegisterCollectionBase"/>
|
||||
/// and assert some propertys with the custom <see cref="RegisterAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="collection">A collection inherting the <see cref="RegisterCollectionBase"/> class</param>
|
||||
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<short>(collection.GetType(), cAttribute.MemoryArea, prop);
|
||||
}
|
||||
|
||||
if (prop.PropertyType == typeof(ushort)) {
|
||||
AddRegister<ushort>(collection.GetType(), cAttribute.MemoryArea, prop);
|
||||
}
|
||||
|
||||
if (prop.PropertyType == typeof(int)) {
|
||||
AddRegister<int>(collection.GetType(), cAttribute.MemoryArea, prop);
|
||||
}
|
||||
|
||||
if (prop.PropertyType == typeof(uint)) {
|
||||
AddRegister<uint>(collection.GetType(), cAttribute.MemoryArea, prop);
|
||||
}
|
||||
|
||||
if (prop.PropertyType == typeof(float)) {
|
||||
AddRegister<float>(collection.GetType(), cAttribute.MemoryArea, prop);
|
||||
}
|
||||
|
||||
if (prop.PropertyType == typeof(string)) {
|
||||
AddRegister<string>(collection.GetType(), cAttribute.MemoryArea, prop, cAttribute.StringLength);
|
||||
}
|
||||
|
||||
if (prop.PropertyType.IsEnum) {
|
||||
|
||||
if (cAttribute.BitCount == BitCount.B16) {
|
||||
AddRegister<short>(collection.GetType(), cAttribute.MemoryArea, prop, _enumType: prop.PropertyType);
|
||||
} else {
|
||||
AddRegister<int>(collection.GetType(), cAttribute.MemoryArea, prop, _enumType: prop.PropertyType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//read number as bit array
|
||||
if (prop.PropertyType == typeof(BitArray)) {
|
||||
|
||||
if (cAttribute.BitCount == BitCount.B16) {
|
||||
AddRegister<short>(collection.GetType(), cAttribute.MemoryArea, prop, _isBitwise: true);
|
||||
} else {
|
||||
AddRegister<int>(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<short>(collection.GetType(), cAttribute.MemoryArea, prop, _isBitwise: true);
|
||||
} else {
|
||||
AddRegister<int>(collection.GetType(), cAttribute.MemoryArea, prop, _isBitwise: true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (prop.PropertyType == typeof(TimeSpan)) {
|
||||
AddRegister<TimeSpan>(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<short> reg16) {
|
||||
var bytes = BitConverter.GetBytes((short)reg16.Value);
|
||||
bitAr = new BitArray(bytes);
|
||||
} else if(reg is NRegister<int> 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<Type> NumericTypes = new HashSet<Type> {
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Sets a register in the PLCs memory
|
||||
/// </summary>
|
||||
/// <param name="registerName">The name the register was given to or a property name from the RegisterCollection class</param>
|
||||
/// <param name="value">The value to write to the register</param>
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a register in the PLCs memory asynchronously, returns the result status from the PLC
|
||||
/// </summary>
|
||||
/// <param name="registerName">The name the register was given to or a property name from the RegisterCollection class</param>
|
||||
/// <param name="value">The value to write to the register</param>
|
||||
public async Task<bool> 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<short>)) {
|
||||
|
||||
return await WriteNumRegister((NRegister<short>)foundRegister, (short)value);
|
||||
|
||||
}
|
||||
|
||||
if (foundRegister.GetType() == typeof(NRegister<ushort>)) {
|
||||
|
||||
return await WriteNumRegister((NRegister<ushort>)foundRegister, (ushort)value);
|
||||
|
||||
}
|
||||
|
||||
if (foundRegister.GetType() == typeof(NRegister<int>)) {
|
||||
|
||||
return await WriteNumRegister((NRegister<int>)foundRegister, (int)value);
|
||||
|
||||
}
|
||||
|
||||
if (foundRegister.GetType() == typeof(NRegister<uint>)) {
|
||||
|
||||
return await WriteNumRegister((NRegister<uint>)foundRegister, (uint)value);
|
||||
|
||||
}
|
||||
|
||||
if (foundRegister.GetType() == typeof(NRegister<float>)) {
|
||||
|
||||
return await WriteNumRegister((NRegister<float>)foundRegister, (float)value);
|
||||
|
||||
}
|
||||
|
||||
if (foundRegister.GetType() == typeof(NRegister<TimeSpan>)) {
|
||||
|
||||
return await WriteNumRegister((NRegister<TimeSpan>)foundRegister, (TimeSpan)value);
|
||||
|
||||
}
|
||||
|
||||
if (foundRegister.GetType() == typeof(SRegister)) {
|
||||
|
||||
return await WriteStringRegister((SRegister)foundRegister, (string)value);
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Low level command handling
|
||||
|
||||
/// <summary>
|
||||
/// Calculates checksum and sends a command to the PLC then awaits results
|
||||
/// </summary>
|
||||
/// <param name="_msg">MEWTOCOL Formatted request string ex: %01#RT</param>
|
||||
/// <returns>Returns the result</returns>
|
||||
public async Task<CommandResult> 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<string> 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
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the current interface and clears all its members
|
||||
/// </summary>
|
||||
public void Dispose () {
|
||||
|
||||
if (Disposed) return;
|
||||
|
||||
Disconnect();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
Disposed = true;
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Accessing Info
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connection info string
|
||||
/// </summary>
|
||||
public string GetConnectionPortInfo () {
|
||||
|
||||
return $"{IpAddress}:{Port}";
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
/// <summary>
|
||||
/// Gets generic information about the PLC
|
||||
/// </summary>
|
||||
/// <returns>A PLCInfo class</returns>
|
||||
public async Task<PLCInfo> 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
|
||||
|
||||
/// <summary>
|
||||
/// Changes the PLCs operation mode to the given one
|
||||
/// </summary>
|
||||
/// <param name="mode">The mode to change to</param>
|
||||
/// <returns>The success state of the write operation</returns>
|
||||
public async Task<bool> 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
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// /// <param name="start">start address of the array</param>
|
||||
/// <param name="byteArr"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> 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;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the bytes from the start adress for counts byte length
|
||||
/// </summary>
|
||||
/// <param name="start">Start adress</param>
|
||||
/// <param name="count">Number of bytes to get</param>
|
||||
/// <param name="onProgress">Gets invoked when the progress changes, contains the progress as a double</param>
|
||||
/// <returns>A byte array or null of there was an error</returns>
|
||||
public async Task<byte[]> ReadByteRange (int start, int count, Action<double> onProgress = null) {
|
||||
|
||||
var byteList = new List<byte>();
|
||||
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Reads the given boolean register from the PLC
|
||||
/// </summary>
|
||||
/// <param name="_toRead">The register to read</param>
|
||||
public async Task<BRegisterResult> 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;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes to the given bool register on the PLC
|
||||
/// </summary>
|
||||
/// <param name="_toWrite">The register to write to</param>
|
||||
/// <param name="value">The value to write</param>
|
||||
/// <returns>The success state of the write operation</returns>
|
||||
public async Task<bool> 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
|
||||
|
||||
/// <summary>
|
||||
/// Reads the given numeric register from the PLC
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of number (short, ushort, int, uint, float)</typeparam>
|
||||
/// <param name="_toRead">The register to read</param>
|
||||
/// <returns>A result with the given NumberRegister containing the readback value and a result struct</returns>
|
||||
public async Task<NRegisterResult<T>> ReadNumRegister<T> (NRegister<T> _toRead) {
|
||||
|
||||
Type numType = typeof(T);
|
||||
|
||||
string requeststring = $"%{GetStationNumber()}#RD{_toRead.BuildMewtocolQuery()}";
|
||||
var result = await SendCommandAsync(requeststring);
|
||||
|
||||
var failedResult = new NRegisterResult<T> {
|
||||
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<T> {
|
||||
Result = result,
|
||||
Register = _toRead
|
||||
};
|
||||
|
||||
return finalRes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the given numeric register from the PLC
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of number (short, ushort, int, uint, float)</typeparam>
|
||||
/// <param name="_toWrite">The register to write</param>
|
||||
/// <param name="_value">The value to write</param>
|
||||
/// <returns>The success state of the write operation</returns>
|
||||
public async Task<bool> WriteNumRegister<T> (NRegister<T> _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
|
||||
|
||||
/// <summary>
|
||||
/// Reads back the value of a string register
|
||||
/// </summary>
|
||||
/// <param name="_toRead">The register to read</param>
|
||||
/// <param name="_stationNumber">The station number of the PLC</param>
|
||||
/// <returns></returns>
|
||||
public async Task<SRegisterResult> 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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a string to a string register
|
||||
/// </summary>
|
||||
/// <param name="_toWrite">The register to write</param>
|
||||
/// <param name="_value">The value to write, if the strings length is longer than the cap size it gets trimmed to the max char length</param>
|
||||
/// <param name="_stationNumber">The station number of the PLC</param>
|
||||
/// <returns>The success state of the write operation</returns>
|
||||
public async Task<bool> 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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
namespace MewtocolNet {
|
||||
|
||||
/// <summary>
|
||||
/// CPU type of the PLC
|
||||
/// </summary>
|
||||
public enum CpuType {
|
||||
/// <summary>
|
||||
/// FP 0 / FP 2.7K
|
||||
/// </summary>
|
||||
FP0_FP1_2_7K,
|
||||
/// <summary>
|
||||
/// FP0 / FP1, 5K / 10K
|
||||
/// </summary>
|
||||
FP0_FP1_5K_10K,
|
||||
/// <summary>
|
||||
/// FP1 M 0.9K
|
||||
/// </summary>
|
||||
FP1_M_0_9K,
|
||||
/// <summary>
|
||||
/// FP2 16k / 32k
|
||||
/// </summary>
|
||||
FP2_16K_32K,
|
||||
/// <summary>
|
||||
/// FP3 C 10K
|
||||
/// </summary>
|
||||
FP3_C_10K,
|
||||
/// <summary>
|
||||
/// FP3 C 16K
|
||||
/// </summary>
|
||||
FP3_C_16K,
|
||||
/// <summary>
|
||||
/// FP5 16K
|
||||
/// </summary>
|
||||
FP5_16K,
|
||||
/// <summary>
|
||||
/// FP 5 24K
|
||||
/// </summary>
|
||||
FP5_24K,
|
||||
/// <summary>
|
||||
/// Includes panasonic FPX, FPX-H, Sigma
|
||||
/// </summary>
|
||||
FP_Sigma_X_H_30K_60K_120K
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace MewtocolNet {
|
||||
|
||||
/// <summary>
|
||||
/// CPU type of the PLC
|
||||
/// </summary>
|
||||
public enum OPMode {
|
||||
/// <summary>
|
||||
/// PLC run mode
|
||||
/// </summary>
|
||||
Run,
|
||||
/// <summary>
|
||||
/// PLC programming mode
|
||||
/// </summary>
|
||||
Prog,
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
namespace MewtocolNet.Registers {
|
||||
/// <summary>
|
||||
/// Contains generic information about the plc
|
||||
/// </summary>
|
||||
public class PLCInfo {
|
||||
|
||||
/// <summary>
|
||||
/// Contains information about the PLCs cpu
|
||||
/// </summary>
|
||||
public CpuInfo CpuInformation {get;set;}
|
||||
/// <summary>
|
||||
/// Contains information about the PLCs operation modes
|
||||
/// </summary>
|
||||
public PLCMode OperationMode {get;set;}
|
||||
/// <summary>
|
||||
/// Current error code of the PLC
|
||||
/// </summary>
|
||||
public string ErrorCode {get;set;}
|
||||
|
||||
/// <summary>
|
||||
/// Current station number of the PLC
|
||||
/// </summary>
|
||||
public int StationNumber { get;set;}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a string containing some of the most important informations
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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}";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace MewtocolNet.Registers {
|
||||
|
||||
/// <summary>
|
||||
/// All modes
|
||||
/// </summary>
|
||||
public class PLCMode {
|
||||
|
||||
/// <summary>
|
||||
/// PLC is running
|
||||
/// </summary>
|
||||
public bool RunMode { get; set; }
|
||||
/// <summary>
|
||||
/// PLC is in test
|
||||
/// </summary>
|
||||
public bool TestRunMode { get; set; }
|
||||
/// <summary>
|
||||
/// BreakExcecuting
|
||||
/// </summary>
|
||||
public bool BreakExcecuting { get; set; }
|
||||
/// <summary>
|
||||
/// BreakValid
|
||||
/// </summary>
|
||||
public bool BreakValid { get; set; }
|
||||
/// <summary>
|
||||
/// PLC output is enabled
|
||||
/// </summary>
|
||||
public bool OutputEnabled { get; set; }
|
||||
/// <summary>
|
||||
/// PLC runs step per step
|
||||
/// </summary>
|
||||
public bool StepRunMode { get; set; }
|
||||
/// <summary>
|
||||
/// Message executing
|
||||
/// </summary>
|
||||
public bool MessageExecuting { get; set; }
|
||||
/// <summary>
|
||||
/// PLC is in remote mode
|
||||
/// </summary>
|
||||
public bool RemoteMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets operation mode from 2 digit hex number
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MewtocolNet.RegisterAttributes {
|
||||
|
||||
/// <summary>
|
||||
/// The size of the bitwise register
|
||||
/// </summary>
|
||||
public enum BitCount {
|
||||
/// <summary>
|
||||
/// 16 bit
|
||||
/// </summary>
|
||||
B16,
|
||||
/// <summary>
|
||||
/// 32 bit
|
||||
/// </summary>
|
||||
B32
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the behavior of a register property
|
||||
/// </summary>
|
||||
[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;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Attribute for string type or numeric registers
|
||||
/// </summary>
|
||||
/// <param name="memoryArea">The area in the plcs memory</param>
|
||||
/// <param name="stringLength">The max string length in the plc</param>
|
||||
public RegisterAttribute (int memoryArea, int stringLength = 1) {
|
||||
|
||||
MemoryArea = memoryArea;
|
||||
StringLength = stringLength;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attribute for boolean registers
|
||||
/// </summary>
|
||||
/// <param name="memoryArea">The area in the plcs memory</param>
|
||||
/// <param name="type">The type of boolean register</param>
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attribute for boolean registers
|
||||
/// </summary>
|
||||
/// <param name="spAdress">The special area in the plcs memory</param>
|
||||
/// <param name="type">The type of boolean register</param>
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attribute to read numeric registers as bitwise
|
||||
/// </summary>
|
||||
/// <param name="memoryArea">The area in the plcs memory</param>
|
||||
/// <param name="bitcount">The number of bits to parse</param>
|
||||
public RegisterAttribute (int memoryArea, BitCount bitcount) {
|
||||
|
||||
MemoryArea = memoryArea;
|
||||
StringLength = 0;
|
||||
BitCount = bitcount;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attribute to read numeric registers as bitwise
|
||||
/// </summary>
|
||||
/// <param name="memoryArea">The area in the plcs memory</param>
|
||||
/// <param name="bitcount">The number of bits to parse</param>
|
||||
/// <param name="assignBit">The index of the bit that gets linked to the bool</param>
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MewtocolNet {
|
||||
|
||||
/// <summary>
|
||||
/// The special register type
|
||||
/// </summary>
|
||||
public enum RegisterType {
|
||||
|
||||
/// <summary>
|
||||
/// Physical input as a bool (Relay)
|
||||
/// </summary>
|
||||
X,
|
||||
/// <summary>
|
||||
/// Physical output as a bool (Relay)
|
||||
/// </summary>
|
||||
Y,
|
||||
/// <summary>
|
||||
/// Internal as a bool (Relay)
|
||||
/// </summary>
|
||||
R,
|
||||
/// <summary>
|
||||
/// Data area as a short (Register)
|
||||
/// </summary>
|
||||
DT_short,
|
||||
/// <summary>
|
||||
/// Data area as an unsigned short (Register)
|
||||
/// </summary>
|
||||
DT_ushort,
|
||||
/// <summary>
|
||||
/// Double data area as an integer (Register)
|
||||
/// </summary>
|
||||
DDT_int,
|
||||
/// <summary>
|
||||
/// Double data area as an unsigned integer (Register)
|
||||
/// </summary>
|
||||
DDT_uint,
|
||||
/// <summary>
|
||||
/// Double data area as an floating point number (Register)
|
||||
/// </summary>
|
||||
DDT_float,
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The special input / output channel address
|
||||
/// </summary>
|
||||
public enum SpecialAddress {
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
/// <summary>
|
||||
/// No defined
|
||||
/// </summary>
|
||||
None,
|
||||
A = -10,
|
||||
B = -11,
|
||||
C = -12,
|
||||
D = -13,
|
||||
E = -14,
|
||||
F = -15,
|
||||
|
||||
#pragma warning restore
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace MewtocolNet.Registers {
|
||||
|
||||
/// <summary>
|
||||
/// The formatted result of a ascii command
|
||||
/// </summary>
|
||||
public struct CommandResult {
|
||||
|
||||
/// <summary>
|
||||
/// Success state of the message
|
||||
/// </summary>
|
||||
public bool Success {get;set;}
|
||||
/// <summary>
|
||||
/// Response text of the message
|
||||
/// </summary>
|
||||
public string Response {get;set;}
|
||||
/// <summary>
|
||||
/// Error code of the message
|
||||
/// </summary>
|
||||
public string Error {get;set;}
|
||||
/// <summary>
|
||||
/// Error text of the message
|
||||
/// </summary>
|
||||
public string ErrorDescription {get;set;}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
using MewtocolNet;
|
||||
|
||||
namespace MewtocolNet.Registers {
|
||||
|
||||
/// <summary>
|
||||
/// Defines a register containing a boolean
|
||||
/// </summary>
|
||||
public class BRegister : IRegister, INotifyPropertyChanged {
|
||||
|
||||
/// <summary>
|
||||
/// Gets called whenever the value was changed
|
||||
/// </summary>
|
||||
public event Action<object> ValueChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Triggers when a property on the register changes
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
internal RegisterType RegType { get; private set; }
|
||||
|
||||
internal SpecialAddress SpecialAddress { get; private set; }
|
||||
|
||||
internal Type collectionType;
|
||||
|
||||
/// <summary>
|
||||
/// The type of collection the register is in or null of added manually
|
||||
/// </summary>
|
||||
public Type CollectionType => collectionType;
|
||||
|
||||
internal bool lastValue;
|
||||
|
||||
/// <summary>
|
||||
/// The value of the register
|
||||
/// </summary>
|
||||
public object Value => lastValue;
|
||||
|
||||
internal string name;
|
||||
/// <summary>
|
||||
/// The register name or null of not defined
|
||||
/// </summary>
|
||||
public string Name => name;
|
||||
|
||||
internal int memoryAdress;
|
||||
/// <summary>
|
||||
/// The registers memory adress if not a special register
|
||||
/// </summary>
|
||||
public int MemoryAddress => memoryAdress;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a register containing a number
|
||||
/// </summary>
|
||||
/// <param name="_address">Memory start adress max 99999</param>
|
||||
/// <param name="_type">Type of boolean register</param>
|
||||
/// <param name="_name">Name of the register</param>
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a register containing a number
|
||||
/// </summary>
|
||||
/// <param name="_address">Memory start adress max 99999</param>
|
||||
/// <param name="_type">Type of boolean register</param>
|
||||
/// <param name="_name">Name of the register</param>
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the register area name
|
||||
/// </summary>
|
||||
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()}";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user