Skip to content

JavaScript API Reference

Alex Spataru edited this page Dec 28, 2025 · 4 revisions

Complete reference for Serial Studio's JavaScript frame parsing API. Use this to write custom parsers for any data format.

Prerequisites

Before diving into custom parsers, make sure you understand:

When do you need a custom parser?

  • Your data isn't simple CSV
  • You have a binary protocol
  • You need to validate checksums
  • You want to filter or transform data
  • Your data format is proprietary

When don't you need one?

  • Your data is comma-separated values (CSV)
  • Quick Plot mode works fine
  • Simple structured data

JavaScript Learning Resources

If you're new to JavaScript or need a refresher, these educational videos will help you understand the fundamentals needed for writing custom parsers:

JavaScript Basics:

JavaScript Programming - Full Course

Video: JavaScript fundamentals covering variables, functions, arrays, and control structures.

JavaScript Arrays and String Methods:

JavaScript Array Methods

Video: Essential array methods like map, filter, split, and join - critical for parsing data.


Table of Contents


The Parse Function

Function Signature

Every custom parser must define a function named parse that takes one parameter:

function parse(frame) {
    // Your parsing logic here
    return [];  // Must return an array
}

Parameters:

  • frame - The input data (type depends on decoder method selected)

Return value:

  • Must return an array of strings or numbers
  • Array indices map to dataset indices in your project:
    • array[0] → Dataset with index 1
    • array[1] → Dataset with index 2
    • array[2] → Dataset with index 3
    • etc.

Critical rules:

  1. Function must be named parse
  2. Must take exactly one parameter
  3. Must return an array (not a string, object, or undefined)
  4. Return empty array [] if frame is invalid

Input Types by Decoder Method

The frame parameter's type and content depend on your Decoder Method setting in the Project Editor:

Decoder Method Input Type Content Example
Plain Text (UTF-8) String "1023,512,850"
Hexadecimal String (hex-encoded) "03FF020035A"
Base64 String (base64-encoded) "Av8CADWg"
Binary (Direct) (Pro) Array of numbers (0-255) [3, 255, 2, 0, 53, 160]

Plain Text (UTF-8)

// frame = "1023,512,850"
function parse(frame) {
    return frame.split(',');  // Returns ["1023", "512", "850"]
}

Hexadecimal

// frame = "03FF020035A" (hex string representing bytes)
function parse(frame) {
    let values = [];
    // Parse 2-byte (4-character) hex values
    for (let i = 0; i < frame.length; i += 4) {
        let hexStr = frame.substring(i, i + 4);
        let value = parseInt(hexStr, 16);  // Convert hex to decimal
        values.push(value);
    }
    return values;  // Returns [1023, 512, 858]
}

Base64

// frame = "Av8CADWg" (base64-encoded bytes)
function parse(frame) {
    // Decode base64 to get original string
    let decoded = atob(frame);  // Built-in JavaScript function

    // Process decoded data
    // (implementation depends on your protocol)
    return decoded.split(',');
}

Binary (Direct) - Pro Only

// frame = [0x03, 0xFF, 0x02, 0x00, 0x35, 0xA0]
function parse(frame) {
    let values = [];

    // Parse 2-byte big-endian integers
    for (let i = 0; i < frame.length; i += 2) {
        let highByte = frame[i];
        let lowByte = frame[i + 1];
        let value = (highByte << 8) | lowByte;  // Combine bytes
        values.push(value);
    }

    return values;  // Returns [1023, 512, 13728]
}

Why use Binary (Direct)?

  • Performance: No string encoding/decoding overhead
  • Precision: Lossless binary protocol handling
  • Efficiency: Direct byte-level access

Return Value Requirements

The parser must always return an array:

// ✅ CORRECT - Array of numbers
return [123, 456, 789];

// ✅ CORRECT - Array of strings
return ["123", "456", "789"];

// ✅ CORRECT - Mixed (will be converted appropriately)
return [123, "456", 789.5];

// ✅ CORRECT - Empty array (when frame is invalid)
return [];

