2302 lines
77 KiB
C
2302 lines
77 KiB
C
// Part of dump1090, a Mode S message decoder for RTLSDR devices.
|
|
//
|
|
// net_io.c: network handling.
|
|
//
|
|
// Copyright (c) 2014-2016 Oliver Jowett <oliver@mutability.co.uk>
|
|
//
|
|
// This file is free software: you may copy, redistribute and/or modify it
|
|
// under the terms of the GNU General Public License as published by the
|
|
// Free Software Foundation, either version 2 of the License, or (at your
|
|
// option) any later version.
|
|
//
|
|
// This file is distributed in the hope that it will be useful, but
|
|
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
// General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
// This file incorporates work covered by the following copyright and
|
|
// permission notice:
|
|
//
|
|
// Copyright (C) 2012 by Salvatore Sanfilippo <antirez@gmail.com>
|
|
//
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are
|
|
// met:
|
|
//
|
|
// * Redistributions of source code must retain the above copyright
|
|
// notice, this list of conditions and the following disclaimer.
|
|
//
|
|
// * Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
#include "dump1090.h"
|
|
|
|
/* for PRIX64 */
|
|
#include <inttypes.h>
|
|
|
|
#include <assert.h>
|
|
#include <stdarg.h>
|
|
|
|
//
|
|
// ============================= Networking =============================
|
|
//
|
|
// Note: here we disregard any kind of good coding practice in favor of
|
|
// extreme simplicity, that is:
|
|
//
|
|
// 1) We only rely on the kernel buffers for our I/O without any kind of
|
|
// user space buffering.
|
|
// 2) We don't register any kind of event handler, from time to time a
|
|
// function gets called and we accept new connections. All the rest is
|
|
// handled via non-blocking I/O and manually polling clients to see if
|
|
// they have something new to share with us when reading is needed.
|
|
|
|
static int handleBeastCommand(struct client *c, char *p);
|
|
static int decodeBinMessage(struct client *c, char *p);
|
|
static int decodeHexMessage(struct client *c, char *hex);
|
|
|
|
static void send_raw_heartbeat(struct net_service *service);
|
|
static void send_beast_heartbeat(struct net_service *service);
|
|
static void send_sbs_heartbeat(struct net_service *service);
|
|
|
|
static void writeFATSVEvent(struct modesMessage *mm, struct aircraft *a);
|
|
static void writeFATSVPositionUpdate(float lat, float lon, float alt);
|
|
|
|
static void autoset_modeac();
|
|
|
|
//
|
|
//=========================================================================
|
|
//
|
|
// Networking "stack" initialization
|
|
//
|
|
|
|
// Init a service with the given read/write characteristics, return the new service.
|
|
// Doesn't arrange for the service to listen or connect
|
|
struct net_service *serviceInit(const char *descr, struct net_writer *writer, heartbeat_fn hb, read_mode_t mode, const char *sep, read_fn handler)
|
|
{
|
|
struct net_service *service;
|
|
|
|
if (!(service = calloc(sizeof(*service), 1))) {
|
|
fprintf(stderr, "Out of memory allocating service %s\n", descr);
|
|
exit(1);
|
|
}
|
|
|
|
service->next = Modes.services;
|
|
Modes.services = service;
|
|
|
|
service->descr = descr;
|
|
service->listener_count = 0;
|
|
service->connections = 0;
|
|
service->writer = writer;
|
|
service->read_sep = sep;
|
|
service->read_mode = mode;
|
|
service->read_handler = handler;
|
|
|
|
if (service->writer) {
|
|
if (! (service->writer->data = malloc(MODES_OUT_BUF_SIZE)) ) {
|
|
fprintf(stderr, "Out of memory allocating output buffer for service %s\n", descr);
|
|
exit(1);
|
|
}
|
|
|
|
service->writer->service = service;
|
|
service->writer->dataUsed = 0;
|
|
service->writer->lastWrite = mstime();
|
|
service->writer->send_heartbeat = hb;
|
|
}
|
|
|
|
return service;
|
|
}
|
|
|
|
// Create a client attached to the given service using the provided socket FD
|
|
struct client *createSocketClient(struct net_service *service, int fd)
|
|
{
|
|
anetSetSendBuffer(Modes.aneterr, fd, (MODES_NET_SNDBUF_SIZE << Modes.net_sndbuf_size));
|
|
return createGenericClient(service, fd);
|
|
}
|
|
|
|
// Create a client attached to the given service using the provided FD (might not be a socket!)
|
|
struct client *createGenericClient(struct net_service *service, int fd)
|
|
{
|
|
struct client *c;
|
|
|
|
anetNonBlock(Modes.aneterr, fd);
|
|
|
|
if (!(c = (struct client *) malloc(sizeof(*c)))) {
|
|
fprintf(stderr, "Out of memory allocating a new %s network client\n", service->descr);
|
|
exit(1);
|
|
}
|
|
|
|
c->service = service;
|
|
c->next = Modes.clients;
|
|
c->fd = fd;
|
|
c->buflen = 0;
|
|
c->modeac_requested = 0;
|
|
Modes.clients = c;
|
|
|
|
++service->connections;
|
|
if (service->writer && service->connections == 1) {
|
|
service->writer->lastWrite = mstime(); // suppress heartbeat initially
|
|
}
|
|
|
|
return c;
|
|
}
|
|
|
|
// Initiate an outgoing connection which will use the given service.
|
|
// Return the new client or NULL if the connection failed
|
|
struct client *serviceConnect(struct net_service *service, char *addr, int port)
|
|
{
|
|
int s;
|
|
char buf[20];
|
|
|
|
// Bleh.
|
|
snprintf(buf, 20, "%d", port);
|
|
s = anetTcpConnect(Modes.aneterr, addr, buf);
|
|
if (s == ANET_ERR)
|
|
return NULL;
|
|
|
|
return createSocketClient(service, s);
|
|
}
|
|
|
|
// Set up the given service to listen on an address/port.
|
|
// _exits_ on failure!
|
|
void serviceListen(struct net_service *service, char *bind_addr, char *bind_ports)
|
|
{
|
|
int *fds = NULL;
|
|
int n = 0;
|
|
char *p, *end;
|
|
char buf[128];
|
|
|
|
if (service->listener_count > 0) {
|
|
fprintf(stderr, "Tried to set up the service %s twice!\n", service->descr);
|
|
exit(1);
|
|
}
|
|
|
|
if (!bind_ports || !strcmp(bind_ports, "") || !strcmp(bind_ports, "0"))
|
|
return;
|
|
|
|
p = bind_ports;
|
|
while (p && *p) {
|
|
int newfds[16];
|
|
int nfds, i;
|
|
|
|
end = strpbrk(p, ", ");
|
|
if (!end) {
|
|
strncpy(buf, p, sizeof(buf));
|
|
buf[sizeof(buf)-1] = 0;
|
|
p = NULL;
|
|
} else {
|
|
size_t len = end - p;
|
|
if (len >= sizeof(buf))
|
|
len = sizeof(buf) - 1;
|
|
memcpy(buf, p, len);
|
|
buf[len] = 0;
|
|
p = end + 1;
|
|
}
|
|
|
|
nfds = anetTcpServer(Modes.aneterr, buf, bind_addr, newfds, sizeof(newfds));
|
|
if (nfds == ANET_ERR) {
|
|
fprintf(stderr, "Error opening the listening port %s (%s): %s\n",
|
|
buf, service->descr, Modes.aneterr);
|
|
exit(1);
|
|
}
|
|
|
|
fds = realloc(fds, (n+nfds) * sizeof(int));
|
|
if (!fds) {
|
|
fprintf(stderr, "out of memory\n");
|
|
exit(1);
|
|
}
|
|
|
|
for (i = 0; i < nfds; ++i) {
|
|
anetNonBlock(Modes.aneterr, newfds[i]);
|
|
fds[n++] = newfds[i];
|
|
}
|
|
}
|
|
|
|
service->listener_count = n;
|
|
service->listener_fds = fds;
|
|
}
|
|
|
|
struct net_service *makeBeastInputService(void)
|
|
{
|
|
return serviceInit("Beast TCP input", NULL, NULL, READ_MODE_BEAST, NULL, decodeBinMessage);
|
|
}
|
|
|
|
struct net_service *makeFatsvOutputService(void)
|
|
{
|
|
return serviceInit("FATSV TCP output", &Modes.fatsv_out, NULL, READ_MODE_IGNORE, NULL, NULL);
|
|
}
|
|
|
|
void modesInitNet(void) {
|
|
struct net_service *s;
|
|
|
|
signal(SIGPIPE, SIG_IGN);
|
|
Modes.clients = NULL;
|
|
Modes.services = NULL;
|
|
|
|
// set up listeners
|
|
s = serviceInit("Raw TCP output", &Modes.raw_out, send_raw_heartbeat, READ_MODE_IGNORE, NULL, NULL);
|
|
serviceListen(s, Modes.net_bind_address, Modes.net_output_raw_ports);
|
|
|
|
s = serviceInit("Beast TCP output", &Modes.beast_out, send_beast_heartbeat, READ_MODE_BEAST_COMMAND, NULL, handleBeastCommand);
|
|
serviceListen(s, Modes.net_bind_address, Modes.net_output_beast_ports);
|
|
|
|
s = serviceInit("Basestation TCP output", &Modes.sbs_out, send_sbs_heartbeat, READ_MODE_IGNORE, NULL, NULL);
|
|
serviceListen(s, Modes.net_bind_address, Modes.net_output_sbs_ports);
|
|
|
|
s = serviceInit("Raw TCP input", NULL, NULL, READ_MODE_ASCII, "\n", decodeHexMessage);
|
|
serviceListen(s, Modes.net_bind_address, Modes.net_input_raw_ports);
|
|
|
|
s = makeBeastInputService();
|
|
serviceListen(s, Modes.net_bind_address, Modes.net_input_beast_ports);
|
|
}
|
|
//
|
|
//=========================================================================
|
|
//
|
|
// This function gets called from time to time when the decoding thread is
|
|
// awakened by new data arriving. This usually happens a few times every second
|
|
//
|
|
static struct client * modesAcceptClients(void) {
|
|
int fd;
|
|
struct net_service *s;
|
|
|
|
for (s = Modes.services; s; s = s->next) {
|
|
int i;
|
|
for (i = 0; i < s->listener_count; ++i) {
|
|
while ((fd = anetTcpAccept(Modes.aneterr, s->listener_fds[i])) >= 0) {
|
|
createSocketClient(s, fd);
|
|
}
|
|
}
|
|
}
|
|
|
|
return Modes.clients;
|
|
}
|
|
//
|
|
//=========================================================================
|
|
//
|
|
// On error free the client, collect the structure, adjust maxfd if needed.
|
|
//
|
|
static void modesCloseClient(struct client *c) {
|
|
if (!c->service) {
|
|
fprintf(stderr, "warning: double close of net client\n");
|
|
return;
|
|
}
|
|
|
|
// Clean up, but defer removing from the list until modesNetCleanup().
|
|
// This is because there may be stackframes still pointing at this
|
|
// client (unpredictably: reading from client A may cause client B to
|
|
// be freed)
|
|
|
|
close(c->fd);
|
|
c->service->connections--;
|
|
|
|
// mark it as inactive and ready to be freed
|
|
c->fd = -1;
|
|
c->service = NULL;
|
|
c->modeac_requested = 0;
|
|
|
|
autoset_modeac();
|
|
}
|
|
//
|
|
//=========================================================================
|
|
//
|
|
// Send the write buffer for the specified writer to all connected clients
|
|
//
|
|
static void flushWrites(struct net_writer *writer) {
|
|
struct client *c;
|
|
|
|
for (c = Modes.clients; c; c = c->next) {
|
|
if (!c->service)
|
|
continue;
|
|
if (c->service == writer->service) {
|
|
#ifndef _WIN32
|
|
int nwritten = write(c->fd, writer->data, writer->dataUsed);
|
|
#else
|
|
int nwritten = send(c->fd, writer->data, writer->dataUsed, 0 );
|
|
#endif
|
|
if (nwritten != writer->dataUsed) {
|
|
modesCloseClient(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
writer->dataUsed = 0;
|
|
writer->lastWrite = mstime();
|
|
}
|
|
|
|
// Prepare to write up to 'len' bytes to the given net_writer.
|
|
// Returns a pointer to write to, or NULL to skip this write.
|
|
static void *prepareWrite(struct net_writer *writer, int len) {
|
|
if (!writer ||
|
|
!writer->service ||
|
|
!writer->service->connections ||
|
|
!writer->data)
|
|
return NULL;
|
|
|
|
if (len > MODES_OUT_BUF_SIZE)
|
|
return NULL;
|
|
|
|
if (writer->dataUsed + len >= MODES_OUT_BUF_SIZE) {
|
|
// Flush now to free some space
|
|
flushWrites(writer);
|
|
}
|
|
|
|
return writer->data + writer->dataUsed;
|
|
}
|
|
|
|
// Complete a write previously begun by prepareWrite.
|
|
// endptr should point one byte past the last byte written
|
|
// to the buffer returned from prepareWrite.
|
|
static void completeWrite(struct net_writer *writer, void *endptr) {
|
|
writer->dataUsed = endptr - writer->data;
|
|
|
|
if (writer->dataUsed >= Modes.net_output_flush_size) {
|
|
flushWrites(writer);
|
|
}
|
|
}
|
|
|
|
//
|
|
//=========================================================================
|
|
//
|
|
// Write raw output in Beast Binary format with Timestamp to TCP clients
|
|
//
|
|
static void modesSendBeastOutput(struct modesMessage *mm) {
|
|
int msgLen = mm->msgbits / 8;
|
|
char *p = prepareWrite(&Modes.beast_out, 2 + 2 * (7 + msgLen));
|
|
char ch;
|
|
int j;
|
|
int sig;
|
|
unsigned char *msg = (Modes.net_verbatim ? mm->verbatim : mm->msg);
|
|
|
|
if (!p)
|
|
return;
|
|
|
|
*p++ = 0x1a;
|
|
if (msgLen == MODES_SHORT_MSG_BYTES)
|
|
{*p++ = '2';}
|
|
else if (msgLen == MODES_LONG_MSG_BYTES)
|
|
{*p++ = '3';}
|
|
else if (msgLen == MODEAC_MSG_BYTES)
|
|
{*p++ = '1';}
|
|
else
|
|
{return;}
|
|
|
|
/* timestamp, big-endian */
|
|
*p++ = (ch = (mm->timestampMsg >> 40));
|
|
if (0x1A == ch) {*p++ = ch; }
|
|
*p++ = (ch = (mm->timestampMsg >> 32));
|
|
if (0x1A == ch) {*p++ = ch; }
|
|
*p++ = (ch = (mm->timestampMsg >> 24));
|
|
if (0x1A == ch) {*p++ = ch; }
|
|
*p++ = (ch = (mm->timestampMsg >> 16));
|
|
if (0x1A == ch) {*p++ = ch; }
|
|
*p++ = (ch = (mm->timestampMsg >> 8));
|
|
if (0x1A == ch) {*p++ = ch; }
|
|
*p++ = (ch = (mm->timestampMsg));
|
|
if (0x1A == ch) {*p++ = ch; }
|
|
|
|
sig = round(sqrt(mm->signalLevel) * 255);
|
|
if (mm->signalLevel > 0 && sig < 1)
|
|
sig = 1;
|
|
if (sig > 255)
|
|
sig = 255;
|
|
*p++ = ch = (char)sig;
|
|
if (0x1A == ch) {*p++ = ch; }
|
|
|
|
for (j = 0; j < msgLen; j++) {
|
|
*p++ = (ch = msg[j]);
|
|
if (0x1A == ch) {*p++ = ch; }
|
|
}
|
|
|
|
completeWrite(&Modes.beast_out, p);
|
|
}
|
|
|
|
static void send_beast_heartbeat(struct net_service *service)
|
|
{
|
|
static char heartbeat_message[] = { 0x1a, '1', 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
|
char *data;
|
|
|
|
if (!service->writer)
|
|
return;
|
|
|
|
data = prepareWrite(service->writer, sizeof(heartbeat_message));
|
|
if (!data)
|
|
return;
|
|
|
|
memcpy(data, heartbeat_message, sizeof(heartbeat_message));
|
|
completeWrite(service->writer, data + sizeof(heartbeat_message));
|
|
}
|
|
|
|
//
|
|
//=========================================================================
|
|
//
|
|
// Write raw output to TCP clients
|
|
//
|
|
static void modesSendRawOutput(struct modesMessage *mm) {
|
|
int msgLen = mm->msgbits / 8;
|
|
char *p = prepareWrite(&Modes.raw_out, msgLen*2 + 15);
|
|
int j;
|
|
unsigned char *msg = (Modes.net_verbatim ? mm->verbatim : mm->msg);
|
|
|
|
if (!p)
|
|
return;
|
|
|
|
if (Modes.mlat && mm->timestampMsg) {
|
|
/* timestamp, big-endian */
|
|
sprintf(p, "@%012" PRIX64,
|
|
mm->timestampMsg);
|
|
p += 13;
|
|
} else
|
|
*p++ = '*';
|
|
|
|
for (j = 0; j < msgLen; j++) {
|
|
sprintf(p, "%02X", msg[j]);
|
|
p += 2;
|
|
}
|
|
|
|
*p++ = ';';
|
|
*p++ = '\n';
|
|
|
|
completeWrite(&Modes.raw_out, p);
|
|
}
|
|
|
|
static void send_raw_heartbeat(struct net_service *service)
|
|
{
|
|
static char *heartbeat_message = "*0000;\n";
|
|
char *data;
|
|
int len = strlen(heartbeat_message);
|
|
|
|
if (!service->writer)
|
|
return;
|
|
|
|
data = prepareWrite(service->writer, len);
|
|
if (!data)
|
|
return;
|
|
|
|
memcpy(data, heartbeat_message, len);
|
|
completeWrite(service->writer, data + len);
|
|
}
|
|
|
|
//
|
|
//=========================================================================
|
|
//
|
|
// Write SBS output to TCP clients
|
|
//
|
|
static void modesSendSBSOutput(struct modesMessage *mm, struct aircraft *a) {
|
|
char *p;
|
|
struct timespec now;
|
|
struct tm stTime_receive, stTime_now;
|
|
int msgType;
|
|
|
|
// For now, suppress non-ICAO addresses
|
|
if (mm->addr & MODES_NON_ICAO_ADDRESS)
|
|
return;
|
|
|
|
p = prepareWrite(&Modes.sbs_out, 200);
|
|
if (!p)
|
|
return;
|
|
|
|
//
|
|
// SBS BS style output checked against the following reference
|
|
// http://www.homepages.mcb.net/bones/SBS/Article/Barebones42_Socket_Data.htm - seems comprehensive
|
|
//
|
|
|
|
// Decide on the basic SBS Message Type
|
|
switch (mm->msgtype) {
|
|
case 4:
|
|
case 20:
|
|
msgType = 5;
|
|
break;
|
|
break;
|
|
|
|
case 5:
|
|
case 21:
|
|
msgType = 6;
|
|
break;
|
|
|
|
case 0:
|
|
case 16:
|
|
msgType = 7;
|
|
break;
|
|
|
|
case 11:
|
|
msgType = 8;
|
|
break;
|
|
|
|
case 17:
|
|
case 18:
|
|
if (mm->metype >= 1 && mm->metype <= 4) {
|
|
msgType = 1;
|
|
} else if (mm->metype >= 5 && mm->metype <= 8) {
|
|
msgType = 2;
|
|
} else if (mm->metype >= 9 && mm->metype <= 18) {
|
|
msgType = 3;
|
|
} else if (mm->metype == 19) {
|
|
msgType = 4;
|
|
} else {
|
|
return;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
return;
|
|
}
|
|
|
|
// Fields 1 to 6 : SBS message type and ICAO address of the aircraft and some other stuff
|
|
p += sprintf(p, "MSG,%d,1,1,%06X,1,", msgType, mm->addr);
|
|
|
|
// Find current system time
|
|
clock_gettime(CLOCK_REALTIME, &now);
|
|
localtime_r(&now.tv_sec, &stTime_now);
|
|
|
|
// Find message reception time
|
|
time_t received = (time_t) (mm->sysTimestampMsg / 1000);
|
|
localtime_r(&received, &stTime_receive);
|
|
|
|
// Fields 7 & 8 are the message reception time and date
|
|
p += sprintf(p, "%04d/%02d/%02d,", (stTime_receive.tm_year+1900),(stTime_receive.tm_mon+1), stTime_receive.tm_mday);
|
|
p += sprintf(p, "%02d:%02d:%02d.%03u,", stTime_receive.tm_hour, stTime_receive.tm_min, stTime_receive.tm_sec, (unsigned) (mm->sysTimestampMsg / 1000));
|
|
|
|
// Fields 9 & 10 are the current time and date
|
|
p += sprintf(p, "%04d/%02d/%02d,", (stTime_now.tm_year+1900),(stTime_now.tm_mon+1), stTime_now.tm_mday);
|
|
p += sprintf(p, "%02d:%02d:%02d.%03u", stTime_now.tm_hour, stTime_now.tm_min, stTime_now.tm_sec, (unsigned) (now.tv_nsec / 1000000U));
|
|
|
|
// Field 11 is the callsign (if we have it)
|
|
if (mm->callsign_valid) {p += sprintf(p, ",%s", mm->callsign);}
|
|
else {p += sprintf(p, ",");}
|
|
|
|
// Field 12 is the altitude (if we have it)
|
|
if (Modes.use_gnss) {
|
|
if (mm->altitude_geom_valid) {
|
|
p += sprintf(p, ",%dH", mm->altitude_geom);
|
|
} else if (mm->altitude_baro_valid && trackDataValid(&a->geom_delta_valid)) {
|
|
p += sprintf(p, ",%dH", mm->altitude_baro + a->geom_delta);
|
|
} else if (mm->altitude_baro_valid) {
|
|
p += sprintf(p, ",%d", mm->altitude_baro);
|
|
} else {
|
|
p += sprintf(p, ",");
|
|
}
|
|
} else {
|
|
if (mm->altitude_baro_valid) {
|
|
p += sprintf(p, ",%d", mm->altitude_baro);
|
|
} else if (mm->altitude_geom_valid && trackDataValid(&a->geom_delta_valid)) {
|
|
p += sprintf(p, ",%d", mm->altitude_geom - a->geom_delta);
|
|
} else {
|
|
p += sprintf(p, ",");
|
|
}
|
|
}
|
|
|
|
// Field 13 is the ground Speed (if we have it)
|
|
if (mm->gs_valid) {
|
|
p += sprintf(p, ",%.0f", mm->gs.selected);
|
|
} else {
|
|
p += sprintf(p, ",");
|
|
}
|
|
|
|
// Field 14 is the ground Heading (if we have it)
|
|
if (mm->heading_valid && mm->heading_type == HEADING_GROUND_TRACK) {
|
|
p += sprintf(p, ",%.0f", mm->heading);
|
|
} else {
|
|
p += sprintf(p, ",");
|
|
}
|
|
|
|
// Fields 15 and 16 are the Lat/Lon (if we have it)
|
|
if (mm->cpr_decoded) {
|
|
p += sprintf(p, ",%1.5f,%1.5f", mm->decoded_lat, mm->decoded_lon);
|
|
} else {
|
|
p += sprintf(p, ",,");
|
|
}
|
|
|
|
// Field 17 is the VerticalRate (if we have it)
|
|
if (Modes.use_gnss) {
|
|
if (mm->geom_rate_valid) {
|
|
p += sprintf(p, ",%dH", mm->geom_rate);
|
|
} else if (mm->baro_rate_valid) {
|
|
p += sprintf(p, ",%d", mm->baro_rate);
|
|
} else {
|
|
p += sprintf(p, ",");
|
|
}
|
|
} else {
|
|
if (mm->baro_rate_valid) {
|
|
p += sprintf(p, ",%d", mm->baro_rate);
|
|
} else if (mm->geom_rate_valid) {
|
|
p += sprintf(p, ",%d", mm->geom_rate);
|
|
} else {
|
|
p += sprintf(p, ",");
|
|
}
|
|
}
|
|
|
|
// Field 18 is the Squawk (if we have it)
|
|
if (mm->squawk_valid) {
|
|
p += sprintf(p, ",%04x", mm->squawk);
|
|
} else {
|
|
p += sprintf(p, ",");
|
|
}
|
|
|
|
// Field 19 is the Squawk Changing Alert flag (if we have it)
|
|
if (mm->alert_valid) {
|
|
if (mm->alert) {
|
|
p += sprintf(p, ",-1");
|
|
} else {
|
|
p += sprintf(p, ",0");
|
|
}
|
|
} else {
|
|
p += sprintf(p, ",");
|
|
}
|
|
|
|
// Field 20 is the Squawk Emergency flag (if we have it)
|
|
if (mm->squawk_valid) {
|
|
if ((mm->squawk == 0x7500) || (mm->squawk == 0x7600) || (mm->squawk == 0x7700)) {
|
|
p += sprintf(p, ",-1");
|
|
} else {
|
|
p += sprintf(p, ",0");
|
|
}
|
|
} else {
|
|
p += sprintf(p, ",");
|
|
}
|
|
|
|
// Field 21 is the Squawk Ident flag (if we have it)
|
|
if (mm->spi_valid) {
|
|
if (mm->spi) {
|
|
p += sprintf(p, ",-1");
|
|
} else {
|
|
p += sprintf(p, ",0");
|
|
}
|
|
} else {
|
|
p += sprintf(p, ",");
|
|
}
|
|
|
|
// Field 22 is the OnTheGround flag (if we have it)
|
|
switch (mm->airground) {
|
|
case AG_GROUND:
|
|
p += sprintf(p, ",-1");
|
|
break;
|
|
case AG_AIRBORNE:
|
|
p += sprintf(p, ",0");
|
|
break;
|
|
default:
|
|
p += sprintf(p, ",");
|
|
break;
|
|
}
|
|
|
|
p += sprintf(p, "\r\n");
|
|
|
|
completeWrite(&Modes.sbs_out, p);
|
|
}
|
|
|
|
static void send_sbs_heartbeat(struct net_service *service)
|
|
{
|
|
static char *heartbeat_message = "\r\n"; // is there a better one?
|
|
char *data;
|
|
int len = strlen(heartbeat_message);
|
|
|
|
if (!service->writer)
|
|
return;
|
|
|
|
data = prepareWrite(service->writer, len);
|
|
if (!data)
|
|
return;
|
|
|
|
memcpy(data, heartbeat_message, len);
|
|
completeWrite(service->writer, data + len);
|
|
}
|
|
|
|
//
|
|
//=========================================================================
|
|
//
|
|
void modesQueueOutput(struct modesMessage *mm, struct aircraft *a) {
|
|
int is_mlat = (mm->source == SOURCE_MLAT);
|
|
|
|
if (a && !is_mlat && mm->correctedbits < 2) {
|
|
// Don't ever forward 2-bit-corrected messages via SBS output.
|
|
// Don't ever forward mlat messages via SBS output.
|
|
modesSendSBSOutput(mm, a);
|
|
}
|
|
|
|
if (!is_mlat && (Modes.net_verbatim || mm->correctedbits < 2)) {
|
|
// Forward 2-bit-corrected messages via raw output only if --net-verbatim is set
|
|
// Don't ever forward mlat messages via raw output.
|
|
modesSendRawOutput(mm);
|
|
}
|
|
|
|
if ((!is_mlat || Modes.forward_mlat) && (Modes.net_verbatim || mm->correctedbits < 2)) {
|
|
// Forward 2-bit-corrected messages via beast output only if --net-verbatim is set
|
|
// Forward mlat messages via beast output only if --forward-mlat is set
|
|
modesSendBeastOutput(mm);
|
|
}
|
|
|
|
if (a && !is_mlat) {
|
|
writeFATSVEvent(mm, a);
|
|
}
|
|
}
|
|
|
|
// Decode a little-endian IEEE754 float (binary32)
|
|
float ieee754_binary32_le_to_float(uint8_t *data)
|
|
{
|
|
double sign = (data[3] & 0x80) ? -1.0 : 1.0;
|
|
int16_t raw_exponent = ((data[3] & 0x7f) << 1) | ((data[2] & 0x80) >> 7);
|
|
uint32_t raw_significand = ((data[2] & 0x7f) << 16) | (data[1] << 8) | data[0];
|
|
|
|
if (raw_exponent == 0) {
|
|
if (raw_significand == 0) {
|
|
/* -0 is treated like +0 */
|
|
return 0;
|
|
} else {
|
|
/* denormal */
|
|
return ldexp(sign * raw_significand, -126 - 23);
|
|
}
|
|
}
|
|
|
|
if (raw_exponent == 255) {
|
|
if (raw_significand == 0) {
|
|
/* +/-infinity */
|
|
return sign < 0 ? -INFINITY : INFINITY;
|
|
} else {
|
|
/* NaN */
|
|
#ifdef NAN
|
|
return NAN;
|
|
#else
|
|
return 0.0f;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/* normalized value */
|
|
return ldexp(sign * ((1 << 23) | raw_significand), raw_exponent - 127 - 23);
|
|
}
|
|
|
|
static void handle_radarcape_position(float lat, float lon, float alt)
|
|
{
|
|
if (!isfinite(lat) || lat < -90 || lat > 90 || !isfinite(lon) || lon < -180 || lon > 180 || !isfinite(alt))
|
|
return;
|
|
|
|
writeFATSVPositionUpdate(lat, lon, alt);
|
|
|
|
if (!(Modes.bUserFlags & MODES_USER_LATLON_VALID)) {
|
|
Modes.fUserLat = lat;
|
|
Modes.fUserLon = lon;
|
|
Modes.bUserFlags |= MODES_USER_LATLON_VALID;
|
|
receiverPositionChanged(lat, lon, alt);
|
|
}
|
|
}
|
|
|
|
// recompute global Mode A/C setting
|
|
static void autoset_modeac() {
|
|
struct client *c;
|
|
|
|
if (!Modes.mode_ac_auto)
|
|
return;
|
|
|
|
Modes.mode_ac = 0;
|
|
for (c = Modes.clients; c; c = c->next) {
|
|
if (c->modeac_requested) {
|
|
Modes.mode_ac = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Send some Beast settings commands to a client
|
|
void sendBeastSettings(struct client *c, const char *settings)
|
|
{
|
|
int len;
|
|
char *buf, *p;
|
|
|
|
len = strlen(settings) * 3;
|
|
buf = p = alloca(len);
|
|
|
|
while (*settings) {
|
|
*p++ = 0x1a;
|
|
*p++ = '1';
|
|
*p++ = *settings++;
|
|
}
|
|
|
|
anetWrite(c->fd, buf, len);
|
|
}
|
|
|
|
//
|
|
// Handle a Beast command message.
|
|
// Currently, we just look for the Mode A/C command message
|
|
// and ignore everything else.
|
|
//
|
|
static int handleBeastCommand(struct client *c, char *p) {
|
|
if (p[0] != '1') {
|
|
// huh?
|
|
return 0;
|
|
}
|
|
|
|
switch (p[1]) {
|
|
case 'j':
|
|
c->modeac_requested = 0;
|
|
break;
|
|
case 'J':
|
|
c->modeac_requested = 1;
|
|
break;
|
|
}
|
|
|
|
autoset_modeac();
|
|
return 0;
|
|
}
|
|
|
|
//
|
|
//=========================================================================
|
|
//
|
|
// This function decodes a Beast binary format message
|
|
//
|
|
// The message is passed to the higher level layers, so it feeds
|
|
// the selected screen output, the network output and so forth.
|
|
//
|
|
// If the message looks invalid it is silently discarded.
|
|
//
|
|
// The function always returns 0 (success) to the caller as there is no
|
|
// case where we want broken messages here to close the client connection.
|
|
//
|
|
static int decodeBinMessage(struct client *c, char *p) {
|
|
int msgLen = 0;
|
|
int j;
|
|
char ch;
|
|
unsigned char msg[MODES_LONG_MSG_BYTES + 7];
|
|
static struct modesMessage zeroMessage;
|
|
struct modesMessage mm;
|
|
MODES_NOTUSED(c);
|
|
memset(&mm, 0, sizeof(mm));
|
|
|
|
ch = *p++; /// Get the message type
|
|
|
|
if (ch == '1' && Modes.mode_ac) {
|
|
msgLen = MODEAC_MSG_BYTES;
|
|
} else if (ch == '2') {
|
|
msgLen = MODES_SHORT_MSG_BYTES;
|
|
} else if (ch == '3') {
|
|
msgLen = MODES_LONG_MSG_BYTES;
|
|
} else if (ch == '5') {
|
|
// Special case for Radarcape position messages.
|
|
float lat, lon, alt;
|
|
|
|
for (j = 0; j < 21; j++) { // and the data
|
|
msg[j] = ch = *p++;
|
|
if (0x1A == ch) {p++;}
|
|
}
|
|
|
|
lat = ieee754_binary32_le_to_float(msg + 4);
|
|
lon = ieee754_binary32_le_to_float(msg + 8);
|
|
alt = ieee754_binary32_le_to_float(msg + 12);
|
|
|
|
handle_radarcape_position(lat, lon, alt);
|
|
} else {
|
|
// Ignore this.
|
|
return 0;
|
|
}
|
|
|
|
if (msgLen) {
|
|
mm = zeroMessage;
|
|
|
|
// Mark messages received over the internet as remote so that we don't try to
|
|
// pass them off as being received by this instance when forwarding them
|
|
mm.remote = 1;
|
|
|
|
// Grab the timestamp (big endian format)
|
|
mm.timestampMsg = 0;
|
|
for (j = 0; j < 6; j++) {
|
|
ch = *p++;
|
|
mm.timestampMsg = mm.timestampMsg << 8 | (ch & 255);
|
|
if (0x1A == ch) {p++;}
|
|
}
|
|
|
|
// record reception time as the time we read it.
|
|
mm.sysTimestampMsg = mstime();
|
|
|
|
ch = *p++; // Grab the signal level
|
|
mm.signalLevel = ((unsigned char)ch / 255.0);
|
|
mm.signalLevel = mm.signalLevel * mm.signalLevel;
|
|
if (0x1A == ch) {p++;}
|
|
|
|
for (j = 0; j < msgLen; j++) { // and the data
|
|
msg[j] = ch = *p++;
|
|
if (0x1A == ch) {p++;}
|
|
}
|
|
|
|
if (msgLen == MODEAC_MSG_BYTES) { // ModeA or ModeC
|
|
Modes.stats_current.remote_received_modeac++;
|
|
decodeModeAMessage(&mm, ((msg[0] << 8) | msg[1]));
|
|
} else {
|
|
int result;
|
|
|
|
Modes.stats_current.remote_received_modes++;
|
|
result = decodeModesMessage(&mm, msg);
|
|
if (result < 0) {
|
|
if (result == -1)
|
|
Modes.stats_current.remote_rejected_unknown_icao++;
|
|
else
|
|
Modes.stats_current.remote_rejected_bad++;
|
|
return 0;
|
|
} else {
|
|
Modes.stats_current.remote_accepted[mm.correctedbits]++;
|
|
}
|
|
}
|
|
|
|
useModesMessage(&mm);
|
|
}
|
|
return (0);
|
|
}
|
|
//
|
|
//=========================================================================
|
|
//
|
|
// Turn an hex digit into its 4 bit decimal value.
|
|
// Returns -1 if the digit is not in the 0-F range.
|
|
//
|
|
static int hexDigitVal(int c) {
|
|
c = tolower(c);
|
|
if (c >= '0' && c <= '9') return c-'0';
|
|
else if (c >= 'a' && c <= 'f') return c-'a'+10;
|
|
else return -1;
|
|
}
|
|
//
|
|
//=========================================================================
|
|
//
|
|
// This function decodes a string representing message in raw hex format
|
|
// like: *8D4B969699155600E87406F5B69F; The string is null-terminated.
|
|
//
|
|
// The message is passed to the higher level layers, so it feeds
|
|
// the selected screen output, the network output and so forth.
|
|
//
|
|
// If the message looks invalid it is silently discarded.
|
|
//
|
|
// The function always returns 0 (success) to the caller as there is no
|
|
// case where we want broken messages here to close the client connection.
|
|
//
|
|
static int decodeHexMessage(struct client *c, char *hex) {
|
|
int l = strlen(hex), j;
|
|
unsigned char msg[MODES_LONG_MSG_BYTES];
|
|
struct modesMessage mm;
|
|
static struct modesMessage zeroMessage;
|
|
|
|
MODES_NOTUSED(c);
|
|
mm = zeroMessage;
|
|
|
|
// Mark messages received over the internet as remote so that we don't try to
|
|
// pass them off as being received by this instance when forwarding them
|
|
mm.remote = 1;
|
|
mm.signalLevel = 0;
|
|
|
|
// Remove spaces on the left and on the right
|
|
while(l && isspace(hex[l-1])) {
|
|
hex[l-1] = '\0'; l--;
|
|
}
|
|
while(isspace(*hex)) {
|
|
hex++; l--;
|
|
}
|
|
|
|
// Turn the message into binary.
|
|
// Accept *-AVR raw @-AVR/BEAST timeS+raw %-AVR timeS+raw (CRC good) <-BEAST timeS+sigL+raw
|
|
// and some AVR records that we can understand
|
|
if (hex[l-1] != ';') {return (0);} // not complete - abort
|
|
|
|
switch(hex[0]) {
|
|
case '<': {
|
|
mm.signalLevel = ((hexDigitVal(hex[13])<<4) | hexDigitVal(hex[14])) / 255.0;
|
|
mm.signalLevel = mm.signalLevel * mm.signalLevel;
|
|
hex += 15; l -= 16; // Skip <, timestamp and siglevel, and ;
|
|
break;}
|
|
|
|
case '@': // No CRC check
|
|
case '%': { // CRC is OK
|
|
hex += 13; l -= 14; // Skip @,%, and timestamp, and ;
|
|
break;}
|
|
|
|
case '*':
|
|
case ':': {
|
|
hex++; l-=2; // Skip * and ;
|
|
break;}
|
|
|
|
default: {
|
|
return (0); // We don't know what this is, so abort
|
|
break;}
|
|
}
|
|
|
|
if ( (l != (MODEAC_MSG_BYTES * 2))
|
|
&& (l != (MODES_SHORT_MSG_BYTES * 2))
|
|
&& (l != (MODES_LONG_MSG_BYTES * 2)) )
|
|
{return (0);} // Too short or long message... broken
|
|
|
|
if ( (0 == Modes.mode_ac)
|
|
&& (l == (MODEAC_MSG_BYTES * 2)) )
|
|
{return (0);} // Right length for ModeA/C, but not enabled
|
|
|
|
for (j = 0; j < l; j += 2) {
|
|
int high = hexDigitVal(hex[j]);
|
|
int low = hexDigitVal(hex[j+1]);
|
|
|
|
if (high == -1 || low == -1) return 0;
|
|
msg[j/2] = (high << 4) | low;
|
|
}
|
|
|
|
// record reception time as the time we read it.
|
|
mm.sysTimestampMsg = mstime();
|
|
|
|
if (l == (MODEAC_MSG_BYTES * 2)) { // ModeA or ModeC
|
|
Modes.stats_current.remote_received_modeac++;
|
|
decodeModeAMessage(&mm, ((msg[0] << 8) | msg[1]));
|
|
} else { // Assume ModeS
|
|
int result;
|
|
|
|
Modes.stats_current.remote_received_modes++;
|
|
result = decodeModesMessage(&mm, msg);
|
|
if (result < 0) {
|
|
if (result == -1)
|
|
Modes.stats_current.remote_rejected_unknown_icao++;
|
|
else
|
|
Modes.stats_current.remote_rejected_bad++;
|
|
return 0;
|
|
} else {
|
|
Modes.stats_current.remote_accepted[mm.correctedbits]++;
|
|
}
|
|
}
|
|
|
|
useModesMessage(&mm);
|
|
return (0);
|
|
}
|
|
//
|
|
//=========================================================================
|
|
//
|
|
// Return a description of planes in json. No metric conversion
|
|
//
|
|
|
|
// usual caveats about function-returning-pointer-to-static-buffer apply
|
|
static const char *jsonEscapeString(const char *str) {
|
|
static char buf[1024];
|
|
const char *in = str;
|
|
char *out = buf, *end = buf + sizeof(buf) - 10;
|
|
|
|
for (; *in && out < end; ++in) {
|
|
unsigned char ch = *in;
|
|
if (ch == '"' || ch == '\\') {
|
|
*out++ = '\\';
|
|
*out++ = ch;
|
|
} else if (ch < 32 || ch > 127) {
|
|
out += snprintf(out, end - out, "\\u%04x", ch);
|
|
} else {
|
|
*out++ = ch;
|
|
}
|
|
}
|
|
|
|
*out++ = 0;
|
|
return buf;
|
|
}
|
|
|
|
static char *append_flags(char *p, char *end, struct aircraft *a, datasource_t source)
|
|
{
|
|
p += snprintf(p, end-p, "[");
|
|
if (a->squawk_valid.source == source)
|
|
p += snprintf(p, end-p, "\"squawk\",");
|
|
if (a->callsign_valid.source == source)
|
|
p += snprintf(p, end-p, "\"callsign\",");
|
|
if (a->position_valid.source == source)
|
|
p += snprintf(p, end-p, "\"lat\",\"lon\",");
|
|
if (a->altitude_baro_valid.source == source)
|
|
p += snprintf(p, end-p, "\"altitude\",");
|
|
if (a->altitude_geom_valid.source == source)
|
|
p += snprintf(p, end-p, "\"alt_geom\",");
|
|
if (a->track_valid.source == source)
|
|
p += snprintf(p, end-p, "\"track\",");
|
|
if (a->mag_heading_valid.source == source)
|
|
p += snprintf(p, end-p, "\"mag_heading\",");
|
|
if (a->true_heading_valid.source == source)
|
|
p += snprintf(p, end-p, "\"true_heading\",");
|
|
if (a->gs_valid.source == source)
|
|
p += snprintf(p, end-p, "\"gs\",");
|
|
if (a->ias_valid.source == source)
|
|
p += snprintf(p, end-p, "\"ias\",");
|
|
if (a->tas_valid.source == source)
|
|
p += snprintf(p, end-p, "\"tas\",");
|
|
if (a->baro_rate_valid.source == source)
|
|
p += snprintf(p, end-p, "\"baro_rate\",");
|
|
if (a->geom_rate_valid.source == source)
|
|
p += snprintf(p, end-p, "\"geom_rate\",");
|
|
if (p[-1] != '[')
|
|
--p;
|
|
p += snprintf(p, end-p, "]");
|
|
return p;
|
|
}
|
|
|
|
static struct {
|
|
nav_modes_t flag;
|
|
const char *name;
|
|
} nav_modes_names[] = {
|
|
{ NAV_MODE_AUTOPILOT, "autopilot" },
|
|
{ NAV_MODE_VNAV, "vnav" },
|
|
{ NAV_MODE_ALT_HOLD, "althold" },
|
|
{ NAV_MODE_APPROACH, "approach" },
|
|
{ NAV_MODE_LNAV, "lnav" },
|
|
{ NAV_MODE_TCAS, "tcas" },
|
|
{ 0, NULL }
|
|
};
|
|
|
|
static char *append_nav_modes(char *p, char *end, nav_modes_t flags, const char *quote, const char *sep)
|
|
{
|
|
int first = 1;
|
|
for (int i = 0; nav_modes_names[i].name; ++i) {
|
|
if (!(flags & nav_modes_names[i].flag)) {
|
|
continue;
|
|
}
|
|
|
|
if (!first) {
|
|
p += snprintf(p, end-p, "%s", sep);
|
|
}
|
|
|
|
first = 0;
|
|
p += snprintf(p, end-p, "%s%s%s", quote, nav_modes_names[i].name, quote);
|
|
}
|
|
|
|
return p;
|
|
}
|
|
|
|
static const char *nav_modes_flags_string(nav_modes_t flags) {
|
|
static char buf[256];
|
|
buf[0] = 0;
|
|
append_nav_modes(buf, buf + sizeof(buf), flags, "", " ");
|
|
return buf;
|
|
}
|
|
|
|
static const char *addrtype_enum_string(addrtype_t type) {
|
|
switch (type) {
|
|
case ADDR_ADSB_ICAO:
|
|
return "adsb_icao";
|
|
case ADDR_ADSB_ICAO_NT:
|
|
return "adsb_icao_nt";
|
|
case ADDR_ADSR_ICAO:
|
|
return "adsr_icao";
|
|
case ADDR_TISB_ICAO:
|
|
return "tisb_icao";
|
|
case ADDR_ADSB_OTHER:
|
|
return "adsb_other";
|
|
case ADDR_ADSR_OTHER:
|
|
return "adsr_other";
|
|
case ADDR_TISB_OTHER:
|
|
return "tisb_other";
|
|
case ADDR_TISB_TRACKFILE:
|
|
return "tisb_trackfile";
|
|
default:
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
char *generateAircraftJson(const char *url_path, int *len) {
|
|
uint64_t now = mstime();
|
|
struct aircraft *a;
|
|
int buflen = 1024; // The initial buffer is incremented as needed
|
|
char *buf = (char *) malloc(buflen), *p = buf, *end = buf+buflen;
|
|
int first = 1;
|
|
|
|
MODES_NOTUSED(url_path);
|
|
|
|
p += snprintf(p, end-p,
|
|
"{ \"now\" : %.1f,\n"
|
|
" \"messages\" : %u,\n"
|
|
" \"aircraft\" : [",
|
|
now / 1000.0,
|
|
Modes.stats_current.messages_total + Modes.stats_alltime.messages_total);
|
|
|
|
for (a = Modes.aircrafts; a; a = a->next) {
|
|
if (a->messages < 2) { // basic filter for bad decodes
|
|
continue;
|
|
}
|
|
|
|
if (first)
|
|
first = 0;
|
|
else
|
|
*p++ = ',';
|
|
|
|
p += snprintf(p, end-p, "\n {\"hex\":\"%s%06x\"", (a->addr & MODES_NON_ICAO_ADDRESS) ? "~" : "", a->addr & 0xFFFFFF);
|
|
if (a->addrtype != ADDR_ADSB_ICAO)
|
|
p += snprintf(p, end-p, ",\"type\":\"%s\"", addrtype_enum_string(a->addrtype));
|
|
if (a->adsb_version >= 0)
|
|
p += snprintf(p, end-p, ",\"version\":%d", a->adsb_version);
|
|
if (trackDataValid(&a->squawk_valid))
|
|
p += snprintf(p, end-p, ",\"squawk\":\"%04x\"", a->squawk);
|
|
if (trackDataValid(&a->callsign_valid))
|
|
p += snprintf(p, end-p, ",\"flight\":\"%s\"", jsonEscapeString(a->callsign));
|
|
if (trackDataValid(&a->position_valid))
|
|
p += snprintf(p, end-p, ",\"lat\":%f,\"lon\":%f,\"nic\":%u,\"rc\":%u\"seen_pos\":%.1f", a->lat, a->lon, a->pos_nic, a->pos_rc, (now - a->position_valid.updated)/1000.0);
|
|
if (trackDataValid(&a->airground_valid) && a->airground_valid.source >= SOURCE_MODE_S_CHECKED && a->airground == AG_GROUND)
|
|
p += snprintf(p, end-p, ",\"altitude\":\"ground\"");
|
|
else {
|
|
if (trackDataValid(&a->altitude_baro_valid))
|
|
p += snprintf(p, end-p, ",\"altitude\":%d", a->altitude_baro);
|
|
if (trackDataValid(&a->altitude_geom_valid))
|
|
p += snprintf(p, end-p, ",\"alt_geom\":%d", a->altitude_geom);
|
|
}
|
|
if (trackDataValid(&a->baro_rate_valid))
|
|
p += snprintf(p, end-p, ",\"baro_rate\":%d", a->baro_rate);
|
|
if (trackDataValid(&a->geom_rate_valid))
|
|
p += snprintf(p, end-p, ",\"geom_rate\":%d", a->geom_rate);
|
|
if (trackDataValid(&a->track_valid))
|
|
p += snprintf(p, end-p, ",\"track\":%.1f", a->track);
|
|
if (trackDataValid(&a->track_rate_valid))
|
|
p += snprintf(p, end-p, ",\"track_rate\":%.2f", a->track_rate);
|
|
if (trackDataValid(&a->mag_heading_valid))
|
|
p += snprintf(p, end-p, ",\"mag_heading\":%.1f", a->mag_heading);
|
|
if (trackDataValid(&a->true_heading_valid))
|
|
p += snprintf(p, end-p, ",\"true_heading\":%.1f", a->true_heading);
|
|
if (trackDataValid(&a->gs_valid))
|
|
p += snprintf(p, end-p, ",\"gs\":%.1f", a->gs);
|
|
if (trackDataValid(&a->ias_valid))
|
|
p += snprintf(p, end-p, ",\"ias\":%u", a->ias);
|
|
if (trackDataValid(&a->tas_valid))
|
|
p += snprintf(p, end-p, ",\"tas\":%u", a->tas);
|
|
if (trackDataValid(&a->mach_valid))
|
|
p += snprintf(p, end-p, ",\"mach\":%.3f", a->mach);
|
|
if (trackDataValid(&a->roll_valid))
|
|
p += snprintf(p, end-p, ",\"roll\":%.1f", a->roll);
|
|
if (a->category != 0)
|
|
p += snprintf(p, end-p, ",\"category\":\"%02X\"", a->category);
|
|
if (trackDataValid(&a->nav_altitude_valid))
|
|
p += snprintf(p, end-p, ",\"nav_alt\":%d", a->nav_altitude);
|
|
if (trackDataValid(&a->nav_heading_valid))
|
|
p += snprintf(p, end-p, ",\"nav_heading\":%.1f", a->nav_heading);
|
|
if (trackDataValid(&a->nav_modes_valid)) {
|
|
p += snprintf(p, end-p, ",\"nav_modes\":[");
|
|
p = append_nav_modes(p, end, a->nav_modes, "\"", ",");
|
|
p += snprintf(p, end-p, "]");
|
|
}
|
|
if (trackDataValid(&a->nav_qnh_valid))
|
|
p += snprintf(p, end-p, ",\"nav_qnh\":%.1f", a->nav_qnh);
|
|
|
|
p += snprintf(p, end-p, ",\"mlat\":");
|
|
p = append_flags(p, end, a, SOURCE_MLAT);
|
|
p += snprintf(p, end-p, ",\"tisb\":");
|
|
p = append_flags(p, end, a, SOURCE_TISB);
|
|
|
|
p += snprintf(p, end-p, ",\"messages\":%ld,\"seen\":%.1f,\"rssi\":%.1f}",
|
|
a->messages, (now - a->seen)/1000.0,
|
|
10 * log10((a->signalLevel[0] + a->signalLevel[1] + a->signalLevel[2] + a->signalLevel[3] +
|
|
a->signalLevel[4] + a->signalLevel[5] + a->signalLevel[6] + a->signalLevel[7] + 1e-5) / 8));
|
|
|
|
// If we're getting near the end of the buffer, expand it.
|
|
if ((end - p) < 512) {
|
|
int used = p - buf;
|
|
buflen *= 2;
|
|
buf = (char *) realloc(buf, buflen);
|
|
p = buf+used;
|
|
end = buf + buflen;
|
|
}
|
|
}
|
|
|
|
p += snprintf(p, end-p, "\n ]\n}\n");
|
|
*len = p-buf;
|
|
return buf;
|
|
}
|
|
|
|
static char * appendStatsJson(char *p,
|
|
char *end,
|
|
struct stats *st,
|
|
const char *key)
|
|
{
|
|
int i;
|
|
|
|
p += snprintf(p, end-p,
|
|
"\"%s\":{\"start\":%.1f,\"end\":%.1f",
|
|
key,
|
|
st->start / 1000.0,
|
|
st->end / 1000.0);
|
|
|
|
if (!Modes.net_only) {
|
|
p += snprintf(p, end-p,
|
|
",\"local\":{\"samples_processed\":%llu"
|
|
",\"samples_dropped\":%llu"
|
|
",\"modeac\":%u"
|
|
",\"modes\":%u"
|
|
",\"bad\":%u"
|
|
",\"unknown_icao\":%u",
|
|
(unsigned long long)st->samples_processed,
|
|
(unsigned long long)st->samples_dropped,
|
|
st->demod_modeac,
|
|
st->demod_preambles,
|
|
st->demod_rejected_bad,
|
|
st->demod_rejected_unknown_icao);
|
|
|
|
for (i=0; i <= Modes.nfix_crc; ++i) {
|
|
if (i == 0) p += snprintf(p, end-p, ",\"accepted\":[%u", st->demod_accepted[i]);
|
|
else p += snprintf(p, end-p, ",%u", st->demod_accepted[i]);
|
|
}
|
|
|
|
p += snprintf(p, end-p, "]");
|
|
|
|
if (st->signal_power_sum > 0 && st->signal_power_count > 0)
|
|
p += snprintf(p, end-p,",\"signal\":%.1f", 10 * log10(st->signal_power_sum / st->signal_power_count));
|
|
if (st->noise_power_sum > 0 && st->noise_power_count > 0)
|
|
p += snprintf(p, end-p,",\"noise\":%.1f", 10 * log10(st->noise_power_sum / st->noise_power_count));
|
|
if (st->peak_signal_power > 0)
|
|
p += snprintf(p, end-p,",\"peak_signal\":%.1f", 10 * log10(st->peak_signal_power));
|
|
|
|
p += snprintf(p, end-p,",\"strong_signals\":%d}", st->strong_signal_count);
|
|
}
|
|
|
|
if (Modes.net) {
|
|
p += snprintf(p, end-p,
|
|
",\"remote\":{\"modeac\":%u"
|
|
",\"modes\":%u"
|
|
",\"bad\":%u"
|
|
",\"unknown_icao\":%u",
|
|
st->remote_received_modeac,
|
|
st->remote_received_modes,
|
|
st->remote_rejected_bad,
|
|
st->remote_rejected_unknown_icao);
|
|
|
|
for (i=0; i <= Modes.nfix_crc; ++i) {
|
|
if (i == 0) p += snprintf(p, end-p, ",\"accepted\":[%u", st->remote_accepted[i]);
|
|
else p += snprintf(p, end-p, ",%u", st->remote_accepted[i]);
|
|
}
|
|
|
|
p += snprintf(p, end-p, "]}");
|
|
}
|
|
|
|
{
|
|
uint64_t demod_cpu_millis = (uint64_t)st->demod_cpu.tv_sec*1000UL + st->demod_cpu.tv_nsec/1000000UL;
|
|
uint64_t reader_cpu_millis = (uint64_t)st->reader_cpu.tv_sec*1000UL + st->reader_cpu.tv_nsec/1000000UL;
|
|
uint64_t background_cpu_millis = (uint64_t)st->background_cpu.tv_sec*1000UL + st->background_cpu.tv_nsec/1000000UL;
|
|
|
|
p += snprintf(p, end-p,
|
|
",\"cpr\":{\"surface\":%u"
|
|
",\"airborne\":%u"
|
|
",\"global_ok\":%u"
|
|
",\"global_bad\":%u"
|
|
",\"global_range\":%u"
|
|
",\"global_speed\":%u"
|
|
",\"global_skipped\":%u"
|
|
",\"local_ok\":%u"
|
|
",\"local_aircraft_relative\":%u"
|
|
",\"local_receiver_relative\":%u"
|
|
",\"local_skipped\":%u"
|
|
",\"local_range\":%u"
|
|
",\"local_speed\":%u"
|
|
",\"filtered\":%u}"
|
|
",\"altitude_suppressed\":%u"
|
|
",\"cpu\":{\"demod\":%llu,\"reader\":%llu,\"background\":%llu}"
|
|
",\"tracks\":{\"all\":%u"
|
|
",\"single_message\":%u}"
|
|
",\"messages\":%u}",
|
|
st->cpr_surface,
|
|
st->cpr_airborne,
|
|
st->cpr_global_ok,
|
|
st->cpr_global_bad,
|
|
st->cpr_global_range_checks,
|
|
st->cpr_global_speed_checks,
|
|
st->cpr_global_skipped,
|
|
st->cpr_local_ok,
|
|
st->cpr_local_aircraft_relative,
|
|
st->cpr_local_receiver_relative,
|
|
st->cpr_local_skipped,
|
|
st->cpr_local_range_checks,
|
|
st->cpr_local_speed_checks,
|
|
st->cpr_filtered,
|
|
st->suppressed_altitude_messages,
|
|
(unsigned long long)demod_cpu_millis,
|
|
(unsigned long long)reader_cpu_millis,
|
|
(unsigned long long)background_cpu_millis,
|
|
st->unique_aircraft,
|
|
st->single_message_aircraft,
|
|
st->messages_total);
|
|
}
|
|
|
|
return p;
|
|
}
|
|
|
|
char *generateStatsJson(const char *url_path, int *len) {
|
|
struct stats add;
|
|
char *buf = (char *) malloc(4096), *p = buf, *end = buf + 4096;
|
|
|
|
MODES_NOTUSED(url_path);
|
|
|
|
p += snprintf(p, end-p, "{\n");
|
|
p = appendStatsJson(p, end, &Modes.stats_current, "latest");
|
|
p += snprintf(p, end-p, ",\n");
|
|
|
|
p = appendStatsJson(p, end, &Modes.stats_1min[Modes.stats_latest_1min], "last1min");
|
|
p += snprintf(p, end-p, ",\n");
|
|
|
|
p = appendStatsJson(p, end, &Modes.stats_5min, "last5min");
|
|
p += snprintf(p, end-p, ",\n");
|
|
|
|
p = appendStatsJson(p, end, &Modes.stats_15min, "last15min");
|
|
p += snprintf(p, end-p, ",\n");
|
|
|
|
add_stats(&Modes.stats_alltime, &Modes.stats_current, &add);
|
|
p = appendStatsJson(p, end, &add, "total");
|
|
p += snprintf(p, end-p, "\n}\n");
|
|
|
|
assert(p <= end);
|
|
|
|
*len = p-buf;
|
|
return buf;
|
|
}
|
|
|
|
//
|
|
// Return a description of the receiver in json.
|
|
//
|
|
char *generateReceiverJson(const char *url_path, int *len)
|
|
{
|
|
char *buf = (char *) malloc(1024), *p = buf;
|
|
int history_size;
|
|
|
|
MODES_NOTUSED(url_path);
|
|
|
|
// work out number of valid history entries
|
|
if (Modes.json_aircraft_history[HISTORY_SIZE-1].content == NULL)
|
|
history_size = Modes.json_aircraft_history_next;
|
|
else
|
|
history_size = HISTORY_SIZE;
|
|
|
|
p += sprintf(p, "{ " \
|
|
"\"version\" : \"%s\", "
|
|
"\"refresh\" : %.0f, "
|
|
"\"history\" : %d",
|
|
MODES_DUMP1090_VERSION, 1.0*Modes.json_interval, history_size);
|
|
|
|
if (Modes.json_location_accuracy && (Modes.fUserLat != 0.0 || Modes.fUserLon != 0.0)) {
|
|
if (Modes.json_location_accuracy == 1) {
|
|
p += sprintf(p, ", " \
|
|
"\"lat\" : %.2f, "
|
|
"\"lon\" : %.2f",
|
|
Modes.fUserLat, Modes.fUserLon); // round to 2dp - about 0.5-1km accuracy - for privacy reasons
|
|
} else {
|
|
p += sprintf(p, ", " \
|
|
"\"lat\" : %.6f, "
|
|
"\"lon\" : %.6f",
|
|
Modes.fUserLat, Modes.fUserLon); // exact location
|
|
}
|
|
}
|
|
|
|
p += sprintf(p, " }\n");
|
|
|
|
*len = (p - buf);
|
|
return buf;
|
|
}
|
|
|
|
char *generateHistoryJson(const char *url_path, int *len)
|
|
{
|
|
int history_index = -1;
|
|
|
|
if (sscanf(url_path, "/data/history_%d.json", &history_index) != 1)
|
|
return NULL;
|
|
|
|
if (history_index < 0 || history_index >= HISTORY_SIZE)
|
|
return NULL;
|
|
|
|
if (!Modes.json_aircraft_history[history_index].content)
|
|
return NULL;
|
|
|
|
*len = Modes.json_aircraft_history[history_index].clen;
|
|
return strdup(Modes.json_aircraft_history[history_index].content);
|
|
}
|
|
|
|
// Write JSON to file
|
|
void writeJsonToFile(const char *file, char * (*generator) (const char *,int*))
|
|
{
|
|
#ifndef _WIN32
|
|
char pathbuf[PATH_MAX];
|
|
char tmppath[PATH_MAX];
|
|
int fd;
|
|
int len = 0;
|
|
mode_t mask;
|
|
char *content;
|
|
|
|
if (!Modes.json_dir)
|
|
return;
|
|
|
|
snprintf(tmppath, PATH_MAX, "%s/%s.XXXXXX", Modes.json_dir, file);
|
|
tmppath[PATH_MAX-1] = 0;
|
|
fd = mkstemp(tmppath);
|
|
if (fd < 0)
|
|
return;
|
|
|
|
mask = umask(0);
|
|
umask(mask);
|
|
fchmod(fd, 0644 & ~mask);
|
|
|
|
snprintf(pathbuf, PATH_MAX, "/data/%s", file);
|
|
pathbuf[PATH_MAX-1] = 0;
|
|
content = generator(pathbuf, &len);
|
|
|
|
if (write(fd, content, len) != len)
|
|
goto error_1;
|
|
|
|
if (close(fd) < 0)
|
|
goto error_2;
|
|
|
|
snprintf(pathbuf, PATH_MAX, "%s/%s", Modes.json_dir, file);
|
|
pathbuf[PATH_MAX-1] = 0;
|
|
rename(tmppath, pathbuf);
|
|
free(content);
|
|
return;
|
|
|
|
error_1:
|
|
close(fd);
|
|
error_2:
|
|
unlink(tmppath);
|
|
free(content);
|
|
return;
|
|
#endif
|
|
}
|
|
|
|
|
|
//
|
|
//=========================================================================
|
|
//
|
|
// This function polls the clients using read() in order to receive new
|
|
// messages from the net.
|
|
//
|
|
// The message is supposed to be separated from the next message by the
|
|
// separator 'sep', which is a null-terminated C string.
|
|
//
|
|
// Every full message received is decoded and passed to the higher layers
|
|
// calling the function's 'handler'.
|
|
//
|
|
// The handler returns 0 on success, or 1 to signal this function we should
|
|
// close the connection with the client in case of non-recoverable errors.
|
|
//
|
|
static void modesReadFromClient(struct client *c) {
|
|
int left;
|
|
int nread;
|
|
int bContinue = 1;
|
|
|
|
while (bContinue) {
|
|
left = MODES_CLIENT_BUF_SIZE - c->buflen - 1; // leave 1 extra byte for NUL termination in the ASCII case
|
|
|
|
// If our buffer is full discard it, this is some badly formatted shit
|
|
if (left <= 0) {
|
|
c->buflen = 0;
|
|
left = MODES_CLIENT_BUF_SIZE;
|
|
// If there is garbage, read more to discard it ASAP
|
|
}
|
|
#ifndef _WIN32
|
|
nread = read(c->fd, c->buf+c->buflen, left);
|
|
#else
|
|
nread = recv(c->fd, c->buf+c->buflen, left, 0);
|
|
if (nread < 0) {errno = WSAGetLastError();}
|
|
#endif
|
|
|
|
// If we didn't get all the data we asked for, then return once we've processed what we did get.
|
|
if (nread != left) {
|
|
bContinue = 0;
|
|
}
|
|
|
|
if (nread == 0) { // End of file
|
|
modesCloseClient(c);
|
|
return;
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
if (nread < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) // No data available (not really an error)
|
|
#else
|
|
if (nread < 0 && errno == EWOULDBLOCK) // No data available (not really an error)
|
|
#endif
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (nread < 0) { // Other errors
|
|
modesCloseClient(c);
|
|
return;
|
|
}
|
|
|
|
c->buflen += nread;
|
|
|
|
char *som = c->buf; // first byte of next message
|
|
char *eod = som + c->buflen; // one byte past end of data
|
|
char *p;
|
|
|
|
switch (c->service->read_mode) {
|
|
case READ_MODE_IGNORE:
|
|
// drop the bytes on the floor
|
|
som = eod;
|
|
break;
|
|
|
|
case READ_MODE_BEAST:
|
|
// This is the Beast Binary scanning case.
|
|
// If there is a complete message still in the buffer, there must be the separator 'sep'
|
|
// in the buffer, note that we full-scan the buffer at every read for simplicity.
|
|
|
|
while (som < eod && ((p = memchr(som, (char) 0x1a, eod - som)) != NULL)) { // The first byte of buffer 'should' be 0x1a
|
|
som = p; // consume garbage up to the 0x1a
|
|
++p; // skip 0x1a
|
|
|
|
if (p >= eod) {
|
|
// Incomplete message in buffer, retry later
|
|
break;
|
|
}
|
|
|
|
char *eom; // one byte past end of message
|
|
if (*p == '1') {
|
|
eom = p + MODEAC_MSG_BYTES + 8; // point past remainder of message
|
|
} else if (*p == '2') {
|
|
eom = p + MODES_SHORT_MSG_BYTES + 8;
|
|
} else if (*p == '3') {
|
|
eom = p + MODES_LONG_MSG_BYTES + 8;
|
|
} else if (*p == '4') {
|
|
eom = p + MODES_LONG_MSG_BYTES + 8;
|
|
} else if (*p == '5') {
|
|
eom = p + MODES_LONG_MSG_BYTES + 8;
|
|
} else {
|
|
// Not a valid beast message, skip 0x1a and try again
|
|
++som;
|
|
continue;
|
|
}
|
|
|
|
// we need to be careful of double escape characters in the message body
|
|
for (p = som + 1; p < eod && p < eom; p++) {
|
|
if (0x1A == *p) {
|
|
p++;
|
|
eom++;
|
|
}
|
|
}
|
|
|
|
if (eom > eod) { // Incomplete message in buffer, retry later
|
|
break;
|
|
}
|
|
|
|
// Have a 0x1a followed by 1/2/3/4/5 - pass message to handler.
|
|
if (c->service->read_handler(c, som + 1)) {
|
|
modesCloseClient(c);
|
|
return;
|
|
}
|
|
|
|
// advance to next message
|
|
som = eom;
|
|
}
|
|
break;
|
|
|
|
case READ_MODE_BEAST_COMMAND:
|
|
while (som < eod && ((p = memchr(som, (char) 0x1a, eod - som)) != NULL)) { // The first byte of buffer 'should' be 0x1a
|
|
char *eom; // one byte past end of message
|
|
|
|
som = p; // consume garbage up to the 0x1a
|
|
++p; // skip 0x1a
|
|
|
|
if (p >= eod) {
|
|
// Incomplete message in buffer, retry later
|
|
break;
|
|
}
|
|
|
|
if (*p == '1') {
|
|
eom = p + 2;
|
|
} else {
|
|
// Not a valid beast command, skip 0x1a and try again
|
|
++som;
|
|
continue;
|
|
}
|
|
|
|
// we need to be careful of double escape characters in the message body
|
|
for (p = som + 1; p < eod && p < eom; p++) {
|
|
if (0x1A == *p) {
|
|
p++;
|
|
eom++;
|
|
}
|
|
}
|
|
|
|
if (eom > eod) { // Incomplete message in buffer, retry later
|
|
break;
|
|
}
|
|
|
|
// Have a 0x1a followed by 1 - pass message to handler.
|
|
if (c->service->read_handler(c, som + 1)) {
|
|
modesCloseClient(c);
|
|
return;
|
|
}
|
|
|
|
// advance to next message
|
|
som = eom;
|
|
}
|
|
break;
|
|
|
|
case READ_MODE_ASCII:
|
|
//
|
|
// This is the ASCII scanning case, AVR RAW or HTTP at present
|
|
// If there is a complete message still in the buffer, there must be the separator 'sep'
|
|
// in the buffer, note that we full-scan the buffer at every read for simplicity.
|
|
|
|
// Always NUL-terminate so we are free to use strstr()
|
|
// nb: we never fill the last byte of the buffer with read data (see above) so this is safe
|
|
*eod = '\0';
|
|
|
|
while (som < eod && (p = strstr(som, c->service->read_sep)) != NULL) { // end of first message if found
|
|
*p = '\0'; // The handler expects null terminated strings
|
|
if (c->service->read_handler(c, som)) { // Pass message to handler.
|
|
modesCloseClient(c); // Handler returns 1 on error to signal we .
|
|
return; // should close the client connection
|
|
}
|
|
som = p + strlen(c->service->read_sep); // Move to start of next message
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if (som > c->buf) { // We processed something - so
|
|
c->buflen = eod - som; // Update the unprocessed buffer length
|
|
memmove(c->buf, som, c->buflen); // Move what's remaining to the start of the buffer
|
|
} else { // If no message was decoded process the next client
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
__attribute__ ((format (printf,3,0))) static char *safe_vsnprintf(char *p, char *end, const char *format, va_list ap)
|
|
{
|
|
p += vsnprintf(p < end ? p : NULL, p < end ? (size_t)(end - p) : 0, format, ap);
|
|
return p;
|
|
}
|
|
|
|
__attribute__ ((format (printf,3,4))) static char *safe_snprintf(char *p, char *end, const char *format, ...)
|
|
{
|
|
va_list ap;
|
|
va_start(ap, format);
|
|
p += vsnprintf(p < end ? p : NULL, p < end ? (size_t)(end - p) : 0, format, ap);
|
|
va_end(ap);
|
|
return p;
|
|
}
|
|
|
|
|
|
__attribute__ ((format (printf,4,5))) static char *appendFATSV(char *p, char *end, const char *field, const char *format, ...)
|
|
{
|
|
va_list ap;
|
|
va_start(ap, format);
|
|
|
|
p = safe_snprintf(p, end, "%s\t", field);
|
|
p = safe_vsnprintf(p, end, format, ap);
|
|
p = safe_snprintf(p, end, "\t");
|
|
|
|
va_end(ap);
|
|
return p;
|
|
}
|
|
|
|
#define TSV_MAX_PACKET_SIZE 400
|
|
|
|
static void writeFATSVPositionUpdate(float lat, float lon, float alt)
|
|
{
|
|
static float last_lat, last_lon, last_alt;
|
|
|
|
if (lat == last_lat && lon == last_lon && alt == last_alt)
|
|
return;
|
|
|
|
last_lat = lat;
|
|
last_lon = lon;
|
|
last_alt = alt;
|
|
|
|
char *p = prepareWrite(&Modes.fatsv_out, TSV_MAX_PACKET_SIZE);
|
|
if (!p)
|
|
return;
|
|
|
|
char *end = p + TSV_MAX_PACKET_SIZE;
|
|
|
|
p = appendFATSV(p, end, "clock", "%" PRIu64, messageNow() / 1000);
|
|
p = appendFATSV(p, end, "type", "%s", "location_update");
|
|
p = appendFATSV(p, end, "lat", "%.5f", lat);
|
|
p = appendFATSV(p, end, "lon", "%.5f", lon);
|
|
p = appendFATSV(p, end, "alt", "%.0f", alt);
|
|
p = appendFATSV(p, end, "altref", "%s", "egm96_meters");
|
|
--p; // remove last tab
|
|
p = safe_snprintf(p, end, "\n");
|
|
|
|
if (p <= end)
|
|
completeWrite(&Modes.fatsv_out, p);
|
|
else
|
|
fprintf(stderr, "fatsv: output too large (max %d, overran by %d)\n", TSV_MAX_PACKET_SIZE, (int) (p - end));
|
|
}
|
|
|
|
static void writeFATSVEventMessage(struct modesMessage *mm, const char *datafield, unsigned char *data, size_t len)
|
|
{
|
|
char *p = prepareWrite(&Modes.fatsv_out, TSV_MAX_PACKET_SIZE);
|
|
if (!p)
|
|
return;
|
|
|
|
char *end = p + TSV_MAX_PACKET_SIZE;
|
|
|
|
p = appendFATSV(p, end, "clock", "%" PRIu64, messageNow() / 1000);
|
|
p = appendFATSV(p, end, (mm->addr & MODES_NON_ICAO_ADDRESS) ? "otherid" : "hexid", "%06X", mm->addr & 0xFFFFFF);
|
|
if (mm->addrtype != ADDR_ADSB_ICAO) {
|
|
p = appendFATSV(p, end, "addrtype", "%s", addrtype_enum_string(mm->addrtype));
|
|
}
|
|
|
|
p = safe_snprintf(p, end, "%s\t", datafield);
|
|
for (size_t i = 0; i < len; ++i) {
|
|
p = safe_snprintf(p, end, "%02X", data[i]);
|
|
}
|
|
p = safe_snprintf(p, end, "\n");
|
|
|
|
if (p <= end)
|
|
completeWrite(&Modes.fatsv_out, p);
|
|
else
|
|
fprintf(stderr, "fatsv: output too large (max %d, overran by %d)\n", TSV_MAX_PACKET_SIZE, (int) (p - end));
|
|
# undef bufsize
|
|
}
|
|
|
|
static void writeFATSVEvent(struct modesMessage *mm, struct aircraft *a)
|
|
{
|
|
// Write event records for a couple of message types.
|
|
|
|
if (!Modes.fatsv_out.service || !Modes.fatsv_out.service->connections) {
|
|
return; // not enabled or no active connections
|
|
}
|
|
|
|
if (a->messages < 2) // basic filter for bad decodes
|
|
return;
|
|
|
|
switch (mm->msgtype) {
|
|
case 20:
|
|
case 21:
|
|
// DF 20/21: Comm-B: emit if they've changed since we last sent them
|
|
switch (mm->commb_format) {
|
|
case COMMB_DATALINK_CAPS:
|
|
// BDS 1,0: data link capability report
|
|
if (memcmp(mm->MB, a->fatsv_emitted_bds_10, 7) != 0) {
|
|
memcpy(a->fatsv_emitted_bds_10, mm->MB, 7);
|
|
writeFATSVEventMessage(mm, "datalink_caps", mm->MB, 7);
|
|
}
|
|
break;
|
|
|
|
case COMMB_ACAS_RA:
|
|
// BDS 3,0: ACAS RA report
|
|
if (memcmp(mm->MB, a->fatsv_emitted_bds_30, 7) != 0) {
|
|
memcpy(a->fatsv_emitted_bds_30, mm->MB, 7);
|
|
writeFATSVEventMessage(mm, "commb_acas_ra", mm->MB, 7);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// nothing
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 17:
|
|
case 18:
|
|
// DF 17/18: extended squitter
|
|
if (mm->metype == 28 && mm->mesub == 2 && memcmp(mm->ME, &a->fatsv_emitted_es_acas_ra, 7) != 0) {
|
|
// type 28 subtype 2: ACAS RA report
|
|
// first byte has the type/subtype, remaining bytes match the BDS 3,0 format
|
|
memcpy(a->fatsv_emitted_es_acas_ra, mm->ME, 7);
|
|
writeFATSVEventMessage(mm, "es_acas_ra", mm->ME, 7);
|
|
} else if (mm->metype == 31 && (mm->mesub == 0 || mm->mesub == 1) && memcmp(mm->ME, a->fatsv_emitted_es_status, 7) != 0) {
|
|
// aircraft operational status
|
|
memcpy(a->fatsv_emitted_es_status, mm->ME, 7);
|
|
writeFATSVEventMessage(mm, "es_op_status", mm->ME, 7);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
static inline unsigned unsigned_difference(unsigned v1, unsigned v2)
|
|
{
|
|
return (v1 > v2) ? (v1 - v2) : (v2 - v1);
|
|
}
|
|
|
|
static inline float heading_difference(float h1, float h2)
|
|
{
|
|
float d = fabs(h1 - h2);
|
|
return (d < 180) ? d : (360 - d);
|
|
}
|
|
|
|
__attribute__ ((format (printf,6,7))) static char *appendFATSVMeta(char *p, char *end, const char *field, struct aircraft *a, const data_validity *source, const char *format, ...)
|
|
{
|
|
const char *sourcetype;
|
|
switch (source->source) {
|
|
case SOURCE_MODE_S:
|
|
sourcetype = "U";
|
|
break;
|
|
case SOURCE_MODE_S_CHECKED:
|
|
sourcetype = "S";
|
|
break;
|
|
case SOURCE_TISB:
|
|
sourcetype = "T";
|
|
break;
|
|
case SOURCE_ADSB:
|
|
sourcetype = "A";
|
|
break;
|
|
default:
|
|
// don't want to forward data sourced from these
|
|
return p;
|
|
}
|
|
|
|
if (!trackDataValid(source)) {
|
|
// expired data
|
|
return p;
|
|
}
|
|
|
|
if (source->updated > messageNow()) {
|
|
// data in the future
|
|
return p;
|
|
}
|
|
|
|
if (source->updated < a->fatsv_last_emitted) {
|
|
// not updated since last time
|
|
return p;
|
|
}
|
|
|
|
uint64_t age = (messageNow() - source->updated) / 1000;
|
|
if (age > 255) {
|
|
// too old
|
|
return p;
|
|
}
|
|
|
|
p = safe_snprintf(p, end, "%s\t", field);
|
|
|
|
va_list ap;
|
|
va_start(ap, format);
|
|
p = safe_vsnprintf(p, end, format, ap);
|
|
va_end(ap);
|
|
|
|
p = safe_snprintf(p, end, " %" PRIu64 " %s\t", age, sourcetype);
|
|
return p;
|
|
}
|
|
|
|
static const char *airground_enum_string(airground_t ag)
|
|
{
|
|
switch (ag) {
|
|
case AG_AIRBORNE:
|
|
return "A+";
|
|
case AG_GROUND:
|
|
return "G+";
|
|
default:
|
|
return "?";
|
|
}
|
|
}
|
|
|
|
static const char *emergency_enum_string(emergency_t emergency)
|
|
{
|
|
switch (emergency) {
|
|
case EMERGENCY_NONE: return "none";
|
|
case EMERGENCY_GENERAL: return "general";
|
|
case EMERGENCY_LIFEGUARD: return "lifeguard";
|
|
case EMERGENCY_MINFUEL: return "minfuel";
|
|
case EMERGENCY_NORDO: return "nordo";
|
|
case EMERGENCY_UNLAWFUL: return "unlawful";
|
|
case EMERGENCY_DOWNED: return "downed";
|
|
default: return "reserved";
|
|
}
|
|
}
|
|
|
|
static const char *sil_type_enum_string(sil_type_t type)
|
|
{
|
|
switch (type) {
|
|
case SIL_UNKNOWN: return "unknown";
|
|
case SIL_PER_HOUR: return "perhour";
|
|
case SIL_PER_SAMPLE: return "persample";
|
|
default: return "invalid";
|
|
}
|
|
}
|
|
|
|
static void writeFATSVBanner()
|
|
{
|
|
char *p = prepareWrite(&Modes.fatsv_out, TSV_MAX_PACKET_SIZE);
|
|
if (!p)
|
|
return;
|
|
char *end = p + TSV_MAX_PACKET_SIZE;
|
|
|
|
p = appendFATSV(p, end, "faup1090_format_version", "%s", "2");
|
|
|
|
--p; // remove last tab
|
|
p = safe_snprintf(p, end, "\n");
|
|
|
|
if (p <= end)
|
|
completeWrite(&Modes.fatsv_out, p);
|
|
else
|
|
fprintf(stderr, "fatsv: output too large (max %d, overran by %d)\n", TSV_MAX_PACKET_SIZE, (int) (p - end));
|
|
}
|
|
|
|
static void writeFATSV()
|
|
{
|
|
struct aircraft *a;
|
|
static uint64_t next_update;
|
|
static int first_run = 1;
|
|
|
|
if (!Modes.fatsv_out.service || !Modes.fatsv_out.service->connections) {
|
|
return; // not enabled or no active connections
|
|
}
|
|
|
|
if (first_run) {
|
|
writeFATSVBanner();
|
|
first_run = 0;
|
|
}
|
|
|
|
uint64_t now = mstime();
|
|
if (now < next_update) {
|
|
return;
|
|
}
|
|
|
|
// scan once a second at most
|
|
next_update = now + 1000;
|
|
|
|
// Pretend we are "processing a message" so the validity checks work as expected
|
|
_messageNow = now;
|
|
for (a = Modes.aircrafts; a; a = a->next) {
|
|
if (a->messages < 2) // basic filter for bad decodes
|
|
continue;
|
|
|
|
// don't emit if it hasn't updated since last time
|
|
if (a->seen < a->fatsv_last_emitted) {
|
|
continue;
|
|
}
|
|
|
|
// some special cases:
|
|
int altValid = trackDataValid(&a->altitude_baro_valid);
|
|
int airgroundValid = trackDataValid(&a->airground_valid) && a->airground_valid.source >= SOURCE_MODE_S_CHECKED; // for non-ADS-B transponders, only trust DF11 CA field
|
|
int gsValid = trackDataValid(&a->gs_valid);
|
|
int squawkValid = trackDataValid(&a->squawk_valid);
|
|
int callsignValid = trackDataValid(&a->callsign_valid) && strcmp(a->callsign, " ") != 0;
|
|
int positionValid = trackDataValid(&a->position_valid);
|
|
|
|
// If we are definitely on the ground, suppress any unreliable altitude info.
|
|
// When on the ground, ADS-B transponders don't emit an ADS-B message that includes
|
|
// altitude, so a corrupted Mode S altitude response from some other in-the-air AC
|
|
// might be taken as the "best available altitude" and produce e.g. "airGround G+ alt 31000".
|
|
if (airgroundValid && a->airground == AG_GROUND && a->altitude_baro_valid.source < SOURCE_MODE_S_CHECKED)
|
|
altValid = 0;
|
|
|
|
// if it hasn't changed altitude, heading, or speed much,
|
|
// don't update so often
|
|
int changed =
|
|
(altValid && abs(a->altitude_baro - a->fatsv_emitted_altitude_baro) >= 50) ||
|
|
(trackDataValid(&a->altitude_geom_valid) && abs(a->altitude_geom - a->fatsv_emitted_altitude_geom) >= 50) ||
|
|
(trackDataValid(&a->baro_rate_valid) && abs(a->baro_rate - a->fatsv_emitted_baro_rate) > 500) ||
|
|
(trackDataValid(&a->geom_rate_valid) && abs(a->geom_rate - a->fatsv_emitted_geom_rate) > 500) ||
|
|
(trackDataValid(&a->track_valid) && heading_difference(a->track, a->fatsv_emitted_track) >= 2) ||
|
|
(trackDataValid(&a->track_rate_valid) && fabs(a->track_rate - a->fatsv_emitted_track_rate) >= 0.5) ||
|
|
(trackDataValid(&a->roll_valid) && fabs(a->roll - a->fatsv_emitted_roll) >= 5.0) ||
|
|
(trackDataValid(&a->mag_heading_valid) && heading_difference(a->mag_heading, a->fatsv_emitted_mag_heading) >= 2) ||
|
|
(trackDataValid(&a->true_heading_valid) && heading_difference(a->true_heading, a->fatsv_emitted_true_heading) >= 2) ||
|
|
(gsValid && fabs(a->gs - a->fatsv_emitted_gs) >= 25) ||
|
|
(trackDataValid(&a->ias_valid) && unsigned_difference(a->ias, a->fatsv_emitted_ias) >= 25) ||
|
|
(trackDataValid(&a->tas_valid) && unsigned_difference(a->tas, a->fatsv_emitted_tas) >= 25) ||
|
|
(trackDataValid(&a->mach_valid) && fabs(a->mach - a->fatsv_emitted_mach) >= 0.02);
|
|
|
|
int immediate =
|
|
(trackDataValid(&a->nav_altitude_valid) && unsigned_difference(a->nav_altitude, a->fatsv_emitted_nav_altitude) > 50) ||
|
|
(trackDataValid(&a->nav_heading_valid) && heading_difference(a->nav_heading, a->fatsv_emitted_nav_heading) > 2) ||
|
|
(trackDataValid(&a->nav_modes_valid) && a->nav_modes != a->fatsv_emitted_nav_modes) ||
|
|
(trackDataValid(&a->nav_qnh_valid) && fabs(a->nav_qnh - a->fatsv_emitted_nav_qnh) > 0.8) || // 0.8 is the ES message resolution
|
|
(callsignValid && strcmp(a->callsign, a->fatsv_emitted_callsign) != 0) ||
|
|
(airgroundValid && a->airground == AG_AIRBORNE && a->fatsv_emitted_airground == AG_GROUND) ||
|
|
(airgroundValid && a->airground == AG_GROUND && a->fatsv_emitted_airground == AG_AIRBORNE) ||
|
|
(squawkValid && a->squawk != a->fatsv_emitted_squawk);
|
|
|
|
uint64_t minAge;
|
|
if (immediate) {
|
|
// a change we want to emit right away
|
|
minAge = 0;
|
|
} else if (!positionValid) {
|
|
// don't send mode S very often
|
|
minAge = 30000;
|
|
} else if ((airgroundValid && a->airground == AG_GROUND) ||
|
|
(altValid && a->altitude_baro < 500 && (!gsValid || a->gs < 200)) ||
|
|
(gsValid && a->gs < 100 && (!altValid || a->altitude_baro < 1000))) {
|
|
// we are probably on the ground, increase the update rate
|
|
minAge = 1000;
|
|
} else if (!altValid || a->altitude_baro < 10000) {
|
|
// Below 10000 feet, emit up to every 5s when changing, 10s otherwise
|
|
minAge = (changed ? 5000 : 10000);
|
|
} else {
|
|
// Above 10000 feet, emit up to every 10s when changing, 30s otherwise
|
|
minAge = (changed ? 10000 : 30000);
|
|
}
|
|
|
|
if ((now - a->fatsv_last_emitted) < minAge)
|
|
continue;
|
|
|
|
char *p = prepareWrite(&Modes.fatsv_out, TSV_MAX_PACKET_SIZE);
|
|
if (!p)
|
|
return;
|
|
char *end = p + TSV_MAX_PACKET_SIZE;
|
|
|
|
p = appendFATSV(p, end, "clock", "%" PRIu64, messageNow() / 1000);
|
|
p = appendFATSV(p, end, (a->addr & MODES_NON_ICAO_ADDRESS) ? "otherid" : "hexid", "%06X", a->addr & 0xFFFFFF);
|
|
|
|
// for fields we only emit on change,
|
|
// occasionally re-emit them all
|
|
int forceEmit = (now - a->fatsv_last_force_emit) > 600000;
|
|
|
|
// these don't change often / at all, only emit when they change
|
|
if (forceEmit || a->addrtype != a->fatsv_emitted_addrtype) {
|
|
p = appendFATSV(p, end, "addrtype", "%s", addrtype_enum_string(a->addrtype));
|
|
}
|
|
if (forceEmit || a->adsb_version != a->fatsv_emitted_adsb_version) {
|
|
p = appendFATSV(p, end, "adsbVer", "%d", a->adsb_version);
|
|
}
|
|
if (forceEmit || a->category != a->fatsv_emitted_category) {
|
|
p = appendFATSV(p, end, "category", "%02X", a->category);
|
|
}
|
|
if (trackDataValid(&a->nac_p_valid) && (forceEmit || a->nac_p != a->fatsv_emitted_nac_p)) {
|
|
p = appendFATSVMeta(p, end, "nac_p", a, &a->nac_p_valid, "%u", a->nac_p);
|
|
}
|
|
if (trackDataValid(&a->nac_v_valid) && (forceEmit || a->nac_v != a->fatsv_emitted_nac_v)) {
|
|
p = appendFATSVMeta(p, end, "nac_v", a, &a->nac_v_valid, "%u", a->nac_v);
|
|
}
|
|
if (trackDataValid(&a->sil_valid) && (forceEmit || a->sil != a->fatsv_emitted_sil || a->sil_type != a->fatsv_emitted_sil_type)) {
|
|
p = appendFATSVMeta(p, end, "sil", a, &a->sil_valid, "{%u %s}", a->sil, sil_type_enum_string(a->sil_type));
|
|
}
|
|
if (trackDataValid(&a->nic_baro_valid) && (forceEmit || a->nic_baro != a->fatsv_emitted_nic_baro)) {
|
|
p = appendFATSVMeta(p, end, "nic_baro", a, &a->nic_baro_valid, "%u", a->nic_baro);
|
|
}
|
|
|
|
// only emit alt, speed, latlon, track etc if they have been received since the last time
|
|
// and are not stale
|
|
|
|
char *dataStart = p;
|
|
|
|
// special cases
|
|
if (airgroundValid)
|
|
p = appendFATSVMeta(p, end, "airGround", a, &a->airground_valid, "%s", airground_enum_string(a->airground));
|
|
if (squawkValid)
|
|
p = appendFATSVMeta(p, end, "squawk", a, &a->squawk_valid, "%04x", a->squawk);
|
|
if (callsignValid)
|
|
p = appendFATSVMeta(p, end, "ident", a, &a->callsign_valid, "{%s}", a->callsign);
|
|
if (altValid)
|
|
p = appendFATSVMeta(p, end, "alt", a, &a->altitude_baro_valid, "%d", a->altitude_baro);
|
|
if (positionValid) {
|
|
p = appendFATSVMeta(p, end, "position", a, &a->position_valid, "{%.5f %.5f %u %u}", a->lat, a->lon, a->pos_nic, a->pos_rc);
|
|
}
|
|
|
|
p = appendFATSVMeta(p, end, "alt_gnss", a, &a->altitude_geom_valid, "%d", a->altitude_geom);
|
|
p = appendFATSVMeta(p, end, "vrate", a, &a->baro_rate_valid, "%d", a->baro_rate);
|
|
p = appendFATSVMeta(p, end, "vrate_geom", a, &a->geom_rate_valid, "%d", a->geom_rate);
|
|
p = appendFATSVMeta(p, end, "speed", a, &a->gs_valid, "%.1f", a->gs);
|
|
p = appendFATSVMeta(p, end, "speed_ias", a, &a->ias_valid, "%u", a->ias);
|
|
p = appendFATSVMeta(p, end, "speed_tas", a, &a->tas_valid, "%u", a->tas);
|
|
p = appendFATSVMeta(p, end, "mach", a, &a->mach_valid, "%.3f", a->mach);
|
|
p = appendFATSVMeta(p, end, "track", a, &a->track_valid, "%.1f", a->track);
|
|
p = appendFATSVMeta(p, end, "track_rate", a, &a->track_rate_valid, "%.2f", a->track_rate);
|
|
p = appendFATSVMeta(p, end, "roll", a, &a->roll_valid, "%.1f", a->roll);
|
|
p = appendFATSVMeta(p, end, "heading_magnetic", a, &a->mag_heading_valid, "%.1f", a->mag_heading);
|
|
p = appendFATSVMeta(p, end, "heading_true", a, &a->true_heading_valid, "%.1f", a->true_heading);
|
|
p = appendFATSVMeta(p, end, "nav_alt", a, &a->nav_altitude_valid, "%u", a->nav_altitude);
|
|
p = appendFATSVMeta(p, end, "nav_heading", a, &a->nav_heading_valid, "%.1f", a->nav_heading);
|
|
p = appendFATSVMeta(p, end, "nav_modes", a, &a->nav_modes_valid, "{%s}", nav_modes_flags_string(a->nav_modes));
|
|
p = appendFATSVMeta(p, end, "nav_qnh", a, &a->nav_qnh_valid, "%.1f", a->nav_qnh);
|
|
p = appendFATSVMeta(p, end, "emergency", a, &a->emergency_valid, "%s", emergency_enum_string(a->emergency));
|
|
|
|
// if we didn't get anything interesting, bail out.
|
|
// We don't need to do anything special to unwind prepareWrite().
|
|
if (p == dataStart) {
|
|
continue;
|
|
}
|
|
|
|
--p; // remove last tab
|
|
p = safe_snprintf(p, end, "\n");
|
|
|
|
if (p <= end)
|
|
completeWrite(&Modes.fatsv_out, p);
|
|
else
|
|
fprintf(stderr, "fatsv: output too large (max %d, overran by %d)\n", TSV_MAX_PACKET_SIZE, (int) (p - end));
|
|
|
|
a->fatsv_emitted_altitude_baro = a->altitude_baro;
|
|
a->fatsv_emitted_altitude_geom = a->altitude_geom;
|
|
a->fatsv_emitted_baro_rate = a->baro_rate;
|
|
a->fatsv_emitted_geom_rate = a->geom_rate;
|
|
a->fatsv_emitted_gs = a->gs;
|
|
a->fatsv_emitted_ias = a->ias;
|
|
a->fatsv_emitted_tas = a->tas;
|
|
a->fatsv_emitted_mach = a->mach;
|
|
a->fatsv_emitted_track = a->track;
|
|
a->fatsv_emitted_track_rate = a->track_rate;
|
|
a->fatsv_emitted_roll = a->roll;
|
|
a->fatsv_emitted_mag_heading = a->mag_heading;
|
|
a->fatsv_emitted_true_heading = a->true_heading;
|
|
a->fatsv_emitted_airground = a->airground;
|
|
a->fatsv_emitted_nav_altitude = a->nav_altitude;
|
|
a->fatsv_emitted_nav_heading = a->nav_heading;
|
|
a->fatsv_emitted_nav_modes = a->nav_modes;
|
|
a->fatsv_emitted_nav_qnh = a->nav_qnh;
|
|
memcpy(a->fatsv_emitted_callsign, a->callsign, sizeof(a->fatsv_emitted_callsign));
|
|
a->fatsv_emitted_addrtype = a->addrtype;
|
|
a->fatsv_emitted_adsb_version = a->adsb_version;
|
|
a->fatsv_emitted_category = a->category;
|
|
a->fatsv_emitted_squawk = a->squawk;
|
|
a->fatsv_emitted_nac_p = a->nac_p;
|
|
a->fatsv_emitted_nac_v = a->nac_v;
|
|
a->fatsv_emitted_sil = a->sil;
|
|
a->fatsv_emitted_sil_type = a->sil_type;
|
|
a->fatsv_emitted_nic_baro = a->nic_baro;
|
|
a->fatsv_last_emitted = now;
|
|
if (forceEmit) {
|
|
a->fatsv_last_force_emit = now;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Perform periodic network work
|
|
//
|
|
void modesNetPeriodicWork(void) {
|
|
struct client *c, **prev;
|
|
struct net_service *s;
|
|
uint64_t now = mstime();
|
|
int need_flush = 0;
|
|
|
|
// Accept new connections
|
|
modesAcceptClients();
|
|
|
|
// Read from clients
|
|
for (c = Modes.clients; c; c = c->next) {
|
|
if (!c->service)
|
|
continue;
|
|
if (c->service->read_handler)
|
|
modesReadFromClient(c);
|
|
}
|
|
|
|
// Generate FATSV output
|
|
writeFATSV();
|
|
|
|
// If we have generated no messages for a while, send
|
|
// a heartbeat
|
|
if (Modes.net_heartbeat_interval) {
|
|
for (s = Modes.services; s; s = s->next) {
|
|
if (s->writer &&
|
|
s->connections &&
|
|
s->writer->send_heartbeat &&
|
|
(s->writer->lastWrite + Modes.net_heartbeat_interval) <= now) {
|
|
s->writer->send_heartbeat(s);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we have data that has been waiting to be written for a while,
|
|
// write it now.
|
|
for (s = Modes.services; s; s = s->next) {
|
|
if (s->writer &&
|
|
s->writer->dataUsed &&
|
|
(need_flush || (s->writer->lastWrite + Modes.net_output_flush_interval) <= now)) {
|
|
flushWrites(s->writer);
|
|
}
|
|
}
|
|
|
|
// Unlink and free closed clients
|
|
for (prev = &Modes.clients, c = *prev; c; c = *prev) {
|
|
if (c->fd == -1) {
|
|
// Recently closed, prune from list
|
|
*prev = c->next;
|
|
free(c);
|
|
} else {
|
|
prev = &c->next;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// =============================== Network IO ===========================
|
|
//
|