# Tutoriel Codec et Mapping

Ce guide détaille comment développer un codec et un mapping de capteur tiers en utilisant un exemple simple. Imaginons un capteur qui qui gère la quantité de café disponible dans une machine. Il possède deux fonctions :

* Il envoie périodiquement la quantité de café restant en litres.
* Il peut recevoir une commande pour définir la nouvelle quantité de café disponible en litre.

Le capteur envoie l'uplink et le downlink suivant :

* **Uplink** (Sortie Capteur) : `0x2E014D` (`0x2E` = Entête, `0x014D` = `333`  = Valeur)
  * Il reste 333L de café disponible.
* **Downlink** (Commande) : `0x3E025A` (`0x3E` = Entête, `0xO25A` = `602` Valeur).
  * Le capteur va mettre 602L de café à disposition.

## Le Codec (Logique JavaScript)

Le codec utilise le standard ChirpStack V4. Il transforme les octets bruts en objets lisibles (JSON) et inversement.

### **Script de décodage et d'encodage**

{% 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 %}

### **JSON du Mapping**

{% 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 %}

### **Test du codec et du mapping (couverture)**

Pour vérifier le comportement du codec, il est possible de définir des tests simplement. Ces tests permettront également de vérifier la couverture mapping/test.

A la fin du codec, ajouter la ligne suivante :

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

Ensuite, dans le même dossier et en considérant que le fichier du codec se nomme `coffee.codec.js`, ajouter les fichiers suivants :

{% 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('Suite de tests Codec IoT', () => {

  // --- 1. Functional tests ---
  testCases.forEach((testCase, index) => {
    test(`[${testCase.type.toUpperCase()}] Test n°${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 %}

Dans le fichier de test ci-dessus :

* `"bytes": [46, 1, 77]` est l'uplink `2E 01 4D` de l'exemple.
* `"bytes": [62, 2, 90]` est le downlink `3E 02 5A` de l'exemple.

Enfin, en utilisant la commande `npm`, initialiser les tests avec `npm install --save-dev jest` et exécuter les tests avec `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
  Suite de tests Codec IoT
    ✓ [UPLINK] Cas n°1: Get available volume of coffee. (1 ms)
    ✓ [DOWNLINK-ENCODE] Cas n°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
```

## Le Mapping (Lien Modbus)

## **Explication du Mapping**

Le Mapping définit comment les noms de variables créés dans le codec sont rangés dans les registres Modbus de la Gateway.

Il se décompose en deux sections : `uplink` et `config`. Chacune des sections comporte un tableau `fields` d'objets décrivant la création du registre Modbus correspondant. Voici le détail de l'objet JSON :

* **name** : Le nom de la valeur (uplink) ou de la commande (downlink) contenue dans le registre.
* **description** : Une description de la valeur (Ex: `Température extérieure`)
* **data\_type** : Il existe deux types qui sont `NUMBER` et `BOOL`.
  * **`NUMBER`** : Utilisé pour les données de type numérique.
  * **`BOOL`** : Utilisé pour les données pouvant prendre les valeurs `0` ou `1`.&#x20;
* **numeric\_type** : Si le `data_type` est `NUMBER` , il faut préciser le format : `UINT8`, `INT8`, `UINT16`, `INT16`, `UINT32`, `INT32` .
* **access\_mode** : Définit l'accès au registre.
  * **`R`** : **Read-only**, le registre est en lecture seule.
  * **`W` :** **Write-only**, le registre en écriture seule.
  * **`R/W`** : **Read-Write**, le registre est accessible en lecture et en écriture.
* **unit** : Il s'agit de l'unité de la valeur contenue (Ex: `°C`). Il est possible de laisser vide ce champ.
* **protocol\_specific** : Certain protocole comme Modbus nécessite des information supplémentaires.
  * **modbus** : Cet section concerne les paramètres spécifiques pour les registre Modbus.
    * **register\_type** : Le type de registre est à définir.
      * **`discrete`** : Registre pour une valeur de type **`BOOL`** en lecture seule (**`R`**).
      * **`coil`** : Registre pour une valeur de type **`BOOL`** en lecture/écriture (**`W`**, **`R/W`**).
      * **`Input`** : Registre pour une valeur de type **`NUMBER`** en lecture seule (**`R`**).
      * **`holding`** : Registre pour une valeur de type **`NUMBER`** en lecture/écriture (**`W`**, **`R/W`**).
    * **factor** : Facteur de multiplication optionnel à utiliser pour transformer la donnée.

{% hint style="warning" %}
Il est important de vérifier la cohérence des informations définies dans le mapping entre les champs **data\_type**, **access\_mode** et **protocol\_specific.modbus.register\_type**.\
\
Exemple : si **data\_type est `NUMERIC`**, **protocol\_specific.modbus.register\_type ne peux pas valoir `coil`.**
{% endhint %}

{% hint style="danger" %}
**Il est à noter également que les type FLOAT32 et FLOAT64 ne sont pas supportés.**
{% endhint %}


---

# Agent Instructions: 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:

```
GET https://enless.gitbook.io/centre-aide/ressources/gateway-lorawan/tutoriel-codec-et-mapping.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
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.
