Converting Siemens S7 Byte Array to ASCII String in Ignition

David Krause14 min read
HMI / SCADASiemensTutorial / How-to
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

Converting Siemens S7 Byte Array to ASCII String in Ignition

Overview

Siemens S7-300/400/1200/1500 PLCs commonly store part numbers, batch IDs, and serial strings as arrays of single bytes in Data Blocks. Each DBB (Data Block Byte) contains one 8-bit ASCII character code (0-255), and 50 bytes from DB400.DBB10 through DB400.DBB59 may represent an 11-character part number right-aligned with leading null bytes (0x00). Ignition by Inductive Automation can subscribe to individual byte tags through the Siemens driver, but treating them as a 50-tag list of integers is inefficient and clumsy for downstream HMIs, historians, and SQL transaction groups.

This reference covers four field-proven methods to consolidate a Siemens byte array into a single readable string tag:

  1. Gateway timer script using system.tag.readAll() and Python's chr().
  2. Direct DBSTRING type read through the Ignition Siemens driver.
  3. String tag created in the S7 PLC with proper S7 string header.
  4. Transaction group mapping of a calculated expression.

Prerequisites

Item Requirement
Ignition version 7.9.x or 8.0/8.1 (gateway scripting available in all)
Siemens driver module Siemens Driver Module installed and licensed
PLC S7-300/400/1200/1500 with data block access (PUT/GET enabled on S7-1200/1500)
Network ISO-on-TCP (port 102) or S7comm to Ignition gateway reachable
Tag provider Siemens driver configured to a real device, e.g. [Siemens]DB400
Python knowledge Basic Ignition system.tag scripting

Siemens S7 Byte Addressing Fundamentals

Siemens uses absolute byte addressing inside a Data Block:

  • DBB<n> – Data Block Byte at offset n (8-bit unsigned, 0-255 decimal)
  • DBW<n> – Data Block Word (16-bit)
  • DBD<n> – Data Block Double Word (32-bit)
  • DBX<n>.<bit> – Data Block Bit at byte n, bit bit

For the working example, the part number occupies 50 contiguous bytes:

PLC Address Typical Decimal Value Hex ASCII
DB400.DBB10 0 0x00 NUL (pad)
DB400.DBB11 0 0x00 NUL (pad)
DB400.DBB12 0 0x00 NUL (pad)
DB400.DBB13 0 0x00 NUL (pad)
DB400.DBB14 49 0x31 '1'
DB400.DBB15 54 0x36 '6'
DB400.DBB16 50 0x32 '2'
DB400.DBB17 50 0x32 '2'
DB400.DBB18 55 0x37 '7'
DB400.DBB19 51 0x33 '3'
DB400.DBB20 95 0x5F '_'
DB400.DBB21 67 0x43 'C'
DB400.DBB22 69 0x45 'E'
DB400.DBB23 76 0x4C 'L'
DB400.DBB24 76 0x4C 'L'
DB400.DBB25 - DBB59 0 0x00 NUL (unused)

Decimal 49 = ASCII '1', 54 = '6', 50 = '2', 55 = '7', 51 = '3', 95 = '_', 67 = 'C', 69 = 'E', 76 = 'L'. Reading left to right produces the correct string 1622733_CELL when leading nulls are skipped. The 15-OPC-tag approach the original integrator used (one tag per byte) is functional but creates tag-database bloat, increases subscription overhead, and forces HMI expressions to manually concatenate in correct order.

Right-alignment caveat: Siemens TIA Portal STRING data types use a 2-byte header (max length, current length) followed by ASCII bytes. When a programmer manually allocates a STRING[50] and writes the value at the start, characters occupy offsets 0-10 with offsets 11-49 holding 0x00. When a programmer allocates ARRAY[0..49] OF BYTE and writes the part number at the right side, the meaningful bytes are at the high offsets (DBB24 down to DBB14 in the example). Always inspect the data in the watch table to confirm direction before scripting.

ASCII Conversion Reference

Decimal Range Hex Range Meaning
0 - 31 0x00 - 0x1F Control characters (not printable)
32 - 126 0x20 - 0x7E Standard printable ASCII
127 0x7F DEL (not printable)
128 - 255 0x80 - 0xFF Extended ASCII (code-page dependent)

For 7-bit ASCII (typical part-number text) the printable range is 32-126. .NET's ASCIIEncoding.GetString method converts an entire byte array to a String in one call; the same logic in Ignition's Jython environment is replicated using Python's chr() with a printable-range filter, or simply slicing until the first null/control character is encountered.

