Resolving VB6 OPC UA Read/Write Errors on S7-1500 Systems

David Krause15 min read
OPC / OPC UASiemensTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Problem Overview

Engineers maintaining legacy SCADA/HMI stacks sometimes need to read or write Siemens S7-1500 data blocks (DBs) from a Visual Basic 6.0 (VB6) application running on a Windows PC. VB6 was retired by Microsoft in 2008, but it still runs in many production environments. The S7-1500 family, in contrast, exposes its data only through the modern OPC UA stack (no native OPC DA server is available on the CPU firmware). Bridging the two creates a hard architectural incompatibility.

The most common failure is the following compile error raised the moment a VB6 project references an OPC UA .NET assembly and tries to call ReadValues or WriteValues:

Compile error:
Function or interface marked as restricted, or the function uses
an Automation type not supported in Visual Basic

The error is not related to the S7-1500, to TIA Portal, or to the network. It is generated by the VB6 compiler at design time because the OPC UA .NET client (Siemens OPC UA .NET Wrapper, OPC Foundation .NET Standard Stack, or similar) does not expose a COM Automation-compatible type library that VB6 can consume. This article explains the root cause, presents the supported alternatives, and walks through the TIA Portal V20 server configuration that any working replacement will require.

Root Cause Analysis

Visual Basic 6.0 can only consume COM Automation (IDispatch-based) components whose type libraries contain only data types that map cleanly to OLE Automation Variant types. The full set of legal VB6-callable types is roughly:

  • Scalar types: Boolean, Byte, Integer, Long, Single, Double, Currency, Date, String, Variant, Object
  • User-defined types (UDTs) declared with Type ... End Type in VB6
  • Arrays of the above
  • COM-compatible interfaces (dual, IDispatch-only, or custom with Automation marshaling)

OPC UA .NET client libraries (Siemens Siemens.UAClientHelper.dll, OPC Foundation OPCFoundation.NetStandard.Opc.Ua, UA-.NETStandard, etc.) use constructs that VB6 cannot marshal:

  • Generic types such as List<T>, IEnumerable<T>, Dictionary<K,V>
  • Value-type structs such as NodeId, ExpandedNodeId, DataValue, Variant, StatusCode
  • Nullable<T> and other generic value wrappers
  • Delegates and async Task/Task<T> methods (the modern OPC UA stacks are entirely async)
  • Read-only properties with no setter and ref-returning members

When VB6 attempts to compile the reference, the type library importer marks the offending method as restricted in the generated .tlb. The compiler then raises the error above for any call to that method. ReadValues typically fails because it returns IList<DataValue> or DataValueCollection - both generic collections the VB6 type-library importer cannot describe.

Design-time failure: The error fires before any code runs. Network state, TIA Portal project state, certificates, and security policies are not involved. The compile error blocks the EXE from being produced at all.

Why OPC UA Cannot Be Made Native to VB6

OPC UA, as specified in IEC 62541, is built on a generic, type-system-aware information model. Nodes carry NodeId values, attributes are returned as DataValue with StatusCode and timestamps, and the client must consume binary or XML ExtensionObject payloads for structured types. The OPC Foundation .NET Standard stack is the reference implementation; every synchronous helper method is async Task<T> and every type is a struct or generic. A direct VB6 binding is therefore architecturally impossible without a custom COM bridge.

Legacy paths that work in VB6 (e.g., Prodave, Libnodave, S7-OWS, ACCON-AGLink) talk to the S7 via the Put/Get protocol on ISO-on-TCP (RFC 1006, port 102), not OPC UA. Those libraries are typically delivered as 32-bit COM DLLs that VB6 references naturally. They are not OPC UA, however, and S7-1500 CPU firmware disables Put/Get by default for security reasons (more on this below).

Recommended Solution Path

