Skip to content

Payload Format

Codec

A complete LoRaWAN payload codec with examples can be found in the downloads section. See also Decoder example for reference.

Overview

The payload of up and downlink messages consists of an arbitrary number of data structs (DS) of different types and lengths.

DS 1 DS 2 ... DS n

Each data struct is a combination of a header and the actual data payload:

L T payload

The header consists of two fields:

Field Description
L Length of data struct, 1 byte, not including the length byte itself
T Data struct type, 1 byte

Data Encoding

Signed integers use two's complement for encoding.

Important

Unless otherwise noted, payloads will use little endian data encoding.

All uplinks are sent on LoRaWAN port 15.

Regular Status Message

The device sends regular status messages at a configurable interval. The content of the status message consists of three messages:

  • Battery Message (only EU868 MHz devices)
  • Current scene Message
  • Battery voltage Message

An additional purpose of the status message is to allow reception of downlink messages if the network server does not support class C.

Battery

This device doesn't have batteries. The battery message can be ignored.

Message type T = 0x02 and length L = 0x05 for EU868 devices

Byte Size Description Format
0 1 Message length (0x05) uint8
1 1 Message type (0x02) uint8
2-5 4 Accu uAh uint32

Example

Payload 05:02:00:00:00:55 reports accumulated energy consumption of 85 uAh

CurrentScene

The CurrentScene message type is used to get information about the current scene. It is included in every regular status message and is also sent in response to a Get Current Scene downlink.

Message type T = 0x04 and length L = 0x02.

Byte Size Description Format
0 1 Message length (0x02) uint8
1 1 Message type (0x04) uint8
2 1 Current Scene uint8

Example

Payload 02:04:07 reports current scene 7

BatteryVoltage

This device doesn't have batteries. The battery voltage message returns the measured voltage and temperature from the microcontroller.

Message type T = 0x03 and length L = 0x03.

Byte Size Description Format
0 1 Message length (0x03) uint8
1 1 Message type (0x03) uint8
2 1 Battery Voltage uint8
3 1 Temperature uint8

Example

Payload 03:03:0C:E4 reports voltage raw value 0x0C and temperature raw value 0xE4

CurrentSceneColor

This uplink is sent in response to a Configure Scene Color downlink command. It reports the stored color of the queried scene.

Message type T = 0x05 and length L = 0x06.

Warning

Message type value is not visible in main.c — confirm numeric T value from the enum definition in the firmware headers.

Byte Size Description Format
0 1 Message length (0x06) uint8
1 1 Message type (0x05) uint8
2 1 Scene number uint8
3 1 Red value uint8
4 1 Green value uint8
5 1 Blue value uint8

CurrentSceneTimeout

This uplink is sent in response to a Configure Scene Timeout downlink command. It reports the stored timeout of the queried scene.

Message type T = 0x06 and length L = 0x05.

Warning

Message type value is not visible in main.c — confirm numeric T value from the enum definition in the firmware headers.

Byte Size Description Format
0 1 Message length (0x05) uint8
1 1 Message type (0x06) uint8
2 1 Scene number uint8
3-4 2 Timeout (0=no timeout) uint16, minutes

CurrentSceneMelody

This uplink is sent in response to a Configure Scene Melody downlink command. It reports the stored melody and repeat setting of the queried scene.

Message type T = (0x07) and length L = 0x05.

Warning

Message type value is not visible in main.c — confirm numeric T value from the enum definition in the firmware headers.

Byte Size Description Format
0 1 Message length (0x05) uint8
1 1 Message type (0x07) uint8
2 1 Scene number uint8
3 1 Melody (see melody enum) enum
4 1 Repeat (0=disabled, 1=enabled) bool

Downlink messages are used to change the configuration of the device. They use the same general payload format as uplinks.

Important

All down-links must be sent on the LoRaWAN port 3

Device Configuration

The device configuration message type is used to set general device configuration.

Message type T = 0x80 and length L = 0x06.

Byte Size Description Format Value Range
0 1 Message length (0x06) 0x06
1 1 Message type (0x80) 0x80
2 1 Flags, bitwise or combination:
Bit 7 = Buzzer (0 = off, 1 = on)
Bit 6 = LoRaWAN Class (0 = A, 1 = C)
Bit 5 = Duty Cycle (0 = off, 1 = on)
Bit 4 = Confirmed uplinks (0 = off, 1 = on)
Bits 0-3: RFU
Bitfield
3-4 2 Keep Alive interval in minutes uint16 0..0xFFFF
5 1 Number of LEDs in ring (max 48) uint8 0..48
6 1 Automatic reset time (0 = disabled) uint8, hours 0..0xFF

