-
Notifications
You must be signed in to change notification settings - Fork 953
JavaScript API Reference
Complete reference for Serial Studio's JavaScript frame parsing API. Use this to write custom parsers for any data format.
Before diving into custom parsers, make sure you understand:
- Data Flow in Serial Studio - How data moves through the system
- Project Editor - Where you write parser functions
- Basic JavaScript programming
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
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:
Video: JavaScript fundamentals covering variables, functions, arrays, and control structures.
JavaScript Arrays and String Methods:
Video: Essential array methods like map, filter, split, and join - critical for parsing data.
- The Parse Function
- Built-in JavaScript Functions
- Global Variables
- Common Parsing Patterns
- Debugging
- Performance Tips
- Error Handling
- Limitations
- Examples
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:
- Function must be named
parse - Must take exactly one parameter
- Must return an array (not a string, object, or undefined)
- Return empty array
[]if frame is invalid
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] |
// frame = "1023,512,850"
function parse(frame) {
return frame.split(','); // Returns ["1023", "512", "850"]
}// 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]
}// 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(',');
}// 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
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;Serial Studio's parser environment includes standard JavaScript (ECMAScript 5/6) functionality.
// 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 substringlet 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);
});// 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 → trueMath.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...// 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:
Video: Learn how to parse and work with JSON data in JavaScript.
// 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:
Video: Master regular expressions for pattern matching and text parsing in JavaScript.
// Decode base64 (built-in browser function)
let decoded = atob("SGVsbG8="); // → "Hello"
// Encode to base64
let encoded = btoa("Hello"); // → "SGVsbG8="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
- Project is reloaded
- Parser code is edited and reapplied
- Serial Studio is restarted
function parse(frame) {
return frame.split(',');
}Input: "22.5,60,1013"
Output: ["22.5", "60", "1013"]
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;
}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]
// 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]
// 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]
// 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]
// 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]
// 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(',');
}// 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);
}// 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;
}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);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...
}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;
}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;
}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
}
}// ❌ 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(',');
}// ❌ 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;
}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);
}// ❌ 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(',');
}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
}
}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(',');
}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;
}You cannot use external JavaScript libraries (lodash, moment.js, etc.). Only built-in JavaScript is available.
The parser must complete synchronously. You cannot use:
-
setTimeout(),setInterval() -
Promise,async/await - AJAX /
fetch()requests
There is no browser DOM. You cannot use:
-
document,window -
localStorage,sessionStorage - Browser-specific APIs
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(',');
}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(',');
}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];
}- Data Flow in Serial Studio - Understanding the parsing pipeline
- Project Editor - Where to write parser code
- Troubleshooting Guide - Common parser issues
- Examples Repository - Real-world examples
// 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.
If you find Serial Studio helpful, please consider supporting the project:
Your support helps keep the project growing, maintained, and continuously improved.



