Installing pip and pymodbus on Siemens IoT2020: Complete Guide

David Krause11 min read
Industrial NetworkingSiemensTutorial / 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

Overview

The Siemens SIMATIC IoT2020 (part of the SIMATIC IOT2000 family) ships with a Yocto Linux image that bundles Python 2.7 for gateway scripting, but pip and most pure-Python third-party packages are not pre-installed in the root filesystem. When developers try to bootstrap pip with the upstream get-pip.py script, the installer crashes with ImportError: No module named getpass because the image's Python build was stripped of the getpass module — a dependency of pip itself. The same missing module then blocks any further pip install of pymodbus, flask, or requests.

This article documents the exact root cause of the failure, the verified recovery procedure (restoring getpass.py from the CPython 2.7.3 source tree), the correct sequence to bootstrap pip, and the field-tested steps to install pymodbus for Modbus TCP and Modbus RTU communication with Siemens S7-1200 / S7-1500 controllers and third-party Modbus devices.

Affected Hardware and Image Versions

Device Order Number (MLFB) CPU Stock Image Python Affected?
SIMATIC IoT2020 6ES7647-0AA00-1YA2 Intel Quark x1020 (x86) iot2000-example-image v2.1.3 / v2.1.4 2.7.3 (stripped) Yes
SIMATIC IoT2040 6ES7647-0KA00-1YA2 Intel Quark x1020 (x86) iot2000-example-image v2.1.3 / v2.1.4 2.7.3 (stripped) Yes
SIMATIC IoT2050 (Basic) 6ES7647-0BA00-1YA2 ARM Cortex-A53 Example Image V1.2.0+ (Debian-based) 3.5 / 3.7 No
SIMATIC IoT2050 (Advanced) 6ES7647-0BA01-1YA2 ARM Cortex-A53 Example Image V1.2.0+ (Debian-based) 3.5 / 3.7 No

The getpass defect is specific to the legacy x86 iot2000-example-image built with Poky/Yocto and the python-2.7.3 recipe. The newer ARM-based IoT2050 ships a complete Debian rootfs and is not affected.

Note: Always confirm your image build by running cat /etc/version and python --version on the gateway console before applying the procedure below. The fix is only required on the legacy v2.1.x example image.

Prerequisites

  • SIMATIC IoT2020 / IoT2040 with the example image booted from microSD.
  • Serial console (115200 8N1 on /dev/ttyS0) or SSH access (root, default password not set on the example image — first-boot autologin).
  • The gateway connected to a network with outbound HTTPS to pypi.org and files.pythonhosted.org.
  • ~80 MB free space on the rootfs or a USB stick mounted at /mnt/usb.
  • Internet access on your engineering workstation to fetch the upstream getpass.py source from the CPython 2.7.3 reference tree.

Root Cause: Why get-pip.py Fails with No module named getpass

The get-pip.py bootstrap script first runs pip.zip from a temporary directory. To confirm Python is functional, the bundled pip package imports pip.vcs.mercurial, which transitively imports pip.download, which in turn imports getpass:

Traceback (most recent call last):
  File "get-pip.py", line 20061, in <module>
    main()
  File "get-pip.py", line 194, in main
    bootstrap(tmpdir=tmpdir)
  File "get-pip.py", line 82, in bootstrap
    import pip
  File "/tmp/tmpSfzewL/pip.zip/pip/__init__.py", line 28, in <module>
  File "/tmp/tmpSfzewL/pip.zip/pip/vcs/mercurial.py", line 9, in <module>
  File "/tmp/tmpSfzewL/pip.zip/pip/download.py", line 5, in <module>
ImportError: No module named getpass

The Yocto recipe for the example image used the python-2.7.3 package and stripped non-essential modules to fit the 8 MB internal flash. getpass, which depends on the optional termios extension, was excluded. pip 1.5+ requires getpass.getpass() for interactive credential prompts and silently fails if the module is missing.

Engineering note: The same getpass defect will break any tool that calls getpass.getpass() interactively, including easy_install when a PyPI index prompts for credentials. Restoration of getpass is therefore a prerequisite for every other pip / setuptools workflow on the legacy image.