Example

payload 06:80:60:00:0A:10:0A will result in following configuration options

Config option Value
Flags Class C, Duty Cycling on, Buzzer off
Status interval 10 minutes
Number of LEDs 16
Reset time 10

Set Scene

This downlink command is used to activate a scene. A scene is a preprogrammed LED color. The same color is set on all LEDs. Optionally, a scene also includes a buzzer melody to be played, either once or on repeat until the scene ends. Only a select few melodies are available. A scene is valid until the next scene will be set with another downlink command or until its preconfigured timeout is reached and the LEDs and buzzer is switched off.

Message type T = 0x81 and length L = 0x02:

Byte Size Description Format Value Range
0 1 Message length (0x02) uint8 0x02
1 1 Message type (0x81) uint8 0x81
2 1 Scene (0=Off) uint8 0..NB_SCENES

Example

payload 02:81:01 will activate scene 1 on the device

Set Brightness

Set LED brightness.

Message type T = 0x82 and length L = 0x02.

Byte Size Description Format Value Range
0 1 Message length (0x02) uint8 0x02
1 1 Message type (0x82) uint8 0x82
2 1 Brightness [0=darkest,255=brightest] uint8 0..0xFF

Example

payload 02:82:80 will set brightness to 128 which is equal to 50% of maximum brightness

Set Volume

Set buzzer volume.

Message type T = 0x85 and length L = 0x02.

Byte Size Description Format Value Range
0 1 Message length (0x02) uint8 0x02
1 1 Message type (0x85) uint8 0x85
2 1 Volume [0=off,1=low,2=medium,3=high] enum 0..3

Note

It is recommended to use the buzzer flag instead of setting the volume to 0.

Example

payload 02:85:03 will set buzzer volume to high (maximum)

Configure Scene (LED)

Configure an LED scene by changing its color and timeout time. Note that scene 0 (Off mode) can't be changed.

Message type T = 0x83 and length L = 0x07 (single scene).

Note

Multiple scenes can be configured in a single message. Each additional scene adds 6 bytes and L increases accordingly.

Byte Size Description Format Value Range
0 1 Message length (0x07) 0x07
1 1 Message type (0x83) 0x83
2 1 Scene to configure uint8 1..NB_SCENES
3 1 Red value uint8 0..0xFF
4 1 Green value uint8 0..0xFF
5 1 Blue value uint8 0..0xFF
6-7 2 Timeout time of scene (0=no timeout) uint16, minutes 0..0xFFFF

Example

payload 07:83:02:FF:9A:00:00:0A will configure scene 2 with following values

Parameter Value
Red value 0xFF = 255
Green value 0x9A = 154
Blue value 0x00 = 0
Timeout time 10 minutes

The hex color 0xff9a00 is a yellow.

Configure Scene (LED and Buzzer/HSP sirene)

Configure an LED scene by changing its color and timeout time. Additionally, configure the buzzer melody for this specific scene. Note that scene 0 (Off mode) can't be changed.

Message type T = 0x84 and length L = 0x09 (single scene).

Note

Multiple scenes can be configured in a single message. Each additional scene adds 8 bytes and L increases accordingly.

Byte Size Description Format Value Range
0 1 Message length (0x09) 0x09
1 1 Message type (0x84) 0x84
2 1 Scene to configure uint8 1..NB_SCENES
3 1 Red value uint8 0..0xFF
4 1 Green value uint8 0..0xFF
5 1 Blue value uint8 0..0xFF
6-7 2 Timeout time of scene (0=no timeout) uint16, minutes 0..0xFFFF
8 1 Melody:
0 = None
1 = Fast
2 = Medium
3 = Slow
4 = Ascending
5 = DoubleUp
6 = Double
7 = Triple
8 = Always On (HSP ON)
9 = Always Off (HSP OFF)
10 = Short Tone 1 Sec Period (HSP 1 sec)
enum 0..10
9 1 Repeat [0=disabled, 1=enabled] bool 0,1

Example

payload 09:84:02:FF:9A:00:00:0A:03:01 will configure scene 2 with following values

