node-wsjtx-companion – Blame information for rev 4

Subversion Repositories:
Rev:
Rev Author Line No. Line
1 office 1 #!/usr/bin/env node
2 /*************************************************************************/
3 /* Copyright (C) 2023 Wizardry and Steamworks - License: GNU GPLv3 */
4 /*************************************************************************/
5  
6 // Import packages.
7 const https = require('https')
8 const http = require('http')
9 const path = require('path')
10 const fs = require('fs')
11 const url = require('url')
12 const moment = require('moment')
13 const winston = require('winston')
14 const yargs = require('yargs')
15 const dns = require('dns')
16 const dgram = require('dgram')
4 office 17 const udpServer = dgram.createSocket( { type: 'udp4', reuseAddr: true } )
1 office 18 const { WsjtxUdpParser } = require('@henriquegravina/wsjtxudpparser')
19 const { AdifReader } = require('tcadif')
20 const Readable = require('stream').Readable
21  
22 // Local imports.
23 const Handler = require(
24 path
25 .resolve(
26 path.dirname(require.main.filename),
27 'src',
28 'handler'
29 )
30 )
31  
32 const certs = require(
33 path
34 .resolve(
35 path.dirname(require.main.filename),
36 'src',
37 'certs'
38 )
39 )
40  
41 // Load configuration file.
42 const config = require(
43 path
44 .resolve(
45 path.dirname(require.main.filename),
46 'config'
47 )
48 )
49  
50 // Get command-line arguments.
51 const argv = yargs
52 .version()
53 .option('root', {
54 alias: 'd',
55 describe: 'Path to the document root',
56 demandOption: true
57 })
58 .help()
59 .argv
60  
61 // Create various logging mechanisms.
62 // RFC5424 - { emerg: 0, alert: 1, crit: 2, error: 3, warning: 4, notice: 5, info: 6, debug: 7 }
63 winston.setLevels(winston.config.syslog.levels)
64 const log = new winston.Logger({
65 transports: [
66 new winston.transports.File({
67 level: 'info',
68 filename: path.resolve(
69 path.dirname(require.main.filename),
70 config.log.file
71 ),
72 handleExceptions: true,
73 json: false,
74 maxsize: 1048576, // 1MiB.
75 maxFiles: 10, // Ten rotations.
76 colorize: false,
77 timestamp: () => moment()
78 .format('YYYYMMDDTHHmmss')
79 }),
80 new winston.transports.Console({
81 level: 'info',
82 handleExceptions: true,
83 json: false,
84 colorize: true,
85 timestamp: () => moment()
86 .format('YYYYMMDDTHHmmss')
87 })
88 ],
89 exitOnError: false
90 })
91  
92 fs.realpath(argv.root, (error, root) => {
93 if (error) {
94 log.error('Could not find document root: ' + argv.root)
95 process.exit(1)
96 }
97  
98 // Start HTTP server.
99 http.createServer(
100 // authentication,
101 (request, response) => {
102 // Configuration path requested, so send the server configuration if allowed.
103 if (config.configuration.enable === true &&
104 url.parse(request.url, true).path ===
105 config.configuration.path) {
106 const address = request.socket.address()
107 log.info('HTTP Server configuration requested by: ' +
108 address.address + ':' +
109 address.port
110 )
111 response.setHeader('Content-Type', 'application/json')
112 response.end(JSON.stringify(config))
113 return
114 }
115  
116 // Process the resource.
117 new Handler().process(config, request, response, root)
118 .on('log', (data) => {
119 log.log(data.severity, data.message)
120 })
121 .on('data', (result) => {
122 response.setHeader('Content-Type', result.type)
123 response.writeHead(result.status)
124 result.data.pipe(response)
125 })
126 }
127 ).listen(config.net.port, config.net.address, () => {
128 log.info('HTTP Server accessible at: http://' +
129 config.net.address +
130 ':' +
131 config.net.port +
132 ' and serving files from directory: ' +
133 root
134 )
135 })
136  
137 // Start HTTPs server if enabled.
138 if (config.ssl.enable) {
139 // Generate certificates for HTTPs.
140 certs.generate(
141 config.site.name,
142 config.net.address,
143 config.ssl.privateKeySize,
144 (certificates) => {
145 https.createServer(
146 // authentication,
147 {
148 key: certificates.privateKey,
149 cert: certificates.certificate,
150 },
151 (request, response) => {
152 // Configuration path requested, so send the server configuration if allowed.
153 if (config.configuration.enable === true &&
154 url.parse(request.url, true).path ===
155 config.configuration.path) {
156 const address = request.socket.address()
157 log.info('HTTP Server configuration requested by: ' +
158 address.address + ':' +
159 address.port
160 )
161 response.setHeader('Content-Type', 'application/json')
162 response.end(JSON.stringify(config))
163 return
164 }
165  
166  
167 new Handler().process(config, request, response, root)
168 .on('log', (data) => {
169 log.log(data.severity, data.message)
170 })
171 .on('data', (result) => {
172 response.setHeader('Content-Type', result.type)
173 response.writeHead(result.status)
174 result.data.pipe(response)
175 })
176 }
177 ).listen(config.ssl.port, config.ssl.address, () => {
178 log.info('HTTPs Server accessible at: https://' +
179 config.ssl.address +
180 ':' +
181 config.ssl.port +
182 ' and serving files from directory: ' +
183 root
184 )
185 })
186 }
187 )
188 }
189 })
190  
191 // start UDP server to listen to WSJT-X UDP messages
192 udpServer.on('error', (err) => {
193 log.error(`UDP server error:\n${err.stack}`)
194 udpServer.close()
195 })
196  
197 // when UDP server is listening
198 udpServer.on('listening', () => {
199 const address = udpServer.address()
200 log.info(`UDP server listening on ${address.address}:${address.port}`)
201 })
202  
203 // process WSJT-X messages in order to generate QSL cards
204 udpServer.on('message', (buffer, rinfo) => { // A cada mensagem recebida passa aqui:
205 try {
206 const msg = new WsjtxUdpParser(buffer)
207 switch (msg.type) {
208 // Decode Adif message
209 case 12:
210 const stringReadable = new Readable()
211 const adifReader = new AdifReader()
212 adifReader.on(
213 'data',
214 record => {
215 // load the QSL template
216 const templateStream = fs.createReadStream(config.ham.qslTemplate)
217 const templateBuffer = []
218 templateStream.on('data', (chunk) => {
219 templateBuffer.push(chunk)
220 })
221 templateStream.on('end', () => {
222 // perform substitutions in order to generate the QSL card
223 let template = Buffer.concat(templateBuffer).toString()
224  
225 // our station
226 let x = config.ham.me
227 let o = config.ham.qslTemplateMap.this
228 x = x.padStart((x.length + o.length) / 2).padEnd(o.length)
229 template = template.replace(o, x)
230  
231 // their station
232 let call = record.CALL
233 var split = call.split('/')
234 x = split.reduce(
235 function (a, b) {
236 return a.length > b.length ? a : b;
237 }
238 )
239 o = config.ham.qslTemplateMap.call
240 x = x.padStart((x.length + o.length) / 2).padEnd(o.length)
241 template = template.replace(o, x)
242  
243 // the date of the QSO
244 x = record.QSO_DATE
245 o = config.ham.qslTemplateMap.date
246 x = x.padStart((x.length + o.length) / 2).padEnd(o.length)
247 template = template.replace(o, x)
248  
4 office 249 // the frequency, mode and band
1 office 250 x = `${parseInt(record.FREQ)} / ${record.MODE} / ${record.BAND}`
251 o = config.ham.qslTemplateMap.conn
252 x = x.padStart((x.length + o.length) / 2).padEnd(o.length)
253 template = template.replace(o, x)
254  
255 // the signal report
256 x = `${record.RST_SENT}/${record.RST_RCVD}`
257 o = config.ham.qslTemplateMap.sign
258 x = x.padStart((x.length + o.length) / 2).padEnd(o.length)
259 template = template.replace(o, x)
260  
261 // write the QSL card to storage
262 const qslStream = fs.createWriteStream(`www/qsl/${call}.txt`)
263 qslStream.write(template)
264 log.info(`Wrote QSL for ${call}`)
265 })
266 }
267 )
268 adifReader.on(
269 'error',
270 err => {
271 console.error('err', err)
272 }
273 )
274 // push the adif reported by WSJT-X to the stream to be processed
275 stringReadable.push(msg.adif)
276 stringReadable.push(null)
277 stringReadable.pipe(adifReader)
278 break;
279 }
280 } catch (exception) {
281 log.error(`Exception thrown while reading WSJT-X packet ${exception}`)
282 }
283 })
284  
285 // bind to the WSJT-X UDP port in order to process messages
286 udpServer.bind(config.ham.udpPort)