node-wsjtx-companion – Rev 5

Subversion Repositories:
Rev:
#!/usr/bin/env node
/*************************************************************************/
/*    Copyright (C) 2023 Wizardry and Steamworks - License: GNU GPLv3    */
/*************************************************************************/

// Import packages.
const https = require('https')
const http = require('http')
const path = require('path')
const fs = require('fs')
const url = require('url')
const moment = require('moment')
const winston = require('winston')
const yargs = require('yargs')
const dns = require('dns')
const dgram = require('dgram')
const udpServer = dgram.createSocket( { type: 'udp4', reuseAddr: true } )
const { WsjtxUdpParser } = require('@henriquegravina/wsjtxudpparser')
const { AdifReader } = require('tcadif')
const Readable = require('stream').Readable

// Local imports.
const Handler = require(
    path
        .resolve(
            path.dirname(require.main.filename),
            'src',
            'handler'
        )
)

const certs = require(
    path
        .resolve(
            path.dirname(require.main.filename),
            'src',
            'certs'
        )
)

// Load configuration file.
const config = require(
    path
        .resolve(
            path.dirname(require.main.filename),
            'config'
        )
)

// Get command-line arguments.
const argv = yargs
    .version()
    .option('root', {
        alias: 'd',
        describe: 'Path to the document root',
        demandOption: true
    })
    .help()
    .argv

// Create various logging mechanisms.
// RFC5424 - { emerg: 0, alert: 1, crit: 2, error: 3, warning: 4, notice: 5, info: 6, debug: 7 }
winston.setLevels(winston.config.syslog.levels)
const log = new winston.Logger({
    transports: [
        new winston.transports.File({
            level: 'info',
            filename: path.resolve(
                path.dirname(require.main.filename),
                config.log.file
            ),
            handleExceptions: true,
            json: false,
            maxsize: 1048576, // 1MiB.
            maxFiles: 10, // Ten rotations.
            colorize: false,
            timestamp: () => moment()
                .format('YYYYMMDDTHHmmss')
        }),
        new winston.transports.Console({
            level: 'info',
            handleExceptions: true,
            json: false,
            colorize: true,
            timestamp: () => moment()
                .format('YYYYMMDDTHHmmss')
        })
    ],
    exitOnError: false
})

fs.realpath(argv.root, (error, root) => {
    if (error) {
        log.error('Could not find document root: ' + argv.root)
        process.exit(1)
    }

    // Start HTTP server.
    http.createServer(
        // authentication,
        (request, response) => {
            // Configuration path requested, so send the server configuration if allowed.
            if (config.configuration.enable === true &&
                url.parse(request.url, true).path ===
                config.configuration.path) {
                const address = request.socket.address()
                log.info('HTTP Server configuration requested by: ' +
                    address.address + ':' +
                    address.port
                )
                response.setHeader('Content-Type', 'application/json')
                response.end(JSON.stringify(config))
                return
            }

            // Process the resource.
            new Handler().process(config, request, response, root)
                .on('log', (data) => {
                    log.log(data.severity, data.message)
                })
                .on('data', (result) => {
                    response.setHeader('Content-Type', result.type)
                    response.writeHead(result.status)
                    result.data.pipe(response)
                })
        }
    ).listen(config.net.port, config.net.address, () => {
        log.info('HTTP Server accessible at: http://' +
            config.net.address +
            ':' +
            config.net.port +
            ' and serving files from directory: ' +
            root
        )
    })

    // Start HTTPs server if enabled.
    if (config.ssl.enable) {
        // Generate certificates for HTTPs.
        certs.generate(
            config.site.name,
            config.net.address,
            config.ssl.privateKeySize,
            (certificates) => {
                https.createServer(
                    // authentication,
                    {
                        key: certificates.privateKey,
                        cert: certificates.certificate,
                    },
                    (request, response) => {
                        // Configuration path requested, so send the server configuration if allowed.
                        if (config.configuration.enable === true &&
                            url.parse(request.url, true).path ===
                            config.configuration.path) {
                            const address = request.socket.address()
                            log.info('HTTP Server configuration requested by: ' +
                                address.address + ':' +
                                address.port
                            )
                            response.setHeader('Content-Type', 'application/json')
                            response.end(JSON.stringify(config))
                            return
                        }


                        new Handler().process(config, request, response, root)
                            .on('log', (data) => {
                                log.log(data.severity, data.message)
                            })
                            .on('data', (result) => {
                                response.setHeader('Content-Type', result.type)
                                response.writeHead(result.status)
                                result.data.pipe(response)
                            })
                    }
                ).listen(config.ssl.port, config.ssl.address, () => {
                    log.info('HTTPs Server accessible at: https://' +
                        config.ssl.address +
                        ':' +
                        config.ssl.port +
                        ' and serving files from directory: ' +
                        root
                    )
                })
            }
        )
    }
})