// ❌ WRONG - String, not array
return "123,456,789";

// ❌ WRONG - Object, not array
return {temp: 123, humidity: 456};

// ❌ WRONG - No return statement (undefined)
function parse(frame) {
    let values = frame.split(',');
    // Missing return!
}

// ❌ WRONG - Returning null
return null;

Built-in JavaScript Functions

Serial Studio's parser environment includes standard JavaScript (ECMAScript 5/6) functionality.

String Methods

// String manipulation
frame.split(',')              // Split by delimiter → array
frame.substring(0, 4)         // Extract substring
frame.substr(start, length)   // Extract substring by length
frame.indexOf('$')            // Find character position → number
frame.lastIndexOf('$')        // Find last occurrence
frame.trim()                  // Remove whitespace
frame.replace('x', 'y')       // Replace substring
frame.toUpperCase()           // Convert to uppercase
frame.toLowerCase()           // Convert to lowercase
frame.charAt(index)           // Get character at position
frame.charCodeAt(index)       // Get character code (0-255)
frame.startsWith('prefix')    // Check if starts with string
frame.endsWith('suffix')      // Check if ends with string
frame.includes('text')        // Check if contains substring

Array Methods

let array = [1, 2, 3, 4, 5];

// Array properties and manipulation
array.length                  // Get array size → 5
array.push(6)                 // Add to end → [1,2,3,4,5,6]
array.pop()                   // Remove from end → 5, array is [1,2,3,4]
array.shift()                 // Remove from start → 1, array is [2,3,4]
array.unshift(0)              // Add to start → [0,2,3,4]
array.slice(1, 3)             // Extract portion → [2,3]
array.splice(1, 2)            // Remove elements → [2,3], array is [1,4]
array.reverse()               // Reverse order → [5,4,3,2,1]
array.sort()                  // Sort array
array.join(',')               // Join to string → "1,2,3,4,5"
array.concat([6, 7])          // Concatenate arrays → [1,2,3,4,5,6,7]

// Array iteration/transformation
array.map(function(x) {       // Transform each element
    return x * 2;             // → [2,4,6,8,10]
});

array.filter(function(x) {    // Filter elements
    return x > 2;             // → [3,4,5]
});

array.forEach(function(x, index) {  // Iterate
    console.log(index, x);
});

Number Parsing

// String to number conversion
parseInt("123", 10)           // Parse integer → 123
parseInt("FF", 16)            // Parse hex → 255
parseInt("1010", 2)           // Parse binary → 10
parseFloat("3.14159")         // Parse float → 3.14159

// Number validation
isNaN("hello")                // Check if not a number → true
isFinite(123)                 // Check if finite → true

Math Functions

Math.abs(-5)                  // Absolute value → 5
Math.min(1, 2, 3)             // Minimum → 1
Math.max(1, 2, 3)             // Maximum → 3
Math.round(3.7)               // Round to nearest → 4
Math.floor(3.7)               // Round down → 3
Math.ceil(3.2)                // Round up → 4
Math.sqrt(16)                 // Square root → 4
Math.pow(2, 8)                // Power (2^8) → 256
Math.sin(x), Math.cos(x)      // Trigonometry (radians)
Math.PI                       // Pi constant → 3.14159...
Math.E                        // Euler's number → 2.71828...
Math.random()                 // Random 0-1 → 0.837492...

JSON Parsing

// Parse JSON string to object
let obj = JSON.parse('{"temp":22.5,"humidity":60}');
console.log(obj.temp);        // → 22.5

// Convert object to JSON string
let json = JSON.stringify({temp: 22.5});  // → '{"temp":22.5}'

Educational Video - Working with JSON:

Working with JSON Data

Video: Learn how to parse and work with JSON data in JavaScript.

Regular Expressions

// Create regex
let regex = /pattern/flags;
let regex = new RegExp('pattern', 'flags');

// Test for match
/\d+/.test("abc123")          // → true (contains digits)

