Endüstriyel Ethernet
TEKNİK REHBER ENDÜSTRİYEL ETHERNET ETHERNET/IP 2026

EtherNet/IP
CIP üzerine endüstriyel Ethernet.

ODVA standardı EtherNet/IP: ControlNet ve DeviceNet mirasından standart Ethernet'e. CIP object modeli, implicit/explicit messaging ve pycomm3 ile Allen-Bradley PLC entegrasyonu.

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.

Taşıma katmanıExplicit messaging (parametre/konfigürasyon) için TCP port 44818; Implicit I/O messaging için UDP port 2222. Standart IP altyapısı üzerinde çalışır, VLAN ve yönlendirme desteğiyle büyük ağlara ölçeklenir.
Encapsulation protokolüCIP mesajları "EtherNet/IP Encapsulation" başlığına sarılır. 24 byte'lık encapsulation header: komut kodu (Command), uzunluk (Length), oturum tanımlayıcı (Session Handle), durum kodu (Status) ve gönderici bağlam (Sender Context) alanlarından oluşur.
ODVA uyumluluk testiEtherNet/IP logosu taşıyan ürünler ODVA onaylı konformans testinden geçmek zorundadır. Bu sayede farklı üreticilerin cihazları birlikte çalışabilir. Yazılım yığınları da ayrıca test edilebilir.
Diğer CIP ağlarıDeviceNet (ISO 11898 CAN, 500 kbps, 64 düğüm), ControlNet (çift koaksiyel, 5 Mbps, deterministik), CompoNet (RS-485, 4 Mbps, 384 düğüm). Hepsi aynı CIP uygulama katmanını paylaşır.
NOT

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 IDZorunluGörev
Identity Object0x01EvetVendor ID, Device Type, Product Code, Major/Minor Revision, Serial Number, Product Name — cihazın kimliği
Message Router0x02EvetGelen CIP mesajlarını ilgili nesneye yönlendiren dahili dispatcher
Assembly Object0x04Profile bağımlıBirden fazla attribute'i tek veri bloğunda birleştirir; I/O verisi Assembly üzerinden taşınır
Connection Manager0x06EvetI/O ve explicit bağlantıların kurulumu, izlenmesi ve kapatılması
Parameter Object0x0FHayırCihaz parametrelerini yapılandırılmış şekilde sunar; mühendislik araçları bu nesneyi okur
TCP/IP Interface0xF5EvetIP adresi, subnet mask, default gateway, hostname ve DNS konfigürasyonu
Ethernet Link0xF6EvetMAC adresi, hız/duplex müzakeresi, RX/TX istatistikleri, autoneg durumu
Port Object0xF4EvetCIP port tanımı (EtherNet/IP = port 2); multi-port cihazlar için birden fazla örnek
Vendor-Specific0x64–0xC7Hayı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:

0x01 — Get_Attributes_AllNesnenin tüm attribute'lerini tek pakette döndürür. Hızlı anlık görüntü almak için kullanılır.
0x02 — Set_Attributes_AllTüm yazılabilir attribute'leri tek pakette günceller. Parametre yükleme sırasında kullanılır.
0x0E — Get_Attribute_SingleBelirli bir attribute'i okur. Attribute numarası path'te belirtilir.
0x10 — Set_Attribute_SingleBelirli bir attribute'i yazar. Sadece yazılabilir attribute'lerde geçerlidir.
0x4C/0x4D — Read_Tag / Write_TagRockwell'e özgü sembolik servisler. Logix PLC'lerde doğrudan tag adıyla erişim sağlar.
0x4E/0x4F — Read_Tag_Fragmented / Write_Tag_FragmentedBüyük dizileri (array) birden fazla pakette parçalı aktarmak için. Tek pakete sığmayan veriler için gereklidir.

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
NOT

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