Step 1 — Restore getpass.py to the Python Standard Library

  1. From your engineering workstation, open getpass.py on the Enthought CPython 2.7.3 mirror and save the file as getpass.py (UTF-8, LF line endings).
  2. Transfer the file to the gateway. With SSH enabled:
    scp getpass.py [email protected]:/tmp/getpass.py
  3. On the gateway console, move the file into the standard library directory used by the example image:
    mv /tmp/getpass.py /usr/lib/python2.7/getpass.py
    chmod 644 /usr/lib/python2.7/getpass.py
    chown root:root /usr/lib/python2.7/getpass.py
  4. Verify the module loads:
    python -c "import getpass; print getpass.getuser()"
    root

    If root is returned, the module is correctly registered and the bootstrap path is unblocked.

Step 2 — Bootstrap pip on the IoT2020

With getpass restored, either the upstream bootstrap script or the pre-shipped easy_install will work. The easy_install route is preferred on the legacy image because it avoids the SSL/TLS version mismatch that the example image's openssl 0.9.8 has with PyPI's TLS 1.2 endpoints.

Option A — pip via easy_install (recommended on legacy image)

  1. Confirm setuptools is present:
    easy_install --version
    setuptools 0.6c11
  2. Install pip:
    easy_install pip
  3. Verify the pip shim resolves against the standard library (not a vendored copy):
    which pip
    pip --version
    pip 1.5.6 from /usr/lib/python2.7/site-packages/pip (python 2.7)

Option B — pip via get-pip.py (Debian-style images)

On the IoT2050 or any image with full Python 3 and a current OpenSSL, the Python Packaging User Guide procedure works directly:

  1. Download the bootstrap script:
    curl -sS https://bootstrap.pypa.io/pip/3.5/get-pip.py -o /tmp/get-pip.py
  2. Run it as the unprivileged iot user:
    python3 /tmp/get-pip.py --user
  3. Confirm:
    ~/.local/bin/pip --version
    pip 23.x from /home/iot/.local/lib/python3.5/site-packages/pip (python 3.5)
Note: Do not run pip as root on the IoT2050 — the example image ships an unprivileged iot account and uses a read-only rootfs with an overlay. Use --user installs and add ~/.local/bin to PATH.

Step 3 — Install pymodbus

pymodbus is a pure-Python implementation of the Modbus protocol maintained by the pymodbus-dev project. It supports Modbus TCP (port 502), Modbus RTU over serial, and Modbus ASCII. Install the release that matches the image's Python version:

Image / Python Recommended pymodbus Install Command
IoT2020 / IoT2040 — Python 2.7.3 pymodbus 1.5.2 (last 2.x release) pip install pymodbus==1.5.2
IoT2050 — Python 3.5 / 3.7 pymodbus 3.5.x pip3 install --user pymodbus
Any — serial RTU Add pyserial for /dev/ttyS0 pip install pyserial
  1. Install pymodbus (and pyserial for RTU):
    pip install pymodbus==1.5.2 pyserial
  2. Confirm installation:
    python -c "import pymodbus; print pymodbus.__version__"
    1.5.2
  3. Confirm pyserial is wired to the on-board UART:
    python -c "import serial; ser = serial.Serial('/dev/ttyS0', 9600, 8, 'E', 1); print ser.name"
    /dev/ttyS0
Engineering note: On the IoT2020 / IoT2040 the RS232/485 Arduino shield routes UART0 to /dev/ttyS0. The example image enables it by default. If you have not installed the shield, serial.Serial will still open the port but no electrical signals will reach the field device — verify the shield is seated and the termios line discipline is loaded.

Step 4 — Verify the Installation

Run the four checks below on the gateway console. All four must pass before you move to application code.

  1. Python and pip resolution
    python --version && pip --version
    Python 2.7.3
    pip 1.5.6 from /usr/lib/python2.7/site-packages/pip (python 2.7)
  2. getpass restored
    python -c "import getpass; print getpass.getuser()"
    root
  3. pymodbus import
    python -c "from pymodbus.client.sync import ModbusTcpClient; print 'tcp ok'"
    tcp ok
  4. Network reachability to PLC
    ping -c 2 192.168.200.10

    Replace the address with the S7-1200 PROFINET interface IP. The default TIA Portal project assigns 192.168.0.1; on the Siemens reference LoRaWAN project that ships with the example image, the PLC is at 192.168.200.10.

