Skip to content

feat: Add S7Comm server plugin for Siemens S7 protocol communication#72

Merged
thiagoralves merged 12 commits into
developmentfrom
feature/s7comm-plugin
Jan 13, 2026
Merged

feat: Add S7Comm server plugin for Siemens S7 protocol communication#72
thiagoralves merged 12 commits into
developmentfrom
feature/s7comm-plugin

Conversation

@thiagoralves

Copy link
Copy Markdown
Contributor

Summary

This PR adds a native S7Comm server plugin that enables Siemens S7 protocol communication with OpenPLC. It allows S7-compatible HMIs, SCADA systems, and other clients to read and write OpenPLC I/O buffers using the industry-standard S7 protocol.

Key Features

  • S7 Server Implementation: Full Snap7 library integration providing S7-300/400 compatible server
  • JSON Configuration: Flexible configuration via JSON file for data blocks, system areas, and server settings
  • Double-Buffered Sync: Thread-safe buffer synchronization with shadow buffers to minimize mutex contention
  • Bidirectional Data Flow:
    • cycle_start: Copy S7 buffer → OpenPLC (S7 client writes visible to PLC)
    • cycle_end: Copy OpenPLC buffer → S7 (PLC outputs visible to S7 clients)
  • Multiple Data Types: Support for bool, int, dint, lint inputs/outputs/memory
  • Configurable Data Blocks: Map S7 DBs to OpenPLC buffer locations

Files Added

  • core/src/drivers/plugins/native/s7comm/ - Complete plugin implementation
    • s7comm_plugin.cpp/h - Main plugin with buffer sync logic
    • s7comm_config.c/h - JSON configuration parser
    • snap7/ - Snap7 library sources (embedded for portability)
    • cjson/ - cJSON library for configuration parsing
    • docs/ - Implementation plan, JSON schema, user guide
  • install.sh - Build system for native plugins
  • plugins_default.conf - Default plugin registration

Configuration Example

{
  "server": {
    "enabled": true,
    "port": 102,
    "max_clients": 32
  },
  "data_blocks": [
    {
      "db_number": 1,
      "size_bytes": 128,
      "mapping": {
        "type": "bool_output",
        "start_buffer": 0
      }
    }
  ]
}

Testing

  • Verified with Python Snap7 client connecting to runtime at 192.168.1.127
  • Confirmed PLC outputs (%QX0.1, %QX0.2, %QX0.5) visible and oscillating correctly via S7 protocol
  • Tested bidirectional buffer sync strategy

Test Plan

  • Plugin builds successfully on Linux
  • S7 server starts and accepts connections on port 102
  • S7 clients can read PLC outputs via configured data blocks
  • PLC program outputs are correctly synchronized to S7 buffer
  • Test with commercial HMI/SCADA software
  • Test write operations from S7 client to PLC inputs
  • Performance testing under load

Related

  • OpenPLC Editor S7Comm configuration UI (separate PR)
  • Future: Journal buffer architecture for multi-plugin synchronization

🤖 Generated with Claude Code

thiagoralves and others added 12 commits January 12, 2026 17:47
- Add comprehensive implementation plan covering:
  - Analysis of OpenPLC v3 S7Comm/Snap7 implementation
  - Snap7 library API documentation
  - JSON configuration schema design
  - Plugin structure and data structures
  - 7-phase implementation roadmap
  - Testing strategy

- Add user guide with:
  - Configuration reference
  - Example configurations
  - S7 address mapping guide
  - Client connection examples
  - Troubleshooting guide

- Add default s7comm_config.json with standard mappings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 1 implementation of S7Comm plugin for OpenPLC Runtime v4:

- Include Snap7 library source code (v1.4.3) for self-contained build
- Add plugin source files (s7comm_plugin.cpp/h)
- Add CMakeLists.txt for standalone plugin compilation
- Implement plugin lifecycle: init, start_loop, stop_loop, cleanup
- Implement cycle hooks for buffer synchronization
- Register S7 system areas (PE, PA, MK) and data blocks (DB1, DB2, DB10, DB20, DB100, DB200)
- Handle big-endian conversion for S7 protocol compatibility
- Add event logging for client connections

Data block mapping (Phase 1 - hardcoded):
- DB1: bool_input (%IX)
- DB2: bool_output (%QX)
- DB10: int_input (%IW)
- DB20: int_output (%QW)
- DB100: int_memory (%MW)
- DB200: dint_memory (%MD)

Note: JSON configuration support will be added in Phase 2.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add build_native_plugins() function that automatically scans for native
plugins with CMakeLists.txt and builds them during installation. This
enables Docker images to include pre-built native plugins like s7comm.

The function:
- Scans core/src/drivers/plugins/native/ for CMakeLists.txt files
- Creates isolated build directories for each plugin
- Passes OPENPLC_ROOT to cmake for proper header resolution
- Copies built .so files to build/plugins/
- Provides summary of successful/failed builds

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add full JSON configuration support using embedded cJSON library:

Configuration features:
- Server settings (port, max_clients, timeouts, PDU size)
- PLC identity (name, module_type, serial_number, etc.)
- Dynamic data block allocation with configurable mappings
- System areas (PE, PA, MK) with configurable sizes
- Logging options (connections, data_access, errors)

Implementation changes:
- Add cJSON library (v1.7.18, MIT license) for JSON parsing
- Add s7comm_config.h with configuration structures
- Add s7comm_config.c with parser and validation
- Refactor s7comm_plugin.cpp to use configuration:
  - Dynamic memory allocation for data blocks
  - All server parameters from config
  - Support for all buffer types (bool, int, dint, lint)
  - 64-bit (LINT) support with swap64 endianness conversion
- Update CMakeLists.txt to include new source files