// Execute and get matches
let match = /(\w+)=(\d+)/.exec("temp=25");
// match[0] = "temp=25" (full match)
// match[1] = "temp" (group 1)
// match[2] = "25" (group 2)

// Find all matches
let matches = "a=1,b=2,c=3".match(/(\w)=(\d)/g);
// → ["a=1", "b=2", "c=3"]

// Replace with regex
"temp=25,hum=60".replace(/(\w+)=(\d+)/g, "$1:$2");
// → "temp:25,hum:60"

Educational Video - Regular Expressions Tutorial:

Regular Expressions (RegEx) Tutorial

Video: Master regular expressions for pattern matching and text parsing in JavaScript.

Base64 Encoding/Decoding

// Decode base64 (built-in browser function)
let decoded = atob("SGVsbG8=");  // → "Hello"

// Encode to base64
let encoded = btoa("Hello");     // → "SGVsbG8="

Global Variables

You can declare variables outside the parse function to maintain state between calls:

// Global variables persist between function calls
let frameCount = 0;
let previousValue = 0;
let history = [];

function parse(frame) {
    frameCount++;  // Increments with each frame

    let values = frame.split(',');
    let current = parseInt(values[0]);

    // Calculate delta from previous frame
    let delta = current - previousValue;
    previousValue = current;

    // Add to history
    history.push(current);
    if (history.length > 100) {
        history.shift();  // Keep only last 100 values
    }

    return [current, delta, frameCount];
}

Use cases for global variables:

  • Frame counters
  • State machines (protocol state tracking)
  • Moving average filters (history buffer)
  • Previous value tracking (delta calculation)
  • Checksum validation state
  • Multi-frame protocol assembly

⚠️ Warning: Globals are reset when:

  • Project is reloaded
  • Parser code is edited and reapplied
  • Serial Studio is restarted

Common Parsing Patterns

CSV Parsing (Basic)

function parse(frame) {
    return frame.split(',');
}

Input: "22.5,60,1013" Output: ["22.5", "60", "1013"]


CSV with Validation

function parse(frame) {
    // Reject empty frames
    if (frame.length === 0)
        return [];

    let values = frame.split(',');

    // Validate expected number of values
    if (values.length !== 6)
        return [];

    return values;
}

JSON Parsing

function parse(frame) {
    try {
        let data = JSON.parse(frame);
        return [
            data.temperature,
            data.humidity,
            data.pressure
        ];
    }
    catch (e) {
        console.log("JSON parse error:", e.message);
        return [];
    }
}

Input: {"temperature":22.5,"humidity":60,"pressure":1013} Output: [22.5, 60, 1013]


Fixed-Width Text Parsing

// Frame format: "TEMP:025HUM:060PRES:1013"
function parse(frame) {
    let temp = parseInt(frame.substring(5, 8));      // Position 5-7
    let humidity = parseInt(frame.substring(12, 15)); // Position 12-14
    let pressure = parseInt(frame.substring(20, 24)); // Position 20-23

    return [temp, humidity, pressure];
}

Input: "TEMP:025HUM:060PRES:1013" Output: [25, 60, 1013]


Key-Value Parsing

// Frame: "temp=22.5,humidity=60,pressure=1013"

// Define key mapping to array indices
const keymap = {
    temp: 0,
    humidity: 1,
    pressure: 2
};

// Pre-allocate array
let values = new Array(Object.keys(keymap).length).fill(0);

function parse(frame) {
    const regex = /([\w]+)=([\d.]+)/g;
    let match;

    while ((match = regex.exec(frame)) !== null) {
        let key = match[1];
        let value = parseFloat(match[2]);

        if (keymap.hasOwnProperty(key)) {
            values[keymap[key]] = value;
        }
    }

    return values.slice();  // Return copy
}

Input: "temp=22.5,humidity=60,pressure=1013" Output: [22.5, 60, 1013]


Binary Protocol (Hexadecimal Decoder)

// Decoder: Hexadecimal
// Frame: "03FF020035A" (hex string)
// Protocol: 2-byte big-endian integers

