Repository: eclipse/leshan
Pull request #922 (2020-11-05 10:24:56)

Add SenML-CBOR Encoder/Decoder 
Currently based on Jackson library but regarding https://github.com/eclipse/leshan/issues/866#issuecomment-696228053 this could be replaced by another CBOR library.


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 4eb0f682..07ba6059 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
@@ -32,11 +32,12 @@ import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 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.LwM2mNodeSenMLJsonDecoder;
+import org.eclipse.leshan.core.node.codec.senml.LwM2mNodeSenMLDecoder;
 import org.eclipse.leshan.core.node.codec.text.LwM2mNodeTextDecoder;
 import org.eclipse.leshan.core.node.codec.tlv.LwM2mNodeTlvDecoder;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.util.Validate;
+import org.eclipse.leshan.senml.cbor.jackson.SenMLCborJacksonEncoderDecoder;
 import org.eclipse.leshan.senml.json.jackson.SenMLJsonJacksonEncoderDecoder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -58,7 +59,8 @@ 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.SENML_JSON, new LwM2mNodeSenMLJsonDecoder(new SenMLJsonJacksonEncoderDecoder()));
+        decoders.put(ContentFormat.SENML_JSON, new LwM2mNodeSenMLDecoder(new SenMLJsonJacksonEncoderDecoder()));
+        decoders.put(ContentFormat.SENML_CBOR, new LwM2mNodeSenMLDecoder(new SenMLCborJacksonEncoderDecoder()));
 
         // tlv
         LwM2mNodeTlvDecoder tlvDecoder = new LwM2mNodeTlvDecoder();
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 ff31a065..b2ff3075 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
@@ -26,11 +26,12 @@ import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 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.LwM2mNodeSenMLJsonEncoder;
+import org.eclipse.leshan.core.node.codec.senml.LwM2mNodeSenMLEncoder;
 import org.eclipse.leshan.core.node.codec.text.LwM2mNodeTextEncoder;
 import org.eclipse.leshan.core.node.codec.tlv.LwM2mNodeTlvEncoder;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.util.Validate;
+import org.eclipse.leshan.senml.cbor.jackson.SenMLCborJacksonEncoderDecoder;
 import org.eclipse.leshan.senml.json.jackson.SenMLJsonJacksonEncoderDecoder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -52,7 +53,8 @@ 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.SENML_JSON, new LwM2mNodeSenMLJsonEncoder(new SenMLJsonJacksonEncoderDecoder()));
+        encoders.put(ContentFormat.SENML_JSON, new LwM2mNodeSenMLEncoder(new SenMLJsonJacksonEncoderDecoder()));
+        encoders.put(ContentFormat.SENML_CBOR, new LwM2mNodeSenMLEncoder(new SenMLCborJacksonEncoderDecoder()));
 
         // tlv
         LwM2mNodeTlvEncoder tlvDecoder = new LwM2mNodeTlvEncoder();
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLDecoder.java
similarity index 98%
rename from leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonDecoder.java
rename to leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLDecoder.java
index b6944128..3e9aff85 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLDecoder.java
@@ -50,13 +50,13 @@ import org.eclipse.leshan.senml.SenMLRecord;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class LwM2mNodeSenMLJsonDecoder implements TimestampedNodeDecoder {
+public class LwM2mNodeSenMLDecoder implements TimestampedNodeDecoder {
 
-    private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeSenMLJsonDecoder.class);
+    private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeSenMLDecoder.class);
 
     private final SenMLDecoder decoder;
 