Ignition Tag Configuration

  1. In the Ignition Designer Tag Browser, browse to the Siemens driver connection (default provider shown as [Siemens] or your custom name).
  2. Create a folder Test/PartNumber.
  3. Inside, create 50 OPC tags of type Integer with one of two addressing schemes:
    • Each tag points to a single byte: DB400.DBB10, DB400.DBB11, ... DB400.DBB59.
    • Or, for less clutter, use the array path syntax: a single Array tag with 50 elements of type Integer, configured as DB400,BYTE,10,50.
  4. Create a destination memory tag PartNo of type String in the same folder. This is where the consolidated result will be written.
  5. Save the project and verify all 50 byte tags read their expected integer values by inspecting them in the Designer or via the Diagnostics tree.
Tag-provider prefix: When using a non-default provider, prefix every path. Example: [MyPlcProvider]Test/PartNumber/PartNo_Char1. Hard-coded [default] paths will fail in production projects that use multiple providers.

Gateway Timer Script Implementation (Method 1)

This is the most flexible approach. It runs on the gateway (not the client) and writes a single string tag that the rest of the project can consume.

Step-by-step

  1. In the Ignition Gateway Web Interface, navigate to Status > Tags to confirm tags are reading.
  2. Go to Config > Scripting > Gateway Timer Scripts.
  3. Click Create New Timer Script, name it PartNumberAssembler.
  4. Set the execution rate. For slowly-changing part numbers, 1000 ms is appropriate. For high-speed changeover lines, 250 ms or 100 ms is acceptable; reads from the same DB are coalesced by the driver.
  5. Paste the following Jython script:
# generate tag list for 50 individual byte tags
tagPath = '[default]Test/PartNumber'
tagList = []
for i in range(10, 60):
    tagList.append(tagPath + '/DBB' + str(i))

# batch-read all 50 byte tags in one call
tagValues = system.tag.readAll(tagList)

# build the output string, stopping at the first null or non-printable byte
stringOut = ''
for tag in tagValues:
    if tag.value == 0 or tag.value < 32 or tag.value > 126:
        break
    stringOut += chr(tag.value)

# write the assembled string to a memory tag
system.tag.write(tagPath + '/PartNo', stringOut)

If the bytes are stored in reverse order (right-aligned, where the last meaningful byte is at the highest offset), reverse the loop direction:

tagList = []
for i in range(15, 0, -1):
    tagList.append(tagPath + '/PartNo_Char' + str(i))

tagValues = system.tag.readAll(tagList)
stringOut = ''.join(chr(t.value) for t in tagValues if 32 <= t.value < 127)
system.tag.write(tagPath + '/PartNo', stringOut)

Why use a gateway timer script?

  • Centralizes logic on the server – every client sees the same assembled string without running their own client timer.
  • Single subscription burst instead of 50 per-client subscriptions.
  • Tag write only when value actually changes (use system.tag.writeBlocking with a previous-value comparison to avoid historian spam).
  • Re-usable in any Ignition version that supports gateway scripting (7.7+).

Optimized version (write only on change)

tagPath = '[default]Test/PartNumber'
tagList = [tagPath + '/PartNo_Char' + str(i) for i in range(1, 16)]

qvs = [system.tag.getConfiguration(tagPath + '/PartNo')]
prev = system.tag.readBlocking([tagPath + '/PartNo'])[0].value

tagValues = system.tag.readBlocking(tagList)
newString = ''.join(chr(t.value) for t in tagValues if 32 <= t.value < 127)

if newString != prev:
    system.tag.writeBlocking([tagPath + '/PartNo'], [newString])

Direct String Read with DBSTRING (Method 2)

For part numbers that are stored in a TIA Portal STRING tag (with the 2-byte length header), the Siemens driver in Ignition can subscribe to the whole string in a single tag, eliminating the script entirely.

PLC-side requirement

The PLC data block must declare a STRING type. In TIA Portal for S7-1200/1500:

"PartNumber" : String[40]; // 40-character STRING, occupies 42 bytes total

S7-300/400 use the same structure: 2 header bytes (max length, current length) followed by up to 254 bytes of ASCII. The string is left-aligned starting at byte 0 of the data area.

Ignition tag configuration

  1. Create a new OPC tag, browse the Siemens device.
  2. Set the data type to String.
  3. Set the address using the driver's string syntax. The format expected by the Ignition Siemens driver is:
DBSTRING<DB>.<Offset>,<Length>

For our example, if the part number string starts at DB400 byte 10 and is 40 characters long:

DBSTRING400.10,40

The driver will automatically parse the 2-byte header, extract the current length, and return the ASCII bytes as a single string value to Ignition. No Python required.

Length precision: The integer after the comma is the maximum string length (matches the S7 STRING[<max>] declaration). The driver reads that many bytes and uses the current-length byte from the header to trim trailing junk. If you set the Ignition length smaller than the PLC's max, trailing characters are clipped. If you set it larger, the driver pads with nulls.

