Декодирование в реальном времени радиосигнала точного времени

image
Сегодня я хочу поговорить о том, как можно получить и декодировать сигналы точного времени, которые передаются по радио. Чтобы выполнить эту задачу, вам даже не понадобятся специальные устройства. Достаточно будет компьютера с более-менее быстрым интернет-соединением.

Технология передачи точного времени по радио не нова. Сигналы точного времени начали передавать практически сразу, как появился радиотелеграф. Сейчас передача сигналов точного времени осуществляется с помощью различных технологий. Помимо радио, информация о времени с разной степенью точности передаётся:

  • в интернете (NTP);
  • в сетях мобильной связи (NITZ);
  • в системах спутниковой навигации GPS, ГЛОНАСС, BeiDou-3, Galileo.


Хотя в этих случаях используются более современные технологии, передача сигналов точного времени по радио продолжает существовать и выполнять свои функции. Промышленностью выпускаются различные устройства, принимающие эти сигналы, например, часы Casion Wave Ceptor. Изучив основы передачи точного времени по радио, вы узнаете много интересного, а также закрепите свои знания в различных областях.

Хочу сразу привести несколько оговорок, чтобы не разочаровать читателя. Под декодированием в реальном времени я понимаю получение информации о текущем времени практически сразу, как сигнал был получен. Так как в процессе получения и декодирования сигнала используется несколько систем, которые вносят свои задержки, сигнал будет декодирован со значительной (если говорить очень об точном времени) задержкой.

Также я старался излагать материал практически без формул. Так как считаю, что обилие математических формул отпугивает неподготовленного читателя на начальном этапе. Имея базовые представления о чём-либо, проще погрузиться в математический аппарат, описывающий явление или процесс.

Немного теории


Без знания теоретических основ трудно сделать что-то более или менее сложное, а особенно понять, что же ты всё-таки сделал, и почему работает именно так, а не иначе. Хотя часто человек в реальной жизни руководствуется или инструкциями, или простым правилом — «методом научного тыка».

Если у вас нет желания изучать теорию, или вы её знаете, то можете сразу приступить к практической части или посмотреть исходный код приложения в моём репозитории на GitHub.

▍ Информация


Информация играет ключевую роль в принятии решений. Существует известный афоризм Натана Ротшильда «Тот, кто владеет информацией, владеет миром», и с ним сложно не согласиться. Поэтому на протяжении всей своей истории человечество прикладывает значительные усилия для разработки технологий передачи, хранения, обработки и защиты информации.

Разработка Клодом Шенноном в 1948 году теории информации, в которой было введено понятие единицы информации, или бита, является важной вехой в процессе разработки информационных технологий.

Мы живём в век информационных технологий, а это значит, что сейчас как никогда эти технологии развиваются.

▍ Сигнал


В интернете есть множество различных определений, что такое сигнал, и этому можно найти объяснение — термин «сигнал» используется в различных областях. Но мы ограничимся определением из теории обработки сигнала.

Сигнал — это функция, которая несёт в себе информацию о явлении. Используя такое определение, иными словами, рассматривая сигнал как функцию, можно применить к сигналам математический аппарат, который разработало человечество.

Сигналы бывают аналоговыми и цифровыми.

Аналоговый сигнал — это сигнал, который представляет собой непрерывную функцию времени. Например, значения напряжения, которые генерирует микрофон, непрерывно изменяются во времени.

Цифровой сигнал — это сигнал, который представляет собой последовательность дискретных значений. Например, цифровой телефонный сигнал в ISDN — это последовательность из 8000 восьмибитных чисел в секунду.

Хотя полностью без аналоговых сигналов не обойтись, в настоящее время обработка цифровых сигналов доминирует.

Цифровой сигнал можно передать по аналоговому каналу, и аналоговый сигнал можно передать по цифровому каналу. Для этого применяются аналогово-цифровые и цифро-аналоговые преобразователи (Analog Digital Converison — ADC, Digital Analog Conversion — DAC)).

Далее мы будем рассматривать цифровые сигналы, поэтому углубляться в аналоговые без необходимости не будем.

Цифровой сигнал получается из аналогового путём разделения сигнала во времени (или пространстве) на дискретные промежутки (дискретизация сигнала), где на каждом промежутке замеряется значение и округляется к одному из конечных уровней сигнала (квантование сигнала).

