Build a CSV tag list for OpenOPC from a symbol export

5 min read · Published November 4, 2026

A renamed symbol export gives you tag names and types only. Build the CSV from the server's browsed item IDs: five columns per row, access and refresh set by you, a real CSV writer for the quoting, and a three-row test before the full twelve tags.

Your OpenOPC client can read its tag list from a comma-separated file, but a renamed symbol export will not produce that file. A symbol export gives you the tag name and the declared type. The address column must hold the exact item ID the OPC server resolves, the access column comes from the server's rights plus your write policy, and the refresh rate is a client setting in milliseconds. Build the CSV from a verified mapping: export the symbol table as inventory, browse the server namespace for the addresses, and write the file with a proper CSV writer. The steps below cover a classic OPC Data Access server, which is the protocol OpenOPC speaks.

What each column holds

Five columns per row, and each one has one source:

Column Where it comes from Example
name Your label. Keep it stable so a server path change touches one column. flow_l_min
address The server's item ID, copied exactly as browsed. Plant1.Filling.Line1.Flow_Lmin
type The PLC declaration, checked against what the server returns. REAL
access Server rights and your write policy: r or rw. r
refresh_ms How often your client asks for the value. 1000

Two of the five are yours alone: the name and the refresh rate. Two come from the server: the address and the rights. The type is shared, and it is the one to verify, because server mappings for arrays and strings do not always match the declaration in the project.

Why the symbol name is not the address

In OPC Data Access, a server exposes a namespace of branches and items. Branches exist for browsing; items are what you read and write. The client asks for items by the server's own identifier, and the server maps that string to its data source. Your PLC symbol works as an item ID only where the server exposes that exact string, and many servers put a device name or a path in front of it. So the address column is filled by browsing, and the symbol export is the checklist you browse against.

OpenOPC ships a command line client that does the browse. With a server selected, this prints what sits under a branch, one name per line:

opc -s SERVERNAME -l "Plant1.Filling"

Copy the identifiers character for character into the address column. If your controller serves OPC UA instead of Data Access, the transport setup is a different job, covered in Set up an OPC UA server in CODESYS for HMI clients.

What you need first

  • The symbol or tag export from the engineering software, with names, addresses, declared types and scope.
  • The OPC server installed and reachable, with the client able to open a session.
  • OpenOPC installed where your script will run.
  • One known-good read. Until a single item reads, stop: the fault is the connection, and no CSV fixes a connection.

Build the file

  1. Export the symbol table from the engineering software. Treat it as inventory. Renaming it to .csv changes the extension and nothing else: the columns, the delimiter and the encoding stay what they were.
  2. Browse the server with the command line client and copy the exact item IDs you need, one branch at a time.
  3. Test-read a few items before mapping anything. The read call takes a list of item IDs and returns one tuple of name, value, quality and time per item. Accept an item into the list only when quality comes back Good.
  4. Match each symbol to its item ID. Resolve differences in prefix, path and array shape. Leave anything you cannot read out of the production list.
  5. Set access per row. Default every row to r. Mark rw only the items your application writes, and confirm the server reports them writable. If the client feeds a display and nothing else, keep all rows r; Specifying a read-only process display for a plant covers that whole job.
  6. Set refresh_ms per row. Group tags with the same rate. When you request a group update rate, the server can hand back a revised value, so log what it returns and use that number in your timing.
  7. Write the CSV with a real writer, never with string joins:
import csv
rows = [
    ["name", "item_id", "type", "access", "refresh_ms"],
    ["flow_l_min", "Plant1.Filling.Line1.Flow_Lmin", "REAL", "r", 1000],
]
with open("tags.csv", "w", newline="") as f:
    csv.writer(f).writerows(rows)

RFC 4180 is the CSV convention the writer applies for you: a field containing a comma, a quote or a line break is wrapped in double quotes, and a quote inside a field is doubled. Hand-built rows skip that rule, and the first tag name with a comma shifts every column after it. 8. Import three rows first: one BOOL, one REAL, one writable item. Parse them, resolve them, read them, and where allowed, write them. Then load the full list.

Example: a filling line with twelve tags

A filling line exposes 12 tags through one Data Access server: 9 BOOL status bits for heads, doors and the e-stop chain, 2 REAL analog values for flow in liters per minute and temperature, and 1 INT speed setpoint the operator may change from the client.

Refresh: all 12 tags at 1000 ms. Load arithmetic: 12 items read once per second is 12 reads per second on that group. If flow alone later needs 250 ms, split it into a second group: the 11 slow tags still take 11 reads per second, flow at 250 ms takes 4, and the total is 15 reads per second instead of forcing all 12 to 48. Access arithmetic: 11 rows carry r, 1 row carries rw.

Three rows of the finished file:

name,item_id,type,access,refresh_ms
head_1_status,Plant1.Filling.Line1.Head1_Status,BOOL,r,1000
flow_l_min,Plant1.Filling.Line1.Flow_Lmin,REAL,r,1000
speed_setpoint,Plant1.Filling.Line1.Speed_SP,INT,rw,1000

Check the import

  • Parse the file with the same csv module your client uses. Every row has 5 fields, no empty address, and a number in every refresh_ms cell.
  • Read all 12 tags in one call and inspect every returned tuple. A failed item comes back with its own quality flag, so check each one instead of trusting the batch.
  • Force one source change, such as bumping the setpoint at the PLC, and confirm the timestamp moves and the value follows within one refresh period, which is about 1 s at 1000 ms.
  • Test the write path on a spare item or on the bench. A write to speed_setpoint on a running line changes what the machine does. When you do test the rw row, write back the value you just read so the process ends where it started.

Once the tags read on schedule, decide where the values go: Live factory data display: signage or dashboard walks that choice for the floor.

The decision

  • Client feeds a display only: every row r, every refresh at 1000 ms, one group.
  • Client writes setpoints: mark only those rows rw, and prove the write on the bench before the line runs.
  • One tag needs speed: second group at 250 ms for that tag alone, 1000 ms for the rest.

Recommendation: build the CSV from browsed item IDs in five columns, generate it with a real CSV writer, import three rows before the twelve, keep every row read-only except the tags your application genuinely writes, and start every refresh rate at 1000 ms, splitting a faster tag into its own group only when the process demands it.

Questions

Can I rename my symbol export to .csv and fix the columns by hand?

Renaming changes the file extension and nothing else. Symbol exports choose their own delimiter, column order and header rows, and the next export from the engineering software overwrites your edits. For a dozen tags the mapping is quick; for two hundred it is the difference between a script you rerun and a file you retype.

Does OpenOPC read the CSV file itself?

No. The library moves values over OPC: you pass it item IDs and it returns one tuple of name, value, quality and time per item. Parsing the CSV and turning rows into read or write calls is your script's job. Parse the file once at startup so a malformed row stops the client before it touches the plant.

What refresh rate should each row carry?

Start at 1000 ms for anything a person watches. Counters and totals that change by the hour can sit at 5000 ms. Use 250 ms only for a tag that drives a decision, and put it in its own group so it does not pull the slow tags along. Every halving of the interval doubles the read load.

How do I get a list of every item the server exposes?

Browse from the top. Point the list command at the top branch and it prints the names inside, one per line; run it again on each sub-branch until you reach the items. Save each run to a file and you have a snapshot of the namespace to match your symbol export against.

Planning information, not legal or engineering advice. Rules differ by place: check codes and permits with your local authority and your licensed trades.

Keep reading

Build your signAsk for a quote