Parameter Value
Red value 0xFF = 255
Green value 0x9A = 154
Blue value 0x00 = 0
Timeout time 10 minutes
Melody slow
Repeat enabled

The hex color 0xff9a00 is a yellow.

Example for HSP siren:

  • Configure Scene 1 Green, no HSP siren: 09:84:01:00:FF:00:00:00:09:00
  • Configure Scene 3 Red, with HSP siren: 09:84:03:FF:00:00:00:00:08:01

Configure Scene Color (AS923 compatible)

Configure only the LED color of a scene. This message is a split version of "Configure Scene (LED and Buzzer)" to fit within AS923 downlink payload size limits.

Adding an offset of 100 to the scene number triggers a read-only operation: the device does not write to flash but instead responds with a CurrentSceneColor uplink.

Note that scene 0 (Off mode) can't be changed.

Message type T = 0x89 and length L = 0x05 (single scene).

Note

Multiple scenes can be configured in a single message. Each additional scene adds 4 bytes and L increases accordingly.

Byte Size Description Format Value Range
0 1 Message length (0x05) uint8 0x05
1 1 Message type (0x89) uint8 0x89
2 1 Scene to configure (add 100 to scene number for read-only) uint8 1..NB_SCENES or 101..NB_SCENES+100
3 1 Red value uint8 0..0xFF
4 1 Green value uint8 0..0xFF
5 1 Blue value uint8 0..0xFF

Configure scene 2 color

payload 05:89:02:FF:9A:00 will configure scene 2 color

Parameter Value
Scene 2
Red value 0xFF = 255
Green value 0x9A = 154
Blue value 0x00 = 0

Read back scene 2 color

payload 05:89:66:00:00:00 triggers a read-back of scene 2 (0x66 = 102 = 100 + 2). The device responds with a CurrentSceneColor uplink. The color bytes are ignored.

Configure Scene Timeout (AS923 compatible)

Configure only the timeout of a scene. This message is a split version of "Configure Scene (LED and Buzzer)" to fit within AS923 downlink payload size limits.

Adding an offset of 100 to the scene number triggers a read-only operation: the device does not write to flash but instead responds with a CurrentSceneTimeout uplink.

Note that scene 0 (Off mode) can't be changed.

Message type T = 0x8A and length L = 0x04 (single scene).

Note

Multiple scenes can be configured in a single message. Each additional scene adds 3 bytes and L increases accordingly.

Byte Size Description Format Value Range
0 1 Message length (0x04) uint8 0x04
1 1 Message type (0x8A) uint8 0x8A
2 1 Scene to configure (add 100 to scene number for read-only) uint8 1..NB_SCENES or 101..NB_SCENES+100
3-4 2 Timeout time of scene (0=no timeout) uint16, minutes 0..0xFFFF

Set scene 2 timeout to 10 minutes

payload 04:8A:02:00:0A

Parameter Value
Scene 2
Timeout time 10 minutes

Read back scene 2 timeout

payload 04:8A:66:00:00 triggers a read-back of scene 2 (0x66 = 102 = 100 + 2). The device responds with a CurrentSceneTimeout uplink. The timeout bytes are ignored.

Configure Scene Melody (AS923 compatible)

Configure the melody and repeat settings of a scene. This message is a split version of "Configure Scene (LED and Buzzer)" to fit within AS923 downlink payload size limits.

Adding an offset of 100 to the scene number triggers a read-only operation: the device does not write to flash but instead responds with a CurrentSceneMelody uplink.

Note that scene 0 (Off mode) can't be changed.

Message type T = 0x8B and length L = 0x04 (single scene).

Note

Multiple scenes can be configured in a single message. Each additional scene adds 3 bytes and L increases accordingly.

Byte Size Description Format Value Range
0 1 Message length (0x04) uint8 0x04
1 1 Message type (0x8B) uint8 0x8B
2 1 Scene to configure (add 100 to scene number for read-only) uint8 1..NB_SCENES or 101..NB_SCENES+100
3 1 Melody:
0 = None
1 = Fast
2 = Medium
3 = Slow
4 = Ascending
5 = DoubleUp
6 = Double
7 = Triple
8 = Always On (HSP ON)
9 = Always Off (HSP OFF)
10 = Short Tone 1 Sec Period (HSP 1 sec)
enum 0..10
4 1 Repeat [0=disabled, 1=enabled] bool 0,1

Set scene 2 melody to Slow, repeating