function parse(frame) {
    let values = [];

    // Parse 2-byte (4-character) hex values
    for (let i = 0; i < frame.length; i += 4) {
        let hexStr = frame.substring(i, i + 4);
        let value = parseInt(hexStr, 16);
        values.push(value);
    }

    return values;
}

Input: "03FF020035A" (hex) Output: [1023, 512, 858]


Binary Protocol (Binary Direct - Pro)

// Decoder: Binary (Direct) - Pro only
// Frame: [0x03, 0xFF, 0x02, 0x00, 0x35, 0xA0]
// Protocol: 2-byte big-endian integers

function parse(frame) {
    let values = [];

    // Parse 2-byte big-endian integers
    for (let i = 0; i < frame.length; i += 2) {
        let highByte = frame[i];
        let lowByte = frame[i + 1];
        let value = (highByte << 8) | lowByte;
        values.push(value);
    }

    return values;
}

Input: [3, 255, 2, 0, 53, 160] (byte array) Output: [1023, 512, 13728]


Checksum Validation (XOR)

// Frame format: "1023,512,850*AB"
// Checksum: XOR of all characters before '*'

function parse(frame) {
    let parts = frame.split('*');
    if (parts.length !== 2)
        return [];

    let data = parts[0];
    let checksumReceived = parseInt(parts[1], 16);

    // Calculate checksum (XOR of all characters)
    let checksumCalculated = 0;
    for (let i = 0; i < data.length; i++) {
        checksumCalculated ^= data.charCodeAt(i);
    }

    // Validate
    if (checksumCalculated !== checksumReceived) {
        console.log("Checksum failed! Expected", checksumCalculated.toString(16),
                    "got", checksumReceived.toString(16));
        return [];
    }

    // Checksum OK, parse data
    return data.split(',');
}

Multi-Message Protocol (State Machine)

// Device sends two message types:
// Type A: "A:temp,humidity,pressure"
// Type B: "B:voltage,current"

// Global state for both message types
let envValues = [0, 0, 0];   // temp, humidity, pressure
let powerValues = [0, 0];     // voltage, current

function parse(frame) {
    let type = frame.charAt(0);
    let data = frame.substring(2);  // Skip "A:" or "B:"

    if (type === 'A') {
        envValues = data.split(',');
    }
    else if (type === 'B') {
        powerValues = data.split(',');
    }
    else {
        console.log("Unknown message type:", type);
        return envValues.concat(powerValues);  // Return current state
    }

    // Return combined state (5 values total)
    return envValues.concat(powerValues);
}

Moving Average Filter

// Smooth noisy data with moving average
const WINDOW_SIZE = 10;
let history = [];

function parse(frame) {
    let values = frame.split(',').map(parseFloat);

    // Add to history
    history.push(values);
    if (history.length > WINDOW_SIZE) {
        history.shift();  // Keep only last WINDOW_SIZE frames
    }

    // Calculate average for each value
    let averaged = [];
    for (let i = 0; i < values.length; i++) {
        let sum = 0;
        for (let j = 0; j < history.length; j++) {
            sum += history[j][i];
        }
        averaged.push(sum / history.length);
    }

    return averaged;
}

Debugging

Using console.log()

Print debug information to Serial Studio's console:

function parse(frame) {
    console.log("=== PARSER START ===");
    console.log("Received frame:", frame);
    console.log("Frame type:", typeof frame);
    console.log("Frame length:", frame.length);

    let values = frame.split(',');
    console.log("Parsed values:", JSON.stringify(values));
    console.log("Number of values:", values.length);

    console.log("=== PARSER END ===");
    return values;
}

Where to see output: Serial Studio's console/terminal window

Pro tip: Use structured logging:

console.log("Input:", frame, "| Type:", typeof frame, "| Length:", frame.length);

Common Debugging Techniques

1. Log the Raw Frame