Limitations

  • Only works if the PLC actually stores a STRING with a 2-byte length header. Raw byte arrays without the header return garbage (the driver interprets the first two bytes as length).
  • Older firmware on the S7-300 may not expose STRING DBs through the standard S7comm PUT/GET interface.
  • The DBSTRING syntax is documented in the Siemens driver module reference. The exact spelling and case may vary between Ignition 7.9, 8.0, and 8.1; verify in your version's tag-address picker.

Transaction Group Approach (Method 3)

If the end goal is to push the part number into a SQL database row-by-row, use an Ignition Transaction Group with an expression item that performs the string build.

Configuration

  1. Create a new Transaction Group of type Block or Historical.
  2. Add 50 OPC items mapped to DB400.DBB10 through DB400.DBB59.
  3. Add a calculated item (Expression) named PartNo with type String. Use the Ignition expression function:
concat(
  if({DB400_DBB10}>31&&{DB400_DBB10}<127, chr({DB400_DBB10}), ""),
  if({DB400_DBB11}>31&&{DB400_DBB11}<127, chr({DB400_DBB11}), ""),
  ...
)

For 11-character part numbers this is unwieldy but workable. For 50-character strings use a Gateway Timer Script (Method 1) and have the Transaction Group subscribe to the resulting PartNo memory tag with a trigger on value change.

Triggering the group

Set the trigger to Item Changed on the PartNo tag, with the rate-limit guard to suppress duplicate rows. The original integrator's symptom of "the entry duplicating every part" usually points to one of two issues:

  1. The trigger is set to Value Changed at a fast poll rate and the OPC tag is bouncing between two values due to PLC scan jitter.
  2. There is no Last Value comparison in the trigger; the same part number is being re-written to the SQL table on every poll.

Fix: use a status flag (a separate boolean tag that the PLC sets only after the part number has stabilized) or add a hand-shake bit the PLC toggles when a new part is ready.

Performance Considerations

Method Subscriptions CPU Overhead Latency Best Use Case
50 individual byte tags + script 50 per client Low (timer script) Poll interval Legacy PLC, no STRING type
DBSTRING direct read 1 per client Negligible Driver poll Modern PLC with STRING
Array tag + script 1 per client (50 elements) Low Poll interval Variable-length strings
Transaction Group expression 50 per group Medium (re-evaluates per item) Group rate Direct DB insert with no intermediate tag

Edge Cases and Field Caveats

Leading null padding

Most S7 programmers either:

  • Right-align a short string in a fixed-length buffer (lots of leading 0x00), or
  • Left-align and pad with 0x00 (no leading nulls, trailing nulls only).

The script must match. For right-aligned data, reverse the byte order before scanning. For left-aligned data, scan forward and break on first null.

Non-ASCII characters (0x80-0xFF)

Extended ASCII (Latin-1, Windows-1252) bytes will pass through Python's chr() but render as question marks in most Windows-based HMI clients. For Unicode strings (UTF-8, UTF-16) the S7 STRING type is unsuitable; use a DB of WORDs and convert with unichr() (Jython 2.7) or chr() with explicit str.decode('utf-8') in modern Ignition Python 3 modes.

Byte 127 (DEL)

The filter 32 <= value < 127 excludes the DEL control character. If your part numbers legitimately contain 0x7F, broaden the filter to value > 31.

Subscription timing

On Ignition gateway startup, the first system.tag.readAll() may return None or Bad_Quality for tags that have not completed their initial subscription. Wrap the read in a quality check:

qualities = [t.quality.isGood() for t in tagValues]
if not all(qualities):
    return   # skip this cycle, wait for next timer tick

Python 2 vs Python 3 in Ignition

Ignition 8.0+ runs Jython 2.7. Ignition 8.1+ supports Python 3 syntax in script consoles but gateway scripting remains on Jython 2.7 for compatibility. The chr() function works identically in both for the 0-127 range; the difference matters only for code pages 128+.

Troubleshooting Matrix

Symptom Likely Cause Fix
Tag values all 0 OPC subscription failed, no PLC connection Check Siemens device status in Gateway, verify rack/slot, verify PUT/GET enabled (S7-1200/1500)
Tag values 255 (0xFF) Byte mapped to wrong DB or wrong offset Cross-check with TIA Portal watch table on the actual address
String reversed ("LLEC_372261") Right-aligned data, script reads forward Reverse loop: range(15, 0, -1)
String contains junk characters Non-string data at adjacent offsets Verify DB layout, check that no other data shares DBB10-DBB59
DBSTRING returns first 2 chars only Length parameter set to 1 instead of 40 Set DBSTRING400.10,40 – matches PLC STRING[40]
DBSTRING returns garbage on raw byte array Array has no STRING header Convert PLC variable to STRING[50] type, or use Method 1
Script writes to wrong provider Hard-coded [default] prefix Use the actual provider name from the Tag Browser
Duplicate SQL rows Trigger fires on every poll Add handshake bit or trigger on explicit PLC flag
Permission denied on write Memory tag is read-only or security zone restricted Check tag security settings in Gateway
NullPointerException in log One of the byte tags does not exist Verify all 50 tags exist; use try/except wrapper

