ZVBI Library  0.2.35
examples/pdc2.c
/*
 *  libzvbi VPS/PDC example 2
 *
 *  Copyright (C) 2009 Michael H. Schimek
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions
 *  are met:
 *  1. Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *  2. 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
 *  OWNER 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.
 */

/* $Id: pdc2.c,v 1.1 2009/03/23 01:30:39 mschimek Exp $ */

/* This example shows how to receive and decode VPS/PDC Program IDs.
   For simplicity channel change functions have been omitted and not
   all PDC features are supported. (A more complete example will be
   added later.)

   To compile this program type:
   gcc -o pdc2 pdc2.c `pkg-config zvbi-0.2 --cflags --libs`

   This program expects the starting date and time, ending time
   and VPS/PDC time of a TV program to record as arguments:
   ./pdc2  YYYY-MM-DD HH:MM  HH:MM  HH:MM

   It opens a V4L2 device at /dev/vbi and scans the currently tuned in
   channel for a matching VPS/PDC label, logging the progress on
   standard output, without actually recording anything.

   The -t option enables a test mode where the program reads VPS/PDC
   signal changes from standard input instead of opening a VBI
   device. See parse_test_file_line() for a description of the file
   format.
*/

#define _GNU_SOURCE 1
#undef NDEBUG

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <float.h>
#include <math.h>
#include <time.h>
#include <locale.h>
#include <ctype.h>
#include <unistd.h>
#include <getopt.h>
#include <limits.h>
#include <errno.h>
#include <assert.h>

#include <libzvbi.h>

#ifndef N_ELEMENTS
#  define N_ELEMENTS(array) (sizeof (array) / sizeof (*(array)))
#endif
#ifndef MIN
#  define MIN(x, y) ((x) < (y) ? (x) : (y))
#endif

static vbi_capture *            cap;
static vbi_decoder *            dec;
static const char *             dev_name;
static vbi_bool                 quit;
static int                      exit_code;

/* The current time of the intended audience of the tuned in network
   according to the network (see VBI_EVENT_LOCAL_TIME). It may differ
   from system time if the system is not in sync with UTC or if we
   receive the TV signal with a delay. */
static time_t                   audience_time;

/* The system time in seconds when the most recent PDC signal was
   received. */
static double                   timestamp;

/* PDC Label Channel state. */
struct lc_state {
        /* The PIL most recently received on this LC, zero if none. */
        vbi_pil                         pil;

        /* The system time in seconds when the PIL was most recently
           received. */
        double                          last_at;
};

/* The most recently received PILs. */
static struct lc_state          lc_state[VBI_MAX_PID_CHANNELS];

/* Video recorder states. */
enum vcr_state {
        /* All capturing stopped. */
        VCR_STATE_STBY,

        /* Searching for a PDC signal. */
        VCR_STATE_SCAN,

        /* Preparing to record. */
        VCR_STATE_PTR,

        /* Recording a program. */
        VCR_STATE_REC
};

/* The current video recorder state. */
static enum vcr_state           vcr_state;

/* The system time in seconds at the last change of vcr_state. */
static double                   vcr_state_since;

/* In timer control mode we start and stop recording at the scheduled
   times. Timer control mode is enabled when the network does not
   transmit program IDs or when we lost all PDC signals. */
static vbi_bool                 timer_control_mode;

/* In VCR_STATE_REC this variable stops recording with a 30 second
   delay as required by EN 300 231. This is a system time in
   seconds, or DBL_MAX if no stop is planned. */
static double                   delayed_stop_at;

/* In VCR_REC_STATE if delayed_stop_at < DBL_MAX, delayed_stop_pid
   contains a copy of the program ID which caused the delayed stop.

   If delayed_stop_pid.luf == 1 the program will continue on the
   channel with delayed_stop_pid.cni, accompanied by
   delayed_stop_pid.pil (which may also provide a new start date and
   time for the schedule).

   Otherwise delayed_stop_pid.pil can be a valid PIL, a RI/T or INT
   service code, or zero if a loss of the PDC signal or service caused
   the delayed stop. */
static vbi_program_id           delayed_stop_pid;

/* A program to be recorded. */
struct program {
        struct program *        next;

        /* A number in lieu of a title. */
        unsigned int            index;

        /* The most recently announced start and end time of the
           program ("AT-1" in EN 300 231 in parlance), in case we do
           not receive a PDC signal. When the duration of the program
           is unknown start_time == end_time. end_time is
           exclusive. */
        time_t                  start_time;
        time_t                  end_time;

        /* The expected Program Identification Label. Usually this is
           the originally announced start date and time of the program
           ("AT-2" in EN 300 231), relative to the time zone of the
           intended audience. */
        vbi_pil                 pil;

        /* The validity window of pil, that is the time when the
           network can be expected to transmit the PIL. Usually from
           00:00 on the same day to 04:00 on the next
           day. pil_valid_end is exclusive. */
        time_t                  pil_valid_start;
        time_t                  pil_valid_end;

        /* Recording is in progress or was interrupted. */
        vbi_bool                continues;
};

/* The recording schedule, a singly-linked list of program
   structures. */
static struct program *         schedule;

/* In VCR_STATE_PTR and VCR_STATE_REC the program we (are about to)
   record, a pointer into the schedule list. Otherwise NULL. */
static struct program *         curr_program;

/* If curr_program != NULL this variable contains a copy of the
   program ID which put us into PTR or REC state. If recording was
   started by the timer curr_pid.pil is zero. */
static vbi_program_id           curr_pid;

static vbi_bool                 test_mode;

/* In test mode this is the expected VCR state after the most recent
   PDC signal change. */
static enum vcr_state           test_exp_vcr_state;

static const double
signal_timeout [VBI_MAX_PID_CHANNELS] = {
        [VBI_PID_CHANNEL_LCI_0] = 2,
        [VBI_PID_CHANNEL_LCI_1] = 2,
        [VBI_PID_CHANNEL_LCI_2] = 2,
        [VBI_PID_CHANNEL_LCI_3] = 2,

        /* VPS signals have no error protection. When the payload
           changes, libzvbi will wait for one repetition to confirm
           correct reception. */
        [VBI_PID_CHANNEL_VPS] = 3 / 25.0,

        /* Other channels not implemented yet. */
};

static const double
signal_period [VBI_MAX_PID_CHANNELS] = {
        /* EN 300 231 Section 8.3: "In the case of the packet 8/30
           version (Method B) the repetition rate of labels in any
           label data channel is once per second." Section E.2: "Where
           more than one label channel is in use the signalling rate
           is normally one line per label channel per second." */
        [VBI_PID_CHANNEL_LCI_0] = 1,
        [VBI_PID_CHANNEL_LCI_1] = 1,
        [VBI_PID_CHANNEL_LCI_2] = 1,
        [VBI_PID_CHANNEL_LCI_3] = 1,

        [VBI_PID_CHANNEL_VPS] = 1 / 25.0,

        /* Other channels not implemented yet. */
};

/* For debugging. */
#define D printf ("%s:%u\n", __FILE__, __LINE__)

/* For debugging. */
static void
print_time                      (time_t                 time)
{
        char buffer[80];
        struct tm tm;

        memset (&tm, 0, sizeof (tm));
        localtime_r (&time, &tm);
        strftime (buffer, sizeof (buffer),
                  "%Y-%m-%d %H:%M:%S %Z = ", &tm);
        fputs (buffer, stdout);

        memset (&tm, 0, sizeof (tm));
        gmtime_r (&time, &tm);
        strftime (buffer, sizeof (buffer),
                  "%Y-%m-%d %H:%M:%S UTC", &tm);
        puts (buffer);
}

/* Attention! This function returns a static string. */
static const char *
pil_str                         (vbi_pil                pil)
{
        static char buffer[32];

        switch (pil) {
        case VBI_PIL_TIMER_CONTROL:     return "TC";
        case VBI_PIL_INHIBIT_TERMINATE: return "RI/T";
        case VBI_PIL_INTERRUPTION:      return "INT";
        case VBI_PIL_CONTINUE:          return "CONT";

        case VBI_PIL_NSPV:
                /* NSVP service code if source is VPS/PDC,
                   END code if source is XDS. */
                return "NSPV/END";

        default:
                snprintf (buffer, sizeof (buffer),
                          "%02u%02uT%02u%02u",
                          VBI_PIL_MONTH (pil),
                          VBI_PIL_DAY (pil),
                          VBI_PIL_HOUR (pil),
                          VBI_PIL_MINUTE (pil));

                return buffer;
        }
}

static void
msg                             (const char *           templ,
                                 ...)
{
        va_list ap;

        va_start (ap, templ);

        if (test_mode) {
                char buffer[80];
                struct tm tm;

                memset (&tm, 0, sizeof (tm));
                localtime_r (&audience_time, &tm);
                strftime (buffer, sizeof (buffer), "%Y%m%dT%H%M%S ", &tm);
                fputs (buffer, stdout);
        }

        vprintf (templ, ap);

        va_end (ap);
}

static void
remove_program_from_schedule    (struct program *       p)
{
        struct program **pp;

        if (p == curr_program) {
                assert (quit
                        || VCR_STATE_STBY == vcr_state
                        || VCR_STATE_SCAN == vcr_state);

                curr_program = NULL;
        }

        for (pp = &schedule; NULL != *pp; pp = &(*pp)->next) {
                if (*pp == p) {
                        *pp = p->next;
                        free (p);
                        break;
                }
        }
}

static void
remove_stale_programs_from_schedule (void)
{
        struct program *p;
        struct program *p_next;

        for (p = schedule; NULL != p; p = p_next) {
                p_next = p->next;

                if (audience_time >= p->end_time
                    && audience_time >= p->pil_valid_end) {
                        msg ("PIL %s no longer valid, "
                             "removing program %u from schedule.\n",
                             pil_str (p->pil), p->index);
                        remove_program_from_schedule (p);
                }
        }
}

static struct program *
find_program_by_pil             (vbi_pil                pil)
{
        struct program *p;

        for (p = schedule; NULL != p; p = p->next) {
                if (pil == p->pil)
                        return p;
        }

        return NULL;
}

static const char *
vcr_state_name                  (enum vcr_state         state)
{
        switch (state) {
#define CASE(x) case VCR_STATE_ ## x: return #x;
        CASE (STBY)
        CASE (SCAN)
        CASE (PTR)
        CASE (REC)
#undef CASE
        }

        assert (0);
}

static void
change_vcr_state                (enum vcr_state         new_state)
{
        if (new_state == vcr_state)
                return;

        msg ("VCR state %s -> %s.\n",
             vcr_state_name (vcr_state),
             vcr_state_name (new_state));

        vcr_state = new_state;
        vcr_state_since = timestamp;
}

static vbi_bool
teletext_8302_available         (void)
{
        return (0 != (lc_state[VBI_PID_CHANNEL_LCI_0].pil |
                      lc_state[VBI_PID_CHANNEL_LCI_1].pil |
                      lc_state[VBI_PID_CHANNEL_LCI_2].pil |
                      lc_state[VBI_PID_CHANNEL_LCI_3].pil));
}

static void
disable_timer_control           (void)
{
        if (!timer_control_mode)
                return;
        msg ("Leaving timer control mode.\n");
        timer_control_mode = FALSE;
}

static void
enable_timer_control            (void)
{
        if (timer_control_mode)
                return;
        msg ("Entering timer control mode.\n");
        timer_control_mode = TRUE;
}

static void
stop_recording_now              (void)
{
        assert (VCR_STATE_REC == vcr_state);

        msg ("Program %u ended according to %s%s.\n",
             curr_program->index,
             timer_control_mode ? "schedule" : "VPS/PDC signal",
             (delayed_stop_at < DBL_MAX) ? " with delay" : "");

        change_vcr_state (VCR_STATE_SCAN);

        delayed_stop_at = DBL_MAX;
}

static void
stop_recording_in_30s           (const vbi_program_id * pid)
{
        assert (VCR_STATE_REC == vcr_state);

        /* What triggered the stop. */
        if (NULL == pid) {
                /* Signal lost. */
                memset (&delayed_stop_pid, 0,
                        sizeof (delayed_stop_pid));
        } else {
                delayed_stop_pid = *pid;
        }

        /* If we stop because the PIL is no longer transmitted we may
           need one second to realize (e.g. receiving LCI 0 at time t,
           LCI 1 at t + 0.2, then LCI 0 at t + 1, and again LCI 0 at
           t + 2 seconds) so we start counting 30 seconds not from
           the current time (t + 2) but the first time the label was
           missing (t + 1). */
        if (NULL == pid && 0 != curr_pid.pil) {
                delayed_stop_at = lc_state[curr_pid.channel].last_at + 31;
        } else {
                delayed_stop_at = timestamp + 30;
        }

        msg ("Will stop recording in %d seconds.\n",
             (int)(delayed_stop_at - timestamp));
}

static void
start_recording_by_pil          (struct program *       p,
                                 const vbi_program_id * pid)
{
        assert (!timer_control_mode);
        assert (VCR_STATE_SCAN == vcr_state
                || VCR_STATE_PTR == vcr_state);

        msg ("Recording program %u using VPS/PDC signal.\n",
             p->index);

        /* EN 300 231 Section 9.4.1: "[When] labels are not received
           correctly during a recording, the recording will be
           continued for the computed duration following the actual
           start time" */
        if (!p->continues) {
                p->end_time += audience_time - p->start_time;
                p->start_time = audience_time;
                p->continues = TRUE;
        }

        change_vcr_state (VCR_STATE_REC);

        curr_program = p;
        curr_pid = *pid;
}

static void
prepare_to_record_by_pil        (struct program *       p,
                                 const vbi_program_id * pid)
{
        assert (!timer_control_mode);
        assert (VCR_STATE_SCAN == vcr_state);

        change_vcr_state (VCR_STATE_PTR);

        curr_program = p;
        curr_pid = *pid;
}

static void
start_recording_by_timer        (struct program *       p)
{
        assert (timer_control_mode);
        assert (VCR_STATE_SCAN == vcr_state);

        msg ("Recording program %u using timer.\n",
             p->index);

        change_vcr_state (VCR_STATE_REC);

        curr_program = p;
        memset (&curr_pid, 0, sizeof (curr_pid));
}

static void
remove_program_if_ended         (struct program *       p,
                                 const vbi_program_id * pid)
{
        if (timer_control_mode) {
                /* We don't know if the program really ends now, so we
                   keep it scheduled until curr_program->pil_valid_end
                   in case we receive its PIL after all. */
                return;
        } else if (NULL != pid && VBI_PIL_INTERRUPTION == pid->pil) {
                /* The program pauses, will not be removed. */
                return;
        } else if (NULL != pid && pid->luf) {
                /* The program has been rescheduled to another date,
                   we don't care in this example. */
        }

        /* Objective accomplished. */
        remove_program_from_schedule (p);
}

static void
signal_or_service_lost          (void)
{
        struct program *p;

        if (timer_control_mode)
                return;

        enable_timer_control ();

        switch (vcr_state) {
        case VCR_STATE_STBY:
                assert (0);

        case VCR_STATE_SCAN:
                break;

        case VCR_STATE_PTR:
                p = curr_program;

                /* According to EN 300 231 Section E.1 and Section E.3
                   Example 12 the program should begin within one
                   minute when PRF=1, so we start recording now. We
                   will stop by PIL if we pick up a VPS or Teletext
                   signal again before curr_program->end_time, but we
                   will not return to VCR_STATE_PTR if PRF is still
                   1. */
                msg ("Recording program %u using lost "
                     "PDC signal with PRF=1.\n",
                     p->index);

                /* Record for the scheduled duration... */
                p->end_time = p->end_time - p->start_time + audience_time;
                /* ...plus one minute since PRF was set. */
                p->end_time += 60 - MIN (vcr_state_since - timestamp,
                                         60.0);
                p->start_time = audience_time;

                change_vcr_state (VCR_STATE_REC);

                /* Now recording by timer. */
                memset (&curr_pid, 0, sizeof (curr_pid));

                break;

        case VCR_STATE_REC:
                if (delayed_stop_at < DBL_MAX) {
                        msg ("PDC signal lost; already stopping in "
                             "%d seconds.\n",
                             (int)(delayed_stop_at - timestamp));
                } else if (curr_program->start_time
                           == curr_program->end_time) {
                        /* Since we don't know the program duration,
                           we cannot record under timer control. We
                           stop recording in 30 seconds as shown in EN
                           300 231 Annex E.3, Example 11, 16:20:10,
                           but with an extra twist: If we receive
                           curr_program->pil again within those 30
                           seconds the stop will be canceled. */
                        stop_recording_in_30s (/* pid */ NULL);
                } else {
                        /* Keep recording by timer. */
                        memset (&curr_pid, 0, sizeof (curr_pid));
                }

                break;
        }
}

static void
pil_no_longer_transmitted       (const vbi_program_id * pid)
{
        vbi_bool mi;

        switch (vcr_state) {
        case VCR_STATE_STBY:
        case VCR_STATE_SCAN:
                assert (0);

        case VCR_STATE_PTR:
                assert (!timer_control_mode);

                msg ("PIL %s is no longer present on LC %u.\n",
                     pil_str (curr_program->pil),
                     curr_pid.channel);

                change_vcr_state (VCR_STATE_SCAN);

                return;

        case VCR_STATE_REC:
                assert (!timer_control_mode);

                msg ("PIL %s is no longer present on LC %u.\n",
                     pil_str (curr_program->pil),
                     curr_pid.channel);

                if (delayed_stop_at < DBL_MAX) {
                        msg ("Already stopping in %d seconds.\n",
                             (int)(delayed_stop_at - timestamp));
                        return;
                }

                break;
        }

        if (NULL != pid
            /* EN 300 231 Annex E.3 Example 8. */
            && !pid->luf
            /* EN 300 231 Section 6.2 p) and Annex E.3 Example 7 and
               9. */
            && (VBI_PIL_INTERRUPTION == pid->pil
                || VBI_PIL_INHIBIT_TERMINATE == pid->pil)) {
                mi = pid->mi;
        } else {
                /* EN 300 231 is unclear about the expected response
                   if a PIL with MI = 1 replaces a PIL with MI = 0 or
                   vice versa. Section 6.2 p) suggests that only the
                   MI flag of the old label determines when the
                   program stops and Annex E.3 Example 1 to 7 are
                   consistent with this interpretation, Example 10 is
                   not. */
                if (0 == curr_pid.pil) {
                        /* Recording was started by timer. */
                        mi = TRUE;
                } else {
                        mi = curr_pid.mi;
                }
        }

        if (mi) {
                stop_recording_now ();
                remove_program_if_ended (curr_program, pid);
        } else {
                stop_recording_in_30s (pid);
        }
}

/* Interruption or Recording Inhibit/Terminate service code. */
static void
received_int_rit                (const vbi_program_id * pid)
{
        switch (vcr_state) {
        case VCR_STATE_STBY:
                assert (0);

        case VCR_STATE_SCAN:
                disable_timer_control ();
                return;

        case VCR_STATE_PTR:
                assert (!timer_control_mode);

                if (pid->channel != curr_pid.channel) {
                        msg ("Ignore %s/%02X with different LCI.\n",
                             pil_str (pid->pil), pid->pty);
                        return;
                }

                break;

        case VCR_STATE_REC:
                if (timer_control_mode) {
                        /* Impossible to know if this service code
                           refers to curr_program, so we keep
                           recording for now. */
                        return;
                } else if (pid->channel != curr_pid.channel) {
                        msg ("Ignore %s/%02X with different LCI.\n",
                             pil_str (pid->pil), pid->pty);
                        return;
                }

                break;
        }

        pil_no_longer_transmitted (pid);
}

static void
received_pil                    (const vbi_program_id * pid)
{
        struct program *p;

        switch (vcr_state) {
        case VCR_STATE_STBY:
                assert (0);

        case VCR_STATE_SCAN:
                disable_timer_control ();
                if (pid->luf)
                        return;
                p = find_program_by_pil (pid->pil);
                break;

        case VCR_STATE_PTR:
                assert (!timer_control_mode);

                if (pid->channel != curr_pid.channel) {
                        msg ("Ignore %s/%02X with different LCI.\n",
                             pil_str (pid->pil), pid->pty);
                        return;
                } else if (pid->luf) {
                        pil_no_longer_transmitted (pid);

                        /* This example does not support VCR
                           reprogramming. */
                        return;
                } else if (pid->pil != curr_pid.pil) {
                        pil_no_longer_transmitted (pid);
                        p = find_program_by_pil (pid->pil);
                        break;
                } else if (pid->prf) {
                        if (timestamp >= vcr_state_since + 60) {
                                /* EN 300 231 Section E.1,
                                   Section E.3 Example 12. */
                                msg ("Overriding stuck PRF flag.\n");
                        } else {
                                msg ("Already prepared to record.\n");
                                return;
                        }
                }

                /* PRF 1 -> 0, program starts now. */

                start_recording_by_pil (curr_program, pid);

                return;

        case VCR_STATE_REC:
                if (timer_control_mode) {
                        if (pid->luf) {
                                /* Impossible to know if this service
                                   code refers to curr_program, so we
                                   keep recording for now. */
                                return;
                        }

                        p = find_program_by_pil (pid->pil);
                        if (p == curr_program) {
                                disable_timer_control ();

                                msg ("Continue recording using "
                                     "VPS/PDC signal.\n");

                                curr_pid = *pid;

                                /* Cancel a delayed stop because the
                                   program is evidently still
                                   running. */
                                delayed_stop_at = DBL_MAX;

                                return;
                        } else if (NULL == p) {
                                /* This program is not scheduled for
                                   recording but the network may
                                   transmit other PILs in parallel, so
                                   we allow some time to pick them up
                                   before we stop. */
                                stop_recording_in_30s (/* pil */ NULL);
                                return;
                        } else {
                                disable_timer_control ();

                                /* Perhaps in practice one should just
                                   open a new file and not restart
                                   capturing. */
                                stop_recording_now ();
                        }
                } else if (pid->channel != curr_pid.channel) {
                        msg ("Ignore %s/%02X with different LCI.\n",
                             pil_str (pid->pil), pid->pty);
                        return;
                } else if (pid->luf) {
                        pil_no_longer_transmitted (pid);

                        /* This example does not support VCR
                           reprogramming. */
                        return;
                } else if (pid->pil == curr_pid.pil) {
                        if (delayed_stop_at < DBL_MAX) {
                                /* We lost all PDC signals and
                                   timer_control() arranged for a
                                   delayed stop. Or we received an INT
                                   or RI/T code or a different PIL
                                   than curr_program->pil with
                                   MI=0. But now we receive
                                   curr_program->pil again. */
                                delayed_stop_at = DBL_MAX;
                                msg ("Delayed stop canceled.\n");
                                return;
                        } else {
                                /* We lost all PDC signals and
                                   timer_control() started recording
                                   out of SCAN or PTR state, but now
                                   we receive curr_program->pil
                                   (again). Or this is just a
                                   retransmission of the PIL which
                                   started recording. Either way, we
                                   do not return to VCR_STATE_PTR if
                                   PRF is (still or again) 1. */
                                msg ("Already recording.\n");
                                return;
                        }
                } else {
                        pil_no_longer_transmitted (pid);
                        if (VCR_STATE_SCAN != vcr_state) {
                                /* Stopping later. */
                                return;
                        }

                        p = find_program_by_pil (pid->pil);
                }

                break;
        }

        assert (VCR_STATE_SCAN == vcr_state);

        if (NULL == p)
                return;

        if (pid->prf) {
                prepare_to_record_by_pil (p, pid);
        } else {
                start_recording_by_pil (p, pid);
        }
}

static void
event_handler                   (vbi_event *            ev,
                                 void *                 user_data)
{
        const vbi_program_id *pid;
        vbi_pid_channel lci;

        user_data = user_data; /* unused, no warning please */

        assert (VCR_STATE_STBY != vcr_state);

        pid = ev->ev.prog_id;
        lci = pid->channel;

        switch (lci) {
        case VBI_PID_CHANNEL_LCI_0:
        case VBI_PID_CHANNEL_LCI_1:
        case VBI_PID_CHANNEL_LCI_2:
        case VBI_PID_CHANNEL_LCI_3:
                break;

        case VBI_PID_CHANNEL_VPS:
                /* EN 300 231 Section 9.4.1: "When both line 16 (VPS)
                   and Teletext-delivered labels are available
                   simultaneously, decoders should default to the
                   Teletext-delivered service;" */
                if (teletext_8302_available ())
                        goto finish;
                break;

        default:
                /* Support for other sources not implemented yet. */
                return;
        }

        msg ("Received PIL %s/%02X on LC %u.\n",
             pil_str (pid->pil), pid->pty, lci);

        switch (pid->pil) {
        case VBI_PIL_TIMER_CONTROL:
        case VBI_PIL_CONTINUE:
                signal_or_service_lost ();
                break;

        case VBI_PIL_INTERRUPTION:
        case VBI_PIL_INHIBIT_TERMINATE:
                received_int_rit (pid);
                break;

        default:
                received_pil (pid);
                break;
        }

 finish:
        lc_state[lci].pil = pid->pil;
        lc_state[lci].last_at = timestamp;
}

static vbi_bool
in_pil_validity_window          (void)
{
        struct program *p;

        for (p = schedule; NULL != p; p = p->next) {
                /* The announced start and end time should fall within
                   the PIL validity window, but just in case. */
                if ((audience_time >= p->start_time
                     && audience_time < p->end_time)
                    || (audience_time >= p->pil_valid_start
                        && audience_time < p->pil_valid_end))
                        return TRUE;
        }

        return FALSE;
}

static void
timer_control                   (void)
{
        struct program *p;

        assert (timer_control_mode);

        switch (vcr_state) {
        case VCR_STATE_STBY:
        case VCR_STATE_PTR:
                assert (0);

        case VCR_STATE_SCAN:
                break;

        case VCR_STATE_REC:
                if (delayed_stop_at < DBL_MAX) {
                        /* Will stop later. */
                        return;
                } else if (audience_time >= curr_program->end_time) {
                        stop_recording_now ();

                        /* We remove the program from the schedule as
                           shown in EN 300 231 Annex E.3, Example 11,
                           01:58:00. However as the example itself
                           demonstrates this is not in the best
                           interest of the user. A better idea may be
                           to keep the program scheduled until
                           curr_program->pil_valid_end, in case the
                           program is late or overrunning and we
                           receive its PIL after all. */
                        remove_program_from_schedule (curr_program);
                } else {
                        /* Still running. */
                        return;
                }

                assert (VCR_STATE_SCAN == vcr_state);

                break;
        }

        for (p = schedule; NULL != p; p = p->next) {
                /* Note if no program length has been specified
                   (start_time == end_time) this function will not
                   record the program. */
                /* We must also compare against p->end_time because we
                   will not always remove the program from the
                   schedule at that time. See
                   remove_program_if_ended(). */
                if (audience_time >= p->start_time
                    && audience_time < p->end_time) {
                        start_recording_by_timer (p);
                        return;
                }
        }
}

static void
pdc_signal_check                (void)
{
        static const unsigned int ttx_chs =
                ((1 << VBI_PID_CHANNEL_LCI_0) |
                 (1 << VBI_PID_CHANNEL_LCI_0) |
                 (1 << VBI_PID_CHANNEL_LCI_0) |
                 (1 << VBI_PID_CHANNEL_LCI_0));
        static const unsigned int vps_ch =
                (1 << VBI_PID_CHANNEL_VPS);
        unsigned int active_chs;
        unsigned int lost_chs;
        vbi_pid_channel i;

        if (timer_control_mode)
                return;

        /* Determine if we lost signals. */

        active_chs = 0;
        lost_chs = 0;

        for (i = 0; i < VBI_MAX_PID_CHANNELS; ++i) {
                double timeout_at;

                if (0 == lc_state[i].pil)
                        continue;

                timeout_at = lc_state[i].last_at + signal_timeout[i];
                if (timestamp >= timeout_at) {
                        lost_chs |= 1 << i;
                } else {
                        active_chs |= 1 << i;
                }
        }

        /* For now only Teletext and VPS delivery is supported, so we
           don't check other channels. */

        if (0 == active_chs) {
                if (0 != lost_chs) {
                        msg ("All Teletext and VPS signals lost, "
                             "will fall back to timer control.\n");

                        signal_or_service_lost ();
                }
        } else {
                if (vps_ch == active_chs
                    && 0 != (lost_chs & ttx_chs)) {
                        msg ("Teletext signal lost, "
                             "will fall back to VPS.\n");

                        if (curr_pid.pil
                            == lc_state[VBI_PID_CHANNEL_VPS].pil) {
                                curr_pid.channel = VBI_PID_CHANNEL_VPS;
                        }
                }

                if ((VCR_STATE_PTR == vcr_state
                     || VCR_STATE_REC == vcr_state)
                    && 0 != curr_pid.pil
                    && 0 != (lost_chs & (1 << curr_pid.channel))) {
                        /* Note if multiple label channels are in use
                           (Teletext only) a PIL may just "disappear"
                           without a RI/T service code or other PIL
                           subsequently transmitted on the same
                           channel. */
                        pil_no_longer_transmitted (/* pid */ NULL);
                }
        }

        if (0 != lost_chs) {
                for (i = 0; i < VBI_MAX_PID_CHANNELS; ++i) {
                        if (0 == (lost_chs & (1 << i)))
                                continue;

                        lc_state[i].pil = 0;
                        lc_state[i].last_at = timestamp;
                }
        }
}

static void
parse_test_file_line            (time_t *               timestamp,
                                 vbi_program_id *       pid,
                                 enum vcr_state *       exp_state,
                                 unsigned int           line_counter,
                                 const char *           test_file_line)
{
        struct tm tm;
        const char *s;
        char *s_end;
        const char *detail;
        unsigned long ul;

        /* Test file format (based on examples in EN 300 231):

           One line of text for each PID change, with 4 or 9 fields
           separated by one or more tabs or spaces:

           1. Name of the broadcasting network, e.g. BBC1.
           2. Date of the change: yyyymmddThhmmss (local time)
                              or  yyyymmddThhmmssZ (UTC)
              Lines must be sorted by this date, oldest first. Dates
              must not repeat unless these lines have different LCI
              fields.
           3. Label Channel Identifier (vbi_pid_channel): 0 ... n
              or the name VPS (channel 4).
           4. Label Update Flag: 0 or 1.
           5. Mode Identifier: 0 or 1 or x (any).
           6. Prepare to Record Flag: 0 or 1 or x (any).
           7. Program Identification Label: mmddThhmm or one of the
              names
              - TC (Timer Control code)
              - RI/T (Recording Inhibit/Terminate code)
              - INT (Interruption code)
              - CONT (Continuation code)
              - NSPV (No Specific PIL Value).
              A Program Type can be appended, separated by a slash:
              - /A to /Z (Series Code)
              - /NN (a hex number, e.g. /3F)
           8. Channel or Network Identifier: a name like BBC1.
           9. Expected VCR state:
              - STBY
              - SCAN
              - PTR
              - REC.

           If fields 4 to 8 are omitted the transmission of the label
           on the given label channel ceases. If field 9 is omitted
           the same VCR state as before is expected.

           All text after a number sign (#) is ignored.
        */

        s = test_file_line;

        /* Network name ignored in this example. */
        while (isalnum (*s))
                ++s;
        detail = "channel field";
        if (!isspace (*s))
                goto invalid;

        memset (&tm, 0, sizeof (tm));
        tm.tm_isdst = -1; /* unknown */

        s = strptime (s, "%n%Y%m%dT%H%M%S", &tm);
        detail = "date field";
        if (NULL == s)
                goto invalid;
        while (isspace (*s))
                ++s;
        if ('Z' == *s) {
                ++s;
                *timestamp = timegm (&tm);
        } else {
                *timestamp = mktime (&tm);
        }
        if ((time_t) -1 == *timestamp)
                goto invalid;

        memset (pid, 0, sizeof (*pid));

        while (isspace (*s))
                ++s;
        if (0 == strncmp (s, "VPS", 3)) {
                pid->channel = VBI_PID_CHANNEL_VPS;
                s += 3;
        } else {
                ul = strtoul (s, &s_end, 0);
                detail = "LCI field";
                if (s_end == s
                    || ul >= (unsigned long) VBI_MAX_PID_CHANNELS)
                        goto invalid;
                pid->channel = ul;
                s = s_end;
        }

        while (isspace (*s))
                ++s;
        if (!isdigit (*s)) {
                /* Cease transmission on this label channel,
                   pid->pil = 0. */
        } else {
                ul = strtoul (s, &s_end, 0);
                detail = "LUF field";
                if (s_end == s || ul > 1)
                        goto invalid;
                pid->luf = ul;
                s = s_end;

                while (isspace (*s))
                        ++s;
                if ('x' == *s) {
                        ++s;
                } else {
                        ul = strtoul (s, &s_end, 0);
                        detail = "MI field";
                        if (s_end == s || ul > 1)
                                goto invalid;
                        pid->mi = ul;
                        s = s_end;
                }

                while (isspace (*s))
                        ++s;
                if ('x' == *s) {
                        ++s;
                } else {
                        ul = strtoul (s, &s_end, 0);
                        detail = "PRF field";
                        if (s_end == s || ul > 1)
                                goto invalid;
                        pid->prf = ul;
                        s = s_end;
                }

                while (isspace (*s))
                        ++s;
                if (0 == strncmp (s, "CONT", 4)) {
                        pid->pil = VBI_PIL_CONTINUE;
                        s += 4;
                } else if (0 == strncmp (s, "END", 3)) {
                        pid->pil = VBI_PIL_END;
                        s += 3;
                } else if (0 == strncmp (s, "INT", 3)) {
                        pid->pil = VBI_PIL_INTERRUPTION;
                        s += 3;
                } else if (0 == strncmp (s, "NSPV", 4)) {
                        pid->pil = VBI_PIL_NSPV;
                        s += 4;
                } else if (0 == strncmp (s, "RI/T", 4)) {
                        pid->pil = VBI_PIL_INHIBIT_TERMINATE;
                        s += 4;
                } else if (0 == strncmp (s, "TC", 2)) {
                        pid->pil = VBI_PIL_TIMER_CONTROL;
                        s += 2;
                } else {
                        ul = strtoul (s, &s_end, 10);
                        detail = "PIL field";
                        if (s_end == s
                            || ul % 100 > 31
                            || ul > 1531)
                                goto invalid;
                        s = s_end;
                        if (ul > 0) {
                                pid->pil = VBI_PIL (ul / 100,
                                                    ul % 100, 0, 0);
                                if ('T' != *s++)
                                        goto invalid;
                                ul = strtoul (s, &s_end, 10);
                                if (s_end == s
                                    || ul % 100 > 63
                                    || ul > 3163)
                                        goto invalid;
                                s = s_end;
                                pid->pil |= VBI_PIL (0, 0,
                                                     ul / 100,
                                                     ul % 100);
                        }
                }

                if ('/' == *s) {
                        do ++s;
                        while (isspace (*s));
                        if (isalpha (s[0]) && 0x20 == s[1]) {
                                /* Series code. This isn't magic, EN
                                   300 231 just gives letters instead
                                   of the codes 0x80 ... 0xFF for
                                   easier reading. */
                                pid->pty = 0x80 | *s++;
                        } else {
                                ul = strtoul (s, &s_end, 16);
                                detail = "PTY field";
                                if (s_end == s || ul > 0xFF)
                                        goto invalid;
                                pid->pty = ul;
                                s = s_end;
                        }
                } else {
                        pid->pty = 0;
                }

                /* Network name ignored in this example. */
                while (isspace (*s))
                        ++s;
                while (isalnum (*s))
                        ++s;
                detail = "CNI field";
                if (0 != *s && !isspace (*s))
                        goto invalid;
                if (VBI_PID_CHANNEL_VPS == pid->channel) {
                        pid->cni_type = VBI_CNI_TYPE_VPS;
                        pid->cni = 0x1234;
                } else {
                        pid->cni_type = VBI_CNI_TYPE_8302;
                        pid->cni = 0x1234;
                }
        }

        while (isspace (*s))
                ++s;
        if ('#' == *s || 0 == *s) {
                *exp_state = -1; /* no change */
                return;
        } else if (0 == strncmp (s, "PTR", 3)) {
                *exp_state = VCR_STATE_PTR;
                s += 3;
        } else if (0 == strncmp (s, "REC", 3)) {
                *exp_state = VCR_STATE_REC;
                s += 3;
        } else if (0 == strncmp (s, "SCAN", 4)) {
                *exp_state = VCR_STATE_SCAN;
                s += 4;
        } else if (0 == strncmp (s, "STBY", 4)) {
                *exp_state = VCR_STATE_STBY;
                s += 4;
        } else {
                detail = "VCR state field";
                goto invalid;
        }

        while (isspace (*s))
                ++s;

        if ('#' == *s || 0 == *s)
                return;

        detail = "garbage at end of line";

 invalid:
        fprintf (stderr, "Error in test file line %u, %s:\n%s\n",
                 line_counter, detail, test_file_line);
        exit (EXIT_FAILURE);
}

static void
simulate_signals                (void)
{
        static char buffer[256];
        static vbi_program_id test_pid[VBI_MAX_PID_CHANNELS];
        static vbi_program_id next_pid;
        static time_t next_event_time = 0;
        static enum vcr_state next_exp_vcr_state = (enum vcr_state) -1;
        static unsigned int line_counter;
        vbi_pid_channel i;

        while (timestamp >= next_event_time) {
                if (0 != buffer[0]) {
                        printf ("> %s", buffer);
                        test_pid[next_pid.channel] = next_pid;
                        if ((enum vcr_state) -1 == next_exp_vcr_state)
                                test_exp_vcr_state = test_exp_vcr_state;
                        else
                                test_exp_vcr_state = next_exp_vcr_state;
                }

                for (;;) {
                        const char *s;

                        if (NULL == fgets (buffer, sizeof (buffer),
                                           stdin)) {
                                printf ("End of test file.\n");
                                next_event_time = INT_MAX;
                                quit = TRUE;
                                break;
                        }

                        s = buffer;

                        while (isspace (*s))
                                ++s;
                        if (0 == *s)
                                continue;

                        if ('#' == *s) {
                                printf ("> %s", s);
                                continue;
                        }

                        parse_test_file_line (&next_event_time,
                                              &next_pid,
                                              &next_exp_vcr_state,
                                              line_counter, s);
                        ++line_counter;

                        break;
                }
        }

        /* See standby_loop(). */
        audience_time = (time_t) timestamp;

        /* We stop recording before examining the received PIDs so we
           can respond to a new PID immediately. */
        if (VCR_STATE_REC == vcr_state
            && timestamp >= delayed_stop_at) {
                stop_recording_now ();
                assert (VCR_STATE_SCAN == vcr_state);
                remove_program_if_ended (curr_program,
                                         &delayed_stop_pid);
        }

        /* Note in reality PIDs may arrive in any order, with a delay
           of several frames between them. */
        for (i = 0; i < VBI_MAX_PID_CHANNELS; ++i) {
                if (0 != test_pid[i].pil) {
                        vbi_event ev;

                        memset (&ev, 0, sizeof (ev));
                        ev.ev.prog_id = &test_pid[i];

                        event_handler (&ev, /* user_data */ NULL);
                }
        }
}

static void
capture_and_decode_frame        (void)
{
        struct timeval timeout;
        vbi_capture_buffer *sliced_buffer;
        unsigned int n_lines;
        int r;

        /* Don't wait more than two seconds for the driver
           to return data. */
        timeout.tv_sec = 2;
        timeout.tv_usec = 0;

        r = vbi_capture_pull (cap,
                              /* raw_buffer */ NULL,
                              &sliced_buffer,
                              &timeout);
        switch (r) {
        case -1:
                fprintf (stderr,
                         "VBI read error: %s.\n",
                         strerror (errno));
                /* Could be ignored, esp. EIO from some
                   drivers. */
                exit (EXIT_FAILURE);

        case 0: 
                fprintf (stderr, "VBI read timeout\n");
                exit (EXIT_FAILURE);

        case 1: /* success */
                break;

        default:
                assert (0);
        }

        timestamp = sliced_buffer->timestamp;
        n_lines = sliced_buffer->size / sizeof (vbi_sliced);

        /* See standby_loop(). */
        audience_time = (time_t) timestamp;

        /* We stop recording before examining the received PIDs so we
           can respond to a new PID immediately. */
        if (VCR_STATE_REC == vcr_state
            && timestamp >= delayed_stop_at) {
                stop_recording_now ();
                assert (VCR_STATE_SCAN == vcr_state);
                remove_program_if_ended (curr_program,
                                         &delayed_stop_pid);
        }

        /* Calls event_handler(). */
        vbi_decode (dec, (vbi_sliced *) sliced_buffer->data,
                    n_lines, timestamp);
}

static void
close_vbi_device                (void)
{
        vbi_capture_delete (cap);
        cap = NULL;
}

static void
open_vbi_device                 (void)
{
        vbi_service_set services;
        char *errstr;

        services = (VBI_SLICED_TELETEXT_B |
                    VBI_SLICED_VPS);

        cap = vbi_capture_v4l2_new (dev_name,
                                    /* buffers */ 5,
                                    &services,
                                    /* strict */ 0,
                                    &errstr,
                                    /* verbose */ FALSE);
        if (NULL == cap) {
                fprintf (stderr,
                         "Cannot capture VBI data from %s "
                         "with V4L2 interface:\n"
                         "%s\n",
                         dev_name, errstr);

                free (errstr);

                exit (EXIT_FAILURE);
        }
}

/* We wait in this function until we receive the expected PIL(s) or a
   program starts and ends as scheduled, and record it. */
static void
capture_loop                    (void)
{
        double last_timestamp;

        assert (VCR_STATE_STBY == vcr_state);

        if (!test_mode)
                open_vbi_device ();

        /* Reset the VBI decoder. */
        vbi_channel_switched (dec, 0);

        change_vcr_state (VCR_STATE_SCAN);

        last_timestamp = 0;

        while (VCR_STATE_STBY != vcr_state && !quit) {
                if (test_mode) {
                        simulate_signals ();
                } else {
                        capture_and_decode_frame ();
                }

                /* Once per second is enough. */
                if ((long) last_timestamp != (long) timestamp) {
                        if (!timer_control_mode) {
                                /* May enable timer control mode. */
                                pdc_signal_check ();
                        }

                        if (timer_control_mode)
                                timer_control ();
                }

                last_timestamp = timestamp;

                if (VCR_STATE_SCAN == vcr_state
                    && !in_pil_validity_window ()) {
                        change_vcr_state (VCR_STATE_STBY);
                }

                if (test_mode) {
                        if ((enum vcr_state) -1 != test_exp_vcr_state
                            && test_exp_vcr_state != vcr_state) {
                                printf ("*** Unexpected VCR state %s\n",
                                        vcr_state_name (vcr_state));

                                exit_code = EXIT_FAILURE;
                        }

                        /* Advance by one second. Note a VPS signal is
                           transmitted on each frame, 25 times per
                           second, but we simulate at most one PID
                           change per second per label channel. */
                        ++timestamp;
                }
        }

        if (!test_mode)
                close_vbi_device ();
}

/* We wait in this function until the starting time of the earliest
   program on the recording schedule is approaching. */
static void
standby_loop                    (void)
{
        while (!quit) {
                struct program *p;
                time_t first_scan;

                assert (VCR_STATE_STBY == vcr_state);

                if (test_mode) {
                        /* Simulated current time. */
                        audience_time = (time_t) timestamp;
                } else {
                        /* The current time of the intended audience
                           of the tuned in network according to the
                           network. It may differ from system time if
                           the system is not in sync with UTC or if we
                           receive the TV signal with a delay. For
                           simplicity we will not determine the offset
                           in this example, see VBI_EVENT_LOCAL_TIME
                           if you want to try that. */
                        audience_time = time (NULL);
                }

                remove_stale_programs_from_schedule ();
                if (NULL == schedule) {
                        printf ("Recording schedule is empty.\n");
                        break;
                }

                first_scan = schedule->start_time;
                for (p = schedule; NULL != p; p = p->next) {
                        if (p->start_time < first_scan)
                                first_scan = p->start_time;
                        if (p->pil_valid_start < first_scan)
                                first_scan = p->pil_valid_start;
                }

                while (first_scan > audience_time) {
                        char buffer[80];
                        struct tm tm;

                        memset (&tm, 0, sizeof (tm));
                        localtime_r (&first_scan, &tm);
                        strftime (buffer, sizeof (buffer),
                                  "%Y-%m-%d %H:%M:%S %Z", &tm);

                        msg ("Sleeping until %s.\n", buffer);

                        if (test_mode) {
                                audience_time = first_scan;
                                timestamp = first_scan;
                        } else {
                                /* In a loop because the sleep()
                                   function may abort earlier. */
                                sleep (first_scan - audience_time);

                                audience_time = time (NULL);
                        }
                }

                capture_loop ();
        }
}

static void
reset_state                     (void)
{
        unsigned int i;

        audience_time = 0.0;
        timestamp = 0.0;

        for (i = 0; i < VBI_MAX_PID_CHANNELS; ++i) {
                lc_state[i].pil = 0; /* none received */
                lc_state[i].last_at = 0.0;
        }

        vcr_state = VCR_STATE_STBY;
        vcr_state_since = 0.0;

        timer_control_mode = FALSE;

        delayed_stop_at = DBL_MAX;

        test_exp_vcr_state = (enum vcr_state) -1; /* unknown */
}

static void
add_program_to_schedule         (const struct tm *      start_tm,
                                 const struct tm *      end_tm,
                                 const struct tm *      pdc_tm)
{
        struct program *p;
        struct program **pp;
        struct tm tm;
        time_t pil_time;

        /* Note PILs represent the originally announced start date of
           the program in the time zone of the intended audience. When
           we convert pdc_tm to a PIL we assume that zone is the same
           as the system time zone (TZ environment variable), and
           start_tm, end_tm and pdc_tm are also given relative to this
           time zone. We do not consider the case where a program
           straddles a daylight saving time discontinuity, e.g. starts
           in the CET zone and ends in the CEST zone. */

        p = calloc (1, sizeof (*p));
        assert (NULL != p);

        tm = *start_tm;
        tm.tm_isdst = -1; /* unknown */
        p->start_time = mktime (&tm);
        if ((time_t) -1 == p->start_time) {
                fprintf (stderr, "Invalid start time.\n");
                exit (EXIT_FAILURE);
        }

        tm = *start_tm;
        tm.tm_isdst = -1; /* unknown */
        tm.tm_hour = end_tm->tm_hour;
        tm.tm_min = end_tm->tm_min;
        if (end_tm->tm_hour < start_tm->tm_hour) {
                /* mktime() should handle a 32nd. */
                ++tm.tm_mday;
        }
        p->end_time = mktime (&tm);
        if ((time_t) -1 == p->end_time) {
                fprintf (stderr, "Invalid end time.\n");
                exit (EXIT_FAILURE);
        }

        tm = *start_tm;
        tm.tm_isdst = -1; /* unknown */
        tm.tm_hour = pdc_tm->tm_hour;
        tm.tm_min = pdc_tm->tm_min;
        if (pdc_tm->tm_hour >= start_tm->tm_hour + 12) {
                /* mktime() should handle a 0th. */
                --tm.tm_mday;
        } else if (pdc_tm->tm_hour + 12 < start_tm->tm_hour) {
                ++tm.tm_mday;
        }

        /* Normalize day and month. */
        pil_time = mktime (&tm);
        if ((time_t) -1 == pil_time
            || NULL == localtime_r (&pil_time, &tm)) {
                fprintf (stderr, "Cannot determine PIL month/day.\n");
                exit (EXIT_FAILURE);
        }

        p->pil = VBI_PIL (tm.tm_mon + 1, /* 1 ... 12 */
                          tm.tm_mday,
                          tm.tm_hour,
                          tm.tm_min);

        if (!vbi_pil_validity_window (&p->pil_valid_start,
                                      &p->pil_valid_end,
                                      p->pil,
                                      p->start_time,
                                      NULL /* system tz */)) {
                fprintf (stderr, "Cannot determine PIL validity.\n");
                exit (EXIT_FAILURE);
        }

        p->index = 0;
        for (pp = &schedule; NULL != *pp; pp = &(*pp)->next)
                ++p->index;

        *pp = p;

        if (0) {
                printf ("Program %u start: ", p->index);
                print_time (p->start_time);
                printf ("End:              ");
                print_time (p->end_time);
                printf ("PIL:              ");
                print_time (pil_time);
                printf ("PIL valid from:   ");
                print_time (p->pil_valid_start);
                printf ("PIL valid until:  ");
                print_time (p->pil_valid_end);
        }
}

static void
usage                           (FILE *                 fp)
{
        fprintf (fp,
"Please specify the start time of a program in the format\n"
"YYYY-MM-DD HH:MM, the end time HH:MM and a VPS/PDC time HH:MM.\n");
}

static void
parse_args                      (int                    argc,
                                 char **                argv)
{
        struct tm start_tm;
        struct tm end_tm;
        struct tm pdc_tm;

        dev_name = "/dev/vbi";

        for (;;) {
                int c;

                c = getopt (argc, argv, "d:ht");

                if (-1 == c)
                        break;

                switch (c) {
                case 'd':
                        dev_name = optarg;
                        break;

                case 'h':
                        usage (stdout);
                        exit (EXIT_SUCCESS);

                case 't':
                        test_mode = TRUE;
                        break;

                default:
                        usage (stderr);
                        exit (EXIT_FAILURE);
                }
        }

        while (argc - optind >= 4) {
                memset (&start_tm, 0, sizeof (struct tm));
                if (NULL == strptime (argv[optind + 0], "%Y-%m-%d",
                                      &start_tm))
                        goto invalid;
                if (NULL == strptime (argv[optind + 1], "%H:%M",
                                      &start_tm))
                        goto invalid;

                memset (&end_tm, 0, sizeof (struct tm));
                if (NULL == strptime (argv[optind + 2], "%H:%M",
                                      &end_tm))
                        goto invalid;

                memset (&pdc_tm, 0, sizeof (struct tm));
                if (NULL == strptime (argv[optind + 3], "%H:%M",
                                      &pdc_tm))
                        goto invalid;

                add_program_to_schedule (&start_tm, &end_tm, &pdc_tm);

                optind += 4;
        }

        if (argc != optind)
                goto invalid;

        return;

 invalid:
        usage (stderr);
        exit (EXIT_FAILURE);
}

int
main                            (int                    argc,
                                 char **                argv)
{
        vbi_bool success;

        setlocale (LC_ALL, "");

        parse_args (argc, argv);

        exit_code = EXIT_SUCCESS;

        dec = vbi_decoder_new ();
        assert (NULL != dec);

        success = vbi_event_handler_register (dec, VBI_EVENT_PROG_ID,
                                              event_handler,
                                              /* user_data */ NULL);
        assert (success);

        reset_state ();

        standby_loop ();

        vbi_decoder_delete (dec);

        while (NULL != schedule)
                remove_program_from_schedule (schedule);

        exit (exit_code);
}