> For the complete documentation index, see [llms.txt](https://adatrack.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://adatrack.gitbook.io/docs/documentation/iot-ingestion/decoders.md).

# Custom Decoders

This document provides technical details on the **Goja-based** JavaScript decoding environment and advanced parsing techniques for IoT binary data.

### The Runtime Environment

AdaTrack embeds the **Goja** engine, a pure Go implementation of JavaScript. The scripting runtime includes a built-in **Babel transpiler** that automatically converts modern JavaScript (ES6+) to ES5 before execution, so you can write decoders using arrow functions, `const`/`let`, template literals, destructuring, default parameters, classes, and other modern syntax.

* **ES6+ Support:** Write decoders with modern JavaScript syntax. The platform transpiles ES6+ to ES5 transparently using an embedded Babel runtime. Existing ES5 scripts continue to work without modification.
* **Concurrency:** Goja runtimes are not thread-safe. AdaTrack maintains a `sync.Pool` of runtimes. Each worker borrows a runtime, executes the script, and returns it to the pool.
* **Compilation Cache:** Decoder scripts are compiled once and cached using a content-addressed LRU cache (keyed by SHA-256 hash of the source). Identical scripts across different profiles share the same compiled program.
* **Performance:** While Goja does not use JIT compilation (like V8), it is extremely efficient for bitwise operations and object manipulation, typically completing a decode in under 500 microseconds. The compilation cache ensures the transpilation + compilation cost is paid only once per unique script.
* **Sandboxing:** The environment is strictly isolated. No `require`, `process`, or `fs` modules are available. `eval()` and the `Function` constructor are disabled to prevent dynamic code generation. A call stack depth limit (default: 1024 frames) guards against stack overflow from infinite recursion.

### Input Specification

Your `Decoder(bytes, port)` function receives:

* `bytes`: An array-like object of numbers (0-255). Use `bytes[i]` and `bytes.length`. To read floats or multi-byte integers with `DataView`, wrap it first: `new DataView(new Uint8Array(bytes).buffer)`.
* `port`: A number. For UDP telemetry this is always `0`; the Decoder Playground passes the value you enter.

The function must be a global named `Decoder` (capital D). It must return a plain object. Numeric `latitude` and `longitude` fields drive the map, geofences and trips (strings are ignored); numeric `rssi` and `snr` are stored in their own columns. Everything you return is also kept in the telemetry JSON payload.

### Decoder Examples

#### ES5 Style (Classic)

```javascript
function Decoder(bytes, port) {
    var temp = (bytes[0] << 8) | bytes[1];
    return {
        temperature: temp / 100.0,
        battery: bytes[2]
    };
}
```

#### ES6+ Style (Modern)

```javascript
const Decoder = (bytes, port) => {
    const temp = (bytes[0] << 8) | bytes[1];
    const battery = bytes[2];
    return {
        temperature: temp / 100.0,
        battery,
        status: temp > 3000 ? "hot" : "normal"
    };
};
```

Both styles are fully supported. The platform automatically detects and transpiles ES6+ syntax.

### Advanced Parsing Patterns

#### 1. Multi-byte Integers (Big Endian)

To combine multiple bytes into a larger integer, use bit-shifting:

```javascript
// 32-bit Signed Integer (e.g., Latitude/Longitude)
const val32 = (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];

// 16-bit Unsigned Integer
const val16 = (bytes[0] << 8) | bytes[1];
```

#### 2. Handling Signed 16-bit Integers (Shorts)

JavaScript's bitwise operators return 32-bit signed integers. For 16-bit signed values, you must manually handle the sign bit:

```javascript
const raw = (bytes[0] << 8) | bytes[1];
const signedVal = raw > 32767 ? raw - 65536 : raw;
```

#### 3. Floating Point (IEEE 754)

Wrap the bytes in a `DataView` to read IEEE 754 floats and doubles directly. The second argument selects little-endian (`true`) or big-endian (omitted/`false`):

```javascript
function Decoder(bytes, port) {
    const view = new DataView(new Uint8Array(bytes).buffer);
    return {
        latitude: view.getFloat64(0, true),   // little-endian double
        longitude: view.getFloat64(8, true),
        speed: view.getFloat32(16, true)
    };
}
```

#### 4. Bit Flags (Status Bytes)

Devices often pack multiple boolean flags into a single byte:

```javascript
const statusByte = bytes[0];
const isMoving = (statusByte & 0x01) !== 0;   // Bit 0
const isCharging = (statusByte & 0x02) !== 0;  // Bit 1
const alarmState = (statusByte & 0x04) !== 0;  // Bit 2
```

#### 5. Destructuring & Template Literals (ES6+)

Modern syntax makes complex decoders more readable:

```javascript
const Decoder = (bytes, port) => {
    const [tempHi, tempLo, battery, status] = bytes;
    const temp = ((tempHi << 8) | tempLo) / 100.0;
    const label = `${temp > 30 ? "HOT" : "OK"} @ ${temp.toFixed(1)}C`;
    return { temperature: temp, battery, status, label };
};
```

### Special Return Fields

The `Decoder` returns a plain object. A few keys have platform meaning:

| Field                   | Type                 | Description                                                                                                                                                                                                                                                      |
| ----------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `latitude`, `longitude` | `number`             | Position of the device. Drives the live map, geofences, trips and route monitoring.                                                                                                                                                                              |
| `rssi`, `snr`           | `number`             | Signal quality, stored in dedicated columns.                                                                                                                                                                                                                     |
| `time`                  | `number` or `string` | Overrides the stored telemetry time. A number is UNIX **seconds**; a string must be RFC 3339 (`2026-01-01T12:00:00Z`). If omitted, the server receive time is used. The timestamp in the packet header is only used for replay protection and is **not** stored. |

All other keys are stored as-is in the telemetry payload and are available to alert rules, dashboards and workflows.

### Rejecting a Packet

A decoder must return an object. If it throws, or returns `null`, `undefined` or a non-object, the packet is dropped and counted as a decoder error (`decoder_error`); nothing is stored. Use this deliberately to reject corrupt payloads, for example `throw new Error("payload too short")`.

### Limitations & Timeouts

* **Script Size:** Maximum 64 KB per decoder script (configurable via `SCRIPTING_MAX_SCRIPT_SIZE`).
* **Execution Time:** UDP decoder scripts are killed after 500 ms by default (configurable via `SCRIPTING_DECODER_TIMEOUT_MS`). The decoder playground allows 3 seconds for interactive testing.
* **Call Stack Depth:** Limited to 1024 frames by default (configurable via `SCRIPTING_MAX_CALL_STACK`). Prevents stack overflow from infinite recursion.
* **Blocked Globals:** `eval()` and `Function` constructor are disabled for security.
* **No async/await:** The Goja runtime is synchronous. `async`/`await` syntax is not supported.
* **No TypeScript:** Only JavaScript (ES5 and ES6+) is supported.

### Real-Time Syntax Validation

The decoder script editor in the Device Profile form and the Workflow JS Transform editor include real-time ES6+ syntax validation. As you type, the editor parses your code using the Acorn parser and displays inline error messages with line numbers. This validation is advisory only — it catches syntax errors early but does not block saving.

### Testing your Decoder

Use the **Decoder Playground** in the Device Profiles page to test your decoder against sample hex payloads. The playground supports both ES5 and ES6+ syntax and provides detailed error messages including line numbers for compilation and runtime errors.

## Decoder Playground

Writing a decoder used to mean guessing whether it worked until a real device sent a packet. The **Decoder Playground**, built into the Device Profile editor, closes that loop: paste a sample payload — as a hex string (for example `09f65702deb07601050b24b9`) or comma-separated bytes — click **Run**, and see the decoded JSON immediately, without touching hardware.

The playground always tests your **current, unsaved** editor content, so you can iterate freely — tweak the script, run it again, tweak again — and only save once you're happy with the result. If the script throws, the playground shows the exact JavaScript error and line number instead of a silent failure; if it returns nothing, you get a clear warning rather than a crash — and in production that packet would be dropped. It runs the same decoding engine used in production, so what you see in the playground is exactly what your device would get.

## Script Templates Library

Rather than starting from a blank editor, you can browse the **Script Templates Library** from the code editor's **Browse Templates** button — available for both device profile decoders and workflow JS Transform nodes. It ships with ready-made templates for common tasks (parsing Cayenne LPP payloads, converting hex to integers, extracting bitmask flags, Celsius-to-Fahrenheit conversion, and more), organized by category with a preview before you apply one.

Beyond the built-in library, you can save your own scripts as templates with **Save as Template** — useful once you've written a decoder pattern you'll want to reuse across profiles, or across your team. Your saved templates are private to you; system templates are available to everyone and can't be edited, though you can duplicate one to create your own editable copy.

## Next Steps

Ready to build the device side? The [Firmware & Client Examples](https://github.com/AdaTrack/adatrack_platform/tree/main/docs/gitbook/documentation/iot-ingestion/firmware/README.md) show complete, signed UDP clients for Python, Arduino/ESP32, STM32 and more, together with the matching decoder.
