LmxProxy is no longer needed. Moved the entire lmxproxy/ workspace, DCL adapter files, and related docs to deprecated/. Removed LmxProxy registration from DataConnectionFactory, project reference from DCL, protocol option from UI, and cleaned up all requirement docs.
26 KiB
LmxProxy Protocol v2 — OPC UA Alignment
This document specifies all changes to the LmxProxy gRPC protocol to align it with OPC UA semantics. The changes replace string-serialized values with typed values and simple quality strings with OPC UA-style status codes.
Baseline: lmxproxy_protocol.md (v1 protocol spec)
Strategy: Clean break — all clients and servers updated simultaneously. No backward compatibility layer.
1. Change Summary
| Message / Field | v1 Type | v2 Type | Breaking? |
|---|---|---|---|
VtqMessage.value |
string |
TypedValue |
Yes |
VtqMessage.quality |
string |
QualityCode |
Yes |
WriteRequest.value |
string |
TypedValue |
Yes |
WriteItem.value |
string |
TypedValue |
Yes |
WriteBatchAndWaitRequest.flag_value |
string |
TypedValue |
Yes |
Unchanged messages: ConnectRequest, ConnectResponse, DisconnectRequest, DisconnectResponse, GetConnectionStateRequest, GetConnectionStateResponse, CheckApiKeyRequest, CheckApiKeyResponse, ReadRequest, ReadBatchRequest, SubscribeRequest, WriteResponse, WriteBatchResponse, WriteBatchAndWaitResponse, WriteResult.
Unchanged RPCs: The ScadaService definition is identical — same RPC names, same request/response pairing. Only the internal message shapes change.
2. Complete Updated Proto File
syntax = "proto3";
package scada;
// ============================================================
// Service Definition (unchanged)
// ============================================================
service ScadaService {
rpc Connect(ConnectRequest) returns (ConnectResponse);
rpc Disconnect(DisconnectRequest) returns (DisconnectResponse);
rpc GetConnectionState(GetConnectionStateRequest) returns (GetConnectionStateResponse);
rpc Read(ReadRequest) returns (ReadResponse);
rpc ReadBatch(ReadBatchRequest) returns (ReadBatchResponse);
rpc Write(WriteRequest) returns (WriteResponse);
rpc WriteBatch(WriteBatchRequest) returns (WriteBatchResponse);
rpc WriteBatchAndWait(WriteBatchAndWaitRequest) returns (WriteBatchAndWaitResponse);
rpc Subscribe(SubscribeRequest) returns (stream VtqMessage);
rpc CheckApiKey(CheckApiKeyRequest) returns (CheckApiKeyResponse);
}
// ============================================================
// NEW: Typed Value System
// ============================================================
// Replaces the v1 string-encoded value field.
// Exactly one field will be set. An unset oneof represents null.
message TypedValue {
oneof value {
bool bool_value = 1;
int32 int32_value = 2;
int64 int64_value = 3;
float float_value = 4;
double double_value = 5;
string string_value = 6;
bytes bytes_value = 7; // byte[]
int64 datetime_value = 8; // UTC DateTime.Ticks (100ns intervals since 0001-01-01)
ArrayValue array_value = 9; // arrays of primitives
}
}
// Container for typed arrays. Exactly one field will be set.
message ArrayValue {
oneof values {
BoolArray bool_values = 1;
Int32Array int32_values = 2;
Int64Array int64_values = 3;
FloatArray float_values = 4;
DoubleArray double_values = 5;
StringArray string_values = 6;
}
}
message BoolArray { repeated bool values = 1; }
message Int32Array { repeated int32 values = 1; }
message Int64Array { repeated int64 values = 1; }
message FloatArray { repeated float values = 1; }
message DoubleArray { repeated double values = 1; }
message StringArray { repeated string values = 1; }
// ============================================================
// NEW: OPC UA-Style Quality Codes
// ============================================================
// Replaces the v1 string quality field ("Good", "Bad", "Uncertain").
message QualityCode {
uint32 status_code = 1; // OPC UA-compatible numeric status code
string symbolic_name = 2; // Human-readable name (e.g., "Good", "BadSensorFailure")
}
// ============================================================
// Connection Lifecycle (unchanged)
// ============================================================
message ConnectRequest {
string client_id = 1;
string api_key = 2;
}
message ConnectResponse {
bool success = 1;
string message = 2;
string session_id = 3;
}
message DisconnectRequest {
string session_id = 1;
}
message DisconnectResponse {
bool success = 1;
string message = 2;
}
message GetConnectionStateRequest {
string session_id = 1;
}
message GetConnectionStateResponse {
bool is_connected = 1;
string client_id = 2;
int64 connected_since_utc_ticks = 3;
}
message CheckApiKeyRequest {
string api_key = 1;
}
message CheckApiKeyResponse {
bool is_valid = 1;
string message = 2;
}
// ============================================================
// Value-Timestamp-Quality (CHANGED)
// ============================================================
message VtqMessage {
string tag = 1; // Tag address (unchanged)
TypedValue value = 2; // CHANGED: typed value instead of string
int64 timestamp_utc_ticks = 3; // UTC DateTime.Ticks (unchanged)
QualityCode quality = 4; // CHANGED: structured quality instead of string
}
// ============================================================
// Read Operations (request unchanged, response uses new VtqMessage)
// ============================================================
message ReadRequest {
string session_id = 1;
string tag = 2;
}
message ReadResponse {
bool success = 1;
string message = 2;
VtqMessage vtq = 3; // Uses updated VtqMessage with TypedValue + QualityCode
}
message ReadBatchRequest {
string session_id = 1;
repeated string tags = 2;
}
message ReadBatchResponse {
bool success = 1;
string message = 2;
repeated VtqMessage vtqs = 3; // Uses updated VtqMessage
}
// ============================================================
// Write Operations (CHANGED: TypedValue instead of string)
// ============================================================
message WriteRequest {
string session_id = 1;
string tag = 2;
TypedValue value = 3; // CHANGED from string
}
message WriteResponse {
bool success = 1;
string message = 2;
}
message WriteItem {
string tag = 1;
TypedValue value = 2; // CHANGED from string
}
message WriteResult {
string tag = 1;
bool success = 2;
string message = 3;
}
message WriteBatchRequest {
string session_id = 1;
repeated WriteItem items = 2;
}
message WriteBatchResponse {
bool success = 1;
string message = 2;
repeated WriteResult results = 3;
}
// ============================================================
// WriteBatchAndWait (CHANGED: TypedValue for items and flag)
// ============================================================
message WriteBatchAndWaitRequest {
string session_id = 1;
repeated WriteItem items = 2; // Uses updated WriteItem with TypedValue
string flag_tag = 3;
TypedValue flag_value = 4; // CHANGED from string — type-aware comparison
int32 timeout_ms = 5;
int32 poll_interval_ms = 6;
}
message WriteBatchAndWaitResponse {
bool success = 1;
string message = 2;
repeated WriteResult write_results = 3;
bool flag_reached = 4;
int32 elapsed_ms = 5;
}
// ============================================================
// Subscription (request unchanged, stream uses new VtqMessage)
// ============================================================
message SubscribeRequest {
string session_id = 1;
repeated string tags = 2;
int32 sampling_ms = 3;
}
// Returns: stream of VtqMessage (updated with TypedValue + QualityCode)
3. Detailed Change Specifications
3.1 Typed Value Representation
What changed: The string value field throughout the protocol is replaced by TypedValue, a protobuf oneof that carries the value in its native type.
v1 behavior (removed):
- All values serialized to string via
.ToString() - Client-side parsing heuristic: numeric → bool → string → null
- Arrays JSON-serialized as strings (e.g.,
"[1,2,3]") - Empty string treated as null
v2 behavior:
- Values transmitted in their native protobuf type
- No parsing ambiguity — the
oneofcase tells you the type - Arrays use dedicated repeated-field messages (
Int32Array,FloatArray, etc.) - Null represented by an unset
oneof(no field selected inTypedValue) datetime_valueusesint64UTC Ticks (same wire encoding as v1 timestamps, but now semantically typed as a DateTime value rather than a string)
Null handling:
| Scenario | v1 | v2 |
|---|---|---|
| Null value | value = "" (empty string) |
TypedValue with no oneof case set |
| Missing VTQ | Treated as Bad quality, null value | Same — Bad quality, unset TypedValue |
Type mapping from internal tag model:
| Tag Data Type | TypedValue Field | Notes |
|---|---|---|
bool |
bool_value |
|
int32 |
int32_value |
|
int64 |
int64_value |
|
float |
float_value |
|
double |
double_value |
|
string |
string_value |
|
byte[] |
bytes_value |
|
DateTime |
datetime_value |
UTC Ticks as int64 |
float[] |
array_value.float_values |
|
int32[] |
array_value.int32_values |
|
| Other arrays | Corresponding ArrayValue field |
3.2 OPC UA-Style Quality Codes
What changed: The string quality field (one of "Good", "Uncertain", "Bad") is replaced by QualityCode containing a numeric OPC UA status code and a human-readable symbolic name.
v1 behavior (removed):
- Quality as case-insensitive string:
"Good","Uncertain","Bad" - No sub-codes — all failures were just
"Bad"
v2 behavior:
status_codeis auint32matching OPC UAStatusCodebit layoutsymbolic_nameis the human-readable equivalent (for logging, debugging, display)- Category derived from high bits:
0x00xxxxxx= Good,0x40xxxxxx= Uncertain,0x80xxxxxx= Bad
Supported quality codes:
The quality codes below are filtered to those actively used by AVEVA System Platform, InTouch, and OI Server/DAServer (per AVEVA Tech Note TN1305). AVEVA's ecosystem maps OPC DA quality codes to OPC UA status codes when communicating over OPC UA. This table includes the OPC UA equivalents for the AVEVA-relevant quality states.
Good Quality:
| Symbolic Name | Status Code | AVEVA OPC DA Hex | AVEVA Description |
|---|---|---|---|
Good |
0x00000000 |
0x00C0 |
Value is reliable, non-specific |
GoodLocalOverride |
0x00D80000 |
0x00D8 |
Value has been manually overridden; input disconnected |
Uncertain Quality:
| Symbolic Name | Status Code | AVEVA OPC DA Hex | AVEVA Description |
|---|---|---|---|
UncertainLastUsableValue |
0x40900000 |
0x0044 |
External source stopped writing; value is stale |
UncertainSensorNotAccurate |
0x42390000 |
0x0050 |
Sensor out of calibration or clamped at limit |
UncertainEngineeringUnitsExceeded |
0x40540000 |
0x0054 |
Value is outside defined engineering limits |
UncertainSubNormal |
0x40580000 |
0x0058 |
Derived from multiple sources with insufficient good sources |
Bad Quality:
| Symbolic Name | Status Code | AVEVA OPC DA Hex | AVEVA Description |
|---|---|---|---|
Bad |
0x80000000 |
0x0000 |
Non-specific bad; value is not useful |
BadConfigurationError |
0x80040000 |
0x0004 |
Server-specific configuration problem (e.g., item deleted) |
BadNotConnected |
0x808A0000 |
0x0008 |
Input not logically connected to a source |
BadDeviceFailure |
0x806B0000 |
0x000C |
Device failure detected |
BadSensorFailure |
0x806D0000 |
0x0010 |
Sensor failure detected |
BadLastKnownValue |
0x80050000 |
0x0014 |
Communication failed; last known value available (check timestamp age) |
BadCommunicationFailure |
0x80050000 |
0x0018 |
Communication failed; no last known value available |
BadOutOfService |
0x808F0000 |
0x001C |
Block is off-scan or locked; item/group is inactive |
Notes:
- AVEVA OPC DA quality codes use a 16-bit structure: 2 bits major (Good/Bad/Uncertain), 4 bits minor (sub-status), 2 bits limit (Not Limited, Low, High, Constant). The OPC UA status codes above are the standard UA equivalents.
- The limit bits (Not Limited
0x00, Low Limited0x01, High Limited0x02, Constant0x03) are appended to any quality code. For example,Good + High Limited=0x00C2in OPC DA. In OPC UA, limits are conveyed via separate status code bits but the base code remains the same. - AVEVA's "Initializing" state (seen when OI Server is still establishing communication) maps to
Badwith no sub-code in OPC DA (0x0000). In OPC UA this isBadWaitingForInitialData(0x80320000). - This is the minimum set needed to simulate realistic AVEVA System Platform behavior. Additional OPC UA codes can be added if specific simulation scenarios require them.
Category helper logic (C#):
public static string GetCategory(uint statusCode) => statusCode switch
{
_ when (statusCode & 0xC0000000) == 0x00000000 => "Good",
_ when (statusCode & 0xC0000000) == 0x40000000 => "Uncertain",
_ when (statusCode & 0xC0000000) == 0x80000000 => "Bad",
_ => "Unknown"
};
public static bool IsGood(uint statusCode) => (statusCode & 0xC0000000) == 0x00000000;
public static bool IsBad(uint statusCode) => (statusCode & 0xC0000000) == 0x80000000;
3.3 WriteBatchAndWait Flag Comparison
What changed: flag_value is now TypedValue instead of string. The server uses type-aware equality comparison instead of string comparison.
v1 behavior (removed):
// v1: string comparison
bool matched = readResult.Value?.ToString() == request.FlagValue;
v2 behavior:
// v2: type-aware comparison
bool matched = TypedValueEquals(readResult.TypedValue, request.FlagValue);
Comparison rules:
- Both values must have the same
oneofcase (same type). Mismatched types are never equal. - Numeric comparison uses the native type's equality (no floating-point string round-trip issues).
- String comparison is case-sensitive (unchanged from v1).
- Bool comparison is direct equality.
- Null (unset
oneof) equals null. Null does not equal any set value. - Array comparison: element-by-element equality, same length required.
datetime_valuecompared asint64equality (tick-level precision).
4. Behavioral Changes
4.1 Read Operations
No RPC signature changes. The returned VtqMessage now uses TypedValue and QualityCode instead of strings.
v1 client code:
var response = await client.ReadAsync(new ReadRequest { SessionId = sid, Tag = "Motor.Speed" });
double value = double.Parse(response.Vtq.Value); // string → double
bool isGood = response.Vtq.Quality.Equals("Good", ...); // string comparison
v2 client code:
var response = await client.ReadAsync(new ReadRequest { SessionId = sid, Tag = "Motor.Speed" });
double value = response.Vtq.Value.DoubleValue; // direct typed access
bool isGood = response.Vtq.Quality.StatusCode == 0x00000000; // numeric comparison
// or: bool isGood = IsGood(response.Vtq.Quality.StatusCode); // helper method
4.2 Write Operations
Client must construct TypedValue instead of converting to string.
v1 client code:
await client.WriteAsync(new WriteRequest
{
SessionId = sid,
Tag = "Motor.Speed",
Value = 42.5.ToString() // double → string
});
v2 client code:
await client.WriteAsync(new WriteRequest
{
SessionId = sid,
Tag = "Motor.Speed",
Value = new TypedValue { DoubleValue = 42.5 } // native type
});
4.3 Subscription Stream
No RPC signature changes. The streamed VtqMessage items now use the updated format. Client onUpdate callbacks receive typed values and structured quality.
4.4 Error Conditions with New Quality Codes
The server now returns specific quality codes instead of generic "Bad":
| Scenario | v1 Quality | v2 Quality |
|---|---|---|
| Tag not found | "Bad" |
BadConfigurationError (0x80040000) |
| Tag read exception / comms loss | "Bad" |
BadCommunicationFailure (0x80050000) |
| Write to read-only tag | success=false |
WriteResult.success=false, message indicates read-only |
| Type mismatch on write | success=false |
WriteResult.success=false, message indicates type mismatch |
| Simulated sensor failure | "Bad" |
BadSensorFailure (0x806D0000) |
| Simulated device failure | "Bad" |
BadDeviceFailure (0x806B0000) |
| Stale value (fault injection) | "Uncertain" |
UncertainLastUsableValue (0x40900000) |
| Block off-scan / disabled | "Bad" |
BadOutOfService (0x808F0000) |
| Local override active | "Good" |
GoodLocalOverride (0x00D80000) |
| Initializing / waiting for first value | "Bad" |
BadWaitingForInitialData (0x80320000) |
5. Migration Guide
5.1 Strategy
Clean break — all clients and servers are updated simultaneously in a single coordinated release. No backward compatibility layer, no version negotiation, no dual-format support.
This is appropriate because:
- The LmxProxy is an internal protocol between ScadaLink components, not a public API
- The number of clients is small and controlled
- Maintaining dual formats adds complexity with no long-term benefit
5.2 Server-Side Changes
Files to update:
| File | Changes |
|---|---|
scada.proto |
Replace with v2 proto (Section 2 of this document) |
ScadaServiceImpl.cs |
Update all RPC handlers to construct TypedValue and QualityCode instead of strings |
SessionManager.cs |
No changes (session model unchanged) |
TagMapper.cs |
Update to return TypedValue from tag reads instead of string conversion |
Server implementation notes:
- When reading a tag, construct
TypedValueby setting the appropriateoneoffield based on the tag's data type. Do not call.ToString(). - When a tag read fails, return
QualityCode { StatusCode = 0x80050000, SymbolicName = "BadCommunicationFailure" }(or a more specific code) instead of the string"Bad". - When handling writes, extract the value from the
TypedValueoneof and apply it to the tag actor. If theoneofcase doesn't match the tag's expected data type, returnWriteResultwithsuccess=falseand message indicating type mismatch. - For
WriteBatchAndWaitflag comparison, implementTypedValueEquals()per the comparison rules in Section 3.3.
5.3 Client-Side Changes
Files to update:
| File | Changes |
|---|---|
ILmxProxyClient.cs |
Interface unchanged (same method signatures, updated message types come from proto regeneration) |
RealLmxProxyClient.cs |
Update value construction in write methods; update value extraction in read callbacks |
LmxProxyDataConnection.cs |
Update DCL adapter to map between DCL's internal value model and TypedValue/QualityCode |
LmxProxyClientFactory.cs |
No changes |
Client implementation notes:
- Replace all
double.Parse(vtq.Value)/bool.Parse(vtq.Value)calls with direct typed access (e.g.,vtq.Value.DoubleValue). - Replace all
vtq.Quality.Equals("Good", ...)string comparisons with numeric status code checks or theIsGood()/IsBad()helpers. - Replace all
.ToString()value serialization in write paths withTypedValueconstruction. - The
onUpdatecallback signature inSubscribeAsyncdoesn't change at the interface level, but theVtqMessageit receives now containsTypedValueandQualityCode.
5.4 Migration Checklist
[ ] Generate updated C# classes from v2 proto file
[ ] Update server: ScadaServiceImpl read handlers → TypedValue + QualityCode
[ ] Update server: ScadaServiceImpl write handlers → accept TypedValue
[ ] Update server: WriteBatchAndWait flag comparison → TypedValueEquals()
[ ] Update server: Error paths → specific QualityCode status codes
[ ] Update client: RealLmxProxyClient read paths → typed value extraction
[ ] Update client: RealLmxProxyClient write paths → TypedValue construction
[ ] Update client: Quality checks → numeric status code comparison
[ ] Update client: LmxProxyDataConnection DCL adapter → map TypedValue ↔ DCL values
[ ] Update all unit tests for new message shapes
[ ] Integration test: client ↔ server round-trip with all data types
[ ] Integration test: WriteBatchAndWait with typed flag comparison
[ ] Integration test: Subscription stream delivers typed VTQ messages
[ ] Integration test: Error paths return correct QualityCode sub-codes
[ ] Remove all string-based value parsing/serialization code
[ ] Remove all string-based quality comparison code
6. Test Scenarios for v2 Validation
These scenarios validate that the v2 protocol behaves correctly across all data types and quality codes.
6.1 Round-Trip Type Fidelity
For each supported data type, write a value via Write, read it back via Read, and verify the TypedValue oneof case and value match exactly:
| Data Type | Test Value | TypedValue Field | Verify |
|---|---|---|---|
bool |
true |
bool_value |
== true |
int32 |
2147483647 |
int32_value |
== int.MaxValue |
int64 |
9223372036854775807 |
int64_value |
== long.MaxValue |
float |
3.14159f |
float_value |
== 3.14159f (exact bits) |
double |
2.718281828459045 |
double_value |
== 2.718281828459045 (exact bits) |
string |
"Hello World" |
string_value |
== "Hello World" |
bytes |
[0x00, 0xFF, 0x42] |
bytes_value |
byte-for-byte match |
DateTime |
638789000000000000L |
datetime_value |
== 638789000000000000L |
float[] |
[1.0f, 2.0f, 3.0f] |
array_value.float_values |
element-wise match |
int32[] |
[10, 20, 30] |
array_value.int32_values |
element-wise match |
| null | (unset) | no oneof case | Value case == None |
6.2 Quality Code Propagation
| Scenario | Trigger | Expected QualityCode |
|---|---|---|
| Normal read | Read a healthy tag | { 0x00000000, "Good" } |
| Local override | Script sets GoodLocalOverride |
{ 0x00D80000, "GoodLocalOverride" } |
| Fault injection: sensor failure | Script sets BadSensorFailure |
{ 0x806D0000, "BadSensorFailure" } |
| Fault injection: device failure | Script sets BadDeviceFailure |
{ 0x806B0000, "BadDeviceFailure" } |
| Fault injection: stale value | Script sets UncertainLastUsableValue |
{ 0x40900000, "UncertainLastUsableValue" } |
| Fault injection: off-scan | Script sets BadOutOfService |
{ 0x808F0000, "BadOutOfService" } |
| Fault injection: comms failure | Script sets BadCommunicationFailure |
{ 0x80050000, "BadCommunicationFailure" } |
| Unknown tag | Read nonexistent tag | { 0x80040000, "BadConfigurationError" } |
| Write to read-only | Write to a read-only tag | WriteResult.success=false, message contains "read-only" |
6.3 WriteBatchAndWait Typed Flag Comparison
| Flag Type | Written Value | Flag Value | Expected Result |
|---|---|---|---|
bool |
true |
TypedValue { bool_value = true } |
flag_reached = true |
bool |
false |
TypedValue { bool_value = true } |
flag_reached = false (timeout) |
double |
42.5 |
TypedValue { double_value = 42.5 } |
flag_reached = true |
double |
42.500001 |
TypedValue { double_value = 42.5 } |
flag_reached = false |
string |
"DONE" |
TypedValue { string_value = "DONE" } |
flag_reached = true |
string |
"done" |
TypedValue { string_value = "DONE" } |
flag_reached = false (case-sensitive) |
int32 |
1 |
TypedValue { double_value = 1.0 } |
flag_reached = false (type mismatch) |
6.4 Subscription Stream
- Subscribe to tags of mixed data types
- Verify each streamed
VtqMessagehas the correctoneofcase matching the tag's data type - Inject a fault mid-stream and verify the quality code changes from
Goodto the injected code - Cancel the subscription and verify the stream terminates cleanly
7. Appendix: v1 → v2 Quick Reference
Reading a value:
// v1
string raw = vtq.Value;
if (double.TryParse(raw, out var d)) { /* use d */ }
else if (bool.TryParse(raw, out var b)) { /* use b */ }
else { /* it's a string */ }
// v2
switch (vtq.Value.ValueCase)
{
case TypedValue.ValueOneofCase.DoubleValue:
double d = vtq.Value.DoubleValue;
break;
case TypedValue.ValueOneofCase.BoolValue:
bool b = vtq.Value.BoolValue;
break;
case TypedValue.ValueOneofCase.StringValue:
string s = vtq.Value.StringValue;
break;
case TypedValue.ValueOneofCase.None:
// null value
break;
// ... other cases
}
Writing a value:
// v1
new WriteItem { Tag = "Motor.Speed", Value = 42.5.ToString() }
// v2
new WriteItem { Tag = "Motor.Speed", Value = new TypedValue { DoubleValue = 42.5 } }
Checking quality:
// v1
bool isGood = vtq.Quality.Equals("Good", StringComparison.OrdinalIgnoreCase);
bool isBad = vtq.Quality.Equals("Bad", StringComparison.OrdinalIgnoreCase);
// v2
bool isGood = (vtq.Quality.StatusCode & 0xC0000000) == 0x00000000;
bool isBad = (vtq.Quality.StatusCode & 0xC0000000) == 0x80000000;
// or use helper:
bool isGood = QualityHelper.IsGood(vtq.Quality.StatusCode);
Constructing quality (server-side):
// v1
vtq.Quality = "Good";
// v2
vtq.Quality = new QualityCode { StatusCode = 0x00000000, SymbolicName = "Good" };
// or for errors:
vtq.Quality = new QualityCode { StatusCode = 0x806D0000, SymbolicName = "BadSensorFailure" };
vtq.Quality = new QualityCode { StatusCode = 0x80050000, SymbolicName = "BadCommunicationFailure" };
vtq.Quality = new QualityCode { StatusCode = 0x00D80000, SymbolicName = "GoodLocalOverride" };
Document version: 1.0 — All decisions resolved. Complete proto, migration guide, and test scenarios.