Tech Blog

Simulating IOT devices using Go

alekyha_circle

Simulating IOT devices using Go

Overview

Often IoT or embedded software developers need to start developing and testing code before they have access to the actual hardware. At Evergreen Innovations, we face this challenge quite frequently. A combination of Go and Docker provides an excellent means of creating simulations of particular elements of hardware.

Our Docker/Go IoT approach allows for the development of interfaces to the hardware and the creation of specific scenarios in the simulator. This allows us to mimic real-world behavior and also to simulate error states. In the case of errors, controlling software can then be developed to handle these scenarios properly.

In this blog, we present an example of a power meter, a typical IoT device in some of our energy storage applications. The power meter reports instantaneous data such as the grid frequency alongside the 3-phases of voltage and current. An example application could be a battery connected to a domestic solar panel.

The purpose of this blog is to demonstrate the communications between such a device and a supervisor via the Modbus protocol. In our case, the Modbus communication will be over TCP.

Modbus is a messaging protocol by establishing a client-server communication. This protocol is robust, well established, and supported by a wide range of industrial sensors and IoT devices. See here for further information on Modbus.

The specifics of reading and writing values from the Modbus registers are not covered in this blog post. Let us know if you would like us to write a blog on this!

Architecture

To provide a simplified interface for this demonstration, we created a Modbus Go package to wrap the excellent modbus server and modbus client Go packages. Our wrapper encompasses both the client and server functionality in a single package.

Data is transferred over the Modbus protocol by writing to and reading from registers. The register mapping will be specific to the particular sensor or IoT device used and can be found in the manufacturer documentation. For example, assuming this power meter, the device stores the grid frequency at address 16384.

The power meter acts as the Modbus server, writing values to registers, and the supervisor act as the client, reading values from the power meter. In our demonstration example below, we will write the code for both sides of the interface. In most practical applications, the sensor (server here) side would be implemented by the manufacturer. Only the supervisor would need to be implemented by the IoT interface developer.

The power meter

The code for the simulated power meter can be found in the “powermeter” folder of this repository and is set up as a standard Go module-enabled project.

The first step in the project is to define the Modbus addresses for the various values exposed by this power meter. The example values chosen here are the frequency, three-phase voltage, and three-phase current as follows:

const (
	FrequencyAddr uint16 = 16384
	PhaseV1Addr   uint16 = 16386
	PhaseV2Addr   uint16 = 16388
	PhaseV3Addr   uint16 = 16390
	CurrentI1Addr uint16 = 16402
	CurrentI2Addr uint16 = 16404
	CurrentI3Addr uint16 = 16406
) 

To allow easy interation over these addresses, they are packed into a slice alongside a human-readable name for convenience.

var registers = []Register{
	{"Frequency", FrequencyAddr},
	{"PhaseV1", PhaseV1Addr},
	{"PhaseV2", PhaseV2Addr},
	{"PhaseV3", PhaseV3Addr},
	{"CurrentI1", CurrentI1Addr},
	{"CurrentI2", CurrentI2Addr},
	{"CurrentI3", CurrentI3Addr},
} 

Given that our Modbus communication is over TCP, commandline arguments for the host and the port are provided using the flag package, with default values provided

host := flag.String("host", defaultHost, "host for the modbus server")
port := flag.String("port", defaultPort, "port for the modbus server")
flag.Parse() 
which then allows us to create our Modbus server:
addr := fmt.Sprintf("%s%s", *host, *port)
s, err := modbus.NewServer(addr)
if err != nil {
	mainErr = fmt.Errorf("creating server: %v", err)
	return
}
defer s.Close() 

Details concerning error handling in the main function are covered in this separate blog.

Finally, each of the registers is assigned a random numerical value within a timed loop. We can then observe those values from the supervisor, as described further below.

rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
ticker := time.NewTicker(500 * time.Millisecond)
for range ticker.C {
	// Loop over the register address values from map and write the values
	for _, r := range registers {
        value := uint16(rnd.Int())
        fmt.Printf("writing to %v[%v] value: %v\n", r.Name, r.Address, value)
		s.WriteRegister(r.Address, value)
	}
} 
The output of the program (using go run main.go) is then:
Modbus server for power meter running at address 0.0.0.0:1503
writing to Frequency[16384] value: 47927
writing to PhaseV1[16386] value: 3661
writing to PhaseV2[16388] value: 8259
writing to PhaseV3[16390] value: 47553
writing to CurrentI1[16402] value: 31286
writing to CurrentI2[16404] value: 6672
writing to CurrentI3[16406] value: 63385

The supervisor

The code structure for the supervisor is similar to that of the power meter and must have identical Modbus register definitions. In the supervisor, however, we create a client rather than a server and use the IP address of the power meter to establish a connection.

 // Set up the commandline options
host := flag.String("host", defaultHost, "host for the modbus listener")
port := flag.String("port", defaultPort, "port for the modbus listener")
flag.Parse()

// Start a listener modbus client
addr := fmt.Sprintf("%s%s", *host, *port)
c, err := modbus.NewClient(addr)
if err != nil {
	mainErr = fmt.Errorf("error creating client: %v", err)
	return
}
defer c.Close() 

The client can now be used to read the registers that the power meter is writing to:

for range ticker.C {
	// Loop over the register address values from map and read the values
	for _, r := range registers {
		v, err := c.ReadRegister(r.Address)
		if err != nil {
			fmt.Printf("error reading %v[%v]: %v\n", r.Name, r.Address, err)
			continue
		}
		fmt.Printf("read %v[%v]: %v\n", r.Name, r.Address, v)
	}
} 
To observe the process in action, open up two terminal windows. In the first terminal, open up the directory for the power meter; in the second terminal, open that of the supervisor. Starting with the power meter, issue the command `go run main.go` in both terminal windows and observe the output. Your output will be slightly different (due to using random numbers as the value), but you should see blocks such as
writing to Frequency[16384] value: 61325
writing to PhaseV1[16386] value: 14234
writing to PhaseV2[16388] value: 48279
writing to PhaseV3[16390] value: 12937
writing to CurrentI1[16402] value: 43749
writing to CurrentI2[16404] value: 9852
writing to CurrentI3[16406] value: 35399
which matches the output in the supervisor:
read Frequency[16384]: 61325
read PhaseV1[16386]: 14234
read PhaseV2[16388]: 48279
read PhaseV3[16390]: 12937
read CurrentI1[16402]: 43749
read CurrentI2[16404]: 9852
read CurrentI3[16406]: 35399
We have therefore tested communication over Modbus on our local development machine, without needing a single sensor!

Docker integration

As outlined in this related blog, our IoT blog series aims to create a complete IoT system for local development. This is most easily achieved using Docker containers. In the directories for both the power meter and the supervisor, we included a `Dockerfile` to build the container. Both of these files have a similar structure and use a two-stage build to minimize the final container size (~3MB rather than ~800MB). To run the services together, we can make use of docker-compose. In the `docker-compose.yml` file, we specify the services that we wish to run – the power meter and the supervisor in this example case. For the supervisor, we specify the commandline arguments in the `command` tag to specify the host for the Modbus connection. We can use ‘powermeter’ as the host, which Docker will resolve into the IP address of the container associated with the ‘powermeter’ service. The Docker images can then be conveniently built by issuing the following command in the same directory as the docker-compose.yml
docker compose build 
Then to run the power meter and the simulator together:
docker-compose up -d 
and to view the logs:
docker-compose logs -f 

You should see output similar to

powermeter_1  | writing to Frequency[16384] value: 60200
powermeter_1  | writing to PhaseV1[16386] value: 45665
powermeter_1  | writing to PhaseV2[16388] value: 16311
powermeter_1  | writing to PhaseV3[16390] value: 36347
powermeter_1  | writing to CurrentI1[16402] value: 44515
powermeter_1  | writing to CurrentI2[16404] value: 14367
powermeter_1  | writing to CurrentI3[16406] value: 54751
supervisor_1  | read Frequency[16384]: 60200
supervisor_1  | read PhaseV1[16386]: 45665
supervisor_1  | read PhaseV2[16388]: 16311
supervisor_1  | read PhaseV3[16390]: 36347
supervisor_1  | read CurrentI1[16402]: 44515
supervisor_1  | read CurrentI2[16404]: 14367
supervisor_1  | read CurrentI3[16406]: 54751

With this in place, we are ready to integrate these simulated IoT devices into the larger project as outlined in the related blog here. The code for this present blog can be found on our Github here.

Conclusion

We hope this blog was useful for you. You can extend this to simulate other protocols and IoT devices.

Please stay tuned for the next part in the series where we continue to build our IoT framework. If you are interested in learning more about managing deployment of the microservices, read this blog on setting up continuous delivery using GitHub Actions. 

Do let us know if there are any other subject that would you like to know about: johannes@evergreeninnovations.co