Building a SCADA System with grpc Microservices: A Modern Approach to Industrial Automation

Let’s dive in and unravel the mystery of SCADA applications, exploring how they are emerging and evolving from traditional monolithic architectures to modern microservices, transforming industrial automation with greater flexibility, scalability, and efficiency.

What is a Monolithic Application and what are Microservices ?

    Monolithic Application

    A monolithic app is a single, unified software application where all components are tightly coupled and run as a single process. In this architecture, the entire application (user interface, business logic, data access layer, etc.) is developed, deployed, and maintained as one cohesive unit.

    Microservices

    A microservices architecture breaks down an application into smaller, independent services that communicate with each other, typically over APIs like gRPC or REST. Each service handles a specific functionality of the application (e.g., user management, payment processing) and can be developed, deployed, and maintained independently.

gRPC microservices vs. a monolithic SCADA system

  1. Scalability
  2. In a microservices architecture, each service (such as data acquisition, control, visualization, etc.) operates independently. This allows for horizontal scaling, meaning you can scale individual services based on load (e.g., more data collection nodes) without impacting the rest of the system. A monolithic SCADA system scales vertically—adding more resources (e.g., CPU, memory) to a single system, which becomes more difficult and costly as the system grows in complexity.

  3. Flexibility & Modularity
  4. With gRPC, you can develop, deploy, and update each service independently. This means you can enhance or replace individual components without affecting the entire SCADA system, allowing for greater flexibility and continuous improvements. Monolithic systems are tightly coupled, meaning changes to one part of the system can impact the entire solution. This makes upgrades, testing, and debugging more difficult and risk-prone.

  5. Performance
  6. gRPC utilizes HTTP/2 for high-performance, low-latency communication and Protocol Buffers for efficient serialization. This makes it faster and more efficient when handling large volumes of real-time data, crucial for SCADA systems where latency and performance are critical. Typically, monolithic systems use older communication protocols (e.g., OPC-DA), which can be less efficient in handling high-throughput, real-time data streams, especially as the system grows.

  7. Resilience & Fault Tolerance
  8. Since each microservice runs independently, a failure in one service (e.g., data logging) will not bring down the entire SCADA system. This makes microservice-based SCADA systems more resilient and fault-tolerant. A single point of failure in a monolithic SCADA system can cause the entire system to fail, leading to more downtime and less reliability.

  9. Ease of Maintenance & Updates
  10. You can update or deploy a specific microservice without needing to stop the entire SCADA system. This allows for zero-downtime deployments and easier maintenance routines. Maintenance in a monolithic SCADA system often requires downtime since changes affect the entire application. Updating the system becomes more complex as it grows, with a higher risk of introducing bugs.

  11. Technology Stack & Interoperability
  12. Microservices allow you to use different technology stacks (languages, databases) for different services, providing greater flexibility. gRPC’s support for multiple languages and platforms (e.g., Go, Python, Java, etc.) makes it easier to integrate with different systems. In a monolithic SCADA system, you are often locked into a specific technology stack, making it difficult to adopt new technologies or integrate with external systems.

  13. Real-Time Communication
  14. gRPC supports bidirectional streaming, allowing real-time data exchange between services. This is ideal for SCADA systems where data acquisition, control, and visualization must be updated in real-time. Real-time data exchange is often slower in monolithic systems due to older protocols and the lack of efficient streaming capabilities.

Modern SCADA system services

In a modern SCADA (Supervisory Control and Data Acquisition) system, several types of services could complement an OPC UA service, providing broader functionality for industrial control, monitoring, and data analysis. A well-architected SCADA system often uses multiple services to handle communication, processing, and storage of data from industrial devices.