payload 04:8B:02:03:01

Parameter Value
Scene 2
Melody Slow
Repeat enabled

Read back scene 2 melody

payload 04:8B:66:03:01 triggers a read-back of scene 2 (0x66 = 102 = 100 + 2). The device responds with a CurrentSceneMelody uplink. The melody/repeat bytes are ignored.

Get Current Scene

Request the currently active scene. The device responds with a CurrentScene uplink on port 15.

Available on devices produced after 2026.

Message type T = 0x8C and length L = 0x01.

Byte Size Description Format Value Range
0 1 Message length (0x01) uint8 0x01
1 1 Message type (0x8C) uint8 0x8C

Example

payload 01:8C requests the current scene from the device

Set Scene Type

Configure the visual animation type and speed of a scene. This determines how the LEDs display the configured color (static, blinking, or breathing).

Note that scene 0 (Off mode) can't be changed.

No possiblity to read back over LoRaWAN.

Message type T = 0x87 and length L = 0x05 (single scene).

Byte Size Description Format Value Range
0 1 Message length (0x05) uint8 0x05
1 1 Message type uint8 0x87
2 1 Scene to configure uint8 1..NB_SCENES
3 1 Scene type:
0 = Static
1 = Blinking
2 = Breathing
enum 0..2
4-5 2 Animation speed in ms (period for blink/breathe) uint16, ms 0..0xFFFF

Configure scene 2 to breathe with 1000ms period

payload: 05:87:02:02:03:E8

Parameter Value
Scene 2
Type Breathing (2)
Speed 0x03E8 = 1000 ms

Request the device to output the internal backtrace log to the debug UART. This command has no effect on LEDs or buzzer.

Message type T = 0x86 and length L = 0x01.

Byte Size Description Format Value Range
0 1 Message length (0x01) uint8 0x01
1 1 Message type uint8 0x86

Set LED Timing (attention: only expert use)

Configure low-level WS2812B LED signal timing parameters. This is an advanced command for tuning timing to match specific LED requirements.

Message type T = 0x88 and length L = 0x0B.

Byte Size Description Format Value Range
0 1 Message length (0x0B) uint8 0x0B
1 1 Message type uint8 0x88
2-3 2 Low time before data (µs) uint16 BE 0..0xFFFF
4-5 2 Low time after data (µs) uint16 BE 0..0xFFFF
6-7 2 High time after commit (µs) uint16 BE 0..0xFFFF
8-9 2 Logic 1 high duration (% of period) uint16 BE 0..100
10-11 2 Logic 0 high duration (% of period) uint16 BE 0..100

Decoder example

var MELODIES = [
    "None", "Fast", "Medium", "Slow", "Ascending",
    "DoubleUp", "Double", "Triple",
    "AlwaysOn", "AlwaysOff", "ShortTone1SecPeriod"
];

var SCENE_TYPES = ["Static", "Blinking", "Breathing"];

function melodyName(value) {
    return value < MELODIES.length ? MELODIES[value] : "Unknown(" + value + ")";
}

function melodyIndex(name) {
    var idx = MELODIES.indexOf(name);
    return idx >= 0 ? idx : -1;
}

function decodeUplink(input) {
    var bytes = input.bytes;
    var port = input.fPort;
    var decoded = {};
    var warnings = [];
    var errors = [];

    if (port === 15) {
        var idx = 0;
        var total = bytes.length;

        while (idx < total) {
            var length = bytes[idx];
            var type = bytes[idx + 1];

            switch (type) {
                case 2: // eMsgBattery – EU868 only, little-endian uint32
                    decoded.battery_usage_uah =
                        bytes[idx + 2] +
                        bytes[idx + 3] * 256 +
                        bytes[idx + 4] * 65536 +
                        bytes[idx + 5] * 16777216;
                    break;

                case 3: // eMsgBatteryVoltage + temperature (one byte each)
                    decoded.battery_voltage = bytes[idx + 2];
                    decoded.battery_temperature = bytes[idx + 3];
                    break;

                case 4: // eMsgCurrentScene
                    decoded.current_scene = bytes[idx + 2];
                    break;

                case 5: // eMsgCurrentSceneColor – scene readout response
                    decoded.scene_color_scene = bytes[idx + 2];
                    decoded.scene_color_r = bytes[idx + 3];
                    decoded.scene_color_g = bytes[idx + 4];
                    decoded.scene_color_b = bytes[idx + 5];
                    break;

                case 6: // eMSgCurrentSceneTimeout – scene readout response
                    decoded.scene_timeout_scene = bytes[idx + 2];
                    decoded.scene_timeout_min = bytes[idx + 4] + bytes[idx + 3] * 256;
                    break;

                case 7: // eMsgCurrentSceneMelody – scene readout response
                    decoded.scene_melody_scene = bytes[idx + 2];
                    decoded.scene_melody = melodyName(bytes[idx + 3]);
                    decoded.scene_melody_repeat = Boolean(bytes[idx + 4]);
                    break;

                default:
                    warnings.push("Unknown uplink type: " + type);
                    break;
            }

            idx += length + 1;
        }
    } else {
        errors.push("Uplink is not on port 15");
    }

    var output = { data: decoded };
    if (warnings.length > 0) output.warnings = warnings;
    if (errors.length > 0) output.errors = errors;
    return output;
}