// start UDP server to listen to WSJT-X UDP messages
udpServer.on('error', (err) => {
    log.error(`UDP server error:\n${err.stack}`)
    udpServer.close()
})

// when UDP server is listening
udpServer.on('listening', () => {
    udpServer.addMembership(config.ham.udpAddress)
    
    const address = udpServer.address()
    log.info(`UDP server listening on ${address.address}:${address.port}`)
})

// process WSJT-X messages in order to generate QSL cards
udpServer.on('message', (buffer, rinfo) => { // A cada mensagem recebida passa aqui:    
    try {
        const msg = new WsjtxUdpParser(buffer)
        switch (msg.type) {
            // Decode Adif message
            case 12:
                const stringReadable = new Readable()
                const adifReader = new AdifReader()
                adifReader.on(
                    'data',
                    record => {
                        // load the QSL template
                        const templateStream = fs.createReadStream(config.ham.qslTemplate)
                        const templateBuffer = []
                        templateStream.on('data', (chunk) => {
                            templateBuffer.push(chunk)
                        })
                        templateStream.on('end', () => {
                            // perform substitutions in order to generate the QSL card
                            let template = Buffer.concat(templateBuffer).toString()

                            // our station
                            let x = config.ham.me
                            let o = config.ham.qslTemplateMap.this
                            x = x.padStart((x.length + o.length) / 2).padEnd(o.length)
                            template = template.replace(o, x)

                            // their station
                            let call = record.CALL
                            var split = call.split('/')
                            x = split.reduce(
                                function (a, b) {
                                    return a.length > b.length ? a : b;
                                }
                            )
                            o = config.ham.qslTemplateMap.call
                            x = x.padStart((x.length + o.length) / 2).padEnd(o.length)
                            template = template.replace(o, x)

                            // the date of the QSO
                            x = record.QSO_DATE
                            o = config.ham.qslTemplateMap.date
                            x = x.padStart((x.length + o.length) / 2).padEnd(o.length)
                            template = template.replace(o, x)

                            // the frequency, mode and band
                            x = `${parseInt(record.FREQ)} / ${record.MODE} / ${record.BAND}`
                            o = config.ham.qslTemplateMap.conn
                            x = x.padStart((x.length + o.length) / 2).padEnd(o.length)
                            template = template.replace(o, x)

                            // the signal report
                            x = `${record.RST_SENT}/${record.RST_RCVD}`
                            o = config.ham.qslTemplateMap.sign
                            x = x.padStart((x.length + o.length) / 2).padEnd(o.length)
                            template = template.replace(o, x)

                            // write the QSL card to storage
                            const qslStream = fs.createWriteStream(`www/qsl/${call}.txt`)
                            qslStream.write(template)
                            log.info(`Wrote QSL for ${call}`)
                        })
                    }
                )
                adifReader.on(
                    'error',
                    err => {
                        console.error('err', err)
                    }
                )
                // push the adif reported by WSJT-X to the stream to be processed
                stringReadable.push(msg.adif)
                stringReadable.push(null)
                stringReadable.pipe(adifReader)
                break;
            default:
                //log.info("Some activity detected...")
                break;
        }
    } catch (exception) {
        log.error(`Exception thrown while reading WSJT-X packet ${exception}`)
    }
})

// bind to the WSJT-X UDP port in order to process messages
udpServer.bind(config.ham.udpPort);

Generated by GNU Enscript 1.6.5.90.