Examples
  • 31 Jan 2024
  • 9 Minutes to read
  • PDF

Examples

  • PDF

Article Summary

Inserting tag values into a MySQL database

Scripting can be used to connect to MySQL by using the mysql internal library. More information about internal libraries can be found at Libraries.

The following example shows how to insert tag changes into the database. This assumes a database and a table are already created with the following schema: 

MySQL table schema 

In order to insert the events, two scripts will be created, one serving as a wrapper over the raw MySQL connection (as a static library), and one startup script that will subscribe to events and export them to the database.

The following libraries configuration is used:

MySQL and MySqlConnector libraries 

The mysql_connector static library contains the following script:

// Import the MySQL internal library
const mysql = $.lib("mysql");

// Export the MySqlConnection class
module.exports = class MySqlConnection {
  // Constructor of the class taking a MySQL client instance
  constructor(client) {
    this._client = client;

    // Error event handler that logs a message to the logger
    client.on("error", (err) => {
      $.logger.warn(
        "Error on client: %s (%d - %s)",
        err.message,
        err.errno,
        err.sqlMessage
      );

      // Additionally, if the error is fatal, the client is dropped
      if (err.fatal) {
        this._client = null;
      }
    });
  }

  // This function must be called when the client is no longer being used
  // in order to terminate the connection
  dispose() {
    return new Promise((resolve, reject) => {
      if (this._client) {
        // If the client is online, end the client
        this._client.end((err) => {
          if (err) {
            reject(err);
          } else {
            resolve();
          }
          this._client = null;
        });
      } else {
        reject(new Error("Connection already terminated"));
      }
    });
  }

  // Checks whether the client is online or not
  online() {
    return this._client !== null;
  }

  // This function executes a query and resolve to a promise
  // containing a tuple with the first element being the result
  // of the query, and the second element the fields returned from
  // the query
  query(query, ...params) {
    return new Promise((resolve, reject) => {
      if (this._client) {
        this._client.query(query, params, (err, result, fields) => {
          if (err) {
            //If the error is fatal, set the client to null
            if (err.fatal) {
              this._client = null;
            }
            reject(err);
          } else {
            resolve([result, fields]);
          }
        });
      } else {
        reject(new Error("Connection is terminated"));
      }
    });
  }

  // Static method used to asynchronously create a new client, which is necessary since creating a connection
  // is an asynchronous operation
  static Connect(connOpts) {
    return new Promise((resolve, reject) => {
      // Create the client with the provided options
      const client = mysql.createConnection(connOpts);

      // Connect the client to the DB
      client.connect((err) => {
        if (err) {
          reject(err);
        } else {
          // If the connection is OK, create a MySqlConnection instance using the client
          // and resolve the promise with that instance
          const connection = new MySqlConnection(client);
          resolve(connection);
        }
      });
    });
  }
};

The startup script that relies on the library and is responsible for sending the event uses the following code:

const MySqlConnection = $.lib("mysql_connector");

// Define the options used to connect to MySQL
const OPTIONS = {
  host: "localhost",
  port: 3306,
  user: "admin",
  password: "n3uron",
  database: "n3_scripting",
};

// Define insertion period
const PERIOD = 10000;

// Define the tags that will be inserted into the DB
const TAGS = ["/Simulated/String", "/Simulated/Bool", "/Simulated/Number"];

// Subscribe to the tags
const subscription = await $.api.tag.subscribe(TAGS, onTagEvent);

// Buffer that holds the events between insertions
const events = [];

// Database client
let client = null;

//Set a 10 seconds timer before pushong the events
setTimeout(pushEvents, PERIOD);

//Handler that gets called whenever the tags have a new event
function onTagEvent(tag, data) {
  //Save the event to the buffer
  events.push({ tag, value: data.value, quality: data.quality, ts: data.ts });
}

