node-wsjtx-companion – Rev 5
?pathlinks?
#!/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.