▍ Физика сигнала


Как говорилось ранее, математически сигнал можно представить в виде функции изменения некоторой величины. Значением функции может быть напряжение, сила тока, напряжённость электромагнитного поля, интенсивность звука, а может быть просто безразмерная величина.

В зависимости от того, какой сигнал, и как мы его хотим описать, параметрами этой функции могут быть время, частота или расстояние.

▍ Понятия модуляции и демодуляции. Передача аналогового сигнала по радиоканалу


Часто, чтобы передать информацию посредством сигнала через канал передачи информации, нужно выполнить модуляцию сигнала на передающей стороне и демодуляцию на принимающей.

Наверное, хорошим примером для понимания модуляции и демодуляции сигнала является передача аналогового сигнала по радиоканалу.

Чтобы передача сигнала по одному радиоканалу не мешала передаче информации по другому, за каждым каналом закреплён диапазон радиочастот, который ещё называют полосой. Использование полосы частот определяется различными локальными или международными законами.

Некоторую частоту в этой полосе принимают за несущую частоту. Именно эту частоту вы указываете в радиоприёмнике, когда хотите прослушать определённую радиостанцию. Радиостанция излучает радиоволны, у которых частоты принадлежат полосе частот, закреплённой за ней.

Изменение характеристик несущей частоты во времени в пределах полосы является сигналом и несёт в себе информацию. Изменяя характеристики несущей частоты в соответствии с информационным сигналом, выполняют модуляцию, а извлекая информационный сигнал из изменяющихся характеристик несущей частоты — демодуляцию. К изменяемым характеристикам относятся частота, амплитуда или фаза колебаний радиоволн.

Соответственно выделяют:

  • частотную модуляцию (FM);
  • амплитудную модуляцию (AM);
  • фазовую модуляцию (PM).


Обычные бытовые радиоприёмники позволяют принимать радиосигнал, который промодулирован с использованием частотной или амплитудной модуляции. Однако в радиоэфире также широко распространён радиосигнал с CW-модуляцией (Continuos Wave — непрерывная волна). CW-модуляция используется, например, при передаче сообщений с помощью азбуки Морзе. Для неё полоса частот может быть минимальной, так как используется только несущая частота. Есть колебания — »1», нет колебаний — »0».

▍ Широтно-импульсная модуляция сигнала


Если сигнал может принимать только два значения — 0 и 1 (Low и High), то при помощи широты импульса можно также закодировать значения. В нашем случае так кодируются биты сообщения DCF77. Если значение 1 длится 0.9 секунды, то передаётся 0, если 0.8 — 1, если 2 секунды — это конец сообщения. Передача таким образом данных называется асинхронной передачей, так как нам в этом случае для кодирования и декодирования не нужно дополнительного сигнала для синхронизации.

▍ Выпрямление сигнала


Выпрямление (rectification) сигнала подразумевает получение сигнала, у которого отсутствуют отрицательные амплитуды. Выделяют full-wave и half-wave rectification. Математически full-wave выпрямление — это получение модуля для каждого значения исходного сигнала.

▍ Фильтрация сигнала


Полученный сигнал содержит информацию, но кроме неё в сигнале присутствуют различные шумы (то, что не несёт в себе полезной информации) или другие частотные составляющие. Избавиться от них можно при помощи фильтров.

Если нужно оставить частоты ниже некоторой частоты, то используется фильтр нижних частот (ФНЧ, low-pass filter), если выше определённой частоты — фильтр высоких частот (ФВЧ, high-pass filter), если частоты в определённом диапазоне — band-pass filter.

▍ Алгоритм приёма и демодуляции AM-сигнала


Для демодуляции AM-сигнала необходимо определить огибающую сигнала (envelope). Один из способов получения огибающей сигнала заключается в выпрямлении сигнала с последующим применением к выпрямленному сигналу фильтра нижних частот (ФНЧ, low-pass filter).

Во многих источниках рассказывается о получении огибающей с использованием преобразования Гильберта, но выпрямление сигнала и использование фильтра нижних частот проще в реализации для демодуляции в реальном времени.

▍ Цифровая обработка сигнала


