> For the complete documentation index, see [llms.txt](https://enless.gitbook.io/centre-aide/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://enless.gitbook.io/centre-aide/ressources/ressources-en/lorawan-gateway/codec-and-mapping-tutorial.md).

# Codec and Mapping tutorial

This guide details how to develop a third-party codec and sensor mapping using a simple example. Imagine a sensor that manages the amount of coffee available in a machine. It has two functions:

* It periodically sends the amount of coffee remaining in liters.
* It can receive a command to set the new amount of coffee available in liters.

The sensor sends the following uplink and downlink:

* **Uplink** (Sensor output): `0x2E014D` (`0x2E` = Header, `0x014D` = `333`  = Value)
  * There is 333L of coffee remaining.
* **Downlink** (Command): `0x3E025A` (`0x3E` = Header, `0xO25A` = `602` Value).
  * The sensor will make 602L of coffee available.

## The Codec (JavaScript Logic)

The codec uses the ChirpStack V4 standard. It converts raw bytes into readable objects (JSON) and vice versa.

### **Decoding and encoding script**

{% code title="coffee.codec.js" %}

```javascript
// --- UPLINK decoding ---
function decodeUplink(input) {
  var bytes = input.bytes;
  var data = {};

  if (bytes[0] === 0x2E) {
    // (0x01 << 8) + 0x4D = 256 + 77 = 333
    data.availableCoffeeVolume = (bytes[1] << 8) | bytes[2];
  }
  return { data: data };
}

// --- DOWNLINK decoding ---
function encodeDownlink(input) {
  var bytes = [];
  var key, i;
  
  for (key in input.data) {
    if (!input.data.hasOwnProperty(key))
      continue;

    switch (key) {
      case "setAvailableCoffeeVolume":
        const new_value = input.data[key];
        // build downlink
        bytes.push(0x3e, (new_value >> 8) & 0xFF, new_value & 0xFF);
        break;
        
      default:
        break;
    }
  }
  return {
    bytes: bytes,
    fPort: 1
  };
}
```

{% endcode %}

### **Mapping JSON**

{% code title="coffee.mapping.json" %}

```json
{
  "uplink": {
    "fields": [
      {
        "name": "availableCoffeeVolume",
        "description": "Current volume of coffee available",
        "data_type": "NUMBER",
        "numeric_type": "UINT16",
        "access_mode": "R",
        "unit": "L",
        "protocol_specific": {
          "modbus": { "register_type": "input", "factor": 1 }
        }
      }
    ]
  },
  "config": {
    "fields": [
      {
        "name": "setAvailableCoffeeVolume",
        "description": "Change the available coffee volume",
        "data_type": "NUMBER",
        "numeric_type": "UNIT16",
        "access_mode": "R/W",
        "unit": "L",
        "protocol_specific": {
          "modbus": { "register_type": "holding", "factor": 1 }
        }
      }
    ]
  },
    "lns_metadata":{
    "fields":[
      {
        "name":"rssi",
        "description":"Received RSSI",
        "data_type":"NUMBER",
        "numeric_type":"INT16",
        "access_mode":"R",
        "unit":"dBm",
        "protocol_specific":{
          "modbus":{
            "register_type":"input",
            "offset":1,
            "factor":1
          }
        }
      },
      {
        "name":"snr",
        "description":"Received SNR",
        "data_type":"NUMBER",
        "numeric_type":"INT16",
        "access_mode":"R",
        "unit":"dB",
        "protocol_specific":{
          "modbus":{
            "register_type":"input",
            "offset":2,
            "factor":1
          }
        }
      },
      {
        "name":"sf",
        "description":"Spreading Factor",
        "data_type":"NUMBER",
        "numeric_type":"UINT8",
        "access_mode":"R",
        "protocol_specific":{
          "modbus":{
            "register_type":"input",
            "offset":3,
            "factor":1
          }
        }
      },
      {
        "name":"minutesSinceLastRx",
        "description":"Minutes since last reception",
        "data_type":"NUMBER",
        "numeric_type":"UINT16",
        "access_mode":"R",
        "unit":"min",
        "protocol_specific":{
          "modbus":{
            "register_type":"input",
            "offset":4,
            "factor":1
          }
        }
      }
    ]
  }
}  
```

{% endcode %}

### **Codec and Mapping Tests (coverage)**

To verify the behavior of the codec, it is possible to define tests simply. These tests will also make it possible to verify mapping/test coverage.

At the end of the codec, add the following line:

```javascript
module.exports = { decodeUplink, encodeDownlink };
```

Then, in the same folder and assuming the codec file is called `coffee.codec.js`, add the following files:

{% code title="package.json" %}

```json
{
  "name": "coffee-example",
  "version": "1.0.0",
  "type": "commonjs",
  "main": "coffee.codec.js",
  "scripts": {
    "test": "jest"
  },
  "devDependencies": {
    "jest": "^30.3.0"
  }
}

```

{% endcode %}

{% code title="coffee.codec.test.js" %}

```js
const { decodeUplink, encodeDownlink } = require('./coffee.codec.js');
const testCases = require('./coffee.codec.test.json');
const mapping = require('./coffee.mapping.json');

describe('IoT Codec Test Suite', () => {

  // --- 1. Functional tests ---
  testCases.forEach((testCase, index) => {
    test(`[${testCase.type.toUpperCase()}] Test no. ${index + 1}: ${testCase.description}`, () => {
      let result;

      if (testCase.type === 'uplink-decode') {
        result = decodeUplink(testCase.input);
      } 
      else if (testCase.type === 'downlink-encode') {
        result = encodeDownlink(testCase.input);
      }

      expect(result).toEqual(testCase.output);
    });

  });

  // --- 2. Mapping Coverage Validation ---
  describe('Modbus Mapping Consistency', () => {
    
    test('Each UPLINK field in the mapping must be included in at least one test', () => {
      const mappingFields = mapping.uplink.fields.map(f => f.name);
      const testedFields = testCases
        .filter(tc => tc.type === 'uplink-decode')
        .flatMap(tc => Object.keys(tc.output.data));

      mappingFields.forEach(fieldName => {
        expect(testedFields).toContain(fieldName);
      });
    });

    test('Each CONFIG field in the mapping must be included in at least one test', () => {
      const mappingFields = mapping.config.fields.map(f => f.name);
      const testedFields = testCases
        .filter(tc => tc.type === 'downlink-encode')
        .flatMap(tc => Object.keys(tc.input.data));

      mappingFields.forEach(fieldName => {
        expect(testedFields).toContain(fieldName);
      });
    });
  });
});

```

{% endcode %}

{% code title="coffee.codec.test.json" %}

```json
[
  {
    "type": "uplink-decode",
    "description": "Get available volume of coffee.",
    "input": {
      "bytes": [46, 1, 77],
      "fPort": 1,
      "recvTime": "2026-03-19T09:51:25.508Z"
    },
    "output": {
      "data": {
        "availableCoffeeVolume": 333
      }
    }
  },
  {
    "type": "downlink-encode",
    "description": "Set the available coffee volume.",
    "input": {
      "data": {
        "setAvailableCoffeeVolume": 602
      }
    },
    "output": {
      "fPort": 1,
      "bytes": [62, 2, 90]
    }
  }
]
```

{% endcode %}

In the test file above:

* `"bytes": [46, 1, 77]` is the uplink `2E 01 4D` from the example.
* `"bytes": [62, 2, 90]` is the downlink `3E 02 5A` from the example.

Finally, using the command `npm`, initialize the tests with `npm install --save-dev jest` and run the tests with `npm test` :

```zsh
$ npm install --save-dev jest
added 294 packages in 1s

44 packages are looking for funding
  run `npm fund` for details

$ npm test
> coffee@1.0.0 test
> jest

(node:26375) Warning: `--localstorage-file` was provided without a valid path
(Use `node --trace-warnings ...` to show where the warning was created)
 PASS  ./coffee.codec.test.js
  IoT Codec Test Suite
    ✓ [UPLINK] Case no. 1: Get available volume of coffee. (1 ms)
    ✓ [DOWNLINK-ENCODE] Case no. 2: Set the available coffee volume.

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.101 s, estimated 1 s
Ran all test suites
```

## The Mapping (Modbus Link)

## **Mapping Explanation**

The Mapping defines how the variable names created in the codec are stored in the Gateway's Modbus registers.

It is divided into two sections: `uplink` and `config`. Each section contains an array `fields` of objects describing the creation of the corresponding Modbus register. Here is the detail of the JSON object:

* **name** : The name of the value (uplink) or command (downlink) contained in the register.
* **description** : A description of the value (e.g.: `Outside temperature`)
* **data\_type** : There are two types, which are `NUMBER` and `BOOL`.
  * **`NUMBER`** : Used for numeric data.
  * **`BOOL`** : Used for data that can take the values `0` or `1`.&#x20;
* **numeric\_type** : If the `data_type` is `NUMBER` , the format must be specified: `UINT8`, `INT8`, `UINT16`, `INT16`, `UINT32`, `INT32` .
* **access\_mode** : Defines access to the register.
  * **`R`** : **Read-only**, the register is read-only.
  * **`W` :** **Write-only**, the register is write-only.
  * **`R/W`** : **Read-Write**, the register can be read and written.
* **unit** : This is the unit of the contained value (e.g.: `°C`). This field can be left empty.
* **protocol\_specific** : Some protocols like Modbus require additional information.
  * **modbus** : This section concerns the specific parameters for Modbus registers.
    * **register\_type** : The register type must be defined.
      * **`discrete`** : Register for a value of type **`BOOL`** in read-only (**`R`**).
      * **`coil`** : Register for a value of type **`BOOL`** in read/write (**`W`**, **`R/W`**).
      * **`Input`** : Register for a value of type **`NUMBER`** in read-only (**`R`**).
      * **`holding`** : Register for a value of type **`NUMBER`** in read/write (**`W`**, **`R/W`**).
    * **factor** : Optional multiplication factor to use to transform the data.

{% hint style="warning" %}
It is important to verify the consistency of the information defined in the mapping between the fields **data\_type**, **access\_mode** and **protocol\_specific.modbus.register\_type**.\
\
Example: if **data\_type is `NUMERIC`**, **protocol\_specific.modbus.register\_type cannot be `coil`.**
{% endhint %}

{% hint style="warning" %}
The fields in the "uplink" section are read-only ("Read Only") and cannot be edited.
{% endhint %}

{% hint style="danger" %}
**It should also be noted that the FLOAT32 and FLOAT64 types are not supported.**
{% endhint %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://enless.gitbook.io/centre-aide/ressources/ressources-en/lorawan-gateway/codec-and-mapping-tutorial.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
