Add component-level documentation for all 14 server subsystems
Provides technical documentation covering OPC UA server, address space, Galaxy repository, MXAccess bridge, data types, read/write, subscriptions, alarms, historian, incremental sync, configuration, dashboard, service hosting, and CLI tool. Updates README with component documentation table. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
141
docs/HistoricalDataAccess.md
Normal file
141
docs/HistoricalDataAccess.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Historical Data Access
|
||||
|
||||
`LmxNodeManager` exposes OPC UA historical data access (HDA) by querying the Wonderware Historian Runtime database. The `HistorianDataSource` class translates OPC UA history requests into SQL queries against the Historian's `History` and `AnalogSummaryHistory` views, and the node manager overrides wire the results back into the OPC UA response.
|
||||
|
||||
## Wonderware Historian Runtime Database
|
||||
|
||||
The Historian stores time-series data in a SQL Server database named `Runtime`. Two views are relevant:
|
||||
|
||||
- **`Runtime.dbo.History`** -- Raw historical samples with columns `DateTime`, `Value` (numeric), `vValue` (string), and `Quality`.
|
||||
- **`Runtime.dbo.AnalogSummaryHistory`** -- Pre-computed aggregates bucketed by `wwResolution` (milliseconds), with columns like `Average`, `Minimum`, `Maximum`, `ValueCount`, `First`, `Last`, `StdDev`.
|
||||
|
||||
Both views require `TagName` in the `WHERE` clause. This is a Historian constraint -- the views are optimized for tag-scoped queries and do not support efficient cross-tag scans.
|
||||
|
||||
## Configuration
|
||||
|
||||
`HistorianConfiguration` controls the historian connection:
|
||||
|
||||
```csharp
|
||||
public class HistorianConfiguration
|
||||
{
|
||||
public bool Enabled { get; set; } = false;
|
||||
public string ConnectionString { get; set; } =
|
||||
"Server=localhost;Database=Runtime;Integrated Security=true;";
|
||||
public int CommandTimeoutSeconds { get; set; } = 30;
|
||||
public int MaxValuesPerRead { get; set; } = 10000;
|
||||
}
|
||||
```
|
||||
|
||||
When `Enabled` is `false`, the `HistorianDataSource` is not instantiated and the node manager returns `BadHistoryOperationUnsupported` for history read requests.
|
||||
|
||||
## Raw Reads
|
||||
|
||||
`HistorianDataSource.ReadRawAsync` queries the `History` view for individual samples within a time range:
|
||||
|
||||
```sql
|
||||
SELECT TOP (@MaxValues) DateTime, Value, vValue, Quality
|
||||
FROM Runtime.dbo.History
|
||||
WHERE TagName = @TagName
|
||||
AND DateTime >= @StartTime AND DateTime <= @EndTime
|
||||
ORDER BY DateTime
|
||||
```
|
||||
|
||||
The `TOP` clause is included only when `maxValues > 0` (the OPC UA client specified `NumValuesPerNode`). Each row is converted to an OPC UA `DataValue`:
|
||||
|
||||
- `Value` column (double) takes priority over `vValue` (string). If both are null, the value is null.
|
||||
- `SourceTimestamp` and `ServerTimestamp` are both set to the `DateTime` column.
|
||||
- `StatusCode` is mapped from the Historian `Quality` byte via `MapQuality`.
|
||||
|
||||
## Aggregate Reads
|
||||
|
||||
`HistorianDataSource.ReadAggregateAsync` queries the `AnalogSummaryHistory` view for pre-computed aggregates:
|
||||
|
||||
```sql
|
||||
SELECT StartDateTime, [{aggregateColumn}]
|
||||
FROM Runtime.dbo.AnalogSummaryHistory
|
||||
WHERE TagName = @TagName
|
||||
AND StartDateTime >= @StartTime AND StartDateTime <= @EndTime
|
||||
AND wwResolution = @Resolution
|
||||
ORDER BY StartDateTime
|
||||
```
|
||||
|
||||
The `aggregateColumn` is interpolated directly into the SQL (it comes from the controlled `MapAggregateToColumn` mapping, not from user input). The `wwResolution` parameter maps from the OPC UA `ProcessingInterval` in milliseconds.
|
||||
|
||||
Null aggregate values return `BadNoData` status rather than `Good` with a null variant.
|
||||
|
||||
## Quality Mapping
|
||||
|
||||
`MapQuality` converts Wonderware Historian quality bytes to OPC UA status codes:
|
||||
|
||||
| Historian Quality | OPC UA StatusCode |
|
||||
|---|---|
|
||||
| 0 | `Good` |
|
||||
| 1 | `Bad` |
|
||||
| 2-127 | `Bad` |
|
||||
| 128+ | `Uncertain` |
|
||||
|
||||
This follows the Wonderware convention where quality 0 indicates a good sample, 1 indicates explicitly bad data, and values at or above 128 represent uncertain quality (e.g., interpolated or suspect values).
|
||||
|
||||
## Aggregate Function Mapping
|
||||
|
||||
`MapAggregateToColumn` translates OPC UA aggregate NodeIds to Historian column names:
|
||||
|
||||
| OPC UA Aggregate | Historian Column |
|
||||
|---|---|
|
||||
| `AggregateFunction_Average` | `Average` |
|
||||
| `AggregateFunction_Minimum` | `Minimum` |
|
||||
| `AggregateFunction_Maximum` | `Maximum` |
|
||||
| `AggregateFunction_Count` | `ValueCount` |
|
||||
| `AggregateFunction_Start` | `First` |
|
||||
| `AggregateFunction_End` | `Last` |
|
||||
| `AggregateFunction_StandardDeviationPopulation` | `StdDev` |
|
||||
|
||||
Unsupported aggregates return `null`, which causes the node manager to return `BadAggregateNotSupported`.
|
||||
|
||||
## HistoryReadRawModified Override
|
||||
|
||||
`LmxNodeManager` overrides `HistoryReadRawModified` to handle raw history read requests:
|
||||
|
||||
1. Resolve the `NodeHandle` to a tag reference via `_nodeIdToTagReference`. Return `BadNodeIdUnknown` if not found.
|
||||
2. Check that `_historianDataSource` is not null. Return `BadHistoryOperationUnsupported` if historian is disabled.
|
||||
3. Call `ReadRawAsync` with the time range and `NumValuesPerNode` from the `ReadRawModifiedDetails`.
|
||||
4. Pack the resulting `DataValue` list into a `HistoryData` object and wrap it in an `ExtensionObject` for the `HistoryReadResult`.
|
||||
|
||||
```csharp
|
||||
var dataValues = _historianDataSource.ReadRawAsync(
|
||||
tagRef, details.StartTime, details.EndTime, maxValues)
|
||||
.GetAwaiter().GetResult();
|
||||
|
||||
var historyData = new HistoryData();
|
||||
historyData.DataValues.AddRange(dataValues);
|
||||
results[idx] = new HistoryReadResult
|
||||
{
|
||||
StatusCode = StatusCodes.Good,
|
||||
HistoryData = new ExtensionObject(historyData)
|
||||
};
|
||||
```
|
||||
|
||||
## HistoryReadProcessed Override
|
||||
|
||||
`HistoryReadProcessed` handles aggregate history requests with additional validation:
|
||||
|
||||
1. Resolve the node and check historian availability (same as raw).
|
||||
2. Validate that `AggregateType` is present in the `ReadProcessedDetails`. Return `BadAggregateListMismatch` if empty.
|
||||
3. Map the requested aggregate to a Historian column via `MapAggregateToColumn`. Return `BadAggregateNotSupported` if unmapped.
|
||||
4. Call `ReadAggregateAsync` with the time range, `ProcessingInterval`, and column name.
|
||||
5. Return results in the same `HistoryData` / `ExtensionObject` format.
|
||||
|
||||
## Historizing Flag and AccessLevel
|
||||
|
||||
During variable node creation in `CreateAttributeVariable`, attributes with `IsHistorized == true` receive two additional settings:
|
||||
|
||||
```csharp
|
||||
if (attr.IsHistorized)
|
||||
accessLevel |= AccessLevels.HistoryRead;
|
||||
variable.Historizing = attr.IsHistorized;
|
||||
```
|
||||
|
||||
- **`Historizing = true`** -- Tells OPC UA clients that this node has historical data available.
|
||||
- **`AccessLevels.HistoryRead`** -- Enables the `HistoryRead` access bit on the node, which the OPC UA stack checks before routing history requests to the node manager override. Nodes without this bit set will be rejected by the framework before reaching `HistoryReadRawModified` or `HistoryReadProcessed`.
|
||||
|
||||
The `IsHistorized` flag originates from the Galaxy repository database query, which checks whether the attribute has Historian logging configured.
|
||||
Reference in New Issue
Block a user