Nabto Edge CoAP Client for Remote Control

The Nabto Edge CoAP communication mechanism is particularly useful for implementing IoT remote control scenarios. That is, sending a command to an embedded device or reading a value. The concept is very similar to using HTTP REST services and is just as simple to implement when using the Nabto Edge SDKs.

On a Nabto Edge connection, the CoAP endpoint on the target device is invoked directly by the CoAP client; no central logic or cloud application functionality to worry about during development and latency is the lowest possible.

A schematic overview of a Nabto Edge CoAP interaction:

The client side realization of this scenario is outlined in the sections below. Only the actual CoAP interaction is described in detail: This takes place on a connection established as described in the Connecting guide.

As seen from the following examples, it is extremely simple to invoke a Nabto Edge CoAP service on an embedded device to control it: Once the connection is established (as assumed in the examples), it is just a few lines of code needed to invoke the device.

A full, production ready remote control app is available as described in the iOS and Android Thermostat guides. They also desribe setting up an embedded device to invoke.

Invoking a Nabto Edge CoAP Service to Update State

The following implements a client that invokes the heatpump as shown in the figure above:

func setCoolingMode(connection: Connection) throws {
  try setMode(connection: connection, "cool")
}

func setHeatingMode(connection: Connection) throws {
  try setMode(connection: connection, "heat")
}

func setRecirculateMode(connection: Connection) throws {
  try setMode(connection: connection, "recirculate")
}

private func setMode(connection: Connection, mode: String) throws {
    let coap = try connection.createCoapRequest(method: "POST", path: "/heat-pump/mode")
    try coap.setRequestPayload(contentFormat: ContentFormat.TEXT_PLAIN.rawValue, data: mode)
    let response = try coap.execute()
    if (response.status == 204) {
        print("Successfully set heat pump mode to \(mode)")
    } else {
        print("Could not set heat pump mode to \(mode), status was \(response.status)")
    }
}

// using https://github.com/FasterXML/jackson-dataformats-binary
import com.fasterxml.jackson.dataformat.cbor.databind.CBORMapper;

private void setCoolingMode(Connection connection) {
    setMode(connection, "COOL");
}

private void setHeatingMode(Connection connection) {
    setMode(connection, "HEAT");
}

private void setRecirculateMode(Connection connection) {
    setMode(connection, "FAN");
}

private void setMode(Connection connection, String mode) {
    String coap = connection.createCoapRequest("POST", "/thermostat/mode");
    ObjectMapper mapper = new CBORMapper();
    byte[] cbor = mapper.writeValueAsBytes(mode);
    coap.setRequestPayload(Coap.ContentFormat.APPLICATION_CBOR, cbor);
    coap.execute();
    int response = coap.getResponseStatusCode();
    if (response == 204) {
        Log.i("Nabto", "Successfully set thermostat mode to " + mode);
    } else {
        Log.i("Nabto", "Could not set heat pump mode to " + mode + ", status was " + response);
    }
}
import kotlinx.serialization.*
import kotlinx.serialization.cbor.Cbor

fun setCoolingMode(connection: Connection) {
    setMode(connection, "COOL")
}

fun setHeatingMode(connection: Connection) {
    setMode(connection, "HEAT")
}

fun setRecirculateMode(connection: Connection) {
    setMode(connection, "FAN")
}

fun setMode(connection: Connection, mode: String) {
    val coap = connection.createCoapRequest("POST", "/thermostat/mode")
    val cbor = Cbor.encodeToByteArray(mode)
    coap.setRequestPayload(Coap.ContentFormat.APPLICATION_CBOR, cbor)
    coap.execute()
    val response = coap.getResponseStatusCode()
    if (response == 204) {
        Log.i("Nabto", "Successfully set thermostat mode to ${mode}")
    } else {
        Log.i("Nabto", "Could not set heat pump mode to ${mode}, status was ${response}")
    }
}
using System.IO;
// using https://github.com/dahomey-technologies/Dahomey.Cbor
using Dahomey.Cbor;
using Nabto.Edge.Client;

public class WriteExample
{
    public static async Task SetCoolingMode(IConnection connection)
    {
        await SetMode(connection, "COOL");
    }

    public static async Task SetHeatingMode(IConnection connection)
    {
        await SetMode(connection, "HEAT");
    }

