Skip to content

Device Control Protocol

The device control protocol uses bytes represented as hexadecimal strings. All multi-byte values are sent in MSB (Most Significant Byte) first format unless stated otherwise.

Message Format

The protocol supports two message formats:

Standard Format (No User ID)

[HEADER][COMMAND][DATA]

User Identification Format

[HEADER][USERID]#[COMMAND][DATA]

Header Structure

The header is 8 bytes long and has the following format:

ByteValueDescription
00x07Always starts with 0x07
10x01 or 0x03Identification byte: 0x01 = no user ID, 0x03 = user ID included
2-70x00Remaining bytes are always 0x00

Header Examples:

  • No User ID: 070100000000000000 (8 bytes)
  • With User ID: 070300000000000000 (8 bytes)

User ID Format

When user identification is included (0x03 in header byte 1), the user ID follows the header and is terminated with a # character:

Numeric User Code

  • Format: 4-6 digits (keypad code/PIN)
  • Example: 1234# for user code 1234

Email Address

  • Format: Full email address
  • Example: user@example.com#

Command Structure

Commands are 2 bytes long and transmitted in LSB first format:

Command ValueHex String Representation
0x000D"0D00"
0x0001"0100"
0x00FF"FF00"

Complete Message Examples

Standard Command (No User ID)

Header: 070100000000000000
Command: 0D00
Data: [command-specific data]
Complete: 0701000000000000000D00[data]

Command with Numeric User ID

Header: 070300000000000000
User ID: 1234#
Command: 0D00
Data: [command-specific data]
Complete: 0703000000000000001234#0D00[data]

Data Section

The [DATA] section content varies by command and will be described in command-specific documentation.

Device Error Responses

When a device encounters an error processing a command, it may return error information in the response data. The error format depends on the device model:

General Error Responses

Error ResponseDescription
[0x00xx][FORMAT]Wrong format of the command
[0x00xx][NACK]Command not recognized
[0x00xx][NRDY]Command and format are valid, but device is not ready to execute
[0x00xx][SNRDY]Command and format are valid, but slave is not ready to execute
[0x00xx][DISABLED]Command and format is valid, but command is disabled or cannot be executed
[0x0000]Command number [0x00xx] is not recognized
[0x00xx][response],[E:reason]Command is valid but an error has occurred during execution
[0x00xx][ERROR:reason]Command and its format is valid, but command not executed

ERROR Responses with Reasons

Error ResponseDescription
[ERROR:WRONG_CODE]Invalid code for system state change
[ERROR:FAULT]Command failed due to a detected fault in the system
[ERROR:CONTROL_AREA]No permission to manage a specific area
[ERROR:NOSLEEP]Controlled area does not have a sleep/stay feature
[ERROR:NACK]Parameters are incorrect, no such controllable objects in the product, synchronization required

Gate Controller Output Control Errors

Error ResponseDescription
[E:USER]No such user exists
[E:TIME]User is not allowed due to time constraints
[E:LORA]No such expander exists
[E:PGM]No PGM user assigned

Notes:

  • [0x00xx] represents the original command code that caused the error
  • Error responses are model-dependent and may vary between device types
  • Gate controllers may return specific errors when controlling outputs
  • These are device-level errors returned in the response data, separate from API-level errors

Command Reference

This page documents the available device control commands that can be sent using the Control Device API endpoint.

0x0001 - Capabilities

Retrieves device capabilities and configuration information.

Request/Response format

Request

[HEADER][0x0001] → 070100000000000000100

Response Format

Success Response:

[HEADER][0x0001][DATA ASCII]

Error Response:

[HEADER][0x0001][NACK]

Response Data Fields

The response data is returned as ASCII text with comma-delimited fields:

FieldFormatDescription
HWHW:xHardware ID (hexadecimal)
ZZ:xMaximum zone count (omit if none)
OO:xMaximum output count (omit if none)
TRBTRB:xTrouble reporting capability (1 = can send faults 0 = can not, see command 0x000F) (may also omit this)
SYSSYS:xMaximum areas count
TMTM:xInternal clock availability (0 = not configurable, 1 = configurable)
SENSEN:xMaximum temperature sensors count
IMEIIMEI:xxxxxxxxxGSM device IMEI number
MACMAC:xx:xx:xx:xx:xx:xxEthernet device MAC address
CMDCMD:x.x.xDevice's supported commands list
VV:xxxxxxxFirmware version
BTBT:xxxxxxxBootloader version
SNSN:xxxxxxxSerial number
FF:xFire sensor support (1 = has fire sensor and supports fire reset command)
NSNS:xStay/Sleep mode support (1 = does NOT support STAY and SLEEP mode)
COCO:xCustom Output feature (1 = device uses Custom Output feature)
UIDUID:xxxxxxxxxUID of device (can be omitted if IMEI or MAC used)

Field Delimiter: All fields are separated by comma (, = 0x2C)

Example Response

Example Response

Raw ASCII Data:

HW:2,Z:32,O:5,TRB:FFFFFFF,SYS:8,IMEI:45464689789,V:SP231_120612,BT:SP131x_boot_1v0,CMD:A.B.D.E.F.10

Field Breakdown:

  • HW:2 - Hardware ID is 2
  • Z:32 - Maximum 32 zones available
  • O:5 - Maximum 5 outputs available
  • TRB:FFFFFFF - Trouble types supported (hex format)
  • SYS:8 - Maximum 8 areas
  • IMEI:45464689789 - Device IMEI code
  • V:SP231_120612 - Firmware version SP231_120612
  • BT:SP131x_boot_1v0 - Bootloader version SP131x_boot_1v0
  • CMD:A.B.D.E.F.10 - List of supported Relay API commands
Usage Example
javascript
async function getDeviceCapabilities(partialUid) {
  // Build capabilities command
  const header = "070100000000000000";  // Standard header
  const command = "0100";                // Command 0x0001 in MSB format
  const commandData = header + command;
  
  try {
    const result = await sendDeviceCommand(partialUid, commandData, true);
    
    if (result.data && result.data.length > 0) {
      // Parse ASCII response
      const capabilitiesText = hexToAscii(result.data[0]);
      const capabilities = parseCapabilities(capabilitiesText);
      
      console.log('Device Capabilities:', capabilities);
      return capabilities;
    }
    
    throw new Error('No capabilities data received');
  } catch (error) {
    console.error('Failed to get capabilities:', error);
    throw error;
  }
}

function parseCapabilities(asciiData) {
  const fields = {};
  const pairs = asciiData.split(',');
  
  pairs.forEach(pair => {
    const [key, value] = pair.split(':');
    if (key && value) {
      fields[key] = value;
    }
  });
  
  return {
    hardwareId: fields.HW,
    maxZones: parseInt(fields.Z),
    maxOutputs: parseInt(fields.O),
    troubleReporting: fields.TRB,
    maxAreas: parseInt(fields.SYS),
    clockConfigurable: fields.TM === '1',
    maxSensors: parseInt(fields.SEN),
    imei: fields.IMEI,
    macAddress: fields.MAC,
    supportedCommands: fields.CMD,
    firmwareVersion: fields.V,
    bootloaderVersion: fields.BT,
    serialNumber: fields.SN,
    fireSupport: fields.F === '1',
    noStaySleep: fields.NS === '1',
    customOutput: fields.CO === '1'
  };
}

function hexToAscii(hex) {
  let ascii = '';
  for (let i = 0; i < hex.length; i += 2) {
    ascii += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
  }
  return ascii;
}

0x0002 - Status

Retrieves comprehensive system status including zone states, output states, partition status, signal strength, trouble bits, temperature sensors, and system time.

Note: Devices can send status responses (0x0002) autonomously without a command request.

Request/Response format

Request

[HEADER][0x0002] → 070100000000000000200

Response Format

Legacy Response (deprecated, old devices):

[HEADER][0x0002][DATA ASCII]

Modern Response (current devices):

[HEADER][0x000D][DATA ASCII]

Note: Modern devices respond with command 0x000D instead of 0x0002. The DAT:01 format is deprecated.

Response Data Format

The response data follows this structure:

#F:1,DAT:01,NR,{zones},{outputs},{partitions},{signal},{trouble},{temperature},{timestamp}

Response Data Fields

PositionFieldFormatDescription
1FF:1Data start indicator
2DATDAT:01Data type - system status
3NR00-FFMessage numbering (HEX ASCII)
4ZonesVariable hex charsZone states (1 char per zone, length depends on device config)
5OutputsVariable hex charsPGM output states (1 char per output, length depends on device config)
6PartitionsVariable hex charsSystem partition states (2 chars per partition, length depends on device config)
7Signal00-99Signal strength
8Trouble8 hex charsTrouble bits (32 bits)
9TemperatureVariable hex charsTemperature sensor states (1 char per sensor, length depends on device config)
10TimestampYYYY/MM/DD HH:MM:SSModule time

Note: Zone, output, partition, and temperature sensor field lengths vary based on device capabilities. Use the 0x0001 - Capabilities command to determine the actual counts for a specific device.

Zone States (Position 4)

Each zone is represented by 1 hex character (4 bits):

  • Bit 0: 1 = Zone enabled, 0 = Zone disabled (Not Available)
  • Bit 1: 1 = Zone alarm, 0 = Zone restore
  • Bit 2: 1 = Zone tamper/wire fault, 0 = Restore OK
  • Bit 3: 1 = Bypass ON, 0 = Bypass OFF

Output States (Position 5)

Each PGM output is represented by 1 hex character (4 bits):

  • Bit 0: 1 = Remote control enabled, 0 = Remote control disabled
  • Bit 1: 1 = Output state ON, 0 = Output state OFF
  • Bit 2: 1 = Reset fire sensor
  • Bit 3: Reserved

Partition States (Position 6)

Each partition uses 2 hex characters representing system states:

Hex ValueState NameDescription
00st_ARMArmed
01st_ARM_startingArming in progress
02st_ARM_waitingWaiting to arm
03st_PrepARMPreparing to arm
04st_DisARMDisarmed
05st_DisARM_waitingWaiting to disarm
06st_PrepDisARMPreparing to disarm
07st_PrepDisARMRemoteRemote disarm/rearm function
08st_Remote_DisARM_waitingRemote disarm waiting
09st_STAY_DELAYStay delay
0Ast_STAY_DELAY_startingStay delay starting
0Bst_STAY_Delay_waitingStay delay waiting
0Cst_STAY_Delay_waiting_prepareStay delay waiting prepare
0Dst_SLEEPSleep mode
0Est_SLEEP_startingSleep starting
0Fst_SLEEP_waiting_prepareSleep waiting prepare
10st_SLEEP_waitingSleep waiting
11st_CancelCancel

Signal Strength (Position 7)

Signal strength values:

  • 0 = -115 dBm or less
  • 1 = -111 dBm
  • 2-30 = -110 to -54 dBm
  • 31 = -52 dBm or greater
  • 99 = Not known or not detectable

Trouble Bits (Position 8)

32-bit trouble status (0 = OK, 1 = Fault):

  • Bit 0: Battery trouble
  • Bit 1: AC power loss
  • Bit 2: Aux current trouble
  • Bit 3: Bell trouble
  • Bit 4: Clock trouble
  • Bit 5: Communication trouble
  • Bit 6: COM BUS trouble
  • Bit 7: MCI trouble
  • Bit 8: Zone tamper
  • Bit 9: Fire alarm
  • Bits 10-31: Reserved

Temperature Sensors (Position 9)

Each temperature sensor is represented by 1 hex character (4 bits):

  • Bit 0: 1 = Sensor enabled, 0 = Sensor disabled (Not Available)
  • Bit 1: 1 = Sensor online/active, 0 = Sensor trouble
  • Bit 2: 1 = High temperature alarm, 0 = High temperature restore
  • Bit 3: 1 = Low temperature alarm, 0 = Low temperature restore
Example Response

Example Response

Raw ASCII Data:

#F:1,DAT:01,NR,1100000010,10103,0404040404040404,15,3331110000,2012/07/15 15:12:45

Field Breakdown:

  • F:1 - Data start indicator
  • DAT:01 - System status data type
  • NR - Message number
  • 1100000010 - Zone states: 10 zones configured (Zone 1&2 enabled+alarm, Zone 9 enabled only, others disabled)
  • 10103 - Output states: 5 outputs configured (OUT1 remote enabled, OUT2 state ON, OUT3 remote enabled)
  • 0404040404040404 - Partition states: 8 partitions configured, all in state 0x04 (st_DisARM)
  • 15 - Signal strength level 15 (-95 dBm approximately)
  • 3331110000 - Temperature sensors: 10 sensors configured (First 3 enabled+online, 4th enabled only)
  • 2012/07/15 15:12:45 - Current module time

Note: This example shows a device with 10 zones, 5 outputs, 8 partitions, and 10 temperature sensors. Actual field lengths will match the device's capabilities.

Usage Example
javascript
async function getSystemStatus(partialUid) {
  // Build status command
  const header = "070100000000000000";  // Standard header
  const command = "0200";                // Command 0x0002 in MSB format
  const commandData = header + command;
  
  try {
    const result = await sendDeviceCommand(partialUid, commandData, true);
    
    if (result.data && result.data.length > 0) {
      // Parse ASCII response
      const statusText = hexToAscii(result.data[0]);
      const status = parseSystemStatus(statusText);
      
      console.log('System Status:', status);
      return status;
    }
    
    throw new Error('No status data received');
  } catch (error) {
    console.error('Failed to get system status:', error);
    throw error;
  }
}

function parseSystemStatus(asciiData) {
  // Remove # prefix and split by commas
  const data = asciiData.replace('#', '').split(',');
  
  if (data.length < 10) {
    throw new Error('Invalid status data format');
  }
  
  return {
    dataStart: data[0], // F:1
    dataType: data[1],  // DAT:01
    messageNumber: data[2], // NR
    zones: parseZoneStates(data[3]),
    outputs: parseOutputStates(data[4]),
    partitions: parsePartitionStates(data[5]),
    signalStrength: parseInt(data[6]),
    troubleBits: parseTroubleBits(data[7]),
    temperatureSensors: parseTemperatureSensors(data[8]),
    timestamp: data[9]
  };
}

function parseZoneStates(zoneData) {
  const zones = [];
  for (let i = 0; i < zoneData.length; i++) {
    const zoneValue = parseInt(zoneData[i], 16);
    zones.push({
      zone: i + 1,
      enabled: !!(zoneValue & 0x01),
      alarm: !!(zoneValue & 0x02),
      tamper: !!(zoneValue & 0x04),
      bypass: !!(zoneValue & 0x08)
    });
  }
  return zones;
}

function parseOutputStates(outputData) {
  const outputs = [];
  for (let i = 0; i < outputData.length; i++) {
    const outputValue = parseInt(outputData[i], 16);
    outputs.push({
      output: i + 1,
      remoteControlEnabled: !!(outputValue & 0x01),
      state: !!(outputValue & 0x02),
      resetFireSensor: !!(outputValue & 0x04)
    });
  }
  return outputs;
}

function parsePartitionStates(partitionData) {
  const partitions = [];
  for (let i = 0; i < partitionData.length; i += 2) {
    const partitionValue = partitionData.substr(i, 2);
    partitions.push({
      partition: (i / 2) + 1,
      state: parseInt(partitionValue, 16),
      stateDescription: getPartitionStateDescription(parseInt(partitionValue, 16))
    });
  }
  return partitions;
}

function parseTroubleBits(troubleData) {
  const troubleValue = parseInt(troubleData, 16);
  return {
    battery: !!(troubleValue & 0x01),
    acLoss: !!(troubleValue & 0x02),
    auxCurrent: !!(troubleValue & 0x04),
    bell: !!(troubleValue & 0x08),
    clock: !!(troubleValue & 0x10),
    communication: !!(troubleValue & 0x20),
    comBus: !!(troubleValue & 0x40),
    mci: !!(troubleValue & 0x80),
    zoneTamper: !!(troubleValue & 0x100),
    fire: !!(troubleValue & 0x200)
  };
}

function parseTemperatureSensors(tempData) {
  const sensors = [];
  for (let i = 0; i < tempData.length; i++) {
    const sensorValue = parseInt(tempData[i], 16);
    sensors.push({
      sensor: i + 1,
      enabled: !!(sensorValue & 0x01),
      online: !!(sensorValue & 0x02),
      highTempAlarm: !!(sensorValue & 0x04),
      lowTempAlarm: !!(sensorValue & 0x08)
    });
  }
  return sensors;
}

function getPartitionStateDescription(state) {
  const states = {
    0x00: 'st_ARM',
    0x01: 'st_ARM_starting',
    0x02: 'st_ARM_waiting',
    0x03: 'st_PrepARM',
    0x04: 'st_DisARM',
    0x05: 'st_DisARM_waiting',
    0x06: 'st_PrepDisARM',
    0x07: 'st_PrepDisARMRemote',
    0x08: 'st_Remote_DisARM_waiting',
    0x09: 'st_STAY_DELAY',
    0x0A: 'st_STAY_DELAY_starting',
    0x0B: 'st_STAY_Delay_waiting',
    0x0C: 'st_STAY_Delay_waiting_prepare',
    0x0D: 'st_SLEEP',
    0x0E: 'st_SLEEP_starting',
    0x0F: 'st_SLEEP_waiting_prepare',
    0x10: 'st_SLEEP_waiting',
    0x11: 'st_Cancel'
  };
  return states[state] || `Unknown state: 0x${state.toString(16).toUpperCase()}`;
}

function hexToAscii(hex) {
  let ascii = '';
  for (let i = 0; i < hex.length; i += 2) {
    ascii += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
  }
  return ascii;
}

0x0003 - Set Output Level

Controls PGM output states, allowing you to turn outputs on or off. Includes special handling for fire sensor reset functionality.

Request/Response format

Request

[HEADER][0x0003][PGM:S] → 070100000000000000300[PGM:S]

Where:

  • PGM = Index of PGM output (decimal format)
  • S = Output status (0 = turn off, 1 = turn on)
  • Special case: PGM index 99 = Fire Reset

Examples:

  • Turn on PGM 1: 070100000000000000300313a31
  • Turn off PGM 3: 070100000000000000300333a30
  • Fire Reset: 07010000000000000030039393a31

Response Format

Success Response (Output State):

[HEADER][0x0003][O:xxxxx]

System State Response:

[HEADER][0x0002][SystemState]

Error Response:

[HEADER][0x0003][FORMAT]
[HEADER][0x0003][NACK]
[HEADER][0x0003][ERROR]

Note: The device may send a system state update (0x0002) in addition to the command response.

Command Parameters

ParameterFormatDescription
PGMDecimal numberPGM output index (1-based, device-specific range)
S0 or 1Output state: 0 = OFF, 1 = ON

Special PGM Indices

PGM IndexDescription
1-NRegular PGM outputs (N = device-specific, see capabilities)
99Fire Reset - Special command to reset fire sensors

Response Data Format

Output State Response:

O:xxxxx

Where xxxxx represents the current state of all PGM outputs (similar to O: field in 0x000D Status response).

Each character represents one output state using the same bit encoding:

  • Bit 0: 1 = Remote control enabled, 0 = Remote control disabled
  • Bit 1: 1 = Output state ON, 0 = Output state OFF
  • Bit 2: 1 = Reset fire sensor
  • Bit 3: Reserved

Event Notifications

Some devices can send event messages in Sur-Gard MLR2-DG multiline receiver Ademco Contact ID format when output states change:

Event Format:

<E|R> 780 <Area> <Zone>

Event Fields:

  • E = PGM activated event
  • R = PGM deactivated event
  • 780 = Event code for PGM control
  • Area = PGM number (decimal)
  • Zone = User ID (decimal)
Example Response

Example Responses

Turn ON PGM 2 - Success:

Request: 070100000000000000300323a31
Response: [HEADER][0x0003][O:10103]

Turn OFF PGM 1 - Success:

Request: 070100000000000000300313a30
Response: [HEADER][0x0003][O:00103]

Fire Reset - Success:

Request: 07010000000000000030039393a31  
Response: [HEADER][0x0003][O:10107]
Event: E 780 99 1234

Invalid PGM Index - Error:

Request: 0701000000000000030031303a31
Response: [HEADER][0x0003][NACK]

Response Breakdown:

  • O:10103 - 5 outputs: OUT1=OFF+remote, OUT2=ON+remote, OUT3=remote, OUT4=OFF, OUT5=OFF
  • O:00103 - Same as above but OUT1 turned OFF
  • O:10107 - Fire reset activated (bit 2 set on applicable output)
  • Event E 780 99 1234 - PGM activated, fire reset (99), by user 1234
Usage Example
javascript
async function setPgmOutput(partialUid, pgmIndex, state) {
  // Build set output command
  const header = "070100000000000000";  // Standard header
  const command = "0300";                // Command 0x0003 in MSB format
  
  // Build command data: PGM:S
  const commandData = `${pgmIndex}:${state}`;
  const fullCommand = header + command + asciiToHex(commandData);
  
  try {
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      // Parse ASCII response
      const responseText = hexToAscii(result.data[0]);
      const outputStates = parseOutputResponse(responseText);
      
      console.log(`PGM ${pgmIndex} set to ${state ? 'ON' : 'OFF'}`);
      console.log('Output states:', outputStates);
      return outputStates;
    }
    
    throw new Error('No response data received');
  } catch (error) {
    console.error(`Failed to set PGM ${pgmIndex}:`, error);
    throw error;
  }
}

// Convenience functions for common operations
async function turnOnOutput(partialUid, pgmIndex) {
  return await setPgmOutput(partialUid, pgmIndex, 1);
}

async function turnOffOutput(partialUid, pgmIndex) {
  return await setPgmOutput(partialUid, pgmIndex, 0);
}

async function fireReset(partialUid) {
  console.log('Initiating fire sensor reset...');
  return await setPgmOutput(partialUid, 99, 1);
}

// Control multiple outputs
async function setMultipleOutputs(partialUid, outputCommands) {
  const results = [];
  
  for (const cmd of outputCommands) {
    try {
      const result = await setPgmOutput(partialUid, cmd.pgm, cmd.state);
      results.push({
        pgm: cmd.pgm,
        state: cmd.state,
        success: true,
        result: result
      });
      
      // Small delay between commands to avoid overwhelming the device
      await new Promise(resolve => setTimeout(resolve, 100));
    } catch (error) {
      results.push({
        pgm: cmd.pgm,
        state: cmd.state,
        success: false,
        error: error.message
      });
    }
  }
  
  return results;
}

function parseOutputResponse(responseText) {
  // Expected format: O:xxxxx
  if (!responseText.startsWith('O:')) {
    throw new Error('Invalid output response format');
  }
  
  const outputData = responseText.substring(2);
  const outputs = [];
  
  for (let i = 0; i < outputData.length; i++) {
    const outputValue = parseInt(outputData[i], 16);
    outputs.push({
      output: i + 1,
      remoteControlEnabled: !!(outputValue & 0x01),
      state: !!(outputValue & 0x02),
      resetFireSensor: !!(outputValue & 0x04),
      raw: outputValue
    });
  }
  
  return outputs;
}

function asciiToHex(ascii) {
  let hex = '';
  for (let i = 0; i < ascii.length; i++) {
    hex += ascii.charCodeAt(i).toString(16).padStart(2, '0');
  }
  return hex;
}

function hexToAscii(hex) {
  let ascii = '';
  for (let i = 0; i < hex.length; i += 2) {
    ascii += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
  }
  return ascii;
}

// Usage examples
async function demonstrateOutputControl(partialUid) {
  try {
    // Turn on multiple outputs
    console.log('Setting multiple outputs...');
    const commands = [
      { pgm: 1, state: 1 },  // Turn on PGM 1
      { pgm: 2, state: 0 },  // Turn off PGM 2  
      { pgm: 3, state: 1 }   // Turn on PGM 3
    ];
    
    const results = await setMultipleOutputs(partialUid, commands);
    console.log('Batch operation results:', results);
    
    // Emergency fire reset
    console.log('Performing fire reset...');
    await fireReset(partialUid);
    
  } catch (error) {
    console.error('Output control demonstration failed:', error);
  }
}

0x0004 - Set Device Time

Sets the internal clock/time on the device. This command allows synchronizing the device's internal clock with an external time source.

Request/Response format

Request

[HEADER][0x0004][YYYY/MM/DD HH:mm:ss] → 070100000000000000400[YYYY/MM/DD HH:mm:ss]

Where:

  • YYYY/MM/DD HH:mm:ss = Date and time in ASCII format
  • Date format: YYYY/MM/DD (4-digit year, 2-digit month, 2-digit day)
  • Time format: HH:mm:ss (24-hour format with colons)

Examples:

  • Set to Dec 25, 2023 3:30:45 PM: 070100000000000000400323032332f31322f32352031353a33303a3435
  • Set to Jan 1, 2024 midnight: 070100000000000000400323032342f30312f30312030303a30303a3030

Response Format

Success Response:

[HEADER][0x0004][TM:YYYY/MM/DD HH-mm-ss]

Error Response:

[HEADER][0x0004][FORMAT]
[HEADER][0x0004][NACK]
[HEADER][0x0004][ERROR]

Note: The response uses hyphens (-) instead of colons (:) in the time portion to match the format used in 0x000D Status responses.

Command Parameters

ParameterFormatRequiredDescription
YYYY4 digitsYesYear (e.g., 2024)
MM2 digitsYesMonth (01-12)
DD2 digitsYesDay (01-31)
HH2 digitsYesHour (00-23, 24-hour format)
mm2 digitsYesMinutes (00-59)
ss2 digitsYesSeconds (00-59)

Time Format Requirements

Request Format:

  • Date separator: / (forward slash)
  • Date/Time separator: (space)
  • Time separator: : (colon)
  • Format: YYYY/MM/DD HH:mm:ss

Response Format:

  • Date separator: / (forward slash)
  • Date/Time separator: (space)
  • Time separator: - (hyphen)
  • Format: TM:YYYY/MM/DD HH-mm-ss

Device Compatibility

Clock Support: Use the 0x0001 - Capabilities command to check if the device supports time configuration:

  • TM:0 = Clock not configurable
  • TM:1 = Clock configurable (supports 0x0004 command)

Important Notes:

  • Only devices with TM:1 capability support this command
  • Time is stored in device's local timezone
  • Some devices may automatically adjust for daylight saving time
  • Setting time may trigger system events on some devices
Example Response

Example Responses

Set time to 2024/03/15 14:30:00 - Success:

Request: 070100000000000000400323032342f30332f31352031343a33303a3030
Response: [HEADER][0x0004][544d3a323032342f30332f31352031343a33303a3030]

Invalid date format - Error:

Request: 0701000000000000004004d61726368203135, 20323032342032 3a33302 504d
Response: [HEADER][0x0004][FORMAT]

Device doesn't support clock - Error:

Request: 070100000000000000400323032342f30332f31352031343a33303a3030
Response: [HEADER][0x0004][NACK]

Invalid time values - Error:

Request: 070100000000000000400323032342f31332f33322032353a37303a3830
Response: [HEADER][0x0004][FORMAT]
Usage Example
javascript
async function setDeviceTime(partialUid, dateTime) {
  // Build set time command
  const header = "070100000000000000";  // Standard header
  const command = "0400";                // Command 0x0004 in MSB format
  
  // Format datetime string (ensure proper format)
  let formattedDateTime;
  if (dateTime instanceof Date) {
    formattedDateTime = formatDateTimeForDevice(dateTime);
  } else if (typeof dateTime === 'string') {
    // Validate and use provided string
    if (!isValidDateTimeFormat(dateTime)) {
      throw new Error('Invalid datetime format. Use YYYY/MM/DD HH:mm:ss');
    }
    formattedDateTime = dateTime;
  } else {
    throw new Error('DateTime must be a Date object or string in YYYY/MM/DD HH:mm:ss format');
  }
  
  const fullCommand = header + command + asciiToHex(formattedDateTime);
  
  try {
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      // Parse ASCII response
      const responseText = hexToAscii(result.data[0]);
      const timeResponse = parseTimeResponse(responseText);
      
      console.log('Device time set successfully:', timeResponse);
      return timeResponse;
    }
    
    throw new Error('No response data received');
  } catch (error) {
    console.error('Failed to set device time:', error);
    throw error;
  }
}

// Convenience functions
async function setDeviceTimeNow(partialUid) {
  const now = new Date();
  return await setDeviceTime(partialUid, now);
}

async function setDeviceTimeFromString(partialUid, dateTimeString) {
  // Example: "2024/03/15 14:30:00"
  return await setDeviceTime(partialUid, dateTimeString);
}

async function checkClockSupport(partialUid) {
  // Get device capabilities to check clock support
  const capabilities = await getDeviceCapabilities(partialUid);
  return capabilities.clockConfigurable;
}

function formatDateTimeForDevice(date) {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  const hours = String(date.getHours()).padStart(2, '0');
  const minutes = String(date.getMinutes()).padStart(2, '0');
  const seconds = String(date.getSeconds()).padStart(2, '0');
  
  return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
}

function isValidDateTimeFormat(dateTimeString) {
  // Check format: YYYY/MM/DD HH:mm:ss
  const regex = /^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}$/;
  if (!regex.test(dateTimeString)) {
    return false;
  }
  
  // Parse and validate actual date values
  const parts = dateTimeString.split(' ');
  const dateParts = parts[0].split('/');
  const timeParts = parts[1].split(':');
  
  const year = parseInt(dateParts[0]);
  const month = parseInt(dateParts[1]);
  const day = parseInt(dateParts[2]);
  const hour = parseInt(timeParts[0]);
  const minute = parseInt(timeParts[1]);
  const second = parseInt(timeParts[2]);
  
  // Basic range validation
  if (month < 1 || month > 12) return false;
  if (day < 1 || day > 31) return false;
  if (hour < 0 || hour > 23) return false;
  if (minute < 0 || minute > 59) return false;
  if (second < 0 || second > 59) return false;
  
  // Create date object to check validity
  const testDate = new Date(year, month - 1, day, hour, minute, second);
  return testDate.getFullYear() === year &&
         testDate.getMonth() === month - 1 &&
         testDate.getDate() === day &&
         testDate.getHours() === hour &&
         testDate.getMinutes() === minute &&
         testDate.getSeconds() === second;
}

function parseTimeResponse(responseText) {
  // Expected format: TM:YYYY/MM/DD HH-mm-ss
  if (!responseText.startsWith('TM:')) {
    throw new Error('Invalid time response format');
  }
  
  const timeString = responseText.substring(3);  // Remove "TM:" prefix
  
  // Convert response format (HH-mm-ss) to standard format (HH:mm:ss)
  const standardFormat = timeString.replace(/(\d{2})-(\d{2})-(\d{2})$/, '$1:$2:$3');
  
  return {
    raw: responseText,
    formatted: standardFormat,
    timestamp: new Date(standardFormat.replace('/', '-').replace('/', '-'))
  };
}

function asciiToHex(ascii) {
  let hex = '';
  for (let i = 0; i < ascii.length; i++) {
    hex += ascii.charCodeAt(i).toString(16).padStart(2, '0');
  }
  return hex;
}

function hexToAscii(hex) {
  let ascii = '';
  for (let i = 0; i < hex.length; i += 2) {
    ascii += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
  }
  return ascii;
}

// Usage examples
async function demonstrateTimeManagement(partialUid) {
  try {
    // Check if device supports clock configuration
    console.log('Checking clock support...');
    const clockSupported = await checkClockSupport(partialUid);
    
    if (!clockSupported) {
      console.log('Device does not support clock configuration');
      return;
    }
    
    // Set device time to current time
    console.log('Setting device time to current time...');
    const currentTimeResult = await setDeviceTimeNow(partialUid);
    console.log('Current time set:', currentTimeResult);
    
    // Set specific time
    console.log('Setting specific time...');
    const specificTime = '2024/12/25 09:00:00';
    const specificTimeResult = await setDeviceTimeFromString(partialUid, specificTime);
    console.log('Specific time set:', specificTimeResult);
    
    // Synchronize with server time (example)
    console.log('Synchronizing with server time...');
    const serverTime = new Date(); // Would typically be from NTP or server API
    const syncResult = await setDeviceTime(partialUid, serverTime);
    console.log('Server time synchronized:', syncResult);
    
  } catch (error) {
    console.error('Time management demonstration failed:', error);
  }
}

0x0005 - Remote Device Reset

Sends a reboot command to the device. This command initiates a device restart/reset remotely without expecting a response from the device.

Request/Response format

Request

[HEADER][0x0005] → 070100000000000000500

Examples:

  • Standard reset: 070100000000000000500

Response Format

No Response Expected:

No response - Device will reboot immediately

Important Notes:

  • This is a fire-and-forget command
  • The device will not send a response before rebooting
  • Connection will be lost during the reset process
  • Device may take 30-120 seconds to fully restart and reconnect

Command Behavior

AspectDescription
ExecutionImmediate device reboot initiation
ResponseNone - command does not wait for acknowledgment
ConnectionWill be terminated during reset
Recovery TimeTypically 30-120 seconds for full restart

Safety Considerations

⚠️ Important Warnings:

  • No Confirmation: Device resets immediately without confirmation
  • Connection Loss: All active connections will be terminated
  • Service Interruption: Device services will be unavailable during restart
  • Unsaved Data: Any unsaved device state may be lost
  • Remote Recovery: Ensure device can be accessed after restart

Use Cases

ScenarioDescription
System RecoveryReset device when it becomes unresponsive
Configuration ApplyForce restart after configuration changes
Memory ClearClear temporary states and caches
MaintenanceScheduled system maintenance restarts

Device Recovery Process

  1. Command Sent: Device receives 0x0005 command
  2. Immediate Reset: Device begins shutdown process
  3. Connection Lost: All network connections terminated
  4. Boot Process: Device hardware and firmware restart
  5. Service Start: Device services and networking restart
  6. Ready State: Device becomes available and reconnects
Example Usage

Example Scenarios

Device Reset:

Request: 070100000000000000500
Response: [Connection lost - no response]
Usage Example
javascript
async function resetDevice(partialUid, waitForReconnect = true) {
  // Build reset command
  const header = "070100000000000000";  // Standard header
  const command = "0500";                // Command 0x0005 in MSB format
  const fullCommand = header + command;
  
  try {
    console.log(`Initiating device reset for ${partialUid}...`);
    
    // Send reset command (expect no response)
    await sendDeviceCommand(partialUid, fullCommand, false);
    console.log('Reset command sent successfully');
    
    if (waitForReconnect) {
      console.log('Waiting for device to restart and reconnect...');
      await waitForDeviceReconnection(partialUid);
      console.log('Device has successfully restarted and reconnected');
    }
    
    return {
      success: true,
      message: 'Device reset completed',
      timestamp: new Date().toISOString()
    };
    
  } catch (error) {
    // Connection errors are expected during reset
    if (error.message.includes('connection') || error.message.includes('timeout')) {
      console.log('Connection lost as expected during device reset');
      
      if (waitForReconnect) {
        console.log('Waiting for device to restart...');
        await waitForDeviceReconnection(partialUid);
        console.log('Device has successfully restarted');
      }
      
      return {
        success: true,
        message: 'Device reset initiated (connection lost as expected)',
        timestamp: new Date().toISOString()
      };
    }
    
    console.error('Failed to send reset command:', error);
    throw error;
  }
}

async function waitForDeviceReconnection(partialUid, maxWaitMinutes = 5) {
  const maxWaitMs = maxWaitMinutes * 60 * 1000;
  const checkInterval = 10000; // Check every 10 seconds
  const startTime = Date.now();
  
  console.log(`Waiting up to ${maxWaitMinutes} minutes for device reconnection...`);
  
  while (Date.now() - startTime < maxWaitMs) {
    try {
      // Try to get device status to check if it's back online
      const status = await getDeviceStatus(partialUid);
      if (status) {
        const waitTime = Math.round((Date.now() - startTime) / 1000);
        console.log(`Device reconnected after ${waitTime} seconds`);
        return true;
      }
    } catch (error) {
      // Device still not ready, continue waiting
      const elapsed = Math.round((Date.now() - startTime) / 1000);
      console.log(`Still waiting... (${elapsed}s elapsed)`);
    }
    
    // Wait before next check
    await new Promise(resolve => setTimeout(resolve, checkInterval));
  }
  
  throw new Error(`Device did not reconnect within ${maxWaitMinutes} minutes`);
}

// Convenience functions
async function emergencyReset(partialUid) {
  console.log('🚨 EMERGENCY RESET - Device may be unresponsive');
  return await resetDevice(partialUid, true);
}

async function scheduledRestart(partialUid) {
  console.log('🔧 SCHEDULED RESTART - Performing maintenance reboot');
  return await resetDevice(partialUid, true);
}

async function quickReset(partialUid) {
  // Reset without waiting for reconnection (fire and forget)
  console.log('⚡ QUICK RESET - Not waiting for reconnection');
  return await resetDevice(partialUid, false);
}

// Enhanced reset with pre-checks
async function safeDeviceReset(partialUid) {
  try {
    // Optional: Check device status before reset
    console.log('Checking device status before reset...');
    try {
      const preResetStatus = await getDeviceStatus(partialUid);
      console.log('Pre-reset status obtained:', preResetStatus ? 'Device responsive' : 'Device unresponsive');
    } catch (error) {
      console.log('Device appears unresponsive, proceeding with reset...');
    }
    
    // Perform the reset
    const result = await resetDevice(partialUid, true);
    
    // Optional: Verify device functionality after reset
    console.log('Verifying device functionality after reset...');
    const postResetStatus = await getDeviceStatus(partialUid);
    console.log('Post-reset verification successful');
    
    return {
      ...result,
      preResetCheck: true,
      postResetVerification: true
    };
    
  } catch (error) {
    console.error('Safe device reset failed:', error);
    throw error;
  }
}

// Usage examples
async function demonstrateDeviceReset(partialUid) {
  try {
    // Example 1: Emergency reset
    console.log('\n=== Emergency Reset Example ===');
    await emergencyReset(partialUid);
    
    // Wait a bit between examples
    await new Promise(resolve => setTimeout(resolve, 2000));
    
    // Example 2: Scheduled maintenance restart
    console.log('\n=== Scheduled Restart Example ===');
    await scheduledRestart(partialUid);
    
    // Example 3: Quick reset without waiting
    console.log('\n=== Quick Reset Example ===');
    await quickReset(partialUid);
    
  } catch (error) {
    console.error('Device reset demonstration failed:', error);
  }
}

0x0006 - BYPASS Zone

Controls zone bypass functionality, allowing zones to be bypassed (ignored during arming) or restored to normal operation.

Request/Response format

Request

[HEADER][0x0006][BYP] → 070100000000000000600[BYP]

Where:

  • BYP = Bypass data string with zone states
  • Format: String of hex characters representing zone bypass states
  • Each character: 0 = Normal operation, 1 = Bypassed

Examples:

  • Bypass zones 1 and 3: 070100000000000000600313031
  • Restore all zones to normal: 070100000000000000600303030303030
  • Bypass zone 5 only: 070100000000000000600303030313030

Response Format

Legacy Response (old devices):

[HEADER][0x0002][SystemState]

Modern Response (current devices):

[HEADER][0x0006][Z:xxxxxxx]

Error Response:

[HEADER][0x0006][FORMAT]
[HEADER][0x0006][NACK]
[HEADER][0x0006][ERROR]

Note: Legacy devices respond with a full system state update (0x0002), while modern devices return the specific zone bypass states in Z: format.

Command Parameters

ParameterFormatDescription
BYPHex stringZone bypass states (1 hex char per zone)

Bypass States

Each zone is represented by 1 hex character:

  • 0 = Normal operation - Zone is active and will trigger alarms
  • 1 = Bypassed - Zone is ignored during arming and operation

Zone Configuration

Zone Count: Use the 0x0001 - Capabilities command to determine the maximum number of zones (Z:x field) supported by the device.

Zone Indexing: Zones are indexed starting from 1:

  • First character = Zone 1
  • Second character = Zone 2
  • And so on...

Response Data Format

Modern Response (Z: format):

Z:xxxxxxx

Where xxxxxxx represents the current bypass state of all zones, using the same format as the Zone States field in 0x0002 Status responses.

Each hex character represents one zone with bit encoding:

  • Bit 0: 1 = Zone enabled, 0 = Zone disabled (Not Available)
  • Bit 1: 1 = Zone alarm, 0 = Zone restore
  • Bit 2: 1 = Zone tamper/wire fault, 0 = Restore OK
  • Bit 3: 1 = Bypass ON, 0 = Bypass OFF

Safety Considerations

⚠️ Important Warnings:

  • Security Impact: Bypassed zones will not trigger alarms
  • Zone Status: Bypassed zones may still show tamper/fault conditions
  • Arming Impact: System can be armed with bypassed zones
  • Automatic Restore: Some systems may automatically restore bypassed zones after disarming
Example Response

Example Responses

Bypass zones 1 and 3 - Success:

Request: 070100000000000000600313031
Response: [HEADER][0x0006][Z:9000900000]

Restore all zones to normal - Success:

Request: 070100000000000000600303030303030
Response: [HEADER][0x0006][Z:1000100010]

Bypass zone 5 only - Success:

Request: 070100000000000000600303030313030
Response: [HEADER][0x0006][Z:1000100090]

Invalid zone configuration - Error:

Request: 0701000000000000006003130313230
Response: [HEADER][0x0006][FORMAT]

Response Breakdown:

  • Z:9000900000 - Zone 1 and 3 bypassed (hex 9 = binary 1001: enabled + bypassed)
  • Z:1000100010 - All zones normal operation (hex 1 = binary 0001: enabled only)
  • Z:1000100090 - Zone 5 bypassed, others normal
Usage Example
javascript
async function setZoneBypass(partialUid, zoneStates) {
  // Build bypass command
  const header = "070100000000000000";  // Standard header
  const command = "0600";                // Command 0x0006 in MSB format
  
  // Build bypass data string
  let bypassData;
  if (Array.isArray(zoneStates)) {
    // Convert array of boolean values to hex string
    bypassData = zoneStates.map(bypassed => bypassed ? '1' : '0').join('');
  } else if (typeof zoneStates === 'string') {
    // Use provided hex string directly
    bypassData = zoneStates;
  } else {
    throw new Error('Zone states must be an array of booleans or a hex string');
  }
  
  const fullCommand = header + command + asciiToHex(bypassData);
  
  try {
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      // Parse ASCII response
      const responseText = hexToAscii(result.data[0]);
      const zoneResponse = parseZoneBypassResponse(responseText);
      
      console.log('Zone bypass states updated:', zoneResponse);
      return zoneResponse;
    }
    
    throw new Error('No response data received');
  } catch (error) {
    console.error('Failed to set zone bypass:', error);
    throw error;
  }
}

// Convenience functions for common operations
async function bypassZone(partialUid, zoneNumber, totalZones) {
  const zoneStates = new Array(totalZones).fill(false);
  zoneStates[zoneNumber - 1] = true; // Zone numbers are 1-based
  return await setZoneBypass(partialUid, zoneStates);
}

async function bypassMultipleZones(partialUid, zoneNumbers, totalZones) {
  const zoneStates = new Array(totalZones).fill(false);
  zoneNumbers.forEach(zoneNum => {
    if (zoneNum >= 1 && zoneNum <= totalZones) {
      zoneStates[zoneNum - 1] = true; // Zone numbers are 1-based
    }
  });
  return await setZoneBypass(partialUid, zoneStates);
}

async function restoreZone(partialUid, zoneNumber, totalZones) {
  const zoneStates = new Array(totalZones).fill(false);
  return await setZoneBypass(partialUid, zoneStates);
}

async function restoreAllZones(partialUid, totalZones) {
  const zoneStates = new Array(totalZones).fill(false);
  return await setZoneBypass(partialUid, zoneStates);
}

// Get current zone bypass states
async function getZoneBypassStates(partialUid) {
  try {
    // Get system status to see current zone states
    const status = await getSystemStatus(partialUid);
    
    if (status && status.zones) {
      return status.zones.map(zone => ({
        zone: zone.zone,
        bypassed: zone.bypass,
        enabled: zone.enabled,
        alarm: zone.alarm,
        tamper: zone.tamper
      }));
    }
    
    throw new Error('No zone status data available');
  } catch (error) {
    console.error('Failed to get zone bypass states:', error);
    throw error;
  }
}

function parseZoneBypassResponse(responseText) {
  // Expected format: Z:xxxxxxx
  if (!responseText.startsWith('Z:')) {
    throw new Error('Invalid zone bypass response format');
  }
  
  const zoneData = responseText.substring(2);
  const zones = [];
  
  for (let i = 0; i < zoneData.length; i++) {
    const zoneValue = parseInt(zoneData[i], 16);
    zones.push({
      zone: i + 1,
      enabled: !!(zoneValue & 0x01),
      alarm: !!(zoneValue & 0x02),
      tamper: !!(zoneValue & 0x04),
      bypassed: !!(zoneValue & 0x08),
      raw: zoneValue
    });
  }
  
  return {
    zones: zones,
    bypassedZones: zones.filter(z => z.bypassed).map(z => z.zone),
    normalZones: zones.filter(z => !z.bypassed && z.enabled).map(z => z.zone)
  };
}

function asciiToHex(ascii) {
  let hex = '';
  for (let i = 0; i < ascii.length; i++) {
    hex += ascii.charCodeAt(i).toString(16).padStart(2, '0');
  }
  return hex;
}

function hexToAscii(hex) {
  let ascii = '';
  for (let i = 0; i < hex.length; i += 2) {
    ascii += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
  }
  return ascii;
}

// Usage examples
async function demonstrateZoneBypass(partialUid) {
  try {
    // Get device capabilities to determine zone count
    console.log('Getting device capabilities...');
    const capabilities = await getDeviceCapabilities(partialUid);
    const maxZones = capabilities.maxZones;
    console.log(`Device supports ${maxZones} zones`);
    
    // Get current zone bypass states
    console.log('Getting current zone states...');
    const currentStates = await getZoneBypassStates(partialUid);
    console.log('Current zone states:', currentStates);
    
    // Bypass specific zones
    console.log('Bypassing zones 1 and 3...');
    const bypassResult1 = await bypassMultipleZones(partialUid, [1, 3], maxZones);
    console.log('Bypass result:', bypassResult1);
    
    // Wait a moment
    await new Promise(resolve => setTimeout(resolve, 2000));
    
    // Bypass a single zone
    console.log('Bypassing zone 5 only...');
    const bypassResult2 = await bypassZone(partialUid, 5, maxZones);
    console.log('Single zone bypass result:', bypassResult2);
    
    // Wait a moment
    await new Promise(resolve => setTimeout(resolve, 2000));
    
    // Restore all zones to normal
    console.log('Restoring all zones to normal...');
    const restoreResult = await restoreAllZones(partialUid, maxZones);
    console.log('Restore result:', restoreResult);
    
    // Final status check
    console.log('Final zone states:');
    const finalStates = await getZoneBypassStates(partialUid);
    console.log('Final states:', finalStates);
    
  } catch (error) {
    console.error('Zone bypass demonstration failed:', error);
  }
}

// Advanced bypass management
async function smartZoneBypass(partialUid, options = {}) {
  const {
    zonesToBypass = [],
    zonesToRestore = [],
    restoreAll = false,
    checkAlarms = true
  } = options;
  
  try {
    // Get device capabilities
    const capabilities = await getDeviceCapabilities(partialUid);
    const maxZones = capabilities.maxZones;
    
    // Get current zone states if checking alarms
    let currentStates = [];
    if (checkAlarms) {
      currentStates = await getZoneBypassStates(partialUid);
      
      // Warn if trying to bypass zones that are in alarm
      const alarmedZones = currentStates.filter(z => z.alarm && zonesToBypass.includes(z.zone));
      if (alarmedZones.length > 0) {
        console.warn('Warning: Attempting to bypass zones currently in alarm:', 
                     alarmedZones.map(z => z.zone));
      }
    }
    
    // Build new zone states
    let newStates;
    if (restoreAll) {
      newStates = new Array(maxZones).fill(false);
    } else {
      // Start with current bypass states or all normal
      newStates = currentStates.length > 0 
        ? currentStates.map(z => z.bypassed)
        : new Array(maxZones).fill(false);
      
      // Apply bypass changes
      zonesToBypass.forEach(zoneNum => {
        if (zoneNum >= 1 && zoneNum <= maxZones) {
          newStates[zoneNum - 1] = true;
        }
      });
      
      // Apply restore changes
      zonesToRestore.forEach(zoneNum => {
        if (zoneNum >= 1 && zoneNum <= maxZones) {
          newStates[zoneNum - 1] = false;
        }
      });
    }
    
    // Apply the new bypass configuration
    const result = await setZoneBypass(partialUid, newStates);
    
    return {
      success: true,
      bypassedZones: result.bypassedZones,
      normalZones: result.normalZones,
      totalZones: maxZones,
      changes: {
        bypassed: zonesToBypass,
        restored: zonesToRestore,
        restoredAll: restoreAll
      }
    };
    
  } catch (error) {
    console.error('Smart zone bypass failed:', error);
    throw error;
  }
}

0x0007 - Get Device TIME

Retrieves the current date and time from the device's internal clock. This command allows reading the device's time without modifying it.

Request/Response format

Request

[HEADER][0x0007] → 070100000000000000700

Examples:

  • Get current device time: 070100000000000000700

Response Format

Success Response:

[HEADER][0x0007][TM:YYYY/MM/DD HH:MM:SS]

Error Response:

[HEADER][0x0007][NACK]
[HEADER][0x0007][ERROR]

Note: The response uses the same time format as the timestamp field in 0x000D Status responses.

Command Parameters

This command has no parameters - it simply requests the current device time.

Response Data Format

Time Response:

TM:YYYY/MM/DD HH:MM:SS

Where:

  • YYYY = 4-digit year
  • MM = 2-digit month (01-12)
  • DD = 2-digit day (01-31)
  • HH = 2-digit hour (00-23, 24-hour format)
  • MM = 2-digit minutes (00-59)
  • SS = 2-digit seconds (00-59)

Time Format Details

Format Structure:

  • Date separator: / (forward slash)
  • Date/Time separator: (space)
  • Time separator: : (colon)
  • Complete format: TM:YYYY/MM/DD HH:MM:SS

Device Compatibility

Clock Support: Use the 0x0001 - Capabilities command to check if the device has an internal clock:

  • TM:0 = Clock not available/configurable
  • TM:1 = Clock available and configurable

Important Notes:

  • Devices without internal clocks will return NACK
  • Time is returned in device's local timezone
  • Time accuracy depends on device's clock source and synchronization
  • Some devices may return approximate time if clock is not precisely synchronized

Use Cases

ScenarioDescription
Time VerificationCheck if device time matches expected time
Synchronization CheckVerify time before/after synchronization
Logging CorrelationGet device timestamp for event correlation
Diagnostic InformationInclude device time in system diagnostics
Example Response

Example Responses

Get current time - Success:

Request: 070100000000000000700
Response: [HEADER][0x0007][544d3a323031322f30372f31352031353a31323a3435]

Device without clock - Error:

Request: 070100000000000000700
Response: [HEADER][0x0007][NACK]

Clock not synchronized - Error:

Request: 070100000000000000700
Response: [HEADER][0x0007][ERROR]

Response Breakdown:

  • TM:2012/07/15 15:12:45 - Device time: July 15, 2012 at 3:12:45 PM
  • Hex encoded: 544d3a323031322f30372f31352031353a31323a3435
Usage Example
javascript
async function getDeviceTime(partialUid) {
  // Build get time command
  const header = "070100000000000000";  // Standard header
  const command = "0700";                // Command 0x0007 in MSB format
  const fullCommand = header + command;
  
  try {
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      // Parse ASCII response
      const responseText = hexToAscii(result.data[0]);
      const timeData = parseTimeResponse(responseText);
      
      console.log('Device time retrieved:', timeData);
      return timeData;
    }
    
    throw new Error('No time data received');
  } catch (error) {
    console.error('Failed to get device time:', error);
    throw error;
  }
}

// Time comparison and validation functions
async function compareDeviceTimeWithSystem(partialUid, toleranceSeconds = 30) {
  try {
    const deviceTimeData = await getDeviceTime(partialUid);
    const systemTime = new Date();
    const deviceTime = deviceTimeData.timestamp;
    
    const timeDifferenceMs = Math.abs(systemTime.getTime() - deviceTime.getTime());
    const timeDifferenceSeconds = Math.floor(timeDifferenceMs / 1000);
    
    const isWithinTolerance = timeDifferenceSeconds <= toleranceSeconds;
    
    return {
      deviceTime: deviceTimeData,
      systemTime: systemTime,
      difference: {
        milliseconds: timeDifferenceMs,
        seconds: timeDifferenceSeconds,
        formatted: formatTimeDifference(timeDifferenceMs)
      },
      withinTolerance: isWithinTolerance,
      toleranceSeconds: toleranceSeconds,
      needsSynchronization: !isWithinTolerance
    };
  } catch (error) {
    console.error('Failed to compare device time with system time:', error);
    throw error;
  }
}

async function checkDeviceClockHealth(partialUid) {
  try {
    // Check if device supports clock
    console.log('Checking device clock capabilities...');
    const capabilities = await getDeviceCapabilities(partialUid);
    
    if (!capabilities.clockConfigurable) {
      return {
        supported: false,
        message: 'Device does not have an internal clock',
        recommendation: 'Time-dependent features may not be available'
      };
    }
    
    // Get device time
    console.log('Retrieving device time...');
    const deviceTimeData = await getDeviceTime(partialUid);
    
    // Compare with system time
    console.log('Comparing with system time...');
    const comparison = await compareDeviceTimeWithSystem(partialUid, 60); // 1 minute tolerance
    
    // Analyze clock health
    const health = {
      supported: true,
      deviceTime: deviceTimeData,
      comparison: comparison,
      status: 'unknown',
      recommendation: ''
    };
    
    if (comparison.withinTolerance) {
      health.status = 'healthy';
      health.recommendation = 'Device clock is synchronized and accurate';
    } else if (comparison.difference.seconds < 300) { // 5 minutes
      health.status = 'drift';
      health.recommendation = 'Minor time drift detected, consider synchronization';
    } else if (comparison.difference.seconds < 3600) { // 1 hour
      health.status = 'significant_drift';
      health.recommendation = 'Significant time drift detected, synchronization recommended';
    } else {
      health.status = 'critical_drift';
      health.recommendation = 'Critical time drift detected, immediate synchronization required';
    }
    
    return health;
    
  } catch (error) {
    return {
      supported: false,
      error: error.message,
      status: 'error',
      recommendation: 'Unable to assess clock health due to error'
    };
  }
}

// Time synchronization workflow
async function synchronizeDeviceTime(partialUid, sourceTime = null) {
  try {
    console.log('Starting time synchronization workflow...');
    
    // Get current device time for comparison
    console.log('Getting current device time...');
    const currentDeviceTime = await getDeviceTime(partialUid);
    console.log('Current device time:', currentDeviceTime.formatted);
    
    // Determine source time
    const syncTime = sourceTime || new Date();
    console.log('Synchronization target time:', syncTime.toISOString());
    
    // Set the new time
    console.log('Setting device time...');
    const setResult = await setDeviceTime(partialUid, syncTime);
    
    // Verify synchronization
    console.log('Verifying synchronization...');
    await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second
    const newDeviceTime = await getDeviceTime(partialUid);
    
    // Calculate synchronization accuracy
    const syncAccuracy = Math.abs(
      new Date().getTime() - newDeviceTime.timestamp.getTime()
    ) / 1000;
    
    return {
      success: true,
      oldTime: currentDeviceTime,
      newTime: newDeviceTime,
      targetTime: syncTime,
      accuracySeconds: syncAccuracy,
      synchronized: syncAccuracy <= 5 // Consider synchronized if within 5 seconds
    };
    
  } catch (error) {
    console.error('Time synchronization failed:', error);
    throw error;
  }
}

function parseTimeResponse(responseText) {
  // Expected format: TM:YYYY/MM/DD HH:MM:SS
  if (!responseText.startsWith('TM:')) {
    throw new Error('Invalid time response format');
  }
  
  const timeString = responseText.substring(3);  // Remove "TM:" prefix
  
  // Parse the time string
  const timeRegex = /^(\d{4})\/(\d{2})\/(\d{2}) (\d{2}):(\d{2}):(\d{2})$/;
  const match = timeString.match(timeRegex);
  
  if (!match) {
    throw new Error('Invalid time format in response');
  }
  
  const [, year, month, day, hour, minute, second] = match;
  const timestamp = new Date(
    parseInt(year),
    parseInt(month) - 1, // Month is 0-based in JavaScript
    parseInt(day),
    parseInt(hour),
    parseInt(minute),
    parseInt(second)
  );
  
  return {
    raw: responseText,
    formatted: timeString,
    timestamp: timestamp,
    components: {
      year: parseInt(year),
      month: parseInt(month),
      day: parseInt(day),
      hour: parseInt(hour),
      minute: parseInt(minute),
      second: parseInt(second)
    }
  };
}

function formatTimeDifference(milliseconds) {
  const seconds = Math.floor(milliseconds / 1000);
  const minutes = Math.floor(seconds / 60);
  const hours = Math.floor(minutes / 60);
  const days = Math.floor(hours / 24);
  
  if (days > 0) {
    return `${days}d ${hours % 24}h ${minutes % 60}m ${seconds % 60}s`;
  } else if (hours > 0) {
    return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
  } else if (minutes > 0) {
    return `${minutes}m ${seconds % 60}s`;
  } else {
    return `${seconds}s`;
  }
}

function hexToAscii(hex) {
  let ascii = '';
  for (let i = 0; i < hex.length; i += 2) {
    ascii += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
  }
  return ascii;
}

// Usage examples
async function demonstrateTimeRetrieval(partialUid) {
  try {
    // Basic time retrieval
    console.log('=== Basic Time Retrieval ===');
    const deviceTime = await getDeviceTime(partialUid);
    console.log('Device time:', deviceTime);
    
    // Time comparison with system
    console.log('\n=== Time Comparison ===');
    const comparison = await compareDeviceTimeWithSystem(partialUid);
    console.log('Time comparison result:', comparison);
    
    // Clock health check
    console.log('\n=== Clock Health Check ===');
    const healthCheck = await checkDeviceClockHealth(partialUid);
    console.log('Clock health:', healthCheck);
    
    // Conditional synchronization
    if (healthCheck.status === 'significant_drift' || healthCheck.status === 'critical_drift') {
      console.log('\n=== Time Synchronization ===');
      const syncResult = await synchronizeDeviceTime(partialUid);
      console.log('Synchronization result:', syncResult);
    }
    
  } catch (error) {
    console.error('Time demonstration failed:', error);
  }
}

// Scheduled time monitoring
async function monitorDeviceTime(partialUid, intervalMinutes = 60) {
  console.log(`Starting time monitoring for device ${partialUid} (every ${intervalMinutes} minutes)`);
  
  const monitor = setInterval(async () => {
    try {
      const comparison = await compareDeviceTimeWithSystem(partialUid, 120); // 2 minute tolerance
      
      console.log(`[${new Date().toISOString()}] Device time check:`, {
        deviceTime: comparison.deviceTime.formatted,
        difference: comparison.difference.formatted,
        status: comparison.withinTolerance ? 'OK' : 'DRIFT'
      });
      
      // Auto-sync if drift is significant
      if (!comparison.withinTolerance && comparison.difference.seconds > 300) {
        console.log('Significant drift detected, initiating auto-synchronization...');
        await synchronizeDeviceTime(partialUid);
      }
      
    } catch (error) {
      console.error('Time monitoring check failed:', error);
    }
  }, intervalMinutes * 60 * 1000);
  
  return monitor; // Return interval ID for cleanup
}

0x0008 - Get Sensors Values

Retrieves temperature sensor values and states from all configured sensors on the device. The response format is similar to the SEN: field in 0x000D Status responses.

Request/Response format

Request

[HEADER][0x0008] → 070100000000000000800

Examples:

  • Get all sensor values: 070100000000000000800

Response Format

Success Response:

[HEADER][0x0008][TempSens]

Error Response:

[HEADER][0x0008][NACK]
[HEADER][0x0008][ERROR]

Note: The response format is identical to the SEN: field format used in 0x000D Status responses but contains only sensor data.

Command Parameters

This command has no parameters - it requests data from all configured temperature sensors.

Response Data Format

Temperature Sensors Response:

TempSens = Variable hex chars (1 char per sensor)

Each temperature sensor is represented by 1 hex character (4 bits) with the following bit encoding:

  • Bit 0: 1 = Sensor enabled, 0 = Sensor disabled (Not Available)
  • Bit 1: 1 = Sensor online/active, 0 = Sensor trouble/offline
  • Bit 2: 1 = High temperature alarm, 0 = High temperature restore
  • Bit 3: 1 = Low temperature alarm, 0 = Low temperature restore

Sensor States

Hex ValueBinaryDescription
00000Sensor disabled/not available
10001Sensor enabled, no issues
20010Sensor enabled, offline/trouble
30011Sensor enabled, online
50101Sensor enabled, high temp alarm
70111Sensor enabled, online + high temp alarm
91001Sensor enabled, low temp alarm
B1011Sensor enabled, online + low temp alarm
D1101Sensor enabled, both temp alarms
F1111Sensor enabled, online + both temp alarms

Device Compatibility

Sensor Support: Use the 0x0001 - Capabilities command to check sensor availability:

  • SEN:0 = No temperature sensors available
  • SEN:x = Maximum number of temperature sensors (where x > 0)

Important Notes:

  • Devices without temperature sensors will return NACK
  • Sensor count and availability varies by device model
  • Response length matches the sensor count from capabilities
  • Sensors may be physically absent but still appear in response as disabled (hex 0)

Use Cases

ScenarioDescription
Environmental MonitoringTrack temperature conditions in protected areas
Alarm VerificationCheck sensor states during temperature events
Maintenance PlanningIdentify sensors with communication issues
System DiagnosticsMonitor sensor health and connectivity
Example Response

Example Responses

Get sensor values - Success (5 sensors):

Request: 070100000000000000800
Response: [HEADER][0x0008][3331110000]

Get sensor values - Success (8 sensors):

Request: 070100000000000000800
Response: [HEADER][0x0008][3310250000F1]

Device without sensors - Error:

Request: 070100000000000000800
Response: [HEADER][0x0008][NACK]

Response Breakdown:

Example 1: 3331110000

  • Sensor 1: 3 = Enabled + Online (binary 0011)
  • Sensor 2: 3 = Enabled + Online (binary 0011)
  • Sensor 3: 3 = Enabled + Online (binary 0011)
  • Sensor 4: 1 = Enabled only (binary 0001)
  • Sensor 5: 1 = Enabled only (binary 0001)
  • Sensors 6-10: 0 = Disabled/Not available

Example 2: 3310250000F1

  • Sensor 1: 3 = Enabled + Online
  • Sensor 2: 3 = Enabled + Online
  • Sensor 3: 1 = Enabled only
  • Sensor 4: 0 = Disabled
  • Sensor 5: 2 = Enabled but offline/trouble
  • Sensor 6: 5 = Enabled + High temp alarm
  • Sensor 7: 0 = Disabled
  • Sensor 8: 0 = Disabled
  • Sensor 9: 0 = Disabled
  • Sensor 10: 0 = Disabled
  • Sensor 11: F = Enabled + Online + Both temp alarms
  • Sensor 12: 1 = Enabled only
Usage Example
javascript
async function getSensorValues(partialUid) {
  // Build get sensors command
  const header = "070100000000000000";  // Standard header
  const command = "0800";                // Command 0x0008 in MSB format
  const fullCommand = header + command;
  
  try {
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      // Parse hex response directly (no ASCII conversion needed)
      const sensorData = result.data[0];
      const sensors = parseSensorValues(sensorData);
      
      console.log('Sensor values retrieved:', sensors);
      return sensors;
    }
    
    throw new Error('No sensor data received');
  } catch (error) {
    console.error('Failed to get sensor values:', error);
    throw error;
  }
}

// Enhanced sensor monitoring with capabilities check
async function getFullSensorStatus(partialUid) {
  try {
    // First check device capabilities
    console.log('Checking sensor capabilities...');
    const capabilities = await getDeviceCapabilities(partialUid);
    
    if (!capabilities.maxSensors || capabilities.maxSensors === 0) {
      return {
        supported: false,
        message: 'Device does not have temperature sensors',
        sensors: []
      };
    }
    
    console.log(`Device supports ${capabilities.maxSensors} temperature sensors`);
    
    // Get sensor values
    const sensors = await getSensorValues(partialUid);
    
    // Analyze sensor health
    const healthSummary = analyzeSensorHealth(sensors);
    
    return {
      supported: true,
      maxSensors: capabilities.maxSensors,
      sensors: sensors,
      health: healthSummary
    };
    
  } catch (error) {
    console.error('Failed to get full sensor status:', error);
    throw error;
  }
}

// Sensor monitoring and alerting
async function monitorTemperatureSensors(partialUid, alertCallback = null) {
  try {
    const sensorStatus = await getFullSensorStatus(partialUid);
    
    if (!sensorStatus.supported) {
      console.log('Temperature monitoring not available - no sensors found');
      return sensorStatus;
    }
    
    // Check for alerts
    const alerts = [];
    
    sensorStatus.sensors.forEach(sensor => {
      if (!sensor.enabled) return;
      
      if (!sensor.online) {
        alerts.push({
          sensor: sensor.sensor,
          type: 'offline',
          message: `Sensor ${sensor.sensor} is offline or has communication trouble`
        });
      }
      
      if (sensor.highTempAlarm) {
        alerts.push({
          sensor: sensor.sensor,
          type: 'high_temperature',
          message: `Sensor ${sensor.sensor} high temperature alarm active`
        });
      }
      
      if (sensor.lowTempAlarm) {
        alerts.push({
          sensor: sensor.sensor,
          type: 'low_temperature',
          message: `Sensor ${sensor.sensor} low temperature alarm active`
        });
      }
    });
    
    // Trigger callback if alerts found
    if (alerts.length > 0 && alertCallback) {
      alertCallback(alerts, sensorStatus);
    }
    
    return {
      ...sensorStatus,
      alerts: alerts,
      alertCount: alerts.length,
      hasAlerts: alerts.length > 0
    };
    
  } catch (error) {
    console.error('Temperature monitoring failed:', error);
    throw error;
  }
}

function parseSensorValues(sensorData) {
  const sensors = [];
  
  for (let i = 0; i < sensorData.length; i++) {
    const sensorValue = parseInt(sensorData[i], 16);
    sensors.push({
      sensor: i + 1,
      enabled: !!(sensorValue & 0x01),
      online: !!(sensorValue & 0x02),
      highTempAlarm: !!(sensorValue & 0x04),
      lowTempAlarm: !!(sensorValue & 0x08),
      raw: sensorValue,
      hexValue: sensorData[i],
      status: getSensorStatusDescription(sensorValue)
    });
  }
  
  return sensors;
}

function getSensorStatusDescription(sensorValue) {
  const descriptions = {
    0x0: 'Disabled/Not Available',
    0x1: 'Enabled - Normal',
    0x2: 'Enabled - Offline/Trouble', 
    0x3: 'Enabled - Online/Normal',
    0x5: 'Enabled - High Temp Alarm',
    0x7: 'Enabled - Online + High Temp Alarm',
    0x9: 'Enabled - Low Temp Alarm',
    0xB: 'Enabled - Online + Low Temp Alarm',
    0xD: 'Enabled - Both Temp Alarms',
    0xF: 'Enabled - Online + Both Temp Alarms'
  };
  
  return descriptions[sensorValue] || `Unknown state: 0x${sensorValue.toString(16).toUpperCase()}`;
}

function analyzeSensorHealth(sensors) {
  const enabled = sensors.filter(s => s.enabled);
  const online = sensors.filter(s => s.enabled && s.online);
  const withAlarms = sensors.filter(s => s.enabled && (s.highTempAlarm || s.lowTempAlarm));
  const offline = sensors.filter(s => s.enabled && !s.online);
  
  return {
    total: sensors.length,
    enabled: enabled.length,
    online: online.length,
    offline: offline.length,
    withAlarms: withAlarms.length,
    healthScore: enabled.length > 0 ? Math.round((online.length / enabled.length) * 100) : 0,
    status: determineOverallSensorHealth(enabled.length, online.length, withAlarms.length)
  };
}

function determineOverallSensorHealth(enabled, online, withAlarms) {
  if (enabled === 0) return 'no_sensors';
  if (withAlarms > 0) return 'alarms_active';
  if (online === 0) return 'all_offline';
  if (online < enabled) return 'some_offline';
  return 'healthy';
}

// Usage examples
async function demonstrateSensorMonitoring(partialUid) {
  try {
    // Basic sensor value retrieval
    console.log('=== Basic Sensor Values ===');
    const sensors = await getSensorValues(partialUid);
    console.log('Sensor values:', sensors);
    
    // Full sensor status with capabilities
    console.log('\n=== Full Sensor Status ===');
    const fullStatus = await getFullSensorStatus(partialUid);
    console.log('Full sensor status:', fullStatus);
    
    // Temperature monitoring with alerts
    console.log('\n=== Temperature Monitoring ===');
    const monitoring = await monitorTemperatureSensors(partialUid, (alerts, status) => {
      console.log('🚨 Temperature Alerts:', alerts);
      alerts.forEach(alert => {
        console.log(`  - ${alert.type.toUpperCase()}: ${alert.message}`);
      });
    });
    
    console.log('Monitoring result:', monitoring);
    
  } catch (error) {
    console.error('Sensor monitoring demonstration failed:', error);
  }
}

// Scheduled sensor monitoring
async function startSensorMonitoring(partialUid, intervalMinutes = 10) {
  console.log(`Starting sensor monitoring for device ${partialUid} (every ${intervalMinutes} minutes)`);
  
  const monitor = setInterval(async () => {
    try {
      const result = await monitorTemperatureSensors(partialUid, (alerts) => {
        const timestamp = new Date().toISOString();
        console.log(`[${timestamp}] 🚨 Temperature alerts detected:`, alerts.length);
        alerts.forEach(alert => console.log(`  ${alert.message}`));
      });
      
      if (!result.hasAlerts) {
        console.log(`[${new Date().toISOString()}] Temperature monitoring: All sensors normal`);
      }
      
    } catch (error) {
      console.error('Sensor monitoring check failed:', error);
    }
  }, intervalMinutes * 60 * 1000);
  
  return monitor; // Return interval ID for cleanup
}

// Compare sensors with system status
async function compareSensorsWithSystemStatus(partialUid) {
  try {
    // Get dedicated sensor values
    const sensorValues = await getSensorValues(partialUid);
    
    // Get system status for comparison
    const systemStatus = await getSystemStatus(partialUid);
    
    // Compare sensor data
    const comparison = {
      dedicatedCommand: sensorValues,
      systemStatus: systemStatus.temperatureSensors,
      match: JSON.stringify(sensorValues) === JSON.stringify(systemStatus.temperatureSensors),
      differences: []
    };
    
    // Find differences if any
    if (!comparison.match && sensorValues.length === systemStatus.temperatureSensors.length) {
      for (let i = 0; i < sensorValues.length; i++) {
        const dedicated = sensorValues[i];
        const system = systemStatus.temperatureSensors[i];
        
        if (dedicated.raw !== system.raw) {
          comparison.differences.push({
            sensor: i + 1,
            dedicated: dedicated.hexValue,
            system: system.hexValue || 'N/A'
          });
        }
      }
    }
    
    return comparison;
    
  } catch (error) {
    console.error('Sensor comparison failed:', error);
    throw error;
  }
}

0x0009 - System ARM/DISARM/STAY/SLEEP

Controls the arming and disarming states of security system areas/partitions. Supports ARM, DISARM, STAY, and SLEEP modes with user authentication.

Request/Response format

Request

Without User Authentication:

[HEADER datatype=1][0x0009][P:S] → 070100000000000000900[P:S]

With User Authentication:

[HEADER datatype=3][UserCode#][0x0009][P:S] → 070300000000000000[UserCode#]0900[P:S]

Where:

  • P = Area/Partition number (1-based)
  • S = System state: 0 = DISARM, 1 = ARM, 2 = SLEEP, 3 = STAY
  • UserCode = Authentication code (numeric PIN only)

Examples:

  • ARM area 1 with user 1234: 0703000000000000001234#0900313a31
  • DISARM area 2 without user: 070100000000000000900323a30
  • STAY mode area 1 with user: 0703000000000000001234#0900313a33

Response Format

Legacy Response (system state update):

[HEADER][0x0002][SystemState]

Modern Response (area states):

[HEADER][0x0009][SYS:xxxxxxxxxxxxxxxx]

Additional Status Response:

[HEADER][0x000D][DATA ASCII]

Error Response:

[HEADER][0x0009][ERROR:reason]
[HEADER][0x0009][NACK]
[HEADER][0x0009][FORMAT]

Note: Modern devices may send both a SYS: response and a full status update (0x000D). The SYS: format is identical to the partition states in 0x000D Status responses.

Command Parameters

ParameterFormatDescription
PDecimal numberArea/Partition number (1-based, device-specific range)
SSingle digitSystem state code

System States

State CodeState NameDescription
0DISARMSystem disarmed, all zones inactive
1ARMFull arm mode, all zones active
2SLEEPSleep mode, perimeter zones active
3STAYStay mode, entry/exit delays disabled

Area Configuration

Area Count: Use the 0x0001 - Capabilities command to determine the maximum number of areas (SYS:x field) supported by the device.

Area Indexing: Areas/partitions are indexed starting from 1:

  • Area 1 = First partition
  • Area 2 = Second partition
  • And so on...

Authentication Requirements

User Codes: Some operations may require user authentication:

  • Numeric PIN: 4-6 digit codes (e.g., 1234#)
  • No Authentication: Some devices allow operation without user codes

Authentication Errors: Invalid or missing user codes may result in:

  • [ERROR:WRONG_CODE] - Invalid user code
  • [ERROR:CONTROL_AREA] - No permission for specific area
  • [ERROR:NACK] - Authentication required but not provided

Response Data Format

Modern Response (SYS: format):

SYS:xxxxxxxxxxxxxxxx

Where each area uses 2 hex characters representing partition states, identical to the format used in 0x0002 Status responses:

Hex ValueState NameDescription
00st_ARMArmed
01st_ARM_startingArming in progress
02st_ARM_waitingWaiting to arm
03st_PrepARMPreparing to arm
04st_DisARMDisarmed
05st_DisARM_waitingWaiting to disarm
06st_PrepDisARMPreparing to disarm
07st_PrepDisARMRemoteRemote disarm/rearm function
08st_Remote_DisARM_waitingRemote disarm waiting
09st_STAY_DELAYStay delay
0Ast_STAY_DELAY_startingStay delay starting
0Bst_STAY_Delay_waitingStay delay waiting
0Cst_STAY_Delay_waiting_prepareStay delay waiting prepare
0Dst_SLEEPSleep mode
0Est_SLEEP_startingSleep starting
0Fst_SLEEP_waiting_prepareSleep waiting prepare
10st_SLEEP_waitingSleep waiting
11st_CancelCancel

Safety Considerations

⚠️ Important Warnings:

  • User Authentication: Verify user codes have appropriate permissions
  • Area Dependencies: Some areas may be linked and affect each other
  • Entry/Exit Delays: ARM and DISARM may have programmed delays
  • Zone Bypass: Check bypassed zones before arming
  • System Faults: Address any system faults before arming
Example Response

Example Responses

ARM area 1 with user code - Success:

Request: 0703000000000000001234#0900313a31
Response 1: [HEADER][0x0009][SYS:0004040404040404]
Response 2: [HEADER][0x000D][#F:1,DAT:01,NR,1100000010,10103,0004040404040404,15,3331110000,2024/10/21 14:30:00]

DISARM area 2 without user - Success:

Request: 070100000000000000900323a30
Response: [HEADER][0x0009][SYS:0404040404040404]

STAY mode area 1 - Success:

Request: 0703000000000000001234#0900313a33
Response: [HEADER][0x0009][SYS:0904040404040404]

Invalid user code - Error:

Request: 0703000000000000009999#0900313a31
Response: [HEADER][0x0009][ERROR:WRONG_CODE]

Area not supported - Error:

Request: 070100000000000000900393a31
Response: [HEADER][0x0009][ERROR:CONTROL_AREA]

Response Breakdown:

  • SYS:0004040404040404 - Area 1 armed (00), others disarmed (04)
  • SYS:0404040404040404 - All areas disarmed (04)
  • SYS:0904040404040404 - Area 1 in STAY delay (09), others disarmed
  • Multiple responses indicate both immediate state and full system status
Usage Example
javascript
async function setSystemState(partialUid, area, state, userCode = null) {
  // Build system control command
  let header, command;
  
  if (userCode) {
    // With user authentication
    header = "070300000000000000";  // Header with user ID flag
    command = `${userCode}#0900`;   // User code + command
  } else {
    // Without user authentication
    header = "070100000000000000";  // Standard header
    command = "0900";               // Command only
  }
  
  // Build command data: P:S
  const commandData = `${area}:${state}`;
  const fullCommand = header + command + asciiToHex(commandData);
  
  try {
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      // Parse response (could be SYS: format or error)
      const responseText = hexToAscii(result.data[0]);
      const systemResponse = parseSystemStateResponse(responseText);
      
      console.log(`Area ${area} state change to ${getStateName(state)} completed`);
      console.log('System response:', systemResponse);
      return systemResponse;
    }
    
    throw new Error('No response data received');
  } catch (error) {
    console.error(`Failed to set area ${area} to state ${state}:`, error);
    throw error;
  }
}

// Convenience functions for common operations
async function armSystem(partialUid, area, userCode = null) {
  console.log(`Arming area ${area}${userCode ? ` with user ${userCode}` : ''}...`);
  return await setSystemState(partialUid, area, 1, userCode);
}

async function disarmSystem(partialUid, area, userCode = null) {
  console.log(`Disarming area ${area}${userCode ? ` with user ${userCode}` : ''}...`);
  return await setSystemState(partialUid, area, 0, userCode);
}

async function setStayMode(partialUid, area, userCode = null) {
  console.log(`Setting area ${area} to STAY mode${userCode ? ` with user ${userCode}` : ''}...`);
  return await setSystemState(partialUid, area, 3, userCode);
}

async function setSleepMode(partialUid, area, userCode = null) {
  console.log(`Setting area ${area} to SLEEP mode${userCode ? ` with user ${userCode}` : ''}...`);
  return await setSystemState(partialUid, area, 2, userCode);
}

// Multi-area operations
async function setMultipleAreas(partialUid, areaCommands, userCode = null) {
  const results = [];
  
  for (const cmd of areaCommands) {
    try {
      const result = await setSystemState(partialUid, cmd.area, cmd.state, userCode);
      results.push({
        area: cmd.area,
        state: cmd.state,
        stateName: getStateName(cmd.state),
        success: true,
        result: result
      });
      
      // Small delay between commands
      await new Promise(resolve => setTimeout(resolve, 500));
    } catch (error) {
      results.push({
        area: cmd.area,
        state: cmd.state,
        stateName: getStateName(cmd.state),
        success: false,
        error: error.message
      });
    }
  }
  
  return results;
}

// System state verification
async function verifySystemStates(partialUid, expectedStates) {
  try {
    // Get current system status
    const systemStatus = await getSystemStatus(partialUid);
    const currentStates = systemStatus.partitions;
    
    const verification = {
      expected: expectedStates,
      current: currentStates,
      matches: [],
      mismatches: []
    };
    
    expectedStates.forEach(expected => {
      const current = currentStates.find(p => p.partition === expected.area);
      
      if (current) {
        const match = {
          area: expected.area,
          expectedState: expected.state,
          expectedStateName: getStateName(expected.state),
          currentState: current.state,
          currentStateName: current.stateDescription,
          matches: isStateMatch(expected.state, current.state)
        };
        
        if (match.matches) {
          verification.matches.push(match);
        } else {
          verification.mismatches.push(match);
        }
      }
    });
    
    return verification;
    
  } catch (error) {
    console.error('System state verification failed:', error);
    throw error;
  }
}

// Enhanced security operations with validation
async function secureSystemOperation(partialUid, operation, options = {}) {
  const {
    area,
    state,
    userCode,
    validateUser = true,
    checkFaults = true,
    verifyResult = true,
    timeout = 30000
  } = options;
  
  try {
    console.log(`Starting secure ${operation} operation for area ${area}`);
    
    // Pre-operation checks
    if (checkFaults) {
      console.log('Checking system faults...');
      const systemStatus = await getSystemStatus(partialUid);
      
      if (systemStatus.troubleBits.fire || systemStatus.troubleBits.zoneTamper) {
        throw new Error('System faults detected - operation aborted for safety');
      }
    }
    
    // User validation (if required)
    if (validateUser && userCode) {
      console.log('Validating user permissions...');
      // This would typically involve checking user permissions
      // For now, we'll proceed with the provided user code
    }
    
    // Execute the operation
    console.log(`Executing ${operation}...`);
    const startTime = Date.now();
    const result = await setSystemState(partialUid, area, state, userCode);
    
    // Verify the result if requested
    if (verifyResult) {
      console.log('Verifying operation result...');
      
      // Wait for state transition to complete
      let verified = false;
      const verifyStartTime = Date.now();
      
      while (!verified && (Date.now() - verifyStartTime) < timeout) {
        await new Promise(resolve => setTimeout(resolve, 2000));
        
        const verification = await verifySystemStates(partialUid, [{ area, state }]);
        
        if (verification.matches.length > 0) {
          verified = true;
          console.log('Operation verified successfully');
        } else if (verification.mismatches.length > 0) {
          const mismatch = verification.mismatches[0];
          console.log(`Still transitioning: ${mismatch.currentStateName} (expected: ${mismatch.expectedStateName})`);
        }
      }
      
      if (!verified) {
        throw new Error(`Operation verification failed - timeout after ${timeout}ms`);
      }
    }
    
    const operationTime = Date.now() - startTime;
    
    return {
      success: true,
      operation: operation,
      area: area,
      state: state,
      stateName: getStateName(state),
      userCode: userCode ? '***' : null, // Mask user code in response
      executionTime: operationTime,
      verified: verifyResult,
      result: result
    };
    
  } catch (error) {
    console.error(`Secure ${operation} operation failed:`, error);
    throw error;
  }
}

function parseSystemStateResponse(responseText) {
  // Handle different response formats
  if (responseText.startsWith('SYS:')) {
    // Parse SYS: format
    const sysData = responseText.substring(4);
    const partitions = [];
    
    for (let i = 0; i < sysData.length; i += 2) {
      const partitionValue = sysData.substr(i, 2);
      partitions.push({
        partition: (i / 2) + 1,
        state: parseInt(partitionValue, 16),
        stateDescription: getPartitionStateDescription(parseInt(partitionValue, 16)),
        raw: partitionValue
      });
    }
    
    return {
      type: 'system_states',
      partitions: partitions
    };
  } else if (responseText.startsWith('ERROR:')) {
    // Parse error response
    const errorReason = responseText.substring(6);
    return {
      type: 'error',
      error: errorReason,
      description: getErrorDescription(errorReason)
    };
  } else {
    // Unknown format
    return {
      type: 'unknown',
      raw: responseText
    };
  }
}

function getStateName(stateCode) {
  const stateNames = {
    0: 'DISARM',
    1: 'ARM',
    2: 'SLEEP',
    3: 'STAY'
  };
  return stateNames[stateCode] || `Unknown(${stateCode})`;
}

function getPartitionStateDescription(state) {
  const states = {
    0x00: 'st_ARM',
    0x01: 'st_ARM_starting',
    0x02: 'st_ARM_waiting',
    0x03: 'st_PrepARM',
    0x04: 'st_DisARM',
    0x05: 'st_DisARM_waiting',
    0x06: 'st_PrepDisARM',
    0x07: 'st_PrepDisARMRemote',
    0x08: 'st_Remote_DisARM_waiting',
    0x09: 'st_STAY_DELAY',
    0x0A: 'st_STAY_DELAY_starting',
    0x0B: 'st_STAY_Delay_waiting',
    0x0C: 'st_STAY_Delay_waiting_prepare',
    0x0D: 'st_SLEEP',
    0x0E: 'st_SLEEP_starting',
    0x0F: 'st_SLEEP_waiting_prepare',
    0x10: 'st_SLEEP_waiting',
    0x11: 'st_Cancel'
  };
  return states[state] || `Unknown state: 0x${state.toString(16).toUpperCase()}`;
}

function getErrorDescription(errorReason) {
  const errors = {
    'WRONG_CODE': 'Invalid user code provided',
    'CONTROL_AREA': 'No permission to control this area',
    'FAULT': 'System fault prevents operation',
    'NOSLEEP': 'Area does not support SLEEP mode',
    'NACK': 'Command parameters incorrect or area not available'
  };
  return errors[errorReason] || `Unknown error: ${errorReason}`;
}

function isStateMatch(requestedState, currentPartitionState) {
  // Map requested states to expected partition states
  const stateMapping = {
    0: [0x04, 0x05, 0x06], // DISARM states
    1: [0x00, 0x01, 0x02, 0x03], // ARM states  
    2: [0x0D, 0x0E, 0x0F, 0x10], // SLEEP states
    3: [0x09, 0x0A, 0x0B, 0x0C]  // STAY states
  };
  
  const expectedStates = stateMapping[requestedState] || [];
  return expectedStates.includes(currentPartitionState);
}

function asciiToHex(ascii) {
  let hex = '';
  for (let i = 0; i < ascii.length; i++) {
    hex += ascii.charCodeAt(i).toString(16).padStart(2, '0');
  }
  return hex;
}

function hexToAscii(hex) {
  let ascii = '';
  for (let i = 0; i < hex.length; i += 2) {
    ascii += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
  }
  return ascii;
}

// Usage examples
async function demonstrateSystemControl(partialUid) {
  try {
    const userCode = '1234'; // Example user code
    
    // Basic operations
    console.log('=== Basic System Control ===');
    
    // ARM area 1
    console.log('Arming area 1...');
    const armResult = await armSystem(partialUid, 1, userCode);
    console.log('ARM result:', armResult);
    
    await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds
    
    // Set STAY mode
    console.log('Setting STAY mode...');
    const stayResult = await setStayMode(partialUid, 1, userCode);
    console.log('STAY result:', stayResult);
    
    await new Promise(resolve => setTimeout(resolve, 5000));
    
    // DISARM
    console.log('Disarming system...');
    const disarmResult = await disarmSystem(partialUid, 1, userCode);
    console.log('DISARM result:', disarmResult);
    
    // Multi-area operations
    console.log('\n=== Multi-Area Operations ===');
    const multiCommands = [
      { area: 1, state: 1 }, // ARM area 1
      { area: 2, state: 3 }, // STAY area 2
    ];
    
    const multiResults = await setMultipleAreas(partialUid, multiCommands, userCode);
    console.log('Multi-area results:', multiResults);
    
    // Secure operations with validation
    console.log('\n=== Secure Operations ===');
    const secureResult = await secureSystemOperation(partialUid, 'ARM', {
      area: 1,
      state: 1,
      userCode: userCode,
      validateUser: true,
      checkFaults: true,
      verifyResult: true
    });
    console.log('Secure operation result:', secureResult);
    
  } catch (error) {
    console.error('System control demonstration failed:', error);
  }
}

0x000A - Set Output Pulse

Controls PGM outputs with timed pulse functionality, allowing outputs to be turned on or off for a specified duration. Supports single or multiple output control in one command with individual timing for each output.

Request/Response format

Request

[HEADER][0x000A][PGM:S:TIME] → 070100000000000000A00[PGM:S:TIME]

Where:

  • PGM = Index of PGM output (decimal format)
  • S = Output state: 0 = turn OFF, 1 = turn ON
  • TIME = Pulse length in seconds (decimal, 0-999999, variable length)

Single Output Examples:

  • Turn on PGM 1 for 10 seconds: 070100000000000000A00313a313a3130
  • Turn off PGM 2 for 5 seconds: 070100000000000000A00323a303a35
  • Turn on PGM 4 for 1589 seconds: 070100000000000000A00343a313a31353839

Multiple Output Examples:

  • Multiple outputs: 070100000000000000A00313a313a31302c323a313a353238392c343a303a31353839
    • PGM 1: ON for 10 seconds
    • PGM 2: ON for 5289 seconds
    • PGM 4: OFF for 1589 seconds

Response Format

System State Response:

[HEADER][0x0002][O:SystemState]

Output State Response:

[HEADER][0x000A][O:xxxxx]

Error Response:

[HEADER][0x000A][NACK]
[HEADER][0x000A][FORMAT] 
[HEADER][0x000A][ERROR]
[HEADER][0x000A][O:xxxxx],[E:xx]

Note: The output state format is similar to the O: field in 0x000D Status responses. Error responses may include both unchanged state and error code.

Command Parameters

ParameterFormatDescription
PGMDecimal numberPGM output index (1-based, device-specific range)
S0 or 1Output state: 0 = OFF, 1 = ON
TIMEDecimal numberPulse duration in seconds (0-999999)

Pulse Duration

Time Range: 0 to 999999 seconds (approximately 11.5 days maximum)

  • 0 = Instantaneous pulse (minimal duration)
  • 1-999999 = Pulse duration in seconds
  • Variable length format (no leading zeros required)

Pulse Behavior:

  • ON Pulse: Output turns ON for specified duration, then automatically turns OFF
  • OFF Pulse: Output turns OFF for specified duration, then automatically turns ON
  • Timing Accuracy: Depends on device timing resolution (typically ±1 second)

Multiple Output Support

Multi-Output Format:

PGM1:S1:TIME1,PGM2:S2:TIME2,PGM3:S3:TIME3

Capabilities:

  • Some devices support multiple outputs in one command
  • Each output can have different state and timing
  • Comma-separated format for multiple outputs
  • Individual error handling per output

Output Configuration

Output Count: Use the 0x0001 - Capabilities command to determine the maximum number of outputs (O:x field) supported by the device.

Output Indexing: Outputs are indexed starting from 1:

  • Output 1 = First PGM output
  • Output 2 = Second PGM output
  • And so on...

Response Data Format

Output State Response:

O:xxxxx

Where xxxxx represents the current state of all PGM outputs, using the same bit encoding as 0x0003 Set Output Level:

  • Bit 0: 1 = Remote control enabled, 0 = Remote control disabled
  • Bit 1: 1 = Output state ON, 0 = Output state OFF
  • Bit 2: 1 = Reset fire sensor
  • Bit 3: Reserved

Error Response with State:

O:xxxxx,E:xx

Where E:xx represents an error code while O:xxxxx shows the unchanged output states.

Event Notifications

Some devices can send event messages in Sur-Gard MLR2-DG multiline receiver Ademco Contact ID format when output states change:

Event Format:

<E|R> 780 <Area> <Zone>

Event Fields:

  • E = PGM activated event (output turned ON)
  • R = PGM deactivated event (output turned OFF)
  • 780 = Event code for PGM control
  • Area = PGM number (decimal)
  • Zone = User ID (decimal)

Use Cases

ScenarioDescription
Timed Access ControlDoor locks, gate controls with automatic timeout
Alert SystemsSirens, beacons with controlled duration
Equipment ControlMotors, pumps with timed operation cycles
Notification SystemsIndicator lights with pulse timing
Example Response

Example Responses

Single output pulse - Success:

Request: 070100000000000000A00313a313a3130
Response 1: [HEADER][0x0002][O:SystemState]
Response 2: [HEADER][0x000A][O:30103]

Multiple output pulse - Success:

Request: 070100000000000000A00313a313a31302c323a313a353238392c343a303a31353839
Response: [HEADER][0x000A][O:33103]

Invalid PGM index - Error:

Request: 070100000000000000A00393a313a3130
Response: [HEADER][0x000A][NACK]

Invalid time value - Error:

Request: 070100000000000000A00313a313a31303030303030
Response: [HEADER][0x000A][FORMAT]

Partial success with error:

Request: 070100000000000000A00313a313a31302c393a313a35
Response: [HEADER][0x000A][O:30103,E:22]

Event notification:

Request: 070100000000000000A00313a313a3130
Response: [HEADER][0x000A][O:30103]
Event: E 780 1 1234
// 10 seconds later
Event: R 780 1 1234

Response Breakdown:

  • O:30103 - PGM 1 ON+remote (3), others same as before
  • O:33103 - PGM 1&2 ON+remote (3), PGM 4 OFF (1 becomes 1)
  • E:22 - Error code 22 (device-specific error)
  • Events show activation and automatic deactivation after pulse time
Usage Example
javascript
async function setOutputPulse(partialUid, pgmIndex, state, pulseTimeSeconds) {
  // Build pulse output command
  const header = "070100000000000000";  // Standard header
  const command = "A00";                 // Command 0x000A in MSB format
  
  // Build command data: PGM:S:TIME
  const commandData = `${pgmIndex}:${state}:${pulseTimeSeconds}`;
  const fullCommand = header + command + asciiToHex(commandData);
  
  try {
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      // Parse responses (may be multiple)
      const responses = result.data.map(data => {
        const responseText = hexToAscii(data);
        return parseOutputPulseResponse(responseText);
      });
      
      console.log(`PGM ${pgmIndex} pulse ${state ? 'ON' : 'OFF'} for ${pulseTimeSeconds}s completed`);
      console.log('Pulse responses:', responses);
      return responses;
    }
    
    throw new Error('No response data received');
  } catch (error) {
    console.error(`Failed to set PGM ${pgmIndex} pulse:`, error);
    throw error;
  }
}

// Multiple output pulse control
async function setMultipleOutputPulses(partialUid, outputCommands) {
  // Build multiple output command
  const header = "070100000000000000";  // Standard header
  const command = "A00";                 // Command 0x000A in MSB format
  
  // Build command data: PGM1:S1:TIME1,PGM2:S2:TIME2,...
  const commandParts = outputCommands.map(cmd => 
    `${cmd.pgm}:${cmd.state}:${cmd.time}`
  );
  const commandData = commandParts.join(',');
  const fullCommand = header + command + asciiToHex(commandData);
  
  try {
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responses = result.data.map(data => {
        const responseText = hexToAscii(data);
        return parseOutputPulseResponse(responseText);
      });
      
      console.log('Multiple output pulse operation completed');
      console.log('Commands:', outputCommands);
      console.log('Responses:', responses);
      return responses;
    }
    
    throw new Error('No response data received');
  } catch (error) {
    console.error('Failed to set multiple output pulses:', error);
    throw error;
  }
}

// Convenience functions for common operations
async function pulseOutputOn(partialUid, pgmIndex, seconds) {
  console.log(`Pulsing PGM ${pgmIndex} ON for ${seconds} seconds...`);
  return await setOutputPulse(partialUid, pgmIndex, 1, seconds);
}

async function pulseOutputOff(partialUid, pgmIndex, seconds) {
  console.log(`Pulsing PGM ${pgmIndex} OFF for ${seconds} seconds...`);
  return await setOutputPulse(partialUid, pgmIndex, 0, seconds);
}

// Common pulse durations
async function shortPulse(partialUid, pgmIndex, state = 1) {
  return await setOutputPulse(partialUid, pgmIndex, state, 5); // 5 seconds
}

async function mediumPulse(partialUid, pgmIndex, state = 1) {
  return await setOutputPulse(partialUid, pgmIndex, state, 30); // 30 seconds
}

async function longPulse(partialUid, pgmIndex, state = 1) {
  return await setOutputPulse(partialUid, pgmIndex, state, 300); // 5 minutes
}

// Advanced pulse control with monitoring
async function monitoredPulse(partialUid, pgmIndex, state, pulseTimeSeconds) {
  try {
    console.log(`Starting monitored pulse: PGM ${pgmIndex}, state ${state}, duration ${pulseTimeSeconds}s`);
    
    // Get initial output state
    const initialStatus = await getSystemStatus(partialUid);
    const initialOutput = initialStatus.outputs.find(o => o.output === pgmIndex);
    
    console.log(`Initial PGM ${pgmIndex} state:`, initialOutput ? initialOutput.state : 'unknown');
    
    // Execute pulse command
    const pulseStart = Date.now();
    const pulseResult = await setOutputPulse(partialUid, pgmIndex, state, pulseTimeSeconds);
    
    // Monitor pulse completion (check periodically)
    const checkInterval = Math.min(pulseTimeSeconds * 1000 / 4, 5000); // Check 4 times or every 5s max
    let pulseCompleted = false;
    
    const monitor = setInterval(async () => {
      try {
        const currentStatus = await getSystemStatus(partialUid);
        const currentOutput = currentStatus.outputs.find(o => o.output === pgmIndex);
        
        if (currentOutput) {
          const expectedState = state === 1;
          const currentState = currentOutput.state;
          
          console.log(`Pulse monitor: PGM ${pgmIndex} is ${currentState ? 'ON' : 'OFF'}`);
          
          // Check if pulse has completed (state returned to opposite)
          const elapsed = Date.now() - pulseStart;
          if (elapsed >= pulseTimeSeconds * 1000 && currentState !== expectedState) {
            console.log(`Pulse completed after ${Math.round(elapsed / 1000)}s`);
            pulseCompleted = true;
            clearInterval(monitor);
          }
        }
      } catch (error) {
        console.error('Pulse monitoring error:', error);
      }
    }, checkInterval);
    
    // Stop monitoring after pulse time + buffer
    setTimeout(() => {
      if (!pulseCompleted) {
        console.log('Pulse monitoring timeout - stopping monitor');
        clearInterval(monitor);
      }
    }, (pulseTimeSeconds + 10) * 1000);
    
    return {
      success: true,
      pgmIndex: pgmIndex,
      state: state,
      pulseTimeSeconds: pulseTimeSeconds,
      startTime: new Date(pulseStart),
      result: pulseResult,
      monitored: true
    };
    
  } catch (error) {
    console.error('Monitored pulse failed:', error);
    throw error;
  }
}

// Pulse sequence operations
async function pulseSequence(partialUid, sequence, delayBetween = 1000) {
  const results = [];
  
  for (let i = 0; i < sequence.length; i++) {
    const pulse = sequence[i];
    
    try {
      console.log(`Sequence ${i + 1}/${sequence.length}: PGM ${pulse.pgm} ${pulse.state ? 'ON' : 'OFF'} for ${pulse.time}s`);
      
      const result = await setOutputPulse(partialUid, pulse.pgm, pulse.state, pulse.time);
      results.push({
        step: i + 1,
        pulse: pulse,
        success: true,
        result: result
      });
      
      // Wait before next pulse (if not last)
      if (i < sequence.length - 1 && delayBetween > 0) {
        console.log(`Waiting ${delayBetween}ms before next pulse...`);
        await new Promise(resolve => setTimeout(resolve, delayBetween));
      }
      
    } catch (error) {
      results.push({
        step: i + 1,
        pulse: pulse,
        success: false,
        error: error.message
      });
      
      console.error(`Sequence step ${i + 1} failed:`, error);
      // Continue with remaining pulses
    }
  }
  
  return {
    sequence: sequence,
    results: results,
    successCount: results.filter(r => r.success).length,
    totalSteps: sequence.length
  };
}

function parseOutputPulseResponse(responseText) {
  // Handle different response formats
  if (responseText.startsWith('O:')) {
    // Check for error code in response
    const parts = responseText.split(',');
    const outputData = parts[0].substring(2); // Remove "O:"
    
    const response = {
      type: 'output_state',
      outputData: outputData,
      outputs: parseOutputStates(outputData)
    };
    
    // Check for error code
    if (parts.length > 1 && parts[1].startsWith('E:')) {
      response.error = {
        code: parts[1].substring(2),
        description: getOutputErrorDescription(parts[1].substring(2))
      };
    }
    
    return response;
  } else if (responseText === 'NACK' || responseText === 'FORMAT' || responseText === 'ERROR') {
    return {
      type: 'error',
      error: responseText,
      description: getOutputErrorDescription(responseText)
    };
  } else {
    return {
      type: 'unknown',
      raw: responseText
    };
  }
}

function parseOutputStates(outputData) {
  const outputs = [];
  for (let i = 0; i < outputData.length; i++) {
    const outputValue = parseInt(outputData[i], 16);
    outputs.push({
      output: i + 1,
      remoteControlEnabled: !!(outputValue & 0x01),
      state: !!(outputValue & 0x02),
      resetFireSensor: !!(outputValue & 0x04),
      raw: outputValue,
      hexValue: outputData[i]
    });
  }
  return outputs;
}

function getOutputErrorDescription(errorCode) {
  const errors = {
    'NACK': 'Command not acknowledged - invalid PGM index or device not ready',
    'FORMAT': 'Invalid command format - check PGM:S:TIME syntax',
    'ERROR': 'General command error',
    '22': 'PGM output error or timing conflict',
    // Add more device-specific error codes as needed
  };
  return errors[errorCode] || `Unknown error code: ${errorCode}`;
}

function asciiToHex(ascii) {
  let hex = '';
  for (let i = 0; i < ascii.length; i++) {
    hex += ascii.charCodeAt(i).toString(16).padStart(2, '0');
  }
  return hex;
}

function hexToAscii(hex) {
  let ascii = '';
  for (let i = 0; i < hex.length; i += 2) {
    ascii += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
  }
  return ascii;
}

// Usage examples
async function demonstrateOutputPulseControl(partialUid) {
  try {
    // Basic pulse operations
    console.log('=== Basic Output Pulse Control ===');
    
    // Short pulse on PGM 1
    const shortResult = await shortPulse(partialUid, 1);
    console.log('Short pulse result:', shortResult);
    
    await new Promise(resolve => setTimeout(resolve, 2000));
    
    // Medium pulse off PGM 2
    const mediumResult = await pulseOutputOff(partialUid, 2, 30);
    console.log('Medium pulse result:', mediumResult);
    
    // Multiple output pulses
    console.log('\n=== Multiple Output Pulses ===');
    const multiCommands = [
      { pgm: 1, state: 1, time: 10 },   // PGM 1 ON for 10s
      { pgm: 2, state: 1, time: 5289 }, // PGM 2 ON for 5289s
      { pgm: 4, state: 0, time: 1589 }  // PGM 4 OFF for 1589s
    ];
    
    const multiResult = await setMultipleOutputPulses(partialUid, multiCommands);
    console.log('Multiple pulse result:', multiResult);
    
    // Monitored pulse
    console.log('\n=== Monitored Pulse ===');
    const monitoredResult = await monitoredPulse(partialUid, 1, 1, 15);
    console.log('Monitored pulse result:', monitoredResult);
    
    // Pulse sequence
    console.log('\n=== Pulse Sequence ===');
    const sequence = [
      { pgm: 1, state: 1, time: 5 },
      { pgm: 2, state: 1, time: 5 },
      { pgm: 3, state: 1, time: 5 }
    ];
    
    const sequenceResult = await pulseSequence(partialUid, sequence, 2000);
    console.log('Pulse sequence result:', sequenceResult);
    
  } catch (error) {
    console.error('Output pulse control demonstration failed:', error);
  }
}

0x000B - Set Device TIME Silently

Sets the internal clock/time on the device without triggering system events or notifications. This command provides a "silent" alternative to 0x0004 - Set Device Time that updates the clock without disturbing normal operations.

Request/Response format

Request

[HEADER][0x000B][YYYY/MM/DD HH:mm:ss] → 070100000000000000B00[YYYY/MM/DD HH:mm:ss]

Where:

  • YYYY/MM/DD HH:mm:ss = Date and time in ASCII format
  • Date format: YYYY/MM/DD (4-digit year, 2-digit month, 2-digit day)
  • Time format: HH:mm:ss (24-hour format with colons)

Examples:

  • Set to Dec 25, 2023 3:30:45 PM: 070100000000000000B00323032332f31322f32352031353a33303a3435
  • Set to Jan 1, 2024 midnight: 070100000000000000B00323032342f30312f30312030303a30303a3030

Response Format

System State Response (legacy):

[HEADER][0x0002][SystemState]

Time Confirmation Response:

[HEADER][0x000B][TM:YYYY/MM/DD HH-mm-ss]

Error Response:

[HEADER][0x000B][FORMAT]
[HEADER][0x000B][NACK]
[HEADER][0x000B][ERROR]

Note: The device may send both a system state update (0x0002) and a time confirmation response. The TM: format is similar to the timestamp field in 0x000D Status responses.

Command Parameters

ParameterFormatRequiredDescription
YYYY4 digitsYesYear (e.g., 2024)
MM2 digitsYesMonth (01-12)
DD2 digitsYesDay (01-31)
HH2 digitsYesHour (00-23, 24-hour format)
mm2 digitsYesMinutes (00-59)
ss2 digitsYesSeconds (00-59)

Time Format Requirements

Request Format:

  • Date separator: / (forward slash)
  • Date/Time separator: (space)
  • Time separator: : (colon)
  • Format: YYYY/MM/DD HH:mm:ss

Response Format:

  • Date separator: / (forward slash)
  • Date/Time separator: (space)
  • Time separator: - (hyphen)
  • Format: TM:YYYY/MM/DD HH-mm-ss

Silent Operation

Key Differences from 0x0004:

  • No System Events: Does not trigger time change events or notifications
  • No User Notifications: Silent operation without user interface updates
  • Background Update: Clock updated in background without service interruption
  • Dual Response: May provide both system state and time confirmation

Use Cases:

  • Automated Synchronization: Regular time sync without user awareness
  • Background Maintenance: System maintenance time updates
  • Service Operations: Remote time correction during normal operation
  • Scheduled Updates: Periodic time synchronization from time servers

Device Compatibility

Clock Support: Use the 0x0001 - Capabilities command to check if the device supports time configuration:

  • TM:0 = Clock not configurable
  • TM:1 = Clock configurable (supports both 0x0004 and 0x000B commands)

Important Notes:

  • Only devices with TM:1 capability support this command
  • Silent operation means no visible indication of time change
  • Time is stored in device's local timezone
  • May be preferred for automated systems to avoid event flooding
  • Some devices may still log the time change internally
Example Response

Example Responses

Set time silently to 2024/03/15 14:30:00 - Success:

Request: 070100000000000000B00323032342f30332f31352031343a33303a3030
Response 1: [HEADER][0x0002][#F:1,DAT:01,NR,1100000010,10103,0404040404040404,15,3331110000,2024/03/15 14:30:00]
Response 2: [HEADER][0x000B][544d3a323032342f30332f31352031343a33303a3030]

Invalid date format - Error:

Request: 070100000000000000B004d61726368203135, 20323032342032 3a33302 504d
Response: [HEADER][0x000B][FORMAT]

Device doesn't support clock - Error:

Request: 070100000000000000B00323032342f30332f31352031343a33303a3030
Response: [HEADER][0x000B][NACK]

Invalid time values - Error:

Request: 070100000000000000B00323032342f31332f33322032353a37303a3830
Response: [HEADER][0x000B][FORMAT]

Response Breakdown:

  • Response 1: Full system state with updated timestamp
  • Response 2: Time confirmation with TM: prefix
  • Silent operation means no additional event notifications sent
Usage Example
javascript
async function setDeviceTimeSilently(partialUid, dateTime) {
  // Build silent set time command
  const header = "070100000000000000";  // Standard header
  const command = "B00";                 // Command 0x000B in MSB format (note: B00 not 0B00)
  
  // Format datetime string (ensure proper format)
  let formattedDateTime;
  if (dateTime instanceof Date) {
    formattedDateTime = formatDateTimeForDevice(dateTime);
  } else if (typeof dateTime === 'string') {
    // Validate and use provided string
    if (!isValidDateTimeFormat(dateTime)) {
      throw new Error('Invalid datetime format. Use YYYY/MM/DD HH:mm:ss');
    }
    formattedDateTime = dateTime;
  } else {
    throw new Error('DateTime must be a Date object or string in YYYY/MM/DD HH:mm:ss format');
  }
  
  const fullCommand = header + command + asciiToHex(formattedDateTime);
  
  try {
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      // Parse responses (may be multiple)
      const responses = result.data.map(data => {
        const responseText = hexToAscii(data);
        return parseTimeResponse(responseText);
      });
      
      console.log('Device time set silently:', responses);
      return responses;
    }
    
    throw new Error('No response data received');
  } catch (error) {
    console.error('Failed to set device time silently:', error);
    throw error;
  }
}

// Silent time synchronization for automated systems
async function silentTimeSync(partialUid, timeSource = 'system') {
  try {
    let syncTime;
    
    switch (timeSource) {
      case 'system':
        syncTime = new Date();
        break;
      case 'ntp':
        // In real implementation, this would fetch from NTP server
        syncTime = new Date(); // Placeholder
        console.log('Getting time from NTP server...');
        break;
      case 'server':
        // In real implementation, this would fetch from time API
        syncTime = new Date(); // Placeholder
        console.log('Getting time from server API...');
        break;
      default:
        throw new Error('Invalid time source. Use: system, ntp, or server');
    }
    
    console.log(`Silent time sync from ${timeSource}: ${syncTime.toISOString()}`);
    
    // Set time silently
    const result = await setDeviceTimeSilently(partialUid, syncTime);
    
    // Extract time confirmation if available
    const timeConfirmation = result.find(r => r.type === 'time_response');
    
    return {
      success: true,
      source: timeSource,
      syncTime: syncTime,
      confirmation: timeConfirmation,
      silent: true,
      responses: result
    };
    
  } catch (error) {
    console.error('Silent time synchronization failed:', error);
    throw error;
  }
}

// Scheduled silent time synchronization
async function scheduleSilentTimeSync(partialUid, intervalMinutes = 60, timeSource = 'system') {
  console.log(`Starting silent time sync every ${intervalMinutes} minutes from ${timeSource}`);
  
  const syncJob = setInterval(async () => {
    try {
      const result = await silentTimeSync(partialUid, timeSource);
      console.log(`[${new Date().toISOString()}] Silent time sync completed:`, {
        source: result.source,
        success: result.success,
        syncTime: result.syncTime.toISOString()
      });
    } catch (error) {
      console.error(`[${new Date().toISOString()}] Silent time sync failed:`, error.message);
    }
  }, intervalMinutes * 60 * 1000);
  
  return syncJob; // Return interval ID for cleanup
}

// Compare silent vs normal time setting
async function compareTimeSettingMethods(partialUid, testTime) {
  try {
    console.log('Comparing silent vs normal time setting methods...');
    
    // Method 1: Normal time setting (0x0004)
    console.log('Testing normal time setting (0x0004)...');
    const normalStart = Date.now();
    const normalResult = await setDeviceTime(partialUid, testTime);
    const normalDuration = Date.now() - normalStart;
    
    // Wait a moment
    await new Promise(resolve => setTimeout(resolve, 2000));
    
    // Method 2: Silent time setting (0x000B)
    console.log('Testing silent time setting (0x000B)...');
    const silentStart = Date.now();
    const silentResult = await setDeviceTimeSilently(partialUid, testTime);
    const silentDuration = Date.now() - silentStart;
    
    return {
      testTime: testTime,
      normal: {
        duration: normalDuration,
        result: normalResult,
        responseCount: 1
      },
      silent: {
        duration: silentDuration,
        result: silentResult,
        responseCount: silentResult.length
      },
      comparison: {
        speedDifference: normalDuration - silentDuration,
        fasterMethod: normalDuration < silentDuration ? 'normal' : 'silent'
      }
    };
    
  } catch (error) {
    console.error('Time setting method comparison failed:', error);
    throw error;
  }
}

function parseTimeResponse(responseText) {
  // Handle multiple response types
  if (responseText.startsWith('TM:')) {
    // Time confirmation response
    const timeString = responseText.substring(3);
    
    // Parse the time string
    const timeRegex = /^(\d{4})\/(\d{2})\/(\d{2}) (\d{2})-(\d{2})-(\d{2})$/;
    const match = timeString.match(timeRegex);
    
    if (!match) {
      throw new Error('Invalid time format in response');
    }
    
    const [, year, month, day, hour, minute, second] = match;
    const timestamp = new Date(
      parseInt(year),
      parseInt(month) - 1,
      parseInt(day),
      parseInt(hour),
      parseInt(minute),
      parseInt(second)
    );
    
    return {
      type: 'time_response',
      raw: responseText,
      formatted: timeString,
      timestamp: timestamp,
      source: 'silent_time_set'
    };
  } else if (responseText.startsWith('#F:')) {
    // System state response
    return {
      type: 'system_state',
      raw: responseText,
      source: 'silent_time_set'
    };
  } else {
    // Unknown response type
    return {
      type: 'unknown',
      raw: responseText,
      source: 'silent_time_set'
    };
  }
}

function formatDateTimeForDevice(date) {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  const hours = String(date.getHours()).padStart(2, '0');
  const minutes = String(date.getMinutes()).padStart(2, '0');
  const seconds = String(date.getSeconds()).padStart(2, '0');
  
  return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
}

function isValidDateTimeFormat(dateTimeString) {
  // Check format: YYYY/MM/DD HH:mm:ss
  const regex = /^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}$/;
  if (!regex.test(dateTimeString)) {
    return false;
  }
  
  // Parse and validate actual date values
  const parts = dateTimeString.split(' ');
  const dateParts = parts[0].split('/');
  const timeParts = parts[1].split(':');
  
  const year = parseInt(dateParts[0]);
  const month = parseInt(dateParts[1]);
  const day = parseInt(dateParts[2]);
  const hour = parseInt(timeParts[0]);
  const minute = parseInt(timeParts[1]);
  const second = parseInt(timeParts[2]);
  
  // Basic range validation
  if (month < 1 || month > 12) return false;
  if (day < 1 || day > 31) return false;
  if (hour < 0 || hour > 23) return false;
  if (minute < 0 || minute > 59) return false;
  if (second < 0 || second > 59) return false;
  
  // Create date object to check validity
  const testDate = new Date(year, month - 1, day, hour, minute, second);
  return testDate.getFullYear() === year &&
         testDate.getMonth() === month - 1 &&
         testDate.getDate() === day &&
         testDate.getHours() === hour &&
         testDate.getMinutes() === minute &&
         testDate.getSeconds() === second;
}

function asciiToHex(ascii) {
  let hex = '';
  for (let i = 0; i < ascii.length; i++) {
    hex += ascii.charCodeAt(i).toString(16).padStart(2, '0');
  }
  return hex;
}

function hexToAscii(hex) {
  let ascii = '';
  for (let i = 0; i < hex.length; i += 2) {
    ascii += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
  }
  return ascii;
}

// Usage examples
async function demonstrateSilentTimeOperations(partialUid) {
  try {
    // Basic silent time setting
    console.log('=== Silent Time Setting ===');
    const testTime = new Date();
    const silentResult = await setDeviceTimeSilently(partialUid, testTime);
    console.log('Silent time setting result:', silentResult);
    
    // Automated time synchronization
    console.log('\n=== Automated Time Sync ===');
    const syncResult = await silentTimeSync(partialUid, 'system');
    console.log('Silent sync result:', syncResult);
    
    // Method comparison
    console.log('\n=== Method Comparison ===');
    const comparison = await compareTimeSettingMethods(partialUid, new Date());
    console.log('Comparison result:', comparison);
    
    // Start scheduled sync (run for 5 minutes then stop)
    console.log('\n=== Scheduled Silent Sync ===');
    const scheduledSync = await scheduleSilentTimeSync(partialUid, 1, 'system'); // Every minute
    
    // Stop after 5 minutes
    setTimeout(() => {
      clearInterval(scheduledSync);
      console.log('Scheduled silent sync stopped');
    }, 5 * 60 * 1000);
    
  } catch (error) {
    console.error('Silent time operations demonstration failed:', error);
  }
}

0x000C - Set Device Ping Period

Sets the ping period for the device communication heartbeat. The ping period determines how frequently the device sends keepalive signals to maintain connection status.

Request/Response format

Request

[HEADER][0x000C][sssss] → 070100000000000000C00[sssss]

Where:

  • sssss = Ping period value (seconds divided by 10)
  • Range: 1-65535 (representing 10 to 655350 seconds)
  • Format: Decimal number as ASCII string

Examples:

  • Set 30 second ping period: 070100000000000000C00330 (sssss = "3")
  • Set 60 second ping period: 070100000000000000C00360 (sssss = "6")
  • Set 300 second ping period: 070100000000000000C003330 (sssss = "30")

Response Format

System State Response:

[HEADER][0x0002][SystemState]

Ping Period Confirmation:

[HEADER][0x000D][PP:sssss]

Error Response:

[HEADER][0x000C][NACK]
[HEADER][0x000C][FORMAT]
[HEADER][0x000C][ERROR]

Note: The ping period confirmation comes through a 0x000D Status response where PP:sssss shows the current setting (multiply by 10 to get actual seconds).

Command Parameters

ParameterFormatDescription
sssssASCII decimalPing period value (seconds ÷ 10)

Ping Period Guidelines

  • 10-30 seconds (send 1-3): Real-time monitoring, frequent updates
  • 30-60 seconds (send 3-6): Standard operation (recommended)
  • 60-300 seconds (send 6-30): Battery conservation, reduced traffic
  • 300+ seconds (send 30+): Minimal monitoring, extended battery life

Error Conditions

  • NACK - Invalid ping period value or device doesn't support setting
  • FORMAT - Invalid data format (non-numeric or out of range)
  • ERROR - Device communication error during configuration
Usage Example
javascript
async function setPingPeriod(partialUid, seconds) {
  // Convert seconds to ping value (seconds ÷ 10)
  const pingValue = Math.round(seconds / 10);
  
  // Validate range
  if (pingValue < 1 || pingValue > 65535) {
    throw new Error(`Ping period out of range: ${seconds}s (value: ${pingValue})`);
  }
  
  // Build command
  const header = "070100000000000000";
  const command = "C00";
  const data = pingValue.toString();
  const dataHex = asciiToHex(data);
  const fullCommand = header + command + dataHex;
  
  try {
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responses = result.data.map(data => {
        const responseText = hexToAscii(data);
        return parsePingResponse(responseText);
      });
      
      console.log(`Ping period set to ${seconds} seconds`);
      return responses;
    }
    
    throw new Error('No response received');
  } catch (error) {
    console.error(`Failed to set ping period:`, error);
    throw error;
  }
}

function parsePingResponse(responseText) {
  if (responseText.startsWith('PP:')) {
    const pingValue = parseInt(responseText.substring(3));
    return {
      type: 'ping_period',
      pingValue: pingValue,
      seconds: pingValue * 10
    };
  }
  
  if (['NACK', 'FORMAT', 'ERROR'].includes(responseText)) {
    return {
      type: 'error',
      error: responseText
    };
  }
  
  return { type: 'unknown', data: responseText };
}

// Usage examples
async function demonstratePingControl(partialUid) {
  try {
    // Set standard 30-second ping
    await setPingPeriod(partialUid, 30);
    
    // Set fast 10-second ping for monitoring
    await setPingPeriod(partialUid, 10);
    
    // Set slow 5-minute ping for battery saving
    await setPingPeriod(partialUid, 300);
    
  } catch (error) {
    console.error('Ping period demonstration failed:', error);
  }
}

0x000D - Status

Retrieves comprehensive system status including zones, outputs, areas, signal strength, troubles, and sensors. This command provides a complete overview of the device state and is essential for monitoring and troubleshooting.

Request/Response format

Request

[HEADER][0x000D] → 070100000000000000D00

Response Format

[HEADER][0x000D][DATA ASCII]

Response Data Fields:

  • Z:xxxxxx - Zone statuses (4 bits per zone)
  • O:xxxxx - Output statuses (4 bits per output)
  • O2:X#SSSS.X#SSSS... - Extended output parameters (dot-separated)
  • SYS:AAAAAA - Area statuses (2 hex digits per area)
  • RSSI:L - GSM signal level
  • TRB:TTTTT - Trouble statuses (4 bits per trouble)
  • SEN:Svvvvvvvv... - Sensor data (status + 8-byte value per sensor)
  • PP:P - Ping period setting (optional)
  • TM:YYYY/MM/DD HH-mm-ss - Device time (optional)

Response Data Structure

FieldFormatDescription
Z:Hex stringZone statuses (4 bits per zone)
O:Hex stringOutput statuses (4 bits per output)
O2:Dot-separatedExtended output parameters
SYS:Hex pairsArea statuses (2 hex digits per area)
RSSI:DecimalGSM signal strength level
TRB:Hex stringTrouble statuses (4 bits per trouble)
SEN:Hex stringSensor statuses and values
PP:DecimalPing period setting (when available)
TM:DateTimeCurrent device time (when available)

Zone Status Decoding

Each zone uses 4 bits encoded as one hex digit:

HexBinaryEnabledAlarmTamperBypassDescription
00000NoNoNoNoDisabled, Normal
10001YesNoNoNoEnabled, Normal
20010NoYesNoNoDisabled, Alarm
30011YesYesNoNoEnabled, Alarm
40100NoNoYesNoDisabled, Tamper
50101YesNoYesNoEnabled, Tamper
81000NoNoNoYesDisabled, Bypassed
91001YesNoNoYesEnabled, Bypassed

Output Status Decoding

Each output uses 4 bits encoded as one hex digit:

HexBinaryRemoteStateFire ResetDescription
00000NoOFFNoNo remote, OFF
10001YesOFFNoRemote enabled, OFF
20010NoONNoNo remote, ON
30011YesONNoRemote enabled, ON
40100NoOFFYesNo remote, OFF, Fire reset
50101YesOFFYesRemote enabled, OFF, Fire reset

Area State Descriptions

CodeStateDescription
00-03ARM StatesArmed or arming process
04-08DISARM StatesDisarmed or disarming process
09-0CSTAY StatesStay mode (partial arm)
0D-10SLEEP StatesSleep mode
11CancelOperation cancelled
12PanicPanic button area
13FCFire communicator area/loop
FFN/AArea disabled

Trouble Status Decoding

Each trouble uses 4 bits encoded as one hex digit (index starts from 1):

HexBinaryEnabledFaultDescription
00000NoNoDisabled, OK
10001YesNoEnabled, OK
20010NoYesDisabled, Fault
30011YesYesEnabled, Fault
4-Fxxxx--Reserved combinations

Trouble Type Descriptions

IndexDescription
1AC lost
2No or Low battery
3Wireless transmitter battery low
4Power failure
5Bell disconnected
6Bell current failure
7Auxiliary current failure
8Communication failure
9Timer loss
10Tamper/Zone wiring failure
11Telephone line monitoring failure
12Fire zone trouble
13Module loss
14Wireless transmitter supervision lost
15Keypad fault

RSSI Signal Strength Decoding

ValuedBm RangeQuality
0≤ -115 dBmNo signal
1-111 dBmVery poor
2-10-110 to -92 dBmPoor
11-20-90 to -72 dBmFair
21-30-70 to -54 dBmGood
31≥ -52 dBmExcellent
99UnknownNot detectable

Sensor Status Decoding

Each sensor uses 1 hex digit for status + 8 hex bytes for value:

HexBinaryEnabledOnlineHigh AlarmLow AlarmDescription
00000NoNoNoNoDisabled, Offline
10001YesNoNoNoEnabled, Offline
20010NoYesNoNoDisabled, Online
30011YesYesNoNoEnabled, Online, Normal
70111YesYesYesNoEnabled, Online, High Alarm
B1011YesYesNoYesEnabled, Online, Low Alarm
F1111YesYesYesYesEnabled, Online, Both Alarms
Usage Example
javascript
async function getSystemStatus(partialUid) {
  // Build status request command
  const header = "070100000000000000";
  const command = "D00";
  const fullCommand = header + command;
  
  try {
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      const status = parseSystemStatus(responseText);
      
      console.log('System Status Retrieved:', status);
      return status;
    }
    
    throw new Error('No status data received');
  } catch (error) {
    console.error('Failed to get system status:', error);
    throw error;
  }
}

function parseSystemStatus(statusText) {
  const status = {
    zones: [],
    outputs: [],
    extendedOutputs: [],
    areas: [],
    rssi: null,
    troubles: [],
    sensors: [],
    pingPeriod: null,
    deviceTime: null,
    raw: statusText
  };
  
  // Parse each field
  const fields = statusText.split(/(?=[A-Z]+:)/);
  
  fields.forEach(field => {
    if (field.startsWith('Z:')) {
      status.zones = parseZoneStatus(field.substring(2));
    } else if (field.startsWith('O2:')) {
      status.extendedOutputs = parseExtendedOutputs(field.substring(3));
    } else if (field.startsWith('O:')) {
      status.outputs = parseOutputStatus(field.substring(2));
    } else if (field.startsWith('SYS:')) {
      status.areas = parseAreaStatus(field.substring(4));
    } else if (field.startsWith('RSSI:')) {
      status.rssi = parseRSSI(field.substring(5));
    } else if (field.startsWith('TRB:')) {
      status.troubles = parseTroubleStatus(field.substring(4));
    } else if (field.startsWith('SEN:')) {
      status.sensors = parseSensorStatus(field.substring(4));
    } else if (field.startsWith('PP:')) {
      status.pingPeriod = parseInt(field.substring(3)) * 10; // Convert to seconds
    } else if (field.startsWith('TM:')) {
      status.deviceTime = field.substring(3);
    }
  });
  
  return status;
}

function parseZoneStatus(zoneData) {
  const zones = [];
  for (let i = 0; i < zoneData.length; i++) {
    const hexDigit = parseInt(zoneData[i], 16);
    zones.push({
      zone: i + 1,
      enabled: !!(hexDigit & 0x01),
      alarm: !!(hexDigit & 0x02),
      tamper: !!(hexDigit & 0x04),
      bypassed: !!(hexDigit & 0x08),
      raw: zoneData[i]
    });
  }
  return zones;
}

function parseOutputStatus(outputData) {
  const outputs = [];
  for (let i = 0; i < outputData.length; i++) {
    const hexDigit = parseInt(outputData[i], 16);
    outputs.push({
      output: i + 1,
      remoteEnabled: !!(hexDigit & 0x01),
      state: !!(hexDigit & 0x02),
      fireReset: !!(hexDigit & 0x04),
      raw: outputData[i]
    });
  }
  return outputs;
}

function parseExtendedOutputs(extData) {
  return extData.split('.').map((item, index) => {
    if (item.length >= 6) {
      return {
        output: index + 1,
        type: item[0], // I, M, O, W
        current: parseInt(item[1]), // 0-3
        schedule: item.substring(2, 6),
        raw: item
      };
    }
    return { output: index + 1, raw: item };
  });
}

function parseAreaStatus(areaData) {
  const areas = [];
  for (let i = 0; i < areaData.length; i += 2) {
    if (i + 1 < areaData.length) {
      const areaCode = areaData.substring(i, i + 2);
      areas.push({
        area: (i / 2) + 1,
        state: areaCode,
        description: getAreaStateDescription(areaCode),
        raw: areaCode
      });
    }
  }
  return areas;
}

function parseRSSI(rssiValue) {
  const rssi = parseInt(rssiValue);
  let dbm;
  
  if (rssi === 0) dbm = '<= -115';
  else if (rssi === 1) dbm = '-111';
  else if (rssi >= 2 && rssi <= 30) dbm = (-110 + (rssi - 2) * 2).toString();
  else if (rssi === 31) dbm = '>= -52';
  else if (rssi === 99) dbm = 'Unknown';
  else dbm = 'Invalid';
  
  return {
    level: rssi,
    dbm: dbm,
    quality: rssi <= 10 ? 'Poor' : rssi <= 20 ? 'Fair' : rssi <= 30 ? 'Good' : 'Excellent'
  };
}

function parseTroubleStatus(troubleData) {
  const troubles = [];
  for (let i = 0; i < troubleData.length; i++) {
    const hexDigit = parseInt(troubleData[i], 16);
    troubles.push({
      trouble: i + 1,
      enabled: !!(hexDigit & 0x01),
      fault: !!(hexDigit & 0x02),
      raw: troubleData[i]
    });
  }
  return troubles;
}

function parseSensorStatus(sensorData) {
  const sensors = [];
  let i = 0;
  
  while (i < sensorData.length) {
    if (i + 8 < sensorData.length) {
      const status = parseInt(sensorData[i], 16);
      const valueHex = sensorData.substring(i + 1, i + 9);
      
      sensors.push({
        sensor: Math.floor(i / 9) + 1,
        enabled: !!(status & 0x01),
        online: !!(status & 0x02),
        highAlarm: !!(status & 0x04),
        lowAlarm: !!(status & 0x08),
        valueHex: valueHex,
        value: parseFloat32FromHex(valueHex),
        raw: sensorData.substring(i, i + 9)
      });
      
      i += 9;
    } else {
      break;
    }
  }
  return sensors;
}

function parseFloat32FromHex(hexValue) {
  // Convert hex string to float32 with byte swap
  try {
    const bytes = [];
    for (let i = 0; i < hexValue.length; i += 2) {
      bytes.push(parseInt(hexValue.substr(i, 2), 16));
    }
    
    // Byte swap for little endian
    const swapped = [bytes[3], bytes[2], bytes[1], bytes[0]];
    const buffer = new ArrayBuffer(4);
    const view = new DataView(buffer);
    
    swapped.forEach((byte, index) => {
      view.setUint8(index, byte);
    });
    
    return view.getFloat32(0, false); // big endian
  } catch (error) {
    return null;
  }
}

function getAreaStateDescription(stateCode) {
  const states = {
    '00': 'Armed',
    '01': 'Arming Starting',
    '02': 'Arming Waiting',
    '03': 'Prepare ARM',
    '04': 'Disarmed',
    '05': 'Disarming Waiting',
    '06': 'Prepare Disarm',
    '07': 'Prepare Remote Disarm',
    '08': 'Remote Disarm Waiting',
    '09': 'Stay Delay',
    '0A': 'Stay Delay Starting',
    '0B': 'Stay Delay Waiting',
    '0C': 'Stay Delay Waiting Prepare',
    '0D': 'Sleep',
    '0E': 'Sleep Starting',
    '0F': 'Sleep Waiting Prepare',
    '10': 'Sleep Waiting',
    '11': 'Cancel',
    '12': 'Panic Button Area',
    '13': 'Fire Control Area',
    'FF': 'Disabled/N/A'
  };
  return states[stateCode.toUpperCase()] || `Unknown (${stateCode})`;
}

// Usage examples
async function demonstrateStatusMonitoring(partialUid) {
  try {
    console.log('=== System Status Monitoring ===');
    
    // Get complete system status
    const status = await getSystemStatus(partialUid);
    
    // Display zone summary
    console.log('\n--- Zone Status ---');
    status.zones.forEach(zone => {
      if (zone.enabled) {
        const alerts = [];
        if (zone.alarm) alerts.push('ALARM');
        if (zone.tamper) alerts.push('TAMPER');
        if (zone.bypassed) alerts.push('BYPASSED');
        
        console.log(`Zone ${zone.zone}: ${alerts.length ? alerts.join(', ') : 'Normal'}`);
      }
    });
    
    // Display area status
    console.log('\n--- Area Status ---');
    status.areas.forEach(area => {
      if (area.state !== 'FF') {
        console.log(`Area ${area.area}: ${area.description}`);
      }
    });
    
    // Display signal strength
    if (status.rssi) {
      console.log(`\n--- Signal Strength ---`);
      console.log(`RSSI: ${status.rssi.level} (${status.rssi.dbm} dBm) - ${status.rssi.quality}`);
    }
    
    // Display active troubles
    console.log('\n--- Troubles ---');
    const activeTroubles = status.troubles.filter(t => t.enabled && t.fault);
    if (activeTroubles.length > 0) {
      activeTroubles.forEach(trouble => {
        console.log(`Trouble ${trouble.trouble}: FAULT`);
      });
    } else {
      console.log('No active troubles');
    }
    
    return status;
    
  } catch (error) {
    console.error('Status monitoring demonstration failed:', error);
  }
}

0x000E - Inputs State of Area

Retrieves the input (zone) status for a specific area or all areas. This command provides zone status information similar to the Z: field in 0x000D Status but filtered by area.

Request/Response format

Request

[HEADER][0x000E][SYS:N] → 070100000000000000E00[SYS:N]
[HEADER][0x000E] → 070100000000000000E00

Where:

  • SYS:N = Area number (N = decimal area number)
  • If SYS:N is omitted, returns input state of all areas

Examples:

  • Get inputs for area 1: 070100000000000000E005359533A31 (SYS:1)
  • Get inputs for area 2: 070100000000000000E005359533A32 (SYS:2)
  • Get inputs for all areas: 070100000000000000E00

Response Format

[HEADER][0x000E][Z:xxxxxx]

Where:

  • Z:xxxxxx = Zone statuses using the same 4-bit encoding as 0x000D Status

Response Examples:

  • Single area zones: [HEADER][0x000E][Z:1100]
  • Multiple area zones: [HEADER][0x000E][Z:11001001]

Command Parameters

ParameterFormatDescription
SYS:NASCIIArea number (optional, omit for all areas)
NDecimalArea number (1-based indexing)

Zone Status Encoding

Uses the same 4-bit encoding per zone as 0x000D Status:

  • Bit 0: 1 = Zone Enabled, 0 = Zone Disabled
  • Bit 1: 1 = Zone Alarm, 0 = Zone Restore
  • Bit 2: 1 = Zone Tamper/Wire Fault, 0 = Restore OK
  • Bit 3: 1 = Zone Bypassed, 0 = Bypass OFF

For complete zone status decoding, see the Zone Status Decoding table in the 0x000D command documentation.

Error Handling

Common Error Responses:

  • NACK - Invalid area number or area doesn't exist
  • FORMAT - Invalid SYS:N format
  • ERROR - Communication error during retrieval
Usage Example
javascript
async function getAreaInputsState(partialUid, areaNumber = null) {
  // Build inputs state request command
  const header = "070100000000000000";
  const command = "E00";
  
  let fullCommand;
  if (areaNumber !== null) {
    // Request specific area
    const areaParam = `SYS:${areaNumber}`;
    const areaHex = asciiToHex(areaParam);
    fullCommand = header + command + areaHex;
  } else {
    // Request all areas
    fullCommand = header + command;
  }
  
  try {
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      const inputsState = parseInputsState(responseText);
      
      console.log(`Inputs state retrieved${areaNumber ? ` for area ${areaNumber}` : ' for all areas'}:`, inputsState);
      return inputsState;
    }
    
    throw new Error('No inputs state data received');
  } catch (error) {
    console.error(`Failed to get inputs state${areaNumber ? ` for area ${areaNumber}` : ''}:`, error);
    throw error;
  }
}

function parseInputsState(responseText) {
  if (!responseText.startsWith('Z:')) {
    throw new Error(`Invalid inputs state response: ${responseText}`);
  }
  
  const zoneData = responseText.substring(2);
  const zones = [];
  
  for (let i = 0; i < zoneData.length; i++) {
    const hexDigit = parseInt(zoneData[i], 16);
    zones.push({
      zone: i + 1,
      enabled: !!(hexDigit & 0x01),
      alarm: !!(hexDigit & 0x02),
      tamper: !!(hexDigit & 0x04),
      bypassed: !!(hexDigit & 0x08),
      raw: zoneData[i],
      status: getZoneStatusDescription(hexDigit)
    });
  }
  
  return {
    zones: zones,
    totalZones: zones.length,
    activeAlarms: zones.filter(z => z.alarm).length,
    tamperFaults: zones.filter(z => z.tamper).length,
    bypassedZones: zones.filter(z => z.bypassed).length,
    raw: responseText
  };
}

function getZoneStatusDescription(hexValue) {
  const descriptions = {
    0x0: 'Disabled, Normal',
    0x1: 'Enabled, Normal',
    0x2: 'Disabled, Alarm',
    0x3: 'Enabled, Alarm',
    0x4: 'Disabled, Tamper',
    0x5: 'Enabled, Tamper',
    0x8: 'Disabled, Bypassed',
    0x9: 'Enabled, Bypassed',
    0xA: 'Disabled, Alarm+Bypassed',
    0xB: 'Enabled, Alarm+Bypassed',
    0xC: 'Disabled, Tamper+Bypassed',
    0xD: 'Enabled, Tamper+Bypassed'
  };
  
  return descriptions[hexValue] || `Unknown status: 0x${hexValue.toString(16)}`;
}

// Convenience functions for common operations
async function getSpecificAreaInputs(partialUid, areaNumber) {
  return await getAreaInputsState(partialUid, areaNumber);
}

async function getAllAreasInputs(partialUid) {
  return await getAreaInputsState(partialUid, null);
}

async function checkAreaAlarms(partialUid, areaNumber) {
  try {
    const inputsState = await getAreaInputsState(partialUid, areaNumber);
    const alarmZones = inputsState.zones.filter(zone => zone.alarm && zone.enabled);
    
    return {
      hasAlarms: alarmZones.length > 0,
      alarmCount: alarmZones.length,
      alarmZones: alarmZones.map(zone => ({
        zone: zone.zone,
        status: zone.status
      }))
    };
  } catch (error) {
    console.error(`Failed to check area ${areaNumber} alarms:`, error);
    throw error;
  }
}

async function checkAreaTampers(partialUid, areaNumber) {
  try {
    const inputsState = await getAreaInputsState(partialUid, areaNumber);
    const tamperZones = inputsState.zones.filter(zone => zone.tamper && zone.enabled);
    
    return {
      hasTampers: tamperZones.length > 0,
      tamperCount: tamperZones.length,
      tamperZones: tamperZones.map(zone => ({
        zone: zone.zone,
        status: zone.status
      }))
    };
  } catch (error) {
    console.error(`Failed to check area ${areaNumber} tampers:`, error);
    throw error;
  }
}

// Monitor specific area for changes
async function monitorAreaInputs(partialUid, areaNumber, intervalSeconds = 5) {
  let previousState = null;
  
  const monitor = setInterval(async () => {
    try {
      const currentState = await getAreaInputsState(partialUid, areaNumber);
      
      if (previousState) {
        // Check for changes
        const changes = detectZoneChanges(previousState.zones, currentState.zones);
        if (changes.length > 0) {
          console.log(`Area ${areaNumber} zone changes detected:`, changes);
        }
      }
      
      previousState = currentState;
      
    } catch (error) {
      console.error(`Area ${areaNumber} monitoring error:`, error);
    }
  }, intervalSeconds * 1000);
  
  return monitor; // Return interval ID for stopping
}

function detectZoneChanges(previousZones, currentZones) {
  const changes = [];
  
  for (let i = 0; i < Math.max(previousZones.length, currentZones.length); i++) {
    const prev = previousZones[i];
    const curr = currentZones[i];
    
    if (!prev && curr) {
      changes.push({ zone: curr.zone, change: 'added', current: curr.status });
    } else if (prev && !curr) {
      changes.push({ zone: prev.zone, change: 'removed', previous: prev.status });
    } else if (prev && curr && prev.raw !== curr.raw) {
      changes.push({ 
        zone: curr.zone, 
        change: 'modified', 
        previous: prev.status, 
        current: curr.status 
      });
    }
  }
  
  return changes;
}

// Usage examples
async function demonstrateInputsMonitoring(partialUid) {
  try {
    console.log('=== Area Inputs Monitoring Demonstration ===');
    
    // Get inputs for specific area
    console.log('\n--- Area 1 Inputs ---');
    const area1Inputs = await getSpecificAreaInputs(partialUid, 1);
    console.log(`Area 1: ${area1Inputs.totalZones} zones, ${area1Inputs.activeAlarms} alarms`);
    
    // Check for alarms in area 1
    const area1Alarms = await checkAreaAlarms(partialUid, 1);
    if (area1Alarms.hasAlarms) {
      console.log('Active alarms in area 1:', area1Alarms.alarmZones);
    }
    
    // Get inputs for all areas
    console.log('\n--- All Areas Inputs ---');
    const allInputs = await getAllAreasInputs(partialUid);
    console.log(`Total zones: ${allInputs.totalZones}`);
    
    // Start monitoring area 2 for 30 seconds
    console.log('\n--- Starting Area 2 Monitoring ---');
    const monitorId = await monitorAreaInputs(partialUid, 2, 3);
    
    setTimeout(() => {
      clearInterval(monitorId);
      console.log('Area 2 monitoring stopped');
    }, 30000);
    
    console.log('Inputs monitoring demonstration completed');
    
  } catch (error) {
    console.error('Inputs monitoring demonstration failed:', error);
  }
}

0x000F - Fault State

Retrieves fault state information for SP231 devices. This command returns device-specific fault information using a different fault classification system than the standard 0x000D Status command.

Request/Response format

Request

[HEADER][0x000F] → 070100000000000000F00

Response Format

[HEADER][0x000F][TRB:GGIIIFFFFFFFFFF]

Where:

  • GG = Fault Group (2 digits)
  • III = Fault Index (3 digits)
  • FFFFFFFFFF = Fault state data (variable length hex)

Response Examples:

  • Single fault: [HEADER][0x000F][TRB:10009] (Group 10, Index 009)
  • Multiple faults: [HEADER][0x000F][TRB:10009FFFFFFFFFF] (Group 10, Index 009 + fault states)

Fault Group Classification

GroupCategoryDescription
10SystemCore system faults (power, battery, hardware)
20CommunicatorCommunication path faults
30Zone TamperZone tampering detection
40Network COMBUSCOMBUS module troubles
50Network MCIMCI module troubles
60Zone FaultZone wiring/sensor faults
70Wireless BatteryWireless zone battery issues
80Wireless SupervisionWireless zone supervision loss
90FunctionalityFirmware and operational faults

System Faults (Group 10)

IndexDescription
001AC Failure
002Low Battery
003Battery Missing/Dead
004Aux Current Limit
005Bell Current Limit
006Bell Absent
007Clock loss
008CROW RF Receiver Jam Detected
009CROW RF Receiver supervision loss

Communicator Faults (Group 20)

IndexDescription
001Primary (IP/SMS)
002Backup1 (IP/SMS)
003Backup2 (SMS)
004MCI Backup (Ethernet/GSM/RF) Parallel Channel
005GSM Registration, Network, SIM Card
006No SIM card
007Incorrect SIM card PIN code
008Programming problem (No APN)
009Registration to GSM network problem
010Registration to GPRS/UMTS network problem
011No connection with the receiver (deprecated)
GroupIndex FormatDescription
30zzzZone Tamper - Zones 1-32 (zzz = zone number)
60zzzZone Fault/Anti-Masking/Fire Loop Trouble - Zones 1-32
70zzzWireless Zone Low Battery - Zones 1-32
80zzzWireless Zone Supervision Loss - Zones 1-32

Module Faults (Groups 40, 50)

GroupIndex FormatDescription
40mmmCOMBUS Module Trouble (mmm = module number)
50mmmMCI Module Trouble (mmm = module number)

Functionality Faults (Group 90)

IndexDescription
001Memory fault
002Firmware is corrupted
003Lost connection with control panel
004Incorrectly set type of operating mode
005Critical error in structure of parameters
006Error in list of users
Usage Example
javascript
async function getFaultState(partialUid) {
  // Build fault state request command (SP231 specific)
  const header = "070100000000000000";
  const command = "F00";
  const fullCommand = header + command;
  
  try {
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      const faultState = parseFaultState(responseText);
      
      console.log('SP231 Fault State Retrieved:', faultState);
      return faultState;
    }
    
    throw new Error('No fault state data received');
  } catch (error) {
    console.error('Failed to get SP231 fault state:', error);
    throw error;
  }
}

function parseFaultState(responseText) {
  if (!responseText.startsWith('TRB:')) {
    throw new Error(`Invalid fault state response: ${responseText}`);
  }
  
  const faultData = responseText.substring(4);
  
  // Parse group and index from first 5 characters
  if (faultData.length < 5) {
    throw new Error('Fault data too short');
  }
  
  const group = parseInt(faultData.substring(0, 2));
  const index = parseInt(faultData.substring(2, 5));
  const stateData = faultData.length > 5 ? faultData.substring(5) : null;
  
  const fault = {
    group: group,
    index: index,
    groupName: getFaultGroupName(group),
    description: getFaultDescription(group, index),
    stateData: stateData,
    raw: responseText
  };
  
  return fault;
}

function getFaultGroupName(group) {
  const groups = {
    10: 'System',
    20: 'Communicator',
    30: 'Zone Tamper',
    40: 'Network COMBUS',
    50: 'Network MCI',
    60: 'Zone Fault',
    70: 'Wireless Battery',
    80: 'Wireless Supervision',
    90: 'Functionality'
  };
  
  return groups[group] || `Unknown Group ${group}`;
}

function getFaultDescription(group, index) {
  // System faults (Group 10)
  if (group === 10) {
    const systemFaults = {
      1: 'AC Failure',
      2: 'Low Battery',
      3: 'Battery Missing/Dead',
      4: 'Aux Current Limit',
      5: 'Bell Current Limit',
      6: 'Bell Absent',
      7: 'Clock loss',
      8: 'CROW RF Receiver Jam Detected',
      9: 'CROW RF Receiver supervision loss'
    };
    return systemFaults[index] || `Unknown System Fault ${index}`;
  }
  
  // Communicator faults (Group 20)
  if (group === 20) {
    const commFaults = {
      1: 'Primary (IP/SMS)',
      2: 'Backup1 (IP/SMS)',
      3: 'Backup2 (SMS)',
      4: 'MCI Backup (Ethernet/GSM/RF) Parallel Channel',
      5: 'GSM Registration, Network, SIM Card',
      6: 'No SIM card',
      7: 'Incorrect SIM card PIN code',
      8: 'Programming problem (No APN)',
      9: 'Registration to GSM network problem',
      10: 'Registration to GPRS/UMTS network problem',
      11: 'No connection with the receiver (deprecated)'
    };
    return commFaults[index] || `Unknown Communicator Fault ${index}`;
  }
  
  // Zone-related faults
  if (group === 30) {
    return `Zone ${index} Tamper`;
  }
  
  if (group === 60) {
    return `Zone ${index} Fault/Anti-Masking/Fire Loop Trouble`;
  }
  
  if (group === 70) {
    return `Wireless Zone ${index} Low Battery`;
  }
  
  if (group === 80) {
    return `Wireless Zone ${index} Supervision Loss`;
  }
  
  // Module faults
  if (group === 40) {
    return `COMBUS Module ${index} Trouble`;
  }
  
  if (group === 50) {
    return `MCI Module ${index} Trouble`;
  }
  
  // Functionality faults (Group 90)
  if (group === 90) {
    const funcFaults = {
      1: 'Memory fault',
      2: 'Firmware is corrupted',
      3: 'Lost connection with control panel',
      4: 'Incorrectly set type of operating mode',
      5: 'Critical error in structure of parameters',
      6: 'Error in list of users'
    };
    return funcFaults[index] || `Unknown Functionality Fault ${index}`;
  }
  
  return `Unknown fault: Group ${group}, Index ${index}`;
}

// Convenience functions for specific fault types
async function checkSystemFaults(partialUid) {
  try {
    const fault = await getFaultState(partialUid);
    return fault.group === 10 ? fault : null;
  } catch (error) {
    console.error('Failed to check system faults:', error);
    return null;
  }
}

async function checkCommunicatorFaults(partialUid) {
  try {
    const fault = await getFaultState(partialUid);
    return fault.group === 20 ? fault : null;
  } catch (error) {
    console.error('Failed to check communicator faults:', error);
    return null;
  }
}

async function checkZoneFaults(partialUid) {
  try {
    const fault = await getFaultState(partialUid);
    return [30, 60, 70, 80].includes(fault.group) ? fault : null;
  } catch (error) {
    console.error('Failed to check zone faults:', error);
    return null;
  }
}

// Monitor faults periodically
async function monitorSP231Faults(partialUid, intervalSeconds = 30) {
  let previousFault = null;
  
  const monitor = setInterval(async () => {
    try {
      const currentFault = await getFaultState(partialUid);
      
      if (!previousFault || 
          previousFault.group !== currentFault.group || 
          previousFault.index !== currentFault.index) {
        
        console.log('SP231 Fault State Changed:', {
          group: currentFault.group,
          groupName: currentFault.groupName,
          index: currentFault.index,
          description: currentFault.description
        });
      }
      
      previousFault = currentFault;
      
    } catch (error) {
      console.error('SP231 fault monitoring error:', error);
    }
  }, intervalSeconds * 1000);
  
  return monitor;
}

// Usage examples
async function demonstrateSP231FaultMonitoring(partialUid) {
  try {
    console.log('=== SP231 Fault State Monitoring ===');
    
    // Get current fault state
    const faultState = await getFaultState(partialUid);
    console.log('Current fault state:', faultState);
    
    // Check specific fault types
    console.log('\n--- System Faults Check ---');
    const systemFault = await checkSystemFaults(partialUid);
    if (systemFault) {
      console.log('System fault detected:', systemFault.description);
    } else {
      console.log('No system faults');
    }
    
    console.log('\n--- Communicator Faults Check ---');
    const commFault = await checkCommunicatorFaults(partialUid);
    if (commFault) {
      console.log('Communicator fault detected:', commFault.description);
    } else {
      console.log('No communicator faults');
    }
    
    console.log('\n--- Zone Faults Check ---');
    const zoneFault = await checkZoneFaults(partialUid);
    if (zoneFault) {
      console.log('Zone fault detected:', zoneFault.description);
    } else {
      console.log('No zone faults');
    }
    
    // Start fault monitoring
    console.log('\n--- Starting Fault Monitoring (60 seconds) ---');
    const monitorId = await monitorSP231Faults(partialUid, 10);
    
    setTimeout(() => {
      clearInterval(monitorId);
      console.log('SP231 fault monitoring stopped');
    }, 60000);
    
    console.log('SP231 fault monitoring demonstration completed');
    
  } catch (error) {
    console.error('SP231 fault monitoring demonstration failed:', error);
  }
}

0x0010 - Fire Sensors Reset

Resets all fire sensors on the device. This command clears fire sensor alarm states and restores sensors to their normal monitoring state.

Request/Response format

Request

[HEADER][0x0010] → 0701000000000000001000

Response Format

The response format varies depending on the device. Three possible acknowledgment responses:

Status Response:

[HEADER][0x000D][Status Data]

System State Response:

[HEADER][0x0002][SystemState]

Command Acknowledgment:

[HEADER][0x0010][ACK]

Note: The specific response type depends on the device model and firmware version. Some devices may send multiple response types.

Command Operation

OperationDescription
Fire Sensor ResetClears alarm states from all fire detection sensors
State RestorationReturns fire sensors to normal monitoring mode
System UpdateUpdates device status to reflect reset operation

Safety Considerations

Important Safety Notes:

  • Only reset fire sensors after confirming no actual fire condition exists
  • Verify fire condition has been resolved before issuing reset command
  • Ensure proper fire department notification procedures have been followed
  • Reset should only be performed by authorized personnel

Response Handling

Multiple Response Types:

  • Different devices may respond with different message types
  • Applications should handle all three possible response formats
  • Response indicates successful fire sensor reset operation

Status Verification:

  • Use 0x000D Status command to verify sensor states after reset
  • Check fire sensor zones for proper restoration to normal state
  • Confirm fire loop integrity after reset operation
Usage Example
javascript
async function resetFireSensors(partialUid) {
  // Build fire sensors reset command
  const header = "070100000000000000";
  const command = "1000";  // Command 0x0010 in MSB format
  const fullCommand = header + command;
  
  try {
    console.log('Resetting all fire sensors...');
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responses = result.data.map(data => {
        const responseText = hexToAscii(data);
        return parseFireResetResponse(responseText);
      });
      
      console.log('Fire sensors reset completed');
      console.log('Reset responses:', responses);
      
      // Verify reset by checking status
      await verifyFireReset(partialUid);
      
      return responses;
    }
    
    throw new Error('No response received for fire sensor reset');
  } catch (error) {
    console.error('Failed to reset fire sensors:', error);
    throw error;
  }
}

function parseFireResetResponse(responseText) {
  // Handle different response types
  if (responseText === 'ACK') {
    return {
      type: 'acknowledgment',
      success: true,
      message: 'Fire sensor reset acknowledged'
    };
  }
  
  if (responseText.startsWith('Z:') || responseText.startsWith('O:') || responseText.startsWith('SYS:')) {
    return {
      type: 'status_data',
      success: true,
      statusData: responseText,
      message: 'Status data received after fire reset'
    };
  }
  
  if (responseText.match(/^[0-9A-F,]+$/)) {
    return {
      type: 'system_state',
      success: true,
      systemState: responseText,
      message: 'System state updated after fire reset'
    };
  }
  
  return {
    type: 'unknown',
    success: false,
    data: responseText,
    message: 'Unknown response format'
  };
}

async function verifyFireReset(partialUid) {
  try {
    console.log('Verifying fire sensor reset...');
    
    // Wait a moment for system to update
    await new Promise(resolve => setTimeout(resolve, 2000));
    
    // Get current system status to verify reset
    const status = await getSystemStatus(partialUid);
    
    // Check fire-related zones
    const fireZones = status.zones.filter(zone => zone.enabled);
    const activeFireAlarms = fireZones.filter(zone => zone.alarm);
    
    if (activeFireAlarms.length === 0) {
      console.log('Fire sensor reset verification: SUCCESS - No active fire alarms');
      return {
        success: true,
        message: 'All fire sensors successfully reset',
        fireZones: fireZones.length,
        activeAlarms: 0
      };
    } else {
      console.log('Fire sensor reset verification: WARNING - Some fire alarms still active');
      return {
        success: false,
        message: 'Some fire alarms remain active after reset',
        fireZones: fireZones.length,
        activeAlarms: activeFireAlarms.length,
        activeZones: activeFireAlarms.map(z => z.zone)
      };
    }
    
  } catch (error) {
    console.error('Failed to verify fire sensor reset:', error);
    return {
      success: false,
      message: 'Unable to verify reset status',
      error: error.message
    };
  }
}

// Safety-focused reset function with confirmation
async function safeFireSensorReset(partialUid, confirmationCallback) {
  try {
    // Safety confirmation
    if (confirmationCallback) {
      const confirmed = await confirmationCallback();
      if (!confirmed) {
        console.log('Fire sensor reset cancelled by user');
        return { success: false, message: 'Reset cancelled - safety confirmation failed' };
      }
    }
    
    // Get pre-reset status
    console.log('Checking system status before fire sensor reset...');
    const preResetStatus = await getSystemStatus(partialUid);
    const preResetAlarms = preResetStatus.zones.filter(z => z.alarm && z.enabled);
    
    console.log(`Found ${preResetAlarms.length} active fire alarms before reset`);
    
    if (preResetAlarms.length === 0) {
      console.log('No active fire alarms detected - reset may not be necessary');
    }
    
    // Perform reset
    const resetResult = await resetFireSensors(partialUid);
    
    // Additional verification delay for safety
    console.log('Waiting for system stabilization...');
    await new Promise(resolve => setTimeout(resolve, 5000));
    
    // Final verification
    const verification = await verifyFireReset(partialUid);
    
    return {
      success: resetResult.length > 0 && verification.success,
      preResetAlarms: preResetAlarms.length,
      postResetAlarms: verification.activeAlarms || 0,
      resetResponses: resetResult,
      verification: verification,
      message: verification.message
    };
    
  } catch (error) {
    console.error('Safe fire sensor reset failed:', error);
    throw error;
  }
}

// Monitor fire sensors after reset
async function monitorPostFireReset(partialUid, durationMinutes = 10) {
  console.log(`Starting ${durationMinutes}-minute post-reset monitoring...`);
  
  let monitorCount = 0;
  const maxMonitors = durationMinutes * 2; // Check every 30 seconds
  
  const monitor = setInterval(async () => {
    try {
      monitorCount++;
      
      const status = await getSystemStatus(partialUid);
      const fireAlarms = status.zones.filter(z => z.alarm && z.enabled);
      
      if (fireAlarms.length > 0) {
        console.log(`⚠️  Fire alarm detected during post-reset monitoring: ${fireAlarms.length} zones`);
        fireAlarms.forEach(zone => {
          console.log(`   Zone ${zone.zone}: ${zone.status}`);
        });
      } else {
        console.log(`✓ Post-reset monitor ${monitorCount}/${maxMonitors}: All fire sensors normal`);
      }
      
      if (monitorCount >= maxMonitors) {
        clearInterval(monitor);
        console.log('Post-fire-reset monitoring completed');
      }
      
    } catch (error) {
      console.error('Post-reset monitoring error:', error);
    }
  }, 30000); // Check every 30 seconds
  
  return monitor;
}

// Usage examples with safety considerations
async function demonstrateFireSensorReset(partialUid) {
  try {
    console.log('=== Fire Sensor Reset Demonstration ===');
    console.log('⚠️  SAFETY WARNING: Only reset fire sensors after confirming no fire condition exists');
    
    // Safety confirmation callback example
    const safetyConfirmation = async () => {
      console.log('Safety Checklist:');
      console.log('1. Fire condition investigated and resolved? [Required]');
      console.log('2. Fire department notified if required? [Required]');
      console.log('3. Authorized personnel performing reset? [Required]');
      
      // In real implementation, this would be interactive confirmation
      // For demo purposes, we'll assume safety conditions are met
      return true;
    };
    
    // Perform safe fire sensor reset
    const resetResult = await safeFireSensorReset(partialUid, safetyConfirmation);
    
    if (resetResult.success) {
      console.log('✓ Fire sensor reset completed successfully');
      console.log(`Pre-reset alarms: ${resetResult.preResetAlarms}`);
      console.log(`Post-reset alarms: ${resetResult.postResetAlarms}`);
      
      // Start post-reset monitoring
      const monitorId = await monitorPostFireReset(partialUid, 5); // 5 minutes
      
      // Stop monitoring after demonstration
      setTimeout(() => {
        clearInterval(monitorId);
        console.log('Fire sensor reset demonstration completed');
      }, 5 * 60 * 1000);
      
    } else {
      console.log('❌ Fire sensor reset failed or incomplete');
      console.log('Reason:', resetResult.message);
    }
    
  } catch (error) {
    console.error('Fire sensor reset demonstration failed:', error);
  }
}

0x0011 - Get Users

Retrieves the complete user list from GV17 and WP17 devices. Users are sent sequentially in multiple packets due to data size limitations. This command supports selective parameter requests to optimize data transfer.

Request/Response format

Request

[HEADER][0x0011][XXXXYY] → 0701000000000000001100024300

Where:

  • XXXX = Requested user parameters (2-byte hex value)
  • YY = Initial packet number (1-byte hex value, typically 00 to start from beginning)

Response Format

First Packet (Packet 0):

[HEADER][0x0011][00XXXXRRFAAA][ASCII users data]

Subsequent Packets:

[HEADER][0x0011][YY][ASCII users data]

Where:

  • 00 = Packet 0 identifier
  • YY = Current packet number (01, 02, 03...)
  • RR = Total number of packets (hex) - only in packet 0
  • F = Internal language encoding (hex) - only in packet 0
  • AAA = Maximum packet length (fixed to 5B0 = 1456 bytes) - only in packet 0

Parameter Request Bits (XXXX)

BitParameterDescription
15Reserved-
14Reserved-
13Reserved-
12Reserved-
11PIN CodeUser PIN code
10Work ModeWorking status (1/0)
9EmailUser email address
8Counter CurrentCurrent counter value (hex up to FF)
7Counter SetCounter set value (hex up to FF)
6Output PermissionsOutput control permissions (hex bits)
5Valid TillValid until timestamp
4Valid FromValid from timestamp
3Scheduler IDAssigned scheduler (0=disabled, 1-A)
2CheckboxesEnable flags (hex bits)
1Phone/RFIDPhone number or RFID tag
0NameUser name (LSB)

Common Parameter Combinations

Use CaseBitsHex ValueDescription
Basic Info0,10x0003"000300"Name and phone/RFID
Access Control0,1,60x0043"004300"Name, phone, permissions
Full Profile0,1,6,90x0243"024300"Name, phone, permissions, email
Time Restricted0,1,4,5,60x0073"007300"Basic info with time validity
Complete DataAll bits0x0FFF"0FFF00"All available user parameters

User Data Format

Data Structure:

UserID,Param1,Param2,Param3...|UserID,Param1,Param2,Param3...
  • Users separated by pipe (|) symbol
  • Parameters separated by comma (,)
  • User ID is always first parameter
  • Parameters order matches bit order (LSB to MSB)

User ID Ranges

RangeUser TypeDescription
1-7AdministratorsFull system access
10Not AuthorizedSpecial unauthorized user
11-1010Regular UsersStandard user accounts

Output Permissions Format

Hex value parsed in bits for output control:

  • Bit 0: Output 1 control
  • Bit 1: Output 2 control
  • Bit 2: Output 3 control (relay)

Checkbox Flags Format

Single hex symbol with bit flags:

  • Bit 0: Enable user
  • Bit 1: Valid from enabled
  • Bit 2: Valid till enabled
  • Bit 3: Enable counter

Time Format

Time values in seconds as hex, calculated from March 1, 2016:

  • 0 seconds = 2016-03-01 00:00:00
  • Example: 0x5A4EC400 = specific date/time after base

Packet Sequence Behavior

Important Notes:

  • After sending packet 0 request, device automatically sends all packets
  • All packets sent sequentially without further requests
  • Maximum packet size: 1456 bytes (1448 data + 8 header)
Usage Example
javascript
async function getUsersList(partialUid, requestedParams = 0x0243, startPacket = 0) {
  // Build users list request
  const header = "070100000000000000";
  const command = "1100";
  
  // Convert parameters to 4-digit hex string
  const paramsHex = requestedParams.toString(16).padStart(4, '0').toUpperCase();
  const packetHex = startPacket.toString(16).padStart(2, '0').toUpperCase();
  const requestData = paramsHex + packetHex;
  const requestDataHex = asciiToHex(requestData);
  
  const fullCommand = header + command + requestDataHex;
  
  try {
    console.log(`Requesting users list with parameters 0x${paramsHex}...`);
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      // Parse all received packets
      let firstPacketParams;
      const packets = result.data.map((data, index) => {
        const responseText = hexToAscii(data);
        if (index === 0) {
          const firstPacket = parseUsersPacket(responseText, index);
          firstPacketParams = firstPacket.requestedParams;
          return firstPacket;
        } else {
          return parseUsersPacket(responseText, index, firstPacketParams);
        }
      });
      
      // Combine all user data from packets
      const allUsers = combineUserPackets(packets);
      
      console.log(`Users list retrieved: ${allUsers.length} users from ${packets.length} packets`);
      return {
        users: allUsers,
        packets: packets,
        requestedParams: requestedParams,
        totalPackets: packets.length > 0 ? packets[0].totalPackets : 0
      };
    }
    
    throw new Error('No users data received');
  } catch (error) {
    console.error('Failed to get users list:', error);
    throw error;
  }
}

function parseUsersPacket(responseText, packetIndex, firstPacketParams) {
  let packetNum, requestedParams, totalPackets, language, maxLength;
  let usersDataStart = 0;
  
  if (packetIndex === 0) {
    // First packet: 00XXXXRRFAAA
    packetNum = parseInt(responseText.substring(0, 2), 16);
    requestedParams = parseInt(responseText.substring(2, 6), 16);
    totalPackets = parseInt(responseText.substring(6, 8), 16);
    language = parseInt(responseText.substring(8, 9), 16);
    maxLength = parseInt(responseText.substring(9, 12), 16);
    usersDataStart = 12;
  } else {
    // Subsequent packets: PP
    packetNum = parseInt(responseText.substring(0, 2), 16);
    requestedParams = firstPacketParams; // Use params from first packet
    usersDataStart = 2;
  }
  
  const usersData = responseText.substring(usersDataStart);
  const users = parseUsersData(usersData, requestedParams);
  
  return {
    packetNumber: packetNum,
    requestedParams: requestedParams,
    totalPackets: totalPackets || null,
    language: language || null,
    maxLength: maxLength || null,
    users: users,
    rawData: usersData
  };
}

function parseUsersData(usersData, requestedParams) {
  if (!usersData || usersData.trim() === '') {
    return [];
  }
  
  const userStrings = usersData.split('|');
  const users = [];
  
  userStrings.forEach(userString => {
    if (userString.trim()) {
      const user = parseUserString(userString, requestedParams);
      if (user) {
        users.push(user);
      }
    }
  });
  
  return users;
}

function parseUserString(userString, requestedParams) {
  const parts = userString.split(',');
  if (parts.length === 0) return null;
  
  const user = {
    id: parseInt(parts[0]),
    type: getUserType(parseInt(parts[0]))
  };
  
  let paramIndex = 1;
  
  // Parse parameters based on requested bits (LSB first)
  for (let bit = 0; bit < 16; bit++) {
    if (requestedParams & (1 << bit)) {
      if (paramIndex < parts.length) {
        const value = parts[paramIndex];
        
        switch (bit) {
          case 0: user.name = value; break;
          case 1: user.phone = value; break;
          case 2: user.checkboxes = parseCheckboxes(value); break;
          case 3: user.schedulerId = parseInt(value, 16); break;
          case 4: user.validFrom = parseTimeValue(value); break;
          case 5: user.validTill = parseTimeValue(value); break;
          case 6: user.outputPermissions = parseOutputPermissions(value); break;
          case 7: user.counterSet = parseInt(value, 16); break;
          case 8: user.counterCurrent = parseInt(value, 16); break;
          case 9: user.email = value; break;
          case 10: user.workMode = parseInt(value) === 1; break;
          case 11: user.pinCode = value; break;
          default: user[`param${bit}`] = value; break;
        }
        
        paramIndex++;
      }
    }
  }
  
  return user;
}

function getUserType(userId) {
  if (userId >= 1 && userId <= 7) return 'Administrator';
  if (userId === 10) return 'Not Authorized';
  if (userId >= 11 && userId <= 1010) return 'Regular User';
  return 'Unknown';
}

function parseCheckboxes(hexValue) {
  const value = parseInt(hexValue, 16);
  return {
    enabled: !!(value & 0x01),
    validFromEnabled: !!(value & 0x02),
    validTillEnabled: !!(value & 0x04),
    counterEnabled: !!(value & 0x08)
  };
}

function parseOutputPermissions(hexValue) {
  const value = parseInt(hexValue, 16);
  return {
    output1: !!(value & 0x01),
    output2: !!(value & 0x02),
    output3: !!(value & 0x04)
  };
}

function parseTimeValue(hexValue) {
  try {
    const seconds = parseInt(hexValue, 16);
    const baseDate = new Date('2016-03-01T00:00:00Z');
    const resultDate = new Date(baseDate.getTime() + (seconds * 1000));
    return {
      timestamp: seconds,
      date: resultDate.toISOString(),
      readable: resultDate.toLocaleString()
    };
  } catch (error) {
    return { timestamp: hexValue, error: 'Invalid time format' };
  }
}

function combineUserPackets(packets) {
  const allUsers = [];
  
  packets.forEach(packet => {
    if (packet.users) {
      allUsers.push(...packet.users);
    }
  });
  
  // Remove duplicates and sort by user ID
  const uniqueUsers = allUsers.filter((user, index, array) => 
    array.findIndex(u => u.id === user.id) === index
  );
  
  return uniqueUsers.sort((a, b) => a.id - b.id);
}

// Convenience functions for common requests
async function getBasicUsersList(partialUid) {
  // Request name and phone/RFID only (bits 0,1 = 0x0003)
  return await getUsersList(partialUid, 0x0003);
}

async function getFullUsersList(partialUid) {
  // Request name, phone, permissions, email (bits 0,1,6,9 = 0x0243)
  return await getUsersList(partialUid, 0x0243);
}

async function getAdministratorsList(partialUid) {
  const allUsers = await getFullUsersList(partialUid);
  return allUsers.users.filter(user => user.type === 'Administrator');
}

async function getActiveUsersList(partialUid) {
  // Request full data including work mode and checkboxes
  const allUsers = await getUsersList(partialUid, 0x0643); // Include bits 2,10
  return allUsers.users.filter(user => user.workMode && user.checkboxes?.enabled);
}

// Usage examples
async function demonstrateUsersListRetrieval(partialUid) {
  try {
    console.log('=== Users List Retrieval Demonstration ===');
    
    // Get basic users list
    console.log('\n--- Basic Users List ---');
    const basicUsers = await getBasicUsersList(partialUid);
    console.log(`Retrieved ${basicUsers.users.length} users (basic info)`);
    basicUsers.users.slice(0, 3).forEach(user => {
      console.log(`User ${user.id} (${user.type}): ${user.name} - ${user.phone}`);
    });
    
    // Get full users list
    console.log('\n--- Full Users List ---');
    const fullUsers = await getFullUsersList(partialUid);
    console.log(`Retrieved ${fullUsers.users.length} users (full info)`);
    console.log(`Total packets received: ${fullUsers.totalPackets}`);
    
    // Display administrators
    const admins = await getAdministratorsList(partialUid);
    console.log(`\nAdministrators: ${admins.length}`);
    admins.forEach(admin => {
      console.log(`Admin ${admin.id}: ${admin.name} - ${admin.email || 'No email'}`);
    });
    
    // Display active users
    const activeUsers = await getActiveUsersList(partialUid);
    console.log(`\nActive Users: ${activeUsers.users.length}`);
    
    console.log('Users list retrieval demonstration completed');
    
  } catch (error) {
    console.error('Users list demonstration failed:', error);
  }
}

0x0012 - Write Users

Writes user data to GV17 and WP17 devices. This command allows adding, editing, or deleting users by sending user information in the same format as 0x0011 Get Users List.

Request/Response format

Request

[HEADER][0x0012][YYXXXX][Users data] → 0701000000000000001200[YYXXXX][Users data]

Where:

  • YY = Current packet number (2-char hex string)
  • XXXX = Sent data parameters (2-byte hex value as 4-char hex string, LSB format)
  • [Users data] = User information in same format as 0x0011 command

Parameter Construction:

  • Take 2-byte hex value for parameters (e.g., 0x0243)
  • Convert to 4-character hex string ("0243")
  • Prepend 2-character packet number ("00")
  • Result: "000243" → ASCII hex encoded

Example Request:

javascript
// Write user with name, phone, permissions, email (0x0243)
// Packet 00, User ID 15: "John,+1234567890,5,john@example.com"
{
  "command": "0x0012000000000000120036303032343315,John,+1234567890,5,john@example.com"
}

Response Format

[HEADER][0x0012][OK]     // Successful write
[HEADER][0x0012][ERROR]  // Write failed

User Data Format

Uses identical format to 0x0011 Get Users List:

Single User:

UserID,Param1,Param2,Param3...

Multiple Users:

UserID1,Param1,Param2|UserID2,Param1,Param2|UserID3,Param1,Param2

User Deletion:

UserID,,,  // Empty data with only commas

Parameter Bits (XXXX)

Uses the same parameter bit mapping as 0x0011 Get Users List:

BitParameterDescription
11PIN CodeUser PIN code
10Work ModeWorking status (1/0)
9EmailUser email address
8Counter CurrentCurrent counter value (hex up to FF)
7Counter SetCounter set value (hex up to FF)
6Output PermissionsOutput control permissions (hex bits)
5Valid TillValid until timestamp
4Valid FromValid from timestamp
3Scheduler IDAssigned scheduler (0=disabled, 1-A)
2CheckboxesEnable flags (hex bits)
1Phone/RFIDPhone number or RFID tag
0NameUser name (LSB)

User Operations

OperationData FormatDescription
Add UserUserID,Param1,Param2...Create new user with specified parameters
Edit UserUserID,Param1,Param2...Update existing user parameters
Delete UserUserID,,,Delete user (empty data with commas)
Multi-UserUser1|User2|User3Multiple operations separated by pipes

Response Handling

Success Response (OK):

  • User data saved successfully
  • User sequence numbers match device list
  • All parameters validated correctly

Error Response (ERROR):

  • User sequence number not found
  • Parameter validation failed
  • Data format incorrect
  • Device storage error

User ID Validation

Must use valid user ID ranges from 0x0011 Get Users List:

  • 1-7: Administrators
  • 10: Not Authorized user
  • 11-1010: Regular users

Data Validation Rules

Parameter Order: Must match requested bits order (LSB to MSB) Required Fields: User ID always required as first parameter Data Types: Each parameter must match expected format for its bit Permissions: Output permissions must be valid hex bit flags Time Values: Must use seconds from March 1, 2016 base

Usage Example
javascript
async function writeUsersList(partialUid, users, parameters = 0x0243, packetNumber = 0) {
  // Build users write command
  const header = "070100000000000000";
  const command = "1200";
  
  // Format packet header: YYXXXX
  const packetHex = packetNumber.toString(16).padStart(2, '0').toUpperCase();
  const paramsHex = parameters.toString(16).padStart(4, '0').toUpperCase();
  const headerData = packetHex + paramsHex;
  
  // Format users data
  const usersData = formatUsersData(users, parameters);
  const fullData = headerData + usersData;
  const dataHex = asciiToHex(fullData);
  
  const fullCommand = header + command + dataHex;
  
  try {
    console.log(`Writing ${users.length} users with parameters 0x${paramsHex}...`);
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      
      if (responseText === 'OK') {
        console.log('Users write successful');
        return { success: true, message: 'Users saved successfully' };
      } else if (responseText === 'ERROR') {
        console.log('Users write failed');
        return { success: false, message: 'Device returned ERROR' };
      } else {
        return { success: false, message: `Unexpected response: ${responseText}` };
      }
    }
    
    throw new Error('No response received');
  } catch (error) {
    console.error('Failed to write users list:', error);
    throw error;
  }
}

function formatUsersData(users, parameters) {
  return users.map(user => formatSingleUser(user, parameters)).join('|');
}

function formatSingleUser(user, parameters) {
  const parts = [user.id.toString()];
  
  // Add parameters based on requested bits (LSB first)
  for (let bit = 0; bit < 16; bit++) {
    if (parameters & (1 << bit)) {
      let value = '';
      
      switch (bit) {
        case 0: value = user.name || ''; break;
        case 1: value = user.phone || ''; break;
        case 2: value = formatCheckboxes(user.checkboxes); break;
        case 3: value = (user.schedulerId || 0).toString(16).toUpperCase(); break;
        case 4: value = formatTimeValue(user.validFrom); break;
        case 5: value = formatTimeValue(user.validTill); break;
        case 6: value = formatOutputPermissions(user.outputPermissions); break;
        case 7: value = (user.counterSet || 0).toString(16).padStart(2, '0').toUpperCase(); break;
        case 8: value = (user.counterCurrent || 0).toString(16).padStart(2, '0').toUpperCase(); break;
        case 9: value = user.email || ''; break;
        case 10: value = user.workMode ? '1' : '0'; break;
        case 11: value = user.pinCode || ''; break;
        default: value = user[`param${bit}`] || ''; break;
      }
      
      parts.push(value);
    }
  }
  
  return parts.join(',');
}

function formatCheckboxes(checkboxes) {
  if (!checkboxes) return '0';
  
  let value = 0;
  if (checkboxes.enabled) value |= 0x01;
  if (checkboxes.validFromEnabled) value |= 0x02;
  if (checkboxes.validTillEnabled) value |= 0x04;
  if (checkboxes.counterEnabled) value |= 0x08;
  
  return value.toString(16).toUpperCase();
}

function formatOutputPermissions(permissions) {
  if (!permissions) return '0';
  
  let value = 0;
  if (permissions.output1) value |= 0x01;
  if (permissions.output2) value |= 0x02;
  if (permissions.output3) value |= 0x04;
  
  return value.toString(16).toUpperCase();
}

function formatTimeValue(timeData) {
  if (!timeData) return '0';
  
  if (typeof timeData === 'number') {
    return timeData.toString(16).toUpperCase();
  }
  
  if (timeData.timestamp) {
    return timeData.timestamp.toString(16).toUpperCase();
  }
  
  // If it's a Date object or date string, convert to timestamp
  try {
    const baseDate = new Date('2016-03-01T00:00:00Z');
    const targetDate = new Date(timeData);
    const seconds = Math.floor((targetDate.getTime() - baseDate.getTime()) / 1000);
    return Math.max(0, seconds).toString(16).toUpperCase();
  } catch (error) {
    return '0';
  }
}

// Convenience functions for common operations
async function addUser(partialUid, user, parameters = 0x0243) {
  return await writeUsersList(partialUid, [user], parameters, 0);
}

async function editUser(partialUid, user, parameters = 0x0243) {
  return await writeUsersList(partialUid, [user], parameters, 0);
}

async function deleteUser(partialUid, userId) {
  // Create delete user with empty data (only commas)
  const deleteUser = {
    id: userId,
    name: '',
    phone: '',
    email: '',
    // All other fields empty
  };
  
  return await writeUsersList(partialUid, [deleteUser], 0x0243, 0);
}

async function addMultipleUsers(partialUid, users, parameters = 0x0243) {
  // Split into packets if too many users
  const maxUsersPerPacket = 10; // Adjust based on data size
  const results = [];
  
  for (let i = 0; i < users.length; i += maxUsersPerPacket) {
    const userBatch = users.slice(i, i + maxUsersPerPacket);
    const packetNumber = Math.floor(i / maxUsersPerPacket);
    
    const result = await writeUsersList(partialUid, userBatch, parameters, packetNumber);
    results.push(result);
    
    if (!result.success) {
      console.error(`Batch ${packetNumber + 1} failed:`, result.message);
      break;
    }
    
    // Small delay between packets
    await new Promise(resolve => setTimeout(resolve, 500));
  }
  
  return {
    success: results.every(r => r.success),
    results: results,
    totalUsers: users.length
  };
}

// User management functions
async function createBasicUser(partialUid, id, name, phone, email = '') {
  const user = {
    id: id,
    name: name,
    phone: phone,
    email: email,
    outputPermissions: { output1: true, output2: false, output3: false },
    checkboxes: { enabled: true, validFromEnabled: false, validTillEnabled: false, counterEnabled: false },
    workMode: true
  };
  
  return await addUser(partialUid, user, 0x0643); // Include work mode and checkboxes
}

async function createAdministrator(partialUid, id, name, phone, email, pinCode) {
  if (id < 1 || id > 7) {
    throw new Error('Administrator ID must be between 1 and 7');
  }
  
  const admin = {
    id: id,
    name: name,
    phone: phone,
    email: email,
    pinCode: pinCode,
    outputPermissions: { output1: true, output2: true, output3: true },
    checkboxes: { enabled: true, validFromEnabled: false, validTillEnabled: false, counterEnabled: false },
    workMode: true
  };
  
  return await addUser(partialUid, admin, 0x0E43); // Include PIN code
}

async function updateUserPermissions(partialUid, userId, permissions) {
  const user = {
    id: userId,
    outputPermissions: permissions
  };
  
  return await editUser(partialUid, user, 0x0040); // Only output permissions bit
}

// Usage examples
async function demonstrateUsersWrite(partialUid) {
  try {
    console.log('=== Users Write Operations Demonstration ===');
    
    // Create a basic user
    console.log('\n--- Creating Basic User ---');
    const createResult = await createBasicUser(partialUid, 50, 'John Doe', '+1234567890', 'john@example.com');
    console.log('Create result:', createResult);
    
    // Edit user permissions
    console.log('\n--- Updating User Permissions ---');
    const newPermissions = { output1: true, output2: true, output3: false };
    const editResult = await updateUserPermissions(partialUid, 50, newPermissions);
    console.log('Edit result:', editResult);
    
    // Create multiple users
    console.log('\n--- Creating Multiple Users ---');
    const newUsers = [
      { id: 51, name: 'Jane Smith', phone: '+1987654321', email: 'jane@example.com' },
      { id: 52, name: 'Bob Wilson', phone: '+1555123456', email: 'bob@example.com' }
    ];
    
    const multiResult = await addMultipleUsers(partialUid, newUsers.map(u => ({
      ...u,
      outputPermissions: { output1: true, output2: false, output3: false },
      checkboxes: { enabled: true, validFromEnabled: false, validTillEnabled: false, counterEnabled: false },
      workMode: true
    })));
    
    console.log('Multiple users result:', multiResult);
    
    // Create administrator
    console.log('\n--- Creating Administrator ---');
    const adminResult = await createAdministrator(partialUid, 2, 'Admin User', '+1800555000', 'admin@example.com', '9999');
    console.log('Administrator result:', adminResult);
    
    // Delete user
    console.log('\n--- Deleting User ---');
    const deleteResult = await deleteUser(partialUid, 52);
    console.log('Delete result:', deleteResult);
    
    console.log('Users write demonstration completed');
    
  } catch (error) {
    console.error('Users write demonstration failed:', error);
  }
}

0x0013 - Add/Remove Users

Command to add or remove users by name, email, and number. When the full list of users in the device is not known, this command adds only the user name and number to the first free slot in the list. Added users will be able to control all outputs. In order of priority, the user is first added to the list of administrators (if not full), otherwise to the list of users. The - sign means that if the user is found according to at least one parameter, he will be deleted from the list.

Request/Response format

Request

[HEADER][0x0013][ASCII user data] → 0701000000000000001300[ASCII user data]

User Data Format

Cloud application sends ASCII text with the following format:

  • First character: + (add) or - (remove)
  • Parameters separated by commas: email,number,name
  • Multiple users separated by pipe character |
  • Each user group must start with + or -
+email,number,name|-,email,number,name|+email2,number2,name2

Request Examples

Single User Addition:

[HEADER][0x0013][+john@john.com,+12345678,Jonathan] 
→ 0701000000000000001300+john@john.com,+12345678,Jonathan

Single User Removal:

[HEADER][0x0013][-john@john.com,+12345678,Jonathan]
→ 0701000000000000001300-john@john.com,+12345678,Jonathan

Multiple Users (Mixed Operations):

[HEADER][0x0013][+john@john.com,+12345678,Jonathan|-jane@example.com,+87654321,Jane]
→ 0701000000000000001300+john@john.com,+12345678,Jonathan|-jane@example.com,+87654321,Jane

Response Format

Success Response:

[HEADER][0x0013][OK]

Error Response:

[HEADER][0x0013][ERROR]

Operation Types

OperationSymbolDescription
Add User+Adds user to first available slot
Remove User-Removes user matching any parameter

Parameter Format

PositionParameterDescriptionRequired
1EmailUser email addressYes
2NumberPhone number with + prefixYes
3NameUser display nameYes

Important Notes

  • Comma Count: Maintain appropriate number of commas even for empty parameters
  • Parameter Matching: For removal, user is deleted if ANY parameter matches
  • Priority Order: Users added to administrators list first, then regular users
  • Permissions: Added users automatically get all output control permissions

User Removal Logic

When using - operation, the user will be removed if any of the provided parameters match:

  • Email matches existing user
  • Phone number matches existing user
  • Name matches existing user

Response Handling

Success Response (OK):

  • User successfully added to available slot
  • User successfully removed from list
  • All parameters processed correctly

Error Response (ERROR):

  • No available slots for new user
  • Parameter format incorrect
  • Device storage error
  • User not found (for removal)

Cross-References

Related commands for user management:

Usage Example
javascript
async function addUserToList(partialUid, email, phoneNumber, name) {
  // Build add user command
  const header = "070100000000000000";
  const command = "1300";
  
  // Format user data: +email,number,name
  const userData = `+${email},${phoneNumber},${name}`;
  const dataHex = asciiToHex(userData);
  
  const fullCommand = header + command + dataHex;
  
  try {
    console.log(`Adding user: ${name} (${email}, ${phoneNumber})`);
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      
      if (responseText === 'OK') {
        console.log('User added successfully');
        return { 
          success: true, 
          message: 'User added to device list',
          user: { email, phoneNumber, name }
        };
      } else if (responseText === 'ERROR') {
        console.log('Failed to add user');
        return { 
          success: false, 
          message: 'Device returned ERROR - no available slots or invalid data'
        };
      } else {
        return { 
          success: false, 
          message: `Unexpected response: ${responseText}` 
        };
      }
    }
    
    throw new Error('No response received from device');
  } catch (error) {
    console.error('Failed to add user:', error);
    throw error;
  }
}

async function removeUserFromList(partialUid, email = '', phoneNumber = '', name = '') {
  // Build remove user command
  const header = "070100000000000000";
  const command = "1300";
  
  // Format user data: -email,number,name (any parameter can match)
  const userData = `-${email},${phoneNumber},${name}`;
  const dataHex = asciiToHex(userData);
  
  const fullCommand = header + command + dataHex;
  
  try {
    console.log(`Removing user matching: email="${email}", phone="${phoneNumber}", name="${name}"`);
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      
      if (responseText === 'OK') {
        console.log('User removed successfully');
        return { 
          success: true, 
          message: 'User removed from device list',
          matchedBy: { email, phoneNumber, name }
        };
      } else if (responseText === 'ERROR') {
        console.log('Failed to remove user');
        return { 
          success: false, 
          message: 'Device returned ERROR - user not found or invalid data'
        };
      } else {
        return { 
          success: false, 
          message: `Unexpected response: ${responseText}` 
        };
      }
    }
    
    throw new Error('No response received from device');
  } catch (error) {
    console.error('Failed to remove user:', error);
    throw error;
  }
}

async function addMultipleUsers(partialUid, users) {
  // Build multiple users command
  const header = "070100000000000000";
  const command = "1300";
  
  // Format multiple users: +email1,num1,name1|+email2,num2,name2|-email3,num3,name3
  const userData = users.map(user => {
    const operation = user.operation || '+'; // Default to add
    return `${operation}${user.email},${user.phoneNumber},${user.name}`;
  }).join('|');
  
  const dataHex = asciiToHex(userData);
  const fullCommand = header + command + dataHex;
  
  try {
    console.log(`Processing ${users.length} user operations`);
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      
      if (responseText === 'OK') {
        console.log('All user operations completed successfully');
        return { 
          success: true, 
          message: 'All user operations processed successfully',
          processedUsers: users.length
        };
      } else if (responseText === 'ERROR') {
        console.log('Some user operations failed');
        return { 
          success: false, 
          message: 'Device returned ERROR - some operations may have failed'
        };
      } else {
        return { 
          success: false, 
          message: `Unexpected response: ${responseText}` 
        };
      }
    }
    
    throw new Error('No response received from device');
  } catch (error) {
    console.error('Failed to process multiple users:', error);
    throw error;
  }
}

// Helper function to validate user data format
function validateUserData(email, phoneNumber, name) {
  const errors = [];
  
  if (!email || !email.includes('@')) {
    errors.push('Valid email address is required');
  }
  
  if (!phoneNumber || !phoneNumber.startsWith('+')) {
    errors.push('Phone number must start with + prefix');
  }
  
  if (!name || name.trim().length === 0) {
    errors.push('User name is required');
  }
  
  // Check for comma characters in parameters (would break format)
  if (email.includes(',') || phoneNumber.includes(',') || name.includes(',')) {
    errors.push('Parameters cannot contain comma characters');
  }
  
  // Check for pipe characters in parameters (would break multi-user format)
  if (email.includes('|') || phoneNumber.includes('|') || name.includes('|')) {
    errors.push('Parameters cannot contain pipe characters');
  }
  
  return {
    valid: errors.length === 0,
    errors: errors
  };
}

// Convenience functions for common operations
async function quickAddUser(partialUid, email, phoneNumber, name) {
  const validation = validateUserData(email, phoneNumber, name);
  if (!validation.valid) {
    throw new Error(`Invalid user data: ${validation.errors.join(', ')}`);
  }
  
  return await addUserToList(partialUid, email, phoneNumber, name);
}

async function quickRemoveUserByEmail(partialUid, email) {
  return await removeUserFromList(partialUid, email, '', '');
}

async function quickRemoveUserByPhone(partialUid, phoneNumber) {
  return await removeUserFromList(partialUid, '', phoneNumber, '');
}

async function quickRemoveUserByName(partialUid, name) {
  return await removeUserFromList(partialUid, '', '', name);
}

async function bulkAddUsers(partialUid, userList) {
  // Add operation to all users
  const usersWithOperation = userList.map(user => ({
    ...user,
    operation: '+'
  }));
  
  return await addMultipleUsers(partialUid, usersWithOperation);
}

async function bulkRemoveUsers(partialUid, userList) {
  // Remove operation to all users
  const usersWithOperation = userList.map(user => ({
    ...user,
    operation: '-'
  }));
  
  return await addMultipleUsers(partialUid, usersWithOperation);
}

// Usage demonstration
async function demonstrateUserManagement(partialUid) {
  try {
    console.log('=== User Management Demonstration ===');
    
    // Add single user
    console.log('\n--- Adding Single User ---');
    const addResult = await quickAddUser(
      partialUid, 
      'john@example.com', 
      '+1234567890', 
      'John Doe'
    );
    console.log('Add result:', addResult);
    
    // Add multiple users
    console.log('\n--- Adding Multiple Users ---');
    const multipleUsers = [
      { email: 'jane@example.com', phoneNumber: '+1987654321', name: 'Jane Smith' },
      { email: 'bob@example.com', phoneNumber: '+1555123456', name: 'Bob Wilson' }
    ];
    
    const bulkAddResult = await bulkAddUsers(partialUid, multipleUsers);
    console.log('Bulk add result:', bulkAddResult);
    
    // Mixed operations
    console.log('\n--- Mixed Add/Remove Operations ---');
    const mixedOperations = [
      { email: 'alice@example.com', phoneNumber: '+1333444555', name: 'Alice Johnson', operation: '+' },
      { email: 'john@example.com', phoneNumber: '+1234567890', name: 'John Doe', operation: '-' }
    ];
    
    const mixedResult = await addMultipleUsers(partialUid, mixedOperations);
    console.log('Mixed operations result:', mixedResult);
    
    // Remove user by different criteria
    console.log('\n--- Removing Users ---');
    
    // Remove by email
    const removeByEmailResult = await quickRemoveUserByEmail(partialUid, 'jane@example.com');
    console.log('Remove by email result:', removeByEmailResult);
    
    // Remove by phone number
    const removeByPhoneResult = await quickRemoveUserByPhone(partialUid, '+1555123456');
    console.log('Remove by phone result:', removeByPhoneResult);
    
    console.log('User management demonstration completed');
    
  } catch (error) {
    console.error('User management demonstration failed:', error);
  }
}

0x0014 - Get Users

Reads a list of users from CG17/SP3 based devices.

Request/Response format

Request

[HEADER][0x0014] → 070100000000000000140

Response Format

Success Response:

[HEADER][0x0014][ASCII users data]

Error Response:

[HEADER][0x0014][ERROR]

Response Data Fields

The response data is returned as ASCII text containing user information.

Device Compatibility

This command is specifically designed for CG17/SP3 based devices.

Cross-References

Related commands for user management:

Usage Example
javascript
async function getUsersFromDevice(partialUid) {
  // Build get users command for CG17/SP3 devices
  const header = "070100000000000000";
  const command = "1400";
  
  const fullCommand = header + command;
  
  try {
    console.log('Requesting users list from CG17/SP3 device...');
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      
      if (responseText === 'ERROR') {
        console.log('Failed to retrieve users');
        return { 
          success: false, 
          message: 'Device returned ERROR - unable to retrieve users'
        };
      } else {
        console.log('Users list retrieved successfully');
        return {
          success: true,
          message: 'Users retrieved successfully',
          usersData: responseText,
          rawResponse: result.data[0]
        };
      }
    }
    
    throw new Error('No response received from device');
  } catch (error) {
    console.error('Failed to get users from device:', error);
    throw error;
  }
}

// Usage demonstration
async function demonstrateGetUsers(partialUid) {
  try {
    console.log('=== Get Users Demonstration (CG17/SP3) ===');
    
    const result = await getUsersFromDevice(partialUid);
    
    if (result.success) {
      console.log('Users data:', result.usersData);
      console.log('Raw response:', result.rawResponse);
    } else {
      console.log('Failed to get users:', result.message);
    }
    
    console.log('Get users demonstration completed');
    
  } catch (error) {
    console.error('Get users demonstration failed:', error);
  }
}

0x0015 - Write Users

Writing user list to device for CG17/SP3 based devices.

Request/Response format

Request

[HEADER][0x0015][ASCII users data] → 0701000000000000001500[ASCII users data]

Response Format

Success Response:

[HEADER][0x0015][OK]

Error Response:

[HEADER][0x0015][ERROR]

Request Data Format

The request data should be sent as ASCII text containing user information formatted for CG17/SP3 devices.

Device Compatibility

This command is specifically designed for CG17/SP3 based devices.

Response Handling

Success Response (OK):

  • Users list successfully written to device
  • All user data processed correctly

Error Response (ERROR):

  • Invalid user data format
  • Device storage error
  • Write operation failed

Cross-References

Related commands for user management:

Usage Example
javascript
async function writeUsersToDevice(partialUid, usersData) {
  // Build write users command for CG17/SP3 devices
  const header = "070100000000000000";
  const command = "1500";
  
  // Convert users data to hex
  const dataHex = asciiToHex(usersData);
  
  const fullCommand = header + command + dataHex;
  
  try {
    console.log('Writing users list to CG17/SP3 device...');
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      
      if (responseText === 'OK') {
        console.log('Users list written successfully');
        return { 
          success: true, 
          message: 'Users list written successfully'
        };
      } else if (responseText === 'ERROR') {
        console.log('Failed to write users list');
        return { 
          success: false, 
          message: 'Device returned ERROR - write operation failed'
        };
      } else {
        return { 
          success: false, 
          message: `Unexpected response: ${responseText}` 
        };
      }
    }
    
    throw new Error('No response received from device');
  } catch (error) {
    console.error('Failed to write users to device:', error);
    throw error;
  }
}

// Usage demonstration
async function demonstrateWriteUsers(partialUid) {
  try {
    console.log('=== Write Users Demonstration (CG17/SP3) ===');
    
    // Example users data - format would depend on device specifications
    const usersData = "user1,data1|user2,data2|user3,data3";
    
    const result = await writeUsersToDevice(partialUid, usersData);
    
    if (result.success) {
      console.log('Users successfully written to device');
    } else {
      console.log('Failed to write users:', result.message);
    }
    
    console.log('Write users demonstration completed');
    
  } catch (error) {
    console.error('Write users demonstration failed:', error);
  }
}

0x0016 - Bypass Zones of Area

Controls zone bypass status for a specific area, allowing zones to be bypassed or restored to normal operation.

Request/Response format

Request

[HEADER][0x0016][N:BYP] → 0701000000000000001600[N:BYP]

Request Parameters

ParameterFormatDescription
NNumberArea number
BYPBinary stringZone bypass states (0=normal, 1=bypassed)

Request Examples

Set bypass for Area 1:

[HEADER][0x0016][1:0001100] 
→ 0701000000000000001600313A30303031313030

Set bypass for Area 2:

[HEADER][0x0016][2:1100000]
→ 0701000000000000001600323A31313030303030

Response Format

Success Response:

[HEADER][0x0016][Z:xxxxxxx]

Error Response:

[HEADER][0x0016][ERROR]

Zone Bypass States

The BYP parameter is a string of binary digits where:

  • 0 = Zone is in normal operation
  • 1 = Zone is bypassed (excluded from area monitoring)

Each position represents a zone number (position 1 = zone 1, position 2 = zone 2, etc.).

Response Data Format

The response contains the current zone states in the format Z:xxxxxxx, where each position indicates:

  • 0 = Zone normal/secure
  • 1 = Zone active/triggered
  • ? = Zone status unknown

This format matches the zone status format from 0x000D - Status.

Bypass Operation Logic

Setting Bypassed (1):

  • Zone is excluded from area monitoring
  • Zone alarms will not trigger area alarm
  • Zone remains monitored but ignored for security purposes

Setting Normal (0):

  • Zone returns to active monitoring
  • Zone alarms will trigger area alarm
  • Zone participates fully in area security

Cross-References

Related commands for zone and area management:

Usage Example
javascript
async function bypassAreaZones(partialUid, areaNumber, bypassStates) {
  // Build bypass zones command
  const header = "070100000000000000";
  const command = "1600";
  
  // Format request data: N:BYP
  const requestData = `${areaNumber}:${bypassStates}`;
  const dataHex = asciiToHex(requestData);
  
  const fullCommand = header + command + dataHex;
  
  try {
    console.log(`Setting zone bypass for area ${areaNumber}: ${bypassStates}`);
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      
      if (responseText.startsWith('Z:')) {
        const zoneStates = responseText.substring(2);
        console.log('Zone bypass set successfully, current zone status:', zoneStates);
        return { 
          success: true, 
          message: 'Zone bypass updated successfully',
          areaNumber: areaNumber,
          bypassStates: bypassStates,
          currentZoneStates: zoneStates
        };
      } else if (responseText === 'ERROR') {
        console.log('Failed to set zone bypass');
        return { 
          success: false, 
          message: 'Device returned ERROR - invalid area or bypass data'
        };
      } else {
        return { 
          success: false, 
          message: `Unexpected response: ${responseText}` 
        };
      }
    }
    
    throw new Error('No response received from device');
  } catch (error) {
    console.error('Failed to set zone bypass:', error);
    throw error;
  }
}

// Helper function to create bypass states string
function createBypassStates(zoneCount, bypassedZones = []) {
  let states = '';
  for (let i = 1; i <= zoneCount; i++) {
    states += bypassedZones.includes(i) ? '1' : '0';
  }
  return states;
}

// Helper function to parse bypass states
function parseBypassStates(bypassStates) {
  const bypassedZones = [];
  for (let i = 0; i < bypassStates.length; i++) {
    if (bypassStates[i] === '1') {
      bypassedZones.push(i + 1); // Zone numbers start from 1
    }
  }
  return bypassedZones;
}

// Convenience functions
async function bypassSpecificZones(partialUid, areaNumber, zoneCount, zonesToBypass) {
  const bypassStates = createBypassStates(zoneCount, zonesToBypass);
  return await bypassAreaZones(partialUid, areaNumber, bypassStates);
}

async function clearAllBypasses(partialUid, areaNumber, zoneCount) {
  const bypassStates = '0'.repeat(zoneCount);
  return await bypassAreaZones(partialUid, areaNumber, bypassStates);
}

async function bypassAllZones(partialUid, areaNumber, zoneCount) {
  const bypassStates = '1'.repeat(zoneCount);
  return await bypassAreaZones(partialUid, areaNumber, bypassStates);
}

// Usage demonstration
async function demonstrateZoneBypass(partialUid) {
  try {
    console.log('=== Zone Bypass Demonstration ===');
    
    const areaNumber = 1;
    const totalZones = 8;
    
    // Bypass specific zones (3 and 5)
    console.log('\n--- Bypassing Zones 3 and 5 ---');
    const bypassResult = await bypassSpecificZones(partialUid, areaNumber, totalZones, [3, 5]);
    console.log('Bypass result:', bypassResult);
    
    if (bypassResult.success) {
      console.log('Bypassed zones:', parseBypassStates(bypassResult.bypassStates));
    }
    
    // Clear specific bypass (zone 3 only)
    console.log('\n--- Bypassing Only Zone 5 ---');
    const partialBypassResult = await bypassSpecificZones(partialUid, areaNumber, totalZones, [5]);
    console.log('Partial bypass result:', partialBypassResult);
    
    // Clear all bypasses
    console.log('\n--- Clearing All Bypasses ---');
    const clearResult = await clearAllBypasses(partialUid, areaNumber, totalZones);
    console.log('Clear bypasses result:', clearResult);
    
    console.log('Zone bypass demonstration completed');
    
  } catch (error) {
    console.error('Zone bypass demonstration failed:', error);
  }
}

0x0017 - Change Object ID

Changes the object ID of the device, allowing modification of the device identifier.

Request/Response format

Request

[HEADER][0x0017][ID1:New_ID] → 0701000000000000001700[ID1:New_ID]

Request Parameters

ParameterFormatDescription
ID1KeywordFixed keyword indicating object number change
New_IDStringNew object ID to assign to device

Request Examples

Change object ID:

[HEADER][0x0017][ID1:ABC123] 
→ 0701000000000000001700493431343A414243313233

Change to numeric ID:

[HEADER][0x0017][ID1:456789]
→ 0701000000000000001700493431343A343536373839

Response Format

Success Response:

[HEADER][0x0017][ID1:New_ID]

Error Responses:

[HEADER][0x0017][ERROR]   // Wrong keyword received
[HEADER][0x0017][FORMAT]  // Faulty object ID length received

Parameter Requirements

ID1 Keyword:

  • Must be exactly ID1
  • Case-sensitive requirement
  • Indicates object number change operation

New_ID Format:

  • Object ID string format
  • Length restrictions apply (device-specific)
  • Alphanumeric characters typically supported

Error Conditions

ErrorConditionDescription
ERRORWrong keywordKeyword other than ID1 was received
FORMATInvalid ID lengthObject ID length doesn't meet device requirements

Response Handling

Success Response (ID1:New_ID):

  • Object ID successfully changed
  • Response echoes the new ID for confirmation
  • Device will use new ID for future communications

Error Response (ERROR):

  • Invalid keyword provided
  • Must use exactly ID1 as keyword

Error Response (FORMAT):

  • Object ID length is invalid
  • Check device specifications for ID length requirements

Cross-References

Related commands for device configuration:

Usage Example
javascript
async function changeObjectId(partialUid, newObjectId) {
  // Build change object ID command
  const header = "070100000000000000";
  const command = "1700";
  
  // Format request data: ID1:New_ID
  const requestData = `ID1:${newObjectId}`;
  const dataHex = asciiToHex(requestData);
  
  const fullCommand = header + command + dataHex;
  
  try {
    console.log(`Changing object ID to: ${newObjectId}`);
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      
      if (responseText.startsWith('ID1:')) {
        const confirmedId = responseText.substring(4);
        console.log('Object ID changed successfully to:', confirmedId);
        return { 
          success: true, 
          message: 'Object ID changed successfully',
          oldId: partialUid,
          newId: confirmedId,
          confirmed: confirmedId === newObjectId
        };
      } else if (responseText === 'ERROR') {
        console.log('Failed to change object ID - wrong keyword');
        return { 
          success: false, 
          message: 'Device returned ERROR - wrong keyword (must use ID1)',
          errorType: 'KEYWORD_ERROR'
        };
      } else if (responseText === 'FORMAT') {
        console.log('Failed to change object ID - invalid format');
        return { 
          success: false, 
          message: 'Device returned FORMAT - invalid object ID length',
          errorType: 'FORMAT_ERROR'
        };
      } else {
        return { 
          success: false, 
          message: `Unexpected response: ${responseText}`,
          errorType: 'UNKNOWN_RESPONSE'
        };
      }
    }
    
    throw new Error('No response received from device');
  } catch (error) {
    console.error('Failed to change object ID:', error);
    throw error;
  }
}

// Helper function to validate object ID format
function validateObjectId(objectId) {
  const errors = [];
  
  if (!objectId || objectId.trim().length === 0) {
    errors.push('Object ID cannot be empty');
  }
  
  if (objectId.length > 32) {  // Assuming max length - adjust based on device specs
    errors.push('Object ID too long (max 32 characters)');
  }
  
  if (objectId.length < 3) {  // Assuming min length - adjust based on device specs
    errors.push('Object ID too short (min 3 characters)');
  }
  
  // Check for invalid characters (adjust regex based on device requirements)
  if (!/^[A-Za-z0-9_-]+$/.test(objectId)) {
    errors.push('Object ID contains invalid characters (only alphanumeric, underscore, and dash allowed)');
  }
  
  // Check for colon character (would break format)
  if (objectId.includes(':')) {
    errors.push('Object ID cannot contain colon character');
  }
  
  return {
    valid: errors.length === 0,
    errors: errors
  };
}

// Convenience function with validation
async function changeObjectIdSafe(partialUid, newObjectId) {
  const validation = validateObjectId(newObjectId);
  if (!validation.valid) {
    throw new Error(`Invalid object ID: ${validation.errors.join(', ')}`);
  }
  
  return await changeObjectId(partialUid, newObjectId);
}

// Usage demonstration
async function demonstrateObjectIdChange(partialUid) {
  try {
    console.log('=== Object ID Change Demonstration ===');
    console.log('Current device ID:', partialUid);
    
    // Change to alphanumeric ID
    console.log('\n--- Changing to Alphanumeric ID ---');
    const newId1 = 'DEV_001';
    const result1 = await changeObjectIdSafe(partialUid, newId1);
    console.log('Change result:', result1);
    
    if (result1.success) {
      // Change to numeric ID
      console.log('\n--- Changing to Numeric ID ---');
      const newId2 = '123456';
      const result2 = await changeObjectIdSafe(result1.newId, newId2);
      console.log('Second change result:', result2);
      
      if (result2.success) {
        console.log('Final device ID:', result2.newId);
      }
    }
    
    // Demonstrate error handling with invalid ID
    console.log('\n--- Testing Invalid ID (too long) ---');
    try {
      const invalidId = 'A'.repeat(50);  // Too long ID
      await changeObjectIdSafe(partialUid, invalidId);
    } catch (error) {
      console.log('Expected validation error:', error.message);
    }
    
    console.log('Object ID change demonstration completed');
    
  } catch (error) {
    console.error('Object ID change demonstration failed:', error);
  }
}

0x0018 - Change Domain/IP Address or Port

Changes network configuration for communication channels, allowing modification of IP addresses, domain names, and port numbers, or disabling specific channels.

Request/Response format

Request

[HEADER][0x0018][kcp:a:n] → 0701000000000000001800[kcp:a:n]

Request Parameters

ParameterFormatDescription
kKeywordCommand purpose: A (change address/port), D (disable channel)
cChannelChannel selection: 1, 2, 3, C (Cloud/Protegus), A (Active)
pPriorityChannel priority: P (Primary), B (Backup)
aAddressNew IP address or domain name (when k=A)
nNumberNew port number (when k=A)

Request Examples

Change First Primary channel:

[HEADER][0x0018][A1P:195.14.1.100:1000]
→ 0701000000000000001800413150343131393535413431343531314131303031313030303030

Change Cloud Primary channel:

[HEADER][0x0018][ACP:i01.protegus.eu:55019]
→ 0701000000000000001800414350693031343470726F746567757334653735353030313939

Disable Second Backup channel:

[HEADER][0x0018][D2B]
→ 0701000000000000001800443242

Response Format

Success Response:

[HEADER][0x0018][OK]

Error Response:

[HEADER][0x0018][ERROR]

Command Keywords

KeywordPurposeParameters Required
AChange address/portChannel, Priority, Address, Port
DDisable channelChannel, Priority only

Channel Selection

CodeChannelDescription
1FirstFirst communication channel
2SecondSecond communication channel
3ThirdThird communication channel
CCloudCloud (Protegus) channel
AActiveCurrently active channel

Priority Types

CodePriorityDescription
PPrimaryPrimary channel configuration
BBackupBackup channel configuration

Parameter Rules

Empty Parameters:

  • Empty parameters are allowed and preserve current values
  • Only specified parameters are changed
  • Useful for partial configuration updates

Address Format:

  • IP addresses: Standard IPv4 format (e.g., 195.14.1.100)
  • Domain names: FQDN format (e.g., i01.protegus.eu)
  • No spaces or special characters except dots and dashes

Port Format:

  • Numeric values (e.g., 1000, 55019)
  • Valid port range: 1-65535

Configuration Management

Change Activation:

  • Changes take effect after device response
  • Device may require restart for full activation
  • Configuration is persistent until restored to defaults

Restoration:

  • Changed data can be canceled using Restore Defaults procedure
  • Device-initiated restart may be required
  • Backup configurations remain available

Cross-References

Related commands for device configuration:

Usage Example
javascript
async function changeChannelConfig(partialUid, keyword, channel, priority, address = '', port = '') {
  // Build change channel configuration command
  const header = "070100000000000000";
  const command = "1800";
  
  let requestData;
  if (keyword === 'A') {
    // Format: kcp:a:n (change address/port)
    requestData = `${keyword}${channel}${priority}:${address}:${port}`;
  } else if (keyword === 'D') {
    // Format: kcp (disable channel)
    requestData = `${keyword}${channel}${priority}`;
  } else {
    throw new Error('Invalid keyword. Use "A" for address change or "D" for disable.');
  }
  
  const dataHex = asciiToHex(requestData);
  const fullCommand = header + command + dataHex;
  
  try {
    console.log(`Changing channel configuration: ${requestData}`);
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      
      if (responseText === 'OK') {
        console.log('Channel configuration changed successfully');
        return { 
          success: true, 
          message: 'Channel configuration updated successfully',
          keyword: keyword,
          channel: channel,
          priority: priority,
          address: address,
          port: port,
          command: requestData
        };
      } else if (responseText === 'ERROR') {
        console.log('Failed to change channel configuration');
        return { 
          success: false, 
          message: 'Device returned ERROR - invalid configuration parameters'
        };
      } else {
        return { 
          success: false, 
          message: `Unexpected response: ${responseText}` 
        };
      }
    }
    
    throw new Error('No response received from device');
  } catch (error) {
    console.error('Failed to change channel configuration:', error);
    throw error;
  }
}

// Helper functions for specific operations
async function changeChannelAddress(partialUid, channel, priority, address, port) {
  return await changeChannelConfig(partialUid, 'A', channel, priority, address, port);
}

async function disableChannel(partialUid, channel, priority) {
  return await changeChannelConfig(partialUid, 'D', channel, priority);
}

// Validation functions
function validateIpAddress(ip) {
  const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
  return ipRegex.test(ip);
}

function validateDomainName(domain) {
  const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/;
  return domainRegex.test(domain) && domain.length <= 253;
}

function validatePort(port) {
  const portNum = parseInt(port);
  return !isNaN(portNum) && portNum >= 1 && portNum <= 65535;
}

function validateChannelConfig(channel, priority, address = '', port = '') {
  const errors = [];
  
  // Validate channel
  if (!['1', '2', '3', 'C', 'A'].includes(channel)) {
    errors.push('Invalid channel. Use 1, 2, 3, C (Cloud), or A (Active)');
  }
  
  // Validate priority
  if (!['P', 'B'].includes(priority)) {
    errors.push('Invalid priority. Use P (Primary) or B (Backup)');
  }
  
  // Validate address if provided
  if (address && address.trim().length > 0) {
    if (!validateIpAddress(address) && !validateDomainName(address)) {
      errors.push('Invalid address format. Use valid IP address or domain name');
    }
  }
  
  // Validate port if provided
  if (port && port.trim().length > 0) {
    if (!validatePort(port)) {
      errors.push('Invalid port number. Use value between 1 and 65535');
    }
  }
  
  return {
    valid: errors.length === 0,
    errors: errors
  };
}

// Safe wrapper functions with validation
async function changeChannelAddressSafe(partialUid, channel, priority, address, port) {
  const validation = validateChannelConfig(channel, priority, address, port);
  if (!validation.valid) {
    throw new Error(`Invalid configuration: ${validation.errors.join(', ')}`);
  }
  
  return await changeChannelAddress(partialUid, channel, priority, address, port);
}

async function disableChannelSafe(partialUid, channel, priority) {
  const validation = validateChannelConfig(channel, priority);
  if (!validation.valid) {
    throw new Error(`Invalid configuration: ${validation.errors.join(', ')}`);
  }
  
  return await disableChannel(partialUid, channel, priority);
}

// Convenience functions for common scenarios
async function setupCloudChannel(partialUid, domain, port) {
  return await changeChannelAddressSafe(partialUid, 'C', 'P', domain, port);
}

async function setupPrimaryChannel(partialUid, channelNum, ipAddress, port) {
  return await changeChannelAddressSafe(partialUid, channelNum, 'P', ipAddress, port);
}

async function setupBackupChannel(partialUid, channelNum, ipAddress, port) {
  return await changeChannelAddressSafe(partialUid, channelNum, 'B', ipAddress, port);
}

// Usage demonstration
async function demonstrateChannelConfig(partialUid) {
  try {
    console.log('=== Channel Configuration Demonstration ===');
    
    // Setup primary channel
    console.log('\n--- Setting Up Primary Channel 1 ---');
    const primaryResult = await setupPrimaryChannel(partialUid, '1', '192.168.1.100', '8080');
    console.log('Primary setup result:', primaryResult);
    
    // Setup backup channel
    console.log('\n--- Setting Up Backup Channel 1 ---');
    const backupResult = await setupBackupChannel(partialUid, '1', '192.168.1.101', '8080');
    console.log('Backup setup result:', backupResult);
    
    // Setup cloud channel
    console.log('\n--- Setting Up Cloud Channel ---');
    const cloudResult = await setupCloudChannel(partialUid, 'i01.protegus.eu', '55019');
    console.log('Cloud setup result:', cloudResult);
    
    // Disable a channel
    console.log('\n--- Disabling Second Backup Channel ---');
    const disableResult = await disableChannelSafe(partialUid, '2', 'B');
    console.log('Disable result:', disableResult);
    
    // Demonstrate partial update (empty parameters)
    console.log('\n--- Updating Only Port (Empty Address) ---');
    const portOnlyResult = await changeChannelAddress(partialUid, '3', 'P', '', '9090');
    console.log('Port-only update result:', portOnlyResult);
    
    // Test validation
    console.log('\n--- Testing Invalid Configuration ---');
    try {
      await changeChannelAddressSafe(partialUid, 'X', 'P', '192.168.1.1', '8080');
    } catch (error) {
      console.log('Expected validation error:', error.message);
    }
    
    console.log('Channel configuration demonstration completed');
    
  } catch (error) {
    console.error('Channel configuration demonstration failed:', error);
  }
}

0x0019 - Change Parameter of Input/Output

Changes configuration parameters of input/output channels, including type configuration and schedule assignments.

Request/Response format

Request

[HEADER][0x0019][T:I/O:State,T:I/O:State,S:I/O:ID] → 0701000000000000001900[T:I/O:State,T:I/O:State,S:I/O:ID]

Request Parameters

ParameterFormatDescription
TTypeParameter type: T (Type change), S (Schedule assignment)
I/ONumberInput/Output number (starts from 1)
StateNumberFor T: 0 (disabled), 1 (input), 2 (output)
IDHexFor S: Schedule data in hex format (4 symbols)

Request Examples

Change I/O 3 to output:

[HEADER][0x0019][T:3:2]
→ 0701000000000000001900543A333A32

Disable I/O 5:

[HEADER][0x0019][T:5:0]
→ 0701000000000000001900543A353A30

Set schedule to output 2:

[HEADER][0x0019][S:2:0A52]
→ 0701000000000000001900533A323A30413532

Multiple changes:

[HEADER][0x0019][T:3:2,T:5:0,S:2:0A52]
→ 0701000000000000001900543A333A322C543A353A302C533A323A30413532

Response Format

Success Response:

[HEADER][0x0019][O:SystemState]  // Same format as 0x000D Status

Error Response:

[HEADER][0x0019][NACK]  // I/O doesn't support function or wrong data

Parameter Types

TypeCodeDescriptionState Values
Type ChangeTChange I/O functionality0=disabled, 1=input, 2=output
ScheduleSSet schedule assignment4-digit hex schedule ID

I/O Type States

StateValueDescription
Disabled0I/O channel is disabled
Input1Channel configured as input
Output2Channel configured as output

Schedule Format

Schedule ID:

  • 4-character hexadecimal format
  • Example: 0A52, FF00, 1234
  • Represents schedule configuration data

I/O Numbering:

  • Starts from 1 (not 0)
  • Corresponds to physical order on device
  • Sequential numbering for inputs and outputs

Multiple Operations

Commands can contain multiple parameter changes separated by commas:

T:3:2,T:5:0,S:2:0A52

This example:

  1. Changes I/O 3 to output type
  2. Disables I/O 5
  3. Sets schedule 0x0A52 to I/O 2

Response Handling

Success Response (O:SystemState):

  • Returns current system state after changes
  • Format matches 0x000D - Status command
  • Confirms successful parameter updates

Error Response (NACK):

  • I/O doesn't support the requested function
  • Invalid parameter data provided
  • Configuration change not permitted

Cross-References

Related commands for I/O management:

Usage Example
javascript
async function changeIOParameter(partialUid, changes) {
  // Build change I/O parameter command
  const header = "070100000000000000";
  const command = "1900";
  
  // Format changes array into command string
  let changeStrings;
  if (Array.isArray(changes)) {
    changeStrings = changes.map(change => {
      if (change.type === 'T') {
        return `T:${change.io}:${change.state}`;
      } else if (change.type === 'S') {
        return `S:${change.io}:${change.scheduleId}`;
      } else {
        throw new Error('Invalid change type. Use "T" for type or "S" for schedule.');
      }
    }).join(',');
  } else if (typeof changes === 'string') {
    changeStrings = changes;
  } else {
    throw new Error('Changes must be array of change objects or string');
  }
  
  const dataHex = asciiToHex(changeStrings);
  const fullCommand = header + command + dataHex;
  
  try {
    console.log(`Changing I/O parameters: ${changeStrings}`);
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      
      if (responseText.startsWith('O:')) {
        const systemState = responseText.substring(2);
        console.log('I/O parameters changed successfully, system state:', systemState);
        return { 
          success: true, 
          message: 'I/O parameters updated successfully',
          changes: changeStrings,
          systemState: systemState
        };
      } else if (responseText === 'NACK') {
        console.log('Failed to change I/O parameters');
        return { 
          success: false, 
          message: 'Device returned NACK - I/O doesn\'t support function or wrong data'
        };
      } else {
        return { 
          success: false, 
          message: `Unexpected response: ${responseText}` 
        };
      }
    }
    
    throw new Error('No response received from device');
  } catch (error) {
    console.error('Failed to change I/O parameters:', error);
    throw error;
  }
}

// Helper functions for specific operations
async function setIOType(partialUid, ioNumber, type) {
  const change = { type: 'T', io: ioNumber, state: type };
  return await changeIOParameter(partialUid, [change]);
}

async function setIOSchedule(partialUid, ioNumber, scheduleId) {
  const change = { type: 'S', io: ioNumber, scheduleId: scheduleId };
  return await changeIOParameter(partialUid, [change]);
}

async function disableIO(partialUid, ioNumber) {
  return await setIOType(partialUid, ioNumber, 0);
}

async function configureAsInput(partialUid, ioNumber) {
  return await setIOType(partialUid, ioNumber, 1);
}

async function configureAsOutput(partialUid, ioNumber) {
  return await setIOType(partialUid, ioNumber, 2);
}

// Validation functions
function validateIONumber(ioNumber) {
  const num = parseInt(ioNumber);
  return !isNaN(num) && num >= 1 && num <= 255; // Assuming max 255 I/Os
}

function validateIOType(type) {
  return [0, 1, 2].includes(parseInt(type));
}

function validateScheduleId(scheduleId) {
  return /^[0-9A-Fa-f]{4}$/.test(scheduleId);
}

function validateIOChange(change) {
  const errors = [];
  
  if (!['T', 'S'].includes(change.type)) {
    errors.push('Invalid change type. Use "T" for type or "S" for schedule');
  }
  
  if (!validateIONumber(change.io)) {
    errors.push('Invalid I/O number. Must be between 1 and 255');
  }
  
  if (change.type === 'T') {
    if (!validateIOType(change.state)) {
      errors.push('Invalid I/O type. Use 0 (disabled), 1 (input), or 2 (output)');
    }
  } else if (change.type === 'S') {
    if (!validateScheduleId(change.scheduleId)) {
      errors.push('Invalid schedule ID. Must be 4-character hex string');
    }
  }
  
  return {
    valid: errors.length === 0,
    errors: errors
  };
}

// Safe wrapper functions with validation
async function changeIOParameterSafe(partialUid, changes) {
  if (!Array.isArray(changes)) {
    changes = [changes];
  }
  
  // Validate all changes
  for (const change of changes) {
    const validation = validateIOChange(change);
    if (!validation.valid) {
      throw new Error(`Invalid change: ${validation.errors.join(', ')}`);
    }
  }
  
  return await changeIOParameter(partialUid, changes);
}

// Convenience functions for common scenarios
async function setupOutputWithSchedule(partialUid, ioNumber, scheduleId) {
  const changes = [
    { type: 'T', io: ioNumber, state: 2 }, // Configure as output
    { type: 'S', io: ioNumber, scheduleId: scheduleId } // Set schedule
  ];
  return await changeIOParameterSafe(partialUid, changes);
}

async function bulkDisableIOs(partialUid, ioNumbers) {
  const changes = ioNumbers.map(ioNumber => ({
    type: 'T',
    io: ioNumber,
    state: 0
  }));
  return await changeIOParameterSafe(partialUid, changes);
}

async function bulkConfigureAsInputs(partialUid, ioNumbers) {
  const changes = ioNumbers.map(ioNumber => ({
    type: 'T',
    io: ioNumber,
    state: 1
  }));
  return await changeIOParameterSafe(partialUid, changes);
}

async function bulkConfigureAsOutputs(partialUid, ioNumbers) {
  const changes = ioNumbers.map(ioNumber => ({
    type: 'T',
    io: ioNumber,
    state: 2
  }));
  return await changeIOParameterSafe(partialUid, changes);
}

// Usage demonstration
async function demonstrateIOParameterChange(partialUid) {
  try {
    console.log('=== I/O Parameter Change Demonstration ===');
    
    // Single type change
    console.log('\n--- Configuring I/O 3 as Output ---');
    const outputResult = await configureAsOutput(partialUid, 3);
    console.log('Output configuration result:', outputResult);
    
    // Disable an I/O
    console.log('\n--- Disabling I/O 5 ---');
    const disableResult = await disableIO(partialUid, 5);
    console.log('Disable result:', disableResult);
    
    // Set schedule
    console.log('\n--- Setting Schedule to I/O 2 ---');
    const scheduleResult = await setIOSchedule(partialUid, 2, '0A52');
    console.log('Schedule result:', scheduleResult);
    
    // Multiple changes at once
    console.log('\n--- Multiple Changes ---');
    const multipleChanges = [
      { type: 'T', io: 1, state: 1 }, // I/O 1 as input
      { type: 'T', io: 2, state: 2 }, // I/O 2 as output
      { type: 'S', io: 2, scheduleId: 'FF00' } // Set schedule for I/O 2
    ];
    const multipleResult = await changeIOParameterSafe(partialUid, multipleChanges);
    console.log('Multiple changes result:', multipleResult);
    
    // Setup output with schedule
    console.log('\n--- Setting Up Complete Output Configuration ---');
    const completeSetupResult = await setupOutputWithSchedule(partialUid, 4, '1234');
    console.log('Complete setup result:', completeSetupResult);
    
    // Bulk operations
    console.log('\n--- Bulk Configure I/Os 6-8 as Inputs ---');
    const bulkInputResult = await bulkConfigureAsInputs(partialUid, [6, 7, 8]);
    console.log('Bulk input configuration result:', bulkInputResult);
    
    // Test validation
    console.log('\n--- Testing Invalid Configuration ---');
    try {
      await changeIOParameterSafe(partialUid, [{ type: 'T', io: 1, state: 5 }]); // Invalid state
    } catch (error) {
      console.log('Expected validation error:', error.message);
    }
    
    console.log('I/O parameter change demonstration completed');
    
  } catch (error) {
    console.error('I/O parameter change demonstration failed:', error);
  }
}

0x001A - Read Schedule

Reads schedule configuration from device. Can request specific types of schedules or multiple schedule types in a single command.

Request/Response format

Request

[HEADER][0x001A][ScheduleType1|ScheduleTypeN-1|ScheduleTypeN] → 0701000000000000001A00[ScheduleTypes]

Request Parameters

ParameterFormatDescription
ScheduleTypeNumberSchedule type to read: 1 (GV17 users), 2 (GV17 outputs)
``Separator

Request Examples

Get both user and output schedules:

[HEADER][0x001A][1|2]
→ 0701000000000000001A00317C32

Get only output schedules:

[HEADER][0x001A][2]
→ 0701000000000000001A0032

Response Format

Success Response:

[HEADER][0x001A][ScheduleData1|ScheduleDataN]

Error Response:

[HEADER][0x001A][NACK]  // Command not supported or wrong data

Schedule Types

TypeCodeDescription
User Schedules1Schedule configuration for GV17 users
Output Schedules2Schedule configuration for GV17 outputs

Schedule Data Format

All schedule data is returned in decimal format with multiple schedules separated by colons (:) and different schedule types separated by pipes (|).

Type 1 - User Schedules

Format: type,id,enableFlags,startHour,startMin,startWeekDay,stopHour,stopMin,stopWeekDay

FieldRangeDescription
type-Schedule type (1 for users)
id-Queue number/schedule ID
enableFlags0-1Enable/disable flag (bit 0)
startHour0-23Start hour (24-hour format)
startMin0-59Start minutes
startWeekDayBitmaskStart weekdays (bit0=Monday, bit6=Sunday)
stopHour0-23Stop hour (24-hour format)
stopMin0-59Stop minutes
stopWeekDayBitmaskStop weekdays (bit0=Monday, bit6=Sunday)
Type 2 - Output Schedules

Format: type,id,enableFlags,outType,startHour,startMin,startWeekDay,stopHour,stopMin,stopWeekDay

FieldRangeDescription
type-Schedule type (2 for outputs)
id-Queue number/schedule ID
enableFlags0-1Enable/disable flag (bit 0)
outType0-1Output type: 0 (pulse), 1 (level)
startHour0-23Start hour (24-hour format)
startMin0-59Start minutes
startWeekDayBitmaskStart weekdays (bit0=Monday, bit6=Sunday)
stopHour0-23Stop hour (24-hour format)
stopMin0-59Stop minutes
stopWeekDayBitmaskStop weekdays (bit0=Monday, bit6=Sunday)

Weekday Bitmask

BitDayValue
0Monday1
1Tuesday2
2Wednesday4
3Thursday8
4Friday16
5Saturday32
6Sunday64

Response Example

Combined user and output schedules:

1,1,1,12,22,15,44,5:1,2,1,14,45,19,22,8|2,1,1,0,12,22,15,44,5:2,2,1,1,14,45,19,22,8

This response contains:

  • Type 1 (User schedules): Two schedules separated by :
  • Type 2 (Output schedules): Two schedules separated by :
  • Types separated by: |

Cross-References

Related commands for schedule management:

Usage Example
javascript
async function readSchedule(partialUid, scheduleTypes) {
  // Build read schedule command
  const header = "070100000000000000";
  const command = "1A00";
  
  // Format schedule types
  let scheduleTypesStr;
  if (Array.isArray(scheduleTypes)) {
    scheduleTypesStr = scheduleTypes.join('|');
  } else {
    scheduleTypesStr = scheduleTypes.toString();
  }
  
  const dataHex = asciiToHex(scheduleTypesStr);
  const fullCommand = header + command + dataHex;
  
  try {
    console.log(`Reading schedules for types: ${scheduleTypesStr}`);
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      
      if (responseText === 'NACK') {
        console.log('Failed to read schedules');
        return { 
          success: false, 
          message: 'Device returned NACK - command not supported or wrong data'
        };
      } else {
        console.log('Schedules read successfully');
        const parsedSchedules = parseScheduleData(responseText);
        return { 
          success: true, 
          message: 'Schedules retrieved successfully',
          requestedTypes: scheduleTypes,
          rawData: responseText,
          schedules: parsedSchedules
        };
      }
    }
    
    throw new Error('No response received from device');
  } catch (error) {
    console.error('Failed to read schedules:', error);
    throw error;
  }
}

// Parse schedule data response
function parseScheduleData(responseText) {
  const schedules = {
    userSchedules: [],
    outputSchedules: []
  };
  
  // Split by pipe to separate different schedule types
  const scheduleTypes = responseText.split('|');
  
  scheduleTypes.forEach(scheduleTypeData => {
    if (!scheduleTypeData.trim()) return;
    
    // Split by colon to get individual schedules
    const individualSchedules = scheduleTypeData.split(':');
    
    individualSchedules.forEach(scheduleStr => {
      if (!scheduleStr.trim()) return;
      
      const parts = scheduleStr.split(',');
      if (parts.length < 9) return; // Invalid schedule data
      
      const schedule = {
        type: parseInt(parts[0]),
        id: parseInt(parts[1]),
        enabled: parseInt(parts[2]) === 1,
        startHour: parseInt(parts[parts.length >= 10 ? 4 : 3]), // Account for outType in type 2
        startMin: parseInt(parts[parts.length >= 10 ? 5 : 4]),
        startWeekDay: parseInt(parts[parts.length >= 10 ? 6 : 5]),
        stopHour: parseInt(parts[parts.length >= 10 ? 7 : 6]),
        stopMin: parseInt(parts[parts.length >= 10 ? 8 : 7]),
        stopWeekDay: parseInt(parts[parts.length >= 10 ? 9 : 8])
      };
      
      // Add outType for type 2 schedules
      if (parts.length >= 10) {
        schedule.outType = parseInt(parts[3]);
        schedule.outTypeDescription = schedule.outType === 0 ? 'pulse' : 'level';
      }
      
      // Add weekday descriptions
      schedule.startWeekDays = parseWeekdays(schedule.startWeekDay);
      schedule.stopWeekDays = parseWeekdays(schedule.stopWeekDay);
      
      // Add formatted time strings
      schedule.startTime = `${schedule.startHour.toString().padStart(2, '0')}:${schedule.startMin.toString().padStart(2, '0')}`;
      schedule.stopTime = `${schedule.stopHour.toString().padStart(2, '0')}:${schedule.stopMin.toString().padStart(2, '0')}`;
      
      // Add to appropriate array
      if (schedule.type === 1) {
        schedules.userSchedules.push(schedule);
      } else if (schedule.type === 2) {
        schedules.outputSchedules.push(schedule);
      }
    });
  });
  
  return schedules;
}

// Parse weekday bitmask
function parseWeekdays(bitmask) {
  const days = [];
  const dayNames = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
  
  for (let i = 0; i < 7; i++) {
    if (bitmask & (1 << i)) {
      days.push(dayNames[i]);
    }
  }
  
  return days;
}

// Helper function to create weekday bitmask
function createWeekdayBitmask(days) {
  const dayMap = {
    'monday': 1, 'tuesday': 2, 'wednesday': 4, 'thursday': 8,
    'friday': 16, 'saturday': 32, 'sunday': 64
  };
  
  let bitmask = 0;
  days.forEach(day => {
    const dayLower = day.toLowerCase();
    if (dayMap[dayLower]) {
      bitmask |= dayMap[dayLower];
    }
  });
  
  return bitmask;
}

// Convenience functions
async function readUserSchedules(partialUid) {
  return await readSchedule(partialUid, [1]);
}

async function readOutputSchedules(partialUid) {
  return await readSchedule(partialUid, [2]);
}

async function readAllSchedules(partialUid) {
  return await readSchedule(partialUid, [1, 2]);
}

// Helper function to format schedule for display
function formatScheduleDisplay(schedule) {
  const typeStr = schedule.type === 1 ? 'User' : 'Output';
  const outTypeStr = schedule.outType !== undefined ? ` (${schedule.outTypeDescription})` : '';
  
  return {
    ...schedule,
    description: `${typeStr} Schedule ${schedule.id}${outTypeStr}`,
    timeRange: `${schedule.startTime} - ${schedule.stopTime}`,
    activeWeekdays: {
      start: schedule.startWeekDays.join(', '),
      stop: schedule.stopWeekDays.join(', ')
    },
    status: schedule.enabled ? 'Enabled' : 'Disabled'
  };
}

// Usage demonstration
async function demonstrateScheduleReading(partialUid) {
  try {
    console.log('=== Schedule Reading Demonstration ===');
    
    // Read all schedules
    console.log('\n--- Reading All Schedules ---');
    const allSchedulesResult = await readAllSchedules(partialUid);
    console.log('All schedules result:', allSchedulesResult);
    
    if (allSchedulesResult.success) {
      console.log('Raw schedule data:', allSchedulesResult.rawData);
      
      // Display user schedules
      if (allSchedulesResult.schedules.userSchedules.length > 0) {
        console.log('\n--- User Schedules ---');
        allSchedulesResult.schedules.userSchedules.forEach(schedule => {
          const formatted = formatScheduleDisplay(schedule);
          console.log(`${formatted.description}: ${formatted.timeRange} (${formatted.status})`);
          console.log(`  Start days: ${formatted.activeWeekdays.start}`);
          console.log(`  Stop days: ${formatted.activeWeekdays.stop}`);
        });
      }
      
      // Display output schedules
      if (allSchedulesResult.schedules.outputSchedules.length > 0) {
        console.log('\n--- Output Schedules ---');
        allSchedulesResult.schedules.outputSchedules.forEach(schedule => {
          const formatted = formatScheduleDisplay(schedule);
          console.log(`${formatted.description}: ${formatted.timeRange} (${formatted.status})`);
          console.log(`  Start days: ${formatted.activeWeekdays.start}`);
          console.log(`  Stop days: ${formatted.activeWeekdays.stop}`);
        });
      }
    }
    
    // Read only user schedules
    console.log('\n--- Reading Only User Schedules ---');
    const userSchedulesResult = await readUserSchedules(partialUid);
    console.log('User schedules result:', userSchedulesResult);
    
    // Read only output schedules
    console.log('\n--- Reading Only Output Schedules ---');
    const outputSchedulesResult = await readOutputSchedules(partialUid);
    console.log('Output schedules result:', outputSchedulesResult);
    
    console.log('Schedule reading demonstration completed');
    
  } catch (error) {
    console.error('Schedule reading demonstration failed:', error);
  }
}

0x001B - Write Schedule

Writes schedule configuration to device. Can write a single schedule or a complete list of schedules using the same format as the Read Schedule command.

Request/Response format

Request

[HEADER][0x001B][ScheduleData1:ScheduleDataN] → 0701000000000000001B00[ScheduleData]

Schedule Data Format

Uses the same format as 0x001A - Read Schedule:

Type 1 (User Schedules):

type,id,enableFlags,startHour,startMin,startWeekDay,stopHour,stopMin,stopWeekDay

Type 2 (Output Schedules):

type,id,enableFlags,outType,startHour,startMin,startWeekDay,stopHour,stopMin,stopWeekDay

Request Examples

Write single user schedule:

[HEADER][0x001B][1,1,1,12,22,15,44,5]
→ 0701000000000000001B00312C312C312C31322C32322C31352C34342C35

Write multiple schedules:

[HEADER][0x001B][1,1,1,12,22,15,44,5:2,1,1,0,14,45,19,22,8]
→ 0701000000000000001B00312C312C312C31322C32322C31352C34342C353A322C312C312C302C31342C34352C31392C32322C38

Response Format

Success Response:

[HEADER][0x001B][OK]

Error Response:

[HEADER][0x001B][NACK]  // Command not supported, invalid schedule, or wrong data

Schedule Data Rules

Data Format:

  • All values in decimal format
  • Multiple schedules separated by colons (:)
  • Fields separated by commas (,)
  • Same format as returned by Read Schedule command

Schedule Types:

  • Type 1: User schedules (9 fields)
  • Type 2: Output schedules (10 fields with outType)

Field Validation:

  • Hours: 0-23 (24-hour format)
  • Minutes: 0-59
  • Weekdays: Bitmask (bit 0=Monday, bit 6=Sunday)
  • Enable flags: 0 (disabled) or 1 (enabled)
  • Output type: 0 (pulse) or 1 (level)

Write Operations

Single Schedule:

  • Write individual schedule entry
  • Overwrites existing schedule with same ID

Multiple Schedules:

  • Write complete schedule list
  • Can mix user and output schedules
  • Separated by colon characters

Schedule Replacement:

  • New schedules replace existing ones
  • Schedule IDs determine which entries are updated

Cross-References

Related commands for schedule management:

Usage Example
javascript
async function writeSchedule(partialUid, scheduleData) {
  // Build write schedule command
  const header = "070100000000000000";
  const command = "1B00";
  
  // Format schedule data
  let scheduleDataStr;
  if (Array.isArray(scheduleData)) {
    // Convert schedule objects to string format
    scheduleDataStr = scheduleData.map(schedule => formatScheduleForWrite(schedule)).join(':');
  } else if (typeof scheduleData === 'string') {
    scheduleDataStr = scheduleData;
  } else if (typeof scheduleData === 'object') {
    // Single schedule object
    scheduleDataStr = formatScheduleForWrite(scheduleData);
  } else {
    throw new Error('Schedule data must be array of schedules, string, or schedule object');
  }
  
  const dataHex = asciiToHex(scheduleDataStr);
  const fullCommand = header + command + dataHex;
  
  try {
    console.log(`Writing schedule data: ${scheduleDataStr}`);
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      
      if (responseText === 'OK') {
        console.log('Schedule written successfully');
        return { 
          success: true, 
          message: 'Schedule configuration updated successfully',
          scheduleData: scheduleDataStr
        };
      } else if (responseText === 'NACK') {
        console.log('Failed to write schedule');
        return { 
          success: false, 
          message: 'Device returned NACK - command not supported, invalid schedule, or wrong data'
        };
      } else {
        return { 
          success: false, 
          message: `Unexpected response: ${responseText}` 
        };
      }
    }
    
    throw new Error('No response received from device');
  } catch (error) {
    console.error('Failed to write schedule:', error);
    throw error;
  }
}

// Format schedule object for write command
function formatScheduleForWrite(schedule) {
  if (schedule.type === 1) {
    // User schedule: type,id,enableFlags,startHour,startMin,startWeekDay,stopHour,stopMin,stopWeekDay
    return [
      schedule.type,
      schedule.id,
      schedule.enabled ? 1 : 0,
      schedule.startHour,
      schedule.startMin,
      schedule.startWeekDay,
      schedule.stopHour,
      schedule.stopMin,
      schedule.stopWeekDay
    ].join(',');
  } else if (schedule.type === 2) {
    // Output schedule: type,id,enableFlags,outType,startHour,startMin,startWeekDay,stopHour,stopMin,stopWeekDay
    return [
      schedule.type,
      schedule.id,
      schedule.enabled ? 1 : 0,
      schedule.outType,
      schedule.startHour,
      schedule.startMin,
      schedule.startWeekDay,
      schedule.stopHour,
      schedule.stopMin,
      schedule.stopWeekDay
    ].join(',');
  } else {
    throw new Error('Invalid schedule type. Must be 1 (user) or 2 (output)');
  }
}

// Helper function to create weekday bitmask from array of day names
function createWeekdayBitmask(days) {
  const dayMap = {
    'monday': 1, 'tuesday': 2, 'wednesday': 4, 'thursday': 8,
    'friday': 16, 'saturday': 32, 'sunday': 64
  };
  
  let bitmask = 0;
  days.forEach(day => {
    const dayLower = day.toLowerCase();
    if (dayMap[dayLower]) {
      bitmask |= dayMap[dayLower];
    }
  });
  
  return bitmask;
}

// Validation functions
function validateScheduleTime(hour, minute) {
  return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59;
}

function validateWeekdayBitmask(bitmask) {
  return bitmask >= 0 && bitmask <= 127; // 7 bits maximum
}

function validateSchedule(schedule) {
  const errors = [];
  
  if (![1, 2].includes(schedule.type)) {
    errors.push('Invalid schedule type. Must be 1 (user) or 2 (output)');
  }
  
  if (!Number.isInteger(schedule.id) || schedule.id < 0) {
    errors.push('Invalid schedule ID. Must be non-negative integer');
  }
  
  if (!validateScheduleTime(schedule.startHour, schedule.startMin)) {
    errors.push('Invalid start time. Hour must be 0-23, minute must be 0-59');
  }
  
  if (!validateScheduleTime(schedule.stopHour, schedule.stopMin)) {
    errors.push('Invalid stop time. Hour must be 0-23, minute must be 0-59');
  }
  
  if (!validateWeekdayBitmask(schedule.startWeekDay)) {
    errors.push('Invalid start weekday bitmask. Must be 0-127');
  }
  
  if (!validateWeekdayBitmask(schedule.stopWeekDay)) {
    errors.push('Invalid stop weekday bitmask. Must be 0-127');
  }
  
  if (schedule.type === 2 && ![0, 1].includes(schedule.outType)) {
    errors.push('Invalid output type for output schedule. Must be 0 (pulse) or 1 (level)');
  }
  
  return {
    valid: errors.length === 0,
    errors: errors
  };
}

// Safe wrapper with validation
async function writeScheduleSafe(partialUid, scheduleData) {
  let schedulesToValidate = Array.isArray(scheduleData) ? scheduleData : [scheduleData];
  
  // Skip validation for string input
  if (typeof scheduleData !== 'string') {
    for (const schedule of schedulesToValidate) {
      const validation = validateSchedule(schedule);
      if (!validation.valid) {
        throw new Error(`Invalid schedule: ${validation.errors.join(', ')}`);
      }
    }
  }
  
  return await writeSchedule(partialUid, scheduleData);
}

// Convenience functions for common scenarios
async function writeUserSchedule(partialUid, id, enabled, startTime, stopTime, startDays, stopDays) {
  const schedule = {
    type: 1,
    id: id,
    enabled: enabled,
    startHour: startTime.hour,
    startMin: startTime.minute,
    startWeekDay: Array.isArray(startDays) ? createWeekdayBitmask(startDays) : startDays,
    stopHour: stopTime.hour,
    stopMin: stopTime.minute,
    stopWeekDay: Array.isArray(stopDays) ? createWeekdayBitmask(stopDays) : stopDays
  };
  
  return await writeScheduleSafe(partialUid, schedule);
}

async function writeOutputSchedule(partialUid, id, enabled, outType, startTime, stopTime, startDays, stopDays) {
  const schedule = {
    type: 2,
    id: id,
    enabled: enabled,
    outType: outType,
    startHour: startTime.hour,
    startMin: startTime.minute,
    startWeekDay: Array.isArray(startDays) ? createWeekdayBitmask(startDays) : startDays,
    stopHour: stopTime.hour,
    stopMin: stopTime.minute,
    stopWeekDay: Array.isArray(stopDays) ? createWeekdayBitmask(stopDays) : stopDays
  };
  
  return await writeScheduleSafe(partialUid, schedule);
}

async function writeMultipleSchedules(partialUid, schedules) {
  return await writeScheduleSafe(partialUid, schedules);
}

// Usage demonstration
async function demonstrateScheduleWriting(partialUid) {
  try {
    console.log('=== Schedule Writing Demonstration ===');
    
    // Write single user schedule
    console.log('\n--- Writing User Schedule ---');
    const userScheduleResult = await writeUserSchedule(
      partialUid,
      1, // ID
      true, // Enabled
      { hour: 12, minute: 22 }, // Start time
      { hour: 14, minute: 45 }, // Stop time
      ['monday', 'tuesday', 'wednesday'], // Start days
      ['friday'] // Stop days
    );
    console.log('User schedule result:', userScheduleResult);
    
    // Write single output schedule
    console.log('\n--- Writing Output Schedule ---');
    const outputScheduleResult = await writeOutputSchedule(
      partialUid,
      1, // ID
      true, // Enabled
      0, // Pulse output
      { hour: 8, minute: 0 }, // Start time
      { hour: 18, minute: 0 }, // Stop time
      ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], // Weekdays
      ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'] // Weekdays
    );
    console.log('Output schedule result:', outputScheduleResult);
    
    // Write multiple schedules
    console.log('\n--- Writing Multiple Schedules ---');
    const multipleSchedules = [
      {
        type: 1,
        id: 2,
        enabled: true,
        startHour: 9,
        startMin: 0,
        startWeekDay: createWeekdayBitmask(['monday', 'wednesday', 'friday']),
        stopHour: 17,
        stopMin: 0,
        stopWeekDay: createWeekdayBitmask(['monday', 'wednesday', 'friday'])
      },
      {
        type: 2,
        id: 2,
        enabled: true,
        outType: 1, // Level output
        startHour: 20,
        startMin: 0,
        startWeekDay: createWeekdayBitmask(['saturday', 'sunday']),
        stopHour: 22,
        stopMin: 30,
        stopWeekDay: createWeekdayBitmask(['saturday', 'sunday'])
      }
    ];
    
    const multipleResult = await writeMultipleSchedules(partialUid, multipleSchedules);
    console.log('Multiple schedules result:', multipleResult);
    
    // Write raw string format
    console.log('\n--- Writing Raw Schedule String ---');
    const rawScheduleString = '1,3,1,6,30,31,18,0,31'; // User schedule: 06:30-18:00 Mon-Fri
    const rawResult = await writeSchedule(partialUid, rawScheduleString);
    console.log('Raw string result:', rawResult);
    
    // Test validation
    console.log('\n--- Testing Invalid Schedule ---');
    try {
      await writeScheduleSafe(partialUid, {
        type: 1,
        id: 1,
        enabled: true,
        startHour: 25, // Invalid hour
        startMin: 0,
        startWeekDay: 1,
        stopHour: 18,
        stopMin: 0,
        stopWeekDay: 1
      });
    } catch (error) {
      console.log('Expected validation error:', error.message);
    }
    
    console.log('Schedule writing demonstration completed');
    
  } catch (error) {
    console.error('Schedule writing demonstration failed:', error);
  }
}

0x001C - Get Checksums

Requests or validates checksums for different data types. Can be used to check data integrity and detect changes in user lists, schedules, and other configuration data. Supports both checksum queries and validation operations.

Request/Response format

Request Format - Query Checksums

[HEADER][0x001C][Type1|Type2|...] → 0701000000000000001C00[TypeList]

Request Format - Validate Checksums

[HEADER][0x001C][Type1:CheckSum|Type2:CheckSum|...] → 0701000000000000001C00[TypeChecksumList]

Request Examples

Query single checksum:

[HEADER][0x001C][1]  // Query user list checksum
→ 0701000000000000001C0031

Query multiple checksums:

[HEADER][0x001C][1|2]  // Query user list and schedule checksums
→ 0701000000000000001C00317C32

Validate checksums:

[HEADER][0x001C][1:A5F6|2:54BB]  // Validate user list and schedule checksums
→ 0701000000000000001C00313A413546367C323A35344242

Response Format

Checksum Response:

[HEADER][0x001C][Type1:CheckSum|TypeX:CheckSum]

Data Update Response (when checksums don't match):

[HEADER][0x00XX][DataBasedOnCommandID]  // Returns updated data for mismatched checksums

Error Response:

[HEADER][0x001C][NACK]  // Command not supported, invalid type, or wrong data

Response Examples

Successful checksum query:

← [HEADER][0x001C][1:A5F6|2:54BB]
← 0701000000000000001C00313A413546367C323A35344242

Data update needed (mismatched checksums):

← [HEADER][0x0018][UserData...]  // Returns updated user data if type 1 checksum mismatch
← [HEADER][0x001A][ScheduleData...]  // Returns updated schedule data if type 2/3 checksum mismatch

Checksum Types

Available Data Types:

  • Type 1: User list (GV17) - Checksum of all user configuration data
  • Type 2: Schedule - GV17 Users schedules
  • Type 3: Schedule - GV17 Outputs schedules

Checksum Format:

  • 4 ASCII characters representing 2-byte hex value
  • Based on TRK CRC16 calculation algorithm
  • Example: A5F6, 54BB, 12CD

Type Combinations:

  • Multiple types can be requested in single command
  • Device will not combine different schedule types inappropriately
  • Valid combinations: schedule output + schedule users
  • Invalid combinations: users + schedule users (same data type)

Operation Modes

Query Mode:

  • Request current checksums for specified types
  • Device returns current checksum values
  • Used to check if local data is up-to-date

Validation Mode:

  • Send known checksums for validation
  • Device compares with current checksums
  • If mismatch detected, device sends updated data via appropriate command

Data Synchronization:

  • Mismatched Type 1: Device responds with [0x0018] user data
  • Mismatched Type 2/3: Device responds with [0x001A] schedule data
  • Matched checksums: Device confirms with checksum response

Cross-References

Related commands for data management:

Usage Example
javascript
async function getChecksums(partialUid, types) {
  // Build get checksums command
  const header = "070100000000000000";
  const command = "1C00";
  
  // Format types list
  let typesStr;
  if (Array.isArray(types)) {
    typesStr = types.join('|');
  } else if (typeof types === 'string') {
    typesStr = types;
  } else {
    typesStr = types.toString();
  }
  
  const dataHex = asciiToHex(typesStr);
  const fullCommand = header + command + dataHex;
  
  try {
    console.log(`Requesting checksums for types: ${typesStr}`);
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      
      if (responseText === 'NACK') {
        return { 
          success: false, 
          message: 'Device returned NACK - command not supported, invalid type, or wrong data'
        };
      }
      
      // Parse checksums response
      const checksums = parseChecksumResponse(responseText);
      console.log('Received checksums:', checksums);
      
      return { 
        success: true, 
        checksums: checksums,
        raw: responseText
      };
    }
    
    throw new Error('No response received from device');
  } catch (error) {
    console.error('Failed to get checksums:', error);
    throw error;
  }
}

async function validateChecksums(partialUid, checksums) {
  // Build validate checksums command
  const header = "070100000000000000";
  const command = "1C00";
  
  // Format checksums for validation
  let checksumStr;
  if (typeof checksums === 'object' && !Array.isArray(checksums)) {
    // Convert object to string format
    checksumStr = Object.entries(checksums)
      .map(([type, checksum]) => `${type}:${checksum}`)
      .join('|');
  } else if (typeof checksums === 'string') {
    checksumStr = checksums;
  } else {
    throw new Error('Checksums must be object with type:checksum pairs or string');
  }
  
  const dataHex = asciiToHex(checksumStr);
  const fullCommand = header + command + dataHex;
  
  try {
    console.log(`Validating checksums: ${checksumStr}`);
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      
      if (responseText === 'NACK') {
        return { 
          success: false, 
          message: 'Device returned NACK - command not supported, invalid type, or wrong data'
        };
      }
      
      // Check if response is checksum confirmation or data update
      if (responseText.includes(':')) {
        // Checksums match - device confirmed
        const updatedChecksums = parseChecksumResponse(responseText);
        console.log('Checksums validated successfully:', updatedChecksums);
        
        return { 
          success: true, 
          validated: true,
          checksums: updatedChecksums,
          message: 'All checksums match - data is up to date'
        };
      } else {
        // Data mismatch detected - device will send updated data
        console.log('Checksum mismatch detected - expecting data update');
        
        return { 
          success: true, 
          validated: false,
          message: 'Checksum mismatch detected - device will send updated data',
          mismatchResponse: responseText
        };
      }
    }
    
    throw new Error('No response received from device');
  } catch (error) {
    console.error('Failed to validate checksums:', error);
    throw error;
  }
}

// Parse checksum response format: "Type1:CheckSum|TypeX:CheckSum"
function parseChecksumResponse(responseText) {
  const checksums = {};
  
  if (!responseText || responseText === 'NACK') {
    return checksums;
  }
  
  // Split by pipe separator
  const pairs = responseText.split('|');
  
  pairs.forEach(pair => {
    const colonIndex = pair.indexOf(':');
    if (colonIndex > 0) {
      const type = pair.substring(0, colonIndex);
      const checksum = pair.substring(colonIndex + 1);
      checksums[type] = checksum;
    }
  });
  
  return checksums;
}

// Get checksum type description
function getChecksumTypeDescription(type) {
  const descriptions = {
    '1': 'User list (GV17)',
    '2': 'Schedule - GV17 Users',
    '3': 'Schedule - GV17 Outputs'
  };
  
  return descriptions[type] || `Unknown type ${type}`;
}

// Calculate TRK CRC16 checksum (placeholder - actual implementation needed)
function calculateTrkCrc16(data) {
  // This is a placeholder for the actual TRK CRC16 algorithm
  // Implementation would depend on the specific CRC16 variant used by TRK
  console.warn('TRK CRC16 calculation not implemented - placeholder function');
  return '0000';
}

// Validate checksum format
function isValidChecksumFormat(checksum) {
  return /^[0-9A-Fa-f]{4}$/.test(checksum);
}

// Convenience functions
async function getUserListChecksum(partialUid) {
  const result = await getChecksums(partialUid, [1]);
  return result.success ? result.checksums['1'] : null;
}

async function getScheduleChecksums(partialUid) {
  const result = await getChecksums(partialUid, [2, 3]);
  if (result.success) {
    return {
      userSchedules: result.checksums['2'],
      outputSchedules: result.checksums['3']
    };
  }
  return null;
}

async function getAllChecksums(partialUid) {
  return await getChecksums(partialUid, [1, 2, 3]);
}

async function validateAllData(partialUid, localChecksums) {
  return await validateChecksums(partialUid, localChecksums);
}

// Data synchronization workflow
async function syncDataWithDevice(partialUid, localChecksums) {
  try {
    console.log('=== Data Synchronization Workflow ===');
    
    // Step 1: Validate current checksums
    console.log('\n--- Validating Local Checksums ---');
    const validationResult = await validateChecksums(partialUid, localChecksums);
    
    if (validationResult.validated) {
      console.log('All data is up to date - no sync needed');
      return {
        synchronized: true,
        message: 'All data is current',
        checksums: validationResult.checksums
      };
    }
    
    // Step 2: Get current checksums to identify what changed
    console.log('\n--- Getting Current Device Checksums ---');
    const currentResult = await getAllChecksums(partialUid);
    
    if (!currentResult.success) {
      throw new Error('Failed to get current checksums from device');
    }
    
    // Step 3: Identify changed data types
    const changedTypes = [];
    Object.entries(localChecksums).forEach(([type, localChecksum]) => {
      const deviceChecksum = currentResult.checksums[type];
      if (deviceChecksum && deviceChecksum !== localChecksum) {
        changedTypes.push({
          type: type,
          description: getChecksumTypeDescription(type),
          local: localChecksum,
          device: deviceChecksum
        });
      }
    });
    
    console.log('Changed data types:', changedTypes);
    
    // Step 4: Request updated data for changed types
    const updatedData = {};
    for (const change of changedTypes) {
      console.log(`\n--- Updating ${change.description} ---`);
      
      if (change.type === '1') {
        // User data changed - would call getUserInformation
        console.log('User data needs update - call getUserInformation()');
        updatedData.users = 'Updated user data would be retrieved here';
      } else if (change.type === '2' || change.type === '3') {
        // Schedule data changed - would call readSchedule
        console.log('Schedule data needs update - call readSchedule()');
        updatedData.schedules = 'Updated schedule data would be retrieved here';
      }
    }
    
    return {
      synchronized: false,
      changedTypes: changedTypes,
      updatedData: updatedData,
      newChecksums: currentResult.checksums,
      message: `Synchronized ${changedTypes.length} data types`
    };
    
  } catch (error) {
    console.error('Data synchronization failed:', error);
    throw error;
  }
}

// Usage demonstration
async function demonstrateChecksumOperations(partialUid) {
  try {
    console.log('=== Checksum Operations Demonstration ===');
    
    // Get all checksums
    console.log('\n--- Getting All Checksums ---');
    const allChecksums = await getAllChecksums(partialUid);
    console.log('All checksums result:', allChecksums);
    
    // Get specific checksums
    console.log('\n--- Getting User List Checksum ---');
    const userChecksum = await getUserListChecksum(partialUid);
    console.log('User checksum:', userChecksum);
    
    console.log('\n--- Getting Schedule Checksums ---');
    const scheduleChecksums = await getScheduleChecksums(partialUid);
    console.log('Schedule checksums:', scheduleChecksums);
    
    // Validate existing checksums
    console.log('\n--- Validating Checksums ---');
    if (allChecksums.success && Object.keys(allChecksums.checksums).length > 0) {
      const validationResult = await validateChecksums(partialUid, allChecksums.checksums);
      console.log('Validation result:', validationResult);
    }
    
    // Test with mock outdated checksums
    console.log('\n--- Testing Outdated Checksums ---');
    const mockOutdatedChecksums = {
      '1': '0000', // Fake outdated user checksum
      '2': '1111'  // Fake outdated schedule checksum
    };
    
    const outdatedValidation = await validateChecksums(partialUid, mockOutdatedChecksums);
    console.log('Outdated validation result:', outdatedValidation);
    
    // Demonstrate synchronization workflow
    console.log('\n--- Data Synchronization Demo ---');
    if (allChecksums.success) {
      const syncResult = await syncDataWithDevice(partialUid, allChecksums.checksums);
      console.log('Synchronization result:', syncResult);
    }
    
    // Test checksum format validation
    console.log('\n--- Testing Checksum Format Validation ---');
    console.log('Valid checksum A5F6:', isValidChecksumFormat('A5F6'));
    console.log('Valid checksum 54bb:', isValidChecksumFormat('54bb'));
    console.log('Invalid checksum XYZ:', isValidChecksumFormat('XYZ'));
    console.log('Invalid checksum 12345:', isValidChecksumFormat('12345'));
    
    console.log('Checksum operations demonstration completed');
    
  } catch (error) {
    console.error('Checksum operations demonstration failed:', error);
  }
}

0x001D - Time Zone Change

Changes the device's time zone offset setting. Allows configuration of timezone offsets in minutes from UTC for proper local time handling and schedule management.

Request/Response format

Request Format

[HEADER][0x001D][ZO:±offset] → 0701000000000000001D00[ZoneOffsetData]

Request Examples

Set positive timezone (UTC+12 hours):

[HEADER][0x001D][ZO:+720]  // +720 minutes = +12 hours
→ 0701000000000000001D005A4F3A2B373230

Set negative timezone (UTC-1 hour):

[HEADER][0x001D][ZO:-60]  // -60 minutes = -1 hour
→ 0701000000000000001D005A4F3A2D3630

Set positive timezone (UTC+2 hours):

[HEADER][0x001D][ZO:+120]  // +120 minutes = +2 hours
→ 0701000000000000001D005A4F3A2B313230

Response Format

Success Response:

[HEADER][0x001D][OK]

Error Response:

[HEADER][0x001D][NACK]  // Command not supported, invalid timezone, or wrong data

Timezone Offset Format

Data Format:

  • Prefix: ZO: (Zone Offset identifier)
  • Sign: + for positive offset (east of UTC), - for negative offset (west of UTC)
  • Value: Offset in minutes from UTC
  • Range: Typically -720 to +840 minutes (-12 to +14 hours)

Common Timezone Examples:

  • UTC+0: ZO:+0 or ZO:-0
  • UTC+1 (CET): ZO:+60
  • UTC-5 (EST): ZO:-300
  • UTC-8 (PST): ZO:-480
  • UTC+9 (JST): ZO:+540
  • UTC+12 (NZST): ZO:+720

Timezone Validation

Valid Ranges:

  • Standard Range: -720 to +720 minutes (-12 to +12 hours)
  • Extended Range: -840 to +840 minutes (-14 to +14 hours)
  • Quarter Hours: Some timezones use 15, 30, 45 minute offsets

Format Requirements:

  • Must include ZO: prefix
  • Must include sign (+ or -)
  • Value must be integer minutes
  • No spaces in the format string

Time Impact

System Effects:

  • Local Time: Changes how device interprets and displays local time
  • Schedules: Affects schedule execution relative to local time
  • Logging: Timestamps in logs adjusted to new timezone
  • Real-time Clock: System time reference updated

Schedule Considerations:

  • Existing schedules may execute at different local times
  • Schedule times are typically stored in local time format
  • Consider schedule adjustments when changing timezones

Cross-References

Related commands for time management:

Usage Example
javascript
async function changeTimezone(partialUid, offsetMinutes) {
  // Build timezone change command
  const header = "070100000000000000";
  const command = "1D00";
  
  // Format timezone offset
  const sign = offsetMinutes >= 0 ? '+' : '-';
  const absOffset = Math.abs(offsetMinutes);
  const timezoneStr = `ZO:${sign}${absOffset}`;
  
  const dataHex = asciiToHex(timezoneStr);
  const fullCommand = header + command + dataHex;
  
  try {
    console.log(`Setting timezone offset to ${offsetMinutes} minutes (${formatTimezoneOffset(offsetMinutes)})`);
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      
      if (responseText === 'OK') {
        console.log('Timezone changed successfully');
        return { 
          success: true, 
          message: `Timezone set to ${formatTimezoneOffset(offsetMinutes)}`,
          offsetMinutes: offsetMinutes,
          timezoneString: timezoneStr
        };
      } else if (responseText === 'NACK') {
        console.log('Failed to change timezone');
        return { 
          success: false, 
          message: 'Device returned NACK - command not supported, invalid timezone, or wrong data'
        };
      } else {
        return { 
          success: false, 
          message: `Unexpected response: ${responseText}` 
        };
      }
    }
    
    throw new Error('No response received from device');
  } catch (error) {
    console.error('Failed to change timezone:', error);
    throw error;
  }
}

// Format timezone offset for display
function formatTimezoneOffset(offsetMinutes) {
  const sign = offsetMinutes >= 0 ? '+' : '-';
  const absOffset = Math.abs(offsetMinutes);
  const hours = Math.floor(absOffset / 60);
  const minutes = absOffset % 60;
  
  if (minutes === 0) {
    return `UTC${sign}${hours}`;
  } else {
    return `UTC${sign}${hours}:${minutes.toString().padStart(2, '0')}`;
  }
}

// Parse timezone offset from string
function parseTimezoneOffset(timezoneString) {
  // Handle formats like "UTC+5", "UTC-8:30", "+0300", "-0500"
  const match = timezoneString.match(/([+-]?)(\d{1,2})(?::?(\d{2}))?/);
  
  if (!match) {
    throw new Error('Invalid timezone format');
  }
  
  const sign = match[1] === '-' ? -1 : 1;
  const hours = parseInt(match[2], 10);
  const minutes = match[3] ? parseInt(match[3], 10) : 0;
  
  return sign * (hours * 60 + minutes);
}

// Validate timezone offset
function validateTimezoneOffset(offsetMinutes) {
  // Standard timezone range: -12 to +14 hours
  const minOffset = -12 * 60; // -720 minutes
  const maxOffset = 14 * 60;  // +840 minutes
  
  if (offsetMinutes < minOffset || offsetMinutes > maxOffset) {
    return {
      valid: false,
      error: `Timezone offset ${offsetMinutes} minutes is outside valid range (${minOffset} to ${maxOffset} minutes)`
    };
  }
  
  return { valid: true };
}

// Common timezone presets
const COMMON_TIMEZONES = {
  'UTC': 0,
  'GMT': 0,
  'CET': 60,     // Central European Time
  'EET': 120,    // Eastern European Time
  'MSK': 180,    // Moscow Time
  'GST': 240,    // Gulf Standard Time
  'IST': 330,    // India Standard Time
  'JST': 540,    // Japan Standard Time
  'AEST': 600,   // Australian Eastern Standard Time
  'NZST': 720,   // New Zealand Standard Time
  'EST': -300,   // Eastern Standard Time
  'CST': -360,   // Central Standard Time
  'MST': -420,   // Mountain Standard Time
  'PST': -480,   // Pacific Standard Time
  'AKST': -540,  // Alaska Standard Time
  'HST': -600    // Hawaii Standard Time
};

// Get timezone offset by name
function getTimezoneByName(timezoneName) {
  const offset = COMMON_TIMEZONES[timezoneName.toUpperCase()];
  if (offset === undefined) {
    throw new Error(`Unknown timezone: ${timezoneName}`);
  }
  return offset;
}

// List available timezone presets
function listAvailableTimezones() {
  return Object.entries(COMMON_TIMEZONES).map(([name, offset]) => ({
    name: name,
    offset: offset,
    display: `${name} (${formatTimezoneOffset(offset)})`
  }));
}

// Convenience functions
async function setTimezoneByName(partialUid, timezoneName) {
  const offset = getTimezoneByName(timezoneName);
  return await changeTimezone(partialUid, offset);
}

async function setTimezoneByString(partialUid, timezoneString) {
  const offset = parseTimezoneOffset(timezoneString);
  return await changeTimezone(partialUid, offset);
}

async function setUTC(partialUid) {
  return await changeTimezone(partialUid, 0);
}

// Safe wrapper with validation
async function changeTimezoneSafe(partialUid, offsetMinutes) {
  const validation = validateTimezoneOffset(offsetMinutes);
  
  if (!validation.valid) {
    throw new Error(validation.error);
  }
  
  return await changeTimezone(partialUid, offsetMinutes);
}

// Get current timezone equivalent for common locations
function suggestTimezoneForLocation(location) {
  const suggestions = {
    'new york': 'EST',
    'los angeles': 'PST',
    'london': 'GMT',
    'paris': 'CET',
    'berlin': 'CET',
    'moscow': 'MSK',
    'dubai': 'GST',
    'mumbai': 'IST',
    'delhi': 'IST',
    'tokyo': 'JST',
    'sydney': 'AEST',
    'auckland': 'NZST'
  };
  
  const locationLower = location.toLowerCase();
  const timezone = suggestions[locationLower];
  
  if (timezone) {
    return {
      timezone: timezone,
      offset: COMMON_TIMEZONES[timezone],
      display: formatTimezoneOffset(COMMON_TIMEZONES[timezone])
    };
  }
  
  return null;
}

// Calculate timezone difference
function calculateTimezoneDifference(offset1, offset2) {
  const diffMinutes = offset2 - offset1;
  return {
    minutes: diffMinutes,
    hours: diffMinutes / 60,
    display: formatTimezoneOffset(Math.abs(diffMinutes)),
    direction: diffMinutes > 0 ? 'ahead' : 'behind'
  };
}

// Usage demonstration
async function demonstrateTimezoneOperations(partialUid) {
  try {
    console.log('=== Timezone Operations Demonstration ===');
    
    // Set timezone by offset
    console.log('\n--- Setting Timezone by Offset ---');
    const offsetResult = await changeTimezoneSafe(partialUid, 60); // UTC+1
    console.log('Offset result:', offsetResult);
    
    // Set timezone by name
    console.log('\n--- Setting Timezone by Name ---');
    const nameResult = await setTimezoneByName(partialUid, 'JST'); // Japan Standard Time
    console.log('Name result:', nameResult);
    
    // Set timezone by string
    console.log('\n--- Setting Timezone by String ---');
    const stringResult = await setTimezoneByString(partialUid, 'UTC-5:30');
    console.log('String result:', stringResult);
    
    // Reset to UTC
    console.log('\n--- Resetting to UTC ---');
    const utcResult = await setUTC(partialUid);
    console.log('UTC result:', utcResult);
    
    // List available timezones
    console.log('\n--- Available Timezone Presets ---');
    const timezones = listAvailableTimezones();
    timezones.forEach(tz => {
      console.log(`  ${tz.display}`);
    });
    
    // Location suggestions
    console.log('\n--- Location-based Suggestions ---');
    const locations = ['new york', 'london', 'tokyo', 'sydney'];
    locations.forEach(location => {
      const suggestion = suggestTimezoneForLocation(location);
      if (suggestion) {
        console.log(`  ${location}: ${suggestion.timezone} (${suggestion.display})`);
      }
    });
    
    // Timezone difference calculation
    console.log('\n--- Timezone Differences ---');
    const diff1 = calculateTimezoneDifference(0, 540); // UTC to JST
    console.log(`UTC to JST: ${diff1.hours} hours ${diff1.direction}`);
    
    const diff2 = calculateTimezoneDifference(-480, 60); // PST to CET
    console.log(`PST to CET: ${diff2.hours} hours ${diff2.direction}`);
    
    // Test validation
    console.log('\n--- Testing Invalid Timezone ---');
    try {
      await changeTimezoneSafe(partialUid, 1000); // Invalid: too large
    } catch (error) {
      console.log('Expected validation error:', error.message);
    }
    
    // Test parsing
    console.log('\n--- Testing Timezone Parsing ---');
    const parseTests = ['UTC+5', 'UTC-8:30', '+0300', '-0500'];
    parseTests.forEach(test => {
      try {
        const parsed = parseTimezoneOffset(test);
        console.log(`  ${test} → ${parsed} minutes (${formatTimezoneOffset(parsed)})`);
      } catch (error) {
        console.log(`  ${test} → Error: ${error.message}`);
      }
    });
    
    console.log('Timezone operations demonstration completed');
    
  } catch (error) {
    console.error('Timezone operations demonstration failed:', error);
  }
}

0x001E - Set User Working Mode

Sets the working state for a specific user, marking them as working or out of work. Requires authorization with datatype 3 (user email authentication). Device responds with status confirmation and may optionally send CID events for work start/stop.

Request/Response format

Request Format

[HEADER][0x001E][UserID:State] → 0701000000000000001E00[UserWorkingData]

Request Examples

Set user 25 to working state:

[HEADER][0x001E][25:1]  // User ID 25, state 1 (working)
→ 0701000000000000001E00 32353A31

Set user 150 to out of work:

[HEADER][0x001E][150:0]  // User ID 150, state 0 (out of work)
→ 0701000000000000001E00 3135303A30

Set user 999 to working:

[HEADER][0x001E][999:1]  // User ID 999, state 1 (working)
→ 0701000000000000001E00 3939393A31

Response Format

Success Response:

[HEADER][0x001E][OK]

Error Response:

[HEADER][0x001E][NACK]  // Command not supported, invalid user ID, wrong state, or authorization failed

Optional CID Events

Device may send Contact ID (CID) events after successful state change:

Work Start Event:

[CID_EVENT] - User started work session

Work Stop Event:

[CID_EVENT] - User ended work session

Authorization Requirements

Authentication Type:

  • Datatype 3: User email authentication required
  • Authorization Method: Must authenticate with existing user email
  • Security Level: User-level access control

Access Control:

  • Only authorized users can change working states
  • User email must exist in device user database
  • Authentication must be valid before command execution

User ID and State Format

User ID Range:

  • Valid Range: 11-999
  • Format: String representation of numeric ID
  • Examples: "25", "150", "999"

Working State Values:

  • State 1: User is working (start work session)
  • State 0: User is out of work (end work session)
  • Format: Single digit (0 or 1)

Data Format:

  • Pattern: UserID:State
  • Separator: Colon (:)
  • Example: 25:1, 150:0, 999:1

Working Mode Effects

System Behavior:

  • Work Tracking: Device tracks user work sessions
  • Event Logging: Work start/stop events logged
  • Access Control: May affect user access permissions
  • Reporting: Work hours and attendance tracking

CID Integration:

  • Optional Events: Device may generate CID events
  • Work Start: CID event when user starts work
  • Work Stop: CID event when user stops work
  • Event Details: Include user ID and timestamp

Cross-References

Related commands for user management:

Usage Example
javascript
async function setUserWorkingMode(partialUid, userId, workingState, userEmail) {
  // Validate parameters
  if (!isValidUserId(userId)) {
    throw new Error('Invalid user ID. Must be between 11 and 999');
  }
  
  if (!isValidWorkingState(workingState)) {
    throw new Error('Invalid working state. Must be 0 (out of work) or 1 (working)');
  }
  
  if (!userEmail) {
    throw new Error('User email required for authorization');
  }
  
  try {
    // Step 1: Authenticate with user email (datatype 3)
    console.log(`Authenticating user email: ${userEmail}`);
    const authResult = await authenticateUser(partialUid, userEmail, 3);
    
    if (!authResult.success) {
      throw new Error('Authentication failed: ' + authResult.message);
    }
    
    // Step 2: Build set working mode command
    const header = "070100000000000000";
    const command = "1E00";
    
    const workingData = `${userId}:${workingState}`;
    const dataHex = asciiToHex(workingData);
    const fullCommand = header + command + dataHex;
    
    console.log(`Setting user ${userId} working state to ${workingState ? 'working' : 'out of work'}`);
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      
      if (responseText === 'OK') {
        const stateDescription = workingState ? 'started work' : 'stopped work';
        console.log(`User ${userId} ${stateDescription} successfully`);
        
        return { 
          success: true, 
          message: `User ${userId} ${stateDescription}`,
          userId: userId,
          workingState: workingState,
          stateDescription: stateDescription
        };
      } else if (responseText === 'NACK') {
        console.log('Failed to set user working mode');
        return { 
          success: false, 
          message: 'Device returned NACK - command not supported, invalid user ID, wrong state, or authorization failed'
        };
      } else {
        return { 
          success: false, 
          message: `Unexpected response: ${responseText}` 
        };
      }
    }
    
    throw new Error('No response received from device');
  } catch (error) {
    console.error('Failed to set user working mode:', error);
    throw error;
  }
}

// Validation functions
function isValidUserId(userId) {
  const numericId = parseInt(userId, 10);
  return !isNaN(numericId) && numericId >= 11 && numericId <= 999;
}

function isValidWorkingState(state) {
  return state === 0 || state === 1 || state === '0' || state === '1';
}

function normalizeWorkingState(state) {
  if (state === '0' || state === 0) return 0;
  if (state === '1' || state === 1) return 1;
  throw new Error('Invalid working state');
}

function normalizeUserId(userId) {
  const numericId = parseInt(userId, 10);
  if (isNaN(numericId)) {
    throw new Error('User ID must be numeric');
  }
  return numericId.toString();
}

// Authentication wrapper (references 0x0008 command)
async function authenticateUser(partialUid, userEmail, datatype) {
  // This would use the 0x0008 Authorization command
  // Implementation depends on the authorization command structure
  console.log(`Performing datatype ${datatype} authentication for ${userEmail}`);
  
  // Placeholder implementation - actual authentication would use 0x0008
  return {
    success: true,
    message: 'Authentication successful',
    userEmail: userEmail,
    datatype: datatype
  };
}

// Convenience functions
async function startUserWork(partialUid, userId, userEmail) {
  return await setUserWorkingMode(partialUid, userId, 1, userEmail);
}

async function stopUserWork(partialUid, userId, userEmail) {
  return await setUserWorkingMode(partialUid, userId, 0, userEmail);
}

async function toggleUserWorkingMode(partialUid, userId, userEmail, currentState) {
  const newState = currentState ? 0 : 1;
  return await setUserWorkingMode(partialUid, userId, newState, userEmail);
}

// Safe wrapper with validation
async function setUserWorkingModeSafe(partialUid, userId, workingState, userEmail) {
  try {
    const normalizedUserId = normalizeUserId(userId);
    const normalizedState = normalizeWorkingState(workingState);
    
    return await setUserWorkingMode(partialUid, normalizedUserId, normalizedState, userEmail);
  } catch (error) {
    console.error('Validation error:', error.message);
    throw error;
  }
}

// Batch operations
async function setMultipleUsersWorkingMode(partialUid, userUpdates, userEmail) {
  const results = [];
  
  for (const update of userUpdates) {
    try {
      const result = await setUserWorkingModeSafe(
        partialUid, 
        update.userId, 
        update.workingState, 
        userEmail
      );
      results.push({ ...update, result });
      
      // Small delay to avoid overwhelming the device
      await new Promise(resolve => setTimeout(resolve, 100));
    } catch (error) {
      results.push({ 
        ...update, 
        result: { success: false, error: error.message } 
      });
    }
  }
  
  return results;
}

// Work session management
class WorkSessionManager {
  constructor(partialUid, userEmail) {
    this.partialUid = partialUid;
    this.userEmail = userEmail;
    this.activeSessions = new Map();
  }
  
  async startWorkSession(userId) {
    try {
      const result = await startUserWork(this.partialUid, userId, this.userEmail);
      
      if (result.success) {
        this.activeSessions.set(userId, {
          startTime: new Date(),
          userId: userId
        });
      }
      
      return result;
    } catch (error) {
      console.error(`Failed to start work session for user ${userId}:`, error);
      throw error;
    }
  }
  
  async stopWorkSession(userId) {
    try {
      const result = await stopUserWork(this.partialUid, userId, this.userEmail);
      
      if (result.success) {
        const session = this.activeSessions.get(userId);
        if (session) {
          session.endTime = new Date();
          session.duration = session.endTime - session.startTime;
        }
        this.activeSessions.delete(userId);
      }
      
      return result;
    } catch (error) {
      console.error(`Failed to stop work session for user ${userId}:`, error);
      throw error;
    }
  }
  
  getActiveSession(userId) {
    return this.activeSessions.get(userId);
  }
  
  getAllActiveSessions() {
    return Array.from(this.activeSessions.entries()).map(([userId, session]) => ({
      userId,
      ...session,
      currentDuration: new Date() - session.startTime
    }));
  }
  
  isUserWorking(userId) {
    return this.activeSessions.has(userId);
  }
}

// CID Event monitoring (placeholder for actual CID handling)
function setupWorkingModeCidMonitoring(partialUid, onCidEvent) {
  // This would integrate with the actual CID event system
  console.log('Setting up CID monitoring for working mode events');
  
  // Placeholder implementation
  const mockCidHandler = (cidEvent) => {
    if (cidEvent.type === 'WORK_START' || cidEvent.type === 'WORK_STOP') {
      console.log('Work mode CID event received:', cidEvent);
      if (onCidEvent) {
        onCidEvent(cidEvent);
      }
    }
  };
  
  return mockCidHandler;
}

// Usage demonstration
async function demonstrateUserWorkingMode(partialUid, userEmail) {
  try {
    console.log('=== User Working Mode Demonstration ===');
    
    // Start work for user 25
    console.log('\n--- Starting Work Session ---');
    const startResult = await startUserWork(partialUid, '25', userEmail);
    console.log('Start work result:', startResult);
    
    // Stop work for user 25
    console.log('\n--- Stopping Work Session ---');
    const stopResult = await stopUserWork(partialUid, '25', userEmail);
    console.log('Stop work result:', stopResult);
    
    // Toggle working mode
    console.log('\n--- Toggle Working Mode ---');
    const toggleResult = await toggleUserWorkingMode(partialUid, '150', userEmail, false);
    console.log('Toggle result:', toggleResult);
    
    // Batch operations
    console.log('\n--- Batch User Updates ---');
    const userUpdates = [
      { userId: '25', workingState: 1 },
      { userId: '150', workingState: 1 },
      { userId: '999', workingState: 0 }
    ];
    
    const batchResults = await setMultipleUsersWorkingMode(partialUid, userUpdates, userEmail);
    console.log('Batch results:', batchResults);
    
    // Work session management
    console.log('\n--- Work Session Management ---');
    const sessionManager = new WorkSessionManager(partialUid, userEmail);
    
    await sessionManager.startWorkSession('100');
    await sessionManager.startWorkSession('200');
    
    console.log('Active sessions:', sessionManager.getAllActiveSessions());
    console.log('User 100 working:', sessionManager.isUserWorking('100'));
    
    await sessionManager.stopWorkSession('100');
    console.log('Active sessions after stop:', sessionManager.getAllActiveSessions());
    
    // CID event monitoring setup
    console.log('\n--- CID Event Monitoring ---');
    const cidHandler = setupWorkingModeCidMonitoring(partialUid, (event) => {
      console.log('Working mode event:', event);
    });
    
    // Test validation
    console.log('\n--- Testing Invalid Inputs ---');
    try {
      await setUserWorkingModeSafe(partialUid, '5', 1, userEmail); // Invalid user ID
    } catch (error) {
      console.log('Expected validation error:', error.message);
    }
    
    try {
      await setUserWorkingModeSafe(partialUid, '25', 2, userEmail); // Invalid state
    } catch (error) {
      console.log('Expected validation error:', error.message);
    }
    
    // Test different input formats
    console.log('\n--- Testing Input Format Variations ---');
    const formatTests = [
      { userId: 25, state: '1' },      // Numeric user ID, string state
      { userId: '150', state: 0 },     // String user ID, numeric state
      { userId: '999', state: '0' }    // Both strings
    ];
    
    for (const test of formatTests) {
      try {
        const result = await setUserWorkingModeSafe(partialUid, test.userId, test.state, userEmail);
        console.log(`Format test ${test.userId}:${test.state} - Success:`, result.success);
      } catch (error) {
        console.log(`Format test ${test.userId}:${test.state} - Error:`, error.message);
      }
    }
    
    console.log('User working mode demonstration completed');
    
  } catch (error) {
    console.error('User working mode demonstration failed:', error);
  }
}

0x001F - Panel Status

Requests comprehensive status information from the security panel. Returns detailed ASCII status data including power, communications, network connectivity, and hardware status information.

Request/Response format

Request Format

[HEADER][0x001F] → 070100000000000000001F00

Response Format

[HEADER][0x001F][Panel status in ASCII]

Response Example

← [HEADER][0x001F][AX:1,AXV:13.22,AC:0,ACV:17.22,BT:1,BTV:13.22,BTC:120,BE:0,BEV:13.75,BEM:1,CMS:1,PROT:2,GL:100%,WL:55%,WSI:Trikdis,WS:3,WST:2020-05-22 12:55,SIS:1,SIT:2020-05-22 12:55,RSS:3,RST:2020-05-22 12:55]

Status Field Definitions

Power System Status:

  • AX: Aux voltage output status - 0 overcurrent, 1 OK
  • AXV: Aux voltage in volts (e.g., 13.22)
  • AC: AC voltage fail status - 0 OK, 1 AC failed
  • ACV: AC voltage in volts (e.g., 17.22)

Battery Status:

  • BT: Battery status - 0 battery OK, 1 battery missing
  • BTV: Battery voltage in volts (e.g., 13.22)
  • BTC: Battery charging current in mA (e.g., 120)

Bell System Status:

  • BE: Bell+ overcurrent status - 0 OK, 1 bell+ overcurrent fault
  • BEV: Bell+ voltage in volts (e.g., 13.75)
  • BEM: Bell missing status - 0 bell OK, 1 bell missing

Communication Systems:

  • CMS: CMS communication type - 0 disabled, 1 GPRS/LAN, 2 WiFi, 3 LAN (RS485)
  • PROT: Protegus communication type - 0 disabled, 1 GPRS/LAN, 2 WiFi, 3 LAN (RS485)

Network Status Details

GSM/Cellular:

  • GL: GSM level percentage - -1 no level/GSM trouble, otherwise GSM signal level
  • SIS: SIM card status:
    • 0 Active
    • 1 No SIM
    • 2 PIN fail
    • 3 GPRS network trouble
    • 4 GSM network failure
    • 5 Mobile data trouble
  • SIT: SIM last checked timestamp (e.g., 2020-05-22 12:55)

WiFi Status:

  • WL: WiFi level percentage - -1 no WiFi connected, otherwise WiFi signal level
  • WSI: WiFi SSID name - empty if disabled, otherwise SSID string
  • WS: WiFi status:
    • 0 WiFi OK
    • 1 No SSID
    • 2 Disconnected
    • 3 No internet
    • Note: For HW4/HW6/HW7, this indicates integrated LAN status
  • WST: Last WiFi check timestamp (e.g., 2020-05-22 12:55)

RS485 LAN Module:

  • RSS: RS485 LAN module status:
    • 0 Active
    • 1 Module not programmed (disabled)
    • 2 Module lost
    • 3 No cable or DHCP trouble
    • 4 Internet trouble
  • RST: RS485 LAN module last checked timestamp

Dual SIM (HW7 only):

  • SI2S: SIM2 status (same values as SIS)
  • SI2T: SIM2 last checked timestamp

Hardware Revision Notes

Integrated LAN Status (Revision 3E00): When using integrated LAN, SIS field meanings change:

  • 0 Active
  • 1 No cable
  • 2 DHCP trouble
  • 3 Internet trouble

HW4/HW6/HW7 WiFi Status: WS field represents integrated LAN status instead of WiFi for these hardware revisions.

Cross-References

Related commands for system monitoring:

Usage Example
javascript
async function getPanelStatus(partialUid) {
  // Build panel status request command
  const header = "070100000000000000";
  const command = "1F00";
  const fullCommand = header + command;
  
  try {
    console.log('Requesting panel status...');
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const statusText = hexToAscii(result.data[0]);
      
      if (statusText === 'NACK') {
        return { 
          success: false, 
          message: 'Device returned NACK - panel status not available'
        };
      }
      
      // Parse panel status
      const panelStatus = parsePanelStatus(statusText);
      console.log('Panel status received:', panelStatus);
      
      return { 
        success: true, 
        status: panelStatus,
        rawStatus: statusText
      };
    }
    
    throw new Error('No response received from device');
  } catch (error) {
    console.error('Failed to get panel status:', error);
    throw error;
  }
}

// Parse panel status string into structured object
function parsePanelStatus(statusText) {
  const status = {};
  
  if (!statusText) {
    return status;
  }
  
  // Split by commas and parse key:value pairs
  const pairs = statusText.split(',');
  
  pairs.forEach(pair => {
    const colonIndex = pair.indexOf(':');
    if (colonIndex > 0) {
      const key = pair.substring(0, colonIndex).trim();
      const value = pair.substring(colonIndex + 1).trim();
      
      // Parse numeric values
      if (isNumericField(key)) {
        const numericValue = parseFloat(value);
        status[key] = isNaN(numericValue) ? value : numericValue;
      } else {
        status[key] = value;
      }
    }
  });
  
  return status;
}

// Identify numeric fields for proper parsing
function isNumericField(field) {
  const numericFields = ['AX', 'AXV', 'AC', 'ACV', 'BT', 'BTV', 'BTC', 'BE', 'BEV', 'BEM', 'CMS', 'PROT', 'WS', 'SIS', 'RSS', 'SI2S'];
  return numericFields.includes(field);
}

// Get interpreted status with human-readable descriptions
function interpretPanelStatus(panelStatus) {
  const interpretation = {
    power: interpretPowerStatus(panelStatus),
    battery: interpretBatteryStatus(panelStatus),
    bell: interpretBellStatus(panelStatus),
    communications: interpretCommunicationStatus(panelStatus),
    network: interpretNetworkStatus(panelStatus)
  };
  
  return interpretation;
}

function interpretPowerStatus(status) {
  return {
    auxOutput: {
      status: status.AX === 1 ? 'OK' : 'Overcurrent',
      voltage: status.AXV,
      healthy: status.AX === 1
    },
    acPower: {
      status: status.AC === 0 ? 'OK' : 'Failed',
      voltage: status.ACV,
      healthy: status.AC === 0
    }
  };
}

function interpretBatteryStatus(status) {
  return {
    status: status.BT === 0 ? 'OK' : 'Missing',
    voltage: status.BTV,
    chargingCurrent: status.BTC,
    healthy: status.BT === 0
  };
}

function interpretBellStatus(status) {
  return {
    overcurrent: {
      status: status.BE === 0 ? 'OK' : 'Overcurrent fault',
      healthy: status.BE === 0
    },
    voltage: status.BEV,
    connection: {
      status: status.BEM === 0 ? 'OK' : 'Missing',
      healthy: status.BEM === 0
    }
  };
}

function interpretCommunicationStatus(status) {
  const commTypes = ['Disabled', 'GPRS/LAN', 'WiFi', 'LAN (RS485)'];
  
  return {
    cms: {
      type: status.CMS,
      description: commTypes[status.CMS] || 'Unknown'
    },
    protegus: {
      type: status.PROT,
      description: commTypes[status.PROT] || 'Unknown'
    }
  };
}

function interpretNetworkStatus(status) {
  const network = {};
  
  // GSM Status
  if (status.GL !== undefined) {
    network.gsm = {
      signalLevel: status.GL === '-1' ? null : parseInt(status.GL.replace('%', '')),
      status: status.GL === '-1' ? 'No signal/Trouble' : 'Connected',
      healthy: status.GL !== '-1'
    };
  }
  
  // SIM Status
  if (status.SIS !== undefined) {
    const simStatuses = ['Active', 'No SIM', 'PIN fail', 'GPRS trouble', 'GSM network failure', 'Mobile data trouble'];
    network.sim = {
      status: status.SIS,
      description: simStatuses[status.SIS] || 'Unknown',
      lastChecked: status.SIT,
      healthy: status.SIS === 0
    };
  }
  
  // WiFi Status
  if (status.WL !== undefined) {
    network.wifi = {
      signalLevel: status.WL === '-1' ? null : parseInt(status.WL.replace('%', '')),
      ssid: status.WSI || null,
      status: getWifiStatusDescription(status.WS),
      lastChecked: status.WST,
      healthy: status.WS === 0,
      connected: status.WL !== '-1'
    };
  }
  
  // RS485 LAN Status
  if (status.RSS !== undefined) {
    const rs485Statuses = ['Active', 'Not programmed', 'Module lost', 'Cable/DHCP trouble', 'Internet trouble'];
    network.rs485 = {
      status: status.RSS,
      description: rs485Statuses[status.RSS] || 'Unknown',
      lastChecked: status.RST,
      healthy: status.RSS === 0
    };
  }
  
  // SIM2 Status (HW7 only)
  if (status.SI2S !== undefined) {
    const simStatuses = ['Active', 'No SIM', 'PIN fail', 'GPRS trouble', 'GSM network failure', 'Mobile data trouble'];
    network.sim2 = {
      status: status.SI2S,
      description: simStatuses[status.SI2S] || 'Unknown',
      lastChecked: status.SI2T,
      healthy: status.SI2S === 0
    };
  }
  
  return network;
}

function getWifiStatusDescription(status) {
  const wifiStatuses = ['WiFi OK', 'No SSID', 'Disconnected', 'No internet'];
  return wifiStatuses[status] || 'Unknown';
}

// Health check functions
function checkPanelHealth(panelStatus) {
  const interpretation = interpretPanelStatus(panelStatus);
  const issues = [];
  const warnings = [];
  
  // Power issues
  if (!interpretation.power.auxOutput.healthy) {
    issues.push('Auxiliary output overcurrent detected');
  }
  
  if (!interpretation.power.acPower.healthy) {
    issues.push('AC power failure detected');
  }
  
  // Battery issues
  if (!interpretation.battery.healthy) {
    issues.push('Battery missing or failed');
  }
  
  if (interpretation.battery.voltage < 11.0) {
    warnings.push('Low battery voltage');
  }
  
  // Bell issues
  if (!interpretation.bell.overcurrent.healthy) {
    issues.push('Bell overcurrent fault');
  }
  
  if (!interpretation.bell.connection.healthy) {
    warnings.push('Bell disconnected');
  }
  
  // Network issues
  const network = interpretation.network;
  
  if (network.sim && !network.sim.healthy) {
    issues.push(`SIM card issue: ${network.sim.description}`);
  }
  
  if (network.gsm && !network.gsm.healthy) {
    issues.push('GSM signal trouble');
  }
  
  if (network.wifi && network.wifi.connected && !network.wifi.healthy) {
    warnings.push(`WiFi issue: ${network.wifi.status}`);
  }
  
  if (network.rs485 && !network.rs485.healthy) {
    warnings.push(`RS485 LAN issue: ${network.rs485.description}`);
  }
  
  return {
    overall: issues.length === 0 ? 'healthy' : 'issues_detected',
    critical: issues,
    warnings: warnings,
    issueCount: issues.length,
    warningCount: warnings.length
  };
}

// Monitoring functions
function monitorPanelStatus(partialUid, intervalSeconds = 60, onStatusUpdate) {
  let monitoring = true;
  
  const checkStatus = async () => {
    if (!monitoring) return;
    
    try {
      const result = await getPanelStatus(partialUid);
      
      if (result.success) {
        const interpretation = interpretPanelStatus(result.status);
        const health = checkPanelHealth(result.status);
        
        if (onStatusUpdate) {
          onStatusUpdate({
            timestamp: new Date(),
            raw: result.status,
            interpreted: interpretation,
            health: health
          });
        }
      }
    } catch (error) {
      console.error('Panel status monitoring error:', error);
    }
    
    // Schedule next check
    if (monitoring) {
      setTimeout(checkStatus, intervalSeconds * 1000);
    }
  };
  
  // Start monitoring
  checkStatus();
  
  // Return stop function
  return () => {
    monitoring = false;
  };
}

// Convenience functions
async function getPowerStatus(partialUid) {
  const result = await getPanelStatus(partialUid);
  if (result.success) {
    return interpretPowerStatus(result.status);
  }
  return null;
}

async function getBatteryStatus(partialUid) {
  const result = await getPanelStatus(partialUid);
  if (result.success) {
    return interpretBatteryStatus(result.status);
  }
  return null;
}

async function getNetworkStatus(partialUid) {
  const result = await getPanelStatus(partialUid);
  if (result.success) {
    return interpretNetworkStatus(result.status);
  }
  return null;
}

async function checkCommunicationHealth(partialUid) {
  const result = await getPanelStatus(partialUid);
  if (result.success) {
    const network = interpretNetworkStatus(result.status);
    const health = {
      gsm: network.gsm?.healthy || false,
      wifi: network.wifi?.healthy || false,
      rs485: network.rs485?.healthy || false
    };
    
    health.overall = health.gsm || health.wifi || health.rs485;
    return health;
  }
  return null;
}

// Usage demonstration
async function demonstratePanelStatus(partialUid) {
  try {
    console.log('=== Panel Status Demonstration ===');
    
    // Get full panel status
    console.log('\n--- Getting Panel Status ---');
    const statusResult = await getPanelStatus(partialUid);
    console.log('Panel status result:', statusResult);
    
    if (statusResult.success) {
      // Interpret status
      console.log('\n--- Status Interpretation ---');
      const interpretation = interpretPanelStatus(statusResult.status);
      console.log('Power status:', interpretation.power);
      console.log('Battery status:', interpretation.battery);
      console.log('Network status:', interpretation.network);
      
      // Health check
      console.log('\n--- Health Check ---');
      const health = checkPanelHealth(statusResult.status);
      console.log('Health summary:', health);
      
      if (health.critical.length > 0) {
        console.log('Critical issues:', health.critical);
      }
      
      if (health.warnings.length > 0) {
        console.log('Warnings:', health.warnings);
      }
      
      // Specific status checks
      console.log('\n--- Specific Status Checks ---');
      const powerStatus = interpretPowerStatus(statusResult.status);
      console.log('Power healthy:', powerStatus.auxOutput.healthy && powerStatus.acPower.healthy);
      
      const batteryStatus = interpretBatteryStatus(statusResult.status);
      console.log('Battery voltage:', batteryStatus.voltage, 'V');
      
      const commHealth = await checkCommunicationHealth(partialUid);
      console.log('Communication health:', commHealth);
    }
    
    // Start monitoring (demo for 30 seconds)
    console.log('\n--- Starting Status Monitoring ---');
    const stopMonitoring = monitorPanelStatus(partialUid, 10, (update) => {
      console.log(`Status update: ${update.health.overall} (${update.health.issueCount} issues, ${update.health.warningCount} warnings)`);
    });
    
    // Stop monitoring after 30 seconds
    setTimeout(() => {
      stopMonitoring();
      console.log('Monitoring stopped');
    }, 30000);
    
    console.log('Panel status demonstration completed');
    
  } catch (error) {
    console.error('Panel status demonstration failed:', error);
  }
}

0x0020 - RF Sensors Data

Requests wireless RF sensor data from the security panel. Returns comprehensive information for up to 42 wireless sensors including sensor type, signal strength, temperature, and battery voltage.

Request/Response format

Request Format

[HEADER][0x0020] → 070100000000000000002000

Response Format

[HEADER][0x0020][RF sensors data in ASCII]

Response Example

← [HEADER][0x0020][ID:32,L:20,T:27,V:3.20|ID:34,L:0,T:24,V:3.25|ID:36,L:0,T:0,V:0.0|ID:31,L:85,T:22,V:3.15|ID:97,L:75,T:25,V:3.30]

Sensor Data Format

Data Structure:

  • Separator: Pipe character (|) separates individual sensors
  • Field Format: FieldName:Value pairs separated by commas
  • Maximum Sensors: Up to 42 sensors supported
  • Fields Per Sensor: ID, L (RSSI level), T (temperature), V (voltage)

Field Definitions:

  • ID: Hexadecimal sensor type identifier (2 characters)
  • L: RSSI signal level as percentage (0-100%)
  • T: Internal sensor temperature in Celsius (0 = not available)
  • V: Battery voltage in volts (0.0 = not available)

Sensor Type Definitions

Security Sensors:

  • 31: Wireless PIR, Curtain PIR
  • 37: Wireless outdoor PIR
  • 32: Magnetic/vibration sensor
  • 33: Pendant button (panic button)
  • 34: Smoke sensor
  • 36: Glass break sensor
  • 38: Flood sensor
  • 39: Magnetic sensor
  • 3C: Temperature, magnetic, flood sensor (multi-function)

System Devices:

  • 97: Wireless icon keypad
  • 45: Wireless siren
  • 46: Wireless siren (alternate type)

Data Availability Notes

Signal Level (L):

  • Range: 0-100%
  • 0: No signal or sensor offline
  • 1-100: Signal strength percentage

Temperature (T):

  • Range: Celsius temperature reading
  • 0: Temperature not available or sensor doesn't support temperature
  • Typical Range: -40°C to +85°C for most sensors

Voltage (V):

  • Format: Decimal voltage (e.g., 3.20, 3.25)
  • 0.0: Voltage not available or sensor doesn't report voltage
  • Typical Range: 2.0V to 3.6V for lithium batteries

Cross-References

Related commands for sensor management:

Usage Example
javascript
async function getRfSensorsData(partialUid) {
  // Build RF sensors data request command
  const header = "070100000000000000";
  const command = "2000";
  const fullCommand = header + command;
  
  try {
    console.log('Requesting RF sensors data...');
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const sensorsText = hexToAscii(result.data[0]);
      
      if (sensorsText === 'NACK') {
        return { 
          success: false, 
          message: 'Device returned NACK - RF sensors data not available'
        };
      }
      
      // Parse RF sensors data
      const sensorsData = parseRfSensorsData(sensorsText);
      console.log(`RF sensors data received: ${sensorsData.length} sensors`);
      
      return { 
        success: true, 
        sensors: sensorsData,
        sensorCount: sensorsData.length,
        rawData: sensorsText
      };
    }
    
    throw new Error('No response received from device');
  } catch (error) {
    console.error('Failed to get RF sensors data:', error);
    throw error;
  }
}

// Parse RF sensors data string into structured array
function parseRfSensorsData(sensorsText) {
  const sensors = [];
  
  if (!sensorsText) {
    return sensors;
  }
  
  // Split by pipe separator
  const sensorStrings = sensorsText.split('|').filter(s => s.trim().length > 0);
  
  sensorStrings.forEach((sensorString, index) => {
    try {
      const sensorData = parseSingleSensor(sensorString.trim(), index + 1);
      if (sensorData) {
        sensors.push(sensorData);
      }
    } catch (error) {
      console.warn(`Failed to parse sensor ${index + 1}: ${error.message}`);
    }
  });
  
  return sensors;
}

// Parse individual sensor data string
function parseSingleSensor(sensorString, sensorIndex) {
  const sensor = { index: sensorIndex };
  
  // Split by commas and parse key:value pairs
  const pairs = sensorString.split(',');
  
  pairs.forEach(pair => {
    const colonIndex = pair.indexOf(':');
    if (colonIndex > 0) {
      const key = pair.substring(0, colonIndex).trim();
      const value = pair.substring(colonIndex + 1).trim();
      
      switch (key) {
        case 'ID':
          sensor.id = value;
          sensor.type = getSensorTypeDescription(value);
          sensor.category = getSensorCategory(value);
          break;
        case 'L':
          sensor.rssi = parseInt(value, 10);
          sensor.signalStrength = getRssiDescription(sensor.rssi);
          sensor.online = sensor.rssi > 0;
          break;
        case 'T':
          sensor.temperature = parseInt(value, 10);
          sensor.temperatureAvailable = sensor.temperature !== 0;
          break;
        case 'V':
          sensor.voltage = parseFloat(value);
          sensor.voltageAvailable = sensor.voltage !== 0.0;
          sensor.batteryLevel = getBatteryLevelDescription(sensor.voltage);
          break;
      }
    }
  });
  
  // Add sensor health assessment
  sensor.health = assessSensorHealth(sensor);
  
  return sensor;
}

// Get sensor type description from ID
function getSensorTypeDescription(sensorId) {
  const sensorTypes = {
    '31': 'Wireless PIR / Curtain PIR',
    '37': 'Wireless Outdoor PIR',
    '32': 'Magnetic/Vibration Sensor',
    '33': 'Pendant Button (Panic)',
    '34': 'Smoke Sensor',
    '36': 'Glass Break Sensor',
    '38': 'Flood Sensor',
    '39': 'Magnetic Sensor',
    '3C': 'Multi-function Sensor (Temp/Magnetic/Flood)',
    '97': 'Wireless Icon Keypad',
    '45': 'Wireless Siren',
    '46': 'Wireless Siren (Alt)'
  };
  
  return sensorTypes[sensorId] || `Unknown Sensor (${sensorId})`;
}

// Get sensor category for grouping
function getSensorCategory(sensorId) {
  const categories = {
    '31': 'Motion Detection',
    '37': 'Motion Detection',
    '32': 'Door/Window',
    '33': 'Emergency',
    '34': 'Fire Safety',
    '36': 'Intrusion',
    '38': 'Environmental',
    '39': 'Door/Window',
    '3C': 'Multi-function',
    '97': 'Control',
    '45': 'Notification',
    '46': 'Notification'
  };
  
  return categories[sensorId] || 'Unknown';
}

// Get RSSI signal strength description
function getRssiDescription(rssi) {
  if (rssi === 0) return 'Offline';
  if (rssi >= 80) return 'Excellent';
  if (rssi >= 60) return 'Good';
  if (rssi >= 40) return 'Fair';
  if (rssi >= 20) return 'Poor';
  return 'Very Poor';
}

// Get battery level description from voltage
function getBatteryLevelDescription(voltage) {
  if (voltage === 0.0) return 'Not Available';
  if (voltage >= 3.0) return 'Good';
  if (voltage >= 2.7) return 'Fair';
  if (voltage >= 2.4) return 'Low';
  return 'Critical';
}

// Assess overall sensor health
function assessSensorHealth(sensor) {
  const issues = [];
  const warnings = [];
  
  // Check connectivity
  if (!sensor.online) {
    issues.push('Sensor offline - no signal');
  } else if (sensor.rssi < 20) {
    warnings.push('Very weak signal strength');
  } else if (sensor.rssi < 40) {
    warnings.push('Weak signal strength');
  }
  
  // Check battery
  if (sensor.voltageAvailable) {
    if (sensor.voltage < 2.4) {
      issues.push('Critical battery level');
    } else if (sensor.voltage < 2.7) {
      warnings.push('Low battery level');
    }
  }
  
  // Check temperature (if available)
  if (sensor.temperatureAvailable) {
    if (sensor.temperature < -20 || sensor.temperature > 60) {
      warnings.push('Temperature out of normal range');
    }
  }
  
  return {
    status: issues.length > 0 ? 'critical' : (warnings.length > 0 ? 'warning' : 'healthy'),
    issues: issues,
    warnings: warnings,
    score: calculateHealthScore(sensor)
  };
}

// Calculate numeric health score (0-100)
function calculateHealthScore(sensor) {
  let score = 100;
  
  // Signal strength impact (0-30 points)
  if (!sensor.online) {
    score -= 50;
  } else {
    score -= Math.max(0, (80 - sensor.rssi) * 0.375); // Linear decrease from 80% to 0%
  }
  
  // Battery impact (0-20 points)
  if (sensor.voltageAvailable) {
    if (sensor.voltage < 2.4) {
      score -= 30;
    } else if (sensor.voltage < 2.7) {
      score -= 15;
    } else if (sensor.voltage < 3.0) {
      score -= 5;
    }
  }
  
  return Math.max(0, Math.round(score));
}

// Filter and query functions
function filterSensorsByType(sensors, sensorTypes) {
  if (!Array.isArray(sensorTypes)) {
    sensorTypes = [sensorTypes];
  }
  
  return sensors.filter(sensor => sensorTypes.includes(sensor.id));
}

function filterSensorsByCategory(sensors, category) {
  return sensors.filter(sensor => sensor.category === category);
}

function filterOnlineSensors(sensors) {
  return sensors.filter(sensor => sensor.online);
}

function filterOfflineSensors(sensors) {
  return sensors.filter(sensor => !sensor.online);
}

function filterLowBatterySensors(sensors, voltageThreshold = 2.7) {
  return sensors.filter(sensor => 
    sensor.voltageAvailable && sensor.voltage < voltageThreshold
  );
}

function filterWeakSignalSensors(sensors, rssiThreshold = 40) {
  return sensors.filter(sensor => 
    sensor.online && sensor.rssi < rssiThreshold
  );
}

// Analysis functions
function analyzeSensorHealth(sensors) {
  const analysis = {
    total: sensors.length,
    online: 0,
    offline: 0,
    healthy: 0,
    warnings: 0,
    critical: 0,
    categories: {},
    batteryIssues: 0,
    signalIssues: 0
  };
  
  sensors.forEach(sensor => {
    // Count by status
    if (sensor.online) analysis.online++;
    else analysis.offline++;
    
    // Count by health
    switch (sensor.health.status) {
      case 'healthy': analysis.healthy++; break;
      case 'warning': analysis.warnings++; break;
      case 'critical': analysis.critical++; break;
    }
    
    // Count by category
    const category = sensor.category;
    if (!analysis.categories[category]) {
      analysis.categories[category] = { total: 0, online: 0, healthy: 0 };
    }
    analysis.categories[category].total++;
    if (sensor.online) analysis.categories[category].online++;
    if (sensor.health.status === 'healthy') analysis.categories[category].healthy++;
    
    // Count specific issues
    if (sensor.voltageAvailable && sensor.voltage < 2.7) {
      analysis.batteryIssues++;
    }
    if (sensor.online && sensor.rssi < 40) {
      analysis.signalIssues++;
    }
  });
  
  // Calculate percentages
  analysis.onlinePercentage = analysis.total > 0 ? (analysis.online / analysis.total * 100).toFixed(1) : 0;
  analysis.healthyPercentage = analysis.total > 0 ? (analysis.healthy / analysis.total * 100).toFixed(1) : 0;
  
  return analysis;
}

// Monitoring functions
function monitorRfSensors(partialUid, intervalSeconds = 300, onSensorsUpdate) {
  let monitoring = true;
  
  const checkSensors = async () => {
    if (!monitoring) return;
    
    try {
      const result = await getRfSensorsData(partialUid);
      
      if (result.success) {
        const analysis = analyzeSensorHealth(result.sensors);
        
        if (onSensorsUpdate) {
          onSensorsUpdate({
            timestamp: new Date(),
            sensors: result.sensors,
            analysis: analysis,
            issues: result.sensors.filter(s => s.health.status === 'critical'),
            warnings: result.sensors.filter(s => s.health.status === 'warning')
          });
        }
      }
    } catch (error) {
      console.error('RF sensors monitoring error:', error);
    }
    
    // Schedule next check
    if (monitoring) {
      setTimeout(checkSensors, intervalSeconds * 1000);
    }
  };
  
  // Start monitoring
  checkSensors();
  
  // Return stop function
  return () => {
    monitoring = false;
  };
}

// Convenience functions
async function getOnlineSensors(partialUid) {
  const result = await getRfSensorsData(partialUid);
  if (result.success) {
    return filterOnlineSensors(result.sensors);
  }
  return [];
}

async function getSensorsByCategory(partialUid, category) {
  const result = await getRfSensorsData(partialUid);
  if (result.success) {
    return filterSensorsByCategory(result.sensors, category);
  }
  return [];
}

async function getLowBatterySensors(partialUid, threshold = 2.7) {
  const result = await getRfSensorsData(partialUid);
  if (result.success) {
    return filterLowBatterySensors(result.sensors, threshold);
  }
  return [];
}

async function getSensorHealthSummary(partialUid) {
  const result = await getRfSensorsData(partialUid);
  if (result.success) {
    return analyzeSensorHealth(result.sensors);
  }
  return null;
}

// Usage demonstration
async function demonstrateRfSensors(partialUid) {
  try {
    console.log('=== RF Sensors Data Demonstration ===');
    
    // Get all sensor data
    console.log('\n--- Getting RF Sensors Data ---');
    const sensorsResult = await getRfSensorsData(partialUid);
    console.log('Sensors result:', sensorsResult);
    
    if (sensorsResult.success) {
      // Analyze sensor health
      console.log('\n--- Sensor Health Analysis ---');
      const analysis = analyzeSensorHealth(sensorsResult.sensors);
      console.log('Health analysis:', analysis);
      
      // Show sensors by category
      console.log('\n--- Sensors by Category ---');
      Object.entries(analysis.categories).forEach(([category, stats]) => {
        console.log(`${category}: ${stats.online}/${stats.total} online, ${stats.healthy} healthy`);
      });
      
      // Filter specific sensor types
      console.log('\n--- Motion Sensors ---');
      const motionSensors = filterSensorsByCategory(sensorsResult.sensors, 'Motion Detection');
      motionSensors.forEach(sensor => {
        console.log(`  ${sensor.type}: Signal ${sensor.rssi}%, Battery ${sensor.voltage}V`);
      });
      
      // Check for issues
      console.log('\n--- Sensor Issues ---');
      const lowBattery = filterLowBatterySensors(sensorsResult.sensors);
      if (lowBattery.length > 0) {
        console.log('Low battery sensors:', lowBattery.map(s => `${s.type} (${s.voltage}V)`));
      }
      
      const weakSignal = filterWeakSignalSensors(sensorsResult.sensors);
      if (weakSignal.length > 0) {
        console.log('Weak signal sensors:', weakSignal.map(s => `${s.type} (${s.rssi}%)`));
      }
      
      const offline = filterOfflineSensors(sensorsResult.sensors);
      if (offline.length > 0) {
        console.log('Offline sensors:', offline.map(s => s.type));
      }
      
      // Get specific categories
      console.log('\n--- Fire Safety Sensors ---');
      const fireSensors = await getSensorsByCategory(partialUid, 'Fire Safety');
      console.log('Fire sensors:', fireSensors.length);
      
      console.log('\n--- Emergency Sensors ---');
      const emergencySensors = await getSensorsByCategory(partialUid, 'Emergency');
      console.log('Emergency sensors:', emergencySensors.length);
    }
    
    // Start monitoring (demo for 60 seconds)
    console.log('\n--- Starting Sensor Monitoring ---');
    const stopMonitoring = monitorRfSensors(partialUid, 30, (update) => {
      console.log(`Sensor update: ${update.analysis.online}/${update.analysis.total} online, ${update.issues.length} critical issues`);
    });
    
    // Stop monitoring after 60 seconds
    setTimeout(() => {
      stopMonitoring();
      console.log('Sensor monitoring stopped');
    }, 60000);
    
    console.log('RF sensors demonstration completed');
    
  } catch (error) {
    console.error('RF sensors demonstration failed:', error);
  }
}

0x0021 - Read Settings

Available on G16x, E16x devices

Status: Undocumented command - implementation details not available.


0x0022 - Write Settings

Available on G16x, E16x devices

Status: Undocumented command - implementation details not available.


0x0023 - Cancel Alarm/Silence Siren

Cancels alarm state and silences sirens for a specific area without disarming the system. Requires user authorization and can optionally generate Cancel Alarm CID events to monitoring stations.

Request/Response format

Request Format

[HEADER][0x0023][Area:Silence:Cancel] → 0701000000000000002300[AreaSilenceData]

Request Examples

Silence alarm in area 1 with CID event:

[HEADER][0x0023][1:1:1]  // Area 1, silence alarm, generate CID
→ 0701000000000000002300313A313A31

Cancel alarm in area 2 without silencing:

[HEADER][0x0023][2:0:1]  // Area 2, don't silence, generate CID
→ 0701000000000000002300323A303A31

Silence area 3 without CID event:

[HEADER][0x0023][3:1:0]  // Area 3, silence alarm, no CID
→ 0701000000000000002300333A313A30

Response Format

Success Response:

[HEADER][0x0023][OK]

Error Response:

[HEADER][0x0023][NACK]  // Command not supported, wrong data, or authorization failed

Parameter Definitions

Area Number:

  • Range: Valid area numbers configured in the system
  • Purpose: Specifies which area to silence or cancel alarm
  • Format: Numeric string (e.g., "1", "2", "3")

Silence Parameter:

  • 0: Alarm will not be turned off (sirens continue)
  • 1: Alarm will be silenced (sirens and buzzers turned off)
  • Purpose: Controls local alarm indication without disarming

Cancel Parameter:

  • 0: Cancel CID will not be generated
  • 1: Cancel CID will be generated to all IP channels
  • Purpose: Notifies monitoring stations of alarm cancellation

Authorization Requirements

User Authentication:

  • Required: User authorization must be completed before command execution
  • Authentication Type: Valid user credentials required
  • Security Level: Appropriate user access level for alarm management

Access Control:

  • Only authorized users can cancel alarms
  • User must have alarm management permissions
  • Authentication failure results in NACK response

Operational Behavior

Alarm Silencing (Silence=1):

  • Sirens: All sirens in specified area turned off
  • Buzzers: All buzzers and audible indicators silenced
  • System State: Area remains in alarm state but audio notifications stopped
  • Restoration: Manual disarm or auto-restore required to clear alarm state

CID Event Generation (Cancel=1):

  • Event Type: Cancel Alarm CID event
  • Recipients: All configured IP channels (monitoring stations)
  • Content: Area information and timestamp
  • Standard: Follows Contact ID protocol specifications

Use Cases

Emergency Situations:

  • False Alarm: Silence sirens while maintaining alarm state
  • Controlled Response: Stop audio alerts without full disarm
  • Monitoring Integration: Notify stations of alarm cancellation

Operational Scenarios:

  • Area Isolation: Silence specific area while others remain active
  • Partial Cancellation: Cancel alarm indication without system disarm
  • Professional Response: Allow security personnel to manage alarm audio

Cross-References

Related commands for alarm management:

Usage Example
javascript
async function cancelAlarmSilenceSiren(partialUid, areaNumber, silenceAlarm, generateCid, userCredentials) {
  // Validate parameters
  if (!isValidAreaNumber(areaNumber)) {
    throw new Error('Invalid area number');
  }
  
  if (!isValidBooleanFlag(silenceAlarm) || !isValidBooleanFlag(generateCid)) {
    throw new Error('Silence and Cancel parameters must be 0 or 1');
  }
  
  if (!userCredentials) {
    throw new Error('User credentials required for alarm cancellation');
  }
  
  try {
    // Step 1: Authenticate user
    console.log('Authenticating user for alarm cancellation...');
    const authResult = await authenticateUser(partialUid, userCredentials);
    
    if (!authResult.success) {
      throw new Error('Authentication failed: ' + authResult.message);
    }
    
    // Step 2: Build cancel alarm command
    const header = "070100000000000000";
    const command = "2300";
    
    const silenceFlag = silenceAlarm ? '1' : '0';
    const cancelFlag = generateCid ? '1' : '0';
    const alarmData = `${areaNumber}:${silenceFlag}:${cancelFlag}`;
    
    const dataHex = asciiToHex(alarmData);
    const fullCommand = header + command + dataHex;
    
    console.log(`Cancelling alarm for area ${areaNumber} (silence: ${silenceFlag}, CID: ${cancelFlag})`);
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      
      if (responseText === 'OK') {
        const actions = [];
        if (silenceAlarm) actions.push('alarm silenced');
        if (generateCid) actions.push('CID event generated');
        
        const actionDescription = actions.length > 0 ? actions.join(', ') : 'alarm cancelled';
        console.log(`Area ${areaNumber} ${actionDescription} successfully`);
        
        return { 
          success: true, 
          message: `Area ${areaNumber} alarm cancelled (${actionDescription})`,
          areaNumber: areaNumber,
          silenced: silenceAlarm,
          cidGenerated: generateCid
        };
      } else if (responseText === 'NACK') {
        console.log('Failed to cancel alarm');
        return { 
          success: false, 
          message: 'Device returned NACK - command not supported, wrong data, or authorization failed'
        };
      } else {
        return { 
          success: false, 
          message: `Unexpected response: ${responseText}` 
        };
      }
    }
    
    throw new Error('No response received from device');
  } catch (error) {
    console.error('Failed to cancel alarm:', error);
    throw error;
  }
}

// Validation functions
function isValidAreaNumber(areaNumber) {
  const numericArea = parseInt(areaNumber, 10);
  return !isNaN(numericArea) && numericArea >= 1 && numericArea <= 99; // Assuming max 99 areas
}

function isValidBooleanFlag(flag) {
  return flag === 0 || flag === 1 || flag === '0' || flag === '1';
}

function normalizeBooleanFlag(flag) {
  return (flag === '1' || flag === 1) ? 1 : 0;
}

// Convenience functions
async function silenceAlarmWithCid(partialUid, areaNumber, userCredentials) {
  return await cancelAlarmSilenceSiren(partialUid, areaNumber, true, true, userCredentials);
}

async function silenceAlarmOnly(partialUid, areaNumber, userCredentials) {
  return await cancelAlarmSilenceSiren(partialUid, areaNumber, true, false, userCredentials);
}

async function cancelAlarmWithCid(partialUid, areaNumber, userCredentials) {
  return await cancelAlarmSilenceSiren(partialUid, areaNumber, false, true, userCredentials);
}

async function generateCancelCidOnly(partialUid, areaNumber, userCredentials) {
  return await cancelAlarmSilenceSiren(partialUid, areaNumber, false, true, userCredentials);
}

// Safe wrapper with validation
async function cancelAlarmSafe(partialUid, areaNumber, silenceAlarm, generateCid, userCredentials) {
  try {
    const normalizedArea = parseInt(areaNumber, 10).toString();
    const normalizedSilence = normalizeBooleanFlag(silenceAlarm);
    const normalizedCancel = normalizeBooleanFlag(generateCid);
    
    return await cancelAlarmSilenceSiren(
      partialUid, 
      normalizedArea, 
      normalizedSilence, 
      normalizedCancel, 
      userCredentials
    );
  } catch (error) {
    console.error('Validation error:', error.message);
    throw error;
  }
}

// Batch operations for multiple areas
async function cancelMultipleAreas(partialUid, areaConfigs, userCredentials) {
  const results = [];
  
  // Authenticate once for all operations
  console.log('Authenticating user for batch alarm cancellation...');
  const authResult = await authenticateUser(partialUid, userCredentials);
  
  if (!authResult.success) {
    throw new Error('Authentication failed: ' + authResult.message);
  }
  
  for (const config of areaConfigs) {
    try {
      const result = await cancelAlarmSafe(
        partialUid, 
        config.area, 
        config.silence, 
        config.generateCid, 
        userCredentials
      );
      results.push({ ...config, result });
      
      // Small delay to avoid overwhelming the device
      await new Promise(resolve => setTimeout(resolve, 200));
    } catch (error) {
      results.push({ 
        ...config, 
        result: { success: false, error: error.message } 
      });
    }
  }
  
  return results;
}

// Emergency alarm management
class AlarmManager {
  constructor(partialUid, userCredentials) {
    this.partialUid = partialUid;
    this.userCredentials = userCredentials;
    this.activeAlarms = new Map();
  }
  
  async silenceArea(areaNumber, generateCid = true) {
    try {
      const result = await silenceAlarmWithCid(
        this.partialUid, 
        areaNumber, 
        this.userCredentials
      );
      
      if (result.success) {
        this.activeAlarms.set(areaNumber, {
          silenced: true,
          timestamp: new Date(),
          cidGenerated: generateCid
        });
      }
      
      return result;
    } catch (error) {
      console.error(`Failed to silence area ${areaNumber}:`, error);
      throw error;
    }
  }
  
  async cancelAreaAlarm(areaNumber) {
    try {
      const result = await cancelAlarmWithCid(
        this.partialUid, 
        areaNumber, 
        this.userCredentials
      );
      
      if (result.success) {
        this.activeAlarms.delete(areaNumber);
      }
      
      return result;
    } catch (error) {
      console.error(`Failed to cancel alarm for area ${areaNumber}:`, error);
      throw error;
    }
  }
  
  async silenceAllAreas(areas) {
    const configs = areas.map(area => ({
      area: area,
      silence: true,
      generateCid: true
    }));
    
    return await cancelMultipleAreas(this.partialUid, configs, this.userCredentials);
  }
  
  getSilencedAreas() {
    return Array.from(this.activeAlarms.entries()).map(([area, info]) => ({
      area,
      ...info
    }));
  }
  
  isAreaSilenced(areaNumber) {
    return this.activeAlarms.has(areaNumber);
  }
}

// CID event monitoring (placeholder for actual CID handling)
function setupAlarmCancellationCidMonitoring(partialUid, onCidEvent) {
  console.log('Setting up CID monitoring for alarm cancellation events');
  
  // Placeholder implementation
  const mockCidHandler = (cidEvent) => {
    if (cidEvent.type === 'CANCEL_ALARM') {
      console.log('Alarm cancellation CID event received:', cidEvent);
      if (onCidEvent) {
        onCidEvent(cidEvent);
      }
    }
  };
  
  return mockCidHandler;
}

// Usage demonstration
async function demonstrateAlarmCancellation(partialUid, userCredentials) {
  try {
    console.log('=== Alarm Cancellation Demonstration ===');
    
    // Silence alarm with CID
    console.log('\n--- Silencing Alarm with CID ---');
    const silenceResult = await silenceAlarmWithCid(partialUid, '1', userCredentials);
    console.log('Silence result:', silenceResult);
    
    // Cancel alarm without silencing
    console.log('\n--- Cancel Alarm Only ---');
    const cancelResult = await cancelAlarmWithCid(partialUid, '2', userCredentials);
    console.log('Cancel result:', cancelResult);
    
    // Silence without CID
    console.log('\n--- Silence Without CID ---');
    const silenceNoResult = await silenceAlarmOnly(partialUid, '3', userCredentials);
    console.log('Silence only result:', silenceNoResult);
    
    // Batch operations
    console.log('\n--- Batch Area Operations ---');
    const areaConfigs = [
      { area: '1', silence: true, generateCid: true },
      { area: '2', silence: true, generateCid: false },
      { area: '3', silence: false, generateCid: true }
    ];
    
    const batchResults = await cancelMultipleAreas(partialUid, areaConfigs, userCredentials);
    console.log('Batch results:', batchResults);
    
    // Alarm manager demo
    console.log('\n--- Alarm Manager ---');
    const alarmManager = new AlarmManager(partialUid, userCredentials);
    
    await alarmManager.silenceArea('5');
    await alarmManager.silenceArea('6');
    
    console.log('Silenced areas:', alarmManager.getSilencedAreas());
    console.log('Area 5 silenced:', alarmManager.isAreaSilenced('5'));
    
    await alarmManager.cancelAreaAlarm('5');
    console.log('Areas after cancellation:', alarmManager.getSilencedAreas());
    
    // CID monitoring setup
    console.log('\n--- CID Event Monitoring ---');
    const cidHandler = setupAlarmCancellationCidMonitoring(partialUid, (event) => {
      console.log('Alarm cancellation event:', event);
    });
    
    // Test validation
    console.log('\n--- Testing Invalid Inputs ---');
    try {
      await cancelAlarmSafe(partialUid, 'invalid', 1, 1, userCredentials);
    } catch (error) {
      console.log('Expected validation error:', error.message);
    }
    
    try {
      await cancelAlarmSafe(partialUid, '1', 'invalid', 1, userCredentials);
    } catch (error) {
      console.log('Expected validation error:', error.message);
    }
    
    console.log('Alarm cancellation demonstration completed');
    
  } catch (error) {
    console.error('Alarm cancellation demonstration failed:', error);
  }
}

0x0024 - Panic/Medical/Fire Event

Generates emergency events (fire, panic, or medical) with optional local alarm activation. Events are always sent to CMS (Central Monitoring Station) via CID protocol, with configurable local alarm indication.

Request/Response format

Request Format

[HEADER][0x0024][Event:Alarm] → 0701000000000000002400[EventAlarmData]

Request Examples

Medical event with local alarm:

[HEADER][0x0024][0:1]  // Medical event, generate local alarm
→ 0701000000000000002400303A31

Fire event silent (CID only):

[HEADER][0x0024][1:0]  // Fire event, CID only, no local alarm
→ 0701000000000000002400313A30

Panic event with local alarm:

[HEADER][0x0024][2:1]  // Panic event, generate local alarm
→ 0701000000000000002400323A31

Response Format

Success Response:

[HEADER][0x0024][OK]

Error Response:

[HEADER][0x0024][NACK]  // Command not supported or wrong data

Event Type Definitions

Event Types:

  • 0 - Medical Event: Medical emergency or assistance required
  • 1 - Fire Event: Fire alarm or smoke detection emergency
  • 2 - Panic Event: Security threat or panic situation

Alarm Generation:

  • 0 - Silent: CID event to CMS only, no local alarm activation
  • 1 - Audible: CID event to CMS plus local alarm (sirens, strobes)

Emergency Response Behavior

CID Event Generation:

  • Always Generated: All emergency events create CID messages to CMS
  • Event Code: Specific Contact ID codes for each emergency type
  • Timestamp: Includes accurate timestamp of event generation
  • Priority: High priority transmission to monitoring stations

Local Alarm Response (Alarm=1):

  • Sirens: Activate local sirens and audible alerts
  • Strobes: Visual indication (if fire event)
  • Panel Display: Emergency status displayed on keypads
  • Notification: Immediate local emergency notification

Emergency Event Types

Medical Emergency (Event=0):

  • Purpose: Medical assistance, health emergency, duress
  • CID Code: Medical alarm Contact ID code
  • Response: Medical/ambulance response typically required
  • Priority: High priority emergency

Fire Emergency (Event=1):

  • Purpose: Fire detection, smoke alarm, fire safety
  • CID Code: Fire alarm Contact ID code
  • Response: Fire department response typically required
  • Priority: Highest priority emergency

Panic Emergency (Event=2):

  • Purpose: Security threat, intrusion, personal safety
  • CID Code: Panic alarm Contact ID code
  • Response: Police/security response typically required
  • Priority: High priority emergency

Cross-References

Related commands for emergency management:

Usage Example
javascript
async function generateEmergencyEvent(partialUid, eventType, generateAlarm) {
  // Validate parameters
  if (!isValidEventType(eventType)) {
    throw new Error('Invalid event type. Must be 0 (medical), 1 (fire), or 2 (panic)');
  }
  
  if (!isValidAlarmFlag(generateAlarm)) {
    throw new Error('Invalid alarm flag. Must be 0 (silent) or 1 (audible)');
  }
  
  // Build emergency event command
  const header = "070100000000000000";
  const command = "2400";
  
  const alarmFlag = generateAlarm ? '1' : '0';
  const eventData = `${eventType}:${alarmFlag}`;
  
  const dataHex = asciiToHex(eventData);
  const fullCommand = header + command + dataHex;
  
  try {
    const eventDescription = getEventTypeDescription(eventType);
    const alarmDescription = generateAlarm ? 'with local alarm' : 'silent (CID only)';
    
    console.log(`Generating ${eventDescription} ${alarmDescription}...`);
    
    const result = await sendDeviceCommand(partialUid, fullCommand, true);
    
    if (result.data && result.data.length > 0) {
      const responseText = hexToAscii(result.data[0]);
      
      if (responseText === 'OK') {
        console.log(`${eventDescription} generated successfully`);
        
        return { 
          success: true, 
          message: `${eventDescription} event generated`,
          eventType: eventType,
          eventDescription: eventDescription,
          localAlarm: generateAlarm,
          cidGenerated: true
        };
      } else if (responseText === 'NACK') {
        console.log('Failed to generate emergency event');
        return { 
          success: false, 
          message: 'Device returned NACK - command not supported or wrong data'
        };
      } else {
        return { 
          success: false, 
          message: `Unexpected response: ${responseText}` 
        };
      }
    }
    
    throw new Error('No response received from device');
  } catch (error) {
    console.error('Failed to generate emergency event:', error);
    throw error;
  }
}

// Validation functions
function isValidEventType(eventType) {
  const numericType = parseInt(eventType, 10);
  return !isNaN(numericType) && numericType >= 0 && numericType <= 2;
}

function isValidAlarmFlag(flag) {
  return flag === 0 || flag === 1 || flag === '0' || flag === '1';
}

function getEventTypeDescription(eventType) {
  const descriptions = {
    0: 'Medical Emergency',
    1: 'Fire Emergency', 
    2: 'Panic Emergency'
  };
  
  return descriptions[eventType] || `Unknown Event Type ${eventType}`;
}

function getEventTypeName(eventType) {
  const names = {
    0: 'Medical',
    1: 'Fire',
    2: 'Panic'
  };
  
  return names[eventType] || 'Unknown';
}

// Convenience functions for specific emergency types
async function generateMedicalEmergency(partialUid, audible = true) {
  return await generateEmergencyEvent(partialUid, 0, audible);
}

async function generateFireEmergency(partialUid, audible = true) {
  return await generateEmergencyEvent(partialUid, 1, audible);
}

async function generatePanicEmergency(partialUid, audible = true) {
  return await generateEmergencyEvent(partialUid, 2, audible);
}

async function generateSilentMedical(partialUid) {
  return await generateEmergencyEvent(partialUid, 0, false);
}

async function generateSilentFire(partialUid) {
  return await generateEmergencyEvent(partialUid, 1, false);
}

async function generateSilentPanic(partialUid) {
  return await generateEmergencyEvent(partialUid, 2, false);
}

// Safe wrapper with normalization
async function generateEmergencyEventSafe(partialUid, eventType, generateAlarm) {
  try {
    const normalizedType = parseInt(eventType, 10);
    const normalizedAlarm = (generateAlarm === '1' || generateAlarm === 1) ? 1 : 0;
    
    return await generateEmergencyEvent(partialUid, normalizedType, normalizedAlarm);
  } catch (error) {
    console.error('Validation error:', error.message);
    throw error;
  }
}

// Emergency management class
class EmergencyManager {
  constructor(partialUid) {
    this.partialUid = partialUid;
    this.activeEmergencies = new Map();
    this.emergencyLog = [];
  }
  
  async activateMedicalEmergency(audible = true) {
    try {
      const result = await generateMedicalEmergency(this.partialUid, audible);
      
      if (result.success) {
        this.recordEmergency('medical', audible, result);
      }
      
      return result;
    } catch (error) {
      console.error('Failed to activate medical emergency:', error);
      throw error;
    }
  }
  
  async activateFireEmergency(audible = true) {
    try {
      const result = await generateFireEmergency(this.partialUid, audible);
      
      if (result.success) {
        this.recordEmergency('fire', audible, result);
      }
      
      return result;
    } catch (error) {
      console.error('Failed to activate fire emergency:', error);
      throw error;
    }
  }
  
  async activatePanicEmergency(audible = true) {
    try {
      const result = await generatePanicEmergency(this.partialUid, audible);
      
      if (result.success) {
        this.recordEmergency('panic', audible, result);
      }
      
      return result;
    } catch (error) {
      console.error('Failed to activate panic emergency:', error);
      throw error;
    }
  }
  
  recordEmergency(type, audible, result) {
    const emergency = {
      id: Date.now(),
      type: type,
      audible: audible,
      timestamp: new Date(),
      result: result
    };
    
    this.activeEmergencies.set(emergency.id, emergency);
    this.emergencyLog.push(emergency);
    
    console.log(`Emergency recorded: ${type} (${audible ? 'audible' : 'silent'})`);
  }
  
  getActiveEmergencies() {
    return Array.from(this.activeEmergencies.values());
  }
  
  getEmergencyHistory() {
    return [...this.emergencyLog];
  }
  
  clearEmergencyRecord(emergencyId) {
    this.activeEmergencies.delete(emergencyId);
  }
  
  getEmergencyStatistics() {
    const stats = {
      total: this.emergencyLog.length,
      medical: 0,
      fire: 0,
      panic: 0,
      audible: 0,
      silent: 0
    };
    
    this.emergencyLog.forEach(emergency => {
      stats[emergency.type]++;
      if (emergency.audible) {
        stats.audible++;
      } else {
        stats.silent++;
      }
    });
    
    return stats;
  }
}

// CID event monitoring for emergency events
function setupEmergencyCidMonitoring(partialUid, onEmergencyEvent) {
  console.log('Setting up CID monitoring for emergency events');
  
  // Placeholder implementation
  const mockCidHandler = (cidEvent) => {
    if (['MEDICAL_ALARM', 'FIRE_ALARM', 'PANIC_ALARM'].includes(cidEvent.type)) {
      console.log('Emergency CID event received:', cidEvent);
      if (onEmergencyEvent) {
        onEmergencyEvent(cidEvent);
      }
    }
  };
  
  return mockCidHandler;
}

// Emergency response protocols
const EMERGENCY_PROTOCOLS = {
  medical: {
    priority: 'high',
    responseType: 'medical',
    contacts: ['ambulance', 'emergency_medical'],
    initialAction: 'Verify nature of medical emergency',
    followUp: 'Dispatch appropriate medical response'
  },
  fire: {
    priority: 'critical',
    responseType: 'fire',
    contacts: ['fire_department', 'emergency_services'],
    initialAction: 'Confirm fire/smoke detection',
    followUp: 'Dispatch fire department and evacuate if needed'
  },
  panic: {
    priority: 'high',
    responseType: 'security',
    contacts: ['police', 'security_services'],
    initialAction: 'Assess security threat level',
    followUp: 'Dispatch appropriate security response'
  }
};

function getEmergencyProtocol(eventType) {
  const typeNames = ['medical', 'fire', 'panic'];
  const typeName = typeNames[eventType];
  return EMERGENCY_PROTOCOLS[typeName] || null;
}

// Usage demonstration
async function demonstrateEmergencyEvents(partialUid) {
  try {
    console.log('=== Emergency Events Demonstration ===');
    
    // Generate different emergency types
    console.log('\n--- Medical Emergency (Audible) ---');
    const medicalResult = await generateMedicalEmergency(partialUid, true);
    console.log('Medical emergency result:', medicalResult);
    
    console.log('\n--- Fire Emergency (Silent) ---');
    const fireResult = await generateSilentFire(partialUid);
    console.log('Fire emergency result:', fireResult);
    
    console.log('\n--- Panic Emergency (Audible) ---');
    const panicResult = await generatePanicEmergency(partialUid, true);
    console.log('Panic emergency result:', panicResult);
    
    // Emergency manager demo
    console.log('\n--- Emergency Manager ---');
    const emergencyManager = new EmergencyManager(partialUid);
    
    await emergencyManager.activateMedicalEmergency(false); // Silent medical
    await emergencyManager.activateFireEmergency(true);     // Audible fire
    await emergencyManager.activatePanicEmergency(true);    // Audible panic
    
    console.log('Active emergencies:', emergencyManager.getActiveEmergencies());
    console.log('Emergency statistics:', emergencyManager.getEmergencyStatistics());
    
    // Protocol information
    console.log('\n--- Emergency Protocols ---');
    for (let i = 0; i <= 2; i++) {
      const protocol = getEmergencyProtocol(i);
      if (protocol) {
        console.log(`${getEventTypeDescription(i)} Protocol:`, protocol);
      }
    }
    
    // CID monitoring setup
    console.log('\n--- CID Event Monitoring ---');
    const cidHandler = setupEmergencyCidMonitoring(partialUid, (event) => {
      console.log('Emergency CID event:', event);
    });
    
    // Test validation
    console.log('\n--- Testing Invalid Inputs ---');
    try {
      await generateEmergencyEventSafe(partialUid, 5, 1); // Invalid event type
    } catch (error) {
      console.log('Expected validation error:', error.message);
    }
    
    try {
      await generateEmergencyEventSafe(partialUid, 1, 'invalid'); // Invalid alarm flag
    } catch (error) {
      console.log('Expected validation error:', error.message);
    }
    
    // Emergency response scenarios
    console.log('\n--- Emergency Response Scenarios ---');
    
    const scenarios = [
      { type: 0, audible: false, scenario: 'Silent medical duress' },
      { type: 1, audible: true, scenario: 'Fire detection with evacuation' },
      { type: 2, audible: true, scenario: 'Security intrusion with alarm' }
    ];
    
    for (const scenario of scenarios) {
      console.log(`\nScenario: ${scenario.scenario}`);
      const result = await generateEmergencyEventSafe(partialUid, scenario.type, scenario.audible);
      console.log(`Result: ${result.success ? 'Success' : 'Failed'} - ${result.message}`);
    }
    
    console.log('Emergency events demonstration completed');
    
  } catch (error) {
    console.error('Emergency events demonstration failed:', error);
  }
}

0x0025 - Reserved

Status: Reserved command - not currently implemented.


0x0026 - Reserved

Status: Reserved command - not currently implemented.


0x0027 - Reserved

Status: Reserved command - not currently implemented.


0x0028 - Reserved

Status: Reserved command - not currently implemented.


Released under the MIT License.