В настоящее время наиболее распространена цифровая обработка сигнала. Если аналоговая обработка состоит в построении физических устройств, которые будут преобразовывать сигнал в реальном времени, то цифровая обработка подразумевает применение алгоритмов к массивам значений, которые представляют собой цифровой сигнал. Методы цифровой обработки сигнала частично опираются на математический аппарат, созданный ранее для аналоговой обработки сигнала.

▍ Цифровые фильтры


В цифровой обработке сигналов фильтр можно рассматривать как систему, которая одну последовательность чисел (сигнал на входе) превращает в другую последовательность чисел (сигнал на выходе).

После применения цифрового фильтра мы из одной последовательности чисел получаем другую последовательность чисел, которая удовлетворяет нашим целям.

Фильтры бывают с обратной связью и без обратной связи. Обратная связь подразумевает, что значения, полученные после фильтрации, влияют на следующие получаемые значения.

▍ Частота Найквиста


Когда аналоговый сигнал преобразуется в цифровой путём семплирования, то необходимо, чтобы частота дискретизации была в два раза больше максимальной частоты интересующей нас в аналоговом сигнале. Эта частота получила название частоты Найквиста. Поэтому, зная частоту дискретизации сигнала, мы можем определить, с какими частотами в сигнале мы можем работать. Например, частота дискретизации CDAudio 44100 Гц, поэтому мы можем работать с частотами вплоть до 22500 Гц.

▍ Специализированные радиостанции, передающие сигналы точного времени


Приведу список сигналов, которые существуют или существовали в недалёком прошлом с кратким их описанием.

▍ WebSDR и KiwiSDR


Я никогда не был радиолюбителем, но мне кажется, что сейчас стать им гораздо проще. В предыдущей статье я описал, как можно просто ловить и анализировать сигнал GSM-сетей, используя обычный приёмник цифрового телевидения. К сожалению, RTL-SDR-приёмник, полученный из приёмника цифрового телевидения, не позволяет принимать сигналы на длинных и сверхдлинных радиоволнах. А именно на них транслируются сигналы точного времени многих стран. Кроме того, сигнал может быть некачественный в вашем регионе или вообще отсутствовать. Но, как всегда, без энтузиастов и альтруистов не обошлось, и сейчас есть такие проекты, как WebSDR и KiwiSDR, которые, хотя и с некоторыми ограничениями, позволяют вам принимать и анализировать радиосигналы, не имея радиоприёмника вообще. Главное условие — это наличие у вас относительно стабильного интернет-соединения.

▍ Виртуальный аудиокабель


Когда нам необходимо каким-то образом захватить для дальнейшей обработки звук в операционной системе Windows, который генерирует программа, например, браузер, удобно воспользоваться виртуальным аудиокабелем (Virtual Audio Cable — VAC). VAC представляет собой драйвер Windows, который добавляет виртуальные аудиоустройства, к которым можно обратиться программно и получить сигнал. Для наших целей достаточно Light-версии VAC.

▍ Как просмотреть и проанализировать цифровой сигнал


Если цифровой сигнал записан в WAV-файл, то для этого удобно использовать программу Audacity. Сигнал можно отфильтровать, получить его частотные характеристики. С моей точки зрения, очень удобное программное решение, чтобы начать практически применять цифровую обработку сигнала.

▍ Сигнал и сообщения DCF77


Описание сигнала и сообщений DCF77 приведены в Википедии, информации достаточно для того, что бы обрабатывать их и декодировать. Приведу только свои мысли по этому поводу:

  1. На Хабре уже была статья по декодированию DCF77, но там не рассматривалось, как можно декодировать DCF77 в реальном времени. Вы также можете использовать эту статью для справки.
  2. При использовании WebSDR или KiwiSDR не нужно обращать внимание на фазовую модуляцию внутри сигнала и рассматривать сигнал, как CW-модулированный с минимальной шириной полосы.
  3. Биты с 1–14 DCF77-сообщения являются информацией о погоде компании Meteotime. Как именно кодируется информация о погоде, было бы интересно знать, но эта информация не является общедоступной.


▍ Использование пакетов (библиотек) Python


На данный момент Python, ввиду своей простоты и наличия огромного количества библиотек для исследований, является языком, на котором решаются научные и прикладные задачи. Поэтому воспользуемся им для декодирования сигналов.

