1. What is PLC?
A programmable logic controller (PLC) is an industrial digital computer which has been ruggedized and adapted for the control of manufacturing processes, such as assembly lines, or robotic devices, or any activity that requires high reliability, ease of programming and process fault diagnosis.
A PLC consists of two basic sections: the central processing unit (CPU) and the input/output interface system. The CPU controls all PLC activity, and the input/output system is physically connected to field devices (e.g., switches, sensors, etc.).
We use the Siemens "S7-200 SMART PLC" as an example.
2. How to communicate with PLC?
Siemens PLC communication protocol
- Serial port protocols:
- MODBUS RTU
- PROFIBUS
- USS
- PPI
- MPI
- Ethernet protocols:
- Modbus TCP/IP
- OPC
- ISO-on-TCP
- UDP
- PROFINET
- S7 protocol
We are used to using s7 network protocol to communicate with Siemens PLC, because we can easily use the s7 library through Python or golang.
3. How to access the data of PLC?
The S7-200 stores information in different memory locations that have unique addresses.
To access a bit in a memory area, you specify the address, which includes the memory area identifier, the byte address, and the bit number.
The following figure shows an example of accessing a bit. In this example, the memory area and byte address(I = input, and 3 = byte 3) are followed by a period (“.”) to separate the bit address (bit 4).
Accessing Data in the Memory Areas:
- Process-Image Input Register: I
- Process-Image Output Register: Q
- Variable Memory Area: V
- Bit Memory Area: M
- Timer Memory Area: T
- Counter Memory Area: C
- High-Speed Counters: HC
- Accumulators: AC
- ......
You can access data in most memory areas (V, I, Q, M, S, L, and SM) as bytes, words, or double words by using the byte-address format.
4. Build linux snap7 dynamic library
If the host device needs to use c or golang language to connect and access data with PLC through the s7 protocol, "snap7 dynamic library" needs to be built and installed firstly.
Taking the x86 architecture host as an example, the following shows how to build and install the linux snap7 dynamic library.
# install dependence
sudo apt install -y make make-guile build-essential p7zip
# download source from https://sourceforge.net/projects/snap7/files/1.4.0/snap7-full-1.4.0.7z/download
# build and install
p7zip -d snap7-full-1.4.0.7z
cd snap7-full-1.4.0/build/unix
make -f x86_64_linux.mk
sudo cp ../bin/x86_64-linux/libsnap7.so /usr/lib
sudo cp ../bin/x86_64-linux/libsnap7.so /usr/local/lib/
sudo ldconfig
The library provides the following API functions to access PLC:
.....
bool Connected();
// Async functions
int SetAsCallback(pfn_CliCompletion pCompletion, void *usrPtr);
bool CheckAsCompletion(int *opResult);
int WaitAsCompletion(longword Timeout);
int AsReadArea(int Area, int DBNumber, int Start, int Amount, int WordLen, void *pUsrData);
int AsWriteArea(int Area, int DBNumber, int Start, int Amount, int WordLen, void *pUsrData);
int AsListBlocksOfType(int BlockType, PS7BlocksOfType pUsrData, int *ItemsCount);
int AsReadSZL(int ID, int Index, PS7SZL pUsrData, int *Size);
int AsReadSZLList(PS7SZLList pUsrData, int *ItemsCount);
int AsUpload(int BlockType, int BlockNum, void *pUsrData, int *Size);
int AsFullUpload(int BlockType, int BlockNum, void *pUsrData, int *Size);
int AsDownload(int BlockNum, void *pUsrData, int Size);
int AsCopyRamToRom(int Timeout);
int AsCompress(int Timeout);
int AsDBRead(int DBNumber, int Start, int Size, void *pUsrData);
int AsDBWrite(int DBNumber, int Start, int Size, void *pUsrData);
int AsMBRead(int Start, int Size, void *pUsrData);
int AsMBWrite(int Start, int Size, void *pUsrData);
.....
5. Project introduction for edgex device service "device-plc"
This Device Service is a reference example of a Device Service developed with Go Device Service SDK.
- Project documents description
$ tree device-plc/
device-plc/
├── cmd
│ └── device-service
│ ├── main.go
│ └── res
│ ├── configuration.toml # configuration file for consul
│ ├── docker
│ │ └── configuration.toml # configuration file for consul via docker
│ └── PLCProfile.yaml # REST API definition profile for PLC device
├── docker-compose-x86.yml # docker-compose file for x86
├── docker-compose.yml # docker-compose file for aarch64
├── Dockerfile # dependency to construct a docker image
├── go.mod
├── go.sum
├── internal
│ ├── driver
│ │ ├── driver.go # edgex driver
│ │ └── plc
│ │ └── plc.go # plc device driver
│ └── mod # dependency of edgex device service
│ ├── device-sdk-go
│ ├── go-mod-core-contracts
│ └── go-mod-registry
├── libsnap7.so # snap7 dynamic library for aarch64
├── libsnap7-x86.so # snap7 dynamic library for x86
├── Makefile # used for build golang project or construct a docker image
└── README.md
- PLC driver: plc.go
PLC driver communicates with PLC device by s7 dynamic library.
package plc
import (
"github.com/danclive/snap7-go"
......
)
type PLC struct {
lc logger.LoggingClient // usage for logging
connected bool //plc connection status
ip string //plc ip address
data string
client snap7.Snap7Client
}
func NewPLC(lc logger.LoggingClient) *PLC {
return &PLC{
lc: lc,
connected: false,
ip: "",
}
}
......
// connect the PLC via ip address
func (r *PLC) ConnectPLC(hostip string) (string, error) {
if r.connected {
r.lc.Info("already connected")
return "connected", nil
}
//Determine ip address
address := net.ParseIP(hostip)
if address == nil {
var rt = errors.New("error ip ")
return "fail", rt
}
Client, err:= snap7.ConnentTo2(hostip, 0x0200, 0x0200, 0)
if err == nil {
r.client = Client
r.connected = true
r.ip = hostip
return "ok", err
} else {
return "fail", err
}
}
// close PLC connection
func (r *PLC) ClosePLC() (string, error) {
if !r.connected {
r.lc.Info("already closed")
return "closed", nil
}
r.client.Close()
r.connected = false
return "ok", nil
}
// Accessing Data in the Memory Area
func (r *PLC) ReadPLC(area int, db_number int, start int, amount int) (string, error) {
if !r.connected {
var rt = errors.New("no connection")
r.lc.Info("no connection")
return "", rt
}
out, err := r.client.ReadArea(area,db_number,start,amount)
if err == nil {
encodedStr := hex.EncodeToString(out)
r.data = encodedStr
return encodedStr, nil
} else {
return "", err
}
}
......
- Edgex driver: driver.go
The driver.go implements the edgex-core-command REST API callback function, such as "Initialize", HandleReadCommands" and "HandleWriteCommands". According to different commands, they can access specific device driver interface and return the sensor data.
func (s *Driver) HandleReadCommands(deviceName string, protocols map[string]contract.ProtocolProperties, reqs []dsModels.CommandRequest) (res []*dsModels.CommandValue, err error) {
s.lc.Debug(fmt.Sprintf("protocols: %v resource: %v attributes: %v", protocols, reqs[0].DeviceResourceName, reqs[0].Attributes))
now := time.Now().UnixNano()
for _, req := range reqs {
switch req.DeviceResourceName {
case "connect":
{
out, err := s.Plc.GetPLCInfo()
if err != nil {
return res, err
}
cv := dsModels.NewStringValue(reqs[0].DeviceResourceName, now, out)
res = append(res, cv)
}
......
}
}
return res, nil
}
func (s *Driver) HandleWriteCommands(deviceName string, protocols map[string]contract.ProtocolProperties, reqs []dsModels.CommandRequest,
params []*dsModels.CommandValue) error {
s.lc.Info(fmt.Sprintf("Driver.HandleWriteCommands: protocols: %v, resource: %v, parameters: %v", protocols, reqs[0].DeviceResourceName, params))
for _, param := range params {
switch param.DeviceResourceName {
case "close":
{
_, err := s.Plc.ClosePLC()
if err != nil {
return err
}
}
......
}
}
return nil
}
- REST API interface from PLCProfile.yaml
Method | Core Command | parameters | Description | Response |
---|---|---|---|---|
put | connect | "connect":[plc_ip] | connect to PLC device via ip address | 200 ok |
get | connect | get connection status | "host ip:[plc_ip], connected:[status]" | |
put | close | "close": "" | disconnect current connection | 200 ok |
put | preparedata | "preparedata":[parameter] | accessing data in the memory area [parameter]: type string,format likes "area,db_number,start,amount" |
200 ok |
get | register | get the return data of accessing memory area | "preparedata":", |
6. Compile project and get docker image
We use Makefile file to build and compile the golang project.
We firstly compile and get the go binary file "device-service", then copy it and snap7 dynamic library "libsnap7-x86.so" to target path.
# Makefile
BUILDDIR=build/
build-arm64: GOARCH=arm64
build-arm64: device-service
device-service: | $(BUILDDIR)
GOARCH=$(GOARCH) $(GO) build $(GOFLAGS) -o $(BUILDDIR)/bin/$@-$(GOARCH) ./cmd/device-service
cp -r cmd/device-service/res $(BUILDDIR)/bin/
cp libsnap7-x86.so $(BUILDDIR)/libsnap7.so
Build docker image by Dockerfile, then we will get the device-plc docker image.
# Dockerfile
FROM ubuntu:16.04
ARG GOARCH
ENV APP_PORT=80
EXPOSE $APP_PORT
WORKDIR /app
COPY bin/device-service-$GOARCH /app/device-service
COPY bin/res /app/res
COPY bin/res/docker/configuration.toml /app/res/configuration.toml
COPY CHANGELOG.md /app
COPY libsnap7.so /usr/lib/
ENTRYPOINT [ "/app/device-service"]
# Makefile
docker-arm64: GOARCH=arm64
docker-arm64: ARCHTAG=arm64v8
docker-arm64: docker
docker: build
docker build \
--label "git_sha=$(GIT_SHA)" \
--build-arg GOARCH=$(GOARCH) \
-f Dockerfile \
-t $(DOCKER_IMAGE_NAME):$(GIT_SHA) \
-t $(DOCKER_IMAGE_NAME):$(ARCHTAG)-cpu-$(VERSION) \
-t $(DOCKER_IMAGE_NAME):$(ARCHTAG)-cpu-laplc \
build
7. Use edgex device service
Startup the device-plc docker image by docker-compose.yml file.
$ docker-compose up
User can access the REST API interface of device-plc by HTTP client.
# 1. get device service attribute interface URL
GET: http://localhost:48082/api/v1/device/name/plc
# 2. connect the PLC device
# Body json
{
"connect":"192.168.1.12"
}
# 3. accessing data in the memory areas
# Body json
{
"preparedata":"0x82,0x00,0x00,0x01"
}
......