#!/usr/bin/php -q log. Also, // the pulse count of the last received digit is provided in the log. // This dial speed tester needs no additional audio files or applications. // 2011-08-20: Added an indication for dial speed run-down/run-up. // Since the timing accuracy of the pulse logging isn't as accurate // as I would like (remember, normal Linux isn't a hard real time // OS, and DAHDI won't compile on RT kernels), I'm using a linear // regression to get a better idea of the dial speed trend. I'm // then taking the percentage delta between the LR value for the // first and last pulses; if it is greater than the threshold // (default of 9%), indicate run-down or run-up. // The 9% threshold was arrived at empirically, by observing the // behavior of electronic pulse dials, a WECO Trimline rotary dial, // and a WECO 302 dial. The 302 audibly runs down in speed. // The electronic dials and the Trimline typically show deltas in // the 2%-5% range, due to system timing inaccuracies. // You can log the statistics in CSV format if you please. // Text-to-speech is now supported. The default is not to use it; // if enabled, the default is to use eSpeak. You could use Flite // or Festival if you prefer. // The standard Asterisk sound library doesn't include the word "steady," // so I'm using tones as indicators if a TTS engine is unavailable. A // steady tone indicates steady speed, hi-lo indicates dial speed run-down, // and lo-hi indicates dial speed run-up. // Copyright (C) 2011 Russ Price // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ob_implicit_flush(false); error_reporting(0); set_time_limit(300); /** * linear regression function * @param $x array x-coords * @param $y array y-coords * @returns array() m=>slope, b=>intercept */ function linear_regression($x, $y) { // calculate number points $n = count($x); // ensure both arrays of points are the same size if ($n != count($y)) { trigger_error("linear_regression(): Number of elements in coordinate arrays do not match.", E_USER_ERROR); } // calculate sums $x_sum = array_sum($x); $y_sum = array_sum($y); $xx_sum = 0; $xy_sum = 0; for($i = 0; $i < $n; $i++) { $xy_sum+=($x[$i]*$y[$i]); $xx_sum+=($x[$i]*$x[$i]); } // calculate slope $m = (($n * $xy_sum) - ($x_sum * $y_sum)) / (($n * $xx_sum) - ($x_sum * $x_sum)); // calculate intercept $b = ($y_sum - ($m * $x_sum)) / $n; // return result return array("m"=>$m, "b"=>$b); } function execute_agi($command) { fputs( STDOUT, $command . "\n" ); fflush( STDOUT ); $resp = fgets( STDIN, 4096 ); if(preg_match("/^([0-9]{1,3}) (.*)/", $resp, $matches)) { if (preg_match('/result=([-0-9a-zA-Z]*)(.*)/', $matches[2], $match)) { $arr['code'] = $matches[1]; $arr['result'] = $match[1]; if (isset($match[3]) && $match[3]) $arr['data'] = $match[3]; return $arr; } else { $arr['code'] = $matches[1]; $arr['result'] = 0; return $arr; } } else { $arr['code'] = -1; $arr['result'] = -1; return $arr; } } function stream($file) { execute_agi("STREAM FILE \"$file\" \"\""); } function saynumber($number) { execute_agi("SAY NUMBER \"$number\" \"\""); } function saydigits($digits) { execute_agi("SAY DIGITS \"$digits\" \"\""); } function calcspeed($chan) { global $threshold, $log_stats, $logfile, $tts, $tts_engine; $fpPulse = fopen("/proc/dahdi_dial/$chan", "r"); if(!$fpPulse) { execute_agi("EXEC Wait 1"); stream("error-number"); saynumber("1"); return; } $i = 0; // Grab pulse count. If it is less than 10, don't bother. $in = fgets($fpPulse); list($pcount) = sscanf($in, "Last pulse count: %d"); if($pcount != 10) { execute_agi("EXEC Wait 1"); stream("bad"); stream("digit"); saydigits($pcount); fclose($fpPulse); return; } else { for($i = 0, $mcnt = 0, $bcnt = 0; $i < 20; $i++) { $in = fgets($fpPulse); if($in == "") { break; } list($state, $sec, $usec) = sscanf($in, "zt_hooksig %d: %d:%d"); if($state) { $make[$mcnt++] = $sec + ($usec * .000001); } else { $break[$bcnt++] = $sec + ($usec * .000001); } } } fclose($fpPulse); if($i < 20) { execute_agi("EXEC Wait 1"); stream("bad"); stream("count"); return; } else { for($i = 0; $i < 10; $i++) { // need an array of x for regression $x[$i] = $i; $breaklen[$i] = $make[$i] - $break[$i]; // Fudge the last "make" length by using the mean // of the nine real makes. if($i < 9) { $makelen[$i] = $break[$i + 1] - $make[$i]; } else { $makelen[$i] = array_sum($makelen) / count($makelen); } $cycle[$i] = $makelen[$i] + $breaklen[$i]; } $break_length = array_sum($breaklen); $make_length = array_sum($makelen); $cycle_length = $break_length + $make_length; $break_percent = 100 * ($break_length / $cycle_length); $make_percent = 100 * ($make_length / $cycle_length); $break_percent = round($break_percent); $make_percent = round($make_percent); $speed = 10 / $cycle_length; $strspeed = sprintf("%.1f", $speed); $LR = linear_regression($x, $cycle); // let's take the regression values for the first and last // pulses, and get their percentage delta $begin = $LR['b']; // x=0 $end = $LR['m']*9+$LR['b']; // x=9 $lrdiff = $end - $begin; $pctdelta = $lrdiff / $begin * 100; if($log_stats) { $fp = fopen($logfile, "a"); fprintf($fp, "Channel,%s\n", $chan); fprintf($fp, "Speed,$strspeed\n", $strspeed); fprintf($fp, "m,%.6f\n", $LR['m']); fprintf($fp, "b,%.6f\n", $LR['b']); fprintf($fp, "pctdelta,%.2f\n", $pctdelta); fprintf($fp, "x,break,make,breaklen,makelen,cycle,LR\n"); for($i = 0; $i < 10; $i++) { fprintf($fp, "%d,%.6f,%.6f,%.6f,%.6f,%.6f,%.6f\n", $i, $break[$i], $make[$i], $breaklen[$i], $makelen[$i], $cycle[$i], $LR['m'] * $i + $LR['b']); } fclose($fp); } execute_agi("EXEC Wait 1"); if($tts) { if($pctdelta > $threshold) { $trend = "falling"; } else if ($pctdelta < -$threshold) { $trend = "rising"; } else { $trend = "steady"; } // Commas don't work well in AGI arguments. $msg = sprintf("Speed: %s. %d percent break; %d percent make. Speed %s.", $strspeed, $break_percent, $make_percent, $trend); execute_agi("EXEC $tts_engine \"$msg\""); } else { stream("speed"); $sp = explode('.', $strspeed); saynumber($sp[0]); stream("wx/point"); saydigits($sp[1]); execute_agi("EXEC Wait 1"); stream("period"); saynumber($break_percent); stream("wx/percent"); saynumber($make_percent); stream("wx/percent"); execute_agi("EXEC Wait 1"); if($pctdelta > $threshold) { // indicate run-down execute_agi("EXEC Wait 1"); execute_agi("EXEC PlayTones 620"); execute_agi("EXEC Wait 0.5"); execute_agi("EXEC PlayTones 350"); execute_agi("EXEC Wait 0.5"); execute_agi("EXEC StopPlayTones"); } else if($pctdelta < -$threshold) { // indicate run-up execute_agi("EXEC Wait 1"); execute_agi("EXEC PlayTones 350"); execute_agi("EXEC Wait 0.5"); execute_agi("EXEC PlayTones 620"); execute_agi("EXEC Wait 0.5"); execute_agi("EXEC StopPlayTones"); } else { // indicate steady execute_agi("EXEC Wait 1"); execute_agi("EXEC PlayTones 480"); execute_agi("EXEC Wait 1"); execute_agi("EXEC StopPlayTones"); } } execute_agi("EXEC Wait 1"); } } $agivars = array(); while (!feof(STDIN)) { $agivar = trim(fgets(STDIN)); if ($agivar === '') { break; } $agivar = explode(':', $agivar); $agivars[$agivar[0]] = trim($agivar[1]); } extract($agivars); if(!preg_match("/(\w+)\/(\d+)/", $agi_channel, $chaninfo)) { execute_agi("EXEC Wait 1"); stream("bad"); stream("channel"); exit; } if($chaninfo[1] != "DAHDI") { execute_agi("EXEC Wait 1"); stream("bad"); stream("channel"); exit; } execute_agi("EXEC PlayTones 600*120"); execute_agi("WAIT FOR DIGIT 30000"); execute_agi("EXEC StopPlayTones"); calcspeed($chaninfo[2]); ?>