function parse(frame) {
    console.log("Raw frame:", frame);
    console.log("Frame type:", typeof frame);
    console.log("Frame length:", frame.length);

    // If binary, log each byte
    if (typeof frame === 'object') {  // Binary Direct mode
        console.log("Bytes:", Array.from(frame).map(b => "0x" + b.toString(16)));
    }

    // Continue parsing...
}

2. Log Intermediate Steps

function parse(frame) {
    console.log("Step 1 - Input:", frame);

    let parts = frame.split(',');
    console.log("Step 2 - After split:", parts);

    let numbers = parts.map(parseFloat);
    console.log("Step 3 - After parseFloat:", numbers);

    return numbers;
}

3. Log Return Value

function parse(frame) {
    // Parsing logic...
    let result = frame.split(',');

    console.log("Returning:", result);
    console.log("Return type:", Array.isArray(result) ? "array" : typeof result);
    console.log("Return length:", result.length);

    return result;
}

4. Catch and Log Errors

function parse(frame) {
    try {
        // Parsing logic
        let data = JSON.parse(frame);
        return [data.temp, data.humidity];
    }
    catch (e) {
        console.log("ERROR:", e.message);
        console.log("Stack:", e.stack);
        console.log("Frame was:", frame);
        return [];  // Return empty array on error
    }
}

Performance Tips

Optimize for High-Frequency Data

1. Avoid Unnecessary Operations

// ❌ SLOW - Creates new regex each time
function parse(frame) {
    return frame.split(/,/);  // Regex is slower
}

// ✅ FAST - String split is faster
function parse(frame) {
    return frame.split(',');
}

2. Reuse Variables

// ❌ SLOW - Creates new array each time
function parse(frame) {
    return frame.split(',').map(parseFloat);
}

// ✅ FASTER - Reuse global array
let result = [];
function parse(frame) {
    let parts = frame.split(',');
    result.length = 0;  // Clear array (reuse memory)
    for (let i = 0; i < parts.length; i++) {
        result.push(parseFloat(parts[i]));
    }
    return result;
}

3. Early Returns for Invalid Data

function parse(frame) {
    // Fast exit for invalid frames
    if (frame.length === 0)
        return [];

    if (frame.charAt(0) !== '$')
        return [];

    // Expensive parsing only if valid
    return expensiveParsingLogic(frame);
}

4. Use Built-in Functions

// ❌ SLOW - Manual parsing
function parse(frame) {
    let result = [];
    let current = "";
    for (let i = 0; i < frame.length; i++) {
        if (frame[i] === ',') {
            result.push(current);
            current = "";
        } else {
            current += frame[i];
        }
    }
    result.push(current);
    return result;
}

// ✅ FAST - Built-in split is optimized
function parse(frame) {
    return frame.split(',');
}

Error Handling

Always Return an Array

Even if parsing fails, return an empty array:

function parse(frame) {
    try {
        // Risky parsing logic
        return complexParsingLogic(frame);
    }
    catch (e) {
        console.log("Parse error:", e.message);
        return [];  // Don't crash, return empty
    }
}

Validate Input

Check frame format before parsing:

function parse(frame) {
    // Check minimum length
    if (frame.length < 10) {
        console.log("Frame too short:", frame.length);
        return [];
    }

    // Check expected prefix
    if (!frame.startsWith("DATA:")) {
        console.log("Invalid prefix");
        return [];
    }

    // Proceed with parsing
    return frame.substring(5).split(',');
}

Handle Missing Values

Deal with incomplete data gracefully:

function parse(frame) {
    let values = frame.split(',');

    // Pad with zeros if not enough values
    while (values.length < 6) {
        values.push("0");
    }

    // Truncate if too many values
    if (values.length > 6) {
        values = values.slice(0, 6);
    }

    return values;
}

Limitations

No External Libraries

You cannot use external JavaScript libraries (lodash, moment.js, etc.). Only built-in JavaScript is available.

No Asynchronous Operations

The parser must complete synchronously. You cannot use:

  • setTimeout(), setInterval()
  • Promise, async/await
  • AJAX / fetch() requests