Here are some common services that would likely be found in a modern SCADA system:

  1. Telemetry, Monitoring & Control Service
  2. Collects and aggregates real-time data from devices like sensors, PLCs, and remote equipment. Provides the ability to send commands to remote devices and actuators (e.g., open a valve, start/stop a pump).

  3. Alarm and Event Management Service
  4. Responsible for handling alarms and events in the system. It can trigger alarms when monitored data exceeds certain thresholds, and it logs events for analysis and audits.

  5. Data Historian Service
  6. Logs historical data (e.g., from sensors, devices) and allows querying for trends and reports.

  7. User Management and Access Control Service
  8. Manages user roles, authentication, and access control.

  9. Configuration Management Service
  10. Allows configuring devices, setting thresholds, and updating parameters of the system remotely

  11. Analytics and AI Service
  12. Provides predictive analytics, anomaly detection, and process optimization using machine learning models.

  13. Reporting and Visualization Service
  14. Generates reports and visualizes trends or system statuses. It could provide custom dashboards and charts based on system data, alarms, and historical records.

  15. Edge Computing and Gateway Services
  16. Provides predictive analytics, anomaly detection, and process optimization using machine learning models. Facilitates data collection from field devices and sensors. Edge services can filter, preprocess, or even compute data before sending it to the SCADA system.

  17. Health and Diagnostics Service
  18. Monitors the health of devices and the system itself, detecting issues such as network failures, sensor malfunctions, and connection problems.

Let’s build the first phase of the Telemetry service together

Firts things first. We need to develop a gRPC server , particularly in Golang, to read OPC UA tags. As an OPC UA server I am going to use the Prosys OPC UA Simulator but you can certainly use any PLC with OPC UA server capabilities like the popular Siemens S7-1500.

To create a gRPC server in Golang to serve OPC UA tags, you'll need to:

  1. Set up the gRPC server using Go's grpc package.
  2. Integrate an OPC UA client that communicates with the OPC UA server to fetch the tags. You can use an OPC UA library for Go, like gopcua.
  3. Serve the OPC UA tags via gRPC by implementing methods that return the tag values.

Here’s a step-by-step outline to get started:

  • Step 1: Install necessary packages
  • go get google.golang.org/grpc
    go get github.com/gopcua/opcua
    go get github.com/golang/protobuf/protoc-gen-go
    

  • Step 2: Define the .proto file
  • A protocol file (commonly called a .proto file) is a key component of Protocol Buffers (protobuf), a language-neutral, platform-neutral mechanism for serializing structured data. This file defines the structure of the data you want to serialize and exchange between services, such as between a gRPC server and a client. Protocol Buffers (protobuf) is a binary serialization format developed by Google, used for efficiently transmitting data between services or over networks. It's particularly useful for microservices, where fast and small data exchange is important.

    syntax = "proto3";
    
    package opcua;
    
    option go_package = "/proto";
    
    service OpcuaService {
    rpc GetTag (TagRequest) returns (TagResponse);
    }
    
    message TagRequest {
    uint32 tagName = 1;
    }
    
    message TagResponse {
    uint32 tagName = 1;
    string value = 2;
    string timestamp = 3;
    }
    
    

  • Step 3: Generate gRPC code
  • protoc --go_out=. --go-grpc_out=. path/to/your.proto
    

  • Step 4: Implement gRPC server and OPC UA integration
  • package main
    
    import (
    "context"
    "fmt"
    "github.com/gopcua/opcua"
    "github.com/gopcua/opcua/ua"
    "google.golang.org/grpc"
    "log"
    "net"
    pb "server/proto" // Import the generated proto file
    "strconv"
    )
    
    type server struct {
    pb.UnimplementedOpcuaServiceServer
    }
    
    func (s *server) GetTag(ctx context.Context, req *pb.TagRequest) (*pb.TagResponse, error) {
    // Create OPC UA client
    opcClient, _ := opcua.NewClient("opc.tcp://phoretonikolaos:53530/OPCUA/SimulationServer", opcua.SecurityPolicy(ua.SecurityPolicyURINone), opcua.SecurityMode(ua.MessageSecurityModeNone))
    err := opcClient.Connect(ctx)
    if err != nil {
    fmt.Println(err)
    } else {
    fmt.Println("Connected to server")
    }
    defer func(opcClient *opcua.Client, ctx context.Context) {
    err := opcClient.Close(ctx)
    if err != nil {
    }
    }(opcClient, ctx)
    
    // Read tag value
    
    nodeID := ua.NewNumericNodeID(3, uint32(req.TagName))
    readRequest := &ua.ReadRequest{
    NodesToRead: []*ua.ReadValueID{
    	{
    		NodeID:      nodeID,
    		AttributeID: ua.AttributeIDValue,
    	},
    },
    }
    
    readResponse, err := opcClient.Read(ctx, readRequest)
    if err != nil {
    fmt.Println("cannot read")
    }
    
    var value string
    switch v := readResponse.Results[0].Value.Value().(type) {
    case int:
    value = strconv.Itoa(readResponse.Results[0].Value.Value().(int))
    case float64:
    value = fmt.Sprintf("%f", readResponse.Results[0].Value.Value())
    fmt.Printf("Float64: %v\n", v)
    }
    
    timestamp := readResponse.Results[0].SourceTimestamp.String()
    
    // Return the tag data via gRPC response
    return &pb.TagResponse{
    TagName:   req.TagName,
    Value:     value,
    Timestamp: timestamp,
    }, nil
    }
    
    func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
    log.Fatalf("failed to listen: %v", err)
    }
    
    grpcServer := grpc.NewServer()
    pb.RegisterOpcuaServiceServer(grpcServer, &server{})
    log.Println("gRPC server started on :50051")
    if err := grpcServer.Serve(lis); err != nil {
    log.Fatalf("failed to serve: %v", err)
    }
    }
    
    

  • Step 5: Create a gRPC Client
  • package main
    
    import (
    	pb "client/proto"
    	"context"
    	"google.golang.org/grpc"
    	"google.golang.org/grpc/credentials/insecure"
    	"log"
    	"os"
    	"os/signal"
    )
    
    func main() {
    	// Set up a connection to the server.
    	conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
    	if err != nil {
    		log.Fatalf("did not connect: %v", err)
    	}
    	defer func(conn *grpc.ClientConn) {
    		err := conn.Close()
    		if err != nil {
    
    		}
    	}(conn)
    
    	// Create a new client from the generated gRPC code
    	client := pb.NewOpcuaServiceClient(conn)
    
    	// Set an interrupt cancellation
    	signalCh := make(chan os.Signal, 1)
    	signal.Notify(signalCh, os.Interrupt)
    
    	ctx, cancel := context.WithCancel(context.Background())
    	defer cancel()
    
    	go func() {
    		<-signalCh
    		println()
    		cancel()
    	}()
    
    	// Call the GetTag function with a specific tag name
    	tagName := 1003
    	req := &pb.TagRequest{TagName: uint32(tagName)}
    	res, err := client.GetTag(ctx, req)
    	if err != nil {
    		log.Fatalf("could not get tag: %v", err)
    	}
    
    	// Output the received tag value
    	log.Printf("Tag: %s, Value: %s, Timestamp: %s", res.TagName, res.Value, res.Timestamp)
    }
    
    

  • Step 6: Test the Client
  • Once the client connects successfully, you should see the output, something like:

    2024/09/09 22:20:49 Tag: 1003, Value: 1.659089, Timestamp: 2024-09-09 19:20:49 +0000 UTC
    