Using pymodbus — Modbus TCP Client Example

The script below polls four holding registers (address 0–3) from a Siemens S7-1200 configured as a Modbus TCP server. The PLC must have the MB_SERVER instruction (S7-1200 firmware ≥ V4.0) or the Modbus TCP library compiled into the user program. Save the file as /root/modbus_tcp_poll.py and run it with python modbus_tcp_poll.py.

#!/usr/bin/env python
# Modbus TCP poll against a Siemens S7-1200 / S7-1500
from pymodbus.client.sync import ModbusTcpClient
from pymodbus.exceptions import ConnectionException
import time

PLC_IP   = "192.168.200.10"
PLC_PORT = 502
UNIT_ID  = 1   # MB_UNIT for S7-1200, default 1

client = ModbusTcpClient(PLC_IP, port=PLC_PORT, timeout=3)
if not client.connect():
    raise SystemExit("Cannot reach PLC at %s:%d" % (PLC_IP, PLC_PORT))

try:
    while True:
        rr = client.read_holding_registers(address=0, count=4, unit=UNIT_ID)
        if rr.isError():
            print "Modbus error:", rr
        else:
            print "Holding [0..3] =", rr.registers
        time.sleep(1.0)
finally:
    client.close()

Expected output on a healthy link:

Holding [0..3] = [0, 0, 0, 0]
Holding [0..3] = [10, 20, 30, 40]

Using pymodbus — Modbus RTU over /dev/ttyS0

For Modbus RTU on the Arduino shield's RS485 transceiver, use the synchronous serial client. The shield's half-duplex direction control is on D3; pymodbus drives it automatically when serial.Serial opens /dev/ttyS0.

#!/usr/bin/env python
# Modbus RTU master over IoT2020 Arduino shield RS485
from pymodbus.client.sync import ModbusSerialClient

client = ModbusSerialClient(
    method='rtu',
    port='/dev/ttyS0',
    baudrate=19200,
    bytesize=8,
    parity='E',
    stopbits=1,
    timeout=1
)

if not client.connect():
    raise SystemExit("Cannot open /dev/ttyS0")

rr = client.read_input_registers(address=0, count=8, unit=1)
if rr.isError():
    print "RTU error:", rr
else:
    print "Input [0..7] =", rr.registers

client.close()
Parameter Value Notes
port /dev/ttyS0 IoT2020 / IoT2040 UART0 routed through Arduino shield
baudrate 9600 / 19200 Match the slave device; S7-1200 CP 1242-7 default 19200
parity 'E' (even) Most Siemens Modbus slaves default to 8E1
stopbits 1 Required for Modbus RTU
timeout 1.0 s Raise to 3 s for 9600 baud over long runs

Connecting to a Siemens S7-1200 / S7-1500

The S7-1200 became a Modbus TCP server natively starting with firmware V4.0 (2014). The configuration required in TIA Portal:

  1. Add the MODBUS TCP instruction library to the user program (Instructions → Communication → MODBUS TCP). The relevant blocks are MB_SERVER for TCP and the MB_CLIENT for outbound polling — but the IoT2020 acts as the client, so only the server side must be configured.
  2. Set the CONNECT parameter to the PLC's IP and port 502.
  3. Bind the data block MB_HOLD_REG to a DB of at least 4 words; pymodbus reads Holding[0..3] in the example above.
  4. Set the access point MB_UNIT parameter to 1 (default).
  5. Compile and download. The PLC's PROFINET interface accepts inbound TCP/502 connections with no firewall rule on the CPU; if a CP is used (e.g., CP 1243-7 LTE), verify the security settings in the CP's WebUI allow inbound TCP/502 from the IoT2020 IP.
Security note: The S7-1200 Modbus TCP server does not authenticate clients. Restrict access at the network layer — place the IoT2020 in the same VLAN/subnet as the PLC and use the built-in PROFINET security or a stateful firewall on the IoT2050 (nftables) to block 502/TCP from any other host.

Troubleshooting Matrix