For new development and for production support, the supported path is to retire the VB6 client. The replacement can be:

  1. VB.NET (Windows Forms or Console) inside the same .NET Framework 4.8 process - minimum-impact port. The OPC UA .NET stack loads natively. The VB6 form is replaced with a WinForms form, but the logic and DB map are preserved.
  2. C# (.NET Framework 4.8 or .NET 6/8) with WinForms or WPF - the canonical Siemens-supported path. The example in Siemens support entry 109737901 uses C# and is the reference implementation.
  3. VBA in Microsoft Excel - if the consumer is really a spreadsheet macro and not a VB6 EXE, the Siemens support entry 109748892 Excel/VBA example compiles under Office 365 and reads/writes DBs directly. Office 32-bit hosts a .NET-compatible VBA runtime, so OPC UA wrappers load.
  4. VB6 + custom COM bridge - build a thin .NET COM-visible wrapper that exposes one method per needed tag (e.g., ReadDInt(tag), WriteReal(tag, val)). The wrapper hides the OPC UA generic types. This is the only path that keeps the original VB6 EXE but adds significant maintenance cost.

Configuring the S7-1500 OPC UA Server (TIA Portal V20)

Whichever client path is chosen, the PLC must be set up as an OPC UA server. The S7-1500 CPU firmware (V2.5 and later) and the S7-1500T (V20 and later) include a built-in OPC UA server. Enable and tune it in TIA Portal under PLC properties → OPC UA → Server.

Server activation

  1. Open the device configuration of the S7-1500 CPU.
  2. Select Properties → OPC UA → Server.
  3. Check Activate OPC UA Server.
  4. Set the desired port (default 4840).
  5. Choose the security policies your client supports. For modern .NET 6/8 clients, enable None only for commissioning, and at minimum Basic128Rsa15 or Basic256Sha256 for production. For firmware V20, prefer Aes128Sha256RsaOaep and Aes256Sha256RsaPss.
  6. Select None / Sign / SignAndEncrypt for Message security mode for each policy.

Endpoint and authentication

By default, the S7-1500 OPC UA server accepts anonymous sessions and username/password sessions. Certificate-based authentication is enforced through the TIA Portal project trust list. In V20, the path is Properties → OPC UA → Server → Security → User authentication:

Authentication mode Setting in TIA Portal Client behavior
Anonymous Tick "Allow anonymous" No credentials required; recommended off in production.
Username/password Tick "Enable user authentication"; assign local users or map to PLC user groups Client supplies UserName/Password token.
Certificate (Application) Trust list of client certs under Security → Trusted clients Client presents X.509 cert; trust list must contain its thumbprint.

Server certificate

The S7-1500 generates a self-signed server certificate the first time the OPC UA server is activated. Export it from the CPU's web server (Diagnostics → Certificates) or via TIA Portal Online → Certificate management, and import it into the client machine's Trusted People store. The reverse is also required: the client application's certificate must be installed on the CPU under Trusted clients. Without this, Bad_CommunicationError with status 0x80210000 (Bad_SecurityChecksFailed) is returned on every session activation.

Managing Data Block Read/Write Rights

By default, every DB on the S7-1500 is fully readable and writable from an OPC UA client. In a production environment you usually want to lock this down. The TIA Portal V20 path for global DB rights is documented in Managing write and read rights for a complete DB (S7-1500 / S7-1500T).

Global read/write toggle (default for new DBs)

  1. Open the device configuration of the CPU.
  2. Select Properties → OPC UA → Server → Data access.
  3. Under Default for new data blocks choose either Read/Write or Read only.
  4. Compile and download to the CPU.

Per-DB access level override

  1. Open the DB in the TIA Portal project tree.
  2. Select Properties → Attributes.
  3. Switch to OPC UA tab.
  4. Set Access to Read or Read/Write explicitly. The setting overrides the global default.

Optimized vs. non-optimized block access

OPC UA can address every tag in every DB regardless of the Optimized block access setting, but the NodeId format is different:

Block attribute NodeId style Example
Optimized = No (classic S7-1500 layout) String-based with offset ns=3;s="DB_Motor"."Speed"
Optimized = Yes (default in V20) Symbolic name only ns=3;s="DB_Motor".Speed
Namespace index: The default S7-1500 server namespace is index 3. A custom namespace index appears only when the project defines it under Properties → OPC UA → Namespaces.

Worked Example: Minimal C# OPC UA Client

The following C# 8.0 example uses the OPC Foundation .NET Standard 1.5 stack and reproduces what the VB6 application was trying to do. It is the basis for porting the VB6 logic to a real .NET environment.