Можно, конечно, и с нуля написать весь код, но это усложнит и статью, и программу.

Я использовал следующие пакеты Python: scipy, numpy, matplotlib, sounddevice, soundfile, pytz.

Практическая часть


Закрепим полученные знания на практике, для чего напишем программу на Python для декодирования сигналов точного времени.

Применив объектно-ориентированный подход и немного функционального программирования, разработаем первоначальную архитектуру приложения, которая не претендует на правильность и/или оптимальность, но позволяет посмотреть, как же всё работает вместе.

Алгоритм декодирования сигнала DCF77 следующий:

  1. Получить сигнал DFC77 в виде звукового сигнала.
  2. Получить огибающую этого сигнала.
  3. Используя граничное значение для различения сигналов 0 и 1, получить ШИМ-сигнал, в котором с помощью широты импульса закодированы 0, 1 и конец DCF77-сообщения.
  4. Декодировать последовательность 0 и 1 в дату со временем, используя спецификацию DCF77.


▍ Звук и сигнал DFC77


На сайтах проектов WebSDR или KiwiSDR можно получить звуковое представление сигнала DCF77. Для этого нужно выбрать:

  • Для WebSDR — полосу частот, равную 0 кГц, CW-модуляцию и несущую частоту 77.5 кГц.
  • Для КiwiSDR — CWN-модуляцию и несущую частоту 77.5 кГц.


В результате можно этот сигнал услышать и записать в WAV-файл на сайте. Из звука в браузере или в WAV-файле можно получить и декодировать сигнал DFC77.

Для получения звука из браузера нужно ещё установить и настроить Virtual Audio Cable (VAC).
Важно получить массив значений, для которого можно применить методы обработки сигналов.
Существуют уже готовые библиотеки Python для получения таких массивов — soundfile и sounddevice.

Особенности работы с библиотеками отличаются. Чтобы сделать обработку сигнала более унифицированной, выделим абстракции и разработаем минимальную иерархию классов.

▍ Иерархия классов исходных сигналов


▍ Класс SourceSignal


SourceSignal — абстрактный класс, содержащий один абстрактный метод stream (), который необходимо переопределить в наследниках, и два свойства: sample_rate (частота дискретизации) и stream (поток).

Метод stream () — это функция-генератор, которая возвращает дискретный цифровой сигнал.

class SourceSignal(ABC):
    def __init__(self, sample_rate, sample_count=None):
        self.sample_rate = sample_rate
        self.stream = self.stream(sample_count)

    @abstractmethod
    def stream(self, sample_count):
        pass


▍ Класс WavFileSignal


Класс WavFileSinal представляет собой сигнал, хранящийся в WAV-файле. Сигнал — это массив значений. Конечно, можно получить этот массив, разобрав файл, но мы воспользуемся библиотекой audiofile. wavfile.read () вернёт нам информацию о частоте дискретизации сигнала и сигнал в виде массива значений. Чтобы не хранить весь массив в памяти, используется mmap=True.

Также с целью отладки и унификации я возвращаю значения сигнала блоками по 1000 значений.

class WavFileSignal(SourceSignal):
    def __init__(self, file_name, sample_count=None):
        self.block_size = 1000
        sample_rate, self.file_data = wavfile.read(file_name, mmap=True)
        super().__init__(sample_rate, sample_count)

    def stream(self, sample_count):

        if not sample_count:
            sample_count = len(self.file_data)
        file_data = self.file_data[0:sample_count]
        cnt = 0
        while True:
            data = file_data[cnt:cnt + self.block_size]
            cnt += self.block_size
            yield data
            if len(data) == 0:
                break


▍ Класс AudioDeviceSignal


Получение сигнала из аудиоустройства — немного более сложная задача. Сигнал, полученный из аудиоустройства, скорее всего, предполагает его обработку в реальном времени. Мы будем использовать библиотеку audiodevice.

Изначально нам нужно получить информацию об устройстве аудиовхода по умолчанию. А именно частоту дискретизации:

self.device_info = sd.query_devices(kind="input")
        sample_rate = self.device_info['default_samplerate']


Чтобы читать данные из аудиопотока, нам необходимо его создать:

input_stream = sd.InputStream(callback=audio_callback)