// Async function called every PERIOD milliseconds to insert data into the DB
async function pushEvents() {
  // If the client is null or offline, connect the client
  if (!client || (client && !client.online())) {
    try {
      await connect();
    } catch (ex) {
      // If the connection fails, schedule a new timer
      $.logger.warn("Error connecting: %s", ex.message);
      return setTimeout(pushEvents, PERIOD);
    }
  }

  // If the client is connected, remove the events from the buffer
  // and transform them into a format that can be used by MySQL
  const ev = events.splice(0).map((el) => {
    // Set number_ev, string_ev and bool_ev either to the event
    // or to null if the type does not match
    const number_ev = typeof el.value === "number" ? el.value : null;
    const string_ev = typeof el.value === "string" ? el.value : null;
    const bool_ev = typeof el.value === "boolean" ? el.value : null;

    // Return a 6 element tuple representing the values that will be inserted into the DB
    return [el.tag, number_ev, string_ev, bool_ev, el.quality, new Date(el.ts)];
  });
  try {
    $.logger.debug("Inserting %d events", ev.length);

    // Execute an INSERT query using ? as placeholder for the values
    await client.query(
      `INSERT into tag_events (tag, number_value, string_value, bool_value, quality, ts) VALUES ?`,
      ev
    );
    $.logger.debug("Inserted OK", ev.length);
  } catch (ex) {
    $.logger.warn("Error during query: %s", ex.message);

    // If the query fails and the client is offline, null the client
    if (!client.online()) {
      client = null;
    }
  } finally {
    // After finishing the query (whether it was successful or not)
    // schedule a new timer
    setTimeout(pushEvents, PERIOD);
  }
}

// Creates a new MySQL client
async function connect() {
  $.logger.debug(
    "Connecting to MySQL using %s@%s:%d",
    OPTIONS.user,
    OPTIONS.host,
    OPTIONS.port
  );
  client = await MySqlConnection.Connect(OPTIONS);
  $.logger.debug("Connected OK");
}

Reading tag values from Microsoft SQLServer database

This example reads data from a Microsoft SQL Server database and saves it to Scripting source tags. This is done using the mssql NPM library, which is integrated in N3uron (and thus can be used as an internal library).

This example assumes the target table has the following schema, and that it's data it's being updated from an external source:

Microsoft SQL Server table schema  

In this example, the table has the following data:

Data sources used in this example

This data will be saved to Scripting source tags using an alias, this allows the tags in N3uron to have a different structure to those in Microsoft SQL Server. A configuration of a source tag can be seen in the next screenshot:

Configuration of source tags 

This example will use two scripts (a periodic script and a static library) as well as an internal library. The following two libraries are used:

Microsoft SQL Server and mssql_connector libraries 

The mssql_connector static library has the following script:

// Import the Microsft SQL Server internal library
const mssql = $.lib("mssql");

// Export the SqlServerConnection class
module.exports = class SqlServerConnection {
  // Constructor of the class taking a SqlServerConnection client instance
  constructor(client) {
    this._client = client;
    // Error event handler that logs a message to the logger and sets the client to null
    client.on("error", (err) => {
      $.logger.warn("Error on client: %s", err.message);
      // Drop the client after an error
      this._client = null;
    });
  }

  // This function must be called when the client is no longer being used
  // in order to terminate the connection
  dispose() {
    if (this._client) {
      this._client.close();
      this._client = null;
    } else {
      throw new Error("Connection is already closed");
    }
  }

  // Checks whether the client is online or not
  online() {
    return this._client !== null;
  }

  // This function executes a query and resolve to a promise
  // containing a tuple with the first element being the result
  // of the query, and the second element the fields returned from
  // the query
  async query(query, ...params) {
    if (this._client) {
      // Creates a new request on the connection, and executes the query
      return await new mssql.Request(this._client).query(query);
    } else {
      throw new Error("Connection is offline");
    }
  }

  // Static method used to asynchronously create a new client, which is necessary since creating a connection
  // is an asynchronous operation
  static async Connect(connOpts) {
    //Create the client with the provided options and connect with DB
    const client = await mssql.connect(connOpts);

    //If the connection is OK, create a SqlServerConnection instance
    //using the client and return it
    return new SqlServerConnection(client);
  }
};