Symptom Root Cause Fix
ImportError: No module named getpass during get-pip.py getpass stripped from python-2.7.3 image Copy getpass.py from CPython 2.7.3 to /usr/lib/python2.7/
ImportError: No module named termios after restoring getpass termios extension also stripped Recompile Python with INTERNAL_PYTHON=python2.7-dev in Yocto or upgrade to the IoT2050 image
pip: command not found after easy_install pip /usr/bin not in PATH for non-root shells export PATH=$PATH:/usr/bin in /etc/profile or use absolute path /usr/bin/pip
ssl.SSLError: [Errno 1] _ssl.c:504: error:1407742E when pip contacts PyPI openssl 0.9.8 in legacy image lacks TLS 1.2 Use easy_install which uses HTTP/1.0, or set --index-url=http://pypi.python.org/simple
pymodbus install succeeds but import pymodbus raises ImportError: No module named serial pyserial not installed pip install pyserial
Modbus TCP ConnectionException PLC firewall or wrong port Verify nc -zv 192.168.200.10 502; check S7-1200 CP firewall rules
Modbus RTU NoResponseReceived Baud / parity mismatch or RS485 direction pin Match PLC MB_CONFIG settings; verify Arduino shield D3 is jumpered to DE/RE
OSError: [Errno 13] Permission denied: '/dev/ttyS0' User not in dialout group usermod -aG dialout iot on IoT2050; or run as root on legacy image

Operational Tips and Best Practices

  • Pin the package version. The legacy image's pip 1.5 will resolve the latest pymodbus, which dropped Python 2 support after 1.5.2. Pin with pip install pymodbus==1.5.2 on IoT2020/2040.
  • Use a virtualenv if space allows. easy_install virtualenv && virtualenv /opt/venv isolates third-party packages from the read-only /usr.
  • Snapshot the SD card. After a clean install of pip, pymodbus, pyserial, and your application, dd if=/dev/mmcblk0 of=/mnt/usb/iot2020-good.img to capture a known-good image.
  • Migrate to the IoT2050 for new projects. Debian-based images, Python 3, and a maintained pip 23+ eliminate the entire getpass workaround and give you pymodbus 3.5+ with async support.

Why does get-pip.py fail with No module named getpass on the IoT2020?

The legacy iot2000-example-image v2.1.x ships a stripped Python 2.7.3 built by Yocto that omits non-essential modules. pip imports getpass during its own bootstrap to handle credential prompts, and the import fails before any package is installed. Restore getpass.py from the CPython 2.7.3 reference into /usr/lib/python2.7/ to unblock the bootstrap.

Can I install pymodbus with pip on the SIMATIC IoT2020?

Yes. After restoring getpass and bootstrapping pip (typically via easy_install pip), run pip install pymodbus==1.5.2 pyserial. Pin to 1.5.2 because later releases of pymodbus require Python 3. For Modbus RTU, add pyserial so the on-board /dev/ttyS0 UART works.

Which Python version is on the IoT2020 vs the IoT2050?

The IoT2020 and IoT2040 example images ship Python 2.7.3. The IoT2050 example image (V1.2.0 and later) is Debian-based and ships Python 3.5 (basic) or 3.7 (advanced). The getpass workaround is only required on the legacy v2.1.x x86 image; the IoT2050 can use the standard pip bootstrap directly.

How do I connect pymodbus to a Siemens S7-1200?

Configure MB_SERVER in the S7-1200 user program (TIA Portal → Instructions → Communication → Modbus TCP). Set the CONNECT IP to the PLC's PROFINET address, port 502, and unit ID 1. From the IoT2020 use ModbusTcpClient(PLC_IP, port=502) with read_holding_registers(address=0, count=4, unit=1). The S7-1200 requires firmware V4.0 or later to act as a Modbus TCP server.

What UART should I use for Modbus RTU on the IoT2020?

Use /dev/ttyS0 (UART0) with the Siemens-approved Arduino RS232/485 shield. Configure 8E1 at 9600 or 19200 baud to match the slave device. The shield's DE/RE direction pin is on D3 and is driven automatically by pymodbus once serial.Serial('/dev/ttyS0', ...) opens the port.

Why does pip still fail after I copied getpass.py?

The most common cause on the legacy image is that the underlying termios extension is also stripped, and getpass itself imports termios on Unix. Recompiling Python against the Yocto SDK, or upgrading to the IoT2050 Debian image, are the two long-term fixes. As a workaround, run the bootstrap with stty rows 0 cols 0 first to force a non-tty environment, or use easy_install pip which does not load the full getpass path.

Back to blog