Laserphile/JS-Telecortex-2-Server

View on GitHub
src/main.js

Summary

Maintainability
A
0 mins
Test Coverage
/* eslint-disable global-require,no-param-reassign */
import sourceMapSupport from 'source-map-support';
import { FRESH_CONTEXT, opcPort, consoleErrorHandler } from '@js-telecortex-2/js-telecortex-2-util';
import yargs from 'yargs';
import { MIDDLEWARE_OPTIONS } from './opc';
import { opcTCPServer } from './opc/tcp-server';
import { opcUDPServer } from './opc/udp-server';
import 'core-js/es/object/from-entries';

sourceMapSupport.install();
const PBX = require('pixelblaze-expander');

let SPI;
if (process.platform === 'linux') {
  SPI = require('pi-spi');
} else {
  console.warn('USING FAKE SPI');
  SPI = require('./testSpi').default;
}

// const SERVER_CONF = require('./config').default;

const configureSpi = (context, { spiOptions }) => {
  const { clockSpeed, mode, channels } = spiOptions;

  Object.entries(channels).forEach(([channelIdx, { bus, device }]) => {
    const spi = SPI.initialize(`/dev/spidev${bus}.${device}`);
    spi.clockSpeed(clockSpeed);
    spi.dataMode(mode);
    context.channels[channelIdx] = data => {
      spi.transfer(Buffer.from(data), data.length, consoleErrorHandler);
    };
  });
};

const configurePbx = (context, { pbxOptions }) => {
  const { ports, channels } = pbxOptions;
  context.devices = {};
  Object.entries(ports).forEach(([portIdx, { name, options }]) => {
    context.devices[portIdx] = new PBX.ExpanderDevice(name, options);
  });
  Object.entries(channels).forEach(([channelIdx, { port, channel }]) => {
    context.channels[channelIdx] = data => {
      // console.log(JSON.stringify(data));
      context.devices[port].send(channel, data, true);
    };
  });
};

const DEVTYPE_OPTIONS = {
  PBX: configurePbx,
  SPI: configureSpi
};

const TRANSPORT_OPTIONS = {
  TCP: opcTCPServer,
  UDP: opcUDPServer
};

const groupYargsOptions = (options, group) => {
  return Object.fromEntries(
    Object.entries(options).map(([key, value]) => {
      return [`${group}-${key}`, { ...value, group }];
    })
  );
};

const unpackYargsGroup = (options, group, args) => {
  const argKeys = Object.keys(args);
  return Object.fromEntries([
    [
      `${group}Options`,
      Object.entries(options).reduce((accumulator, [key, spec]) => {
        const target = `${group}-${key}`;
        if (argKeys.includes(target)) {
          let value = args[target];
          if (spec.json) {
            try {
              value = JSON.parse(value);
            } catch (err) {
              throw new SyntaxError(`could not parse JSON string: '${value}'\n${err}`);
            }
          }
          accumulator[key] = value;
        }
        return accumulator;
      }, {})
    ]
  ]);
};

const SPI_OPTIONS = {
  clockSpeed: {
    description: 'SPI Clock speed to use for all devices',
    default: 3e6,
    type: 'number'
  },
  mode: {
    description: 'SPI Data mode to use for all devices',
    default: 0,
    type: 'number'
  },
  channels: {
    description: 'SPI Channel information (as JSON)',
    type: 'string',
    json: true,
    default:
      '{"0": {"bus": 0, "device": 0}, "1": {"bus": 0, "device": 1}, "2": {"bus": 1, "device": 0}, "3": {"bus": 1, "device": 1}}'
  }
};

const PBX_OPTIONS = {
  ports: {
    description: 'PBX Serial port definitions for js-pixelblaze-expander',
    type: 'string',
    json: true,
    default:
      '{"0": {"name": "/dev/ttyS0", "options": {"channels": { "0": { "capacity": 300 }, "1": { "capacity": 300 }, "2": { "capacity": 300 }, "3": { "capacity": 300 }, "4": { "capacity": 300 }, "5": { "capacity": 300 }}}}}'
  },
  channels: {
    description: 'PBX channel definitions for js-pixelblaze-expander',
    type: 'string',
    json: true,
    default:
      '{"0": { "port": 0, "channel": 0 }, "1": { "port": 0, "channel": 1 }, "2": { "port": 0, "channel": 2 }, "3": { "port": 0, "channel": 3 }, "4": { "port": 0, "channel": 4 }, "5": { "port": 0, "channel": 5 }}'
  }
};

const YARGS_OPTIONS = {
  devType: {
    description: 'Type of device used',
    alias: 'd',
    default: 'SPI',
    choices: Object.keys(DEVTYPE_OPTIONS)
  },
  port: {
    description: 'port used to listen for OPC commands',
    alias: 'p',
    default: opcPort,
    type: 'number'
  },
  brightness: {
    description: 'global brightness value (from 0.0 to 1.0)',
    alias: 'b',
    default: 1.0,
    type: 'number'
  },
  transportProtocol: {
    description: 'OSI Transport Layer protocol with which the server will listen',
    alias: 't',
    default: 'UDP',
    choices: Object.keys(TRANSPORT_OPTIONS)
  },
  middlewareProtocol: {
    description: 'Protocol used to translate colours before sending to device',
    alias: 'm',
    default: 'colours2sk9822',
    choices: Object.keys(MIDDLEWARE_OPTIONS)
  }
};

const parseArgs = args => {
  const result = yargs
    .env('TELECORTEX')
    .options(YARGS_OPTIONS)
    .options(groupYargsOptions(SPI_OPTIONS, 'spi'))
    .options(groupYargsOptions(PBX_OPTIONS, 'pbx'))
    .help()
    .alias('help', 'h')
    .parse(args);
  return {
    ...result,
    ...unpackYargsGroup(SPI_OPTIONS, 'spi', result),
    ...unpackYargsGroup(PBX_OPTIONS, 'pbx', result)
  };
};

export const server = args => {
  // const config = { ...SERVER_CONF, ...parseArgs() };
  const config = parseArgs(args);
  console.log('config: ', config);
  const { port, devType, middlewareProtocol, transportProtocol, brightness } = config;
  // TODO: flick status led

  const context = {
    ...FRESH_CONTEXT,
    channels: {},
    opcPort: port,
    middlewareProtocol,
    brightness
  };

  const configureFn = DEVTYPE_OPTIONS[devType];
  if (!configureFn) throw new Error(`unknown devType: `, devType);
  configureFn(context, config);

  const serverFn = TRANSPORT_OPTIONS[transportProtocol];
  if (!serverFn) throw new Error(`unknown transportProtocol: `, transportProtocol);
  serverFn(context);
};

server(process.argv);