Thursday, October 15, 2015

CSS to Display UPnP Media Info

I was playing with UPnP and my goal interest was to display on webpage what is currently playing on XBOX360. (But would work on most of UPnP devices.)
So first we need python script which will ask for information. For this we need service AVTransport and action GetMediaInfo, if you are not familiar with UPnP you can find more here
I have few others monitoring scripts which are storing info about devices in my network to sqlite3 DB. Following script use this DB to find current uri for of my XBOX 360 UPnP service and send the request to get the data.
#! /usr/bin/python

import sqlite3
import sys
import cgi
import httplib, urllib
import re
import suds

#As UPnP ports are dynamic I'm getting info from my DB, info is stored by monitoring script.
select_query = "SELECT ip,upnp_port FROM devices WHERE name like 'XBOX 360';"
judodb = sqlite3.connect('/var/www/judo.db')
cur = judodb.cursor()
rows = cur.execute(select_query)
for row in rows:
  ip = row[0]
  upnp_port = row[1]

judodb.commit()
judodb.close()


url = ip + ":" + str(upnp_port)
controlUrl = "/Control/AVTransport"
service = "urn:schemas-upnp-org:service:AVTransport:1"
soap_args = "0"

actions = ["GetTransportInfo", "GetMediaInfo"]

for action in actions:
  conn = httplib.HTTPConnection(url)
  soap_data = ''+soap_args+''

  #FOR DEBUG
  #print soap_data

  params = urllib.urlencode({'q': 'set'})
  headers = { "Content-Type": "text/xml", "Content-Length": "%d" % len(soap_data), "SOAPACTION": '"'+service+'#'+action+'"' }

  conn.request("POST", controlUrl , "", headers)
  conn.send(soap_data)

  response = conn.getresponse()

  #FOR DEBUG
  #print response.status, response.reason

  #In UPnP there is sometime nested xml inside xml as string with < and > instead of < and >
  #lines below fix this to male it all one xml
  responsestr = response.read()
  responsestr = responsestr.replace(">",">")
  responsestr = responsestr.replace("<","<")

  print responsestr

I use javascript XMLHttpRequest to call the script and write the data I get to webpage. Below is eaxmple of such data.
<div id="UPnPPlayInfo" class="list"><!--?xml version="1.0"?-->
<s:envelope s:encodingstyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
 <s:body>
  <u:gettransportinforesponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
    <currenttransportstate>STOPPED</currenttransportstate>
    <currenttransportstatus>OK</currenttransportstatus>
    <currentspeed>1</currentspeed>
  </u:gettransportinforesponse>
 </s:body>
</s:envelope>


<!--?xml version="1.0"?-->
<s:envelope s:encodingstyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
 <s:body>
  <u:getmediainforesponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
           <nrtracks>1</nrtracks>
           <mediaduration>0:01:51.856</mediaduration>
           <currenturi>http://10.0.0.33:9001/disk/DLNA-PNMP3-OP01-FLAGS01700000/O0$1$8I566796.mp3</currenturi>
           <currenturimetadata>
             <item id="0$1$12$4477R566796" parentid="0$1$12$4477" refid="0$1$8I566796" restricted="1">
               <dc:title>Ja zadam</dc:title><dc:date>2009-01-01</dc:date>
               <upnp:genre>Folk</upnp:genre
               ><upnp:album>Virtualky</upnp:album>
               <upnp:originaltracknumber>4</upnp:originaltracknumber>
               <dc:creator>Jaromir Nohavica</dc:creator>
               <upnp:albumarturi dlna:profileid="JPEG_TN">http://10.0.0.33:9001/disk/DLNA-PNJPEG_TN-OP01-CI1-FLAGS00d00000/defaultalbumart/a_u_d_i_o.jpg/O0$1$8I566796.jpg?scale=160x160</upnp:albumarturi>
               <upnp:artist>Jaromir Nohavica</upnp:artist>
               <pv:extension>mp3</pv:extension>
               <upnp:albumartist>Jaromir Nohavica</upnp:albumartist>
               <pv:modificationtime>1255889570</pv:modificationtime>
               <pv:addedtime>1401613856</pv:addedtime>
               <pv:lastupdated>1255889570</pv:lastupdated>
               <pv:album_crosslink>0$1$12$4153</pv:album_crosslink>
               <pv:artist_crosslink>0$1$11$3960</pv:artist_crosslink>
               <pv:genre_crosslink>0$1$10$2756</pv:genre_crosslink>
               <pv:numberofthisdisc>1</pv:numberofthisdisc>
               <upnp:author role="Composer">Jaromir Nohavica</upnp:author>
               <upnp:artist role="Composer">Jaromir Nohavica</upnp:artist>
               <pv:bookmark>uuid:55076f6e-6b79-4d65-64a7-5067f0a1791f,-L21udC9tZWRpYVNoYXJlcy9tdXNpYy9tdXNpYy9KYXJvbWlyX05vaGF2aWNhL1ZpcnR1YWxreS9qYV96YWRhbS5tcDM=</pv:bookmark>
            </item>
          </currenturimetadata>
    </u:getmediainforesponse>
  </s:body>
</s:envelope>

Last thing I need to do is to use CSS to display only what I want and add some formatting and additional text.
/* LIST OF TAGS TO BE NOT DISPLAYED  */
currenttransportstatus, currentspeed, currenturi, playmedium, recordmedium, writestatus, res, mediaduration, nrtracks, desc {display: none}
upnp\:class, upnp\:albumarturi, dc\:creator, upnp\:albumartist, upnp\:author  {display: none}
pv\:extension, pv\:modificationtime, pv\:addedtime, pv\:lastupdated, pv\:album_crosslink, pv\:artist_crosslink, pv\:genre_crosslink, pv\:bookmark, pv\:numberofdiscs, pv\:numberofthisdisc {display: none}

DIDL-Lite {display: block}

Meta_Info:before {content: "Currently Playing:"; font-weight: bold}
Play_Mode:before {content: "Play mode:"; font-weight: bold}

/* LIST OF TAGS TO BE DISPLAYED ONE PER LINE */
upnp\:genre, upnp\:originaltracknumber, upnp\:album, upnp\:artist, dc\:title, dc\:date, dc\:publisher {display: list-item; list-style-type: none}


