diff --git a/include/abuf.h b/include/abuf.h
index 4d3ac40b..0e658923 100644
--- a/include/abuf.h
+++ b/include/abuf.h
@@ -12,8 +12,8 @@
 #ifndef INCLUDE_ABUF_H_
 #define INCLUDE_ABUF_H_
 
-#if defined _MSC_VER // Microsoft Visual Studio
-    // MSC has something like C99 restrict as __restrict
+#if defined _MSC_VER || defined ESP32 // Microsoft Visual Studio or ESP32
+    // MSC and ESP32 have something like C99 restrict as __restrict
     #ifndef restrict
     #define restrict  __restrict
     #endif
diff --git a/include/data.h b/include/data.h
index c6e51218..34fe7952 100644
--- a/include/data.h
+++ b/include/data.h
@@ -21,14 +21,7 @@
 #ifndef INCLUDE_DATA_H_
 #define INCLUDE_DATA_H_
 
-#include <stdio.h>
-
-#if defined(_MSC_VER) && !defined(__clang__)
-    // MSVC has something like C99 restrict as __restrict
-    #ifndef restrict
-    #define restrict __restrict
-    #endif
-#endif
+#include <stddef.h>
 
 typedef enum {
     DATA_DATA,   /**< pointer to data is stored */
@@ -141,20 +134,6 @@ typedef struct data_output {
     void (*output_free)(struct data_output *output);
 } data_output_t;
 
-/** Construct data output for CSV printer.
-
-    @param file the output stream
-    @return The auxiliary data to pass along with data_csv_printer to data_print.
-            You must release this object with data_output_free once you're done with it.
-*/
-struct data_output *data_output_csv_create(FILE *file);
-
-struct data_output *data_output_json_create(FILE *file);
-
-struct data_output *data_output_kv_create(FILE *file);
-
-struct data_output *data_output_syslog_create(const char *host, const char *port);
-
 /** Setup known field keys and start output, used by CSV only.
 
     @param output the data_output handle from data_output_x_create
diff --git a/include/decoder.h b/include/decoder.h
index f0ffeb2c..e6d32953 100644
--- a/include/decoder.h
+++ b/include/decoder.h
@@ -6,6 +6,7 @@
 #define INCLUDE_DECODER_H_
 
 #include <string.h>
+#include <stdio.h>
 #include "r_device.h"
 #include "bitbuffer.h"
 #include "data.h"
diff --git a/include/decoder_util.h b/include/decoder_util.h
index d34da61d..f4609e2a 100644
--- a/include/decoder_util.h
+++ b/include/decoder_util.h
@@ -17,6 +17,12 @@
 #include "data.h"
 #include "r_device.h"
 
+#if defined _MSC_VER || defined ESP32 // Microsoft Visual Studio or ESP32
+    // MSC and ESP32 have something like C99 restrict as __restrict
+    #ifndef restrict
+    #define restrict  __restrict
+    #endif
+#endif
 // Defined in newer <sal.h> for MSVC.
 #ifndef _Printf_format_string_
 #define _Printf_format_string_
diff --git a/include/output_file.h b/include/output_file.h
new file mode 100644
index 00000000..d4685803
--- /dev/null
+++ b/include/output_file.h
@@ -0,0 +1,32 @@
+/** @file
+    File outputs for rtl_433 events.
+
+    Copyright (C) 2021 Christian Zuckschwerdt
+
+    This program is free software; you can redistribute it 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.
+*/
+
+#ifndef INCLUDE_OUTPUT_FILE_H_
+#define INCLUDE_OUTPUT_FILE_H_
+
+#include "data.h"
+#include <stdio.h>
+
+/** Construct data output for CSV printer.
+
+    @param file the output stream
+    @return The auxiliary data to pass along with data_csv_printer to data_print.
+            You must release this object with data_output_free once you're done with it.
+*/
+struct data_output *data_output_csv_create(FILE *file);
+
+struct data_output *data_output_json_create(FILE *file);
+
+struct data_output *data_output_kv_create(FILE *file);
+
+struct data_output *data_output_syslog_create(const char *host, const char *port);
+
+#endif /* INCLUDE_OUTPUT_FILE_H_ */
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 32f9af54..70c5b540 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -22,6 +22,7 @@ add_library(r_433 STATIC
     list.c
     mongoose.c
     optparse.c
+    output_file.c
     output_influx.c
     output_mqtt.c
     pulse_analyzer.c
diff --git a/src/data.c b/src/data.c
index 35c88840..f51a497b 100644
--- a/src/data.c
+++ b/src/data.c
@@ -10,66 +10,21 @@
     (at your option) any later version.
 */
 
+#include "data.h"
+
+#include "abuf.h"
+#include "fatal.h"
+
 #include <stdarg.h>
 #include <assert.h>
 #include <string.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <stdbool.h>
-#include <limits.h>
-// gethostname() needs _XOPEN_SOURCE 500 on unistd.h
-#ifndef _XOPEN_SOURCE
-#define _XOPEN_SOURCE 500
-#endif
 
-#ifndef _MSC_VER
-#include <unistd.h>
-#endif
-
-#ifdef _WIN32
-    #if !defined(_WIN32_WINNT) || (_WIN32_WINNT < 0x0600)
-    #undef _WIN32_WINNT
-    #define _WIN32_WINNT 0x0600   /* Needed to pull in 'struct sockaddr_storage' */
-    #endif
-
-    #include <winsock2.h>
-    #include <ws2tcpip.h>
-#else
-    #include <sys/types.h>
-    #include <sys/socket.h>
-    #include <netdb.h>
-    #include <netinet/in.h>
-
-    #define SOCKET          int
-    #define INVALID_SOCKET  (-1)
-    #define closesocket(x)  close(x)
-#endif
-
-#include <time.h>
-
-#include "term_ctl.h"
-#include "abuf.h"
-#include "fatal.h"
-#include "r_util.h"
-
-#include "data.h"
-
-#ifdef _WIN32
-    #define _POSIX_HOST_NAME_MAX  128
-    #define perror(str)           ws2_perror(str)
-
-    static void ws2_perror (const char *str)
-    {
-        if (str && *str)
-            fprintf(stderr, "%s: ", str);
-        fprintf(stderr, "Winsock error %d.\n", WSAGetLastError());
-    }
-#endif
-#ifdef ESP32
-    #include <tcpip_adapter.h>
-    #define _POSIX_HOST_NAME_MAX 128
-    #define gai_strerror strerror
-#endif
+// Macro to prevent unused variables (passed into a function)
+// from generating a warning.
+#define UNUSED(x) (void)(x)
 
 typedef void* (*array_elementwise_import_fn)(void*);
 typedef void (*array_element_release_fn)(void*);
@@ -456,548 +411,6 @@ void print_array_value(data_output_t *output, data_array_t *array, char const *f
     }
 }
 
-/* JSON printer */
-
-typedef struct {
-    struct data_output output;
-    FILE *file;
-} data_output_json_t;
-
-static void print_json_array(data_output_t *output, data_array_t *array, char const *format)
-{
-    data_output_json_t *json = (data_output_json_t *)output;
-
-    fprintf(json->file, "[");
-    for (int c = 0; c < array->num_values; ++c) {
-        if (c)
-            fprintf(json->file, ", ");
-        print_array_value(output, array, format, c);
-    }
-    fprintf(json->file, "]");
-}
-
-static void print_json_data(data_output_t *output, data_t *data, char const *format)
-{
-    UNUSED(format);
-    data_output_json_t *json = (data_output_json_t *)output;
-
-    bool separator = false;
-    fputc('{', json->file);
-    while (data) {
-        if (separator)
-            fprintf(json->file, ", ");
-        output->print_string(output, data->key, NULL);
-        fprintf(json->file, " : ");
-        print_value(output, data->type, data->value, data->format);
-        separator = true;
-        data = data->next;
-    }
-    fputc('}', json->file);
-}
-
-static void print_json_string(data_output_t *output, const char *str, char const *format)
-{
-    UNUSED(format);
-    data_output_json_t *json = (data_output_json_t *)output;
-
-    size_t str_len = strlen(str);
-    if (str[0] == '{' && str[str_len - 1] == '}') {
-        // Print embedded JSON object verbatim
-        fprintf(json->file, "%s", str);
-        return;
-    }
-
-    fprintf(json->file, "\"");
-    while (*str) {
-        if (*str == '\r') {
-            fprintf(json->file, "\\r");
-            continue;
-        }
-        if (*str == '\n') {
-            fprintf(json->file, "\\n");
-            continue;
-        }
-        if (*str == '\t') {
-            fprintf(json->file, "\\t");
-            continue;
-        }
-        if (*str == '"' || *str == '\\')
-            fputc('\\', json->file);
-        fputc(*str, json->file);
-        ++str;
-    }
-    fprintf(json->file, "\"");
-}
-
-static void print_json_double(data_output_t *output, double data, char const *format)
-{
-    UNUSED(format);
-    data_output_json_t *json = (data_output_json_t *)output;
-
-    fprintf(json->file, "%.3f", data);
-}
-
-static void print_json_int(data_output_t *output, int data, char const *format)
-{
-    UNUSED(format);
-    data_output_json_t *json = (data_output_json_t *)output;
-
-    fprintf(json->file, "%d", data);
-}
-
-static void print_json_flush(data_output_t *output)
-{
-    data_output_json_t *json = (data_output_json_t *)output;
-
-    if (json && json->file) {
-        fputc('\n', json->file);
-        fflush(json->file);
-    }
-}
-
-static void data_output_json_free(data_output_t *output)
-{
-    if (!output)
-        return;
-
-    free(output);
-}
-
-struct data_output *data_output_json_create(FILE *file)
-{
-    data_output_json_t *json = calloc(1, sizeof(data_output_json_t));
-    if (!json) {
-        WARN_CALLOC("data_output_json_create()");
-        return NULL; // NOTE: returns NULL on alloc failure.
-    }
-
-    json->output.print_data   = print_json_data;
-    json->output.print_array  = print_json_array;
-    json->output.print_string = print_json_string;
-    json->output.print_double = print_json_double;
-    json->output.print_int    = print_json_int;
-    json->output.output_flush = print_json_flush;
-    json->output.output_free  = data_output_json_free;
-    json->file                = file;
-
-    return &json->output;
-}
-
-/* Pretty Key-Value printer */
-
-static int kv_color_for_key(char const *key)
-{
-    if (!key || !*key)
-        return TERM_COLOR_RESET;
-    if (!strcmp(key, "tag") || !strcmp(key, "time"))
-        return TERM_COLOR_BLUE;
-    if (!strcmp(key, "model") || !strcmp(key, "type") || !strcmp(key, "id"))
-        return TERM_COLOR_RED;
-    if (!strcmp(key, "mic"))
-        return TERM_COLOR_CYAN;
-    if (!strcmp(key, "mod") || !strcmp(key, "freq") || !strcmp(key, "freq1") || !strcmp(key, "freq2"))
-        return TERM_COLOR_MAGENTA;
-    if (!strcmp(key, "rssi") || !strcmp(key, "snr") || !strcmp(key, "noise"))
-        return TERM_COLOR_YELLOW;
-    return TERM_COLOR_GREEN;
-}
-
-static int kv_break_before_key(char const *key)
-{
-    if (!key || !*key)
-        return 0;
-    if (!strcmp(key, "model") || !strcmp(key, "mod") || !strcmp(key, "rssi") || !strcmp(key, "codes"))
-        return 1;
-    return 0;
-}
-
-static int kv_break_after_key(char const *key)
-{
-    if (!key || !*key)
-        return 0;
-    if (!strcmp(key, "id") || !strcmp(key, "mic"))
-        return 1;
-    return 0;
-}
-
-typedef struct {
-    struct data_output output;
-    FILE *file;
-    void *term;
-    int color;
-    int ring_bell;
-    int term_width;
-    int data_recursion;
-    int column;
-} data_output_kv_t;
-
-#define KV_SEP "_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ "
-
-static void print_kv_data(data_output_t *output, data_t *data, char const *format)
-{
-    UNUSED(format);
-    data_output_kv_t *kv = (data_output_kv_t *)output;
-
-    int color = kv->color;
-    int ring_bell = kv->ring_bell;
-
-    // top-level: update width and print separator
-    if (!kv->data_recursion) {
-        kv->term_width = term_get_columns(kv->term); // update current term width
-        if (color)
-            term_set_fg(kv->term, TERM_COLOR_BLACK);
-        if (ring_bell)
-            term_ring_bell(kv->term);
-        char sep[] = KV_SEP KV_SEP KV_SEP KV_SEP;
-        if (kv->term_width < (int)sizeof(sep))
-            sep[kv->term_width > 0 ? kv->term_width - 1 : 40] = '\0';
-        fprintf(kv->file, "%s\n", sep);
-        if (color)
-            term_set_fg(kv->term, TERM_COLOR_RESET);
-    }
-    // nested data object: break before
-    else {
-        if (color)
-            term_set_fg(kv->term, TERM_COLOR_RESET);
-        fprintf(kv->file, "\n");
-        kv->column = 0;
-    }
-
-    ++kv->data_recursion;
-    while (data) {
-        // break before some known keys
-        if (kv->column > 0 && kv_break_before_key(data->key)) {
-            fprintf(kv->file, "\n");
-            kv->column = 0;
-        }
-        // break if not enough width left
-        else if (kv->column >= kv->term_width - 26) {
-            fprintf(kv->file, "\n");
-            kv->column = 0;
-        }
-        // pad to next alignment if there is enough width left
-        else if (kv->column > 0 && kv->column < kv->term_width - 26) {
-            kv->column += fprintf(kv->file, "%*s", 25 - kv->column % 26, " ");
-        }
-
-        // print key
-        char *key = *data->pretty_key ? data->pretty_key : data->key;
-        kv->column += fprintf(kv->file, "%-10s: ", key);
-        // print value
-        if (color)
-            term_set_fg(kv->term, kv_color_for_key(data->key));
-        print_value(output, data->type, data->value, data->format);
-        if (color)
-            term_set_fg(kv->term, TERM_COLOR_RESET);
-
-        // force break after some known keys
-        if (kv->column > 0 && kv_break_after_key(data->key)) {
-            kv->column = kv->term_width; // force break;
-        }
-
-        data = data->next;
-    }
-    --kv->data_recursion;
-
-    // top-level: always end with newline
-    if (!kv->data_recursion && kv->column > 0) {
-        //fprintf(kv->file, "\n"); // data_output_print() already adds a newline
-        kv->column = 0;
-    }
-}
-
-static void print_kv_array(data_output_t *output, data_array_t *array, char const *format)
-{
-    data_output_kv_t *kv = (data_output_kv_t *)output;
-
-    //fprintf(kv->file, "[ ");
-    for (int c = 0; c < array->num_values; ++c) {
-        if (c)
-            fprintf(kv->file, ", ");
-        print_array_value(output, array, format, c);
-    }
-    //fprintf(kv->file, " ]");
-}
-
-static void print_kv_double(data_output_t *output, double data, char const *format)
-{
-    data_output_kv_t *kv = (data_output_kv_t *)output;
-
-    kv->column += fprintf(kv->file, format ? format : "%.3f", data);
-}
-
-static void print_kv_int(data_output_t *output, int data, char const *format)
-{
-    data_output_kv_t *kv = (data_output_kv_t *)output;
-
-    kv->column += fprintf(kv->file, format ? format : "%d", data);
-}
-
-static void print_kv_string(data_output_t *output, const char *data, char const *format)
-{
-    data_output_kv_t *kv = (data_output_kv_t *)output;
-
-    kv->column += fprintf(kv->file, format ? format : "%s", data);
-}
-
-static void print_kv_flush(data_output_t *output)
-{
-    data_output_kv_t *kv = (data_output_kv_t *)output;
-
-    if (kv && kv->file) {
-        fputc('\n', kv->file);
-        fflush(kv->file);
-    }
-}
-
-static void data_output_kv_free(data_output_t *output)
-{
-    data_output_kv_t *kv = (data_output_kv_t *)output;
-
-    if (!output)
-        return;
-
-    if (kv->color)
-        term_free(kv->term);
-
-    free(output);
-}
-struct data_output *data_output_kv_create(FILE *file)
-{
-    data_output_kv_t *kv = calloc(1, sizeof(data_output_kv_t));
-    if (!kv) {
-        WARN_CALLOC("data_output_kv_create()");
-        return NULL; // NOTE: returns NULL on alloc failure.
-    }
-
-    kv->output.print_data   = print_kv_data;
-    kv->output.print_array  = print_kv_array;
-    kv->output.print_string = print_kv_string;
-    kv->output.print_double = print_kv_double;
-    kv->output.print_int    = print_kv_int;
-    kv->output.output_flush = print_kv_flush;
-    kv->output.output_free  = data_output_kv_free;
-    kv->file                = file;
-
-    kv->term = term_init(file);
-    kv->color = term_has_color(kv->term);
-
-    kv->ring_bell = 0; // TODO: enable if requested...
-
-    return &kv->output;
-}
-
-/* CSV printer; doesn't really support recursive data objects yet */
-
-typedef struct {
-    struct data_output output;
-    FILE *file;
-    const char **fields;
-    int data_recursion;
-    const char *separator;
-} data_output_csv_t;
-
-static void print_csv_data(data_output_t *output, data_t *data, char const *format)
-{
-    UNUSED(format);
-    data_output_csv_t *csv = (data_output_csv_t *)output;
-
-    const char **fields = csv->fields;
-    int i;
-
-    if (csv->data_recursion)
-        return;
-
-    int regular = 0; // skip "states" output
-    for (data_t *d = data; d; d = d->next) {
-        if (!strcmp(d->key, "msg") || !strcmp(d->key, "codes") || !strcmp(d->key, "model")) {
-            regular = 1;
-            break;
-        }
-    }
-    if (!regular)
-        return;
-
-    ++csv->data_recursion;
-    for (i = 0; fields[i]; ++i) {
-        const char *key = fields[i];
-        data_t *found = NULL;
-        if (i)
-            fprintf(csv->file, "%s", csv->separator);
-        for (data_t *iter = data; !found && iter; iter = iter->next)
-            if (strcmp(iter->key, key) == 0)
-                found = iter;
-
-        if (found)
-            print_value(output, found->type, found->value, found->format);
-    }
-    --csv->data_recursion;
-}
-
-static void print_csv_array(data_output_t *output, data_array_t *array, char const *format)
-{
-    data_output_csv_t *csv = (data_output_csv_t *)output;
-
-    for (int c = 0; c < array->num_values; ++c) {
-        if (c)
-            fprintf(csv->file, ";");
-        print_array_value(output, array, format, c);
-    }
-}
-
-static void print_csv_string(data_output_t *output, const char *str, char const *format)
-{
-    UNUSED(format);
-    data_output_csv_t *csv = (data_output_csv_t *)output;
-
-    while (*str) {
-        if (strncmp(str, csv->separator, strlen(csv->separator)) == 0)
-            fputc('\\', csv->file);
-        fputc(*str, csv->file);
-        ++str;
-    }
-}
-
-static int compare_strings(const void *a, const void *b)
-{
-    return strcmp(*(char **)a, *(char **)b);
-}
-
-static void data_output_csv_start(struct data_output *output, char const *const *fields, int num_fields)
-{
-    data_output_csv_t *csv = (data_output_csv_t *)output;
-
-    int csv_fields = 0;
-    int i, j;
-    const char **allowed = NULL;
-    int *use_count = NULL;
-    int num_unique_fields;
-    if (!csv)
-        goto alloc_error;
-
-    csv->separator = ",";
-
-    allowed = calloc(num_fields, sizeof(const char *));
-    if (!allowed) {
-        WARN_CALLOC("data_output_csv_start()");
-        goto alloc_error;
-    }
-    memcpy((void *)allowed, fields, sizeof(const char *) * num_fields);
-
-    qsort((void *)allowed, num_fields, sizeof(char *), compare_strings);
-
-    // overwrite duplicates
-    i = 0;
-    j = 0;
-    while (j < num_fields) {
-        while (j > 0 && j < num_fields &&
-                strcmp(allowed[j - 1], allowed[j]) == 0)
-            ++j;
-
-        if (j < num_fields) {
-            allowed[i] = allowed[j];
-            ++i;
-            ++j;
-        }
-    }
-    num_unique_fields = i;
-
-    csv->fields = calloc(num_unique_fields + 1, sizeof(const char *));
-    if (!csv->fields) {
-        WARN_CALLOC("data_output_csv_start()");
-        goto alloc_error;
-    }
-
-    use_count = calloc(num_unique_fields + 1, sizeof(*use_count)); // '+ 1' so we never alloc size 0
-    if (!use_count) {
-        WARN_CALLOC("data_output_csv_start()");
-        goto alloc_error;
-    }
-
-    for (i = 0; i < num_fields; ++i) {
-        const char **field = bsearch(&fields[i], allowed, num_unique_fields, sizeof(const char *),
-                compare_strings);
-        int *field_use_count = use_count + (field - allowed);
-        if (field && !*field_use_count) {
-            csv->fields[csv_fields] = fields[i];
-            ++csv_fields;
-            ++*field_use_count;
-        }
-    }
-    csv->fields[csv_fields] = NULL;
-    free((void *)allowed);
-    free(use_count);
-
-    // Output the CSV header
-    for (i = 0; csv->fields[i]; ++i) {
-        fprintf(csv->file, "%s%s", i > 0 ? csv->separator : "", csv->fields[i]);
-    }
-    fprintf(csv->file, "\n");
-    return;
-
-alloc_error:
-    free(use_count);
-    free((void *)allowed);
-    if (csv)
-        free((void *)csv->fields);
-    free(csv);
-}
-
-static void print_csv_double(data_output_t *output, double data, char const *format)
-{
-    UNUSED(format);
-    data_output_csv_t *csv = (data_output_csv_t *)output;
-
-    fprintf(csv->file, "%.3f", data);
-}
-
-static void print_csv_int(data_output_t *output, int data, char const *format)
-{
-    UNUSED(format);
-    data_output_csv_t *csv = (data_output_csv_t *)output;
-
-    fprintf(csv->file, "%d", data);
-}
-
-static void print_csv_flush(data_output_t *output)
-{
-    data_output_csv_t *csv = (data_output_csv_t *)output;
-
-    if (csv && csv->file) {
-        fputc('\n', csv->file);
-        fflush(csv->file);
-    }
-}
-
-static void data_output_csv_free(data_output_t *output)
-{
-    data_output_csv_t *csv = (data_output_csv_t *)output;
-
-    free((void *)csv->fields);
-    free(csv);
-}
-
-struct data_output *data_output_csv_create(FILE *file)
-{
-    data_output_csv_t *csv = calloc(1, sizeof(data_output_csv_t));
-    if (!csv) {
-        WARN_CALLOC("data_output_csv_create()");
-        return NULL; // NOTE: returns NULL on alloc failure.
-    }
-
-    csv->output.print_data   = print_csv_data;
-    csv->output.print_array  = print_csv_array;
-    csv->output.print_string = print_csv_string;
-    csv->output.print_double = print_csv_double;
-    csv->output.print_int    = print_csv_int;
-    csv->output.output_start = data_output_csv_start;
-    csv->output.output_flush = print_csv_flush;
-    csv->output.output_free  = data_output_csv_free;
-    csv->file                = file;
-
-    return &csv->output;
-}
-
 /* JSON string printer */
 
 typedef struct {
@@ -1141,167 +554,3 @@ size_t data_print_jsons(data_t *data, char *dst, size_t len)
 
     return len - jsons.msg.left;
 }
-
-/* Datagram (UDP) client */
-
-typedef struct {
-    struct sockaddr_storage addr;
-    socklen_t addr_len;
-    SOCKET sock;
-} datagram_client_t;
-
-static int datagram_client_open(datagram_client_t *client, const char *host, const char *port)
-{
-    if (!host || !port)
-        return -1;
-
-    struct addrinfo hints, *res, *res0;
-    int    error;
-    SOCKET sock;
-
-    memset(&hints, 0, sizeof(hints));
-    hints.ai_family = PF_UNSPEC;
-    hints.ai_socktype = SOCK_DGRAM;
-    hints.ai_flags = AI_ADDRCONFIG;
-    error = getaddrinfo(host, port, &hints, &res0);
-    if (error) {
-        fprintf(stderr, "%s\n", gai_strerror(error));
-        return -1;
-    }
-    sock = INVALID_SOCKET;
-    for (res = res0; res; res = res->ai_next) {
-        sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
-        if (sock >= 0) {
-            client->sock = sock;
-            memset(&client->addr, 0, sizeof(client->addr));
-            memcpy(&client->addr, res->ai_addr, res->ai_addrlen);
-            client->addr_len = res->ai_addrlen;
-            break; // success
-        }
-    }
-    freeaddrinfo(res0);
-    if (sock == INVALID_SOCKET) {
-        perror("socket");
-        return -1;
-    }
-
-    //int broadcast = 1;
-    //int ret = setsockopt(client->sock, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));
-
-    return 0;
-}
-
-static void datagram_client_close(datagram_client_t *client)
-{
-    if (!client)
-        return;
-
-    if (client->sock != INVALID_SOCKET) {
-        closesocket(client->sock);
-        client->sock = INVALID_SOCKET;
-    }
-
-#ifdef _WIN32
-    WSACleanup();
-#endif
-}
-
-static void datagram_client_send(datagram_client_t *client, const char *message, size_t message_len)
-{
-    int r =  sendto(client->sock, message, message_len, 0, (struct sockaddr *)&client->addr, client->addr_len);
-    if (r == -1) {
-        perror("sendto");
-    }
-}
-
-/* Syslog UDP printer, RFC 5424 (IETF-syslog protocol) */
-
-typedef struct {
-    struct data_output output;
-    datagram_client_t client;
-    int pri;
-    char hostname[_POSIX_HOST_NAME_MAX + 1];
-} data_output_syslog_t;
-
-static void print_syslog_data(data_output_t *output, data_t *data, char const *format)
-{
-    UNUSED(format);
-    data_output_syslog_t *syslog = (data_output_syslog_t *)output;
-
-    // we expect a normal message around 500 bytes
-    // full stats report would be 12k and we want a max of MTU anyway
-    char message[1024];
-    abuf_t msg = {0};
-    abuf_init(&msg, message, sizeof(message));
-
-    time_t now;
-    struct tm tm_info;
-    time(&now);
-#ifdef _WIN32
-    gmtime_s(&tm_info, &now);
-#else
-    gmtime_r(&now, &tm_info);
-#endif
-    char timestamp[21];
-    strftime(timestamp, 21, "%Y-%m-%dT%H:%M:%SZ", &tm_info);
-
-    abuf_printf(&msg, "<%d>1 %s %s rtl_433 - - - ", syslog->pri, timestamp, syslog->hostname);
-
-    msg.tail += data_print_jsons(data, msg.tail, msg.left);
-    if (msg.tail >= msg.head + sizeof(message))
-        return; // abort on overflow, we don't actually want to send more than fits the MTU
-
-    size_t abuf_len = msg.tail - msg.head;
-    datagram_client_send(&syslog->client, message, abuf_len);
-}
-
-static void data_output_syslog_free(data_output_t *output)
-{
-    data_output_syslog_t *syslog = (data_output_syslog_t *)output;
-
-    if (!syslog)
-        return;
-
-    datagram_client_close(&syslog->client);
-
-    free(syslog);
-}
-
-struct data_output *data_output_syslog_create(const char *host, const char *port)
-{
-    data_output_syslog_t *syslog = calloc(1, sizeof(data_output_syslog_t));
-    if (!syslog) {
-        WARN_CALLOC("data_output_syslog_create()");
-        return NULL; // NOTE: returns NULL on alloc failure.
-    }
-#ifdef _WIN32
-    WSADATA wsa;
-
-    if (WSAStartup(MAKEWORD(2,2),&wsa) != 0) {
-        perror("WSAStartup()");
-        free(syslog);
-        return NULL;
-    }
-#endif
-
-    syslog->output.print_data   = print_syslog_data;
-    syslog->output.output_free  = data_output_syslog_free;
-    // Severity 5 "Notice", Facility 20 "local use 4"
-    syslog->pri = 20 * 8 + 5;
-    #ifdef ESP32
-    const char* adapter_hostname = NULL;
-    tcpip_adapter_get_hostname(TCPIP_ADAPTER_IF_STA, &adapter_hostname);
-    if (adapter_hostname) {
-        memcpy(syslog->hostname, adapter_hostname, _POSIX_HOST_NAME_MAX);
-    }
-    else {
-        syslog->hostname[0] = '\0';
-    }
-    #else
-    gethostname(syslog->hostname, _POSIX_HOST_NAME_MAX + 1);
-    #endif
-    syslog->hostname[_POSIX_HOST_NAME_MAX] = '\0';
-    datagram_client_open(&syslog->client, host, port);
-
-    return &syslog->output;
-}
diff --git a/src/output_file.c b/src/output_file.c
new file mode 100644
index 00000000..caae3851
--- /dev/null
+++ b/src/output_file.c
@@ -0,0 +1,776 @@
+/** @file
+    File outputs for rtl_433 events.
+
+    Copyright (C) 2021 Christian Zuckschwerdt
+
+    This program is free software; you can redistribute it 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.
+*/
+
+#include "output_file.h"
+
+#include "data.h"
+#include "term_ctl.h"
+#include "abuf.h"
+#include "r_util.h"
+#include "fatal.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <limits.h>
+// gethostname() needs _XOPEN_SOURCE 500 on unistd.h
+#ifndef _XOPEN_SOURCE
+#define _XOPEN_SOURCE 500
+#endif
+
+#ifndef _MSC_VER
+#include <unistd.h>
+#endif
+
+#ifdef _WIN32
+    #if !defined(_WIN32_WINNT) || (_WIN32_WINNT < 0x0600)
+    #undef _WIN32_WINNT
+    #define _WIN32_WINNT 0x0600   /* Needed to pull in 'struct sockaddr_storage' */
+    #endif
+
+    #include <winsock2.h>
+    #include <ws2tcpip.h>
+#else
+    #include <sys/types.h>
+    #include <sys/socket.h>
+    #include <netdb.h>
+    #include <netinet/in.h>
+
+    #define SOCKET          int
+    #define INVALID_SOCKET  (-1)
+    #define closesocket(x)  close(x)
+#endif
+
+#include <time.h>
+
+#ifdef _WIN32
+    #define _POSIX_HOST_NAME_MAX  128
+    #define perror(str)           ws2_perror(str)
+
+    static void ws2_perror (const char *str)
+    {
+        if (str && *str)
+            fprintf(stderr, "%s: ", str);
+        fprintf(stderr, "Winsock error %d.\n", WSAGetLastError());
+    }
+#endif
+#ifdef ESP32
+    #include <tcpip_adapter.h>
+    #define _POSIX_HOST_NAME_MAX 128
+    #define gai_strerror strerror
+#endif
+
+/* JSON printer */
+
+typedef struct {
+    struct data_output output;
+    FILE *file;
+} data_output_json_t;
+
+static void print_json_array(data_output_t *output, data_array_t *array, char const *format)
+{
+    data_output_json_t *json = (data_output_json_t *)output;
+
+    fprintf(json->file, "[");
+    for (int c = 0; c < array->num_values; ++c) {
+        if (c)
+            fprintf(json->file, ", ");
+        print_array_value(output, array, format, c);
+    }
+    fprintf(json->file, "]");
+}
+
+static void print_json_data(data_output_t *output, data_t *data, char const *format)
+{
+    UNUSED(format);
+    data_output_json_t *json = (data_output_json_t *)output;
+
+    bool separator = false;
+    fputc('{', json->file);
+    while (data) {
+        if (separator)
+            fprintf(json->file, ", ");
+        output->print_string(output, data->key, NULL);
+        fprintf(json->file, " : ");
+        print_value(output, data->type, data->value, data->format);
+        separator = true;
+        data = data->next;
+    }
+    fputc('}', json->file);
+}
+
+static void print_json_string(data_output_t *output, const char *str, char const *format)
+{
+    UNUSED(format);
+    data_output_json_t *json = (data_output_json_t *)output;
+
+    size_t str_len = strlen(str);
+    if (str[0] == '{' && str[str_len - 1] == '}') {
+        // Print embedded JSON object verbatim
+        fprintf(json->file, "%s", str);
+        return;
+    }
+
+    fprintf(json->file, "\"");
+    while (*str) {
+        if (*str == '\r') {
+            fprintf(json->file, "\\r");
+            continue;
+        }
+        if (*str == '\n') {
+            fprintf(json->file, "\\n");
+            continue;
+        }
+        if (*str == '\t') {
+            fprintf(json->file, "\\t");
+            continue;
+        }
+        if (*str == '"' || *str == '\\')
+            fputc('\\', json->file);
+        fputc(*str, json->file);
+        ++str;
+    }
+    fprintf(json->file, "\"");
+}
+
+static void print_json_double(data_output_t *output, double data, char const *format)
+{
+    UNUSED(format);
+    data_output_json_t *json = (data_output_json_t *)output;
+
+    fprintf(json->file, "%.3f", data);
+}
+
+static void print_json_int(data_output_t *output, int data, char const *format)
+{
+    UNUSED(format);
+    data_output_json_t *json = (data_output_json_t *)output;
+
+    fprintf(json->file, "%d", data);
+}
+
+static void print_json_flush(data_output_t *output)
+{
+    data_output_json_t *json = (data_output_json_t *)output;
+
+    if (json && json->file) {
+        fputc('\n', json->file);
+        fflush(json->file);
+    }
+}
+
+static void data_output_json_free(data_output_t *output)
+{
+    if (!output)
+        return;
+
+    free(output);
+}
+
+struct data_output *data_output_json_create(FILE *file)
+{
+    data_output_json_t *json = calloc(1, sizeof(data_output_json_t));
+    if (!json) {
+        WARN_CALLOC("data_output_json_create()");
+        return NULL; // NOTE: returns NULL on alloc failure.
+    }
+
+    json->output.print_data   = print_json_data;
+    json->output.print_array  = print_json_array;
+    json->output.print_string = print_json_string;
+    json->output.print_double = print_json_double;
+    json->output.print_int    = print_json_int;
+    json->output.output_flush = print_json_flush;
+    json->output.output_free  = data_output_json_free;
+    json->file                = file;
+
+    return &json->output;
+}
+
+/* Pretty Key-Value printer */
+
+static int kv_color_for_key(char const *key)
+{
+    if (!key || !*key)
+        return TERM_COLOR_RESET;
+    if (!strcmp(key, "tag") || !strcmp(key, "time"))
+        return TERM_COLOR_BLUE;
+    if (!strcmp(key, "model") || !strcmp(key, "type") || !strcmp(key, "id"))
+        return TERM_COLOR_RED;
+    if (!strcmp(key, "mic"))
+        return TERM_COLOR_CYAN;
+    if (!strcmp(key, "mod") || !strcmp(key, "freq") || !strcmp(key, "freq1") || !strcmp(key, "freq2"))
+        return TERM_COLOR_MAGENTA;
+    if (!strcmp(key, "rssi") || !strcmp(key, "snr") || !strcmp(key, "noise"))
+        return TERM_COLOR_YELLOW;
+    return TERM_COLOR_GREEN;
+}
+
+static int kv_break_before_key(char const *key)
+{
+    if (!key || !*key)
+        return 0;
+    if (!strcmp(key, "model") || !strcmp(key, "mod") || !strcmp(key, "rssi") || !strcmp(key, "codes"))
+        return 1;
+    return 0;
+}
+
+static int kv_break_after_key(char const *key)
+{
+    if (!key || !*key)
+        return 0;
+    if (!strcmp(key, "id") || !strcmp(key, "mic"))
+        return 1;
+    return 0;
+}
+
+typedef struct {
+    struct data_output output;
+    FILE *file;
+    void *term;
+    int color;
+    int ring_bell;
+    int term_width;
+    int data_recursion;
+    int column;
+} data_output_kv_t;
+
+#define KV_SEP "_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ "
+
+static void print_kv_data(data_output_t *output, data_t *data, char const *format)
+{
+    UNUSED(format);
+    data_output_kv_t *kv = (data_output_kv_t *)output;
+
+    int color = kv->color;
+    int ring_bell = kv->ring_bell;
+
+    // top-level: update width and print separator
+    if (!kv->data_recursion) {
+        kv->term_width = term_get_columns(kv->term); // update current term width
+        if (color)
+            term_set_fg(kv->term, TERM_COLOR_BLACK);
+        if (ring_bell)
+            term_ring_bell(kv->term);
+        char sep[] = KV_SEP KV_SEP KV_SEP KV_SEP;
+        if (kv->term_width < (int)sizeof(sep))
+            sep[kv->term_width > 0 ? kv->term_width - 1 : 40] = '\0';
+        fprintf(kv->file, "%s\n", sep);
+        if (color)
+            term_set_fg(kv->term, TERM_COLOR_RESET);
+    }
+    // nested data object: break before
+    else {
+        if (color)
+            term_set_fg(kv->term, TERM_COLOR_RESET);
+        fprintf(kv->file, "\n");
+        kv->column = 0;
+    }
+
+    ++kv->data_recursion;
+    while (data) {
+        // break before some known keys
+        if (kv->column > 0 && kv_break_before_key(data->key)) {
+            fprintf(kv->file, "\n");
+            kv->column = 0;
+        }
+        // break if not enough width left
+        else if (kv->column >= kv->term_width - 26) {
+            fprintf(kv->file, "\n");
+            kv->column = 0;
+        }
+        // pad to next alignment if there is enough width left
+        else if (kv->column > 0 && kv->column < kv->term_width - 26) {
+            kv->column += fprintf(kv->file, "%*s", 25 - kv->column % 26, " ");
+        }
+
+        // print key
+        char *key = *data->pretty_key ? data->pretty_key : data->key;
+        kv->column += fprintf(kv->file, "%-10s: ", key);
+        // print value
+        if (color)
+            term_set_fg(kv->term, kv_color_for_key(data->key));
+        print_value(output, data->type, data->value, data->format);
+        if (color)
+            term_set_fg(kv->term, TERM_COLOR_RESET);
+
+        // force break after some known keys
+        if (kv->column > 0 && kv_break_after_key(data->key)) {
+            kv->column = kv->term_width; // force break;
+        }
+
+        data = data->next;
+    }
+    --kv->data_recursion;
+
+    // top-level: always end with newline
+    if (!kv->data_recursion && kv->column > 0) {
+        //fprintf(kv->file, "\n"); // data_output_print() already adds a newline
+        kv->column = 0;
+    }
+}
+
+static void print_kv_array(data_output_t *output, data_array_t *array, char const *format)
+{
+    data_output_kv_t *kv = (data_output_kv_t *)output;
+
+    //fprintf(kv->file, "[ ");
+    for (int c = 0; c < array->num_values; ++c) {
+        if (c)
+            fprintf(kv->file, ", ");
+        print_array_value(output, array, format, c);
+    }
+    //fprintf(kv->file, " ]");
+}
+
+static void print_kv_double(data_output_t *output, double data, char const *format)
+{
+    data_output_kv_t *kv = (data_output_kv_t *)output;
+
+    kv->column += fprintf(kv->file, format ? format : "%.3f", data);
+}
+
+static void print_kv_int(data_output_t *output, int data, char const *format)
+{
+    data_output_kv_t *kv = (data_output_kv_t *)output;
+
+    kv->column += fprintf(kv->file, format ? format : "%d", data);
+}
+
+static void print_kv_string(data_output_t *output, const char *data, char const *format)
+{
+    data_output_kv_t *kv = (data_output_kv_t *)output;
+
+    kv->column += fprintf(kv->file, format ? format : "%s", data);
+}
+
+static void print_kv_flush(data_output_t *output)
+{
+    data_output_kv_t *kv = (data_output_kv_t *)output;
+
+    if (kv && kv->file) {
+        fputc('\n', kv->file);
+        fflush(kv->file);
+    }
+}
+
+static void data_output_kv_free(data_output_t *output)
+{
+    data_output_kv_t *kv = (data_output_kv_t *)output;
+
+    if (!output)
+        return;
+
+    if (kv->color)
+        term_free(kv->term);
+
+    free(output);
+}
+struct data_output *data_output_kv_create(FILE *file)
+{
+    data_output_kv_t *kv = calloc(1, sizeof(data_output_kv_t));
+    if (!kv) {
+        WARN_CALLOC("data_output_kv_create()");
+        return NULL; // NOTE: returns NULL on alloc failure.
+    }
+
+    kv->output.print_data   = print_kv_data;
+    kv->output.print_array  = print_kv_array;
+    kv->output.print_string = print_kv_string;
+    kv->output.print_double = print_kv_double;
+    kv->output.print_int    = print_kv_int;
+    kv->output.output_flush = print_kv_flush;
+    kv->output.output_free  = data_output_kv_free;
+    kv->file                = file;
+
+    kv->term = term_init(file);
+    kv->color = term_has_color(kv->term);
+
+    kv->ring_bell = 0; // TODO: enable if requested...
+
+    return &kv->output;
+}
+
+/* CSV printer; doesn't really support recursive data objects yet */
+
+typedef struct {
+    struct data_output output;
+    FILE *file;
+    const char **fields;
+    int data_recursion;
+    const char *separator;
+} data_output_csv_t;
+
+static void print_csv_data(data_output_t *output, data_t *data, char const *format)
+{
+    UNUSED(format);
+    data_output_csv_t *csv = (data_output_csv_t *)output;
+
+    const char **fields = csv->fields;
+    int i;
+
+    if (csv->data_recursion)
+        return;
+
+    int regular = 0; // skip "states" output
+    for (data_t *d = data; d; d = d->next) {
+        if (!strcmp(d->key, "msg") || !strcmp(d->key, "codes") || !strcmp(d->key, "model")) {
+            regular = 1;
+            break;
+        }
+    }
+    if (!regular)
+        return;
+
+    ++csv->data_recursion;
+    for (i = 0; fields[i]; ++i) {
+        const char *key = fields[i];
+        data_t *found = NULL;
+        if (i)
+            fprintf(csv->file, "%s", csv->separator);
+        for (data_t *iter = data; !found && iter; iter = iter->next)
+            if (strcmp(iter->key, key) == 0)
+                found = iter;
+
+        if (found)
+            print_value(output, found->type, found->value, found->format);
+    }
+    --csv->data_recursion;
+}
+
+static void print_csv_array(data_output_t *output, data_array_t *array, char const *format)
+{
+    data_output_csv_t *csv = (data_output_csv_t *)output;
+
+    for (int c = 0; c < array->num_values; ++c) {
+        if (c)
+            fprintf(csv->file, ";");
+        print_array_value(output, array, format, c);
+    }
+}
+
+static void print_csv_string(data_output_t *output, const char *str, char const *format)
+{
+    UNUSED(format);
+    data_output_csv_t *csv = (data_output_csv_t *)output;
+
+    while (*str) {
+        if (strncmp(str, csv->separator, strlen(csv->separator)) == 0)
+            fputc('\\', csv->file);
+        fputc(*str, csv->file);
+        ++str;
+    }
+}
+
+static int compare_strings(const void *a, const void *b)
+{
+    return strcmp(*(char **)a, *(char **)b);
+}
+
+static void data_output_csv_start(struct data_output *output, char const *const *fields, int num_fields)
+{
+    data_output_csv_t *csv = (data_output_csv_t *)output;
+
+    int csv_fields = 0;
+    int i, j;
+    const char **allowed = NULL;
+    int *use_count = NULL;
+    int num_unique_fields;
+    if (!csv)
+        goto alloc_error;
+
+    csv->separator = ",";
+
+    allowed = calloc(num_fields, sizeof(const char *));
+    if (!allowed) {
+        WARN_CALLOC("data_output_csv_start()");
+        goto alloc_error;
+    }
+    memcpy((void *)allowed, fields, sizeof(const char *) * num_fields);
+
+    qsort((void *)allowed, num_fields, sizeof(char *), compare_strings);
+
+    // overwrite duplicates
+    i = 0;
+    j = 0;
+    while (j < num_fields) {
+        while (j > 0 && j < num_fields &&
+                strcmp(allowed[j - 1], allowed[j]) == 0)
+            ++j;
+
+        if (j < num_fields) {
+            allowed[i] = allowed[j];
+            ++i;
+            ++j;
+        }
+    }
+    num_unique_fields = i;
+
+    csv->fields = calloc(num_unique_fields + 1, sizeof(const char *));
+    if (!csv->fields) {
+        WARN_CALLOC("data_output_csv_start()");
+        goto alloc_error;
+    }
+
+    use_count = calloc(num_unique_fields + 1, sizeof(*use_count)); // '+ 1' so we never alloc size 0
+    if (!use_count) {
+        WARN_CALLOC("data_output_csv_start()");
+        goto alloc_error;
+    }
+
+    for (i = 0; i < num_fields; ++i) {
+        const char **field = bsearch(&fields[i], allowed, num_unique_fields, sizeof(const char *),
+                compare_strings);
+        int *field_use_count = use_count + (field - allowed);
+        if (field && !*field_use_count) {
+            csv->fields[csv_fields] = fields[i];
+            ++csv_fields;
+            ++*field_use_count;
+        }
+    }
+    csv->fields[csv_fields] = NULL;
+    free((void *)allowed);
+    free(use_count);
+
+    // Output the CSV header
+    for (i = 0; csv->fields[i]; ++i) {
+        fprintf(csv->file, "%s%s", i > 0 ? csv->separator : "", csv->fields[i]);
+    }
+    fprintf(csv->file, "\n");
+    return;
+
+alloc_error:
+    free(use_count);
+    free((void *)allowed);
+    if (csv)
+        free((void *)csv->fields);
+    free(csv);
+}
+
+static void print_csv_double(data_output_t *output, double data, char const *format)
+{
+    UNUSED(format);
+    data_output_csv_t *csv = (data_output_csv_t *)output;
+
+    fprintf(csv->file, "%.3f", data);
+}
+
+static void print_csv_int(data_output_t *output, int data, char const *format)
+{
+    UNUSED(format);
+    data_output_csv_t *csv = (data_output_csv_t *)output;
+
+    fprintf(csv->file, "%d", data);
+}
+
+static void print_csv_flush(data_output_t *output)
+{
+    data_output_csv_t *csv = (data_output_csv_t *)output;
+
+    if (csv && csv->file) {
+        fputc('\n', csv->file);
+        fflush(csv->file);
+    }
+}
+
+static void data_output_csv_free(data_output_t *output)
+{
+    data_output_csv_t *csv = (data_output_csv_t *)output;
+
+    free((void *)csv->fields);
+    free(csv);
+}
+
+struct data_output *data_output_csv_create(FILE *file)
+{
+    data_output_csv_t *csv = calloc(1, sizeof(data_output_csv_t));
+    if (!csv) {
+        WARN_CALLOC("data_output_csv_create()");
+        return NULL; // NOTE: returns NULL on alloc failure.
+    }
+
+    csv->output.print_data   = print_csv_data;
+    csv->output.print_array  = print_csv_array;
+    csv->output.print_string = print_csv_string;
+    csv->output.print_double = print_csv_double;
+    csv->output.print_int    = print_csv_int;
+    csv->output.output_start = data_output_csv_start;
+    csv->output.output_flush = print_csv_flush;
+    csv->output.output_free  = data_output_csv_free;
+    csv->file                = file;
+
+    return &csv->output;
+}
+
+/* Datagram (UDP) client */
+
+typedef struct {
+    struct sockaddr_storage addr;
+    socklen_t addr_len;
+    SOCKET sock;
+} datagram_client_t;
+
+static int datagram_client_open(datagram_client_t *client, const char *host, const char *port)
+{
+    if (!host || !port)
+        return -1;
+
+    struct addrinfo hints, *res, *res0;
+    int    error;
+    SOCKET sock;
+
+    memset(&hints, 0, sizeof(hints));
+    hints.ai_family = PF_UNSPEC;
+    hints.ai_socktype = SOCK_DGRAM;
+    hints.ai_flags = AI_ADDRCONFIG;
+    error = getaddrinfo(host, port, &hints, &res0);
+    if (error) {
+        fprintf(stderr, "%s\n", gai_strerror(error));
+        return -1;
+    }
+    sock = INVALID_SOCKET;
+    for (res = res0; res; res = res->ai_next) {
+        sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
+        if (sock >= 0) {
+            client->sock = sock;
+            memset(&client->addr, 0, sizeof(client->addr));
+            memcpy(&client->addr, res->ai_addr, res->ai_addrlen);
+            client->addr_len = res->ai_addrlen;
+            break; // success
+        }
+    }
+    freeaddrinfo(res0);
+    if (sock == INVALID_SOCKET) {
+        perror("socket");
+        return -1;
+    }
+
+    //int broadcast = 1;
+    //int ret = setsockopt(client->sock, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));
+
+    return 0;
+}
+
+static void datagram_client_close(datagram_client_t *client)
+{
+    if (!client)
+        return;
+
+    if (client->sock != INVALID_SOCKET) {
+        closesocket(client->sock);
+        client->sock = INVALID_SOCKET;
+    }
+
+#ifdef _WIN32
+    WSACleanup();
+#endif
+}
+
+static void datagram_client_send(datagram_client_t *client, const char *message, size_t message_len)
+{
+    int r =  sendto(client->sock, message, message_len, 0, (struct sockaddr *)&client->addr, client->addr_len);
+    if (r == -1) {
+        perror("sendto");
+    }
+}
+
+/* Syslog UDP printer, RFC 5424 (IETF-syslog protocol) */
+
+typedef struct {
+    struct data_output output;
+    datagram_client_t client;
+    int pri;
+    char hostname[_POSIX_HOST_NAME_MAX + 1];
+} data_output_syslog_t;
+
+static void print_syslog_data(data_output_t *output, data_t *data, char const *format)
+{
+    UNUSED(format);
+    data_output_syslog_t *syslog = (data_output_syslog_t *)output;
+
+    // we expect a normal message around 500 bytes
+    // full stats report would be 12k and we want a max of MTU anyway
+    char message[1024];
+    abuf_t msg = {0};
+    abuf_init(&msg, message, sizeof(message));
+
+    time_t now;
+    struct tm tm_info;
+    time(&now);
+#ifdef _WIN32
+    gmtime_s(&tm_info, &now);
+#else
+    gmtime_r(&now, &tm_info);
+#endif
+    char timestamp[21];
+    strftime(timestamp, 21, "%Y-%m-%dT%H:%M:%SZ", &tm_info);
+
+    abuf_printf(&msg, "<%d>1 %s %s rtl_433 - - - ", syslog->pri, timestamp, syslog->hostname);
+
+    msg.tail += data_print_jsons(data, msg.tail, msg.left);
+    if (msg.tail >= msg.head + sizeof(message))
+        return; // abort on overflow, we don't actually want to send more than fits the MTU
+
+    size_t abuf_len = msg.tail - msg.head;
+    datagram_client_send(&syslog->client, message, abuf_len);
+}
+
+static void data_output_syslog_free(data_output_t *output)
+{
+    data_output_syslog_t *syslog = (data_output_syslog_t *)output;
+
+    if (!syslog)
+        return;
+
+    datagram_client_close(&syslog->client);
+
+    free(syslog);
+}
+
+struct data_output *data_output_syslog_create(const char *host, const char *port)
+{
+    data_output_syslog_t *syslog = calloc(1, sizeof(data_output_syslog_t));
+    if (!syslog) {
+        WARN_CALLOC("data_output_syslog_create()");
+        return NULL; // NOTE: returns NULL on alloc failure.
+    }
+#ifdef _WIN32
+    WSADATA wsa;
+
+    if (WSAStartup(MAKEWORD(2,2),&wsa) != 0) {
+        perror("WSAStartup()");
+        free(syslog);
+        return NULL;
+    }
+#endif
+
+    syslog->output.print_data   = print_syslog_data;
+    syslog->output.output_free  = data_output_syslog_free;
+    // Severity 5 "Notice", Facility 20 "local use 4"
+    syslog->pri = 20 * 8 + 5;
+    #ifdef ESP32
+    const char* adapter_hostname = NULL;
+    tcpip_adapter_get_hostname(TCPIP_ADAPTER_IF_STA, &adapter_hostname);
+    if (adapter_hostname) {
+        memcpy(syslog->hostname, adapter_hostname, _POSIX_HOST_NAME_MAX);
+    }
+    else {
+        syslog->hostname[0] = '\0';
+    }
+    #else
+    gethostname(syslog->hostname, _POSIX_HOST_NAME_MAX + 1);
+    #endif
+    syslog->hostname[_POSIX_HOST_NAME_MAX] = '\0';
+    datagram_client_open(&syslog->client, host, port);
+
+    return &syslog->output;
+}
diff --git a/src/r_api.c b/src/r_api.c
index 9c8be859..40fb5ad7 100644
--- a/src/r_api.c
+++ b/src/r_api.c
@@ -29,6 +29,7 @@
 #include "data_tag.h"
 #include "list.h"
 #include "optparse.h"
+#include "output_file.h"
 #include "output_mqtt.h"
 #include "output_influx.h"
 #include "write_sigrok.h"
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index fb43c898..aaae3d49 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -1,7 +1,7 @@
 ########################################################################
 # Compile test cases
 ########################################################################
-add_executable(data-test data-test.c)
+add_executable(data-test data-test.c ../src/output_file.c)
 
 target_link_libraries(data-test data)
 
diff --git a/tests/data-test.c b/tests/data-test.c
index eed82ea4..6c2c9623 100644
--- a/tests/data-test.c
+++ b/tests/data-test.c
@@ -21,6 +21,7 @@
 #include <stdio.h>
 
 #include "data.h"
+#include "output_file.h"
 
 int main()
 {
diff --git a/vs15/rtl_433.vcxproj b/vs15/rtl_433.vcxproj
index e0bac55a..5b0800f6 100644
--- a/vs15/rtl_433.vcxproj
+++ b/vs15/rtl_433.vcxproj
@@ -112,6 +112,7 @@ COPY ..\..\libusb\MS64\dll\libusb*.dll $(TargetDir)</Command>
     <ClInclude Include="..\include\list.h" />
     <ClInclude Include="..\include\mongoose.h" />
     <ClInclude Include="..\include\optparse.h" />
+    <ClInclude Include="..\include\output_file.h" />
     <ClInclude Include="..\include\output_influx.h" />
     <ClInclude Include="..\include\output_mqtt.h" />
     <ClInclude Include="..\include\pulse_analyzer.h" />
@@ -150,6 +151,7 @@ COPY ..\..\libusb\MS64\dll\libusb*.dll $(TargetDir)</Command>
     <ClCompile Include="..\src\list.c" />
     <ClCompile Include="..\src\mongoose.c" />
     <ClCompile Include="..\src\optparse.c" />
+    <ClCompile Include="..\src\output_file.c" />
     <ClCompile Include="..\src\output_influx.c" />
     <ClCompile Include="..\src\output_mqtt.c" />
     <ClCompile Include="..\src\pulse_analyzer.c" />
diff --git a/vs15/rtl_433.vcxproj.filters b/vs15/rtl_433.vcxproj.filters
index 28ef1703..7f7d12fd 100644
--- a/vs15/rtl_433.vcxproj.filters
+++ b/vs15/rtl_433.vcxproj.filters
@@ -74,6 +74,9 @@
     <ClInclude Include="..\include\optparse.h">
       <Filter>Header Files</Filter>
     </ClInclude>
+    <ClInclude Include="..\include\output_file.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
     <ClInclude Include="..\include\output_influx.h">
       <Filter>Header Files</Filter>
     </ClInclude>
@@ -184,6 +187,9 @@
     <ClCompile Include="..\src\optparse.c">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\src\output_file.c">
+      <Filter>Source Files</Filter>
+    </ClCompile>
     <ClCompile Include="..\src\output_influx.c">
       <Filter>Source Files</Filter>
     </ClCompile>