In pvbrowser we use a separate daemon to connect to an individual hardware resource. The daemon reads input signals cyclic from the hardware and stores it in a shared memory. Also the daemon is waiting on a mailbox for output messages. Thus pvserver can easily communicate with the hardware. This can be illustrated by the following picture.
It is not necessary to write the daemon manually. Instead you can use a generator to build the daemon from a INI file. Within the generated files we heavily use rllib.
Using Siemens PLC's there are 2 different interfaces available. You may connect either using a serial interface (PPI) or via ethernet (ISO on TCP). We know PPI, which is a trivial serial line protocol. But we can't implement this because the Siemens License rules forbid this. Thus we use the reverse enigineered libnodave which also works well. For ISO on TCP we implemented the protocol in rllib using RFC 1006 and protocol description for fetch/write from Siemens But this works for S5, S7_400 and S7_300 only. For S7_200 we took libnodave. Only S7_200 connectivity could be tested up to now.
You have to write a small ini file for communicating with Siemens PLC's using the PPI protocol. Here is an example:
############################################ # Project file for generating ppi daemon # # mkppi name.mkppi # ############################################ shared_memory=/srv/automation/shm/ppi.shm mailbox=/srv/automation/mbx/ppi.mbx tty=/dev/ttyS0 baudrate=9600 idletime=10 milliseconds # area := # daveSD # daveInputs # daveOutputs # daveFlags # daveDB //data blocks # daveDI //not tested # daveLocal //not tested # daveV //don't know what it is # daveCounter //not tested # daveTimer //not tested # cycle := area, dbnum, start, len cycle1 slave=2 area=daveDB dbnum=1 start=0 len=64 cycle2 slave=2 area=daveDB dbnum=1 start=64 len=64 target=ppidaemon
Now you can build the daemon using the following command:
mkppi ppidaemon.mkppi g++ ppidaemon.cpp -o ppidaemon -I/usr/local/include/rllib -I/usr/local/include/libnodave -L/usr/lib/ /usr/lib/librllib.so /usr/lib/libpthread.so /usr/lib/libnodavePPI.so
The generated file (ppidaemon.cpp) looks as follows:
// // PPI daemon template (C) R. Lehrig 2004 // // // Attention: this program must be run as super user // because libnodave only supports unix, you can't use this program on windows #include#include #include "rlsharedmemory.h" #include "rlmailbox.h" #include "rlthread.h" #include "rlcutil.h" #include "setport.c" extern "C" { #include "nodavePPI.h" }; #define IDLETIME 10 #define SERIAL_DEVICE "/dev/ttyS0" #define BAUDRATE "9600" rlSharedMemory shm("/srv/automation/shm/ppi.shm",128); rlMailbox mbx("/srv/automation/mbx/ppi.mbx"); rlThread thread; rlThread watchdog; static daveConnection *dc[32+1]; static daveInterface *di; static _daveOSserialType fds; // watchdog static char *av0 = ""; static int watchcnt1 = 0; void *watchdogthread(void *arg) { int cnt1 = -1; while(1) { rlsleep(5000); if(cnt1 == watchcnt1) break; cnt1 = watchcnt1; } rlsleep(100); #ifdef unix rlexec(av0); #endif exit(0); // pcontrol may start me again if rlexec fails return arg; } // read mailbox and write to modbus void *reader(void *arg) { int ret,slave,area,dbnum,start,len,buflen; unsigned char buf[1024+1]; mbx.clear(); // clear old messages while((buflen = mbx.read(buf,sizeof(buf))) > 0) { slave = buf[0]; area = buf[1]; dbnum = buf[2]; start = buf[3]; len = buf[4]; if(len > 0 && len < 256 && slave >=0 && slave < 32) { thread.lock(); //int daveWriteBytes(daveConnection * dc,int area, int DBnum, int start,int len, void * buffer); ret = daveWriteBytes(dc[slave],area,dbnum,start,len,&buf[5]); thread.unlock(); if(ret!=0) printf("daveWriteBytes ret=%d\n",ret); } } return arg; } // read cycle on ppi int ppiCycle(int offset, daveConnection *dc, int area, int dbnum, int start, int len) { unsigned char data[1024+1]; int ret; if(len > 256) return -1; watchcnt1++; if(watchcnt1 > 10000) watchcnt1 = 0; thread.lock(); ret = daveReadBytes(dc,area,dbnum,start,len,data); thread.unlock(); if(ret == 0) shm.write(offset,data,len); if(ret != 0) printf("daveReadBytes ret=%d\n",ret); return len; } int main(int ac, char **av) { int offset,ret,adr; if(ac > 0) av0 = av[0]; fds.rfd=setPort(SERIAL_DEVICE,BAUDRATE,'E'); fds.wfd=fds.rfd; if(fds.rfd>0) { printf("\nstarting ppidaemon\n"); di = daveNewInterface(fds,"IF1",0,daveProtoPPI); for(adr=1; adr<32; adr++) dc[adr] = daveNewConnection(di,adr,0,0); thread.create(reader,NULL); watchdog.create(watchdogthread,NULL); while(1) { rlsleep(IDLETIME); offset = 0; // ppiCycle(offset, dc, area, dbnum, start, len); ret = ppiCycle(offset, dc[2], daveDB, 1, 0, 64); if(ret>0) offset += ret; else continue; ret = ppiCycle(offset, dc[2], daveDB, 1, 64, 64); if(ret>0) offset += ret; else continue; } } printf("\nerror opening SERIAL_DEVICE\n"); return 0; }
You write a small ini file like this.
################################################ # Project file for generating siemens daemon # # mksiemens name.mksiemens # ################################################ shared_memory=/srv/automation/shm/siemens.shm mailbox=/srv/automation/mbx/siemens.mbx # type := S7_200 | S7_300 | S7_400 | S5 slave=0 adr=192.168.1.101 type=S7_200 slave=1 adr=192.168.1.102 type=S7_200 idletime=50 milliseconds #eventlog host=localhost port=6000 # ORG := ORG_DB | ORG_M | ORG_E | ORG_A | ORG_PEPA | ORG_Z | ORG_T # cycle := slave + org + dbnum + start + len cycle1 slave=0 org=ORG_E dbnum=0 start=0 len=8 cycle2 slave=0 org=ORG_A dbnum=0 start=0 len=8 cycle3 slave=0 org=ORG_DB dbnum=0 start=0 len=32 target=siemensdaemon
Now you can generate your daemon. The result looks like this.
// // SiemensTCP daemon template (C) R. Lehrig 2004 // // // Attention: this program must be run as super user // #include#include #include "rlsiemenstcp.h" #include "rlsharedmemory.h" #include "rlmailbox.h" #include "rlthread.h" #include "rlcutil.h" #define CYCLE_TIME 50 rlSiemensTCP slave0("192.168.1.101",rlSiemensTCP::S7_200); rlSiemensTCP slave1("192.168.1.102",rlSiemensTCP::S7_200); rlSharedMemory shm("/srv/automation/shm/siemens.shm",48); rlMailbox mbx("/srv/automation/mbx/siemens.mbx"); rlThread thread; rlSiemensTCP *slave_array[256]; // read mailbox and write to siemensTCP void *reader(void *arg) { int buflen,slave,org,dbnr,start_adr,len; unsigned char buf[2048+8]; mbx.clear(); // clear old messages while((buflen = mbx.read(buf,sizeof(buf))) > 0) { slave = buf[0]; org = buf[1]; dbnr = buf[2]*256 + buf[3]; start_adr = buf[4]*256 + buf[5]; len = buf[6]*256 + buf[7]; if(slave >= 0 && slave < 256) { thread.lock(); slave_array[slave]->write(org,dbnr,start_adr,len,&buf[8]); thread.unlock(); } } return arg; } // read cycle on SiemensTCP int siemensTCPCycle(int slave, int offset, int org, int dbnr, int start_adr, int len) { unsigned char data[4096]; int ret; thread.lock(); ret = slave_array[slave]->fetch(org,dbnr,start_adr,len,data); thread.unlock(); if(ret > 0) shm.write(offset,data,ret); //printf("ret=%d\n",ret); return len; } int main() { int offset,ret; slave_array[0] = &slave0; slave_array[1] = &slave1; //rlSetDebugPrintf(1); // set debugging on thread.create(reader,NULL); while(1) { rlsleep(CYCLE_TIME); offset = 0; // SiemensTCP.Cycle.fetch(slave,offset,fetch(int org, int dbnr, int start_adr, int len); ret = siemensTCPCycle(0,offset, rlSiemensTCP::ORG_E, 0, 0, 8); if(ret>0) offset += ret; else continue; ret = siemensTCPCycle(0,offset, rlSiemensTCP::ORG_A, 0, 0, 8); if(ret>0) offset += ret; else continue; ret = siemensTCPCycle(0,offset, rlSiemensTCP::ORG_DB, 0, 0, 32); if(ret>0) offset += ret; else continue; } return 0; }