Skip to content

Python blocks fix; Modbus echo response fix#92

Merged
thiagoralves merged 8 commits into
mainfrom
development
Jan 28, 2026
Merged

Python blocks fix; Modbus echo response fix#92
thiagoralves merged 8 commits into
mainfrom
development

Conversation

@thiagoralves
Copy link
Copy Markdown
Contributor

This pull request introduces major improvements to the Python function block integration and Modbus slave behavior in OpenPLC. The changes focus on robust resource management for Python blocks (ensuring subprocesses, threads, and shared memory are cleaned up safely), and on compliance with the Modbus protocol for certain function codes. Additionally, new error handling and logging mechanisms are introduced to improve maintainability and debuggability.

Python Function Block Resource Management

  • Added a global tracking system (python_blocks) for all Python function blocks, including their subprocess PIDs, runner threads, shared memory, and script files, with a mutex for thread safety. This enables coordinated cleanup and prevents resource leaks.
  • Implemented python_blocks_cleanup(), a function that gracefully terminates all Python subprocesses, joins runner threads, unmaps/unlinks shared memory, and deletes script files, preventing crashes during plugin unload. This is now called before unloading the plugin shared library. [1] [2] [3]
  • Python subprocesses are now launched via fork() and execlp(), with their stdout/stderr piped to a dedicated runner thread for logging, replacing the earlier popen() approach. This allows for proper process control and cleanup. [1] [2]
  • Improved error handling and cleanup logic throughout the loader: on failure, resources are released and the tracking slot is deactivated. [1] [2]

Modbus Protocol Compliance

  • Introduced OpenPLCDeviceContext, a custom Modbus device context that ensures the correct echo response for FC5 (Write Single Coil) and FC6 (Write Single Register) per the Modbus specification, by caching values per asyncio task and returning them as required.
  • Updated device context instantiation in the Modbus slave to use OpenPLCDeviceContext for proper FC5/FC6 handling.

Other Improvements

  • Enhanced logging macros to prevent null pointer dereferencing if loggers are not set.
  • Updated includes for portability and correctness (added <unistd.h>, <signal.h>, <stdbool.h>, <sys/wait.h>). [1] [2]
  • Minor: Ensured plugin manager header is included where needed.

These changes significantly improve the stability, correctness, and maintainability of both the Python function block subsystem and Modbus slave implementation.

thiagoralves and others added 8 commits January 26, 2026 12:38
Per Modbus specification, FC5 (Write Single Coil) and FC6 (Write Single
Register) responses should echo the requested value. However, pymodbus
implements these by calling setValues() then getValues(), using the
read-back value in the response.

OpenPLC uses a journal-based write system where writes are queued and
applied at the next PLC scan cycle. This caused getValues() to return
the old buffer value instead of the just-written value, making clients
like libmodbus report errors due to request/response mismatch.

This fix introduces OpenPLCDeviceContext that caches FC5/FC6 write
values and returns them on the subsequent getValues() call. The cache
uses asyncio task ID to isolate concurrent requests, preventing race
conditions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The iec_python.h header is included during compilation of generated
PLC code. Python Function Blocks generate inline C code that calls
getpid(), but the header only included sys/types.h (for pid_t) and
was missing unistd.h (for getpid() declaration), causing compilation
to fail with "implicit declaration of function 'getpid'" error.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The runtime crashed when stopping a PLC with Python Function Blocks
that were actively printing to stdout. This happened because:

1. python_loader.c was compiled into libplc_*.so (not main executable)
2. Runner threads reading Python stdout were detached and never tracked
3. When dlclose() unloaded the library, runner threads crashed because
   their code (runner_thread function) was unmapped from memory

Changes:
- Track all Python blocks in an array with thread IDs, PIDs, and shm info
- Use fork()/exec() instead of popen() to get the Python subprocess PID
- Add python_blocks_cleanup() function that:
  - Sends SIGTERM to all Python processes (then SIGKILL if needed)
  - Joins all runner threads (they exit on EOF when Python dies)
  - Unmaps and unlinks shared memory regions
  - Removes Python script files
- Call python_blocks_cleanup() before dlclose() in unload_plc_program()
- Add null checks to LOG_INFO/LOG_ERROR macros for safety during cleanup

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
fix: Python Function Block compilation and cleanup
…e-echo

fix: Correct FC5/FC6 Modbus response to echo requested value
@thiagoralves thiagoralves merged commit 36fedbf into main Jan 28, 2026
2 checks passed
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.

1 participant