    public static async Task SetRecirculateMode(IConnection connection)
    {
        await SetMode(connection, "FAN");
    }

    public static async Task SetMode(IConnection connection, string mode)
    {
        using var coap = connection.CreateCoapRequest("POST", "/thermostat/mode");
        var cbor = new MemoryStream();
        await Cbor.SerializeAsync(mode, cbor);
        coap.SetRequestPayload(60, cbor.GetBuffer());
        var response = await coap.ExecuteAsync();
        var status = response.GetResponseStatusCode();
        if (status == 204)
        {
            Console.WriteLine($"Successfully set thermostat mode to {mode}");
        }
        else
        {
            Console.WriteLine($"Could not set heat pump mode to ${mode}, status was {status}");
        }
    }
}

This and all examples in this section use the synchronous APIs for simplicity. So if re-using snippets in your own app, make sure to run all potentially slow operations on a different thread than the main thead, e.g. using a global dispatch queue on iOS. Alternatively, use the asynchronous variants of the functions in iOS and the Kotlin extenion’s await variants in Android.

Invoking a Nabto Edge CoAP Service to Read State

Reading state can be done using a CoAP GET request as follows:

private func getMode(connection: Connection, mode: String) throws -> String {
    let coap = try connection.createCoapRequest(method: "GET", path: "/heat-pump/mode")
    let response = try coap.execute()
    if (response.status == 205 && response.contentFormat == ContentFormat.TEXT_PLAIN) {
        let mode = String(decoding: response.payload, as: UTF8.self)
        print("Successfully read the heat pump mode: \(mode)")
    } else {
        print("Could not set heat pump mode to \(mode), status was \(response.status)")
    }
}

private void getMode(Connection connection, String mode) {
    Coap coap = connection.createCoap("GET", "/thermostat/mode")
    coap.execute();
    int response = coap.getResponseStatusCode();
    int contentFormat =  coap.getResponseContentFormat();
    if (response == 205 && contentFormat == Coap.ContentFormat.TEXT_PLAIN) {
        String mode = new String(coap.getResponsePayload());
        Log.i("Nabto", "Device replied: " + mode);
    } else {
        Log.i("Nabto", "Device returned non-ok status: " + response);
    }
}

fun getMode(Connection connection, String mode) {
    val coap = connection.createCoap("GET", "/thermostat/mode")
    coap.execute()
    val response = coap.getResponseStatusCode()
    val contentFormat =  coap.getResponseContentFormat()
    if (response == 205 && contentFormat == Coap.ContentFormat.TEXT_PLAIN) {
        val mode = String(coap.getResponsePayload())
        Log.i("Nabto", "Device replied: ${mode}")
    } else {
        Log.i("Nabto", "Device returned non-ok status: ${response}")
    }
}

using Nabto.Edge.Client;

public class ReadExample {

    public static async Task<string> GetMode(IConnection connection)
    {
        using var coap = connection.CreateCoapRequest("GET", "/thermostat/mode");
        var response = await coap.ExecuteAsync();
        if (response.GetResponseStatusCode() == 205)
        {
            var contentFormat = response.GetResponseContentFormat();
            if (contentFormat != 0)
            {
                throw new Exception($"Unexpected content format {contentFormat}");
            }
            var deviceMode = System.Text.Encoding.UTF8.GetString(response.GetResponsePayload());
            Console.WriteLine($"Device replied: {deviceMode}");
            return deviceMode;
        }
        else
        {
            Console.WriteLine($"Device returned non-ok status: {response.GetResponseStatusCode()}");
            throw new Exception($"Could not get thermostat mode, COAP request returned {response}");
        }
    }

}


CBOR encoding for Complex Data

In the examples above, the data exchanged was sent as plain text. You can use any encoding preferred, the Nabto SDKs involved in the communication just passes the data and the indicated content type. So it is up to the applications alone to encode and decode (and in general interpret the content format indication).

Often CBOR encoding is used as it is a compact data representation and allows exchanging complex data structures. The following example shows how CBOR can be used to pass a struct-like structure with the full state of the heatpump. 3rd party components are used for handling the actual CBOR encoding and decoding.

import CBORCoding    // https://github.com/SomeRandomiOSDev/CBORCoding