// Program.cs (console target, .NET 6 or later)
using Opc.Ua;
using Opc.Ua.Client;

class Program
{
    static async Task Main(string[] args)
    {
        // 1) Discovery
        var endpointUrl = "opc.tcp://192.168.0.10:4840";
        var discovery = new DiscoveryClient(
            await DiscoveryClient.CreateAsync(
                new Uri(endpointUrl + "/discovery"),
                new EndpointConfiguration()));

        var endpoints = await discovery.GetEndpointsAsync(null);
        var selected = Server.FindEndpoint(
            endpoints, endpointUrl, SecurityPolicyUris.None);

        // 2) Session
        var appConfig = new ApplicationConfiguration {
            ApplicationName = "VB6-Replace",
            ApplicationType = ApplicationType.Client,
            SecurityConfiguration = new SecurityConfiguration {
                AutoAcceptUntrustedCertificates = true,
            },
            TransportQuotas = new TransportQuotas { OperationTimeout = 15000 },
            ClientConfiguration = new ClientConfiguration(),
        };
        await appConfig.ValidateAsync(ApplicationType.Client);

        var session = await Session.Create(
            appConfig, new ConfiguredEndpoint(selected), false,
            "VB6-Replace", 60000, null, null);

        // 3) Build ReadValueId list
        var nodes = new ReadValueIdCollection {
            new ReadValueId {
                NodeId = new NodeId("DB_Motor.Speed", 3),
                AttributeId = Attributes.Value,
            },
            new ReadValueId {
                NodeId = new NodeId("DB_Motor.Current", 3),
                AttributeId = Attributes.Value,
            },
        };

        // 4) Read
        var response = await session.ReadAsync(
            null, 0, TimestampsToReturn.Both, nodes);

        foreach (var dv in response.Results)
        {
            Console.WriteLine(
                $"Value = {dv.Value}, Status = {dv.StatusCode}");
        }

        // 5) Write (single tag)
        var writeNodes = new WriteValueCollection {
            new WriteValue {
                NodeId = new NodeId("DB_Motor.Setpoint", 3),
                AttributeId = Attributes.Value,
                Value = new DataValue((float)1234.5),
            },
        };
        await session.WriteAsync(null, writeNodes);

        await session.CloseAsync();
    }
}

The same project compiles to a single EXE that replaces the VB6 client. Connection loss, certificate prompts, and authentication are all handled by the .NET stack, removing the original VB6 compile error entirely.

VB6 + .NET COM Bridge (If You Must Keep VB6)

When there is no budget to rewrite the VB6 application, a thin .NET COM-visible assembly can expose OPC UA tags as Automation-compatible scalar methods. The bridge DLL is registered with regasm /codebase and then referenced from VB6 exactly like any other COM component.

// OpcBridge.cs (Class Library, .NET Framework 4.8, COM visible)
using System;
using System.Runtime.InteropServices;
using Opc.Ua;
using Opc.Ua.Client;
using System.Threading.Tasks;

namespace Vb6OpcBridge
{
    [ComVisible(true)]
    [Guid("A0B1C2D3-1111-2222-3333-444455556666")]
    [InterfaceType(ComInterfaceType.InterfaceIsDual)]
    public interface IOpcBridge
    {
        [DispId(1)] bool Connect(string endpointUrl, string user, string pwd);
        [DispId(2)] void Disconnect();
        [DispId(3)] object ReadScalar(string nodeId);   // returns Double or String
        [DispId(4)] bool   WriteScalar(string nodeId, object value);
    }

    [ComVisible(true)]
    [Guid("A0B1C2D3-7777-8888-9999-AAAABBBBCCCC")]
    [ClassInterface(ClassInterfaceType.None)]
    [ProgId("Vb6OpcBridge.OpcBridge")]
    public class OpcBridge : IOpcBridge
    {
        private Session _session;
        private static readonly TaskScheduler _uiScheduler =
            TaskScheduler.FromCurrentSynchronizationContext();

        public bool Connect(string endpointUrl, string user, string pwd)
        {
            var t = Task.Run(async () => await ConnectAsync(
                endpointUrl, user, pwd));
            t.Wait();
            return t.Result;
        }