upnp\:artist:before {content: "Artist: "; margin-left: 10px; font-weight: bold}
upnp\:album:before {content: "Album: "; margin-left: 10px; font-weight: bold}
upnp\:genre:before {content: "Genre: "; margin-left: 10px; font-weight: bold}
upnp\:originaltracknumber:before {content: "Track No.: "; margin-left: 10px; font-weight: bold}
dc\:title:before {content: "Song: "; margin-left: 10px; font-weight: bold}
dc\:date:before {content: "Date: "; margin-left: 10px; font-weight: bold}
dc\:publisher:before {content: "Publisher: "; margin-left: 10px; font-weight: bold}

Monday, May 4, 2015

Home entertainment system monitoring and auto switch off

I have few devices which are creating my home entertainment system:

  • NEC monitor (sometimes good monitor is better then TV)
  • Yamaha HTR-4065 
  • PC Zotac
  • Evolveo DaulCoder DVB-T
  • XBOX 360

All those are connected to network via ethernet and there is some way to monitor or even to remotely control, lets start one by one:

DVB-T and PC

As you might know one of the easiest way to check if device is up or down would be to ping it, however in case of audio/video systems it's bit hard because if they are in Standby mode they will answer to the ping.
In some cases I don't even give the device permanent IP(DVB-T and PC). However there is the way to discover them via UPnP.

There is linux utility gssdp-discovery which use ssdp to discover UPnP capable device and as far as each device have unique identificator uuid we can check if device is running by checking if device is responding to ssdp discovery.

Below is small script which check based on uuid if device is running, which IP it was assigned and on which port UPnP is listening and record this to sqlite database.

#! /bin/bash

#test if device can be discovered using SSDP
#IMPORTANT return true if device is down
testUPnP()
{
  return `gssdp-discover -n 10 -t uuid:$1 | grep "resource available" | wc -l`
}

HFILE='/var/www/monitoring/device_upnp.list'
DBFILE='/var/www/judo.db'

if [[ -z $HFILE ]]
then
  echo "$HFILE doesn't exist"
  exit 1
fi

while read line
do
  uuid=$(echo "$line" | cut -d, -f5)
  mac=$(echo "$line" | cut -d, -f4)
  new_polls=1
  prev_status_polls=$(sqlite3 $DBFILE "SELECT cur_state,polls_in_state FROM dev_status WHERE mac = '$mac';")
  prev_status=$(echo "$prev_status_polls" | cut -d\| -f1)
  prev_polls=$(echo "$prev_status_polls" | cut -d\| -f2)

  #remeber testUPnP returns true if device is down
  if `testUPnP $uuid`
  then
    new_status="Down"
    if [ "$prev_status" == "Up" ]
    then
      sqlite3 $DBFILE "UPDATE devices SET ip = NULL, upnp_port = NULL WHERE mac = '$mac';"
    fi
  else
    new_status="Up"
    if [ "$prev_status" == "Down" ]
    then
      location=`gssdp-discover -n 10 -t uuid:$uuid | grep "Location:" | sed 's/ *Location: http:\/\///g'| cut -d/ -f1 | sed 's/\///g'`
      echo "$location"
      ip=$(echo "$location" | cut -d: -f1)
      upnp_port=$(echo "$location" | cut -d: -f2)
      echo "$ip:$upnp_port"
      sqlite3 $DBFILE "UPDATE devices SET ip = '$ip', upnp_port = '$upnp_port' WHERE mac = '$mac';"
    fi
  fi

  if [ "$prev_status" == "$new_status" ]
  then
    new_polls=$((prev_polls + 1))
  fi
  sqlite3 $DBFILE "UPDATE dev_status SET cur_state = '$new_status', polls_in_state = '$new_polls' WHERE mac = '$mac';"
  echo "$uuid,$mac,$new_status,$prev_status,$new_polls"
done < ${HFILE}

Yamaha HTR-4065


Even though Yamaha have UPnP we will rather use YNC to get the status, because we also want to know which input is actually active. Below is python script which finds the status and input and store that in sqlite database.
#! /usr/bin/python

import sqlite3
import sys
import httplib, urllib
import re
from xml.dom import minidom
from xml.parsers.expat import ExpatError

conn = httplib.HTTPConnection("10.0.0.36:80")
xml_req_power = '<?xml version="1.0" encoding="utf-8"?><YAMAHA_AV cmd="GET"><Main_Zone><Power_Control><Power>GetParam</Power></Power_Control></Main_Zone></YAMAHA_AV>'

xml_req_input = '<?xml version="1.0" encoding="utf-8"?><YAMAHA_AV cmd="GET"><Main_Zone><Input><Input_Sel>GetParam</Input_Sel></Input></Main_Zone></YAMAHA_AV>'

params = urllib.urlencode({'q': 'set'})
headers = { 'Content-Type': 'application/xml', "Content-Length": "%d" % len(xml_req_power)}

conn.request("POST", "/YamahaRemoteControl/ctrl", "", headers)
conn.send(xml_req_power)

xmldoc = minidom.parseString(conn.getresponse().read())
xml_input_element = xmldoc.getElementsByTagName('Power')
#print xml_input_element[0]
for node in xml_input_element:
  power_status = node.firstChild.nodeValue

conn.close()

#=================================
#Geting input

headers = { 'Content-Type': 'application/xml', "Content-Length": "%d" % len(xml_req_input)}

conn.request("POST", "/YamahaRemoteControl/ctrl", "", headers)
conn.send(xml_req_input)
#response = conn.getresponse()

#print response.status, response.reason
#response_data = response.read()

#print response_data

xmldoc = minidom.parse(conn.getresponse())
xml_input_element = xmldoc.getElementsByTagName('Input_Sel')
for node in xml_input_element:
  av_input = node.firstChild.nodeValue

conn.close()

print "Status\t\tAV Input"
print power_status + "\t\t" + av_input

select_query = "SELECT cur_state,polls_in_state,input FROM dev_status WHERE mac = '00:a0:de:92:6b:78';"
#print update_query
new_polls=1
judodb = sqlite3.connect('/var/www/judo.db')
cur = judodb.cursor()
rows = cur.execute(select_query)
for row in rows:
  prev_state = row[0]
  prev_polls = row[1]
  prev_input = row[2]
if prev_state == power_status and prev_input == av_input:
  new_polls = prev_polls + 1
print prev_state + "\t\t" + str(new_polls)
update_query = "UPDATE dev_status SET input = \'" + av_input + "\', cur_state = \'" + power_status + "\', polls_in_state = " + str(new_polls)  + " WHERE mac = '00:a0:de:92:6b:78'"
cur.execute(update_query)
judodb.commit()
judodb.close()

NEC

NEC responds to ping in Standby and of course don't have UPnP, however the NEC remote control over ethernet let us find out the actual status.
Below python script check the status and store it in sqlite database.
#! /usr/bin/python

import sys
import socket
import sqlite3

monitor_ip = '10.0.0.35'
port = 7142
buffer_size = 2048
data_on = '\x01\x30\x41\x30\x41\x30\x36\x02\x30\x31\x44\x36\x03\x74\x0d'


new = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
new.connect((monitor_ip, port))
new.send(data_on)

data = ''

#message is split over several packets so need to get all content, message we expect have 25bytes
packet = new.recv(buffer_size)
data += packet
while len(data) < 25 or not packet:
   packet = new.recv(buffer_size)
   data += packet

new.close()

#Reponse OFF
#00AB120200D60000040004q
#Response ON
#00AB120200D60000040001t

off_response = b'\x30\x30\x41\x42\x31\x32\x30\x32\x30\x30\x44\x36\x30\x30\x30\x30\x30\x34\x30\x30\x30\x34'
on_response = bytes("\x30\x30\x41\x42\x31\x32\x30\x32\x30\x30\x44\x36\x30\x30\x30\x30\x30\x34\x30\x30\x30\x31")


#there seems to be some not printable chars because of packet fragmentation
#using index is dirty hack but don't care
if data[23] == off_response[21]:
  power_status = "Off"
elif data[23] == on_response[21]:
  power_status = "On"
else:
  power_status = "Unknown"

#Now we need to update sqlite with status

select_query = "SELECT cur_state,polls_in_state FROM dev_status WHERE mac = '00:25:5c:2e:36:39';"
#print update_query
new_polls=1

judodb = sqlite3.connect('/var/www/judo.db')
cur = judodb.cursor()
rows = cur.execute(select_query)
for row in rows:
  prev_state = row[0]
  prev_polls = row[1]
if prev_state == power_status:
  new_polls = prev_polls + 1
print prev_state + "\t\t" + str(new_polls)
update_query = "UPDATE dev_status SET cur_state = \'" + power_status + "\', polls_in_state = " + str(new_polls)  + " WHERE mac = '00:25:5c:2e:36:39'"
cur.execute(update_query)
judodb.commit()
judodb.close()


XBOX 360

For monitoring of XBOX 360 I've have to use arping and UPnP, because of limitation of XBOX 360 OS more can be find here including the script.

Automatic Switch Off


Now as we have ways of getting status we just use crontab to run the scripts each 10 minutes and to get the status and store it in to sqlite database. We are storing also how many time we see it consecutively in current status. (polls_in_state column in table below)

sqlite> .schema dev_status
CREATE TABLE dev_status(mac TEXT, cur_state TEXT, polls_in_state NUM, input TEXT);
sqlite> select * from dev_status;
00:25:5c:2e:36:39|On|3|unknown
00:a0:de:92:6b:78|On|3|HDMI1
00:ce:39:b5:48:9e|Up|4|unknown
7c:1e:52:c2:6c:a7|Up|3|unknown
00:01:2e:3a:a3:b2|Down|1214|unknown
Last thing we have to do is set crontab to run script, which check status based on the status of devices finds what is running and if no device coresponding to AV receiver input is running, (means that AV receiver is running for nothing) it will switch it off. Same for NEC.
crontab -l

#Monitoring of home network
*/10 * * * * /var/www/monitoring/judo_icmp_monitoring &> /dev/null
*/10 * * * * /var/www/monitoring/judo_upnp_monitoring &> /dev/null
*/10 * * * * /var/www/monitoring/HTR-4065_State.py &> /dev/null
*/10 * * * * /var/www/monitoring/NECStatus.py &> /dev/null
*/10 * * * * /var/www/monitoring/XBOX360Status.sh &> /dev/null

5,15,25,35,45,55 * * * * /var/www/monitoring/autostop >> /media/nas/public/autostop.log

autostop script
#! /bin/bash

HTR_MAC='00:a0:de:92:6b:78'
NEC_MAC='58:C2:32:97:3A:48'
Evolve_MAC='00:ce:39:b5:48:9e'
XBOX_MAC='7c:1e:52:c2:6c:a7'
Zotac_MAC='00:01:2e:3a:a3:b2'

CGIDIR='/var/www/cgi-bin'
DBFILE='/var/www/judo.db'

getDeviceStatus(){
  SQLQUERY="SELECT cur_state,polls_in_state,input FROM dev_status WHERE mac = '$1';"
  SQLRESULT=`sqlite3 $DBFILE "$SQLQUERY" | sed 's/|/ /g;'`
  echo "$SQLRESULT"
}


ACTION='No action'

echo -n ""

read HTR_STATUS HTR_POLLS_IN_STATUS HTR_INPUT <<< `getDeviceStatus $HTR_MAC`
echo -n "$HTR_STATUS $HTR_POLLS_IN_STATUS $HTR_INPUT"

#Example of output
#00:a0:de:92:6b:78|On|51|HDMI1

if [[ $HTR_STATUS == "On" && $HTR_POLLS_IN_STATUS -gt 3 ]]
then
  case $HTR_INPUT in
    "HDMI1")
       #echo "Check Evolve status"
       read Evolve_STATUS Evolve_POLLS_IN_STATUS Evolve_INPUT <<< `getDeviceStatus $Evolve_MAC`
       echo -n "$Evolve_STATUS $Evolve_POLLS_IN_STATUS $Evolve_INPUT"
       if [[ $Evolve_STATUS == "Down" && $Evolve_POLLS_IN_STATUS -gt 3 ]]
       then
         $CGIDIR/NECOff.py
         $CGIDIR/HTR-4065_Off.py
         ACTION="Switching HTR-4065 and NEC off"
       fi
       ;;
    "HDMI2")
       #echo "Check XBOX 360 status"
       read XBOX_STATUS XBOX_POLLS_IN_STATUS XBOX_INPUT <<< `getDeviceStatus $XBOX_MAC`
       echo -n "$XBOX_STATUS $XBOX_POLLS_IN_STATUS $XBOX_INPUT"
       if [[ $XBOX_STATUS == "Down" && $XBOX_POLLS_IN_STATUS -gt 3 ]]
       then
         $CGIDIR/NECOff.py
         $CGIDIR/HTR-4065_Off.py
         ACTION="Switching HTR-4065 and NEC off"
       fi
       ;;
    "HDMI3")
       #echo "Checking PC Zotac Status"
       read Zotac_STATUS Zotac_POLLS_IN_STATUS Zotac_INPUT <<< `getDeviceStatus $Zotac_MAC`
       echo -n "$Zotac_STATUS $Zotac_POLLS_IN_STATUS $Zotac_INPUT"
       if [[ $Zotac_STATUS == "Down" && $Zotac_POLLS_IN_STATUS -gt 3 ]]
       then
         $CGIDIR/NECOff.py
         $CGIDIR/HTR-4065_Off.py
         ACTION="Switching HTR-4065 and NEC off"
       fi
       ;;
  esac
else
  #AV reciever is Standby for more then hour and NEC is On for
  if [[ ! $HTR_STATUS == "On" ]]
  then
    read NEC_STATUS NEC_POLLS_IN_STATUS NEC_INPUT <<< `getDeviceStatus $NEC_MAC`
    echo -n "$NEC_STATUS $NEC_POLLS_IN_STATUS $NEC_INPUT"

    if [[ $HTR_POLLS_IN_STATE -gt 6 && $NEC_STATUS == "On" && $NEC_POLLS_IN_STATUS -gt 3 ]]
    then
      $CGIDIR/NECoff.py
      ACTION="Switching NEC Off"
    fi
  fi
fi

echo -n "$ACTION"
echo ""
#HTR-4065 HDMI1 : Evolve Down,3 => OFF: NEC, HTR-4065
#HTR-4065 HDMI2 : XBOX_360 Down,3 => OFF: NEC, HTR-4065
#HTR-4065 HDMI3 : PC_Zotac Down,3 => OFF: NEC, HTR-4065
#HTR-4065 HDMI4 : Not Assigned

Thursday, March 26, 2015

XBOX 360 monitoring

XBOX 360 is great even it's "old" already compared to XBOX One and other next gen consoles. Never the less I like XBOX 360 even though I'm missing few thinks (limited USB stack implementation, missing Wake On LAN).

However I've decided I shall monitor my entertaining systems to be able switch off AV reciever or Display if forget to. For this I need to know if my Xbox 360, PC or DVB-T is running.... I'll cover this "auto switch off system" in another post.

During my exploration I find following problems:
  1. ping is not the option, XBOX 360 doesn't reply to ICMP request
  2. UPnP is running only when not playing game
  3. arping works only with IP, if only MAC address is provide it will send request with broadcast IP which xbox will not respond
So the solution is to set for XBOX fixed IP and use:
  1. arping to verify if device is up
  2. gssdp-discovery to check if game is running (and get UPnP Port if not)

Below is script using arping to verify if device is up and if so then find what UPnP port is it listening(if game is not played), results are stored in sqlite db for purpose of further use

#! /bin/bash


#test ICMP connectivity with ping
testARPing()
{
  return `arping -c 4 $1 | grep "100% unanswered" | wc -l`
}

XBOXIP='10.0.0.41'
XBOXMAC='7c:1e:52:c2:6c:a7'
XBOXUUID='03166842-2743-2000-0000-7c1e52c26ca7'

DBFILE='/var/www/judo.db'

if `testARPing $XBOXIP`
  then
    new_status="Up"
    location=`gssdp-discover -n 10 -t uuid:$XBOXUUID | grep "Location:" | sed 's/ *Location: http:\/\///g'| cut -d/ -f1 | sed 's/\///g'`
    #echo "$location"
    #ip=$(echo "$location" | cut -d: -f1)
    upnp_port=$(echo "$location" | cut -d: -f2)
  else
    new_status="Down"
fi

new_polls=1
prev_status_polls=$(sqlite3 $DBFILE "SELECT cur_state,polls_in_state FROM dev_status WHERE mac = '$XBOXMAC';")
prev_status=$(echo "$prev_status_polls" | cut -d\| -f1)
prev_polls=$(echo "$prev_status_polls" | cut -d\| -f2)
if [ "$prev_status" == "$new_status" ]
then
  new_polls=$((prev_polls + 1))
fi
sqlite3 $DBFILE "UPDATE dev_status SET cur_state = '$new_status', polls_in_state = '$new_polls' WHERE mac = '$XBOXMAC';"
sqlite3 $DBFILE "UPDATE devices SET upnp_port = '$upnp_port' WHERE mac = '$XBOXMAC';"
echo "$XBOXIP,$XBOXMAC,$new_status,$prev_status,$new_polls"

Tuesday, March 17, 2015

Home printer monitoring - Brother DCP-J4110DW

As I'm trying to automate and monitor everything in my home I wanted to monitor my printer/scanner, Brother DCP-J4110DW.

First thing I did, and I do with every device I have, here is what I find.
21/tcp   open  ftp -
220 FTP print service:V-1.13/Use the network password for the ID if updating.
Name (10.0.0.39:pi): admin
23/tcp   open  telnet - Doesn't ask for anything(no request for user or password, no command-line)
25/tcp   open  smtp
80/tcp   open  http - Web Management and setting
443/tcp  open  https -  Web Management and setting
515/tcp  open  printer
631/tcp  open  ipp
9100/tcp open  jetdirect

Second thing is snmpwalk with community public and in this case it works (even I didn't find snmp agent setting in printer configuration). I've tried to find following statistics:
    Total Pages printed - iso.3.6.1.2.1.43.10.2.1.4.1.1 = Counter32: 115
    Monochromatic pages - iso.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.52.1.1.3.3 = Counter32: 17
    Color pages - iso.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.52.1.1.3.4 = Counter32: 98

I was not successful to get ink level, even though the OIDs for those are present the value given is 'Unknown', most probably this is only implemented on printers targeted for companies.
However I'm able to see the ink level on status webpage (status webpages is accessible without login which is fine and easier to get statistics).
The html code we need to get info from is looking like this below and ink level is actually the height of the color image.
<div id="ink_level">
<table id="inkLevel" summary="ink level">
<tr><th></th><th></th><th></th><th><img src="../common/images/low.gif" alt="Low" /></th></tr><tr>
<td><img src="../common/images/magenta.gif" alt="Magenta" class="tonerremain" height="23px" /></td>
<td><img src="../common/images/cyan.gif" alt="Cyan" class="tonerremain" height="18px" /></td>
<td><img src="../common/images/yellow.gif" alt="Yellow" class="tonerremain" height="16px" /></td>
<td><img src="../common/images/black.gif" alt="Black" class="tonerremain" height="9px" /></td>
</tr><tr><th>M</th><th>C</th><th>Y</th><th>BK</th></tr>
</table>
</div>

So I wrote following code in python (Brother_inkLevel) to get the ink level and provide in text output which would be easy to parse by other scripts Brother_inkLevel.py


#! /usr/bin/python

import sys
import httplib, urllib
import re
from xml.dom import minidom
from xml.parsers.expat import ExpatError

#get status html
url = 'http://10.0.0.39/general/status.html'
html = urllib.urlopen(url).read()

inkLevel = ''

#parse hmtl
#get all img tags
#if it's inklevel img I get the height, it's represent the ink level
xmldoc = minidom.parseString(html)
xml_images = xmldoc.getElementsByTagName('img')
for img in xml_images:
  if img.getAttribute('alt') == 'Cyan':
    c = "C:" + img.getAttribute('height') + " "
  elif img.getAttribute('alt') == 'Magenta':
    m = "M:" + img.getAttribute('height') + " "
  elif img.getAttribute('alt') == 'Yellow':
    y = "Y:" + img.getAttribute('height') + " "
  elif img.getAttribute('alt') == 'Black':
    k = "K:" + img.getAttribute('height') + " "

inkLevel = c + m + y + k
print inkLevel


And here is the output of the script
root@raspberrypi:/var/www/monitoring# ./Brother_inkLevel.py
C:18px M:23px Y:16px K:9px

Next I used bash script to get printed page statistics, include ink Level and store it in sqlite db
#! /bin/bash

BROTHER_IP='10.0.0.39'
UUID='e3248000-80ce-11db-8000-30055c1cd6d3'
COMMUNITY='public'

DBFILE='/var/www/judo.db'
RD='/var/www/monitoring'

DATE=`date +%Y-%m-%d`

MONO_PAGE_OID='.1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.52.1.1.3.3'
COLOR_PAGE_OID='.1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.52.1.1.3.4'
TOTAL_PAGE_OID='.1.3.6.1.2.1.43.10.2.1.4.1.1'

MONO_PAGE_COUNT=`/usr/bin/snmpget -v 2c -c $COMMUNITY $BROTHER_IP $COLOR_PAGE_OID | awk '{print $4}'`
COLOR_PAGE_COUNT=`/usr/bin/snmpget -v 2c -c $COMMUNITY $BROTHER_IP $MONO_PAGE_OID | awk '{print $4}'`
TOTAL_PAGE_COUNT=`/usr/bin/snmpget -v 2c -c $COMMUNITY $BROTHER_IP $TOTAL_PAGE_OID | awk '{print $4}'`

echo "Monochromatic pages: $MONO_PAGE_COUNT, Color pages: $COLOR_PAGE_COUNT, Total pages: $TOTAL_PAGE_COUNT"

INKLEVEL=`$RD/Brother_inkLevel.py | sed 's/px/,/g;' | sed 's/C://g;' | sed 's/M://g' | sed 's/Y://g;' | sed 's/K://g;' | sed 's/, $//g;'`

echo "$INKLEVEL"

sqlite3 $DBFILE "INSERT INTO Brother_Daily_Stats VALUES('$UUID', '$DATE', $TOTAL_PAGE_COUNT, $MONO_PAGE_COUNT, $COLOR_PAGE_COUNT, $INKLEVEL);"

I've let that run for few days and bellow are results, however to get more relevant info I'll be monitoring for one year and update here more findings.
sqlite> select * from Brother_Daily_Stats;
e3248000-80ce-11db-8000-30055c1cd6d3|2015-02-21|137|119|18|18|23|16|9
e3248000-80ce-11db-8000-30055c1cd6d3|2015-02-22|137|119|18|18|23|16|9
e3248000-80ce-11db-8000-30055c1cd6d3|2015-02-23|137|119|18|18|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-02-24|137|119|18|18|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-02-25|137|119|18|18|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-02-26|137|119|18|18|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-02-27|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-02-28|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-01|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-02|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-03|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-04|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-05|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-06|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-07|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-08|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-09|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-10|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-11|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-12|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-13|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-14|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-15|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-16|137|119|18|13|19|11|7
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-17|137|119|18|13|18|11|5

Thursday, January 29, 2015

Understanding binary file

Most of DVB-Ts and TVs got channel list in file called dtv_channel.txt Of course there are tools to manipulate this file content, but most of them is for windows and I just need export the list to show it in my web customer remote controler.
We begin with hexdump, but to understand it better we convert the output to human readable ASCII and add underscore after each char to separate them for better reading using following command:

hexdump -e '"%_u\_"' dtv_channel.txt

syn_nul_so_nul_lf_nul_* 
O_c_k_o_ _G_o_l_d_nul_* 
88_fe_85_dle_h_b4_85_dle_80_V___._nul_* 
bs_nul_* 
cb_ _stx_etx_stx_eot_nul_* 
soh_nul_* 
dle_nul_ht_nul_* 
O_c_k_o_ _G_o_l_d_nul_* 
soh_etb_soh_nul_* 
soh_etb_stx_nul_* 
soh_nul_* 
stx_etb_etx_nul_e_z_c_nul_* 
ff_nul_* 
ff_nul_* 
etb_nul_si_nul_ht_nul_* 
S_l_a_g_r_ _T_V_nul_* 
dle_stx_86_dle_nul_fb_85_dle_80_V___._nul_* 
bs_nul_* 
cb_ _stx_etx_soh_syn_nul_* 
soh_nul_* 
dle_nul_bs_nul_* 
S_l_a_g_r_ _T_V_nul_* 
soh_ht_soh_nul_* 
soh_ht_stx_nul_* 
soh_nul_* 
stx_ht_etx_nul_e_z_c_nul_* 
ff_nul_* 
ff_nul_* 
can_nul_dle_nul_bel_nul_* 
A_C_T_I_V_E_nul_* 
88_fe_85_dle_80_V___._nul_* 
bs_nul_* 
cb_ _stx_etx_soh_fs_nul_* 
soh_nul_* 
dle_nul_ack_nul_* 
A_C_T_I_V_E_nul_* 
soh_nak_soh_nul_* 
soh_nak_stx_nul_* 
soh_nul_* 
stx_nak_etx_nul_e_z_c_nul_* 
ff_nul_* 
ff_nul_*
After closer look we can see that there is clear record separator (we can divide to rows)
ff_nul_* 
ff_nul_*
Another think we can see is each "line" ends with nul_* in fact this in the reality it's more nuls. Some file formats use fix length for filed and if data is shorter it's filled with nuls which is this case.

So let's expect that nul* is field separator. Also there are lines containing bs_nul_* and soh_nul_* and they repeat in pattern, so most probably this is also some kind of separator.

Now lets used couple of seds to get some more readable format which we can further analyze.
hexdump -e '"%_u\_"' dtv_channel.txt | sed 's/^ff_nul_\*$/;;/g' | sed 's/nul_\*/,/g' | sed 's/^soh_,$//g' | sed 's/^bs_,$//g' | while read line; do echo -n $line; done | sed 's/;;/;\n/g' | sed '/^;$/d'

Let me explain for those not familiar:
#substitute nul_* with , (comma), we will use comma as filed separator
sed 's/nul_\*/,/g'
#remove lines which contains only soh_, and bs_,
sed 's/^soh_,$//g' | sed 's/^bs_,$//g'
#now let's get all data in one single line
while read line; do echo -n $line;
#and get it separated in to lines based and add ; (semicolon) as clear record separator I used just one ff_, because I find sometimes not every two ff_ separate record (not sure why)
sed 's/,ff_,/;\n/g'

we can remove Following from begining as it's file header ack_,ff_em_,

Now this is example of output
soh_nul_soh_nul_bs_,C_T_ _1_ _J_M_,f8_c7_85_dle_,80_:_dcl_ _,cb_ _dc2_soh_*,dle_nul_bel_,C_T_ _1_ _J_M_,soh_*,soh_*stx_,stx_,dcl_soh_etx_nul_e_z_c_nul_dc3_soh_etx_,;
!_soh_*,e_z_c_,;
enq_nul_soh_nul_dle_,C_R_o_ _R_A_D_I_O_Z_U_R_N_A_L_,`_8d_85_dle_b8_83_85_dle_80_:_dcl_ _,cb_ _dc2_soh_*A_,stx_,_nul_si_,C_R_o_ _R_A_D_I_O_Z_U_R_N_A_L_,dcl_dle_,dcl_dle_etx_nul_e_z_c_,;
stx_nul_stx_nul_enq_,C_T_ _2_,80_cb_85_dle_f8_c7_85_dle_80_:_dcl_ _,cb_ _dc2_soh_stx_soh_,dle_nul_eot_,C_T_ _2_,soh_stx_soh_,soh_stx_*,stx_,dcl_stx_etx_nul_e_z_c_nul_dc3_stx_etx_,;
!_stx_soh_*,e_z_c_,;
ack_nul_stx_nul_vt_,C_R_o_ _D_V_O_J_K_A_,p_d4_85_dle_`_8d_85_dle_80_:_dcl_ _,cb_ _dc2_soh_stx_A_,stx_,_nul_lf_,C_R_o_ _D_V_O_J_K_A_,dcl_*,dcl_*etx_nul_e_z_c_,;
etx_nul_etx_nul_ack_,C_T_ _2_4_,bs_cf_85_dle_80_cb_85_dle_80_:_dcl_ _,cb_ _dc2_soh_etx_soh_,dle_nul_enq_,C_T_ _2_4_,soh_etx_soh_,soh_etx_stx_,stx_,dcl_etx_*nul_e_z_c_nul_dc3_etx_*,;
!_etx_soh_*,e_z_c_,;

we can see some shorter line like this !_stx_soh_*,e_z_c_,; if we take closer look we will find that nul_* is not always field, separator (nuls can be valid part of data).
Never the less we can remove those shorter lines for the moment or manually move them

So now we have "nice" output we can open in any spreadsheet program and parse by comma. We see that 2nd column contains Name of channel. If we elaborate more on first column we can see that nul_ separate some 3 values.

soh_nul_soh_nul_bs_
enq_nul_soh_nul_dle_
stx_nul_stx_nul_enq_
ack_nul_stx_nul_vt_
etx_nul_etx_nul_ack_
bel_nul_etx_nul_vt_
eot_nul_eot_nul_ht_
bs_nul_eot_nul_si_
ht_nul_enq_nul_lf_
cr_nul_enq_nul_ack_
lf_nul_ack_nul_dcl_
so_nul_ack_nul_ff_
1st is actually 1,5,2,6,3,7,4,8,9,13... as far as values are unique I guess this might be index
2nd values are actually sequential, but twice (1,1,2,2,3,3...) that's because there are TV's and Radios mixed together.
3rd as we can see values repeating here randomly so there is and values are: 8,16,5,11,6,11,.... this is slightly complected, but actually it's length of channel name+1

As already mentioned the file use fix length of fields and records, we already have some knowledge of the data so lets try to find the length of record and hopefully also fields, to get that lets use following command:

hexdump -C dtv_channel.txt

Lets use colors to highlight few thinks
Beginning of record
Index, Channel Number, Length of channel name
Channel Name

AddressData in HEXHuman readable data
00004da000 00 00 00 00 00 00 00 17 00 0f 00 09 00 00 00|................|
00004db053 6c 61 67 72 20 54 56 00 00 00 00 00 00 00 00|Slagr TV........|
00004dc000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00|................|
00004dd010 02 86 10 00 fb 85 10 80 56 5f 2e 00 00 00 00|.........V_.....|
00004de008 00 00 00 cb 20 02 03 01 16 00 00 00 00 00 00|..... ..........|
00004df001 00 00 00 00 00 10 00 08 00 00 00 53 6c 61 67|............Slag|
00004e0072 20 54 56 00 00 00 00 00 00 00 00 00 00 00 00|r TV............|
00004e1000 00 00 00 00 00 00 00 00 00 00 00 01 09 01 00|................|
00004e2000 00 00 00 01 09 02 00 00 00 00 00 00 00 00 00|................|
00004e3000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00|................|
*
00004e8000 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00|................|
00004e9002 09 03 00 65 7a 63 00 00 00 00 00 00 00 00 00|....ezc.........|
00004ea000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00|................|
*
00004ed000 ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00|................|
00004ee000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00|................|
*
000050b000 ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00|................|
000050c000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00|................|
*
0000510000 00 00 00 00 00 00 00 18 00 10 00 07 00 00 00|................|
0000511041 43 54 49 56 45 00 00 00 00 00 00 00 00 00 00|ACTIVE..........|
0000512000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00|................|
0000513000 00 00 00 88 fe 85 10 80 56 5f 2e 00 00 00 00|.........V_.....|
0000514008 00 00 00 cb 20 02 03 01 1c 00 00 00 00 00 00|..... ..........|
0000515001 00 00 00 00 00 10 00 06 00 00 00 41 43 54 49|............ACTI|
0000516056 45 00 00 00 00 00 00 00 00 00 00 00 00 00 00|VE..............|
0000517000 00 00 00 00 00 00 00 00 00 00 00 01 15 01 00|................|
0000518000 00 00 00 01 15 02 00 00 00 00 00 00 00 00 00|................|
0000519000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00|................|
*
000051e000 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00|................|
000051f002 15 03 00 65 7a 63 00 00 00 00 00 00 00 00 00|....ezc.........|
0000520000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00|................|
*
0000523000 ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00|................|
0000524000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00|................|
*
0000541000 ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00|................|
0000542000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00|................|
*
0000546000 00 00 00 00 00 00 00 47 0e f9 12 |........G...|

by subtracting two addresses of record beginning
4da0 (19872 Dec)
5100 (20736 Dec)
we will get record length = 864
first 8 bytes can be droped

Now with this knowledge we can split into records, we can use split -b 864 and we will have one record per file. The we can use following to check more on structure of records:

for f in `ls x*`; do echo $f; hd $f; done

xat
00000000  00 00 00 00 00 00 00 00  13 00 0b 00 0b 00 00 00  |................|
00000010  50 72 69 6d 61 20 4c 4f  56 45 00 00 00 00 00 00  |Prima LOVE......|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  e0 b0 85 10 d0 a9 85 10  80 56 5f 2e 00 00 00 00  |.........V_.....|
00000040  08 00 00 00 cb 20 02 03  04 03 00 00 00 00 00 00  |..... ..........|
00000050  01 00 00 00 00 00 10 00  0a 00 00 00 50 72 69 6d  |............Prim|
00000060  61 20 4c 4f 56 45 00 00  00 00 00 00 00 00 00 00  |a LOVE..........|
00000070  00 00 00 00 00 00 00 00  00 00 00 00 01 02 01 00  |................|
00000080  00 00 00 00 01 02 02 00  00 00 00 00 00 00 00 00  |................|
00000090  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000000e0  00 00 00 00 01 00 00 00  00 00 00 00 00 00 00 00  |................|
000000f0  02 02 03 00 65 7a 63 00  00 00 00 00 00 00 00 00  |....ezc.........|
00000100  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000130  00 ff 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000140  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000002b0  00 00 00 00 00 00 00 00  00 00 00 00 01 00 00 00  |................|
000002c0  00 00 00 00 00 00 00 00  03 02 01 01 00 00 00 00  |................|
000002d0  65 7a 63 00 00 00 00 00  00 00 00 00 00 00 00 00  |ezc.............|
000002e0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000310  00 ff 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000320  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000360
xau
00000000  00 00 00 00 00 00 00 00  14 00 0c 00 0b 00 00 00  |................|
00000010  50 72 69 6d 61 20 5a 4f  4f 4d 00 00 00 00 00 00  |Prima ZOOM......|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  68 b4 85 10 58 ad 85 10  80 56 5f 2e 00 00 00 00  |h...X....V_.....|
00000040  08 00 00 00 cb 20 02 03  06 03 00 00 00 00 00 00  |..... ..........|
00000050  01 00 00 00 00 00 10 00  0a 00 00 00 50 72 69 6d  |............Prim|
00000060  61 20 5a 4f 4f 4d 00 00  00 00 00 00 00 00 00 00  |a ZOOM..........|
00000070  00 00 00 00 00 00 00 00  00 00 00 00 01 14 01 00  |................|
00000080  00 00 00 00 01 14 02 00  00 00 00 00 00 00 00 00  |................|
00000090  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000000e0  00 00 00 00 01 00 00 00  00 00 00 00 00 00 00 00  |................|
000000f0  02 14 03 00 65 7a 63 00  00 00 00 00 00 00 00 00  |....ezc.........|
00000100  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000130  00 ff 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000140  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000310  00 ff 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000320  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000360


We can see that from address 310 there are just null's, we also can see that in some cases there are data between address 90 and 310, however checking on context those data doesn't help us to recognize between radio and TV channel so we can actually do two more things to make more clear:

We can just print first 144 bytes and also we can drop first 8 bytes as we figure out previously and compare again, now it will be easier to find what's always same for Radio stations and TV Stations.

root@raspberrypi:/media/nas/public# for f in `ls x*`; do echo $f; hexdump -s 8 -n 144 -C $f; done
xaa
00000008  01 00 01 00 08 00 00 00  43 54 20 31 20 4a 4d 00  |........CT 1 JM.|
00000018  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000028  00 00 00 00 00 00 00 00  f8 c7 85 10 00 00 00 00  |................|
00000038  80 3a 11 20 00 00 00 00  08 00 00 00 cb 20 12 01  |.:. ......... ..|
00000048  01 01 00 00 00 00 00 00  01 00 00 00 00 00 10 00  |................|
00000058  07 00 00 00 43 54 20 31  20 4a 4d 00 00 00 00 00  |....CT 1 JM.....|
00000068  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000078  00 00 00 00 01 01 01 00  00 00 00 00 01 01 02 00  |................|
00000088  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000098
xab
00000008  05 00 01 00 10 00 00 00  43 52 6f 20 52 41 44 49  |........CRo RADI|
00000018  4f 5a 55 52 4e 41 4c 00  00 00 00 00 00 00 00 00  |OZURNAL.........|
00000028  00 00 00 00 00 00 00 00  60 8d 85 10 b8 83 85 10  |........`.......|
00000038  80 3a 11 20 00 00 00 00  08 00 00 00 cb 20 12 01  |.:. ......... ..|
00000048  01 41 00 00 00 00 00 00  02 00 00 00 00 00 20 00  |.A............ .|
00000058  0f 00 00 00 43 52 6f 20  52 41 44 49 4f 5a 55 52  |....CRo RADIOZUR|
00000068  4e 41 4c 00 00 00 00 00  00 00 00 00 00 00 00 00  |NAL.............|
00000078  00 00 00 00 11 10 00 00  00 00 00 00 00 00 00 00  |................|
00000088  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000098
xac
00000008  02 00 02 00 05 00 00 00  43 54 20 32 00 00 00 00  |........CT 2....|
00000018  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000028  00 00 00 00 00 00 00 00  80 cb 85 10 f8 c7 85 10  |................|
00000038  80 3a 11 20 00 00 00 00  08 00 00 00 cb 20 12 01  |.:. ......... ..|
00000048  02 01 00 00 00 00 00 00  01 00 00 00 00 00 10 00  |................|
00000058  04 00 00 00 43 54 20 32  00 00 00 00 00 00 00 00  |....CT 2........|
00000068  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000078  00 00 00 00 01 02 01 00  00 00 00 00 01 02 02 00  |................|
00000088  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000098
xad
00000008  06 00 02 00 0b 00 00 00  43 52 6f 20 44 56 4f 4a  |........CRo DVOJ|
00000018  4b 41 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |KA..............|
00000028  00 00 00 00 00 00 00 00  70 d4 85 10 60 8d 85 10  |........p...`...|
00000038  80 3a 11 20 00 00 00 00  08 00 00 00 cb 20 12 01  |.:. ......... ..|
00000048  02 41 00 00 00 00 00 00  02 00 00 00 00 00 20 00  |.A............ .|
00000058  0a 00 00 00 43 52 6f 20  44 56 4f 4a 4b 41 00 00  |....CRo DVOJKA..|
00000068  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000078  00 00 00 00 11 11 00 00  00 00 00 00 00 00 00 00  |................|
00000088  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000098
xae
00000008  03 00 03 00 06 00 00 00  43 54 20 32 34 00 00 00  |........CT 24...|
00000018  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000028  00 00 00 00 00 00 00 00  08 cf 85 10 80 cb 85 10  |................|
00000038  80 3a 11 20 00 00 00 00  08 00 00 00 cb 20 12 01  |.:. ......... ..|
00000048  03 01 00 00 00 00 00 00  01 00 00 00 00 00 10 00  |................|
00000058  05 00 00 00 43 54 20 32  34 00 00 00 00 00 00 00  |....CT 24.......|
00000068  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000078  00 00 00 00 01 03 01 00  00 00 00 00 01 03 02 00  |................|
00000088  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000098
xaf
00000008  07 00 03 00 0b 00 00 00  43 52 6f 20 56 4c 54 41  |........CRo VLTA|
00000018  56 41 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |VA..............|
00000028  00 00 00 00 00 00 00 00  70 c4 85 10 70 d4 85 10  |........p...p...|
00000038  80 3a 11 20 00 00 00 00  08 00 00 00 cb 20 12 01  |.:. ......... ..|
00000048  03 41 00 00 00 00 00 00  02 00 00 00 00 00 20 00  |.A............ .|
00000058  0a 00 00 00 43 52 6f 20  56 4c 54 41 56 41 00 00  |....CRo VLTAVA..|
00000068  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000078  00 00 00 00 11 12 00 00  00 00 00 00 00 00 00 00  |................|
00000088  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000098
xag
00000008  04 00 04 00 09 00 00 00  43 54 20 73 70 6f 72 74  |........CT sport|
00000018  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000028  00 00 00 00 00 00 00 00  58 53 85 10 08 cf 85 10  |........XS......|
00000038  80 3a 11 20 00 00 00 00  08 00 00 00 cb 20 12 01  |.:. ......... ..|
00000048  04 01 00 00 00 00 00 00  01 00 00 00 00 00 10 00  |................|
00000058  08 00 00 00 43 54 20 73  70 6f 72 74 00 00 00 00  |....CT sport....|
00000068  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000078  00 00 00 00 01 04 01 00  00 00 00 00 01 04 02 00  |................|
00000088  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000098

So now I have everything I need I just need to have it in nice output:

Channel number
Channel name
Type of channel (Radio/TV)

00000008  01 00 01 00 08 00 00 00  43 54 20 31 20 4a 4d 00  |........CT 1 JM.|
00000018  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000028  00 00 00 00 00 00 00 00  f8 c7 85 10 00 00 00 00  |................|
00000038  80 3a 11 20 00 00 00 00  08 00 00 00 cb 20 12 01  |.:. ......... ..|
00000048  01 01 00 00 00 00 00 00  01 00 00 00 00 00 10 00  |................|
00000058  07 00 00 00 43 54 20 31  20 4a 4d 00 00 00 00 00  |....CT 1 JM.....|
00000068  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000078  00 00 00 00 01 01 01 00  00 00 00 00 01 01 02 00  |................|
00000088  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000098

#! /bin/sh

DTV_FILE='/media/nas/public/dtv_channel.txt'
split -b 864 $DTV_FILE

for f in `ls x*`
do
  channel_num_hd=`hexdump -s 10 -n 1 -e '"%_u"' $f`
  channel_num=`ascii -t $channel_num_hd | cut -d ' ' -f 4`
  channel_type=`hexdump -s 126 -n 1 -e '"%_u"' $f | sed 's/soh/TV/' | sed 's/nul/Radio/'`
  channel_name=`hexdump -s 16 -n 32 -v -e '"%_u\_"' $f | sed 's/nul_*//g' | sed 's/_//g'`
  echo "$channel_num,$channel_type,$channel_name"
done

rm -f x*