Node command line tool and client/server application

Colin Rosati
Aug 31, 2018 · 10 min read

Intro

Fti_Scope_collector , FTI_Scope_collector_access_point

These two apps are created for service users to interface Fortress Technology Industry’s metal detection products via a Raspberry Pi running Node.JS. Users are able to stream “Scope”(metal detection unit) data with detailed product session information. These two apps allow for service users to trigger the application from A) Raspberry Pi’s terminal and B) accessing a wireless front-end application from the Raspberry Pi as a server. Both apps accomplish the same functionality. This functionality is dedicated for service users to enter in product information and run time to initialize the scope streaming. The user is able to read previous index data from Scope data sessions.

Once this application finishes collecting data it writes this data in two separate files. The first file scopedata.txt logs packets receive in HEX buffers. The second file scope_index_data.json is an index for the scope data in JSON format. This file contains user environment details such as Time began, size of file, product information, session time, amount of lines in scope session, and file run time.

Getting Started

Terminal application Fti_Scope_collector.js

Assuming you have the correct file directory you should navigate to the parent directory (Data Collector) where FTI_Scope_collector.js is.

In the terminal run node :

$ FTI_Scope_collector.js –t 60000 –m Gelatine Capsules

-t is the session time in milliseconds

-m product message for service user to enter useful product details

Web Server Application FTI_Scope_collector_access_point Server.js

The server is run from the Raspberry Pi terminal from the Node Server.js file

$node server.js

User connects to hidden Wi-Fi network: FTI_SCOPE

The server builds the front-end HTML static pages receiving an HTTP “GET” request.