        private async Task<bool> ConnectAsync(
            string endpointUrl, string user, string pwd)
        {
            var cfg = new ApplicationConfiguration { /* ...as above... */ };
            await cfg.ValidateAsync(ApplicationType.Client);
            var ep = await DiscoveryClient.SelectEndpointAsync(
                new Uri(endpointUrl + "/discovery"), false);
            var userToken = new UserIdentity(user, pwd);
            _session = await Session.Create(
                cfg, new ConfiguredEndpoint(ep), false, false,
                "VB6-Bridge", 60000, userToken, null);
            return _session != null && _session.Connected;
        }

        public void Disconnect() => _session?.CloseAsync().Wait();

        public object ReadScalar(string nodeId)
        {
            var t = Task.Run(async () => await _session.ReadValueAsync(
                new NodeId(nodeId, 3)));
            t.Wait();
            return t.Result?.Value;
        }

        public bool WriteScalar(string nodeId, object value)
        {
            var dv = new DataValue(value);
            var t = Task.Run(async () => await _session.WriteValueAsync(
                new NodeId(nodeId, 3), dv));
            t.Wait();
            return t.Result == StatusCodes.Good;
        }
    }
}

From VB6 the call becomes:

Dim bridge As Object
Set bridge = CreateObject("Vb6OpcBridge.OpcBridge")

If bridge.Connect("opc.tcp://192.168.0.10:4840", "", "") Then
    Dim speed As Variant
    speed = bridge.ReadScalar("DB_Motor.Speed")
    Debug.Print "Speed = " & speed

    bridge.WriteScalar "DB_Motor.Setpoint", 1234.5
End If

The bridge serialises all OPC UA types into Variant at the COM boundary, which is the only type VB6 can carry safely. Struct fields, arrays, and timestamps have to be flattened into separate methods (ReadScalar, ReadArray, ReadTimestamp) by hand.

Verification Steps

After the OPC UA server is enabled and the client is connected, validate end-to-end with the following checks:

  1. Server reachable: From a Windows PC, run opc.tcp://192.168.0.10:4840 in the Siemens OPC UA Scout (V20) or in the UaExpert client. A successful endpoint list confirms TIA Portal settings and certificate exchange.
  2. Anonymous session blocked (if disabled): Connect with empty credentials; the session must be rejected with Bad_IdentityTokenRejected (0x80210000). If a session is created, the user-token policy is still anonymous.
  3. Read of every variable class: Read at least one Bool, one Int, one Real, one String, and one DTL (date/time) variable. The status code must be Good (0x00000000).
  4. Read-only DB enforced: On a DB that has Access = Read, attempt a write. The server must return Bad_NotWritable (0x803B0000).
  5. Optimized vs. non-optimized NodeId: Verify the NodeId by browsing the address space. The format must match the table above.
  6. Round-trip latency: 100 reads of a 4-byte variable should complete in < 200 ms on a clean gigabit LAN. A higher number indicates certificate verification overhead or a slow Subscribe setup.

Troubleshooting Matrix

Symptom Likely cause Fix
VB6: "Function or interface marked as restricted" OPC UA .NET assembly referenced from a VB6 project Migrate to VB.NET/C# or build a COM bridge (see above).
Session rejected with Bad_CommunicationError Firewall blocks TCP 4840 or wrong port Open 4840/tcp on the S7-1500 and on Windows Defender Firewall; verify with Test-NetConnection 192.168.0.10 -Port 4840.
Bad_SecurityChecksFailed (0x80210000) Client cert not in CPU trust list or server cert not in client trust store Export the CPU's server cert and import it into the client. Export the client's app cert and import it into CPU Properties → Security → Trusted clients.
Bad_IdentityTokenRejected (0x80200000) Anonymous disabled but no username supplied, or wrong password Use Session.Create with UserIdentity("user", "pwd"); verify the user is in the PLC user list under Properties → OPC UA → Security → User management.
Bad_NodeIdUnknown (0x80340000) Wrong namespace or wrong NodeId syntax Browse the address space with UaExpert to confirm NodeId string. For non-optimized DBs, the syntax is "DB_name"."Tag" with quotes; for optimized, "DB_name".Tag without quotes on the second segment.
Bad_NotWritable (0x803B0000) DB or variable is set to read-only Change Properties → Attributes → OPC UA → Access to Read/Write; recompile and download to the CPU.
Server not visible in UaExpert OPC UA server not activated in TIA Portal or CPU in STOP Activate the server, recompile hardware, run CPU in RUN, wait one minute for endpoint advertisement.
Put/Get works but OPC UA does not Put/Get (port 102) is enabled by default and not blocked; OPC UA server disabled Enable the OPC UA server under Properties → OPC UA → Server; on firmware V20, both can coexist but you still need the OPC UA-specific settings.
High latency, drops during heavy reads Subscription queue too small or max monitored items too low Increase OPC UA → Server → Subscription settings: max monitored items per session, publish interval, queue size.

