Open-source template for integrating the Visionify AI Safety Platform with Programmable Logic Controllers (PLCs).
This application receives real-time safety events from Visionify via webhooks and translates them into PLC commands — enabling automated machine stops, alerts, and safety interlocks driven by AI-powered vision.
- Overview
- Architecture
- Supported PLC Protocols
- Quick Start
- Configuration Reference
- Webhook Event Payload
- PLC Handler Examples
- Testing
- Deployment
- Troubleshooting
- Contributing
- License
The Visionify AI Safety Platform uses computer vision to detect safety hazards in real time — people entering restricted zones, PPE violations, forklift proximity risks, spills, and more. When a safety event is detected, Visionify can trigger a hard-stop or alert action by sending a webhook to this template application.
This template application:
- Receives webhook events from Visionify over HTTP (POST requests)
- Validates the request using HMAC-SHA256 signature verification
- Routes the event to the appropriate action (stop, alert, or ignore) based on your configuration
- Executes PLC logic — writing coils, registers, tags, or OPC-UA nodes to control your machinery
This is a template. You download it, configure it for your site, customize the PLC communication code for your specific hardware, and run it on your local network alongside your PLC.
┌─────────────────────┐ Webhook (HTTP POST) ┌──────────────────────────┐
│ │ ──────────────────────────────────► │ │
│ Visionify AI │ Event: hard_stop, ppe_violation, │ PLC Template App │
│ Safety Platform │ zone_breach, forklift_proximity │ (this application) │
│ │ │ │
│ - Camera feeds │ ◄───────────────────────────────── │ - Validates signature │
│ - AI detection │ 200 OK / acknowledgment │ - Routes event │
│ - Trigger rules │ │ - Executes PLC logic │
│ - Zone config │ │ │
└─────────────────────┘ └────────────┬─────────────┘
│
Modbus TCP / EtherNet/IP /
OPC-UA / Digital Output
│
▼
┌──────────────────────────┐
│ │
│ PLC / Safety Relay │
│ │
│ - Machine stop │
│ - HMI alert display │
│ - Safety interlock │
│ │
└──────────────────────────┘
How it works:
- Visionify detects a safety event (e.g., person enters a restricted zone near a press machine).
- Visionify sends a POST request to this application's webhook endpoint with event details.
- This application verifies the signature, looks up the event type in its configuration, and determines the action (stop or alert).
- The appropriate PLC handler writes the signal to your PLC — setting a stop coil, writing an event code register, or toggling a digital output.
- Your PLC's safety routine takes over (machine halt, alarm, interlock hold, etc.).
| Protocol | Handler | Compatible Hardware | Library |
|---|---|---|---|
| Modbus TCP | modbus_tcp |
Schneider, ABB, Wago, Delta, any Modbus-compatible PLC | pymodbus |
| EtherNet/IP | ethernet_ip |
Allen-Bradley ControlLogix, CompactLogix, Micro800 | pycomm3 |
| OPC-UA | opcua |
Siemens S7-1500, Beckhoff TwinCAT, any OPC-UA server | asyncua |
| Digital Output | digital_output |
Raspberry Pi GPIO, USB relay boards, industrial I/O | RPi.GPIO |
Don't see your protocol? The handler architecture is modular — see Adding a Custom Handler to create your own.
- Python 3.9 or later
- Network connectivity between this application and your PLC
- A running Visionify instance with webhook configuration access
git clone https://github.com/visionify/visionify-plc-template-application.git
cd visionify-plc-template-application
# Create a virtual environment
python -m venv venv
source venv/bin/activate # Linux/macOS
# venv\Scripts\activate # Windows
# Install base dependencies
pip install -r requirements.txt
# Install the PLC library for your protocol (pick one):
pip install pymodbus>=3.6 # Modbus TCP
pip install pycomm3>=1.2 # EtherNet/IP (Allen-Bradley)
pip install asyncua>=1.0 # OPC-UA
pip install RPi.GPIO>=0.7 # Digital Output (Raspberry Pi)# Create your site-specific config (git-ignored)
cp config.yaml config.local.yamlEdit config.local.yaml:
webhook:
port: 5000
secret: "your-strong-secret-here" # Must match Visionify webhook settings
plc:
protocol: "modbus_tcp" # Choose: modbus_tcp, ethernet_ip, opcua, digital_output
modbus_tcp:
host: "192.168.1.100" # Your PLC's IP address
port: 502
stop_coil_address: 0 # Coil to trigger emergency stop
event_register_address: 100 # Register for event codepython app.pyYou should see:
[2026-03-21 10:00:00] INFO ============================================================
[2026-03-21 10:00:00] INFO Visionify PLC Template Application
[2026-03-21 10:00:00] INFO ============================================================
[2026-03-21 10:00:00] INFO PLC Protocol : modbus_tcp
[2026-03-21 10:00:00] INFO Webhook URL : http://0.0.0.0:5000/visionify/webhook
[2026-03-21 10:00:00] INFO Health Check : http://0.0.0.0:5000/health
[2026-03-21 10:00:00] INFO Reset URL : http://0.0.0.0:5000/visionify/reset
[2026-03-21 10:00:00] INFO ============================================================
In the Visionify web app:
- Navigate to Integrations > Webhook Settings
- Click Add Webhook
- Enter the endpoint URL:
http://<this-machine-ip>:5000/visionify/webhook - Enter the same secret token you configured in
config.local.yaml - Select the event types to forward (Hard-Stop Trigger, PPE Violation, etc.)
- Click Save, then Send Test Event to verify the connection
# Send a simulated hard-stop event
python test_webhook.py
# Send a specific event type
python test_webhook.py --event ppe_violation
# Send all sample events
python test_webhook.py --all
# Test the reset endpoint
python test_webhook.py --resetThe configuration file (config.yaml / config.local.yaml) has four sections:
webhook:
host: "0.0.0.0" # Bind address
port: 5000 # Listen port
secret: "your-secret" # HMAC-SHA256 shared secret
endpoint: "/visionify/webhook" # Webhook pathplc:
protocol: "modbus_tcp" # Handler to use
modbus_tcp: # Modbus TCP settings
host: "192.168.1.100"
port: 502
unit_id: 1
stop_coil_address: 0
event_register_address: 100
timeout: 3
ethernet_ip: # EtherNet/IP settings
host: "192.168.1.101"
stop_tag: "Safety_EStop"
event_tag: "Safety_EventCode"
opcua: # OPC-UA settings
endpoint: "opc.tcp://192.168.1.102:4840"
stop_node_id: "ns=2;s=Safety.EStop"
event_node_id: "ns=2;s=Safety.EventCode"
username: null
password: null
digital_output: # GPIO / Relay settings
pin: 17
logic: "active_high"
pulse_duration_ms: 0 # 0 = latchingMap Visionify event types to PLC actions. Each event type can trigger a stop, alert, or ignore action, and writes a numeric event_code to the PLC register for HMI identification.
events:
mapping:
hard_stop:
action: "stop" # "stop", "alert", or "ignore"
event_code: 1 # Numeric code written to PLC register
description: "Emergency stop"
ppe_violation:
action: "alert"
event_code: 10
description: "PPE violation detected"logging:
level: "INFO" # DEBUG, INFO, WARNING, ERROR
file: "logs/plc_webhook.log"
max_size_mb: 10
backup_count: 5Visionify sends JSON payloads to your webhook endpoint. Here is the structure:
{
"event_id": "evt-abc-123",
"event_type": "hard_stop",
"timestamp": "2026-03-21T14:30:00.000Z",
"confidence": 0.92,
"camera": {
"id": "cam-01",
"name": "Press Area Camera 1"
},
"zone": {
"id": "zone-a",
"name": "ZONE-A-PRESS-RESTRICTED"
},
"trigger": {
"id": "trig-01",
"name": "ZONE-A-INTRUSION-STOP"
},
"detection": {
"type": "person",
"bounding_box": {
"x": 320,
"y": 240,
"width": 80,
"height": 200
}
}
}| Field | Type | Description |
|---|---|---|
event_id |
string | Unique identifier for this event |
event_type |
string | Event category (matches keys in events.mapping) |
timestamp |
string | ISO 8601 timestamp of the detection |
confidence |
float | AI model confidence score (0.0 - 1.0) |
camera |
object | Camera that captured the event |
zone |
object | Safety zone where the event occurred |
trigger |
object | Trigger rule that fired (null for alert-only events) |
detection |
object | Detection details (type, bounding box, etc.) |
| Event Type | Description | Default Action |
|---|---|---|
hard_stop |
Emergency stop triggered by safety violation | stop |
restricted_zone_breach |
Person detected in restricted zone | stop |
forklift_proximity |
Forklift too close to personnel | stop |
ppe_violation |
Required PPE not detected | alert |
person_detected |
Person detected in monitored area | alert |
spill_detected |
Spill or obstruction detected | alert |
Every webhook request includes an X-Visionify-Signature header containing an HMAC-SHA256 signature:
X-Visionify-Signature: sha256=<hex-digest>
The signature is computed over the raw request body using the shared secret configured in both Visionify and this application.
In plc_handlers/modbus_tcp.py, the execute_stop() method writes to a coil and register:
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient(host="192.168.1.100", port=502)
client.connect()
# Write event code to holding register (for HMI display)
client.write_register(address=100, value=event_code, slave=1)
# Set emergency stop coil HIGH
client.write_coil(address=0, value=True, slave=1)
client.close()In plc_handlers/ethernet_ip.py:
from pycomm3 import LogixDriver
with LogixDriver("192.168.1.101") as plc:
plc.write(("Safety_EventCode", event_code))
plc.write(("Safety_EStop", True))In plc_handlers/opcua.py:
from asyncua.sync import Client
from asyncua import ua
client = Client("opc.tcp://192.168.1.102:4840")
client.connect()
event_node = client.get_node("ns=2;s=Safety.EventCode")
event_node.write_value(ua.Variant(event_code, ua.VariantType.Int32))
stop_node = client.get_node("ns=2;s=Safety.EStop")
stop_node.write_value(ua.Variant(True, ua.VariantType.Boolean))
client.disconnect()In plc_handlers/digital_output.py:
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(17, GPIO.OUT)
GPIO.output(17, GPIO.HIGH) # Activate stop relayTo add support for a new PLC protocol:
- Create a new file in
plc_handlers/(e.g.,plc_handlers/profinet.py) - Subclass
BasePLCHandlerand implement the three required methods:
from plc_handlers.base import BasePLCHandler
class ProfinetHandler(BasePLCHandler):
def __init__(self, config, logger):
super().__init__(config, logger)
self.plc_config = config.get("plc", {}).get("profinet", {})
# Initialize your connection parameters here
def execute_stop(self, event_code: int, event: dict) -> dict:
# Send stop signal via your protocol
return {"success": True, "protocol": "profinet", "action": "stop"}
def execute_alert(self, event_code: int, event: dict) -> dict:
# Send alert signal
return {"success": True, "protocol": "profinet", "action": "alert"}
def execute_reset(self) -> dict:
# Clear stop state
return {"success": True, "protocol": "profinet", "action": "reset"}- Register it in
plc_handlers/__init__.py:
from plc_handlers.profinet import ProfinetHandler
HANDLER_MAP = {
...
"profinet": ProfinetHandler,
}- Add configuration settings under
plc.profinetinconfig.yaml.
The application ships in simulation mode by default — all PLC communication is logged but not actually sent. This lets you test the full webhook flow without a live PLC connection.
# Terminal 1: Start the application
python app.py
# Terminal 2: Send test events
python test_webhook.py --allcurl http://localhost:5000/healthReturns:
{
"status": "ok",
"service": "visionify-plc-template",
"plc_protocol": "modbus_tcp",
"timestamp": "2026-03-21T14:30:00.000Z"
}When you're ready to connect to a real PLC:
- Install the appropriate PLC library (see Quick Start)
- Open the handler file for your protocol (e.g.,
plc_handlers/modbus_tcp.py) - Uncomment the real PLC communication blocks (clearly marked in the code)
- Update
config.local.yamlwith your PLC's IP address, port, and register/tag mappings - Restart the application
Safety Note: Always test PLC integration with the machine in a safe state (de-energized or in maintenance interlock mode) before going live.
Create a systemd service file at /etc/systemd/system/visionify-plc.service:
[Unit]
Description=Visionify PLC Template Application
After=network.target
[Service]
Type=simple
User=plcuser
WorkingDirectory=/opt/visionify-plc-template-application
ExecStart=/opt/visionify-plc-template-application/venv/bin/python app.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetThen:
sudo systemctl daemon-reload
sudo systemctl enable visionify-plc
sudo systemctl start visionify-plcFROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Uncomment and install your PLC library:
# RUN pip install pymodbus>=3.6
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]docker build -t visionify-plc .
docker run -d -p 5000:5000 -v $(pwd)/config.local.yaml:/app/config.local.yaml visionify-plc- This application should run on the same local network as your PLC (OT network)
- Do not expose the webhook endpoint to the public internet without VPN or firewall protection
- Use a strong, randomly generated secret token:
python -c "import secrets; print(secrets.token_hex(32))" - Consider running behind a reverse proxy (nginx) with TLS for the webhook endpoint if Visionify is on a separate network segment
| Metric | Target | Maximum |
|---|---|---|
| Detection-to-trigger latency | < 150 ms | 300 ms |
| Trigger-to-PLC signal latency | < 50 ms | 100 ms |
| End-to-end (detection to machine stop) | < 500 ms | 800 ms |
| Symptom | Likely Cause | Resolution |
|---|---|---|
401 Unauthorized on webhook |
Secret mismatch | Ensure webhook.secret in config matches the token in Visionify webhook settings |
Connection refused |
App not running or wrong port | Check the app is running and the port matches the URL configured in Visionify |
| PLC not responding | Network issue or wrong IP/port | Verify PLC is reachable: ping <plc-ip>. Check firewall rules on the OT network |
No mapping for event type in logs |
Event type not in config | Add the event type to events.mapping in config.yaml |
| High latency (> 300ms) | Network congestion or PLC overload | Check network path between app and PLC. Reduce PLC polling frequency |
[SIMULATION] in logs |
Real PLC code not enabled | Uncomment the PLC communication blocks in your handler file |
Set logging.level in config.yaml:
- DEBUG — Full request/response details, PLC register values
- INFO — Event received, actions taken, connection status
- WARNING — Stop actions, signature failures, missing mappings
- ERROR — Exceptions, connection failures
| Endpoint | Method | Description |
|---|---|---|
/visionify/webhook |
POST | Receive Visionify safety events |
/visionify/reset |
POST | Remotely reset PLC stop state |
/health |
GET | Health check and status |
visionify-plc-template-application/
├── app.py # Main application entry point
├── config.yaml # Configuration template
├── requirements.txt # Python dependencies
├── test_webhook.py # Test script for simulating events
├── plc_handlers/
│ ├── __init__.py # Handler factory and registry
│ ├── base.py # Abstract base class
│ ├── modbus_tcp.py # Modbus TCP handler
│ ├── ethernet_ip.py # EtherNet/IP handler (Allen-Bradley)
│ ├── opcua.py # OPC-UA handler (Siemens, Beckhoff)
│ └── digital_output.py # GPIO / relay handler
├── LICENSE # MIT License
└── README.md # This file
Contributions are welcome! If you've integrated Visionify with a PLC type not yet covered, consider adding a handler.
- Fork the repository
- Create a feature branch:
git checkout -b feature/profinet-handler - Add your handler following the Adding a Custom Handler guide
- Submit a pull request
Please include:
- A brief description of the PLC hardware/protocol you tested with
- Any configuration additions needed in
config.yaml - Example usage in the handler's module docstring
This project is licensed under the MIT License — see the LICENSE file for details.
Visionify AI Safety Platform | visionify.ai | Documentation