Types of gRPC Implementations

The implementation presented above is a unary gRPC implementation. In gRPC, a unary RPC is the simplest type of communication where:

  • The client sends a single request to the server.
  • The server processes the request and sends back a single response.

Besides unary, gRPC supports three other communication types:

  • Server Streaming: The client sends a single request, but the server returns a stream of responses.
  • Client Streaming: The client sends a stream of requests, and the server returns a single response.
  • Bidirectional Streaming: Both client and server send a stream of messages to each other.

C'est la fin

To experiment further with SCADA microservices, you can extend your gRPC server by adding additional functionality, such as OPC UA subscription and write operations. For example, you could implement a server-streaming gRPC method to handle subscriptions, allowing the client to receive real-time updates when OPC UA tag values change. Additionally, you could add write functionality to your service, enabling clients to modify tag values in the OPC UA server via gRPC.

Beyond that, you can explore building other microservices like alarm management, historical data logging, and control services to enhance the SCADA system's capabilities. Each service can communicate through gRPC, enabling modular and scalable system architecture. By experimenting with these different microservices, you can create a fully distributed and efficient industrial control system.

In conclusion, Building a SCADA System with gRPC Microservices: A Modern Approach to Industrial Automation marks a pivotal step toward revolutionizing industrial control systems. By embracing gRPC microservices, SCADA systems unlock a new era of scalability, agility, and real-time precision, far surpassing the constraints of traditional monolithic architectures. This journey doesn’t end here—it's the beginning of a transformative adventure, paving the way for innovation, resilience, and seamless integration in the world of SCADA microservices. The future of industrial automation is here, and it's more dynamic than ever.