function decodeDownlink(input) {
    var bytes = input.bytes;
    var port = input.fPort;
    var decoded = {};
    var warnings = [];
    var errors = [];

    if (port === 3) {
        var idx = 0;
        var total = bytes.length;

        while (idx < total) {
            var length = bytes[idx];
            var type = bytes[idx + 1];

            switch (type) {
                case 128: // eMsgConfiguration (0x80)
                    decoded.settings_confirmed    = Boolean(bytes[idx + 2] & 0x80);
                    decoded.settings_duty_cycle   = Boolean(bytes[idx + 2] & 0x40);
                    decoded.settings_class_c      = Boolean(bytes[idx + 2] & 0x20);
                    decoded.settings_buzzer       = Boolean(bytes[idx + 2] & 0x10);
                    decoded.settings_status_interval_min = bytes[idx + 4] + bytes[idx + 3] * 256;
                    decoded.settings_num_led      = bytes[idx + 5];
                    decoded.settings_reset_time_h = bytes[idx + 6];
                    break;

                case 129: // eMsgControlLED (0x81) – Set Scene
                    decoded.current_scene = bytes[idx + 2];
                    break;

                case 130: // eMsgSetBrightness (0x82)
                    decoded.settings_brightness_percent = Math.round(100 / 255 * bytes[idx + 2]);
                    break;

                case 131: // eMsgSetScene (0x83) – Configure Scene (LED only)
                    decoded.selected_scene    = bytes[idx + 2];
                    decoded.scene_red         = bytes[idx + 3];
                    decoded.scene_green       = bytes[idx + 4];
                    decoded.scene_blue        = bytes[idx + 5];
                    decoded.scene_timeout_min = bytes[idx + 7] + bytes[idx + 6] * 256;
                    break;

                case 132: // eMsgSetSceneBuzzer (0x84) – Configure Scene (LED + Buzzer)
                    decoded.selected_scene      = bytes[idx + 2];
                    decoded.scene_red           = bytes[idx + 3];
                    decoded.scene_green         = bytes[idx + 4];
                    decoded.scene_blue          = bytes[idx + 5];
                    decoded.scene_timeout_min   = bytes[idx + 7] + bytes[idx + 6] * 256;
                    decoded.scene_buzzer_melody = melodyName(bytes[idx + 8]);
                    decoded.scene_buzzer_repeat = Boolean(bytes[idx + 9]);
                    break;

                case 133: // eMsgSetVolume (0x85)
                    switch (bytes[idx + 2]) {
                        case 0:  decoded.settings_volume = "off";    break;
                        case 1:  decoded.settings_volume = "low";    break;
                        case 2:  decoded.settings_volume = "medium"; break;
                        case 3:  decoded.settings_volume = "high";   break;
                        default:
                            warnings.push("Unknown volume: " + bytes[idx + 2]);
                            break;
                    }
                    break;

                case 135: // eMsgSetSceneType (0x87)
                    decoded.scene_type_scene    = bytes[idx + 2];
                    decoded.scene_type          = bytes[idx + 3] < SCENE_TYPES.length
                        ? SCENE_TYPES[bytes[idx + 3]]
                        : "Unknown(" + bytes[idx + 3] + ")";
                    decoded.scene_type_speed_ms = bytes[idx + 5] + bytes[idx + 4] * 256;
                    break;

                case 137: // eMsgSetSceneColor (0x89) – Configure Scene Color, AS923
                    decoded.scene_color_scene = bytes[idx + 2];
                    decoded.scene_color_r     = bytes[idx + 3];
                    decoded.scene_color_g     = bytes[idx + 4];
                    decoded.scene_color_b     = bytes[idx + 5];
                    break;

                case 138: // eMsgSetSceneTimeout (0x8A) – Configure Scene Timeout, AS923
                    decoded.scene_timeout_scene = bytes[idx + 2];
                    decoded.scene_timeout_min   = bytes[idx + 4] + bytes[idx + 3] * 256;
                    break;

                case 139: // eMsgSetSceneMelody (0x8B) – Configure Scene Melody, AS923
                    decoded.scene_melody_scene  = bytes[idx + 2];
                    decoded.scene_melody        = melodyName(bytes[idx + 3]);
                    decoded.scene_melody_repeat = Boolean(bytes[idx + 4]);
                    break;

                case 140: // eMsgGetCurrentScene (0x8C)
                    decoded.get_current_scene = true;
                    break;

                default:
                    warnings.push("Unknown downlink type: " + type);
                    break;
            }

            idx += length + 1;
        }
    } else {
        errors.push("Downlink is not on port 3");
    }

    var output = { data: decoded };
    if (warnings.length > 0) output.warnings = warnings;
    if (errors.length > 0) output.errors = errors;
    return output;
}

