Repository: eclipse/leshan
Pull request #943 (2020-12-02 14:53:22)

Add support of CBOR content format.


Patch:
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeDecoder.java
index 0a0f13f5..26582aa6 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeDecoder.java
@@ -30,6 +30,7 @@ import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
 import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
+import org.eclipse.leshan.core.node.codec.cbor.LwM2mNodeCborDecoder;
 import org.eclipse.leshan.core.node.codec.json.LwM2mNodeJsonDecoder;
 import org.eclipse.leshan.core.node.codec.opaque.LwM2mNodeOpaqueDecoder;
 import org.eclipse.leshan.core.node.codec.senml.LwM2mNodeSenMLDecoder;
@@ -59,6 +60,7 @@ public class DefaultLwM2mNodeDecoder implements LwM2mNodeDecoder {
         Map<ContentFormat, NodeDecoder> decoders = new HashMap<>();
         decoders.put(ContentFormat.TEXT, new LwM2mNodeTextDecoder());
         decoders.put(ContentFormat.OPAQUE, new LwM2mNodeOpaqueDecoder());
+        decoders.put(ContentFormat.CBOR, new LwM2mNodeCborDecoder());
         decoders.put(ContentFormat.SENML_JSON, new LwM2mNodeSenMLDecoder(new SenMLJsonJacksonEncoderDecoder()));
         decoders.put(ContentFormat.SENML_CBOR, new LwM2mNodeSenMLDecoder(new SenMLCborUpokecenterEncoderDecoder()));
 
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java
index 16aeb034..c412936c 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java
@@ -24,6 +24,7 @@ import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.node.LwM2mNode;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
+import org.eclipse.leshan.core.node.codec.cbor.LwM2mNodeCborEncoder;
 import org.eclipse.leshan.core.node.codec.json.LwM2mNodeJsonEncoder;
 import org.eclipse.leshan.core.node.codec.opaque.LwM2mNodeOpaqueEncoder;
 import org.eclipse.leshan.core.node.codec.senml.LwM2mNodeSenMLEncoder;
@@ -53,6 +54,7 @@ public class DefaultLwM2mNodeEncoder implements LwM2mNodeEncoder {
         Map<ContentFormat, NodeEncoder> encoders = new HashMap<>();
         encoders.put(ContentFormat.TEXT, new LwM2mNodeTextEncoder());
         encoders.put(ContentFormat.OPAQUE, new LwM2mNodeOpaqueEncoder());
+        encoders.put(ContentFormat.CBOR, new LwM2mNodeCborEncoder());
         encoders.put(ContentFormat.SENML_JSON, new LwM2mNodeSenMLEncoder(new SenMLJsonJacksonEncoderDecoder()));
         encoders.put(ContentFormat.SENML_CBOR, new LwM2mNodeSenMLEncoder(new SenMLCborUpokecenterEncoderDecoder()));
 
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/cbor/LwM2mNodeCborDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/cbor/LwM2mNodeCborDecoder.java
new file mode 100644
index 00000000..e9e96475
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/cbor/LwM2mNodeCborDecoder.java
@@ -0,0 +1,168 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.node.codec.cbor;
+
+import java.util.Date;
+
+import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.model.ResourceModel;
+import org.eclipse.leshan.core.model.ResourceModel.Type;
+import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.LwM2mPath;
+import org.eclipse.leshan.core.node.LwM2mResourceInstance;
+import org.eclipse.leshan.core.node.LwM2mSingleResource;
+import org.eclipse.leshan.core.node.ObjectLink;
+import org.eclipse.leshan.core.node.codec.CodecException;
+import org.eclipse.leshan.core.node.codec.NodeDecoder;
+import org.eclipse.leshan.core.util.datatype.ULong;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.upokecenter.cbor.CBORException;
+import com.upokecenter.cbor.CBORNumber;
+import com.upokecenter.cbor.CBORObject;
+import com.upokecenter.cbor.CBORType;
+
+public class LwM2mNodeCborDecoder implements NodeDecoder {
+    private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeCborDecoder.class);
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends LwM2mNode> T decode(byte[] content, LwM2mPath path, LwM2mModel model, Class<T> nodeClass)
+            throws CodecException {
+
+        // Support only single value
+        if (!path.isResource() && !path.isResourceInstance())
+            throw new CodecException("Invalid path %s : CborDecoder decodes resource OR resource instance only", path);
+
+        if (content == null)
+            return null;
+
+        // Parse CBOR
+        CBORObject cborObject;
+        try {
+            cborObject = CBORObject.DecodeFromBytes(content);
+        } catch (CBORException e) {
+            throw new CodecException(e, "Unable to parse CBORD value %s for resource %s", content, path);
+        }
+
+        // Find model to know expected type
+        Type expectedType;
+        ResourceModel rDesc = model.getResourceModel(path.getObjectId(), path.getResourceId());
+        if (rDesc != null) {
+            expectedType = rDesc.type;
+        } else {
+            // try to guess type from CBOR ?
+            expectedType = guessTypeFromCbor(cborObject, path);
+            LOG.debug("Decoding CBOR resource or resource instance without model, type guessed {}", expectedType);
+        }
+
+        // Get Node Value
+        Object nodeValue = parseCborValue(cborObject, expectedType, path);
+
+        // Create Node
+        if (path.isResource()) {
+            return (T) LwM2mSingleResource.newResource(path.getResourceId(), nodeValue, expectedType);
+        } else {
+            return (T) LwM2mResourceInstance.newInstance(path.getResourceInstanceId(), nodeValue, expectedType);
+        }
+    }
+
+    private Type guessTypeFromCbor(CBORObject cborObject, LwM2mPath path) {
+        switch (cborObject.getType()) {
+        case Boolean:
+            return Type.BOOLEAN;
+        case FloatingPoint:
+            if (cborObject.HasTag(1))
+                return Type.TIME;
+            else
+                return Type.FLOAT;
+        case Integer:
+            if (cborObject.HasTag(1))
+                return Type.TIME;
+            else
+                return Type.FLOAT;
+        case TextString:
+            if (cborObject.HasTag(0))
+                return Type.TIME;
+            else
+                return Type.STRING;
+        case ByteString:
+            return Type.OPAQUE;
+        default:
+            throw new CodecException("Unable to guess LWM2M type for resource %s, cbor type is {}", path,
+                    cborObject.getType());
+        }
+    }
+
+    private Object parseCborValue(CBORObject cborObject, Type type, LwM2mPath path) throws CodecException {
+        LOG.trace("CBOR value for path {} and expected type {}: {}", path, type, cborObject.toString());
+
+        try {
+            if (cborObject.isNull())
+                return null;
+
+            switch (type) {
+            case STRING:
+                if (cborObject.getType() == CBORType.TextString) {
+                    return cborObject.AsString();
+                }
+                break;
+            case INTEGER:
+                if (cborObject.getType() == CBORType.Integer) {
+                    return cborObject.AsInt64Value();
+                }
+                break;
+            case UNSIGNED_INTEGER:
+                if (cborObject.getType() == CBORType.Integer) {
+                    CBORNumber number = cborObject.AsNumber();
+                    if (number.IsInteger() && !number.IsNegative()
+                            && number.ToEIntegerIfExact().GetUnsignedBitLengthAsInt64() <= 64) {
+                        return ULong.valueOf(number.ToInt64Unchecked());
+                    }
+                }
+                break;
+            case BOOLEAN:
+                if (cborObject.getType() == CBORType.Boolean) {
+                    return cborObject.AsBoolean();
+                }
+            case FLOAT:
+                if (cborObject.getType() == CBORType.FloatingPoint) {
+                    return cborObject.AsDoubleValue();
+                }
+                break;
+            case TIME:
+                return cborObject.ToObject(Date.class);
+            case OBJLNK:
+                if (cborObject.getType() == CBORType.TextString) {
+                    return ObjectLink.decodeFromString(cborObject.AsString());
+                }
+            case OPAQUE:
+                if (cborObject.getType() == CBORType.ByteString) {
+                    return cborObject.GetByteString();
+                }
+                break;
+            default:
+                throw new CodecException("Unsupported type %s for resource %s", type, path);
+            }
+        } catch (IllegalStateException | ArithmeticException | NumberFormatException e) {
+            throw new CodecException(e, "Unable to convert CBOR value %s of type %s in type %s for resource %s",
+                    cborObject.toString(), cborObject.getType(), type, path);
+        }
+        throw new CodecException("Unable to convert CBOR value %s of type %s in type %s for resource %s",
+                cborObject.toString(), cborObject.getType(), type, path);
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/cbor/LwM2mNodeCborEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/cbor/LwM2mNodeCborEncoder.java
new file mode 100644
index 00000000..b974d44c
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/cbor/LwM2mNodeCborEncoder.java
@@ -0,0 +1,165 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.node.codec.cbor;
+
+import java.util.Date;
+
+import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.model.ResourceModel;
+import org.eclipse.leshan.core.model.ResourceModel.Type;
+import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.LwM2mNodeVisitor;
+import org.eclipse.leshan.core.node.LwM2mObject;
+import org.eclipse.leshan.core.node.LwM2mObjectInstance;
+import org.eclipse.leshan.core.node.LwM2mPath;
+import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.LwM2mResourceInstance;
+import org.eclipse.leshan.core.node.ObjectLink;
+import org.eclipse.leshan.core.node.codec.CodecException;
+import org.eclipse.leshan.core.node.codec.LwM2mValueConverter;
+import org.eclipse.leshan.core.node.codec.NodeEncoder;
+import org.eclipse.leshan.core.util.Validate;
+import org.eclipse.leshan.core.util.datatype.NumberUtil;
+import org.eclipse.leshan.core.util.datatype.ULong;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.upokecenter.cbor.CBORObject;
+
+public class LwM2mNodeCborEncoder implements NodeEncoder {
+
+    private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeCborEncoder.class);
+
+    @Override
+    public byte[] encode(LwM2mNode node, LwM2mPath path, LwM2mModel model, LwM2mValueConverter converter)
+            throws CodecException {
+        Validate.notNull(node);
+        Validate.notNull(path);
+        Validate.notNull(model);
+
+        InternalEncoder internalEncoder = new InternalEncoder();
+        internalEncoder.path = path;
+        internalEncoder.model = model;
+        internalEncoder.converter = converter;
+        node.accept(internalEncoder);
+        return internalEncoder.encoded;
+    }
+
+    private static class InternalEncoder implements LwM2mNodeVisitor {
+        // visitor inputs
+        private LwM2mPath path;
+        private LwM2mModel model;
+        private LwM2mValueConverter converter;
+
+        // visitor output
+        private byte[] encoded = null;
+
+        @Override
+        public void visit(LwM2mObject object) {
+            throw new CodecException("Object %s cannot be encoded in cbor format", path);
+        }
+
+        @Override
+        public void visit(LwM2mObjectInstance instance) {
+            throw new CodecException("Object instance %s cannot be encoded in cbor format", path);
+        }
+
+        @Override
+        public void visit(LwM2mResource resource) {
+            if (resource.isMultiInstances()) {
+                throw new CodecException("Multiple instances resource %s cannot be encoded in cbor format", path);
+            }
+            LOG.trace("Encoding resource {} into cbor", resource);
+
+            ResourceModel rSpec = model.getResourceModel(path.getObjectId(), resource.getId());
+            Type expectedType = rSpec != null ? rSpec.type : resource.getType();
+            Object val = converter.convertValue(resource.getValue(), resource.getType(), expectedType, path);
+
+            if (expectedType == null) {
+                throw new CodecException(
+                        "Unable to encode value for resource {} without type(probably a executable one)", path);
+            }
+
+            CBORObject cbor = getCborValue(expectedType, val);
+
+            encoded = cbor.EncodeToBytes();
+        }
+
+        @Override
+        public void visit(LwM2mResourceInstance instance) {
+            LOG.trace("Encoding resource instance {} into cbor", instance);
+
+            ResourceModel rSpec = model.getResourceModel(path.getObjectId(), path.getResourceId());
+            Type expectedType = rSpec != null ? rSpec.type : instance.getType();
+            Object val = converter.convertValue(instance.getValue(), instance.getType(), expectedType, path);
+
+            if (expectedType == null) {
+                throw new CodecException(
+                        "Unable to encode value for resource {} without type(probably a executable one)", path);
+            }
+
+            CBORObject cbor = getCborValue(expectedType, val);
+
+            encoded = cbor.EncodeToBytes();
+        }
+
+        private CBORObject getCborValue(Type expectedType, Object val) {
+            CBORObject cbor;
+            if (val == null) {
+                return CBORObject.Null;
+            }
+            switch (expectedType) {
+            case INTEGER:
+                cbor = CBORObject.FromObject((long) val);
+                break;
+            case FLOAT:
+                cbor = CBORObject.FromObject((double) val);
+                break;
+            case STRING:
+                cbor = CBORObject.FromObject((String) val);
+                break;
+            case UNSIGNED_INTEGER:
+                cbor = CBORObject.FromObject(NumberUtil.unsignedLongToEInteger(((ULong) val).longValue()));
+                break;
+            case BOOLEAN:
+                cbor = ((Boolean) val) ? CBORObject.True : CBORObject.False;
+                break;
+            case TIME:
+                // see https://tools.ietf.org/html/rfc7049#section-2.4.1
+                // number of seconds since 1970/1/1
+                long time = ((Date) val).getTime() / 1000;
+                cbor = CBORObject.FromObjectAndTag(time, 1);
+
+                // maybe a new API will be available in new version of CBOR-java :
+                // https://github.com/peteroupc/CBOR-Java/issues/14#issuecomment-737114305
+
+                // using this could works but encode the date as string sounds a bit less straightforward.
+                // cbor = CBORObject.FromObject(val); // val must be a Date
+                break;
+            case OBJLNK:
+                ObjectLink objlnk = (ObjectLink) val;
+                cbor = CBORObject.FromObject(objlnk.encodeToString());
+                break;
+            case OPAQUE:
+                cbor = CBORObject.FromObject((byte[]) val);
+                break;
+            default:
+                throw new CodecException("Cannot encode %s in cbor format for %s", val, path);
+            }
+            return cbor;
+        }
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/ContentFormat.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/ContentFormat.java
index 23dec12e..87adeb4f 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/ContentFormat.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/ContentFormat.java
@@ -24,6 +24,7 @@ public class ContentFormat {
     public static final int TEXT_CODE = 0;
     public static final int OPAQUE_CODE = 42;
     public static final int LINK_CODE = 40;
+    public static final int CBOR_CODE = 60;
 
     // Keep old code for backward-compatibility
     public static final int OLD_JSON_CODE = 1543;
@@ -41,11 +42,12 @@ public class ContentFormat {
             SENML_JSON_CODE);
     public static final ContentFormat SENML_CBOR = new ContentFormat("SENML_CBOR", "application/senml+cbor",
             SENML_CBOR_CODE);
+    public static final ContentFormat CBOR = new ContentFormat("CBOR", "application/cbor", CBOR_CODE);
 
     public static final ContentFormat DEFAULT = TLV;
 
     private static final ContentFormat knownContentFormat[] = new ContentFormat[] { TLV, JSON, SENML_JSON, SENML_CBOR,
-                            TEXT, OPAQUE, LINK };
+                            TEXT, OPAQUE, CBOR, LINK };
 
     private final String name;
     private final String mediaType;
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/util/datatype/NumberUtil.java b/leshan-core/src/main/java/org/eclipse/leshan/core/util/datatype/NumberUtil.java
index d7f115fe..10965acc 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/util/datatype/NumberUtil.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/util/datatype/NumberUtil.java
@@ -17,6 +17,8 @@ package org.eclipse.leshan.core.util.datatype;
 
 import java.math.BigInteger;
 
+import com.upokecenter.numbers.EInteger;
+
 public class NumberUtil {
 
     /**
@@ -95,4 +97,13 @@ public class NumberUtil {
         throw new IllegalStateException(String.format("Can not convert %s to long safely : Unsupported number %s",
                 number, number.getClass().getCanonicalName()));
     }
+
+    // This will maybe be added in a new version of CBOR-java : https://github.com/peteroupc/CBOR-Java/issues/15
+    public static EInteger unsignedLongToEInteger(long v) {
+        if (v >= 0) {
+            return EInteger.FromInt64(v);
+        } else {
+            return EInteger.FromInt32(1).ShiftLeft(64).Add(v);
+        }
+    }
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/upokecenter/SenMLCborPackSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/upokecenter/SenMLCborPackSerDes.java
index 57621845..05d46bcd 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/upokecenter/SenMLCborPackSerDes.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/upokecenter/SenMLCborPackSerDes.java
@@ -24,6 +24,7 @@ import java.util.Collection;
 
 import org.eclipse.leshan.core.model.ResourceModel.Type;
 import org.eclipse.leshan.core.util.Base64;
+import org.eclipse.leshan.core.util.datatype.NumberUtil;
 import org.eclipse.leshan.core.util.datatype.ULong;
 import org.eclipse.leshan.core.util.json.JsonException;
 import org.eclipse.leshan.senml.SenMLException;
@@ -33,7 +34,6 @@ import org.eclipse.leshan.senml.SenMLRecord;
 import com.upokecenter.cbor.CBORNumber;
 import com.upokecenter.cbor.CBORObject;
 import com.upokecenter.cbor.CBORType;
-import com.upokecenter.numbers.EInteger;
 
 public class SenMLCborPackSerDes {
 
@@ -65,17 +65,26 @@ public class SenMLCborPackSerDes {
                     CBORNumber number = v.AsNumber();
                     if (number.IsInteger()) {
                         if (number.IsNegative()) {
-                            record.setFloatValue(number.ToInt64Checked());
-                        } else {
                             if (number.CanFitInInt64()) {
                                 record.setFloatValue(number.ToInt64Unchecked());
                             } else {
-                                // There is maybe a better way to do that.
+                                record.setFloatValue((BigInteger) v.ToObject(BigInteger.class));
+                            }
+                        } else {
+                            if (number.CanFitInInt64()) {
+                                record.setFloatValue(number.ToInt64Unchecked());
+                            } else if (number.ToEIntegerIfExact().GetSignedBitLengthAsInt64() == 64) {
                                 record.setFloatValue(ULong.valueOf(number.ToInt64Unchecked()));
+                            } else {
+                                record.setFloatValue((BigInteger) v.ToObject(BigInteger.class));
                             }
                         }
                     } else {
-                        record.setFloatValue(v.AsDoubleValue());
+                        if (v.AsNumber().CanFitInDouble()) {
+                            record.setFloatValue(v.AsDoubleValue());
+                        } else {
+                            record.setFloatValue((BigDecimal) v.ToObject(BigDecimal.class));
+                        }
                     }
                     hasValue = true;
                 }
@@ -155,8 +164,7 @@ public class SenMLCborPackSerDes {
                         }
                         // unsigned integer
                         else if (value instanceof ULong) {
-                            // There is maybe a better way to do that.
-                            cborRecord.Add(2, EInteger.FromString(value.toString()));
+                            cborRecord.Add(2, NumberUtil.unsignedLongToEInteger(((ULong) value).longValue()));
                         }
                         // floating-point
                         else if (value instanceof Float) {
diff --git a/leshan-server-demo/src/main/resources/webapp/partials/client-detail.html b/leshan-server-demo/src/main/resources/webapp/partials/client-detail.html
index 46a76cc1..9902a273 100644
--- a/leshan-server-demo/src/main/resources/webapp/partials/client-detail.html
+++ b/leshan-server-demo/src/main/resources/webapp/partials/client-detail.html
@@ -37,6 +37,7 @@
 				</button>
 				<ul class="dropdown-menu dropdown-menu-contentformat" role="menu">
 					<li><a ng-click="settings.single.format='TLV'">TLV</a></li>
+					<li><a ng-click="settings.single.format='CBOR'">CBOR</a></li>
 					<li><a ng-click="settings.single.format='SENML_JSON'">SENML_JSON</a></li>
 					<li><a ng-click="settings.single.format='SENML_CBOR'">SENML_CBOR</a></li>
 					<li><a ng-click="settings.single.format='JSON'">JSON</a></li>


Test patch:
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadOpaqueValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadOpaqueValueTest.java
index 25d34dcc..0c2396fa 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadOpaqueValueTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadOpaqueValueTest.java
@@ -46,6 +46,7 @@ public class ReadOpaqueValueTest {
                                 { ContentFormat.OPAQUE }, //
                                 { ContentFormat.TEXT }, //
                                 { ContentFormat.TLV }, //
+                                { ContentFormat.CBOR }, //
                                 { ContentFormat.JSON }, //
                                 { ContentFormat.SENML_JSON }, //
                                 { ContentFormat.SENML_CBOR } });
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadSingleValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadSingleValueTest.java
index 0ec3210f..98b268ec 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadSingleValueTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadSingleValueTest.java
@@ -45,6 +45,7 @@ public class ReadSingleValueTest {
         return Arrays.asList(new Object[][] { //
                                 { ContentFormat.TEXT }, //
                                 { ContentFormat.TLV }, //
+                                { ContentFormat.CBOR }, //
                                 { ContentFormat.JSON }, //
                                 { ContentFormat.SENML_JSON }, //
                                 { ContentFormat.SENML_CBOR } });
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteOpaqueValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteOpaqueValueTest.java
index 0dd27d6c..15c9dd1e 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteOpaqueValueTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteOpaqueValueTest.java
@@ -50,6 +50,7 @@ public class WriteOpaqueValueTest {
                                 { ContentFormat.OPAQUE }, //
                                 { ContentFormat.TEXT }, //
                                 { ContentFormat.TLV }, //
+                                { ContentFormat.CBOR }, //
                                 { ContentFormat.fromCode(ContentFormat.OLD_TLV_CODE) }, //
                                 { ContentFormat.JSON }, //
                                 { ContentFormat.fromCode(ContentFormat.OLD_JSON_CODE) }, //
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteSingleValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteSingleValueTest.java
index 622fecaa..1724361a 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteSingleValueTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteSingleValueTest.java
@@ -59,6 +59,7 @@ public class WriteSingleValueTest {
         return Arrays.asList(new Object[][] { //
                                 { ContentFormat.TEXT }, //
                                 { ContentFormat.TLV }, //
+                                { ContentFormat.CBOR }, //
                                 { ContentFormat.fromCode(ContentFormat.OLD_TLV_CODE) }, //
                                 { ContentFormat.JSON }, //
                                 { ContentFormat.fromCode(ContentFormat.OLD_JSON_CODE) }, //
@@ -130,7 +131,7 @@ public class WriteSingleValueTest {
     @Test
     public void write_integer_resource() throws InterruptedException {
         // write resource
-        long expectedvalue = 999l;
+        long expectedvalue = -999l;
         WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
                 new WriteRequest(contentFormat, TEST_OBJECT_ID, 0, INTEGER_RESOURCE_ID, expectedvalue));
 
@@ -197,7 +198,7 @@ public class WriteSingleValueTest {
         // write resource
         Date expectedvalue = new Date(946681000l); // second accuracy
         WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new WriteRequest(contentFormat, TEST_OBJECT_ID, 0, TIME_RESOURCE_ID, expectedvalue));
+                new WriteRequest(contentFormat, TEST_OBJECT_ID, 0, TIME_RESOURCE_ID, expectedvalue), 100000000);
 
         // verify result
         assertEquals(ResponseCode.CHANGED, response.getCode());
@@ -206,7 +207,7 @@ public class WriteSingleValueTest {
 
         // read resource to check the value changed
         ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(contentFormat, TEST_OBJECT_ID, 0, TIME_RESOURCE_ID));
+                new ReadRequest(contentFormat, TEST_OBJECT_ID, 0, TIME_RESOURCE_ID), 100000000);
         LwM2mResource resource = (LwM2mResource) readResponse.getContent();
         assertEquals(expectedvalue, resource.getValue());
     }