notes and code for my payphone project
1#!/usr/bin/perl
2
3use Asterisk::AGI;
4use Cwd "realpath";
5use File::Basename;
6use Time::HiRes;
7use strict;
8
9use constant {
10 # where we live
11 SOUND_DIR => dirname(realpath($0)),
12
13 # outbound context we'll send calls to (since our current context should
14 # only allow dialing 0 from the ATA)
15 OUTBOUND_CONTEXT => "payphone-outbound",
16
17 # timeout in seconds to wait to get a number or a coin
18 TIMEOUT => 30,
19
20 # amount required to make a call
21 CALL_CENTS => 25,
22
23 # numbers allowed to be dialed without requiring coins
24 FREE_CALLS => qr/^(1?(8[02345678]{2})[0-9]{7}|911)/,
25
26 # valid numbers which will immediately dial instead of timing out
27 VALID_NUMBER => qr/^(1?[2-8][0-9]{9}|911)/,
28};
29
30my $AGI = new Asterisk::AGI;
31
32my $last_activity = time();
33my @last_coins;
34my $inserted = 0;
35my $dialed = "";
36
37# generate dialtone while we wait for coins
38$AGI->exec("Playtones", "dial");
39my $dialtone = 1;
40
41while (1) {
42 my $dig = $AGI->wait_for_digit(1000);
43
44 if ($dig >= 1) {
45 if ($dig == ord('$')) {
46 $AGI->verbose("got coin tone at " . Time::HiRes::time, 4);
47
48 # shift out old coins
49 my @new_coins;
50 for (my $x = 0; $x <= $#last_coins; $x++) {
51 if (Time::HiRes::time - $last_coins[$x] < 0.5) {
52 push(@new_coins, $last_coins[$x]);
53 }
54 }
55 @last_coins = @new_coins;
56
57 push(@last_coins, Time::HiRes::time);
58
59 # if we've heard 3 coin tones in close proximity, always count it
60 # as 5 since we might not hear the other 2
61 if ($#last_coins == 2) {
62 $inserted += 15;
63 $AGI->verbose("got 5-cent tone, counting as 15 cents (now "
64 . $inserted . " cents)", 4);
65 # provide some audible feedback that it's ok to make a call
66 $AGI->exec("Playtones", "stutter");
67 }
68 elsif ($#last_coins > 2) {
69 $AGI->verbose("ignoring 4th or 5th 5-cent tone of 25-cents "
70 . "(still " . $inserted . " cents)", 4);
71 }
72 else {
73 $inserted += 5;
74 $AGI->verbose("got 5-cent tone (now " . $inserted . " cents)",
75 4);
76 }
77 }
78 else {
79 if ($dialtone) {
80 $AGI->exec("StopPlaytones", "dial");
81 $dialtone = 0;
82 }
83
84 $dialed .= chr($dig);
85 $AGI->verbose("dialed " . chr($dig) . " (now " . $dialed . ")", 4);
86 }
87
88 $last_activity = time();
89 }
90
91 my $elapsed = time() - $last_activity;
92
93 if ($dialed =~ FREE_CALLS) {
94 $AGI->verbose("connecting to " . $dialed . " (free call)", 1);
95 dial($dialed);
96 exit;
97 }
98 elsif ($dialed =~ VALID_NUMBER) {
99 if ($inserted < CALL_CENTS) {
100 $AGI->verbose("need " . CALL_CENTS . " cents to dial " . $dialed
101 . ", have " . $inserted, 4);
102
103 $AGI->stream_file(SOUND_DIR . "/not_deposited", "0");
104 }
105 else {
106 $AGI->verbose("connecting to " . $dialed . " (inserted "
107 . $inserted . " cents)", 1);
108 dial($dialed);
109 }
110 exit;
111 }
112 elsif ($elapsed >= TIMEOUT) {
113 $AGI->verbose("timed out waiting for coins or digits", 1);
114
115 if ($dialed ne "") {
116 $AGI->stream_file(SOUND_DIR . "/invalid_number", "0");
117 }
118
119 exit;
120 }
121}
122
123sub dial {
124 my ($number) = @_;
125
126 $AGI->set_context(OUTBOUND_CONTEXT);
127 $AGI->set_extension($dialed);
128 $AGI->exec("Goto", "1");
129}