audio_callback — это функция, которая вызывается каждый раз, когда в аудиопотоке появляются данные для обработки. Следует заметить, что функция вызывается в отдельном потоке выполнения (не следует путать с потоком данных), поэтому нам нужно позаботиться о синхронизации потоков и передаче данных между ними.

Также к функции накладываются довольно жёсткие требования ко времени выполнения, чтобы не было пропусков данных из аудиопотока. Поэтому она выполняет минимум действий:

  • получает данные из аудиопотока;
  • выбирает данные для первой аудиодорожки;
  • помещает данные в очередь для обработки.


Непосредственно обработка данных сигнала осуществляется в другом потоке выполнения.
Очередь данных для обработки организуем с помощью класса Queue. Queue является потокобезопасным классом. Это значит, что мы можем использовать один и тот же экземпляр класса в разных потоках, не думая о синхронизации доступа к нему.

Для того, чтобы сообщить обрабатывающему потоку, что данные из аудиопотока больше не будут поступать в очередь для обработки, будем использовать примитив синхронизации потоков threading.Event.

 class AudioDeviceSignal(SourceSignal):

    def __init__(self, sample_count=None):
        self.processed_count = 0
        self.device_info = sd.query_devices(kind="input")
        sample_rate = self.device_info['default_samplerate']
        super().__init__(sample_rate, sample_count)

    def stream(self, sample_count):
        event = threading.Event()
        queue = Queue()
        self.processed_count = 0

        def audio_callback(indata, samples, time, status):
            if status:
                print(status)
                return
            data = indata[:, 0]
            self.processed_count += samples
            if not (sample_count is None) and self.processed_count > sample_count:
                rest = self.processed_count - sample_count
                data = data[0: -rest]
                stop = True
            else:
                stop = False
            queue.put(data)
            if stop:
                event.set()

        input_stream = sd.InputStream(callback=audio_callback)
        with input_stream:
            while True:
                yield queue.get()
                if event.is_set():
                    break


▍ Основной цикл программы


Создав такую иерархию классов, мы можем полиморфно работать с сигналами, не задумываясь о том, откуда сигнал к нам поступил (из файла или аудиоустройства):

for data in source_signal.stream:


Каждый блок данных data мы можем рассматривать как отдельный сигнал и определять для него огибающую, преобразовывать в ШИМ-сигнал. Последовательно обрабатывая ШИМ-сигналы, мы будем получать последовательность 0, 1 из признаков конца минуты, которую можно превратить в последовательность сообщений DCF77. А из самого сообщения можно будет получить дату и время.

Как это реализовано, можно посмотреть в следующем коде на Python:

def process_date_time(source_signal, threshold_value):
        print_diff = isinstance(source_signal, AudioDevice)
        dcf_77_message = ''
        envelope_detector = EnvelopeDetector(source_signal.sample_rate)
        message_parser = Dcf77MessageParser(source_signal.sample_rate)

        for data in source_signal.stream:
            envelope_signal = envelope_detector.get_envelope(data)
            pwm_signal = threshold(envelope_signal, threshold_value)
            symbols = message_parser.parse(pwm_signal)
            print(symbols, end="", flush=True)
            for i in range(len(symbols)):
                if symbols[i] == 'M':
                    try:
                        date = dcf_77_decode(dcf_77_message)
                        print_date(date, print_diff=print_diff)
                    except Exception as e:
                        print('\nError parsing DCF77 message:' + str(e), flush=True)
                    dcf_77_message = ''
                else:
                    dcf_77_message += symbols[i]


▍ Основные составляющие программы


Реализации классов EnvelopeDetector, Dcf77MessageParser функций threshhold, dcf_77_decode, print_datetime достаточно просты и изолированы друг от друга. Это позволяет как удобно их тестировать, так и изменять их реализацию по мере необходимости.

Остановимся на каждой реализации подробнее.

▍ Класс EnvelopeDetector

class EnvelopeDetector:
    def __init__(self, sample_rate, cut_off_frequency=10):
        self.sample_rate = sample_rate
        self.filter = LowPassFilter(sample_rate, cut_off_frequency)

    def get_envelope(self, data):
        arr = np.array([math.fabs(el) for el in data])
        result = self.filter.apply(arr)
        return result