Security Hardening Notes

  • Disable anonymous sessions on production CPUs. Use username/password or, preferably, certificate-based authentication.
  • Disable SecurityPolicy - None after commissioning. Keep at least one of Basic256Sha256, Aes128Sha256RsaOaep, Aes256Sha256RsaPss enabled.
  • Rotate the S7-1500 OPC UA server certificate annually. The self-signed cert can be replaced with one signed by your company CA using the Security → Certificates → Server certificate dialog.
  • Restrict DB access to read-only wherever the application does not need to write. The TIA Portal V20 path for per-DB override is described in the official Managing write and read rights for a complete DB documentation.
  • Run the OPC UA server on a separate VLAN from the office network; restrict 4840/tcp to known client IPs at the switch level.

Migration Checklist From VB6

  1. Inventory the tags that the VB6 application reads/writes. Capture the DB name, tag name, data type, and direction (R/W).
  2. Decide on the replacement technology: VB.NET, C#, or a VBA macro in Excel. Prefer C# for new code.
  3. Activate the OPC UA server on the S7-1500 (TIA Portal V20) and harden the security policies.
  4. Set per-DB access rights in Properties → Attributes → OPC UA.
  5. Build the new client using the Siemens support entry 109737901 pattern or the Excel support entry 109748892 pattern.
  6. Side-by-side run with the VB6 EXE for at least one production cycle. Compare read values bit-for-bit.
  7. Cut over. Keep the VB6 EXE in a frozen image for at least one quarter as a rollback.

Why does the VB6 error happen at compile time and not at run time?

Visual Basic 6.0 imports the type library of every referenced COM or .NET-assembly-COM-visible DLL when the project is opened. The importer inspects each method signature and rejects any parameter or return type that cannot be expressed in OLE Automation. Because ReadValues returns IList<DataValue>, the importer marks the method restricted and the compiler refuses to produce the EXE. The OPC UA server, network, and PLC are never touched.

Can I just enable Put/Get on the S7-1500 and keep my Prodave or Libnodave code in VB6?

Yes, if the application is closed-loop inside the plant network and you accept that Put/Get sends plaintext user data with no authentication or encryption. On firmware V20, Put/Get can be enabled under Properties → Communication → Put/Get. Note that OPC UA offers authentication, encryption, certificate trust, and structured DataBlock access, so it remains the recommended path where modern clients are available.

What is the difference between namespace index 1, 2, and 3 on the S7-1500 OPC UA server?

Index 0 is the OPC UA base namespace. Index 1 holds the server-defined types, index 2 holds the data type system, and index 3 is the default S7-1500 application namespace where the PLC exposes its variables. Most third-party clients, including the Siemens example, use index 3 for all DB tags.

How do I set a DB on the S7-1500 to read-only for OPC UA clients?

Open the DB in the TIA Portal project, choose Properties → Attributes → OPC UA, and set Access to Read. Compile and download the project. A subsequent write attempt from any OPC UA client will return status code Bad_NotWritable (0x803B0000). The complete procedure for TIA Portal V20 is in the official Siemens TIA Portal V20 documentation.

Will the same VB6 code work against a SoftPLC S7-1500 or the S7-1500T?

Yes, the OPC UA server interface is identical between S7-1500, S7-1500T, ET 200SP CPU, and the SoftPLC S7-1500 (PLCSIM Advanced V20). NodeId syntax, security policies, namespace indexes, and status codes are all the same. Configuration of the OPC UA server on the S7-1500T in TIA Portal V20 uses the same menus as on the standard S7-1500.

Back to blog