From 9983e7e56ecc1ab388b071a9aa3638483770ce21 Mon Sep 17 00:00:00 2001 From: "Matthias P. Braendli" Date: Sun, 17 Apr 2016 19:36:33 +0200 Subject: Add UECP parser prototype for RadioText extraction --- uecpparse/crc.py | 57 ++++++++++++++++ uecpparse/uecp_parse.py | 168 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 uecpparse/crc.py create mode 100755 uecpparse/uecp_parse.py diff --git a/uecpparse/crc.py b/uecpparse/crc.py new file mode 100644 index 0000000..813542a --- /dev/null +++ b/uecpparse/crc.py @@ -0,0 +1,57 @@ +#library for crc16 calculation + + +crc16tab = [ + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, + 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, + 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, + 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, + 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, + 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, + 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, + 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, + 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, + 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, + 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, + 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, + 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, + 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, + 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, + 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, + 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, + 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, + 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, + 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, + 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, + 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, + 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 ] + +def crc16(data, l_crc=0xffff): + for d in data: + #print("crc 0x{:x} 0x{:x}".format(ord(d), l_crc)) + l_crc = (l_crc << 8) ^ crc16tab[(l_crc >> 8) ^ d] + l_crc = l_crc & 0xFFFF + + return l_crc + +def crc_ccitt(data): + crc = 0xFFFF + + for d in data: + crc = (((crc >> 8) & 0xFF) | (crc << 8)) & 0xFFFF + crc ^= d + crc ^= ((crc & 0xff) >> 4) & 0xFFFF + crc ^= ((crc << 8) << 4) & 0xFFFF + crc ^= (((crc & 0xff) << 4) << 1) & 0xFFFF + + return ((crc ^ 0xFFFF) & 0xFFFF); + diff --git a/uecpparse/uecp_parse.py b/uecpparse/uecp_parse.py new file mode 100755 index 0000000..cdfccd5 --- /dev/null +++ b/uecpparse/uecp_parse.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2016 Matthias P. Braendli +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# +# A UECP decoder, reading from a file generated by +# my patched VLC, from ancillary MP2 bytes in a MPEG-TS +# +# Please refer to RDS UECP specification + +import sys +import struct +import crc + +# Message Element Codes +uecp_mec_names = { + 0x01: "PI" , # Program Identification + 0x02: "PS" , # Program Service name + 0x06: "PIN" , # Program Item Number + 0x04: "DI_PTYI" , # Decoder Information / Dynamic PTY Indicator + 0x03: "TA_TP" , # Traffic Anouncement / Traffic Programme + 0x05: "MS" , # Music / Speech switch + 0x07: "PTY" , # Programme Type + 0x3E: "PTYN" , # Programme Type Name + 0x0A: "RT" , # RadioText + 0x0D: "RTC" , # Real Time Clock + 0x19: "CT" , # Enable RTC (group 4 version A) transmits + 0x1C: "DSN_SELECT" , # Set active Data Set + 0x0B: "PSN_ENABLE" , # Enable / disable a specific PSN + 0x1E: "RDSON" , # Enable / disable the RDS output signal + 0x23: "SET_SITE_ADDR" , # Add a site address to the encoder + 0x27: "SET_ENC_ADDR" , # Add an encoder address to the encoder + 0x17: "MSG_REQUEST" , # Request data from the encoder + 0x18: "MSG_ACK" , # ACK of a received message + 0x2C: "SET_COMM_MODE" } # Set communication mode for the encoder + +def usage(): + print("Usage:\n{} ".format(sys.argv[0])) + +if len(sys.argv) < 2: + usage() + sys.exit(1) + +class UECP_Message_Decoder(): + def __init__(self, message_bytes): + """Create a message decoder that creates + Message Elements from a message""" + self.message = message_bytes + self.mec = message_bytes[0] + assert(0x01 <= self.mec <= 0xFD) + + if self.mec in uecp_mec_names: + name = uecp_mec_names[self.mec] + print(" MEC={}".format(name)) + if name == "PS": + dsn = message_bytes[1] + psn = message_bytes[2] + ps = message_bytes[3:-1] + print(" PS DSN={:02x} PSN={:02x} ".format(dsn, psn) + "".join(chr(d) for d in ps)) + elif name == "RT": + dsn = message_bytes[1] + psn = message_bytes[2] + mel = message_bytes[3] + med = message_bytes[4:-1] + print(" RT DSN={:02x} PSN={:02x} MEL={:02} ".format(dsn, psn, mel) + " ".join("{:02x}".format(d) for d in med)) + + else: + print(" MEC={}".format(self.mec)) + +class UECP_Frame_Decoder(): + def __init__(self, uecp_bytes): + """Create a new decoder from bytes containing a + complete UECP frame, from STA to STO. + See 2.2.1 General Frame Format""" + assert(uecp_bytes[0] == 0xfe) + assert(uecp_bytes[-1] == 0xff) + self.trapped_data = uecp_bytes + self.untrap() + self.decode_frame() + + def untrap(self): + """Unescapes characters as defined in 2.2.9""" + self.data = [0xfe] + + next_untrap = False + for b in self.trapped_data[1:-1]: + if next_untrap: + next_untrap = False + if b == 0x00: + self.data.append(0xFD) + elif b == 0x01: + self.data.append(0xFE) + elif b == 0x02: + self.data.append(0xFF) + else: + raise ValueError("untrap {} invalid".format(b)) + elif b == 0xFD: + next_untrap = True + elif b == 0xFE or b == 0xFF: + raise ValueError("untrapped data {} invalid".format(b)) + else: + self.data.append(b) + self.data.append(0xff) + + def check_crc(self): + if 0: + print("B " + " ".join("{:02x}".format(b) for b in self.data)) + for i in range(1, len(self.data)): + print(" i={:02} crc=0x{:04x} calc1=0x{:04x} calc2=0x{:04x}".format(i, + self.crc, + 0xffff ^ crc.crc16(self.data[1:i]), + crc.crc_ccitt(self.data[1:i]))) + + self.crc_calc = crc.crc_ccitt(self.data[1:4+self.mfl]) + return self.crc_calc == self.crc + + def decode_frame(self): + """Decodes an untrapped frame""" + self.addr = self.data[1] * 256 + self.data[2] + self.sqc = self.data[3] + self.mfl = self.data[4] + self.msg = self.data[5:5+self.mfl] + assert(self.mfl + 8 == len(self.data)) + self.crc = self.data[5+self.mfl] * 256 + self.data[5+self.mfl+1] + self.crc_ok = self.check_crc() + + UECP_Message_Decoder(self.msg) + +in_fd = open(sys.argv[1], "r") +for line in in_fd: + line_bytes = [int(i, 16) for i in line.split()] + + line_bytes.reverse() + + anc_header = line_bytes[0] + if anc_header == 0xfd: + anc_len = line_bytes[1] + if anc_len > 0: + anc_bytes = line_bytes[2:2+anc_len] + + if anc_bytes[-1] == 0xff: + uecp = UECP_Frame_Decoder(anc_bytes) + + print("Addr={} sqc={} mfl={} msg={} crc=0x{:04x} crc_calc=0x{:04x} ok={}".format( + uecp.addr, uecp.sqc, uecp.mfl, + uecp.msg, uecp.crc, uecp.crc_calc, uecp.crc_ok)) + -- cgit v1.2.3