function checkAllKeys(keys, object) {
    for (var i in keys) {
        if (!(keys[i] in object)) return false;
    }
    return true;
}

function checkOneKey(keys, object) {
    for (var i in keys) {
        if (keys[i] in object) return true;
    }
    return false;
}

function encodeDownlink(input) {
    var warnings = [];
    var errors = [];
    var output = { fPort: 3, bytes: [] };
    var data = input.data;

    var configKeys = [
        "settings_confirmed", "settings_duty_cycle", "settings_class_c", "settings_buzzer",
        "settings_status_interval_min", "settings_num_led", "settings_reset_time_h"
    ];
    var configSceneKeys = ["selected_scene", "scene_red", "scene_green", "scene_blue", "scene_timeout_min"];
    var configSceneBuzzerKeys = ["selected_scene", "scene_red", "scene_green", "scene_blue",
        "scene_timeout_min", "scene_buzzer_melody", "scene_buzzer_repeat"];

    // Device Configuration (0x80)
    if (checkOneKey(configKeys, data)) {
        if (checkAllKeys(configKeys, data)) {
            var flags = 0;
            if (data.settings_confirmed)  flags |= 0x80;
            if (data.settings_duty_cycle) flags |= 0x40;
            if (data.settings_class_c)    flags |= 0x20;
            if (data.settings_buzzer)     flags |= 0x10;
            output.bytes.push(6, 128, flags);
            output.bytes.push((parseInt(data.settings_status_interval_min) >> 8) & 0xff);
            output.bytes.push(parseInt(data.settings_status_interval_min) & 0xff);
            output.bytes.push(parseInt(data.settings_num_led) & 0xff);
            output.bytes.push(parseInt(data.settings_reset_time_h) & 0xff);
        } else {
            errors.push("All config keys required: " + JSON.stringify(configKeys));
        }
    }

    // Set Scene (0x81)
    if ("current_scene" in data) {
        output.bytes.push(2, 129);
        output.bytes.push(parseInt(data.current_scene) & 0xff);
    }

    // Set Brightness (0x82)
    if ("settings_brightness_percent" in data) {
        var brightness = parseFloat(data.settings_brightness_percent);
        output.bytes.push(2, 130);
        if (brightness >= 0 && brightness <= 100) {
            output.bytes.push(Math.round(brightness / 100 * 255) & 0xff);
        } else {
            errors.push("Brightness must be 0–100 (percent)");
            output.bytes.push(0);
        }
    }

    // Set Volume (0x85)
    if ("settings_volume" in data) {
        output.bytes.push(2, 133);
        switch (data.settings_volume) {
            case "off":    output.bytes.push(0); break;
            case "low":    output.bytes.push(1); break;
            case "medium": output.bytes.push(2); break;
            case "high":   output.bytes.push(3); break;
            default:
                output.bytes.push(0);
                errors.push("Volume must be 'off', 'low', 'medium', or 'high'");
                break;
        }
    }

    // Configure Scene LED only (0x83) – only when buzzer keys are absent
    if (checkAllKeys(configSceneKeys, data) && !checkOneKey(["scene_buzzer_melody", "scene_buzzer_repeat"], data)) {
        var sceneNo = parseInt(data.selected_scene);
        if (sceneNo < 1 || sceneNo > 22) {
            errors.push("Scene must be 1–22");
            sceneNo = 1;
        }
        output.bytes.push(7, 131, sceneNo & 0xff);
        output.bytes.push(parseInt(data.scene_red) & 0xff);
        output.bytes.push(parseInt(data.scene_green) & 0xff);
        output.bytes.push(parseInt(data.scene_blue) & 0xff);
        output.bytes.push((parseInt(data.scene_timeout_min) >> 8) & 0xff);
        output.bytes.push(parseInt(data.scene_timeout_min) & 0xff);
    }

    // Configure Scene LED + Buzzer (0x84)
    if (checkAllKeys(configSceneBuzzerKeys, data)) {
        var sceneNo = parseInt(data.selected_scene);
        if (sceneNo < 1 || sceneNo > 22) {
            errors.push("Scene must be 1–22");
            sceneNo = 1;
        }
        output.bytes.push(9, 132, sceneNo & 0xff);
        output.bytes.push(parseInt(data.scene_red) & 0xff);
        output.bytes.push(parseInt(data.scene_green) & 0xff);
        output.bytes.push(parseInt(data.scene_blue) & 0xff);
        output.bytes.push((parseInt(data.scene_timeout_min) >> 8) & 0xff);
        output.bytes.push(parseInt(data.scene_timeout_min) & 0xff);
        var mIdx = melodyIndex(data.scene_buzzer_melody);
        if (mIdx >= 0) {
            output.bytes.push(mIdx);
        } else {
            output.bytes.push(0);
            errors.push("Melody must be one of: " + JSON.stringify(MELODIES));
        }
        output.bytes.push(data.scene_buzzer_repeat ? 1 : 0);
    }

    // Configure Scene Type (0x87)
    if ("scene_type_scene" in data) {
        var sceneNo = parseInt(data.scene_type_scene);
        var typeIdx = SCENE_TYPES.indexOf(data.scene_type);
        var speed   = parseInt(data.scene_type_speed_ms) || 0;
        if (sceneNo < 1 || sceneNo > 22) {
            errors.push("Scene must be 1–22");
            sceneNo = 1;
        }
        if (typeIdx < 0) {
            errors.push("Scene type must be one of: " + JSON.stringify(SCENE_TYPES));
            typeIdx = 0;
        }
        output.bytes.push(5, 135, sceneNo & 0xff, typeIdx);
        output.bytes.push((speed >> 8) & 0xff);
        output.bytes.push(speed & 0xff);
    }

    // Configure Scene Color, AS923 (0x89)
    if ("scene_color_scene" in data) {
        output.bytes.push(5, 137);
        output.bytes.push(parseInt(data.scene_color_scene) & 0xff);
        output.bytes.push(parseInt(data.scene_color_r) & 0xff);
        output.bytes.push(parseInt(data.scene_color_g) & 0xff);
        output.bytes.push(parseInt(data.scene_color_b) & 0xff);
    }

    // Configure Scene Timeout, AS923 (0x8A)
    if ("scene_timeout_scene" in data) {
        var timeout = parseInt(data.scene_timeout_min) || 0;
        output.bytes.push(4, 138);
        output.bytes.push(parseInt(data.scene_timeout_scene) & 0xff);
        output.bytes.push((timeout >> 8) & 0xff);
        output.bytes.push(timeout & 0xff);
    }

    // Configure Scene Melody, AS923 (0x8B)
    if ("scene_melody_scene" in data) {
        var mIdx = melodyIndex(data.scene_melody);
        if (mIdx < 0) {
            errors.push("Melody must be one of: " + JSON.stringify(MELODIES));
            mIdx = 0;
        }
        output.bytes.push(4, 139);
        output.bytes.push(parseInt(data.scene_melody_scene) & 0xff);
        output.bytes.push(mIdx);
        output.bytes.push(data.scene_melody_repeat ? 1 : 0);
    }

    // Get Current Scene (0x8C)
    if ("get_current_scene" in data && data.get_current_scene) {
        output.bytes.push(1, 140);
    }

    if (warnings.length > 0) output.warnings = warnings;
    if (errors.length > 0) output.errors = errors;
    return output;
}