Verification Procedure

  1. In the Designer, open a new window with a Label component bound to the PartNo memory tag.
  2. Trigger a part-number change in the PLC (use TIA Portal watch table to write a known value, e.g. 1622733_CELL into DBB14-DBB24).
  3. Confirm the Label updates within one poll cycle (typically 1 second for a 1000 ms timer script).
  4. Open the Ignition Gateway console and tail the script log. Add print stringOut at the end of the script to log every successful build.
  5. For the DBSTRING method, verify with a Label bound directly to the new DBSTRING tag – if it shows the part number, the script is unnecessary.
  6. For the Transaction Group method, run the group manually once and inspect the SQL table to confirm a single row per part number, not duplicates.

Security and Access Notes

  • Gateway timer scripts run with full gateway-level permissions. Anyone with edit access to the script can read/write any tag in any provider.
  • Memory tags written by scripts inherit the security zone of the script, not the client. Configure zone restrictions on the destination PartNo tag if certain clients should not see the part number.
  • S7-1200/1500 require explicit PUT/GET permission in TIA Portal under Properties > Protection > Connection mechanisms. Without it, the Siemens driver cannot read any DB.

Reference: Siemens Driver String Syntax Across Ignition Versions

Ignition Version String Address Format Notes
7.7 - 7.9 DBSTRING<DB>.<Offset>,<Length> Direct tag picker support
8.0 DBSTRING<DB>.<Offset>,<Length> Same syntax, upgraded driver internals
8.1+ DBSTRING<DB>.<Offset>,<Length> Same syntax; verify against the version's Siemens driver manual

Reference: Standard Siemens S7 STRING Memory Layout

Offset Size (bytes) Content
+0 1 Maximum string length (e.g. 40)
+1 1 Current string length (e.g. 11)
+2 to +(2+max) max ASCII characters, left-aligned, padded with 0x00

Total memory occupied by a STRING[N] is N+2 bytes. A STRING[40] at DB400.DBB10 occupies DBB10 (max) through DBB51 (last char), totaling 42 bytes. The Ignition Siemens driver's DBSTRING400.10,40 tells it to read 42 bytes from DBB10 and to use the first two as the header.

Related Ignition Modules

  • Tag Historian – record the assembled PartNo tag over time, triggered on value change.
  • SQL Bridge – push part numbers into MES or ERP via Transaction Groups.
  • Alarm Pipeline – raise an alarm if PartNo contains non-printable characters (indicates PLC program error).
  • Perspective – display the part number in a mobile-friendly view with a binding directly to the memory tag.

Frequently Asked Questions

What is the difference between DBB and DBW in Siemens S7?

DBB is Data Block Byte (8-bit, 0-255), DBW is Data Block Word (16-bit, 0-65535), and DBD is Data Block Double Word (32-bit). A single ASCII character fits in one DBB. Two adjacent characters can be read together as a DBW only if you handle the byte-order (big-endian on S7).

Can Ignition read a Siemens STRING tag directly without a script?

Yes. Use the DBSTRING syntax in the Siemens driver: DBSTRING<DBNumber>.<ByteOffset>,<MaxLength>. The driver parses the 2-byte S7 string header and returns the current string value to Ignition. This only works if the PLC actually stores a STRING type, not a raw byte array.

How do I handle part numbers stored right-aligned with leading null bytes?

Either reverse the byte loop in the script (read from highest offset to lowest) or filter null bytes from the front in post-processing. The example script using range(15, 0, -1) shows the reverse-read pattern for 15 characters of part-number data.

Why does my Transaction Group insert duplicate rows for the same part number?

The trigger is firing on every poll because the PLC is repeatedly writing the same value, or the trigger is set to evaluate at a fast rate without a value-change check. Add a PLC handshake bit that toggles only when a new part is ready, or add a comparison in the trigger to ignore identical consecutive values.

What ASCII range should I accept as printable?

Standard 7-bit printable ASCII is 32-126, which covers all letters, digits, and common punctuation. If your part numbers include 0x7F (DEL) you should broaden the upper bound to 127, and for extended characters in code pages 128-255 you need to know which code page the PLC uses and decode accordingly in Jython.

Does DBSTRING work on S7-300 and S7-400 the same as S7-1200/1500?

The S7 STRING data type is identical across all four families – 2-byte header (max length, current length) followed by ASCII bytes. The driver syntax is the same. Older S7-300 CPUs with firmware below V2.x may not support STRING data blocks over the standard S7comm interface, in which case the gateway script method is the only option.

Back to blog