-    public LwM2mNodeSenMLJsonDecoder(SenMLDecoder decoder) {
+    public LwM2mNodeSenMLDecoder(SenMLDecoder decoder) {
         this.decoder = decoder;
     }
 
@@ -382,7 +382,7 @@ public class LwM2mNodeSenMLJsonDecoder implements TimestampedNodeDecoder {
     }
 
     private Object parseResourceValue(Object value, Type expectedType, LwM2mPath path) throws CodecException {
-        LOG.trace("Parse SenML JSON value for path {} and expected type {}: {}", path, expectedType, value);
+        LOG.trace("Parse SenML value for path {} and expected type {}: {}", path, expectedType, value);
 
         try {
             switch (expectedType) {
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLEncoder.java
similarity index 95%
rename from leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonEncoder.java
rename to leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLEncoder.java
index b5feeda8..78deb168 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLEncoder.java
@@ -42,12 +42,12 @@ import org.eclipse.leshan.senml.SenMLRecord;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class LwM2mNodeSenMLJsonEncoder implements TimestampedNodeEncoder {
-    private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeSenMLJsonEncoder.class);
+public class LwM2mNodeSenMLEncoder implements TimestampedNodeEncoder {
+    private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeSenMLEncoder.class);
 
     private final SenMLEncoder encoder;
 
-    public LwM2mNodeSenMLJsonEncoder(SenMLEncoder encoder) {
+    public LwM2mNodeSenMLEncoder(SenMLEncoder encoder) {
         this.encoder = encoder;
     }
 
@@ -122,7 +122,7 @@ public class LwM2mNodeSenMLJsonEncoder implements TimestampedNodeEncoder {
 
         @Override
         public void visit(LwM2mObject object) {
-            LOG.trace("Encoding Object {} into SenML JSON", object);
+            LOG.trace("Encoding Object {} into SenML", object);
             // Validate request path
             if (!requestPath.isObject()) {
                 throw new CodecException("Invalid request path %s for object encoding", requestPath);
@@ -139,7 +139,7 @@ public class LwM2mNodeSenMLJsonEncoder implements TimestampedNodeEncoder {
 
         @Override
         public void visit(LwM2mObjectInstance instance) {
-            LOG.trace("Encoding object instance {} into SenML JSON", instance);
+            LOG.trace("Encoding object instance {} into SenML", instance);
             for (LwM2mResource resource : instance.getResources().values()) {
                 // Validate request path & compute resource path
                 String prefixPath;
@@ -168,7 +168,7 @@ public class LwM2mNodeSenMLJsonEncoder implements TimestampedNodeEncoder {
 
         @Override
         public void visit(LwM2mResourceInstance resourceInstance) {
-            LOG.trace("Encoding resource instance {} into SenML JSON", resourceInstance);
+            LOG.trace("Encoding resource instance {} into SenML", resourceInstance);
             if (!requestPath.isResourceInstance()) {
                 throw new CodecException("Invalid request path %s for resource  instance encoding", requestPath);
             }
@@ -231,7 +231,7 @@ public class LwM2mNodeSenMLJsonEncoder implements TimestampedNodeEncoder {
         }
 
         private void setResourceValue(Object value, Type type, LwM2mPath resourcePath, SenMLRecord record) {
-            LOG.trace("Encoding resource value {} in SenML JSON", value);
+            LOG.trace("Encoding resource value {} in SenML", value);
 
             if (type == null || type == Type.NONE) {
                 throw new CodecException(
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 85945dfd..23dec12e 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
@@ -30,6 +30,7 @@ public class ContentFormat {
     public static final int OLD_TLV_CODE = 1542;
 
     public static final int SENML_JSON_CODE = 110;
+    public static final int SENML_CBOR_CODE = 112;
 
     public static final ContentFormat TLV = new ContentFormat("TLV", "application/vnd.oma.lwm2m+tlv", TLV_CODE);
     public static final ContentFormat JSON = new ContentFormat("JSON", "application/vnd.oma.lwm2m+json", JSON_CODE);
@@ -38,11 +39,13 @@ public class ContentFormat {
     public static final ContentFormat LINK = new ContentFormat("LINK", "application/link-format", LINK_CODE);
     public static final ContentFormat SENML_JSON = new ContentFormat("SENML_JSON", "application/senml+json",
             SENML_JSON_CODE);
+    public static final ContentFormat SENML_CBOR = new ContentFormat("SENML_CBOR", "application/senml+cbor",
+            SENML_CBOR_CODE);
 
     public static final ContentFormat DEFAULT = TLV;
 
-    private static final ContentFormat knownContentFormat[] = new ContentFormat[] { TLV, JSON, SENML_JSON, TEXT, OPAQUE,
-                            LINK };
+    private static final ContentFormat knownContentFormat[] = new ContentFormat[] { TLV, JSON, SENML_JSON, SENML_CBOR,
+                            TEXT, OPAQUE, LINK };
 
     private final String name;
     private final String mediaType;
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/SenMLCbor.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/SenMLCbor.java
deleted file mode 100644
index 85020c29..00000000
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/SenMLCbor.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*******************************************************************************
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * and Eclipse Distribution License v1.0 which accompany this distribution.
- * 
- * The Eclipse Public License is available at
- *    http://www.eclipse.org/legal/epl-v10.html
- * and the Eclipse Distribution License is available at
- *    http://www.eclipse.org/org/documents/edl-v10.html.
- * 
- * Contributors:
- *     Boya Zhang - initial API and implementation
- *******************************************************************************/
-
-package org.eclipse.leshan.senml.cbor;
-
-import org.eclipse.leshan.senml.SenMLPack;
-
-/**
- * Helper for encoding/decoding SenML CBOR format
- */
-public class SenMLCbor {
-    private static final SenMLCborPackSerDes serDes = new SenMLCborPackSerDes();
-
-    public static byte[] toSenMLCbor(SenMLPack pack) throws SenMLCborException {
-        return serDes.serializeToCbor(pack);
-    }
-}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/SenMLCborPackSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/SenMLCborPackSerDes.java
deleted file mode 100644
index 7483d4b5..00000000
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/SenMLCborPackSerDes.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*******************************************************************************
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * and Eclipse Distribution License v1.0 which accompany this distribution.
- * 
- * The Eclipse Public License is available at
- *    http://www.eclipse.org/legal/epl-v10.html
- * and the Eclipse Distribution License is available at
- *    http://www.eclipse.org/org/documents/edl-v10.html.
- * 
- * Contributors:
- *     Boya Zhang - initial API and implementation
- *******************************************************************************/
-
-package org.eclipse.leshan.senml.cbor;
-
-import java.io.ByteArrayOutputStream;
-
-import org.eclipse.leshan.core.model.ResourceModel.Type;
-import org.eclipse.leshan.senml.SenMLPack;
-import org.eclipse.leshan.senml.SenMLRecord;
-
-import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
-import com.fasterxml.jackson.dataformat.cbor.CBORGenerator;
-
-public class SenMLCborPackSerDes {
-
-    public byte[] serializeToCbor(SenMLPack pack) throws SenMLCborException {
-        CBORFactory factory = new CBORFactory();
-        ByteArrayOutputStream out = new ByteArrayOutputStream();
-
-        try {
-            CBORGenerator generator = factory.createGenerator(out);
-            generator.writeStartArray(pack.getRecords().size());
-
-            for (SenMLRecord record : pack.getRecords()) {
-                boolean hasBaseName = false;
-                boolean hasBaseTime = false;
-                boolean hasName = false;
-                boolean hasTime = false;
-                int objectSize = 1;
-
-                if (record.getBaseName() != null && !record.getBaseName().isEmpty()) {
-                    hasBaseName = true;
-                    objectSize++;
-                }
-
-                if (record.getBaseTime() != null) {
-                    hasBaseTime = true;
-                    objectSize++;
-                }
-
-                if (record.getName() != null && !record.getName().isEmpty()) {
-                    hasName = true;
-                    objectSize++;
-                }
-
-                if (record.getTime() != null) {
-                    hasTime = true;
-                    objectSize++;
-                }
-
-                generator.writeStartObject(objectSize);
-
-                if (hasBaseName) {
-                    generator.writeFieldId(-2);
-                    generator.writeString(record.getBaseName());
-                }
-
-                if (hasBaseTime) {
-                    generator.writeFieldId(-3);
-                    generator.writeNumber(record.getBaseTime());
-                }
-
-                if (hasName) {
-                    generator.writeFieldId(0);
-                    generator.writeString(record.getName());
-                }
-
-                if (hasTime) {
-                    generator.writeFieldId(6);
-                    generator.writeNumber(record.getTime());
-                }
-
-                Type type = record.getType();
-                if (type != null) {
-                    switch (record.getType()) {
-                    case FLOAT:
-                        generator.writeFieldId(2);
-                        generator.writeNumber(record.getFloatValue().intValue());
-                        break;
-                    case BOOLEAN:
-                        generator.writeFieldId(4);
-                        generator.writeBoolean(record.getBooleanValue());
-                        break;
-                    case OBJLNK:
-                        generator.writeStringField("vlo", record.getObjectLinkValue());
-                        break;
-                    case OPAQUE:
-                        generator.writeFieldId(8);
-                        generator.writeBinary(record.getOpaqueValue());
-                    case STRING:
-                        generator.writeFieldId(3);
-                        generator.writeString(record.getStringValue());
-                        break;
-                    default:
-                        break;
-                    }
-                }
-                generator.writeEndObject();
-            }
-
-            generator.writeEndArray();
-            generator.close();
-        } catch (Exception ex) {
-            throw new SenMLCborException("Impossible to encode pack to CBOR: \n" + pack, ex);
-        }
-
-        return out.toByteArray();
-    }
-}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/SenMLCborException.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/jackson/SenMLCborException.java
similarity index 90%
rename from leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/SenMLCborException.java
rename to leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/jackson/SenMLCborException.java
index 229d0b06..e8e91e71 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/SenMLCborException.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/jackson/SenMLCborException.java
@@ -1,10 +1,10 @@
 /*******************************************************************************
  * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
+ * 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-v10.html
+ *    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.
  * 
@@ -12,7 +12,7 @@
  *     Boya Zhang - initial API and implementation
  *******************************************************************************/
 
-package org.eclipse.leshan.senml.cbor;
+package org.eclipse.leshan.senml.cbor.jackson;
 
 /**
  * Exception thrown in case of Cbor encoding error
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/jackson/SenMLCborJacksonEncoderDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/jackson/SenMLCborJacksonEncoderDecoder.java
new file mode 100644
index 00000000..bb0fa874
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/jackson/SenMLCborJacksonEncoderDecoder.java
@@ -0,0 +1,54 @@
+/*******************************************************************************
+ * 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:
+ *     Boya Zhang - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.leshan.senml.cbor.jackson;
+
+import java.io.IOException;
+
+import org.eclipse.leshan.senml.SenMLDecoder;
+import org.eclipse.leshan.senml.SenMLEncoder;
+import org.eclipse.leshan.senml.SenMLException;
+import org.eclipse.leshan.senml.SenMLPack;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.cbor.databind.CBORMapper;
+
+/**
+ * Helper for encoding/decoding SenML CBOR using Jackson
+ */
+public class SenMLCborJacksonEncoderDecoder implements SenMLDecoder, SenMLEncoder {
+    private static final SenMLCborPackSerDes serDes = new SenMLCborPackSerDes();
+    private static final ObjectMapper mapper = new CBORMapper();
+
+    @Override
+    public byte[] toSenML(SenMLPack pack) throws SenMLException {
+        if (pack == null)
+            return null;
+        return serDes.serializeToCbor(pack);
+    }
+
+    @Override
+    public SenMLPack fromSenML(byte[] jsonString) throws SenMLException {
+        try {
+            JsonNode node = mapper.readTree(jsonString);
+            if (!node.isArray()) {
+                throw new SenMLException("Unable to parse SenML CBOR: Array expected but was %s", node.getNodeType());
+            }
+            return serDes.deserializeFromCbor(node.iterator());
+        } catch (IOException e) {
+            throw new SenMLException("Unable to parse SenML CBOR.", e);
+        }
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/jackson/SenMLCborPackSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/jackson/SenMLCborPackSerDes.java
new file mode 100644
index 00000000..fe79e88b
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/jackson/SenMLCborPackSerDes.java
@@ -0,0 +1,213 @@
+/*******************************************************************************
+ * 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:
+ *     Boya Zhang - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.leshan.senml.cbor.jackson;
+
+import java.io.ByteArrayOutputStream;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Iterator;
+
+import org.eclipse.leshan.core.model.ResourceModel.Type;
+import org.eclipse.leshan.core.util.Base64;
+import org.eclipse.leshan.core.util.datatype.ULong;
+import org.eclipse.leshan.senml.SenMLException;
+import org.eclipse.leshan.senml.SenMLPack;
+import org.eclipse.leshan.senml.SenMLRecord;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
+import com.fasterxml.jackson.dataformat.cbor.CBORGenerator;
+
+public class SenMLCborPackSerDes {
+
+    public SenMLPack deserializeFromCbor(Iterator<JsonNode> nodes) throws SenMLException {
+        SenMLPack senMLPack = new SenMLPack();
+        while (nodes.hasNext()) {
+            JsonNode o = nodes.next();
+            if (o == null)
+                return null;
+
+            SenMLRecord record = new SenMLRecord();
+
+            JsonNode bn = o.get("-2");
+            if (bn != null && bn.isTextual())
+                record.setBaseName(bn.asText());
+
+            JsonNode bt = o.get("-3");
+            if (bt != null && bt.isNumber())
+                record.setBaseTime(bt.asLong());
+
+            JsonNode n = o.get("0");
+            if (n != null && n.isTextual())
+                record.setName(n.asText());
+
+            JsonNode t = o.get("6");
+            if (t != null && t.isNumber())
+                record.setTime(t.asLong());
+
+            JsonNode v = o.get("2");
+            boolean hasValue = false;
+            if (v != null && v.isNumber()) {
+                record.setFloatValue(v.numberValue());
+                hasValue = true;
+            }
+
+            JsonNode vb = o.get("4");
+            if (vb != null && vb.isBoolean()) {
+                record.setBooleanValue(vb.asBoolean());
+                hasValue = true;
+            }
+
+            JsonNode vs = o.get("3");
+            if (vs != null && vs.isTextual()) {
+                record.setStringValue(vs.asText());
+                hasValue = true;
+            }
+
+            JsonNode vlo = o.get("vlo");
+            if (vlo != null && vlo.isTextual()) {
+                record.setObjectLinkValue(vlo.asText());
+                hasValue = true;
+            }
+
+            JsonNode vd = o.get("8");
+            if (vd != null && vd.isTextual()) {
+                record.setOpaqueValue(Base64.decodeBase64(vd.asText()));
+                hasValue = true;
+            }
+
+            if (!hasValue)
+                throw new SenMLException("Invalid SenML record : record must have a value (v,vb,vlo,vd,vs) : %s", o);
+
+            senMLPack.addRecord(record);
+        }
+        return senMLPack;
+    }
+
+    public byte[] serializeToCbor(SenMLPack pack) throws SenMLException {
+        CBORFactory factory = new CBORFactory();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+        try (CBORGenerator generator = factory.createGenerator(out)) {
+            generator.writeStartArray(pack.getRecords().size());
+
+            for (SenMLRecord record : pack.getRecords()) {
+                boolean hasBaseName = false;
+                boolean hasBaseTime = false;
+                boolean hasName = false;
+                boolean hasTime = false;
+                int objectSize = 1;
+
+                if (record.getBaseName() != null && !record.getBaseName().isEmpty()) {
+                    hasBaseName = true;
+                    objectSize++;
+                }
+
+                if (record.getBaseTime() != null) {
+                    hasBaseTime = true;
+                    objectSize++;
+                }
+
+                if (record.getName() != null && !record.getName().isEmpty()) {
+                    hasName = true;
+                    objectSize++;
+                }
+
+                if (record.getTime() != null) {
+                    hasTime = true;
+                    objectSize++;
+                }
+
+                generator.writeStartObject(objectSize);
+
+                if (hasBaseName) {
+                    generator.writeFieldId(-2);
+                    generator.writeString(record.getBaseName());
+                }
+
+                if (hasBaseTime) {
+                    generator.writeFieldId(-3);
+                    generator.writeNumber(record.getBaseTime());
+                }
+
+                if (hasName) {
+                    generator.writeFieldId(0);
+                    generator.writeString(record.getName());
+                }
+
+                if (hasTime) {
+                    generator.writeFieldId(6);
+                    generator.writeNumber(record.getTime());
+                }
+
+                Type type = record.getType();
+                if (type != null) {
+                    switch (record.getType()) {
+                    case FLOAT:
+                        Number value = record.getFloatValue();
+                        generator.writeFieldId(2);
+                        if (value instanceof Byte) {
+                            generator.writeNumber(value.byteValue());
+                        } else if (value instanceof Short) {
+                            generator.writeNumber(value.shortValue());
+                        } else if (value instanceof Integer) {
+                            generator.writeNumber(value.intValue());
+                        } else if (value instanceof Long) {
+                            generator.writeNumber(value.longValue());
+                        } else if (value instanceof BigInteger) {
+                            generator.writeNumber((BigInteger) value);
+                        }
+                        // unsigned integer
+                        else if (value instanceof ULong) {
+                            generator.writeNumber(((ULong) value).toBigInteger());
+                        }
+                        // floating-point
+                        else if (value instanceof Float) {
+                            generator.writeNumber(value.floatValue());
+                        } else if (value instanceof Double) {
+                            generator.writeNumber(value.doubleValue());
+                        } else if (value instanceof BigDecimal) {
+                            generator.writeNumber((BigDecimal) value);
+                        }
+                        break;
+                    case BOOLEAN:
+                        generator.writeFieldId(4);
+                        generator.writeBoolean(record.getBooleanValue());
+                        break;
+                    case OBJLNK:
+                        generator.writeStringField("vlo", record.getObjectLinkValue());
+                        break;
+                    case OPAQUE:
+                        generator.writeFieldId(8);
+                        generator.writeBinary(record.getOpaqueValue());
+                    case STRING:
+                        generator.writeFieldId(3);
+                        generator.writeString(record.getStringValue());
+                        break;
+                    default:
+                        break;
+                    }
+                }
+                generator.writeEndObject();
+            }
+
+            generator.writeEndArray();
+        } catch (Exception ex) {
+            throw new SenMLException(ex, "Impossible to encode pack to CBOR: %s", pack, ex);
+        }
+
+        return out.toByteArray();
+    }
+}
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 af5cab9f..46a76cc1 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
@@ -26,6 +26,7 @@
 				<ul class="dropdown-menu dropdown-menu-contentformat" role="menu">
 					<li><a ng-click="settings.multi.format='TLV'">TLV</a></li>
 					<li><a ng-click="settings.multi.format='SENML_JSON'">SENML_JSON</a></li>
+					<li><a ng-click="settings.multi.format='SENML_CBOR'">SENML_CBOR</a></li>
 					<li><a ng-click="settings.multi.format='JSON'">JSON</a></li>
 				</ul>
 			</div>
@@ -37,6 +38,7 @@
 				<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='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>
 					<li><a ng-click="settings.single.format='Text'">Text</a></li>
 					<li><a ng-click="settings.single.format='Opaque'">Opaque</a></li>


Test patch:
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/senml/AbstractSenMLTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/AbstractSenMLTest.java
similarity index 93%
rename from leshan-core/src/test/java/org/eclipse/leshan/senml/AbstractSenMLTest.java
rename to leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/AbstractSenMLTest.java
index 57fb5029..421e3a89 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/senml/AbstractSenMLTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/AbstractSenMLTest.java
@@ -1,10 +1,10 @@
 /*******************************************************************************
  * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
+ * 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-v10.html
+ *    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.
  * 
@@ -12,7 +12,10 @@
  *     Boya Zhang - initial API and implementation
  *******************************************************************************/
 
-package org.eclipse.leshan.senml;
+package org.eclipse.leshan.core.senml.cbor;
+
+import org.eclipse.leshan.senml.SenMLPack;
+import org.eclipse.leshan.senml.SenMLRecord;
 
 public abstract class AbstractSenMLTest {
     protected void givenResourceWithFloatValue(SenMLPack pack, String n, Number value) {
@@ -78,7 +81,7 @@ public abstract class AbstractSenMLTest {
         givenResourceWithFloatValue(pack, "9", 100);
         givenResourceWithFloatValue(pack, "10", 15);
         givenResourceWithFloatValue(pack, "11/0", 0);
-        givenResourceWithFloatValue(pack, "13", 1367491215l);
+        givenResourceWithFloatValue(pack, "13", 1367491215);
 
         givenResourceWithStringValue(pack, "14", "+02:00");
         givenResourceWithStringValue(pack, "16", "U");
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLCborSerializerTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLCborSerializerTest.java
similarity index 56%
rename from leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLCborSerializerTest.java
rename to leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLCborSerializerTest.java
index 90878d26..be855bcc 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLCborSerializerTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLCborSerializerTest.java
@@ -1,10 +1,10 @@
 /*******************************************************************************
  * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
+ * 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-v10.html
+ *    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.
  * 
@@ -12,10 +12,11 @@
  *     Boya Zhang - initial API and implementation
  *******************************************************************************/
 
-package org.eclipse.leshan.senml;
+package org.eclipse.leshan.core.senml.cbor;
 
 import org.eclipse.leshan.core.util.Hex;
-import org.eclipse.leshan.senml.cbor.SenMLCbor;
+import org.eclipse.leshan.senml.SenMLPack;
+import org.eclipse.leshan.senml.cbor.jackson.SenMLCborJacksonEncoderDecoder;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -23,7 +24,7 @@ public class SenMLCborSerializerTest extends AbstractSenMLTest {
 
     @Test
     public void serialize_device_object_to_senml_cbor() throws Exception {
-        byte[] cbor = SenMLCbor.toSenMLCbor(givenDeviceObjectInstance());
+        byte[] cbor = new SenMLCborJacksonEncoderDecoder().toSenML(givenDeviceObjectInstance());
 
         String expectedValue = "90a321652f332f302f00613003744f70656e204d6f62696c6520416c6c69616e6365a200613103764c696768747765696768"
                 + "74204d324d20436c69656e74a20061320369333435303030313233a20061330363312e30a20063362f300201a20063362f310205a20063372f"
@@ -32,4 +33,15 @@ public class SenMLCborSerializerTest extends AbstractSenMLTest {
 
         Assert.assertTrue(expectedValue.equals(Hex.encodeHexString(cbor)));
     }
+
+    @Test
+    public void deserialize_device_object_to_senml_cbor() throws Exception {
+        String inputValue = "90a321652f332f302f00613003744f70656e204d6f62696c6520416c6c69616e6365a200613103764c696768747765696768"
+                + "74204d324d20436c69656e74a20061320369333435303030313233a20061330363312e30a20063362f300201a20063362f310205a20063372f"
+                + "3002190ed8a20063372f3102191388a20063382f3002187da20063382f3102190384a2006139021864a200623130020fa2006431312f300200"
+                + "a200623133021a5182428fa20062313403662b30323a3030a200623136036155";
+
+        SenMLPack pack = new SenMLCborJacksonEncoderDecoder().fromSenML(Hex.decodeHex(inputValue.toCharArray()));
+        Assert.assertEquals(givenDeviceObjectInstance(), pack);
+    }
 }
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLJsonSerDesTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLJsonSerDesTest.java
similarity index 88%
rename from leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLJsonSerDesTest.java
rename to leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLJsonSerDesTest.java
index 33319d62..cf77545c 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLJsonSerDesTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLJsonSerDesTest.java
@@ -1,10 +1,10 @@
 /*******************************************************************************
  * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
+ * 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-v10.html
+ *    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.
  * 
@@ -12,11 +12,15 @@
  *     Boya Zhang - initial API and implementation
  *******************************************************************************/
 
-package org.eclipse.leshan.senml;
+package org.eclipse.leshan.core.senml.cbor;
 
 import java.util.Arrays;
 import java.util.Collection;
 
+import org.eclipse.leshan.senml.SenMLDecoder;
+import org.eclipse.leshan.senml.SenMLEncoder;
+import org.eclipse.leshan.senml.SenMLException;
+import org.eclipse.leshan.senml.SenMLPack;
 import org.eclipse.leshan.senml.json.jackson.SenMLJsonJacksonEncoderDecoder;
 import org.eclipse.leshan.senml.json.minimaljson.SenMLJsonMinimalEncoderDecoder;
 import org.junit.Assert;
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/create/CreateTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/create/CreateTest.java
index f1dfa8d7..75f8e41d 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/create/CreateTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/create/CreateTest.java
@@ -56,7 +56,8 @@ public class CreateTest {
         return Arrays.asList(new Object[][] { //
                                 { ContentFormat.TLV }, //
                                 { ContentFormat.JSON }, //
-                                { ContentFormat.SENML_JSON } });
+                                { ContentFormat.SENML_JSON }, //
+                                { ContentFormat.SENML_CBOR } });
     }
 
     private ContentFormat contentFormat;
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTimeStampTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTimeStampTest.java
index 22b536ef..0ab590a4 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTimeStampTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTimeStampTest.java
@@ -56,7 +56,8 @@ public class ObserveTimeStampTest {
     public static Collection<?> contentFormats() {
         return Arrays.asList(new Object[][] { //
                                 { ContentFormat.JSON }, //
-                                { ContentFormat.SENML_JSON } });
+                                { ContentFormat.SENML_JSON }, //
+                                { ContentFormat.SENML_CBOR } });
     }
 
     protected IntegrationTestHelper helper = new IntegrationTestHelper();
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadMultiValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadMultiValueTest.java
index cbe471e5..9ef0715f 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadMultiValueTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadMultiValueTest.java
@@ -46,7 +46,8 @@ public class ReadMultiValueTest {
         return Arrays.asList(new Object[][] { //
                                 { ContentFormat.TLV }, //
                                 { ContentFormat.JSON }, //
-                                { ContentFormat.SENML_JSON } });
+                                { ContentFormat.SENML_JSON }, //
+                                { ContentFormat.SENML_CBOR } });
     }
 
     private ContentFormat contentFormat;
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 05e41ed4..59f67e2c 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
@@ -47,7 +47,8 @@ public class ReadOpaqueValueTest {
                                 { ContentFormat.TEXT }, //
                                 { ContentFormat.TLV }, //
                                 { ContentFormat.JSON }, //
-                                { ContentFormat.SENML_JSON } });
+                                { ContentFormat.SENML_JSON }, //
+                                { ContentFormat.SENML_CBOR } });
     }
 
     private ContentFormat contentFormat;
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 ea6a6ac6..78447936 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
@@ -46,7 +46,8 @@ public class ReadSingleValueTest {
                                 { ContentFormat.TEXT }, //
                                 { ContentFormat.TLV }, //
                                 { ContentFormat.JSON }, //
-                                { ContentFormat.SENML_JSON } });
+                                { ContentFormat.SENML_JSON }, //
+                                { ContentFormat.SENML_CBOR } });
     }
 
     private ContentFormat contentFormat;
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteMultiValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteMultiValueTest.java
index c2f6b342..6c05c283 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteMultiValueTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteMultiValueTest.java
@@ -58,7 +58,8 @@ public class WriteMultiValueTest {
                                 { ContentFormat.fromCode(ContentFormat.OLD_TLV_CODE) }, //
                                 { ContentFormat.JSON }, //
                                 { ContentFormat.fromCode(ContentFormat.OLD_JSON_CODE) }, //
-                                { ContentFormat.SENML_JSON } });
+                                { ContentFormat.SENML_JSON }, //
+                                { ContentFormat.SENML_CBOR } });
     }
 
     private ContentFormat contentFormat;
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 60bcaab1..9f2ec66b 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
@@ -53,7 +53,8 @@ public class WriteOpaqueValueTest {
                                 { ContentFormat.fromCode(ContentFormat.OLD_TLV_CODE) }, //
                                 { ContentFormat.JSON }, //
                                 { ContentFormat.fromCode(ContentFormat.OLD_JSON_CODE) }, //
-                                { ContentFormat.SENML_JSON } });
+                                { ContentFormat.SENML_JSON }, //
+                                { ContentFormat.SENML_CBOR } });
     }
 
     private ContentFormat contentFormat;
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 5c9330ca..a3362a57 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
@@ -62,7 +62,8 @@ public class WriteSingleValueTest {
                                 { ContentFormat.fromCode(ContentFormat.OLD_TLV_CODE) }, //
                                 { ContentFormat.JSON }, //
                                 { ContentFormat.fromCode(ContentFormat.OLD_JSON_CODE) }, //
-                                { ContentFormat.SENML_JSON } });
+                                { ContentFormat.SENML_JSON }, //
+                                { ContentFormat.SENML_CBOR } });
     }
 
     private ContentFormat contentFormat;