func coapGetHeatpumpInfo(connection: Connection) throws -> HeatpumpDetails {
    let request = try connection.createCoapRequest(method: "GET", path: "/heat-pump")
    let response = try request.execute()
    if (response.status == 205) {
        return try HeatpumpDetails.decode(cbor: response.payload)
    } else {
        throw Failed(detail: "Could not get thermostat details, device returned status \(response.status)")
    }
}

struct HeatpumpDetails: Codable, CustomStringConvertible {
    public let Mode: String
    public let Target: Double
    public let Power: Bool
    public let Temperature: Double

    public init(Mode: String, Target: Double, Power: Bool, Temperature: Double) {
        self.Mode = Mode
        self.Target = Target
        self.Power = Power
        self.Temperature = Temperature
    }

    public static func decode(cbor: Data) throws -> HeatpumpDetails {
        let decoder = CBORDecoder()
        do {
            return try decoder.decode(HeatpumpDetails.self, from: cbor)
        } catch {
            throw Failed(detail: "Could not decode thermostat response: \(error)")
        }
    }
}

enum ExampleError: Error {
    case Failed(detail: String)
    case Ok
}

// using https://github.com/FasterXML/jackson-dataformats-binary
import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.dataformat.cbor.databind.CBORMapper;

public class ThermostatDetails {
    @JsonProperty(value = "Mode", required = true)
    public String mode;

    @JsonProperty(value = "Power", required = true)
    public boolean power;

    @JsonProperty(value = "Target", required = true)
    public double target;

    @JsonProperty(value = "Temperature", required = true)
    public double temperature;

    @JsonCreator
    public ThermostatDetails(
        @JsonProperty(value = "Mode", required = true) String mode,
        @JsonProperty(value = "Power", required = true) boolean power,
        @JsonProperty(value = "Target", required = true) double target,
        @JsonProperty(value = "Temperature", required = true) double temperature
    ) {
        this.mode = mode;
        this.target = target;
        this.power = power;
        this.temperature = temperature;
    }
}

// ... somewhere else in your code ...
private ThermostatDetails coapGetThermostatInfo(Connection connection) {
    Coap request = connection.createCoap("GET", "/thermostat");
    request.execute();
    int response = request.getResponseStatusCode();
    if (response == 205) {
        ObjectMapper mapper = new CBORMapper();
        ThermostatDetails result = mapper.readValue(request.getResponsePayload(), ThermostatDetails.class);
        return result;
    } else {
        throw new RuntimeException("Could not get thermostat details, COAP request returned " + response);
    }
}

import kotlinx.serialization.*
import kotlinx.serialization.cbor.Cbor

@Serializable
data class ThermostatDetails(
    @Required @SerialName("Mode") val mode: String,
    @Required @SerialName("Power") val power: Boolean,
    @Required @SerialName("Target") val target: Double,
    @Required @SerialName("Temperature") val temperature: Double
)


// ... somewhere else in your code ...
fun coapGetThermostatInfo(connection: Connection): ThermostatDetails {
    val request = connection.createCoap("GET", "/thermostat")
    request.execute()
    val response = request.getResponseStatusCode()
    if (response == 205) {
        return Cbor.decodeFromByteArray<ThermostatDetails>(request.getResponsePayload())
    } else {
        throw new RuntimeException("Could not get thermostat details, COAP request returned " + response)
    }
}

// using https://github.com/dahomey-technologies/Dahomey.Cbor
using Dahomey.Cbor;

using Nabto.Edge.Client;

public struct ThermostatDetails
{
    public string Mode { get; set; }
    public bool Power { get; set; }
    public double Target { get; set; }
    public double Temperature { get; set; }
}

public class CborExample {

    // ... somewhere else in your code ...
    public static async Task<ThermostatDetails> CoapGetThermostatInfo(IConnection connection)
    {
        using var coap = connection.CreateCoapRequest("GET", "/thermostat");
        var response = await coap.ExecuteAsync();
        var status = response.GetResponseStatusCode();
        if (status == 205)
        {
            var stream = new MemoryStream(response.GetResponsePayload());
            return await Cbor.DeserializeAsync<ThermostatDetails>(stream);
        }
        else
        {
            throw new InvalidOperationException($"Could not get thermostat details, COAP request returned {status}");
        }
    }

}