Для определения огибающей нам необходимо знать частоту дискретизации (sample_rate) и частоту, выше которой мы будем отсекать частоты из сигнала (cut_off_frequency).

Определение огибающей, как это было сказано ранее, это выпрямление сигнала с последующим применением фильтра нижних частот.

Математически выпрямление сигнала — это абсолютное значение для каждого сэмпла:

arr = np.array([math.fabs(el) for el in data])


Применение фильтра нижних частот выглядит следующим образом:

result = self.filter.apply(arr)


▍ Класс LowPassFilter

 class LowPassFilter:
    def __init__(self, sample_rate, frequency, order=2):
        self.b, self.a = signal.butter(order, frequency, btype='low', fs=sample_rate)
        self.zi = signal.lfilter_zi(self.b, self.a)
        self.first_run = True

    def apply(self, data):
        if self.first_run:
            result, self.zi = lfilter(self.b, self.a, data, zi=data[0] * self.zi)
            self.first_run = False
        else:
            result, self.zi = lfilter(self.b, self.a, data, zi=self.zi)
        return result


Для применения фильтра к дискретному цифровому сигналу в библиотеке scipy нам сначала необходимо определить массивы коэффициентов a и b, которые будут использоваться в разностном уравнении IIR-фильтра, а также начальные условия zi для фильтра.

self.b, self.a = signal.butter(order, frequency, btype='low', fs=sample_rate)
        self.zi = signal.lfilter_zi(self.b, self.a)


Мы используем фильтра Баттерворта (butter) второго порядка order=2.

Начальные условия необходимы для того, чтобы на выходе фильтра несколько первых значений не были равны 0, а имели значения, близкие к исходному сигналу.

Когда фильтр получает первый блок данных, нужно умножить начальные условия на 0-й элемент этого блока, чтобы условия были в том же масштабе, что и остальные данные.

zi=data[0] * self.zi


Функция lfilter возвращает два значения: отфильтрованный сигнал и начальные значения для следующего вызова функции, это позволяет обрабатывать сигнал по порциям как единое целое. Поэтому полученные значения начальных условий мы сохраняем в переменной zi экземпляра класса LowpassFilter

▍ Класс Dcf77MessageParser

class Dcf77MessageParser:
    def __init__(self, sample_rate):
        self.sample_rate = sample_rate
        self.one_length = sample_rate*0.8
        self.zero_length = sample_rate*0.9
        self.end_of_minute_length = sample_rate*2

        self.cnt = 0
        self.prev = 0

    def parse(self, data):
        i = 0
        buff = ''
        while i < len(data):
            if self.prev == 0 and data[i] == 1:
                self.cnt = 1
            elif self.prev == 1 and data[i] == 0:
                if 0.95*self.one_length < self.cnt < 1.05 * self.one_length:
                    buff += '1'
                    self.cnt = 0
                elif 0.95*self.zero_length < self.cnt < 1.05 * self.zero_length:
                    buff += '0'
                    self.cnt = 0
                elif 0.9 * self.end_of_minute_length < self.cnt < 1.1 * self.end_of_minute_length:
                    buff += 'M'
                    self.cnt = 0
                else:
                    buff += 'E'
                    self.cnt = 0
            elif self.prev == 1 and data[i] == 1:
                self.cnt += 1
            self.prev = data[i]
            i += 1
        return buff


Класс Dcf77MessageParser подразумевает, что на вход подаются данные ШИМ-сигнала c дискретизацией sample_rate. Если сигнал находится в состоянии »1» 0.8 секунды — то передаётся »1», если 0.9 секунды — то »0», если 2 секунды — это конец минуты.


        self.one_length = sample_rate*0.8
        self.zero_length = sample_rate*0.9
        self.end_of_minute_length = sample_rate*2


Выполнять проверку на размер ширины импульса необходимо в момент перехода сигнала из »1» в »0».


elif self.prev == 1 and data[i] == 0:


Так как огибающая превращается в ШИМ-сигнал с погрешностью, ширина импульса может варьироваться, поэтому используем проверку на принадлежность интервалу значений.

if 0.95*self.one_length < self.cnt < 1.05 * self.one_length:

Я определил интервал опытным путём, никаких математических расчётов не проводил.

▍ Функция threshold


Реализация функции простая, если у нас значение сигнала ниже определённого порогового значения, то это »0», если равно или выше — это »1».