RPI — Requested Packet IntervalDöngüsel güncelleme periyodu (mikrosaniye cinsinden). Örn: 10000 = 10 ms. Gönderici bu periyotta paket üretir. Alıcı taraf, 4 × RPI süre boyunca paket gelmezse bağlantıyı zaman aşımına uğratır (Connection Timeout Multiplier).
O→T (Originator to Target)Bağlantıyı başlatan (genellikle PLC/scanner) tarafından hedef cihaza (drive, I/O modül) giden veri yönü. PLC çıkışları, setpoint ve komutlar bu yönde akar.
T→O (Target to Originator)Hedef cihazdan başlatana dönen veri yönü. Gerçek değerler (akım, konum, sıcaklık) ve durum bitleri bu yönde gelir.
Connection PointAssembly nesnesi örnek numarası. Geleneksel: 0x64 (100) = Output Assembly (O→T), 0x65 (101) = Input Assembly (T→O), 0x66 (102) = Configuration Assembly. EDS dosyasında tanımlanır.
Transport ClassClass 1: döngüsel I/O (sequenced, acknowledge yok). Class 0: en basit, sıra numarası da yok. Class 3: explicit connected messaging.
Bağlantı türüExclusive Owner: tek kontrolcü, yazma+okuma. Input Only: sadece T→O veri okur, O→T yok. Listen Only: başka bir Exclusive Owner bağlantısı varken dinleyici olarak çalışır.
Multicast vs UnicastT→O yönünde multicast seçilirse, aynı I/O verisi birden fazla scanner'a aynı anda iletilebilir. IGMP snooping destekleyen akıllı switch gerektirir, aksi halde tüm porta flood olur.

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

UCMM — Unconnected Message ManagerTCP bağlantısı kurulduktan sonra her mesaj için ayrı bir CIP bağlantısı açılmaz; mesaj doğrudan hedef cihazın Message Router'ına gönderilir. Tek seferlik sorgularda uygundur ancak her mesaj için full CIP header taşınır.
Class 3 — Connected ExplicitÖnce ForwardOpen ile Class 3 bağlantı kurulur, ardından bu bağlantı ID'si üzerinden çok sayıda mesaj gönderilir. Yüksek frekanslı parametre okumada daha verimlidir; oturum overhead'i ortadan kalkar.
Large Forward OpenStandart ForwardOpen maksimum 508 byte veri boyutunu destekler. Large Forward Open (servis kodu 0x5B) ile O→T ve T→O boyutları 65535 byte'a kadar çıkarılabilir. Büyük firmware güncelleme paketleri veya geniş I/O modülleri için gereklidir.

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('
NOT

Ü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

[File]EDS dosyasının meta verileri: oluşturma tarihi, revizyon, açıklama metni ve üretici web adresi.
[Device]Cihaz kimlik bilgileri: VendorCode, ProdType, ProdCode, MajRev, MinRev, ProdName. Bu değerler Identity Object (0x01) ile eşleşmelidir; uyumsuzluk durumunda araçlar cihazı tanımayabilir.
[Params]Parametre tanımları: veri tipi, min/max/default değer, birim, parametre adı ve CIP yolu. EDS Wizard bu bölümü cihazdan sorgulanarak otomatik doldurabilir.
[Assembly]I/O Assembly örnekleri ve boyutları. Her assembly, içerdiği parametre referanslarını listeler.
[Connection Manager]Desteklenen I/O bağlantı profilleri. Her bağlantı için O→T / T→O boyutu, RPI aralığı, bağlantı tipi ve assembly path'leri tanımlanır.
$ 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
İPUCU

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üphaneRolDilLisansDurumNot
OpENerAdaptör (Device)CBSD 3-ClauseAktifODVA üyesi desteği, POSIX + FreeRTOS portu
pycomm3Scanner (Client)PythonMITAktifRockwell Logix odaklı, en popüler Python çözümü
cpppoScanner + AdaptörPython/C++GPL-3.0AktifGenel CIP, EtherNet/IP + DF1 desteği
CIPsterAdaptörC++Apache 2.0PasifOpENer'ın C++ yeniden yazımı
libethernetipScannerCLGPLPasifHafif, 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

Yol (path)"IP/slot" formatı. Slot numarası backplane slotunu belirtir: 0 = CPU ile aynı kart (CompactLogix), 1-15 = ControlLogix kasasındaki modül slotu. Örn: "192.168.1.1/0" veya "192.168.1.1/1".
init_tagsTrue (varsayılan) olduğunda bağlantı sırasında tüm controller ve program tag listesi sorgulanır. Büyük PLC programlarında bu işlem birkaç saniye sürebilir; devre dışı bırakmak için init_tags=False kullanılır.
init_infoTrue olduğunda bağlantı sırasında PLC kimlik bilgileri (product_name, revision, serial) okunur.

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 AilesiDriverSembolik TagNot
ControlLogix / CompactLogix (Logix5000)LogixDriverEvetÖnerilen; tam UDT ve array desteği
Micro800 (Micro820/850)LogixDriverEvetSınırlı tag listesi; büyük UDT desteği yok
PLC5 (eski)LogixDriver (PCCC modu)HayırN7:0, F8:0 gibi dosya adresleri kullanılır
SLC500 (eski)LogixDriver (PCCC modu)HayırPCCC protokolü, EtherNet/IP encapsulation üzerinde
NOT

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
İPUCU

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.