Scriptable Serial Ports - Emulate serial devices with scripts and debug serial applications
Download Virtual Serial Port Tools Hide this button

Script Ports

Virtual Script Port is the unique and powerful capability offered by Virtual Serial Port Tools. It allows the user to create a virtual serial port, “backed up” by a custom script, written in TypeScript or JavaScript.

Script Port

Overview

A script that implements a virtual serial device logic (called device script further in this document) must be written in TypeScript or JavaScript (ES6) and must define a class that implements the IScriptDevice interface and define a createDevice or createDeviceAsync global function. This global function is called each time a virtual script port is opened by an application. This function must create an instance of the port class and return a reference to it.

Device creation function is allowed to perform some initialization and is passed an optional initialization value string. If it needs to perform an asynchronous initialization, use createDeviceAsync function, otherwise, use createDevice function.

VSPT API ISerialPortLibrary.createScriptPort method is used to create a virtual serial script port. Set an optional script initialization parameter using the IScriptPortDevice.initializationValue. Change the default script execution logging folder by setting the IScriptPortDevice.logPath property.

Once you have your port script ready, set it by calling IScriptPortDevice.setScriptFile or IScriptPortDevice.setScriptText method.

NOTE

The library compiles and validates the script and stores it internally. If setScriptFile method is successful, the original script file will never be read again.

This is done for security reasons: by storing the script internally, VSPT protects the created virtual script port from unwanted script updates.

If you make changes to a script file and want those changes applied, call setScriptFile method again.

If this method returns false, observe the IScriptPortDevice.validationErrors property to get a list of validation errors. This method may also throw an exception which usually means that while there were no syntax errors in a script, it still failed additional validation, like missing createDevice or createDeviceAsync global functions.

Script Structure

Below is the minimal device script:

///<reference path="hhdscriptport.d.ts" />

class MyDevice implements Port.IScriptDevice {   // 1
    // The following method is optional, see below
    public async onSend(data: Uint8Array): Promise<void> {  // 2
        // ...
    }

    // The following method is optional, see below
    public setParam(name: string, value: string): void {    // 3
        // ...
    }
}

// Either createDevice or createDeviceAsync global functions must be defined. If both are defined, createDeviceAsync will be used
async function createDeviceAsync(initializationValue?: string): Promise<Port.IScriptDevice> { // 4
    return new MyDevice;
}

// Either createDevice or createDeviceAsync global functions must be defined. If both are defined, createDeviceAsync will be used
function createDevice(initializationValue?: string): Port.IScriptDevice {    // 5
    return new MyDevice;
}

The following list describes the numbered elements in the sample above:

  1. Script logic must be incorporated into a class which implements a Port.IScriptDevice interface.

  2. An optional IScriptDevice.onSend method is used in the push model.

  3. An optional IScriptDevice.setParam method, if present, allows the external code to send arbitrary data to the script, either by calling IScriptPortDevice.setScriptParam method or by sending a custom IOCTL_SCRIPTPORT_SET_PARAM device I/O request to the opened port.

  4. createDeviceAsync global function, if present, will be called each time the virtual script port is opened. It is passed a copy of the IScriptPortDevice.initializationValue. This function must create an instance of a device class and return it.

    At least one of createDeviceAsync or createDevice global functions must be defined, otherwise, a validation error occurs. If both functions are defined, only createDeviceAsync will be used.

  5. createDevice global function, if present, will be called each time the virtual script port is opened. It is passed a copy of the IScriptPortDevice.initializationValue. This function must create an instance of a device class and return it.

    At least one of createDeviceAsync or createDevice global functions must be defined, otherwise, a validation error occurs. If both functions are defined, only createDeviceAsync will be used.

NOTE

This script execution context is created after the port is opened and is destroyed as soon as it is closed. Any global variables you create will only live this long.

That is, you cannot store any state between port opening attempts. However, the execution context is guaranteed to exist until the port is closed by the application. Anyway, prefer storing any state in the created device script object, not on a global scope.

Send Data Model

Device script can use one of the following data flow models:

Push Model
In this model Virtual Script Port proactively sends all the data sent to the port to device script's onSend method.
Pull Model
In this model, device script queries (pulls) for the sent data.

The virtual serial port driver chooses the model by determining the presence of the IScriptDevice.onSend method in the device script. The sections below provide detailed explanation on flow models:

Push Model

Whenever an application sends any data to the virtual serial port, device script's IScriptDevice.onSend method is called with a copy of data sent. onSend method returns a promise. Once this promise is resolved, virtual serial port immediately discards sent data and completes the original application's Write request.

Loopback Serial Port (Push Model).ts sample script, included in the default product installation, illustrates the usage of a push model:

///<reference path="hhdscriptport.d.ts" />
/**
 * This script implements a simple "loopback" virtual serial device
 * It immediately returns everything that has been sent to it
 * 
 * This version illustrates the usage of so-called "push" model
 * In this mode, virtual serial port driver calls an onSend method with data sent by application to a serial port
 */
class LoopbackSerialDevicePush implements Port.IScriptDevice {
    public async onSend(data: Uint8Array): Promise<void> {
        // This script emulates an echo device, we simply provide the same data back
        port.provideReceivedData(data);
    }
}

function createDevice(): Port.IScriptDevice {
    return new LoopbackSerialDevicePush;
}

Pull Model

Whenever an application sends any data to the virtual serial port, it is put into the internal output queue. Application's Write request is completed immediately. At any time, a device script may query for the whole or part of the output queue by calling the IPort.getSentData method. This method returns a promise that is resolved with a copy of the output queue, or a part of it.

Loopback Serial Port (Pull Model).ts sample script, included in the default product installation, illustrates the usage of a push model:

///<reference path="hhdscriptport.d.ts" />
/**
 * This script implements a simple "loopback" virtual serial device
 * It immediately returns everything that has been sent to it
 * 
 * This version illustrates the usage of so-called "pull" model
 * In this mode, a port implementation calls the virtual serial port driver's getSentData function that returns a Promise.
 * This promise is resolved with a byte array sent by application to a serial port
 */
class LoopbackSerialDevicePull implements Port.IScriptDevice {
    constructor() {
        this.run();
    }
    async run() {
        while (true) {
            var sentData = await port.getSentData();
            port.provideReceivedData(sentData);
        }
    }
}

function createDevice(): Port.IScriptDevice {
    return new LoopbackSerialDevicePull;
}

Receiving Data

Data model only governs the processing of the data sent by an application to a port. Whenever the device script needs to emulate received data, it calls the IPort.provideReceivedData method with the data it wants to make available for application. Next time application attempts to read from the serial port it gets a copy of those data.

Device Script API

Virtual script port provides the following functionality to the device script:

Port API
Provided by means of a global object port which implements the IPort interface. It represents the virtual serial port itself and provides methods for reading and writing data.
File System
Provided by means of a global object fs which implements the FS.IFileManager interface. It allows the device script to create and delete folders and files, open files for reading and writing and read and write data from them. Device script may also enumerate the contents of any folder.
Sockets
Provided by means of a global object net which implements the Net.INetworkManager interface. It allows the device script to create TCP and UDP sockets and use them to perform network communication.
HTTP
Provided by means of a global object http which implements the Http.IHttpClient interface. It allows the device script to send all kinds of HTTP requests.

Script Debugging

Device script runs in a controlled and secure environment. Therefore, external access to a running script is limited. The only debugging facility available for a device script is a global log method.

By default, all log files are created in a %TEMP%\vspt_script_logs folder. Note that %TEMP% environment variable is expanded under the context of a System account and is usually resolved into c:\Windows\TEMP. Log file folder location may also be changed by setting the IScriptPortDevice.logPath property.

A log file is only created if a device script makes a call to a log method, or throws an unhandled exception. The log file name follows this scheme:

vspt.YYYY-MM-dd.hh-mm-ss.PID.log

AbbrDescription
YYYYYear
MMMonth
ddDay
hhHour in 24-hours format
mmMinutes
ssSeconds
PIDProcess ID of a port opening process