Пороговое значение для конкретного сигнала я определял экспериментально по графическому представлению огибающей.

def threshold(data, threshold_value):
    result = np.array([0 if el < threshold_value else 1 for el in data])
    return result


▍ Функция dcf_77_decode


def dcf_77_decode(bits):
    dcf_77_validate(bits)
    dcf_date_time = {
        'summer_time_announce': bits[16],
        'cest': bits[17] == '1',
        'cet': bits[18] == '1',
        'leap_sec_announce': bits[19],
        'minute': from_bcd(bits[21:28]),
        'hour': from_bcd((bits[29:35])),
        'day_of_month': from_bcd((bits[36:42])),
        'day_of_week': from_bcd((bits[42:45])),
        'month': from_bcd((bits[45:50])),
        'year': from_bcd((bits[50:58])),
    }
    offset = 2 if dcf_date_time['cest'] else 1

    date = datetime(year=dcf_date_time['year']+2000,
                    month=dcf_date_time['month'],
                    day=dcf_date_time['day_of_month'],
                    hour=dcf_date_time['hour'],
                    minute=dcf_date_time['minute'],
                    tzinfo=timezone(timedelta(hours=offset))
                    )
    return date


Для упрощения я валидировал только длину сообщения, но можно проверять и биты чётности и биты-маркеры.

Большая часть данных в сообщении DCF77 закодирована в виде двоично-десятичного кода, поэтому определим функцию from_bcd.

Время в DCF77-сообщении передаётся во временной зоне CEST или CET в зависимости от того, летнее время в Германии или зимнее. Время во временной зоне CEST опережает время UTC на +2 часа, во временной зоне СET — на +1 час.

Поэтому мы определяем смещение из DCF77-сообщения:

offset = 2 if dcf_date_time['cest'] else 3


И указываем его при создании объекта datetime:

tzinfo=timezone(timedelta(hours=offset))


Объект datetime с указанием временной зоны позволяет нам использовать интересности в методе print_date.

▍ Функция print_datetime

def print_datetime(datetime_with_tz, print_diff=False):
    system_time = datetime.now().astimezone()
    if print_diff:
        time_diff = system_time - datetime_with_tz
    print(f'\nUTC Time: {datetime_with_tz.astimezone(pytz.utc)}', flush=True)
    print(f'Encoded Time: {datetime_with_tz}', flush=True)
    print(f'Time in your timezone: {datetime_with_tz.astimezone(tzlocal())}', flush=True)
    print(f'System time: {system_time}', flush=True)
    if print_diff:
        print(f'Time difference: {time_diff}', flush=True)


Имея объект datetime со временной зоной, мы можем определить:

1) Какое UTC-время соответствует ему:

datetime_with_tz.astimezone(pytz.utc)


2) Какое локальное время соответствует ему:

datetime_with_tz.astimezone(tzlocal())


3) Насколько отличаются моменты времени, соответствующие текущему времени и времени объекта datetime:

system_time = datetime.now().astimezone()
    if print_diff:
        time_diff = system_time - datetime_with_tz


▍ Как пользоваться приложением-примером


Вы можете увидеть, как работает декодирование сигналов DCF77, загрузив и запустив у себя моё приложение-пример, архитектуру которого я описывал выше.

Для этого нужно зайти на сайт WebSDR или KiwiSDR и выбрать сайт с приёмником. Удобнее всего выбрать WebSDR-приёмник университета Твенте (Нидерланды). Следует заметить, что KiwiSDR позволяет декодировать DCF77 в реальном времени, не используя моё приложение, но я преследовал цель в статье показать читателю, как именно происходит процесс декодирования.

Для декодирования необходимо выполнить следующие шаги:

1. Настроить приёмник на приём сигнала DCF77.

Для WebSDR — это ширина полосы 0 кГц, CW-модуляция и частота 77.5 кГц.

image


Для KiwiSDR — это CWN-модуляция и частота 77.5 кГц.

image


2. Нужно определиться, из какого источника вы хотите декодировать сигналы DCF77: из WAV-файла или напрямую с сайта.

