#!/bin/sh
#
# DOCSIS cable load monitor
#
# tool to monitor downstream load on DOCSIS cable networks
#
#
https://github.com/sp4rkie/docsis-cable-load-monitor #
cat > $0_$$.awk << !
func setup( \
)
{
CLARGS = "$0|$1|$2|$3|$4|$5|$6|$7|$8|$9|${10}|${11}|${12}|${13}|${14}|${15}|${16}|${17}|${18}|${19}|${20}"
# avoid dangling include files. incore inode still exists
system("rm $0_$$.awk")
# take care of shell expansion (if security is an issue)
setup_lib($#, "awk|" CLARGS, $$, "`pwd`", "`hostname`", "$R_")
PRP(CLARGS "\n")
# required if Sundtek hardware is in use as we install with '-service' option
LD_PL = "LD_PRELOAD=/opt/lib/libmediaclient.so "
# where to find your TC4400 cable modem (if any)
CABLE_MODEM_IP = "192.168.100.1"
# file current recording parameters as incompatible changes must trigger a new RRD generation
CONF_FILE = INSTPATH "CableLoadMonitor.cfg"
FREQ_FACTOR = 1000000
# default RRDBASE
RRDBASE_FILE = INSTPATH "CableLoadMonitor.rrd"
RRDBASE_RRASTEPS = 10 # defaults to 10s
RRDBASE_HISTSIZE = 60 * 60 * 24 * 7 # defaults to 1 week
# default RRDGRAPH
RRDGRAPH_NAME = "/var/www/html/" "CableLoadMonitor"
RRDGRAPH_EXT = ".png"
RRDGRAPH_TMP = RRDGRAPH_NAME RRDGRAPH_EXT "_"
RRDGRAPH_WIDTH = 2000
RRDGRAPH_HEIGHT = 1000
RRDGRAPH_Y_UPPER_LIMIT = 200 # scale Y axis to this minimum
BWIDTH_SNAPSHOT = RRDGRAPH_NAME "_bwidth.txt"
# feel free to add other history sizes (aka generated graphs) as well
RRDGRAPHS[RRDGRAPHS_CNT++] = 60 * 60 " |1h"
RRDGRAPHS[RRDGRAPHS_CNT++] = 60 * 60 * 6 " |6h"
RRDGRAPHS[RRDGRAPHS_CNT++] = 60 * 60 * 24 " |1d"
RRDGRAPHS[RRDGRAPHS_CNT++] = 60 * 60 * 24 * 7 " |7d"
RRDGRAPHS[RRDGRAPHS_CNT++] = 60 * 60 * 24 * 30 " |30d"
RRDGRAPHS[RRDGRAPHS_CNT++] = 60 * 60 * 24 * 365 " |365d"
# ---
rrdbase_rrasteps = RRDBASE_RRASTEPS
rrdbase_histsize = RRDBASE_HISTSIZE
rrdgraph_width = RRDGRAPH_WIDTH
rrdgraph_height = RRDGRAPH_HEIGHT
# --- define this to display a black theme
if (++use_black_theme) {
IF_BLACK_THEME = IF_BLACK_THEME "--color \"ARROW#ffffff\"" " \\\\\n"
IF_BLACK_THEME = IF_BLACK_THEME "--color \"BACK#000000\"" " \\\\\n"
IF_BLACK_THEME = IF_BLACK_THEME "--color \"CANVAS#000000\"" " \\\\\n"
IF_BLACK_THEME = IF_BLACK_THEME "--color \"GRID#ffffff\"" " \\\\\n"
IF_BLACK_THEME = IF_BLACK_THEME "--color \"MGRID#ffffff\"" " \\\\\n"
IF_BLACK_THEME = IF_BLACK_THEME "--color \"FONT#ffffff\"" " \\\\\n"
IF_BLACK_THEME = IF_BLACK_THEME "--color \"AXIS#ffffff\"" " \\\\\n"
IF_BLACK_THEME = IF_BLACK_THEME "--color \"FRAME#ffffff\"" " \\\\\n"
IF_BLACK_THEME = IF_BLACK_THEME "--border 1" " \\\\\n"
}
# --- define this to rescale the sum graph
if (use_2nd_yaxis) {
IF_2ND_YAXIS_SCALE = 10
IF_2ND_YAXIS = IF_2ND_YAXIS "--right-axis " 1 / IF_2ND_YAXIS_SCALE ":1" " \\\\\n"
IF_2ND_YAXIS = IF_2ND_YAXIS "--right-axis-label sum" " \\\\\n"
SUM_DEF = SUM_DEF sprintf("DEF:sum_=" RRDBASE_FILE ":sum:AVERAGE", i, i) " \\\\\n"
SUM_DEF = SUM_DEF "CDEF:sum=sum_," IF_2ND_YAXIS_SCALE ",/" " \\\\\n"
} else {
SUM_DEF = SUM_DEF sprintf("DEF:sum=" RRDBASE_FILE ":sum:AVERAGE", i, i) " \\\\\n"
}
}
func read_cfg( \
line, CMD, a)
{
if (ex("ls " CONF_FILE, 0, "^(" CONF_FILE ")$")) {
CMD = "cat " CONF_FILE
#PR("reading last recently used downstream channel frequencies and RRD parameters")
FREQ_SPEC_CFG = ex_line(CMD)
while ((line = ex_line(CMD)) != -1) {
if (match(line, "^RRA:AVERAGE:.*

[0-9]+)

[0-9]+)$", a)) {
RRA_STEPS_CFG = a[1]
HIST_SIZE_CFG = a[2]
}
}
}
}
func exit_(msg \
)
{
PRE(msg)
PRE("exiting...")
++ERR; exit
}
func set_freqs(str, \
i, a)
{
if (!match(str, "^[0-9]+

[0-9]+)*$")) {
return 0
}
split(str, a, ":")
for (i = 1; a
; ++i) {
FREQ[FREQ_CNT++] = a
}
return 1
}
func retrieve_down_freqs(freqs, \
URL, ignore, line, WGET, LYNX, i, a)
{
URL = "http://" CABLE_MODEM_IP "/cmconnectionstatus.html"
if (freqs == "force_new_scan" || !freqs) {
#
# specific to TC4400 yet
#
PR("trying to retrieve downstream channel frequencies from TC4400 modem")
WGET = "echo; wget --connect-timeout=1 -t 1 -q --http-user=admin --http-password=\"bEn2o#US9s\" " URL " -O -"
LYNX = "lynx -nolist -width 300 -dump -stdin"
while ((line = ex_line(WGET)) != -1) {
if (match(line, "<script")) {
++ignore
} else if (ignore) {
if (match(line, "/script?")) {
ignore = 0
}
} else {
print line |& LYNX
}
}
close(LYNX, "to")
while (LYNX |& getline line > 0) {
if (match(line, "Locked +SC-QAM +Downstream +Bonded +([0-9]+) ", a)) {
FREQ[FREQ_CNT++] = a[1] / FREQ_FACTOR
}
}
close(LYNX)
} else if (freqs) {
PR("evaluating given downstream channel frequencies")
if (!set_freqs(freqs)) {
exit_("can't handle the given downstream channel frequency specification")
}
} else {
# error -> will exit
}
PRF("downstream channel frequencies now in use: ")
if (!FREQ_CNT) {
exit_("could not retrieve any")
}
PRF("[ " FREQ_CNT " ] ")
for (i = 0; i < FREQ_CNT; ++i) {
PRF(FREQ " ")
BWIDTH_OF = "na"
FREQ_SPEC_NEW = FREQ_SPEC_NEW (FREQ_SPEC_NEW ? ":" : "") FREQ
}
PR("")
}
func assemble_rrdcreate_cmd( \
CMD, i)
{
CMD = "rrdtool create " RRDBASE_FILE " -s 1" " \\\\\n"
for (i = 0; i < FREQ_CNT; ++i) {
CMD = CMD sprintf("DS:f%02d:GAUGE:120:U:U", i) " \\\\\n"
}
CMD = CMD sprintf("DS:sum:GAUGE:120:U:U") " \\\\\n"
CMD = CMD sprintf("RRA:AVERAGE:0.1:" rrdbase_rrasteps ":" rrdbase_histsize)
RRDB_CCMD_NEW = CMD
}
func assemble_rrdgraph_cmd(end, size, \
group, GROUPS, CMD, i, k)
{
# use the first version if interested in average and min too
GROUPS = "MAX:max:MIN:min:LAST:last:AVERAGE:avg"
GROUPS = "MAX:max:LAST:last"
split(GROUPS, group, ":")
CMD = "rrdtool graph " RRDGRAPH_TMP " -a PNG -l 0 -u " RRDGRAPH_Y_UPPER_LIMIT * FREQ_FACTOR " \\\\\n" \
"--title=\"Graph generated at \`date\`\"" " \\\\\n" \
"-w " rrdgraph_width " -h " rrdgraph_height " \\\\\n" \
"--end " end " --start end-" size "s" " \\\\\n"
CMD = CMD " \\\\\n"
CMD = CMD IF_BLACK_THEME " \\\\\n"
CMD = CMD IF_2ND_YAXIS " \\\\\n"
CMD = CMD " \\\\\n"
for (i = 0; i < FREQ_CNT; ++i) {
CMD = CMD sprintf("DEF:f%02d=" RRDBASE_FILE ":f%02d:AVERAGE", i, i) " \\\\\n"
}
CMD = CMD SUM_DEF " \\\\\n"
CMD = CMD " \\\\\n"
for (i = 0; i < FREQ_CNT; ++i) {
if (i < FREQ_CNT / 2) {
CMD = CMD sprintf("\"LINE:f%02d#ff0000:%d\"", i, FREQ) " \\\\\n"
} else {
CMD = CMD sprintf("\"LINE:f%02d#0000ff:%d\"", i, FREQ) " \\\\\n"
}
}
CMD = CMD " \\\\\n"
CMD = CMD "\"LINE:sum#00ff00:sum\\\l\"" " \\\\\n"
CMD = CMD " \\\\\n"
for (k = 1; group[k]; k += 2) {
for (i = 0; i < FREQ_CNT; ++i) {
CMD = CMD sprintf("\"GPRINT:f%02d:%s:%%3.0lf %%s\"", i, group[k]) " \\\\\n"
}
CMD = CMD sprintf("\"GPRINT:%s:%s:%%3.0lf %%s\"", "sum", group[k])
CMD = CMD "\" " group[k + 1] "\\\l\"" " \\\\\n"
}
return CMD
}
func assemble_rrdgraphs_cmd( \
i, a)
{
for (i = 0; i < RRDGRAPHS_CNT; ++i) {
split(RRDGRAPHS, a, "|")
if (a[1] > rrdbase_histsize) {
break
}
RRDGRAPH_CMD = assemble_rrdgraph_cmd("now", gensub(" +", "", "g", a[1]))
RRDGRAPH_FILE = RRDGRAPH_NAME "_" a[2] RRDGRAPH_EXT
PR("generating graph for: " sprintf("%4s", a[2]) " recording length, size " rrdgraph_width "x" rrdgraph_height " pixels")
}
}
func scan_down_channels( \
RRDUPD_STR, BWIDTH_SUM, a, i, k)
{
PRF(strftime("%T: "))
for (i = 0; i < FREQ_CNT; ++i) {
# tuning via mediaclient not in use ATM
# ex("timeout 10 /opt/bin/mediaclient -d /dev/dvb/adapter0/frontend0 -m DVBC -f " FREQ * FREQ_FACTOR " -M Q256 -S 6952000")
# ex(LD_PL "timeout 10 dvbtune -f " FREQ * FREQ_FACTOR " -s 6952 -qam 256", 0, "Bit error rate: ([0-9]+)$", a)
if ( dvb_device % 2 == 0 ) {
next_device = 1
} else {
next_device = 0
}
if (i == FREQ_CNT) {
nohup dvbtune -c " next_device " -f " FREQ[0] * $FREQ_FACTOR " -s 6952 -qam 256
} else {
nohup dvbtune -c " next_device " -f " FREQ[i + 1] * FREQ_FACTOR " -s 6952 -qam 256
}
#nohup cmd
# PR("\nusing [ " cmd " ]")
ex("timeout 2 dvbsnoop -adapter " dvb_device " -s bandwidth 8190 -n 100")
#
# in case of very small bandwidth values limit the measurement
# interval to something useful to keep the overall scan time sufficiently low.
#
# the arbitrarily chosen stop value of 80000 appears to be high enough to even
# handle high bandwidths with enough accuracy.
#
# PR("\nscanning for [ " FREQ " ] on device " dvb_device " with dvbsnoop ...")
if (!ex("timeout 1 dvbsnoop -adapter " dvb_device " -s bandwidth 8190 -n 80000", 0, "^## PID:.*Avrg: +([^ ]+) ", a)) {
#
# UPDATE as of 2019_08_03:
# - in the event of dvbsnoop failure: no longer exit but retry instead
# - you may want to reload your DVB drivers if these crashed
#
PR("\ndvbsnoop fails for [ " FREQ " ], retrying...")
#
# place your specific driver reload code here
#
ex("systemctl stop sundtek; sleep 5; systemctl start sundtek; sleep 20", 1)
--i; continue
# exit_("dvbsnoop fails")
}
BWIDTH_OF = int(a[1] + 0.5)
PRF(sprintf("%6s", BWIDTH_OF))
BWIDTH_SUM = 0
RRDUPD_STR = "rrdtool update " RRDBASE_FILE " N"
for (k = 0; k < FREQ_CNT; ++k) {
BWIDTH_SUM += BWIDTH_OF[k]
RRDUPD_STR = RRDUPD_STR ":" BWIDTH_OF[k] * 1000
}
#
# do not update until the initial scan completes
#
if (BWIDTH_OF[FREQ_CNT - 1] != "na") {
RRDUPD_STR = RRDUPD_STR ":" BWIDTH_SUM * 1000
ex(RRDUPD_STR, 2)
}
dvb_device = next_device
}
PR(sprintf("%7s", BWIDTH_SUM))
print BWIDTH_SUM > BWIDTH_SNAPSHOT; close(BWIDTH_SNAPSHOT)
}
func generate_rrdgraphs( \
i)
{
for (i = 0; RRDGRAPH_CMD; ++i) {
ex(RRDGRAPH_CMD)
ex("mv " RRDGRAPH_TMP " " RRDGRAPH_FILE, 2)
}
}
func watch_the_scenery( \
)
{
#
# some inital setup specific to certain devices. this may fail
# on one or another device. don't care.
#
#
# fix for:
# * Silicon Labs Si2168 card not compatible? · Issue #1 · sp4rkie/docsis-cable-load-monitor
# https://github.com/sp4rkie/docsis-cable-load-monitor/issues/1
#
ex("echo 0 | sudo tee -a /sys/module/dvb_core/parameters/dvb_powerdown_on_sleep")
#
# fix for:
# * set dtvmode
# https://www.unitymediaforum.de/viewtopic.php?p=428995#p428995
#
ex("/opt/bin/mediaclient -d /dev/dvb/adapter0/frontend0 --setdtvmode=DVBC")
dvb_device = 0
ex("nohub dvbtune -c " dvb_device " -f " FREQ[0] * FREQ_FACTOR " -s 6952 -qam 256")
while (1) {
generate_rrdgraphs()
scan_down_channels()
}
}
func usage( \
locked, line, a)
{
PR("Usage: " PRGBNAME)
while ((line = ex_line("cat " PRGNAME)) != -1) {
if (locked) {
if (match(line, "match[^,]+, \"(.+)\"[^\"]+# (.*)$", a)) {
PR(sprintf(" %-21s - %s", gensub("\\\\$", "", "g", a[1]), a[2]))
}
} else {
if (match(line, "^func process_cmdline")) ++locked
}
}
PR("\ndefaults:")
PR(" -c " sprintf("%-10d", RRDBASE_RRASTEPS) " # 10 secs")
PR(" -r " sprintf("%-10d", RRDBASE_HISTSIZE) " # 1 week" )
PR(" -w " sprintf("%-10d", RRDGRAPH_WIDTH) )
PR(" -h " sprintf("%-10d", RRDGRAPH_HEIGHT) )
}
func process_cmdline( \
renew_rrd_base, backup_time, freq_list, i, a)
{
read_cfg()
if (FREQ_SPEC_CFG) freq_list = FREQ_SPEC_CFG
if (RRA_STEPS_CFG) rrdbase_rrasteps = RRA_STEPS_CFG
if (HIST_SIZE_CFG) rrdbase_histsize = HIST_SIZE_CFG
#
# regexps for arg matching are kept somewhat sloppy to avoid clutter in usage() text output
#
for (i = 2; i < _ARGC; ++i) {
if (match(_ARGV _ARGV[i + 1], "-h$")) { # print this help and exit
usage()
exit
} else if (match(_ARGV " " _ARGV[i + 1], "-f [0-9]+
[0-9]+)*$")) { # manually specify downstream channel frequencies
freq_list = _ARGV[i + 1]
++i
} else if (match(_ARGV, "-f$")) { # scan a TC4400 for current downstream channel freqs
freq_list = "force_new_scan"
} else if (match(_ARGV " " _ARGV[i + 1], "-c [0-9]+$")) { # create a RRD base with given RRA steps (in secs)
rrdbase_rrasteps = _ARGV[i + 1]
++i
} else if (match(_ARGV " " _ARGV[i + 1], "-r [0-9]+$")) { # recording history size (in secs)
rrdbase_histsize = _ARGV[i + 1]
++i
} else if (match(_ARGV " " _ARGV[i + 1], "-w [0-9]+$")) { # width of generated graph(s)
rrdgraph_width = _ARGV[i + 1]
++i
} else if (match(_ARGV " " _ARGV[i + 1], "-h [0-9]+$")) { # height of generated graph(s)
rrdgraph_height = _ARGV[i + 1]
++i
} else if (match(_ARGV " " _ARGV[i + 1] " " _ARGV[i + 2], "-g [0-9]+ [0-9]+$")) { # generate a graph with given end and length (in secs)
rrdgraph_histend = strftime("%s") - _ARGV[i + 1]
rrdgraph_length = _ARGV[i + 2]
i += 2
} else if (match(_ARGV, "-i$")) { # ignore errors reported by dvbtune
++dvbtune_ignerrs
} else {
usage()
++ERR; exit
}
}
retrieve_down_freqs(freq_list)
if (rrdgraph_histend) {
PR("point in time where the generated graph ends: " strftime("%T", rrdgraph_histend))
PR("time span covered by the generated graph: " rrdgraph_length "s")
PR("graph is written to: " RRDGRAPH_NAME RRDGRAPH_EXT)
ex(assemble_rrdgraph_cmd(rrdgraph_histend, rrdgraph_length), 2)
ex("mv " RRDGRAPH_TMP " " RRDGRAPH_NAME RRDGRAPH_EXT)
exit
}
PR("recording RRA step size: " rrdbase_rrasteps " seconds")
PR("recording RRA history size: " \
int(rrdbase_histsize / (60 * 60 * 24)) " day(s) " \
int(rrdbase_histsize % (60 * 60 * 24) / (60 * 60 )) " hour(s) " \
int(rrdbase_histsize % (60 * 60 ) / (60 )) " minute(s) " \
int(rrdbase_histsize % (60 ) ) " second(s) " \
)
if (dvbtune_ignerrs) PR("dvbtune: " "ignore bit errors")
assemble_rrdcreate_cmd()
assemble_rrdgraphs_cmd()
if (split(FREQ_SPEC_CFG, a, ":") != split(FREQ_SPEC_NEW, a, ":")) {
if (FREQ_SPEC_CFG) PR("count of monitored channel frequencies did change")
renew_rrd_base += 2
} else if (FREQ_SPEC_CFG != FREQ_SPEC_NEW) {
if (FREQ_SPEC_CFG) PR("values of monitored channel frequencies did change")
renew_rrd_base += 1
}
if (RRA_STEPS_CFG != rrdbase_rrasteps \
|| HIST_SIZE_CFG != rrdbase_histsize) {
if (RRA_STEPS_CFG) PR("RRD base recording parameters did change")
renew_rrd_base += 2
}
if (renew_rrd_base) {
if (ex("ls " RRDBASE_FILE, 0 ,"^(" RRDBASE_FILE ")$")) {
backup_time = strftime(".%Y-%m-%d_%T")
PR("backing up old RRD data")
ex("cp -va " RRDBASE_FILE " " RRDBASE_FILE backup_time)
ex("cp -va " CONF_FILE " " CONF_FILE backup_time)
}
print FREQ_SPEC_NEW > CONF_FILE
print RRDB_CCMD_NEW > CONF_FILE
close(CONF_FILE)
if (renew_rrd_base > 1) {
PR("(re)creating the RRD base")
ex(RRDB_CCMD_NEW, 2)
}
}
watch_the_scenery()
}
BEGIN {
QUIET = 1
setup()
process_cmdline()
exit
}
END {
cleanup()
PRP("\n" PRGBNAME " " "exits with: " (ERR ? ERR : 0) " " "error(s)")
PRP("====== [" sprintf(" %5d ", PROCINFO["pid"]) "] program stop [" strftime() "] on " HOSTNAME " ======")
close(PROTOCOL)
exit ERR
}
!
[ -t 0 ] && STDIN="< /dev/null"
eval exec awk -f /usr/local/lib/l5.awklib -f $0_$$.awk $STDIN 2>&1