00 EtherNet/IP ve CIP nedir — ODVA ve miras
EtherNet/IP (Ethernet Industrial Protocol), CIP'i (Common Industrial Protocol) standart TCP/IP ve UDP/IP üzerinde taşıyan endüstriyel iletişim protokolüdür. 2000 yılında Rockwell Automation ve ControlNet International'ın birleşiminden doğan ODVA (Open DeviceNet Vendors Association) tarafından yönetilmekte; IEC 61158 Type 2 (ControlNet) ve Type 11 (EtherNet/IP) kapsamında standartlaştırılmaktadır.
CIP ailesinin doğuşu
CIP, taşıma katmanından bağımsız tek bir uygulama katmanı protokolü olarak tasarlanmıştır. Allen-Bradley'nin 1990'larda geliştirdiği DeviceNet (CAN üzeri, 1994) ve ControlNet (koaksiyel kablo, 1995) aynı CIP çekirdeğini farklı fiziksel ortamlarda taşıyordu. 2000'de bu miras, standart Ethernet'e taşınarak EtherNet/IP adını aldı.
CIP (Common Industrial Protocol)
Nesne Modeli • Servisler • Profiller
│
┌───────────────┼───────────────┐
│ │ │
DeviceNet ControlNet EtherNet/IP
(CAN, 500 kbps) (koaksiyel) (100/1000 Mbps)
1994 1995 2000
ISO 11898 IEC 61158-3 IEC 61158-11
Type 2 Type 11
CIP ayrıca CompoNet (RS-485 tabanlı sensör/aktüatör seviyesi ağı, 2004) ve EtherNet/IP Safety (SIL 3/PLe güvenlik profili) gibi uzantıları da kapsar. Tüm bu ağlarda aynı nesne sözlüğü, servis kodları ve cihaz profilleri geçerlidir; bu sayede bir PLC programcısı DeviceNet ve EtherNet/IP cihazlarına aynı mantıkla erişebilir.
Endüstrideki konum
2025 itibarıyla dünya genelinde kurulu EtherNet/IP düğüm sayısı 40 milyonu aşmaktadır. Protokolün bu denli yaygınlaşmasının başlıca sebebi Allen-Bradley/Rockwell Automation'ın Kuzey Amerika endüstrisindeki baskın konumudur. Otomobil üretimi, gıda ve içecek, petrokimya ve su arıtma sektörlerinde standart haline gelmiştir.
Allen-Bradley terminolojisinde "Logix" ailesi (CompactLogix, ControlLogix, MicroLogix) EtherNet/IP'i doğrudan destekler. Eski PLC5 ve SLC500 serisi Ethernet modülü üzerinden EtherNet/IP konuşur ancak sembolik tag desteği yoktur; bunlar için PCCC (Programmable Controller Communication Commands) protokolü gerekir. pycomm3 her iki modu da destekler.
01 CIP nesne modeli — Identity, Assembly, Connection Manager
CIP'in temel soyutlama birimi "nesne"dir. Her cihaz zorunlu ve isteğe bağlı nesnelerden oluşan bir nesne modeli uygular. Her nesne; sınıf (Class), örnek (Instance) ve özellik (Attribute) üçlüsüyle adreslenir. Bu model, SNMP'nin MIB hiyerarşisine kavramsal olarak benzer ancak çok daha güçlü servis mekanizmaları içerir.
Zorunlu ve yaygın CIP nesneleri
| Sınıf Adı | Class ID | Zorunlu | Görev |
|---|---|---|---|
| Identity Object | 0x01 | Evet | Vendor ID, Device Type, Product Code, Major/Minor Revision, Serial Number, Product Name — cihazın kimliği |
| Message Router | 0x02 | Evet | Gelen CIP mesajlarını ilgili nesneye yönlendiren dahili dispatcher |
| Assembly Object | 0x04 | Profile bağımlı | Birden fazla attribute'i tek veri bloğunda birleştirir; I/O verisi Assembly üzerinden taşınır |
| Connection Manager | 0x06 | Evet | I/O ve explicit bağlantıların kurulumu, izlenmesi ve kapatılması |
| Parameter Object | 0x0F | Hayır | Cihaz parametrelerini yapılandırılmış şekilde sunar; mühendislik araçları bu nesneyi okur |
| TCP/IP Interface | 0xF5 | Evet | IP adresi, subnet mask, default gateway, hostname ve DNS konfigürasyonu |
| Ethernet Link | 0xF6 | Evet | MAC adresi, hız/duplex müzakeresi, RX/TX istatistikleri, autoneg durumu |
| Port Object | 0xF4 | Evet | CIP port tanımı (EtherNet/IP = port 2); multi-port cihazlar için birden fazla örnek |
| Vendor-Specific | 0x64–0xC7 | Hayır | Üretici tanımlı özel nesneler; standart nesnelerin karşılamadığı özellikler için |
Servis kodları
CIP'te her nesne servis adı verilen işlemleri destekler. Temel servisler tüm nesnelerde ortaktır; nesneye özgü servisler ek olarak tanımlanır:
CIP path yapısı ve adresleme
Her CIP isteği bir "path" (yol) segmenti içerir. Path, hangi nesnenin hangi örneğinin hangi attribute'üne erişileceğini bayt dizisi olarak kodlar. Üç temel segment tipi vardır: Logical (class/instance/attribute), Port (ağ geçişi) ve Symbolic (Rockwell tag adı).
Logical Segment kodlaması (1 byte segment tipi + 1 byte değer): ┌────────────────────────────────────────────────────────────┐ │ Segment Tip Byte Değer Byte Açıklama │ │ ────── ──────── ────────── ──────── │ │ Class 0x20 0x01 Class ID = Identity │ │ Instance 0x24 0x01 Instance = 1 │ │ Attribute 0x30 0x07 Attribute = Product Name │ │ │ │ Tam path: 20 01 24 01 30 07 (6 byte = 3 word) │ └────────────────────────────────────────────────────────────┘ Symbolic Segment (Rockwell Extended Symbol): 0x91 0x0B "MotorSpeed\0" (type=symbolic, len=11, name) → Controller scope tag "MotorSpeed" 0x91 0x12 "Program:Main" 0x91 0x08 "TankTemp\0" → Program scope tag: Program:Main.TankTemp
Identity Object (0x01) Attribute 7, "Product Name" alanını içerir ve ASCII string döndürür. Attribute 3 ise "State" (Nonexistent/Device Self Testing/Standby/Operational/Major Recoverable Fault/Major Unrecoverable Fault) bilgisidir. Bir ağ tarama aracı yazarken bu iki attribute genellikle ilk hedef olur.
02 Implicit messaging — I/O bağlantısı ve Class 1
Implicit messaging (I/O messaging), gerçek zamanlı döngüsel veri alışverişi için kullanılır. UDP port 2222 üzerinden çalışır; her pakette CIP başlığı yoktur, yalnızca ham proses verisi ve 32-bit Real-Time Header taşınır. "Implicit" adını, ne gönderileceğinin bağlantı kurulumunda önceden tanımlanmış olmasından alır.
Class 1 bağlantı parametreleri
ForwardOpen el sıkışması
Scanner (PLC) Target (Drive)
│ │
│── TCP SYN / SYN-ACK / ACK ──────────────►│ (44818)
│── RegisterSession ───────────────────────►│
│◄─ RegisterSession Response (SessionID) ───│
│ │
│── ForwardOpen Request (TCP 44818) ───────►│
│ ┌─ Priority & TimeTick │
│ ├─ O→T Connection Params: │
│ │ Connection ID: 0xABCD0001 │
│ │ RPI: 10000 µs │
│ │ Size: 4 bytes │
│ │ Type: Point-to-Point, UDP │
│ ├─ T→O Connection Params: │
│ │ Connection ID: 0xABCD0002 │
│ │ RPI: 10000 µs │
│ │ Size: 8 bytes │
│ └─ Connection Path: 20 04 24 64 (Out) │
│ 20 04 24 65 (In) │
│ 20 04 24 66 (Cfg) │
│ │
│◄─ ForwardOpen Response (TCP 44818) ───────│
│ O→T Conn ID: 0xABCD0001 │
│ T→O Conn ID: 0xABCD0002 │
│ O→T API: 10000 µs (actual) │
│ │
│══ UDP I/O Cyclic 10 ms ─────────────────►│ (2222)
│◄═ UDP I/O Cyclic 10 ms ───────────────── │
│ [32-bit Conn ID][32-bit SeqNum][data] │
Real-Time Header
Her I/O UDP paketinin başında 32-bit bir Real-Time Header bulunur. Bu alan, Run/Idle bitini ve sıra sayacını içerir. Run biti "1" ise PLC'nin Program modundan değil Run modundan veri gönderdiğini belirtir; hedef cihaz bu bite bakarak güvenli duruma geçip geçmeyeceğine karar verir.
Consumed / Produced tag (Rockwell)
Allen-Bradley ControlLogix/CompactLogix mimarisinde "Produced Tag" ve "Consumed Tag" mekanizması, PLC'den PLC'ye I/O bağlantısını soyutlar. Bir tag "produced" olarak işaretlendiğinde diğer PLC'ler onu "consumed" tag olarak tüketebilir. Bu yapı, aslında CIP Class 1 I/O bağlantısının Logix Studio 5000 arayüzüne yansımasıdır; arka planda ForwardOpen ile kurulan UDP bağlantısı aynıdır.
03 Explicit messaging — Class 3, UCMM ve Forward_Open
Explicit messaging, parametre okuma/yazma, cihaz konfigürasyonu ve teşhis için istek-yanıt (request-response) modeliyle çalışır. TCP port 44818 üzerinden iletilir; her mesaj hangi nesnenin hangi attribute'üne ne yapılacağını açıkça kodlar.
Explicit messaging türleri
MR Request/Reply formatı
CIP Explicit Request (SendRRData payload): ┌──────────┬──────────┬────────────────────┬──────────────┐ │ Service │ Path │ Path (segments) │ Request Data │ │ 1 byte │ Size(wds)│ variable │ variable │ │ 0x0E │ 0x03 │ 20 01 24 01 30 07 │ (none) │ │ GetAttr │ │ Cls Ins Attr │ │ └──────────┴──────────┴────────────────────┴──────────────┘ CIP Explicit Reply: ┌──────────┬──────────┬──────────────────────────────────┐ │ Service │ Reserved │ General Status │ Response Data │ │ 1 byte │ 1 byte │ 1 byte │ variable │ │ 0x8E │ 0x00 │ 0x00 (OK) │ "CompactLogix" │ │ (reply │ │ │ (ASCII str) │ │ = 0x80 │ │ │ │ │ | req) │ │ │ │ └──────────┴──────────┴────────────────┴─────────────────┘ General Status Kodları: 0x00 = Success 0x08 = Service not supported 0x09 = Invalid attribute value 0x14 = Attribute not supported 0x16 = Object does not exist
Forward_Open servisi (0x54)
Hem implicit hem de explicit connected messaging için bağlantı kurmak üzere Connection Manager nesnesi (0x06) üzerinde çağrılan servistir. Başarılı yanıt iki Connection ID döndürür: O→T ve T→O için ayrı ID'ler. Bu ID'ler sonraki tüm bağlantı mesajlarında taşınır.
import socket, struct
def register_session(ip: str, port: int = 44818) -> tuple[socket.socket, int]:
"""EtherNet/IP oturumu aç, session handle döndür."""
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5.0)
s.connect((ip, port))
# RegisterSession: Command=0x0065, Length=4, Session=0, Status=0, options
pkt = struct.pack(' bytes:
"""UCMM ile Get_Attribute_Single (servis 0x0E)."""
sock, session = register_session(ip)
path = bytes([
0x20, class_id, # Class segment
0x24, instance, # Instance segment
0x30, attribute, # Attribute segment
])
cip_req = bytes([0x0E, len(path) // 2]) + path # service, path_words, path
# Interface handle + timeout + item list (2 items: null address + connected data)
null_item = struct.pack('
Üretim ortamlarında ham soket yerine pycomm3 kullanın. Ham soket kodu, encapsulation fragmantasyonu, TCP yeniden bağlantı mantığı ve CIP hata durumlarını yönetmez. Yukarıdaki örnek yalnızca protokol anlayışı amacıyla sunulmuştur.
04 EDS dosyası — Electronic Data Sheet
EDS (Electronic Data Sheet), bir CIP cihazının kimliğini, parametrelerini, I/O bağlantı yapılandırmalarını ve desteklenen servisleri tanımlayan ASCII metin dosyasıdır. ODVA'nın tanımladığı bu format, mühendislik yazılımlarının (RSLogix, Studio 5000, RSNetWorx, Sysmac Studio) cihazı otomatik tanımasını ve yapılandırmasını sağlar.
EDS dosyasının temel bölümleri
$ EDS örneği: EmbeddedDeck_DigitalIO.eds
[File]
DescText = "EmbeddedDeck 8DI/8DO Modülü";
CreateDate = 04-12-2026;
CreateTime = 09:00:00;
ModDate = 04-12-2026;
ModTime = 09:00:00;
Revision = 2.1;
HomeURL = "https://x3beche.github.io/embedded-deck";
[Device]
VendorCode = 0xFFFF; $ Test Vendor ID (ODVA'dan alınacak)
VendorName = "EmbeddedDeck";
ProdType = 7; $ 0x07 = Generic Device
ProdTypeStr = "Generic Device";
ProdCode = 1;
MajRev = 2;
MinRev = 1;
ProdName = "EDBED-DIO-8x8";
Catalog = "EDBED-DIO-001";
Icon = "generic_io.ico";
[Device Classification]
Class1 = EtherNetIP;
[Params]
Param1 =
0, $ reserved
,, $ path
0x0004, $ Class = Assembly
0x0064, $ Instance = 100 (O→T output)
0x0003, $ Attribute = Data
0x0000, $ access = get/set
"Dijital Çıkışlar",
"bitfield",
"DO7..DO0 bit alanı",
0, 0xFF, 0,
1, 1,
0, ;
Param2 =
0,,,
0x0004, 0x0065, 0x0003,
0x0000,
"Dijital Girişler",
"bitfield",
"DI7..DI0 bit alanı",
0, 0xFF, 0,
1, 1,
0, ;
[Assembly]
Assem100 = $ Output Assembly (O→T): PLC → Modül
0x0000,
"$PARAM[1]",
,
1, 0; $ 1 byte, proxy=0
Assem101 = $ Input Assembly (T→O): Modül → PLC
0x0000,
"$PARAM[2]",
,
1, 0;
Assem102 = $ Configuration Assembly (boş)
0x0000,
"",
,
0, 0;
[Connection Manager]
Connection1 =
0x04010002, $ Transport: Class 1, Cyclic, Server
0x44240305, $ Point-to-Point, Exclusive Owner
0x00002710, $ O→T RPI: 10000 µs
,,
0x00002710, $ T→O RPI: 10000 µs
,,
,
"Exclusive Owner",
,
"20 04 24 64", $ O→T Path: Assembly 100
"20 04 24 65", $ T→O Path: Assembly 101
"20 04 24 66",; $ Config Path: Assembly 102
ODVA'nın ücretsiz EDS Validation Tool'u (Windows) ile EDS dosyalarını kaydetmeden önce doğrulayın. RSNetWorx ve Studio 5000, EDS import öncesinde CRC ve syntax kontrolü yapar; hatalı EDS cihazın tanınmamasına veya yanlış boyutlu I/O bağlantısına yol açar. EDS dosyasını Git'te sürümleyin ve her firmware değişikliğinde revizyon numarasını artırın.
05 Linux'ta EtherNet/IP — OpENer ve açık kaynak yığınlar
Linux üzerinde EtherNet/IP adaptör (device/slave) veya scanner (master/client) geliştirmek için birkaç olgun açık kaynak seçenek mevcuttur. Seçim; rolünüze (adaptör mü, scanner mı), lisans gereksinimlerine ve hedef donanıma bağlıdır.
Açık kaynak EtherNet/IP yığınları
| Kütüphane | Rol | Dil | Lisans | Durum | Not |
|---|---|---|---|---|---|
| OpENer | Adaptör (Device) | C | BSD 3-Clause | Aktif | ODVA üyesi desteği, POSIX + FreeRTOS portu |
| pycomm3 | Scanner (Client) | Python | MIT | Aktif | Rockwell Logix odaklı, en popüler Python çözümü |
| cpppo | Scanner + Adaptör | Python/C++ | GPL-3.0 | Aktif | Genel CIP, EtherNet/IP + DF1 desteği |
| CIPster | Adaptör | C++ | Apache 2.0 | Pasif | OpENer'ın C++ yeniden yazımı |
| libethernetip | Scanner | C | LGPL | Pasif | Hafif, embedded hedefler için |
OpENer — Linux üzerinde derleme ve çalıştırma
OpENer, ODVA'nın desteklediği referans uygulamaya en yakın açık kaynak EtherNet/IP adaptör yığınıdır. POSIX platformu için örnek bir "sample application" içerir; bu uygulamayı temel alarak kendi I/O modülünüzü geliştirebilirsiniz.
# Bağımlılıklar
sudo apt update
sudo apt install -y cmake build-essential libpcap-dev git
# Kaynak kodu
git clone https://github.com/EIPStackGroup/OpENer.git
cd OpENer
# POSIX platformu için derleme
mkdir build && cd build
cmake ../source \
-DOPENER_PLATFORM=POSIX \
-DCMAKE_BUILD_TYPE=Release \
-DOPENER_TESTS=OFF
make -j$(nproc)
# Çalıştır (root gereklidir — raw socket için)
# Sözdizimi: OpENer
sudo ./src/ports/POSIX/OpENer eth0 192.168.1.20 255.255.255.0 192.168.1.1
# Doğrulama: pycomm3 veya cpppo ile Identity oku
python3 -c "
from pycomm3 import CIPDriver
with CIPDriver('192.168.1.20') as d:
r = d.generic_message(
service=0x01, # Get_Attributes_All
class_code=0x01, # Identity Object
instance=1,
data_type=None,
name='identity'
)
print('OpENer Identity:', r.value)
"
OpENer'da özel Assembly tanımlama
Kendi I/O verilerinizi taşımak için OpENer'ın user_application.c dosyasını düzenler ve CIP_Assembly_CreateAssemblyObject() ile yeni assembly örnekleri eklersiniz. Üretilen/tüketilen veri alanları için global değişkenler tanımlayıp bu assembly'lere bağlarsınız. Her döngüde çağrılan ApplicationRunHook() fonksiyonu donanım okuma/yazma noktanızdır.
/* user_application.c — basit 8DI / 8DO örneği */
#include "opener_api.h"
static uint8_t g_digital_outputs = 0x00; /* O→T: PLC → biz */
static uint8_t g_digital_inputs = 0x00; /* T→O: biz → PLC */
EipStatus ApplicationInitialization(void) {
/* Output Assembly: Class 0x04, Instance 100, 1 byte */
CipAssembly *out_asm = CreateAssemblyObject(
100, (EipByte *)&g_digital_outputs, 1);
/* Input Assembly: Class 0x04, Instance 101, 1 byte */
CipAssembly *in_asm = CreateAssemblyObject(
101, (EipByte *)&g_digital_inputs, 1);
/* Config Assembly: boş */
CreateAssemblyObject(102, NULL, 0);
return kEipStatusOk;
}
void RunIdleChanged(EipUint32 run_idle_value) {
if (run_idle_value & 1) {
/* PLC Run modunda — çıkışları uygula */
/* gpio_write(GPIO_PORT_DO, g_digital_outputs); */
} else {
/* PLC Idle/Program modunda — çıkışları sıfırla */
/* gpio_write(GPIO_PORT_DO, 0x00); */
}
}
EipStatus AfterAssemblyDataReceived(CipAssembly *assembly) {
/* O→T verisi geldi (g_digital_outputs güncellendi) */
/* gpio_write(GPIO_PORT_DO, g_digital_outputs); */
return kEipStatusOk;
}
void BeforeAssemblyDataSend(CipAssembly *assembly) {
/* T→O göndermeden önce giriş değerlerini tazele */
/* g_digital_inputs = gpio_read(GPIO_PORT_DI); */
}
06 pycomm3 Python kütüphanesi
pycomm3, Allen-Bradley CompactLogix, ControlLogix ve Micro800 PLC'lerle iletişim için en yaygın kullanılan Python kütüphanesidir. Rockwell'in Logix5000 sembolik tag protokolünü (CIP servisler 0x4C/0x4D) uygular; PLC programının tag adlarına doğrudan erişim sağlar.
pip install pycomm3 # son kararlı sürüm
pip install pycomm3==0.12.4 # belirli sürüm sabitleme (üretim için)
LogixDriver bağlantı parametreleri
Tag okuma: atomic, array ve UDT
from pycomm3 import LogixDriver
with LogixDriver('192.168.1.1/0') as plc:
# Atomic tag (tek değer)
speed = plc.read('Motor1.Speed') # REAL
running = plc.read('Motor1.Running') # BOOL
fault = plc.read('Motor1.FaultCode') # DINT
print(f"Hız: {speed.value:.1f} RPM "
f"Çalışıyor: {running.value} "
f"Hata: {fault.value}")
# Çoklu tag — tek TCP round-trip (verimli)
results = plc.read(
'Motor1.Speed',
'Motor1.Current',
'Motor1.Running',
'Tank.Temperature',
'System.CycleCount',
)
for r in results:
if r.error:
print(f" HATA [{r.tag}]: {r.error}")
else:
print(f" {r.tag} = {r.value} ({r.type})")
# Array dilimi: SensorArray[0]{8} → ilk 8 eleman
sensors = plc.read('SensorArray[0]{8}')
print(f"Sensörler: {sensors.value}") # [12.3, 14.1, ...]
# UDT (User Defined Type) okuma
motor = plc.read('MotorData')
# motor.value örn: {'Speed': 1498.5, 'Current': 4.2,
# 'Fault': False, 'Name': 'Pompa1'}
print(f"UDT Motor: {motor.value}")
# Program scope tag: Program:ProgramAdı.TagAdı
p_temp = plc.read('Program:HavuzKontrol.SetNoktasi')
print(f"Program tag: {p_temp.value}")
Tag yazma
from pycomm3 import LogixDriver
with LogixDriver('192.168.1.1/0') as plc:
# Tek tag yaz — tuple (tag_adı, değer)
r = plc.write(('Motor1.SpeedRef', 1500.0))
print("SpeedRef yazıldı:", r.error or "OK")
# Çoklu yazma — tek CIP paketiyle (verimli)
results = plc.write(
('Motor1.SpeedRef', 1500.0), # REAL
('Motor1.RunCmd', True), # BOOL
('Conveyor.TargetPos', 3500), # DINT
('System.Message', "Hazir"), # STRING (82 char max)
)
for r in results:
status = "OK" if r.error is None else f"HATA: {r.error}"
print(f" {r.tag}: {status}")
# Array yazma
plc.write(('RecipeArray[0]{5}', [100.0, 95.0, 88.0, 92.0, 97.0]))
PLC tipine göre farklılıklar
| PLC Ailesi | Driver | Sembolik Tag | Not |
|---|---|---|---|
| ControlLogix / CompactLogix (Logix5000) | LogixDriver | Evet | Önerilen; tam UDT ve array desteği |
| Micro800 (Micro820/850) | LogixDriver | Evet | Sınırlı tag listesi; büyük UDT desteği yok |
| PLC5 (eski) | LogixDriver (PCCC modu) | Hayır | N7:0, F8:0 gibi dosya adresleri kullanılır |
| SLC500 (eski) | LogixDriver (PCCC modu) | Hayır | PCCC protokolü, EtherNet/IP encapsulation üzerinde |
pycomm3'ün batch read/write özelliği (tek çağrıda birden fazla tag), ayrı ayrı çağrılara göre önemli ölçüde daha verimlidir. 10 tag için ayrı ayrı 10 TCP istek yerine tek bir isteğe yaklaşık 1-2 ms yeterlidir. Ağ gecikmesi kritikse çağrıları batch yapın. Öte yandan tek pakete sığmayan büyük batchler otomatik olarak fragmentlenir.
07 Pratik: CompactLogix'ten sıcaklık okuma ve CSV loglama
Bu bölümde Allen-Bradley CompactLogix PLC'den sıcaklık ve proses tag'lerini periyodik olarak okuyup CSV dosyasına kaydeden, yeniden bağlanma mantığı ve alarm kontrolü içeren eksiksiz bir izleme uygulaması geliştirilir. Uygulama systemd servisi olarak arka planda çalışabilir; CSV verisi Grafana ile görselleştirilebilir.
Sistem mimarisi
Allen-Bradley CompactLogix
192.168.1.1 Slot 0
Studio 5000 PLC Programı
│
│ EtherNet/IP (TCP 44818)
│ CIP Read_Tag (0x4C) — sembolik tag
│
Linux PC / Raspberry Pi
pycomm3 → plc_logger.py
│
├─► stdout (terminal ekranı, gerçek zamanlı)
├─► /var/log/plc/plc_data.csv (CSV dosyası)
└─► Prometheus Pushgateway (opsiyonel metrik)
│
└─► Grafana Dashboard
#!/usr/bin/env python3
"""
plc_logger.py — Allen-Bradley CompactLogix sıcaklık ve proses tag izleme
Gereksinim : pip install pycomm3
Kullanım : python3 plc_logger.py --ip 192.168.1.1 --slot 0 --interval 1.0
"""
import csv
import time
import signal
import logging
import argparse
from datetime import datetime
from pathlib import Path
from pycomm3 import LogixDriver, RequestError, CommError
# ── Loglama yapılandırması ───────────────────────────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
log = logging.getLogger("plc_logger")
# ── İzlenecek tag listesi ────────────────────────────────────────────────────
TAGS = [
"Firin.Sicaklik_PV", # REAL — fırın proses değeri (°C)
"Firin.Sicaklik_SP", # REAL — fırın set noktası (°C)
"Firin.Isitici_Cikis", # REAL — ısıtıcı çıkış yüzdesi (%)
"Kazan.Basinc_PV", # REAL — kazan basıncı (bar)
"Kazan.Sicaklik_PV", # REAL — kazan sıcaklığı (°C)
"Pompa1.Hiz", # REAL — pompa hızı (RPM)
"Pompa1.Akis", # REAL — debi (L/dak)
"Pompa1.Calisiyor", # BOOL — çalışma durumu
"Sistem.UretimSayaci", # DINT — üretim döngü sayacı
"Sistem.HataKodu", # DINT — aktif hata kodu (0 = yok)
]
# ── Alarm eşikleri: {tag: (min, max)} ───────────────────────────────────────
ALARMLAR = {
"Firin.Sicaklik_PV": (50.0, 250.0), # 50 °C altı / 250 °C üstü
"Kazan.Basinc_PV": (0.5, 12.0), # 0.5 bar altı / 12 bar üstü
"Kazan.Sicaklik_PV": (20.0, 180.0),
"Pompa1.Hiz": (0.0, 3600.0),
"Pompa1.Akis": (2.0, 150.0),
}
_running = True # SIGINT / SIGTERM ile False yapılır
def _stop(sig, frame):
global _running
log.info("Durdurma sinyali alındı, kapatılıyor...")
_running = False
def alarm_kontrol(tag: str, deger) -> str | None:
"""Değeri eşiklerle karşılaştırır; alarm metni veya None döndürür."""
if tag not in ALARMLAR or deger is None:
return None
try:
v = float(deger)
lo, hi = ALARMLAR[tag]
if v < lo:
return f"DUSUK({v:.2f}<{lo})"
if v > hi:
return f"YUKSEK({v:.2f}>{hi})"
except (TypeError, ValueError):
pass
return None
def toplu_oku(plc: LogixDriver) -> dict:
"""Tüm tag'leri tek bir CIP isteğiyle oku."""
sonuclar: dict = {}
try:
okumalar = plc.read(*TAGS)
if not isinstance(okumalar, (list, tuple)):
okumalar = [okumalar]
for r in okumalar:
sonuclar[r.tag] = r.value if r.error is None else None
if r.error:
log.warning("Tag hatası [%s]: %s", r.tag, r.error)
except (RequestError, CommError) as e:
log.error("CIP iletişim hatası: %s", e)
return sonuclar
def csv_hazirla(yol: Path) -> tuple:
"""CSV dosyasını oluştur/aç; writer ve file nesnelerini döndür."""
sutunlar = ["zaman"] + TAGS + ["alarmlar"]
yeni = not yol.exists()
f = yol.open("a", newline="", encoding="utf-8")
writer = csv.DictWriter(f, fieldnames=sutunlar)
if yeni:
writer.writeheader()
log.info("CSV oluşturuldu: %s", yol)
return writer, f
def main() -> None:
ap = argparse.ArgumentParser(description="CompactLogix PLC Tag Logger")
ap.add_argument("--ip", default="192.168.1.1",
help="PLC IP adresi")
ap.add_argument("--slot", type=int, default=0,
help="Backplane slot numarası (CompactLogix=0)")
ap.add_argument("--interval", type=float, default=1.0,
help="Örnekleme periyodu (saniye, varsayılan: 1.0)")
ap.add_argument("--output", default="/var/log/plc/plc_data.csv",
help="CSV çıkış dosyası yolu")
ap.add_argument("--prometheus-gw", default="",
help="Prometheus Pushgateway URL (opsiyonel)")
args = ap.parse_args()
signal.signal(signal.SIGINT, _stop)
signal.signal(signal.SIGTERM, _stop)
csv_yol = Path(args.output)
csv_yol.parent.mkdir(parents=True, exist_ok=True)
writer, csv_dosya = csv_hazirla(csv_yol)
plc_adres = f"{args.ip}/{args.slot}"
yeniden_gecikme = 5.0
plc: LogixDriver | None = None
log.info("Başlıyor PLC=%s interval=%.2f s csv=%s",
plc_adres, args.interval, csv_yol)
try:
while _running:
t0 = time.monotonic()
# ── Bağlantı kur / yeniden bağlan ─────────────────────────────
if plc is None:
try:
plc = LogixDriver(plc_adres, init_tags=True, init_info=True)
plc.open()
bilgi = plc.info
log.info("Bağlandı: %s Rev=%s S/N=%s",
bilgi.get("product_name", "?"),
bilgi.get("revision", "?"),
bilgi.get("serial", "?"))
except (CommError, Exception) as e:
log.error("Bağlantı kurulamadı: %s — %.0f s sonra tekrar",
e, yeniden_gecikme)
plc = None
time.sleep(yeniden_gecikme)
continue
# ── Tag'leri oku ───────────────────────────────────────────────
veri = toplu_oku(plc)
if not veri:
log.warning("Veri yok, bağlantı yenileniyor...")
try:
plc.close()
except Exception:
pass
plc = None
time.sleep(yeniden_gecikme)
continue
# ── Alarm kontrolü ─────────────────────────────────────────────
aktif_alarmlar = []
for tag_adi, deger in veri.items():
mesaj = alarm_kontrol(tag_adi, deger)
if mesaj:
aktif_alarmlar.append(f"{tag_adi}:{mesaj}")
log.warning("ALARM %s %s", tag_adi, mesaj)
# ── Ekran çıktısı ──────────────────────────────────────────────
simdi = datetime.now()
print(f"\n{'─'*58}")
print(f" {simdi:%Y-%m-%d %H:%M:%S} | {len(aktif_alarmlar)} alarm")
print(f"{'─'*58}")
for tag_adi in TAGS:
deger = veri.get(tag_adi)
if isinstance(deger, float):
val_str = f"{deger:>10.3f}"
elif deger is None:
val_str = f"{'N/A':>10}"
else:
val_str = f"{str(deger):>10}"
alarm_str = " [!]" if any(tag_adi in a
for a in aktif_alarmlar) else ""
print(f" {tag_adi:<32} {val_str}{alarm_str}")
# ── CSV kaydı ──────────────────────────────────────────────────
satir = {"zaman": simdi.isoformat(),
"alarmlar": "|".join(aktif_alarmlar)}
satir.update({t: veri.get(t, "") for t in TAGS})
writer.writerow(satir)
csv_dosya.flush()
# ── Periyot bekle ──────────────────────────────────────────────
gecen = time.monotonic() - t0
bekle = args.interval - gecen
if bekle > 0:
time.sleep(bekle)
else:
log.warning("Döngü süresi aşıldı: %.3f s", gecen)
finally:
csv_dosya.close()
if plc is not None:
try:
plc.close()
except Exception:
pass
log.info("Logger durduruldu. CSV: %s", csv_yol)
if __name__ == "__main__":
main()
Systemd servisi olarak çalıştırma
# Scripti kopyala
sudo cp plc_logger.py /opt/plc_logger.py
sudo chmod +x /opt/plc_logger.py
# Servis dosyası oluştur
sudo tee /etc/systemd/system/plc-logger.service <<'EOF'
[Unit]
Description=Allen-Bradley CompactLogix Tag Logger
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=pi
ExecStart=/usr/bin/python3 /opt/plc_logger.py \
--ip 192.168.1.1 \
--slot 0 \
--interval 1.0 \
--output /var/log/plc/plc_data.csv
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now plc-logger
# Durum kontrol
sudo systemctl status plc-logger
journalctl -u plc-logger -f # canlı log izleme
Pandas ile CSV analizi
import pandas as pd
import matplotlib.pyplot as plt
# CSV yükle
df = pd.read_csv(
"/var/log/plc/plc_data.csv",
index_col="zaman",
parse_dates=True,
)
# Son 1 saatin verisi
son_1_saat = df.last("1H")
# Temel istatistikler
print(son_1_saat[["Firin.Sicaklik_PV", "Kazan.Basinc_PV"]].describe())
# Alarm sayısı
alarm_satirlari = df[df["alarmlar"] != ""]
print(f"Toplam alarm: {len(alarm_satirlari)}")
# Sıcaklık grafiği
son_1_saat[["Firin.Sicaklik_PV", "Firin.Sicaklik_SP"]].plot(
title="Fırın Sıcaklığı — Son 1 Saat",
ylabel="Sıcaklık (°C)",
figsize=(12, 4),
)
plt.tight_layout()
plt.savefig("/tmp/firin_sicaklik.png", dpi=150)
Grafana entegrasyonu
CSV verilerini Grafana'da görselleştirmenin en kolay yolu Grafana'nın CSV datasource eklentisidir. Alternatif olarak pycomm3 verilerini doğrudan InfluxDB veya Prometheus'a iletebilirsiniz.
pycomm3 → plc_logger.py
│
├── CSV → Grafana CSV Plugin (dosya tabanlı, basit)
│
├── InfluxDB line protocol (HTTP POST)
│ └── Grafana InfluxDB datasource
│
└── Prometheus Pushgateway (push metrik)
└── Prometheus scrape → Grafana
pycomm3'ün plc.read(*TAGS) batch sözdizimi, tüm tag'leri tek bir CIP isteğinde gönderir ve yanıtları tek seferde alır. 10 tag için ayrı ayrı read çağrıları yaklaşık 10-15 ms sürerken batch okuma genellikle 1-3 ms içinde tamamlanır. Yüksek frekanslı loglama (100 ms altı) planlanıyorsa bu fark belirleyici olur.