The periodic script has a 10-second trigger, and it's used to retrieve data from the database and save it to the source tags. It has the following code:

const SqlServerConnection = $.lib("mssql_connector");

// Define the options used to connect to Microsoft SQL Server
const OPTIONS = {
  server: "10.1.3.169",
  port: 1433,
  user: "sa",
  password: "n3uron$!",
  database: "n3_scripting_manual",
};

// If the local variables are not initialized, initialize them
if ($.local.init === undefined) {
  await init();
}

// Retrieve the data, and exit the script after the async call is finish
await getData();

// Async function used to retrieve the data
async function getData() {
  // If the client is not online, attempt to reconnect
  if ($.local.client === null || !$.local.client.online()) {
    const client = await connect();
    // If the client connects OK, save it to $.local.client and keep going
    if (client !== null) {
      $.local.client = client;
    } else {
      // If the client is null, it means the connection was unsuccessful, so the query can't be executed
      return;
    }
  }
  try {
    // Try to retrieve the data using a SELECT query
    const queryResult = await $.local.client.query(
      "SELECT tag, value FROM dbo.data"
    );
    // For every row in the result, destructure the row object into the tag and value variables
    // and execute a tag alias update
    for (const { tag, value } of queryResult) {
      $.api.tag.updateAlias(tag, value);
    }
  } catch (ex) {
    // If the query has an error, log the error
    $.logger.warn("Error during query: %s", ex.message);
  }
}

// Creates and returns a new Microsoft SQL Server client
async function connect() {
  $.logger.debug(
    "Connecting to Microsoft SQL Server using %s@%s:%d",
    OPTIONS.user,
    OPTIONS.server,
    OPTIONS.port
  );
  try {
    const client = await SqlServerConnection.Connect(OPTIONS);
    $.logger.debug("Connected OK");
    return client;
  } catch (ex) {
    $.logger.warn("Error connecting client: %s", ex.message);
    return null;
  }
}

// Initializes the required local variables
async function init() {
  $.local.init = true;
  // The client must be saved in a local variable to only has one instance
  // that persists between script executions
  $.local.client = null;
}

Writing data to XLSX files

This example reads data from Historian, using the history API call, and stores it to a template XLSX file that can be used for further data processing. This example will use the following XLSX file:

The Processed Data worksheet contains an average function over all the rows (skipping the first row, which are going to be used as the headers) of column B of _Data, while the _Data worksheet is empty, and it will be automatically filled by N3uron based on data from Historian.

In order to have access to the XLSX writer library, an internal library instance must be created:

Finally, the following script is used to retrieve events from Historian, and insert them as well as the headers:

// Load the XLSX library
const Workbook = $.lib("xlsx");

const tag = "/Memory/Historical";

// Retrieve 1 hour of data
const end = Date.now();
const start = end - 3600000;
const history = await $.api.tag.history(tag, start, end, { mode: "raw" });

// Open the template workbook
const workbookTemplatePath = String.raw`C:\Dev\Manual.xlsx`;
$.logger.debug("Opening workbook...");
const workbook = await Workbook.Open(workbookTemplatePath);

// Retrieve the _Data worksheet
$.logger.debug("Workbook opened OK, retrieving worksheet...");
const sheet = await workbook.getSheet("_Data");
$.logger.debug("Worksheet retrieved OK, inserting data...");

// Write the headers in the first row
sheet.writeData([["Timestamp", tag]], 0, 0);

// Parse the data as necessary
const cellData = [];
for (const [ts, value] of history.data) {
  cellData.push([ts, value]);
}

// Write the data, setting the startRow to 1 in order to not overwrite the headers
sheet.writeData(cellData, 1, 0);

// Save the workbook to a different path to avoid overwriting the template
const workbookOutPath = String.raw`C:\Dev\Manual_modified.xlsx`;
$.logger.debug("Data inserted OK, saving workbook...");
await workbook.save(workbookOutPath);
$.logger.debug("Workbook saved OK");

Was this article helpful?

What's Next