PLCExtension 1.0.5
dotnet add package PLCExtension --version 1.0.5
NuGet\Install-Package PLCExtension -Version 1.0.5
<PackageReference Include="PLCExtension" Version="1.0.5" />
<PackageVersion Include="PLCExtension" Version="1.0.5" />
<PackageReference Include="PLCExtension" />
paket add PLCExtension --version 1.0.5
#r "nuget: PLCExtension, 1.0.5"
#:package PLCExtension@1.0.5
#addin nuget:?package=PLCExtension&version=1.0.5
#tool nuget:?package=PLCExtension&version=1.0.5
PLC Data Context - PLC Data Access/Update Mechanism
Introduction
This mechanism provides an easy, type-safe, and clean way to work with PLC data instead of managing addresses manually.
What's New
Instead of manually managing PLC addresses, the new mechanism provides:
- Property-based access - Access data like normal properties
- Type-safe - Automatic IntelliSense completion
- Auto conversion - Automatic data type conversion
- Clean code - Reduces complexity and potential errors
var qrData = new QRScanData1(_plc);
await qrData.LoadAsync();
if (qrData.IsLocked)
{
qrData.ClearCommand = true;
qrData.RollID = rollId; // Automatically converted
await qrData.SaveAsync();
}
How It Works
Method 1: Using Attributes (Traditional)
1. Define Data Class
public class QRScanData : PLCDataContext
{
public QRScanData(McpX plcDevice) : base(plcDevice) { }
// Attribute specifies PLC address
[PLCAddress("M6011", description: "QR Scan Command")]
public bool ScanCommand { get; set; }
[PLCAddress("M6012", description: "OK Status")]
public bool OkStatus { get; set; }
[PLCAddress("D6200", length: 100, description: "QR Buffer")]
public string QRBarcode { get; set; } = "";
[PLCAddress("D6300", length: 2, description: "Speed")]
public int RollSpeed { get; set; }
[PLCAddress("D6310", length: 2, description: "Length")]
public float RollLength { get; set; }
}
2. Usage
// Initialize object with PLC device
var data = new QRScanData(_plc);
// FIRST, read ALL from PLC into object
await data.LoadAsync();
// Now can access properties normally
bool isOk = data.OkStatus;
string barcode = data.QRBarcode;
// Modify data
data.OkStatus = true;
data.RollSpeed = 500;
data.RollLength = 1000.5f;
// WRITE ALL changes back to PLC
await data.SaveAsync();
Method 2: Using Dynamic JSON Mapping (Advanced)
1. Prepare JSON File
Create mapping.json with format:
[
{
"fieldName": "ScanCommand",
"address": "M6011",
"length": 1,
"description": "QR Scan Command"
},
{
"fieldName": "OkStatus",
"address": "M6012",
"length": 1,
"description": "OK Status"
},
{
"fieldName": "QRBarcode",
"address": "D6200",
"length": 100,
"description": "QR Buffer"
},
{
"fieldName": "RollSpeed",
"address": "D6300",
"length": 2,
"description": "Speed"
},
{
"fieldName": "RollLength",
"address": "D6310",
"length": 2,
"description": "Length"
}
]
2. Define Data Class (No Attributes Needed)
public class SendRollMES : PLCDataContext
{
public SendRollMES(McpX plcDevice) : base(plcDevice) { }
public SendRollMES(McpX plcDevice, Dictionary<string, PLCFieldMap> mappings) : base(plcDevice, mappings) { }
// Declare properties WITHOUT [PLCAddress] attribute
public bool ScanCommand { get; set; }
public bool OkStatus { get; set; }
public string QRBarcode { get; set; } = "";
public int RollSpeed { get; set; }
public float RollLength { get; set; }
}
3. Usage With JSON Mapping
// Read JSON file
var json = File.ReadAllText("mapping.json");
// Deserialize into List<PLCFieldMap>
var fieldMapList = JsonSerializer.Deserialize<List<PLCFieldMap>>(json);
// Convert to Dictionary (key: FieldName, value: PLCFieldMap)
var mappings = fieldMapList!
.ToDictionary(x => x.FieldName, x => x);
// Initialize object with mapping
var context = new SendRollMES(_plc, mappings);
// Read from PLC (using JSON mapping)
await context.LoadAsync();
// Process data
if (context.ScanCommand)
{
context.OkStatus = true;
context.RollSpeed = 1000;
context.RollLength = 500.5f;
}
// Write back to PLC
await context.SaveAsync();
4. Benefits of JSON Mapping
- Customize addresses without recompile - Just update mapping.json
- Easy config management - Can be stored outside codebase
- Support multiple versions - Can have mapping.v1.json, mapping.v2.json
- Easy testing - Mock mapping for unit tests
- Easy maintenance - Single place to manage all PLC addresses
Supported Data Types
| Type | Words Used | Example Address |
|---|---|---|
bool |
1 | M3800 |
string |
Length | D6200 |
int |
2 | D6100 |
float |
2 | D6310 |
short[] |
Length | D6000 |
Automatic Type Conversion
// String (uses multiple Words)
[PLCAddress("D6200", length: 100)]
public string QRCode { get; set; } = "";
// Int32 (uses 2 Words)
[PLCAddress("D6300", length: 2)]
public int Speed { get; set; }
// Float (uses 2 Words)
[PLCAddress("D6310", length: 2)]
public float Length { get; set; }
// Boolean (uses 1 Word from M register)
[PLCAddress("M6000")]
public bool IsRunning { get; set; }
// Raw Word Array
[PLCAddress("D6000", length: 10)]
public short[] RawData { get; set; } = Array.Empty<short>();
Detailed API
Main Methods
// Read ALL from PLC into object (supports attributes or JSON mapping)
await data.LoadAsync();
// Write ALL from object to PLC (supports attributes or JSON mapping)
await data.SaveAsync();
// Read specific property from PLC
var value = await data.ReadValueAsync("PropertyName");
// Write specific property to PLC
await data.WriteValueAsync("PropertyName", value);
Extension Methods (Utilities)
// Get list of all PLC properties
var props = data.GetPLCProperties();
// Output: List<(PropertyInfo, PLCAddressAttribute)>
// Print detailed mapping
Console.WriteLine(data.GetPLCMapping());
// Clone data from another object
data.CopyValuesFrom(otherData);
// Compare with another object
bool isEqual = data.ValuesEqual(otherData);
// Export to Dictionary (easy serialize)
var dict = data.ExportToDictionary();
// Import from Dictionary
data.ImportFromDictionary(dict);
Property Change Tracking
PLCDataContext supports INotifyPropertyChanged for easy property change tracking. There are 3 ways to use it:
Method 1: Using WhenPropertyChanges (Easiest)
This method is easiest and suitable for most use cases:
String Version (Basic)
var scanRoll = new HMIScanRollScreenData(_plc);
// Subscribe to property changes
var subscription = scanRoll.WhenPropertyChanges<bool>("ConfirmSubRollButton", (newValue) =>
{
_logger.LogInformation($"ConfirmSubRollButton changed to: {newValue}");
if (newValue)
{
// Handle button confirm
}
});
// When no longer needed, unsubscribe
subscription.Dispose();
Lambda Expression Version (Type-Safe - Recommended)
Use extension method - x will be type-inferred, IntelliSense works great:
var scanRoll = new HMIScanRollScreenData(_plc);
// Subscribe with lambda expression - x is HMIScanRollScreenData, IntelliSense support!
var subscription = scanRoll.WhenPropertyChanges(
x => x.ConfirmSubRollButton, // ← x. shows all properties
(newValue) =>
{
_logger.LogInformation($"ConfirmSubRollButton changed to: {newValue}");
if (newValue)
{
// Handle button confirm
}
}
);
// With async callback:
var asyncSubscription = scanRoll.WhenPropertyChanges(
x => x.ConfirmSubRollButton,
async (newValue) => // ← Receives parameter value
{
_logger.LogInformation($"ConfirmSubRollButton changed to: {newValue}");
if (newValue)
{
// Async operations
await ProcessSubRollConfirmAsync();
}
}
);
// Nested properties also supported:
var nestedSubscription = scanRoll.WhenPropertyChanges(
x => x.ConfirmMainRollButton,
async (value) => await HandleButtonAsync(value)
);
// When no longer needed, unsubscribe
subscription.Dispose();
asyncSubscription.Dispose();
nestedSubscription.Dispose();
With old value (Lambda Expression):
var subscription = scanRoll.WhenPropertyValueChanges(
x => x.ConfirmSubRollButton, // ← x. shows properties
(oldValue, newValue) =>
{
_logger.LogInformation($"ConfirmSubRollButton changed from {oldValue} to {newValue}");
}
);
// With async:
var asyncSubscription = scanRoll.WhenPropertyValueChanges(
x => x.ConfirmSubRollButton,
async (oldValue, newValue) =>
{
_logger.LogInformation($"ConfirmSubRollButton changed from {oldValue} to {newValue}");
if (newValue)
await ProcessAsync();
}
);
Advantages of Lambda Expression:
- ✅ Type-safe - Compiler checks property exists
- ✅ Refactor-safe - Rename property automatically updates
- ✅ IntelliSense support - Autocomplete property names
- ✅ Supports nested properties (E.g:
x => x.ScanRollScreen.ConfirmMainRollButton)
Method 2: Using PropertyChanged Event (Standard .NET)
This approach follows MVVM pattern and .NET standards:
var scanRoll = new HMIScanRollScreenData(_plc);
// Subscribe to PropertyChanged event
scanRoll.PropertyChanged += (s, e) =>
{
if (e.PropertyName == "ConfirmSubRollButton")
{
_logger.LogInformation($"ConfirmSubRollButton changed");
}
};
Method 3: Using SetProperty Method (In Subclass)
When defining a subclass, can use SetProperty helper method:
private bool _confirmSubRollButton;
public bool ConfirmSubRollButton
{
get => _confirmSubRollButton;
set => SetProperty(ref _confirmSubRollButton, value, nameof(ConfirmSubRollButton));
}
Comparison of Methods
| Method | Advantages | Disadvantages | Use Case |
|---|---|---|---|
| WhenPropertyChanges (String) | Simple, runtime flexible | Not type-safe | Quick prototyping |
| WhenPropertyChanges (Lambda) | Type-safe, IntelliSense, refactor | Longer syntax | Production code, recommended |
| PropertyChanged Event | Standard .NET, MVVM pattern | Need handler | MVVM app, complex binding |
| SetProperty | Clean, reusable | Need subclass | Property with backing field |
Real-World Examples
Example 1: QR Scan (Using Attributes)
private async Task HandleQRScanAsync()
{
var qrData = new QRScanData1(_plc);
// Read scan status
await qrData.LoadAsync();
if (qrData.ScanCommand)
{
_logger.LogInformation($"Barcode: {qrData.QRBarcode}");
// Call API to process
var result = await _api.ProcessBarcodeAsync(qrData.QRBarcode);
if (result.IsValid)
{
// Update status
qrData.OkStatus = true;
qrData.RollID = result.RollId;
qrData.IsLocked = false;
// Write back
await qrData.SaveAsync();
}
}
}
Example 2: Using JSON Mapping
private async Task InitializeWithMappingAsync()
{
// Load mapping from JSON file
var json = File.ReadAllText("config/plc-mapping.json");
var fieldMaps = JsonSerializer.Deserialize<List<PLCFieldMap>>(json);
var mappings = fieldMaps!.ToDictionary(x => x.FieldName);
// Initialize with mapping
var mesData = new SendRollMES(_plc, mappings);
var scanData = new QRScanData(_plc, mappings);
// Read data
await mesData.LoadAsync();
await scanData.LoadAsync();
// Process...
await mesData.SaveAsync();
await scanData.SaveAsync();
}
Example 3: Managing Multiple Data Objects (Parallel)
private async Task MainLoopAsync()
{
var qrData1 = new QRScanData1(_plc);
var qrData2 = new QRScanData2(_plc);
var jobData = new JobData(_plc);
var control = new ControlData(_plc);
while (!stoppingToken.IsCancellationRequested)
{
// Read data in parallel
await Task.WhenAll(
qrData1.LoadAsync(),
qrData2.LoadAsync(),
jobData.LoadAsync(),
control.LoadAsync()
);
// Process logic
await ProcessScan1Async(qrData1);
await ProcessScan2Async(qrData2);
await ProcessJobAsync(jobData);
// Write data in parallel
await Task.WhenAll(
qrData1.SaveAsync(),
qrData2.SaveAsync(),
jobData.SaveAsync(),
control.SaveAsync()
);
await Task.Delay(50);
}
}
Example 4: Export/Import Data
// Export data from PLC (storage/transmission)
var qrData = new QRScanData1(_plc);
await qrData.LoadAsync();
// Get individual property values
var scanCmd = qrData.ScanCommand;
var barcode = qrData.QRBarcode;
// Or serialize entire object
var jsonData = JsonSerializer.Serialize(qrData);
await _db.SaveHistoryAsync(jsonData);
Example 5: Property Change Tracking
This example shows how to track button presses and auto-trigger handlers:
private async Task SetupPropertyTrackingAsync()
{
var scanRoll = new HMIScanRollScreenData(_plc);
await scanRoll.LoadAsync();
// ===== Method 1: WhenPropertyChanges + Lambda Expression (Recommended) =====
// Lambda expression - Type-safe and easy to refactor
var subRollSubscription = scanRoll.WhenPropertyChanges(
x => x.ConfirmSubRollButton,
async (newValue) =>
{
if (newValue) // When button is pressed
{
_logger.LogInformation("Sub Roll Button Confirmed!");
// Handle logic
await ProcessSubRollConfirmAsync(scanRoll);
}
}
);
// Subscribe with old value - Lambda Expression
var mainRollSubscription = scanRoll.WhenPropertyValueChanges(
x => x.ConfirmMainRollButton,
async (oldValue, newValue) =>
{
_logger.LogInformation($"MainRoll: {oldValue} -> {newValue}");
if (newValue)
await HandleMainRollChangeAsync();
}
);
// ===== Method 1B: WhenPropertyChanges + String (If runtime flexibility needed) =====
// String-based - Flexible but not type-safe
// var subscription = scanRoll.WhenPropertyChanges<bool>("Button", newValue => { ... });
// ===== Method 2: PropertyChanged Event (MVVM Pattern) =====
scanRoll.PropertyChanged += (s, e) =>
{
if (e.PropertyName == "IsMainRollValid" && s is HMIScanRollScreenData data)
{
_logger.LogInformation($"MainRoll Valid: {data.IsMainRollValid}");
}
};
// Main loop
while (!stoppingToken.IsCancellationRequested)
{
await scanRoll.LoadAsync();
await Task.Delay(100);
// Subscriptions will auto-trigger when property changes
}
// Cleanup
subRollSubscription?.Dispose();
mainRollSubscription?.Dispose();
}
private async Task ProcessSubRollConfirmAsync(HMIScanRollScreenData scanRoll)
{
var qrCode = scanRoll.SubRollQRCode;
_logger.LogInformation($"Processing QR: {qrCode}");
// Call API to process
var result = await _api.ValidateQRCodeAsync(qrCode);
if (result.IsValid)
{
scanRoll.IsSubRollValid = true;
}
await scanRoll.SaveAsync();
}
Benefits of Property Change Tracking:
- ✅ Auto-trigger handler when property changes
- ✅ No need for polling or manual checks
- ✅ Type-safe callbacks
- ✅ Easy cleanup with
.Dispose() - ✅ Support both old/new values
Performance Optimization
Selective Read/Write (Performance)
Instead of reading/writing all data, can read/write only needed properties:
// Only read Scan property
var scanCmd = await data.ReadValueAsync("ScanCommand");
// Only write OkStatus property
await data.WriteValueAsync("OkStatus", true);
Batch Operations (Faster)
When working with multiple data objects, use Task.WhenAll() to read/write in parallel:
// Read data in parallel
await Task.WhenAll(
data1.LoadAsync(),
data2.LoadAsync(),
data3.LoadAsync()
);
// Write data in parallel
await Task.WhenAll(
data1.SaveAsync(),
data2.SaveAsync(),
data3.SaveAsync()
);
Troubleshooting
Error: "Unknown device type"
Ensure address starts with M or D:
[PLCAddress("M100")] // M-register: Boolean
[PLCAddress("D100")] // D-register: Word-based
// Or in JSON mapping:
{
"fieldName": "MyFlag",
"address": "M100", // Correct: starts with M or D
"length": 1
}
Error: Data Not Changing
Must call SaveAsync() after changing a property:
data.Speed = 500;
await data.SaveAsync(); // Required!
Error: Mapping Not Found
When using JSON mapping, ensure fieldName in JSON matches property name:
{
"fieldName": "ScanCommand", // Must match property name
"address": "M6011",
"length": 1
}
public class MyData : PLCDataContext
{
public bool ScanCommand { get; set; } // Must match fieldName
}
Error: Properties Not Loaded
Check:
- Does property have
[PLCAddress]attribute or declared in JSON mapping? - Called
LoadAsync()yet?
// ✗ Wrong: Not reading from PLC
var data = new MyData(_plc);
var value = data.MyProperty; // Value is default (0, false, "")
// ✓ Correct: Read from PLC first
var data = new MyData(_plc);
await data.LoadAsync();
var value = data.MyProperty; // Value from PLC
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- McpX (>= 0.6.0)
- Newtonsoft.Json (>= 13.0.4)
-
net10.0
- McpX (>= 0.6.0)
- Newtonsoft.Json (>= 13.0.4)
-
net6.0
- McpX (>= 0.6.0)
- Newtonsoft.Json (>= 13.0.4)
-
net7.0
- McpX (>= 0.6.0)
- Newtonsoft.Json (>= 13.0.4)
-
net8.0
- McpX (>= 0.6.0)
- Newtonsoft.Json (>= 13.0.4)
-
net9.0
- McpX (>= 0.6.0)
- Newtonsoft.Json (>= 13.0.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.