The plugin now reads s7comm_config.json at startup and dynamically
allocates and registers S7 data areas based on configuration.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Refactor buffer synchronization to use double-buffering pattern:
- Add shadow buffers for all S7 areas (system areas + data blocks)
- Add S7 mutex (pthread_mutex_t) to protect S7 buffers during sync
- Implement three-step sync in cycle_end:
  1. Lock mutex, copy S7 -> shadow (capture client writes)
  2. Sync shadow <-> OpenPLC (slow part, no S7 mutex held)
  3. Lock mutex, copy shadow -> S7 (publish new values)
- Remove sync from cycle_start (now no-op)

This allows S7 clients to read/write asynchronously from the main
PLC cycle with minimal mutex contention.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use Srv_GetStatus to check client count at the start of cycle_end.
If no clients are connected, return early without any mutex operations
or buffer copies, reducing overhead when S7 server is idle.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add JSON_SCHEMA.md with complete configuration reference for Editor
- Document all fields, types, constraints, and default values
- Add Editor UI recommendations and validation rules
- Update USER_GUIDE.md with double-buffering architecture section
- Add data flow diagram and related documentation links

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix Srv_GetStatus: use references (int&) not pointers (int*)
- Fix CMakeLists.txt: apply -Wno-class-memaccess only to C++ files
  using COMPILE_LANGUAGE generator expression

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add S7Comm native plugin entry to plugins.conf and plugins_default.conf
so the runtime will recognize and load the S7Comm server when a config
file is provided by the editor.

Plugin configuration format:
- name: s7comm
- path: ./core/src/drivers/plugins/native/s7comm/libs7comm_plugin.so
- enabled: 0 (disabled by default, enabled when config file is present)
- type: 1 (native plugin)
- config_path: ./core/src/drivers/plugins/native/s7comm/s7comm_config.json

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update path to match where the installer places the built library:
core/src/drivers/plugins/native/s7comm/build/plugins/libs7comm_plugin.so

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The Srv_GetStatus client count check was incorrectly returning 0
even when clients were connected, causing buffer sync to be skipped.
Remove this optimization to ensure buffer sync always happens.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ication

Change the sync strategy to copy ENTIRE buffers in both directions:
- cycle_start: Copy ENTIRE S7 buffer -> OpenPLC buffer (all types)
- cycle_end: Copy ENTIRE OpenPLC buffer -> S7 buffer (all types)

This allows S7 clients to write to any location (inputs, outputs, memory).
Values actively driven by the PLC program or I/O drivers will naturally
overwrite S7 writes during the scan cycle, but S7 writes to other
locations (e.g., outputs used only as contacts) will persist.

Fixes issue where PLC outputs were being overwritten with zeros from
the S7 buffer.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@thiagoralves thiagoralves requested a review from Copilot January 13, 2026 18:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds native S7Comm server functionality to OpenPLC Runtime, enabling Siemens S7 protocol communication with compatible HMI and SCADA systems. The implementation integrates the Snap7 library and provides bidirectional synchronization between S7 data areas and OpenPLC I/O buffers through a plugin architecture.

Changes:

  • Added complete S7Comm plugin implementation with embedded Snap7 library
  • Integrated native plugin build system into install.sh
  • Registered s7comm plugin in default configuration files

Reviewed changes

Copilot reviewed 42 out of 44 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
plugins_default.conf Added s7comm plugin registration entry
plugins.conf Added s7comm plugin registration entry
install.sh Added build_native_plugins function to compile CMake-based plugins
core/src/drivers/plugins/native/s7comm/snap7/sys/*.h Snap7 threading and platform abstraction headers
core/src/drivers/plugins/native/s7comm/snap7/sys/*.cpp Snap7 threading, utilities, socket, and TCP server implementations
core/src/drivers/plugins/native/s7comm/snap7/lib/*.h Snap7 library main interface headers
core/src/drivers/plugins/native/s7comm/snap7/lib/*.cpp Snap7 library main implementation
core/src/drivers/plugins/native/s7comm/snap7/core/*.h S7 protocol type definitions and class interfaces
core/src/drivers/plugins/native/s7comm/snap7/core/*.cpp S7 protocol implementations
core/src/drivers/plugins/native/s7comm/s7comm_plugin.h Plugin interface header with lifecycle function declarations

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

TSnapThread::TSnapThread()
{
Started = false;
Closed=false;

Copilot AI Jan 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent spacing around assignment operator. Should be 'Closed = false;' to match the style on line 74.

Suggested change
Closed=false;
Closed = false;

Copilot uses AI. Check for mistakes.
#else
struct timespec ts;
ts.tv_sec = (time_t)(Delay_ms / 1000);
ts.tv_nsec =(long)((Delay_ms - ts.tv_sec) * 1000000);

Copilot AI Jan 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space after assignment operator. Should be 'ts.tv_nsec = (long)' for consistency.

Suggested change
ts.tv_nsec =(long)((Delay_ms - ts.tv_sec) * 1000000);
ts.tv_nsec = (long)((Delay_ms - ts.tv_sec) * 1000000);

Copilot uses AI. Check for mistakes.
case evcClientTerminated : strcat(S,"Client terminated");break;
case evcClientsDropped:
strcat(S, IntToString(Event.EvtParam1, Buf));
strcat(S, " clients have been dropped bacause unresponsive");

Copilot AI Jan 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'bacause' to 'because'.

Suggested change
strcat(S, " clients have been dropped bacause unresponsive");
strcat(S, " clients have been dropped because unresponsive");

Copilot uses AI. Check for mistakes.
@thiagoralves thiagoralves merged commit 8edf9c0 into development Jan 13, 2026
2 checks passed
@thiagoralves thiagoralves deleted the feature/s7comm-plugin branch January 13, 2026 20:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants