Device Commands

Device commands are request-reply messages from the server to a set of devices.

Editing the Schema

You must define two protobuf messages per command: one for the request and one for the reply.

Let's add a command to our example to activate a HVAC system. First, let's define them in the schema:

message ActivateHVACCmd {
	option (mir.device.v1.message_type) = MESSAGE_TYPE_TELECOMMAND;

	int32 duration_sec = 1;
}

message ActivateHVACResp {
  bool success = 1;
}

As you can see, instead of having the option as MESSAGE_TYPE_TELEMETRY, it is now MESSAGE_TYPE_TELECOMMAND. This will tell the server that this message is a command and should be handled as such. The response does not need any special annotation.

Let's regenerate the schema:

just proto
# or
make proto

Handle the Command

Each command takes a callback function that will be called when the server sends a command to the device:

m.HandleCommand(
	&schemav1.ActivateHVACCmd{},
	func(msg proto.Message) (proto.Message, error) {
		cmd := msg.(*schemav1.ActivateHVACCmd) // Cast the proto.Message to the command type

		/* Command processing...*/

  	// Return the command response. This can be any proto message.
   	// You can also return an error instead that will be pass back to the server and requester.
    return &schemav1.ActivateHVACResp{
    	Success: true,
    }, nil
})

Let's complete our example by adding a command handler that output some logs after the duration:

package main

import (
	"context"
	"math/rand/v2"
	"mir-device/schemav1"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/maxthom/mir/pkgs/device/mir"
	schemav1 "github.com/maxthom/mir.device.buff/proto/gen/schema/v1"
	"google.golang.org/protobuf/proto"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	m, err := mir.Builder().
		DeviceId("weather").
		Target("nats://127.0.0.1:4222").
		LogPretty(false).
		Schema(schemav1.File_schema_proto).
		Build()
	if err != nil {
		panic(err)
	}

	wg, err := m.Launch(ctx)
	if err != nil {
		panic(err)
	}
	dataRate := 3

	m.HandleCommand(
		&schemav1.ActivateHVACCmd{},
		func(msg proto.Message) (proto.Message, error) {
			cmd := msg.(*schemav1.ActivateHVACCmd)
			m.Logger().Info().Msgf("handling command: activating HVAC for %d sec", cmd.DurationSec)

			go func() {
				<-time.After(time.Duration(cmd.DurationSec) * time.Second)
				m.Logger().Info().Msg("turning off HVAC")
			}()

			return &schemav1.ActivateHVACResp{
				Success: true,
			}, nil
		})

	go func() {
		for {
			select {
			case <-ctx.Done():
				wg.Done()
				return
			case <-time.After(time.Duration(dataRate) * time.Second):
				if err := m.SendTelemetry(&schemav1.Env{
					Ts:          time.Now().UTC().UnixNano(),
					Temperature: rand.Int32N(101),
					Pressure:    rand.Int32N(101),
					Humidity:    rand.Int32N(101),
				}); err != nil {
					m.Logger().Error().Err(err).Msg("error sending telemetry")
				}
			}
		}
	}()

	osSignal := make(chan os.Signal, 1)
	signal.Notify(osSignal, syscall.SIGINT, syscall.SIGTERM)
	<-osSignal

	cancel()
	wg.Wait()
}

Rerun the code:

just run
# or
make run

Send a Command

Our device is now sending periodic telemetry and can receive one command. Let's test it:

# List all available commands
mir dev cmd send weather
  schema.v1.ActivateHVACCmd{}

# Show command JSON template for payload
mir cmd send weather/default -n schema.v1.ActivateHVACCmd -j
  {
    "durationSec": 0
  }

Multiple ways to send a command:

# Send command to activate the HVAC
# ps: use single quotes for easy json
mir cmd send weather/default -n schema.v1.ActivateHVACCmd -p '{"durationSec": 5}'
  1. weather/default COMMAND_RESPONSE_STATUS_SUCCESS
  schema.v1.ActivateHVACResp
  {
    "success": true
  }

# Use pipes to pass payload
mir cmd send weather/default -n schema.v1.ActivateHVACCmd -j > ActivateHVACCmd.json
# Edit ActivateHVACCmd.json
# Send it!
cat ActivateHVACCmd.json | mir cmd send weather/default -n schema.v1.ActivateHVACCmd
  1. weather/default COMMAND_RESPONSE_STATUS_SUCCESS
  schema.v1.ActivateHVACResp
  {
    "success": true
  }

# Interactively edit for easy interaction
# Upon quit and save, it will send the command
mir cmd send weather/default -n schema.v1.ActivateHVACCmd -e
  1. weather/default COMMAND_RESPONSE_STATUS_SUCCESS
  schema.v1.ActivateHVACResp
  {
    "success": true
  }

Voila! You have successfully sent a command to the device to change it's data rate. Look at your device logs to see the command into effect.