var server = http.createServer(function(request, response) {
request.on('error', (err) => {
console.error(err.stack);
});
fs.readFile(index, function (err, html) {
if (err) {
response.writeHead(404)
response.write("File not found")
throw err;
}
switch(request.method){
case "GET":
if(request.url ==='/'){
response.writeHeader(200, {"Content-Type": "text/html"});
response.write(html)
response.end()
}
break;

Once the Static page is built the User can interface entering the url address “192.168.1.96:8001” . This server/client app has the same functionality as the node terminal app.

Tutorial

In this section I will elaborate more detail about how this application is built. Both of these Node applications are built on top of previously developed FTI_RPC libraries that interface with the DSP board and metal detector via node UDP datagram Sockets. This library establishes host & port IP address connection for Fortress Technology ARM devices as well as handles RPC packets to and from the metal detectors. The main functionality of these applications develops this library modifying the RPC packets to stream with closure around the application adding user input and data handling that writes to files, parses, and creates an index about scope sessions.

Node Command-line application Fti_Scope_collector

This application does a couple of initialization steps for environment handling. The main function do a few environment functions to allow user interface. These include:

exit_keys() allows for keystroke “Q”, “X”, “esc”, “Ctr+c” to close down the program. Readline library is required for reacting to keystroke press. This function creates a series of if else logic to handle keypresses.

exit_keys()
{
var sc_index = new index()
readline.emitKeypressEvents(process.stdin);
const scope_data = new Fti_scope.Scope
process.stdin.setRawMode(true);
process.stdin.on('keypress', (str, key) => {
if(key.sequence === 'q') {
new Promise((resolve,reject)=>{
if(resolve){
resolve(console.log('q signal quiting ...'))
}else
reject(console.log("quit promise reject"));
}).then((results)=>{
scope_data.scope(1000,'quit')
})
}
if(key.sequence === 'x') {
new Promise((resolve,reject)=>{
if(resolve){
resolve(console.log('x signal quiting ...'))
}else
reject(console.log("quit promise reject"));
}).then((results)=>{
scope_data.scope(1000,'quit')
})
}
if(key.sequence === '\u0003') {
new Promise((resolve,reject)=>{
if(resolve){
resolve(console.log('crt+c signal quiting ...'))
}else
reject(console.log("quit promise reject"));
}).then((results)=>{
scope_data.scope(1000,'quit')
})
}
});

}

Instruction_prompt() logs to console instructions to quit as well as application data file name and file size.

instruction_prompt()
{
console.log("##########################\n Scope Collector\n##########################\n##### Press q, x, ctrl + c to quit\n##### Using the FTI device to begin collecing scope data to "+path);
console.log("##### scope data file ("+path+ ') is '+this.Filesize(path));
}

LineCount() calls the class setup_app attribute get_line(). This attribute returns the amount of lines in data file and logs to console. From fs.statSync.size I return the file size in mega bytes. This is divided by the packet size to receive the amount of lines in the scope data file.

get_line(callback) {
this.packet_size((packet)=>{
console.log("get line packet size", packet);
if(isNaN(packet)){
callback(null,packet)
console.log("packet is not a number", packet);
return packet
}
else{
const stats = fs.statSync(path)
let byteLine = stats.size / packet
if(isNaN(byteLine)){
byteLine = '0'
}
console.log("packet from get line = ", byteLine);
callback(null, byteLine);
return byteLine
}
})
}

Command_arg() parses with Process.ARGV() and returns the command line for session run time and product data. Using Node Process.argv.and process.argv.forEach(val,index){} I am able to parse the command looking for –t, -m, and — reset as command line modifiers.

Arg_time parses the command

 $ FTI_Scope_collector.js –t 60000 –m Gelatine Capsules 

Parsing looks for –T. Arg_time holds the value of 60000 as time in milliseconds.

-m represents product information

— reset wipes all the information saved in the scopedata.txt, scope_index_data.json

command_args(callback){
let Arg_time = this.Arg_time
let myArgs = this.myArgs
myArgs = process.argv.slice(2)
console.log('##### command line arguments = \n',myArgs)
myArgs.forEach((val, index) => {
let i = this.i
i = index
var msg_cmd = formInfo
if(val == "--reset"){
console.log("##### about to erase the all scope data from ", path ,' and ', scope_index)
setTimeout(function(){
fs.writeFile(path, '', function(){console.log('done erasing conent of ',path)})
fs.writeFile( scope_index, '', function(){console.log('done erasing conent of ',scope_index)})
console.log('##### done erasing conent ')
process.exit()
}, 500)
}
if(val == 'formTime'){
Arg_time = formTime
console.log('##### commandline argument rpc stream time =', (Arg_time/60000) +' ( mins )')
return Arg_time;
}
})
new Promise(function(resolve, reject) {
const app = new setup_app
app.get_line(function(err, file_line){
if(file_line)
resolve(file_line)
else
reject(console.log("cant get line number")).catch(function(error){ console.log('caught', error.message); })
})
}).then(function(result) {
console.log("##### amount of data lines in "+ path+ " = ", result); // "Stuff worked!"
callback()
},function(err) {
console.log(err); // Error: "It broke"
})
}

Fti_locate() finds FTI ARM devices and establishes connection returning the DSP IP address.

Fti_Locate(callback){
'use strict'
var Arm_Array = [];
arloc.ArmLocator.scan(1,function(list){
console.log('##### device : ' + list);
callback()
});
return;
}

Scope_data.scope() This is the main asynchronous class call which takes in the returned DSPIP from FTI_LOCATE, time_arg_cb from command_args() and file_line which is returned from get_line(). This attribute call does the majority of the underlying DSP connection, binding socket, and RPC streaming. This class attribute is passed in the runtime as a close callback. This closes the UDP socket.

async function aScope(){
await closer;
console.log("Scope echo...");
arm.echo_cb(function(array){
arm.dsp_open_cb(function(pl){
self.bindSo(dspip,time_arg_cb,file_line,function(){
self.bindNP(dspip, time_arg_cb,function (test){
console.log('bindnp')
self.rpc_stream(function(){
let state = true
console.log('finding the state of scope trigger stream', stateCB)
},closer);
});
},closer);
});
});
}

Node server/client application FTI_Scope_collector_access_point

This server/client application modifies the terminal FTI_Scope_collector app and transforms the Raspberry Pi into a hidden access point broadcasting a server using Node.js. A user can find the hidden network “FTI_SCOPE” and access the front-end web app. This web app transforms the command line user interfaces changing the input methods and the environment details. The server uses AJAX to request and respond to the clients XMLHttpRequest calls. As noted before the server builds the static index.html page from a GET response.

var server = http.createServer(function(request, response) {
request.on('error', (err) => {
console.error(err.stack);
});
fs.readFile(index, function (err, html) {
if (err) {
response.writeHead(404)
response.write("File not found")
throw err;
}
switch(request.method){
case "GET":
if(request.url ==='/'){
response.writeHeader(200, {"Content-Type": "text/html"});
response.write(html)
response.end()
}
break;

The server uses the library HTTP to create a new server. Inside of this server we use the Node File System fs.readFile to read our html file. The response writes the HTML file building the front end.

To retrieve index data from the server is achieved by pressing the “Get Index data” button. This button calls the loadIndex() creating an XHR “POST” request. This function additionally writes the data and state to our body DIVS

function loadIndex(){
console.log('button clicked')
var xhr = new XMLHttpRequest();
xhr.open('POST', 'index', true);
xhr.onload = function (){
if(this.status == 200){
var response = this.responseText
console.log(response)
document.getElementById("index").innerHTML = response;
document.getElementById("state").innerHTML = 'load Index data';
}
}
xhr.onerror = function(){
console.log("request error...")
}
xhr.send()
}

Once the server receives this “POST” request.url “/index”, it responds sending the index data. Note all of the post response will be in this switch statement.

case "POST":
if (request.url === '/index'){
console.log("hit your button baby!")
loadJSON((data)=>{
console.log('callbak data', data)
response.write(data)
response.end()
})
}

Once the server responds with the text data it is added to the innerHTML of the index and state DIVs.

To begin the scope streaming the user needs to fill in the two forms: Time and Product. Without these forms filled out pressing the Submit button will not work. You will be prompted to enter a valid number inside <div id=”state”></div> element. The submit button will also populate the form fields in the <div id=”index ></div> element.

Submitting the form creates another XHR “POST” request in our switch statement.

function formSubmit(){let Time = document.getElementById('time')
let Info = document.getElementById('info')
var data = '{"time":"'+time.value+'", "info":"'+info.value+'"}';
var xhr = new XMLHttpRequest();
xhr.open('POST', 'scopeform', true);
xhr.onerror = function(){
console.log("request error...")
}
xhr.onload = function (){
if(this.status == 200){
var response = this.responseText
console.log('responding to form', response)
document.getElementById("index").innerHTML = response;
if(isNaN(time.value) ){
console.log('time argument is not valid, please enter a number for streaming scope data in millieseconds')
document.getElementById("state").innerHTML = 'time argument is not valid, please enter a number for streaming scope data in millieseconds';
}
}
}
xhr.send(data)
}

Notice I check the response if we recieve a valid number with if(isNaN(time.value)){} to populate the state DIV element.

The server routes the request and responds with the submitted form data.

else if (request.url === '/scopeform'){
let reqBody = '';
request.on('data', (data)=>{ // adding chunks of data to request body
response.write(data)
reqBody += data.toString();
if(reqBody.lenght > 1e7) // max allowance of chunks is 10mb show other response
{
response.writeHead(413, 'Request message too large',{"Content-Type": "text/html"})
response.write('413: server resuest is too big')
response.end()
}
});
request.on('end',(data)=>{
const scopedata = reqBody
formBody = reqBody
response.write(reqBody)
response.end()
});
}

Once the two forms are filled out and the submit button is pressed the user can press the Stream Scope Data Button. Once the streaming session is complete the application appends its new data to appropriate files.

This stream scope creates a new XHR “POST” request.

function scopeTrigger(){
let Time = document.getElementById('time')
let Info = document.getElementById('info')
var data = '{"time":"'+time.value+'", "info":"'+info.value+'"}';
if(isNaN(time.value) ){
console.log('time argument is not valid, please enter a number for streaming scope data in millieseconds')
document.getElementById("state").innerHTML = 'time argument is not valid!!! Submit a number in time form';
}var xhr = new XMLHttpRequest();
xhr.open('POST', 'scope', true);
xhr.onerror = function(){
console.log("request error...")
}
xhr.onload = function (){
if(this.status == 200){
document.getElementById("state").innerHTML = this.responseText
}
}
xhr.send()
}

The server route calls trigger() in a promise asynchronisly to update the state DIV of the index.html page. This trigger function calls my FTI_SCOPE library to begin the scope streaming.

else if (request.url === '/scope'){
response.write('streaming')
new Promise((resolve,reject)=>{
let state = false
response.write("streaming")
trigger(state) // make this either return true or false

if(state){
resolve('streaming complete')
} else {
reject('still streaming')
}
}).then((results)=>{
console.log('trigger promise state',results)
response.write(results)
response.end()
}).catch((results)=>{
response.write(results)
response.end()
})
}

This class attribute is essentially the same as the node asynchronous Scope function. The only difference is it returns its state.

async function aScope(){
// await lineCount;
await closer;
console.log("Scope echo...");
arm.echo_cb(function(array){
// console.log("echo_cb array ...",array);
arm.dsp_open_cb(function(pl){
// console.log("dsp open. ..",pl);
self.bindSo(dspip,time_arg_cb,file_line,function(){
self.bindNP(dspip, time_arg_cb,function (test){
console.log('bindnp')
// self.photoEye(function(){
self.rpc_stream(function(){
let state = true
console.log('finding the state of scope trigger stream', stateCB)
},closer);

// })
});
},closer);
});
});
}

The Reset button erases all the data from the index and data file. This populates the DIV “state” element. This button creates a XHR “POST” request. Where the server routes the request to reset the data files.

Reseting the data involves writing both the files with blank with an empty string ‘ ’ using the node FS.writefile() function.

function reset(){
console.log("##### about to erase the all scope data from ", path ,' and ', scope_index)
setTimeout(function(){
fs.writeFile(path, '', function(){console.log('done erasing conent of ',path)})
fs.writeFile( scope_index, '', function(){console.log('done erasing conent of ',scope_index)})
console.log('##### done erasing conent ')
process.exit()
}, 500)
}

The Close Scope button ends the front-end session populating the DIV “state” element with a close session state signal. This is achieved through an XHR “POST” request and server routing.

The server route calls a function to close the app with process.exit()

function close_web_app(){
console.log('quiting web app')
process.exit()
}

This web application uses node to create a server and routes data to the FTI library interfacing with the front end XHR requests.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade