javascriptnode.jsserial-portnode-serialporttransform-stream

How to write an async function that resolves when `data` event emitter fires


I am using node-serialport to communicate with a piece of hardware. It just writes a command and receives a response.

https://serialport.io/docs/en/api-parsers-overview

The following code works:

const port = new SerialPort(path);
const parser = port.pipe(new Readline({ delimiter: '\r', encoding: 'ascii' }));

const requestArray = [];

parser.on('data', (data) => {
  // get first item in array
  const request = requestArray[0];

  // remove first item
  requestArray.shift();

  // resolve promise
  request.promise.resolve(data);
});

export const getFirmwareVersion = async () => {
  let resolvePromise;
  let rejectPromise;

  const promise = new Promise((resolve, reject) => {
    resolvePromise = resolve;
    rejectPromise = reject;
  });

  const title = 'getFirmwareVersion';
  const cmd = 'V\r';

  requestArray.push({
    title,
    cmd,
    promise: {
      resolve: resolvePromise,
      reject: rejectPromise
    }
  });

  await v2Port.write(cmd);

  return promise;
};

Then from my app (which is written in electron/react) I can call the function:

<Button onClick={() => {
  let data = await _api.getFirmwareVersion();
  console.log('done waiting...');
  console.log(data);
}>
  Click Me
</Button>

Is there anyway I can refactor this code to make it more succinct?

Is there a way to get the Promise from the async function, rather than having to make a new Promise?

Is there a way to tap into the Transform Stream that already exists and pipe the Promise in there somehow?

I'm also new to async/await, and wanted to avoid using callbacks, especially in the React/Redux side of things.

I aim to have a lot of these endpoints for the api (i.e. getFirmwareVersion, getTemperature, etc...). So I want to make the code as concise as possible. I don't want the UI to have any underlying knowledge of how the API is getting the data. It just needs to request it like any other API and wait for a response.


Solution

  • Based on help from Mehmet, here is what I ended up with:

    const _port = new SerialPort(path);
    const _parser = _port.pipe(new Readline({ delimiter: '\r', encoding: 'ascii' }));
    
    const waitForData = async () => {
      return new Promise((resolve, reject) => {
        const timeoutId = setTimeout(() => reject('Write Timeout'), 500);
    
        _parser.once('data', (data) => {
          clearTimeout(timeoutId);
          resolve(data);
        });
      });
    };
    
    const createAPIFunction = (cmdTemplate, validationString) => {
      return async (config) => {
        try {
          // replace {key} in template with config[key] props
          const cmd = cmdTemplate.replace(/{(\w+)}/g, (_, key) => {
            return config[key];
          });
    
          _port.write(cmd + '\r');
    
          const data = await waitForData();
    
          // validate data
          if (data.startsWith(validationString)) {
            // is valid
            return data;
          } else {
            // invalid data
            throw new Error('Invalid Data Returned');
          }
        } catch (err) {
          throw err;
        }
      };
    };
    
    export const getFirmwareVersion = createAPIFunction('V', 'V1');
    export const enableSampling = createAPIFunction('G1{scope}', 'G11');