Если вы хотите декодировать сигналы из браузера, то сначала необходимо установить и настроить виртуальный аудиокабель (специальный драйвер, который перенаправляет звук из операционной системы на виртуальное аудиоустройство). Достаточно установить Light-версию виртуального аудиокабеля. Для этого нужно зайти в настройки звука (Control Panel → Sound) и на вкладках Recording и Playback выбрать виртуальный аудиокабель. Теперь вы не будете слышать звук через динамики и у вас не будет работать микрофон, поэтому после не забудьте вернуть настройки на место. Желательно это сделать сразу же после окончания эксперимента, чтобы вы потом долго не ломали голову, почему у вас не работает звук в скайпе или другом приложении.

image


image

3. Загрузить и установить Python 3, если он у вас не установлен.

4. Установить пакеты Python.

python -m pip install soundfile sounddevice numpy scipy matplotlib pytz


5. Посмотреть, как выглядит сигнал DCF77 в файле или аудиоустройстве.

python signal_processor.py plot -s file --threshold 1500 --sample
-count=41000 samples/dcf_77_1.wav


image
python signal_processor.py plot -s audio-device --threshold 0.01 --sample-count=160000


image


Это нужно для определения «на глаз» порогового значения, используемого при конвертировании огибающей в ШИМ-сигнал.

6. Запустить декодирование, указав среди прочих параметров пороговое значение. Вы должны увидеть процесс декодирования сигнала DCF77.

Из файла:

python signal_processor.py decode-dcf77 -s file --threshold 1500 samples/dcf_77_1.wav


image


Из аудиоустройства, когда слушается сигнал DCF77 на сайте WebSDR:

python signal_processor.py decode-dcf77 -s audio-device --threshold 0.01


image


Из аудиоустройства, когда слушается сигнал DCF77 на сайте KiwiSDR:

python signal_processor.py decode-dcf77 -s audio-device --threshold 0.05


image


7. Если вы его не увидели или возникает много ошибок, то можно попробовать изменить значение порогового значения и запустить приложение ещё раз.

Заключение


Предполагаемая аудитория этой статьи — те люди, которые имеют лишь слабое представление об обработке сигналов. Чтобы не испугать и запутать, я не стал приводить математические выкладки, которые сопутствуют теории обработки сигналов. Но думаю, что приведённого материала должно хватить, чтобы понять, что же вы делали, а книги по обработке сигналов не будут восприниматься, как что-то выше вашего понимания. Вероятно, что-то из приведённого вы изучали или могли изучать в вузе, но в статье я немного систематизировал и описал междисциплинарные связи, которые часто не озвучивают в вузе, что ещё больше усложняет восприятие и так непростых дисциплин.

Как всегда, копнув немного глубже в определённой теме, обнаруживаешь много интересных и связанных между собой фактов, но не всегда получается уместить их в одну статью, приходится чем-то жертвовать и искать компромисс между освещением всего найденного и целостностью и последовательностью статьи. Надеюсь, что компромисс был найден. Иногда осветить интересности получается в ответах на вопросы в комментариях к статье, поэтому я считаю, что комментарии к статье являются её хорошим дополнением.

Для меня радио и обработка сигналов всегда были тёмным лесом, чёрным ящиком — чем угодно, но у меня не было твёрдого понимания основ, благодаря которым можно изучать вопрос. Вероятно, это из-за того, что не было практики и наглядности, что в школе, что в институте, а основы излагали таким образом, что ты просто не понимал, что есть важное, и как его применить.

Декодируя сигнал с WebSDR и KiwiSDR, я заметил, что расхождение с точным временем при использовании KiwiSDR больше, чем при WebSDR, и это расхождение с каждой митутой работы приложения увеличивается, чего не наблюдается при использовании WebSDR. Точного объяснения этому я не нашел, но думаю это определяеся особенностью работы KiwiSDR приемника. Но сигнал, принимаемый при помощи KiwiSDR более чистый

Разработанному приложению есть куда развиваться, например, его можно расширить на декодирование других сигналов точного времени, приведенных в статье или определять пороговое значение автоматически, путем калибровки источника сигнала. Но мне хотелось простоты.

Я думаю, что радиолюбитель со стажем или специалист в области радиоэлектроники сможет найти неточности в изложении материала. Всегда рад получить отклик в комментариях.

Выиграй телескоп и другие призы в космическом квизе от RUVDS. Поехали?

© Habrahabr.ru