No DOM Access

There is no browser DOM. You cannot use:

  • document, window
  • localStorage, sessionStorage
  • Browser-specific APIs

Memory Considerations

Global variables persist in memory. Avoid:

  • Large arrays that grow indefinitely
  • Memory leaks from unbounded data structures

Example of memory leak:

// ❌ BAD - Grows forever
let allFrames = [];
function parse(frame) {
    allFrames.push(frame);  // Memory leak!
    return frame.split(',');
}

// ✅ GOOD - Limited size
let recentFrames = [];
function parse(frame) {
    recentFrames.push(frame);
    if (recentFrames.length > 100) {
        recentFrames.shift();  // Keep only last 100
    }
    return frame.split(',');
}

Examples

Real-World Example 1: Arduino ADC with Checksum

Device code (Arduino):

void sendData() {
    String data = String(analogRead(A0)) + "," +
                  String(analogRead(A1)) + "," +
                  String(analogRead(A2));

    // Calculate XOR checksum
    byte checksum = 0;
    for (int i = 0; i < data.length(); i++) {
        checksum ^= data[i];
    }

    Serial.print("$");        // Start delimiter
    Serial.print(data);
    Serial.print("*");
    Serial.print(checksum, HEX);
    Serial.println();         // End delimiter
}

Parser:

function parse(frame) {
    // Remove start delimiter
    if (frame.charAt(0) === '$') {
        frame = frame.substring(1);
    }

    // Split data and checksum
    let parts = frame.split('*');
    if (parts.length !== 2)
        return [];

    let data = parts[0];
    let checksumReceived = parseInt(parts[1], 16);

    // Calculate checksum
    let checksumCalculated = 0;
    for (let i = 0; i < data.length; i++) {
        checksumCalculated ^= data.charCodeAt(i);
    }

    // Validate
    if (checksumCalculated !== checksumReceived) {
        console.log("Checksum error!");
        return [];
    }

    // Parse data
    return data.split(',');
}

Real-World Example 2: Binary Sensor Data

Protocol: 2-byte temperature (°C * 10), 2-byte humidity (% * 10), 1-byte checksum

Parser (Binary Direct - Pro):

function parse(frame) {
    // Expected: [tempHigh, tempLow, humHigh, humLow, checksum]
    if (frame.length !== 5)
        return [];

    // Extract values
    let tempRaw = (frame[0] << 8) | frame[1];
    let humRaw = (frame[2] << 8) | frame[3];
    let checksumReceived = frame[4];

    // Validate checksum (sum of first 4 bytes & 0xFF)
    let checksumCalculated = (frame[0] + frame[1] + frame[2] + frame[3]) & 0xFF;
    if (checksumCalculated !== checksumReceived) {
        console.log("Checksum error!");
        return [];
    }

    // Convert to actual values
    let temperature = tempRaw / 10.0;  // °C
    let humidity = humRaw / 10.0;      // %

    return [temperature, humidity];
}

See Also


Quick Reference Card

// BASIC TEMPLATE
function parse(frame) {
    // 1. Validate input
    if (frame.length === 0)
        return [];

    // 2. Parse
    let values = frame.split(',');

    // 3. Validate output
    if (values.length !== EXPECTED_COUNT)
        return [];

    // 4. Return array
    return values;
}

// GLOBAL VARIABLES (persist between calls)
let counter = 0;
let previousValue = 0;

// DEBUGGING
console.log("Debug message", variable);

// COMMON OPERATIONS
frame.split(',')                    // Split string
parseInt(str, 10)                   // Parse integer (base 10)
parseInt(str, 16)                   // Parse hex (base 16)
parseFloat(str)                     // Parse float
JSON.parse(str)                     // Parse JSON
array.map(parseFloat)               // Convert array to numbers
array.filter(x => x > 0)            // Filter array
(highByte << 8) | lowByte           // Combine bytes (16-bit)

Happy parsing! Need help? Check the Troubleshooting Guide or ask on GitHub Discussions.

Clone this wiki locally