2016/02/05

Asterisk : The Cookbook 食譜 035 - a2billing

Q035: 如果我想在 Asterisk 之上再加裝 GUI 套件,請問有沒有什麼好建議?

比較流行的可能是 FreePBX, 另外我正在研讀的是 a2billing, 很多成功的案例通常都是在講 FreePBX, 其他像 AsteriskNOW, 及 PBX in a Flash 等專案也都是基於 FreePBX, 我們當然不會花太多心力在它們上面,主要原因有二,一個最主要原因是這些 GUI 套件都不怎麼提底層的 Asterisk 本身,第二個原因是要找這些 GUI 專案文件很容易,反而專門討論 Asterisk 的中文文件很少。

當然並不是說這些 GUI 專案不好,正因為有這些 GUI 套件才讓 Asterisk 成功起來。只是我想從基礎讀起而已。反過來說,對於一個想快速建立網路電話的人來說,相當建議您直接採用像 AsteriskNOW 這種使用 FreePBX 界面的專案套件來用。

這邊建議一個專案叫 a2billing, 它不止是 Asterisk 的 GUI 界面,而且提供完整的 VoIP 網路電話營運商解決方案。雖然如此,你也千萬別以為下載安裝了 a2billing 之後就可以安心當個網路電話運營商。

首先你可能要安裝一些套件,例如: perl-DBD-Pg subversion libapache2-mod-php5 php5 php5-common php5-cli php5-mysql mysql-server apache2 php5-gd openssh-server  php5-mcrypt python-setuptools python-mysqldb,總之,GUI 一般就是像這樣,透過 apache2 + php5 + Mysql
  1. 取得套件
    1. mkdir /usr/local/src/a2billing
    2. git clone https://github.com/Star2Billing/a2billing.git /usr/local/src/a2billing/
    3. cd /usr/local/src/a2billing
  2. 複製設定檔: cp a2billing.conf /etc/
  3. 更新相依套件
    1. curl -sS https://getcomposer.org/installer | php
    2. mv composer.phar /usr/local/bin/composer
    3. composer update && composer install
  4. 設定資料庫
    1. mysql -u root -p < DataBase/mysql-5.x/a2billing-createdb-user.sql
      這個步驟會產生底下資料庫資訊
      database name: mya2billing
      database user: a2billinguser
      user password: a2billing
    2. cd DataBase/mysql-5.x/ ; ./install-db.sh
    3. 驗證 
      1. mysql -u root -p mya2billing
        這一步會要求你輸入 MySQL 的 root 密碼,請自行根據設定登入
      2. mysql> show tables;
        這一步可以看到相當多表格,以我的來看是 97 個
      3. mysql> exit
    4. 修改 /etc/a2billing.conf,修改 [database] 節即可,如下
          [database]
          hostname = localhost
          port = 3306
          user = a2billinguser
          password = a2billing
          dbname = mya2billing
          dbtype = mysql
      
    5. 新增設定檔:
          touch /etc/asterisk/additional_a2billing_iax.conf
          touch /etc/asterisk/additional_a2billing_sip.conf
          echo \#include additional_a2billing_sip.conf >> /etc/asterisk/sip.conf
          echo \#include additional_a2billing_iax.conf >> /etc/asterisk/iax.conf
      
    6. 安裝音頻檔
      1. cd /usr/local/src/a2billing/addons/sounds
      2. ./install_a2b_sounds.sh
    7. 安裝 agi-bin
      1. cd /usr/local/src/a2billing
      2. cp -a AGI/*.php common/lib /var/lib/asterisk/agi-bin/
    8. 安裝網頁界面:
      1. 將必要的檔案複製/或移動到現有的 apache DocumentRoot 下
        1. mkdir /var/www/html/a2billing
        2. mv admin  agent  common  customer vendor  webservice /var/www/html/a2billing/
    9. 新增撥號計劃 /etc/asterisk/extensions.conf
          [a2billing]
          include => a2billing_callingcard
          include => a2billing_monitoring
          include => a2billing_voucher
          [a2billing_callingcard]
          ; CallingCard application
          exten => _X.,1,NoOp(A2Billing Start)
          exten => _X.,n,DeadAgi(a2billing.php|1)
          exten => _X.,n,Hangup
          [a2billing_voucher]
          exten => _X.,1,Answer(1)
          exten => _X.,n,DeadAgi(a2billing.php|1|voucher)
          ;exten => _X.,n,AGI(a2billing.php|1|voucher44) ; will add 44 in front of the callerID for the CID authentication
          exten => _X.,n,Hangup
          [a2billing_did]
          exten => _X.,1,DeadAgi(a2billing.php|1|did)
          exten => _X.,2,Hangup
    10. 增加 crontab, 請自行編譯,例如透過 crontab -e
       0 6 * * * php /usr/local/src/a2billing/Cronjobs/currencies_update_yahoo.php
      0 6 1 * * php /usr/local/src/a2billing/Cronjobs/a2billing_subscription_fee.php
      0 * * * * php /usr/local/src/a2billing/Cronjobs/a2billing_notify_account.php
      0 2 * * * php /usr/local/src/a2billing/Cronjobs/a2billing_bill_diduse.php
      0 12 * * * php /usr/local/src/a2billing/Cronjobs/a2billing_batch_process.php
      0 6 * * * php /usr/local/src/a2billing/Cronjobs/a2billing_batch_billing.php
      */5 * * * * php /usr/local/src/a2billing/Cronjobs/a2billing_batch_autodialer.php
      0 * * * * php /usr/local/src/a2billing/Cronjobs/a2billing_alarm.php
    11. 手動增加 log files:
      mkdir /var/log/a2billing; cd /var/log/a2billing
      touch cront_a2b_alarm.log
      touch cront_a2b_autorefill.log
      touch cront_a2b_batch_process.log
      touch cront_a2b_archive_data.log
      touch cront_a2b_bill_diduse.log
      touch cront_a2b_subscription_fee.log
      touch cront_a2b_currency_update.log
      touch cront_a2b_invoice.log
      touch cront_a2b_check_account.log
      touch a2billing_paypal.log
      touch a2billing_epayment.log
      touch a2billing_api_ecommerce_request.log
      touch a2billing_api_callback_request.log
      touch a2billing_api_card.log
      touch a2billing_agi.log
      cd /usr/local/src/a2billing
    12. 修改權限
      1. chown -R asterisk.asterisk /etc/asterisk /var/{log,run,lib,spool}/asterisk /var/log/a2billing /usr/local/src/a2billing /var/www/html/a2billing
      2. find /etc/asterisk /var/lib/asterisk /usr/local/src/a2billing -type d -exec chmod 777 {} \;
      3. 如果您發現網頁看不到或是其它怪怪的時候,很可能就是權限沒設定好,包括這邊寫的可能有遺漏, apache2 也要檢查
    13. 安裝 daemon
      1. cd /usr/local/src/a2billing/CallBack/callback-daemon-py/
      2. easy install sqlalchemy
      3. cp -a callback_daemon/a2b-callback-daemon.debian /etc/init.d/a2b-callback-daemon
      4. chmod a+x /etc/init.d/a2b-callback-daemon
      5. 如果要開機自動執行,請執行:
        update-rc.d a2b-callback-daemon  defaults
      6. tar zxf dist/callback_daemon-1.0.prod-r1528.tar.gz -C /tmp
      7. cd /tmp/callback_daemon-1.0.prod-r1528/
      8. touch callback_daemon/__init__.py
      9. python setup.py build &&python setup.py bdist_egg && python setup.py install --prefix=/usr
      10. easy_install dist/callback_daemon-1.0.prod_r1528-py2.7.egg
    14. 重新啟動系統
      1. 其實可以透過 service 重啟即可,但是因為我在寫這道食譜時亂試一通,系統後來怪怪的,重新啟動後又一切正常
      2. 需要重新啟動的或許是底下幾個
        1. mysql
        2. apache2
        3. asterisk
        4. a2b-callback-daemon
    15. 測試
      用瀏覽器瀏覽 http://localhost/a2billing/admin/

2016/02/04

Asterisk : The Cookbook 食譜 034 - 輪用辦公桌

Q034: 我想提供輪用辦公桌(Hot-Desking)使用電話系統,請問該怎麼設計?

說真的,以目前的食譜要來設計這樣的需求是足夠了,先來看看流程圖



簡單講就是,判斷是不是有人已經用同一支電話登入了,如果有,就將它登出,然後再用新的登入,否則直接登入。

底下直接寫範例(我也還在研讀當中):

[HotDesking]
  exten => 7000,1,Verbose(2,Attempting logoff from device ${CHANNEL(peername)})
    same => n,Set(PeerName=${CHANNEL(peername)})
    same => n,Set(CurrentExtension=${DB(HotDesk/${PeerName})})
    same => n,GoSubIf($[${EXISTS(${CurrentExtension})}]?  subDeviceLogoff,1(${PeerName},${CurrentExtension}):loggedoff)
    same => n,GotoIf($[${GOSUB_RETVAL} = 0]?loggedoff)
    same => n,Playback(an-error-has-occurred)
    same => n,Hangup()
    same => n(loggedoff),Playback(silence/1&agent-loggedoff)
    same => n,Hangup()
上面這段設定分機號碼 7000 用來登出裝置,

  1. 裝置以 ${PeerName} = ${CHANNEL(peername)} 表示,
  2. 分機號碼以 ${DB(HotDesk/${PeerName})} 表示

底下 71XX 則表示先登出現有的分機號碼再登入

  exten => _71XX,1,Verbose(2,Attempting to login device ${CHANNEL(peername)} to extension ${EXTEN:1})
    same => n,Set(NewPeerName=${CHANNEL(peername)})
    same => n,Set(NewExtension=${EXTEN:1})

    same => n,Set(ExistingExtension=${DB(HotDesk/${NewPeerName})})
    same => n,GotoIf($[${EXISTS(${ExistingExtension})}]?get_existing_device)

    same => n(check_device),NoOp()
    same => n,Set(ExistingDevice=${DB(HotDesk/${NewExtension})})
    same => n,GotoIf($[${EXISTS(${ExistingDevice})}]?get_existing_extension)
    same => n,NoOp(Nothing to logout)
    same => n,Goto(login)

    same => n(get_existing_device),NoOp()
    same => n,Set(ExistingDevice=${DB(HotDesk/${ExistingExtension})})
    same => n,GotoIf($[${ISNULL(${ExistingDevice})}]?login)
    same => n,GoSub(subDeviceLogoff,1(${ExistingDevice},${ExistingExtension}))
    same => n,GotoIf($[${GOSUB_RETVAL} = 0]?check_device)
    same => n,Playback(silence/1&an-error-has-occurred)
    same => n,Hangup()

    same => n(get_existing_extension),NoOp()
    same => n,Set(ExistingExtension=${DB(HotDesk/${ExistingDevice})})
    same => n,GoSubIf($[${EXISTS(${ExistingExtension})}]? subDeviceLogoff,1(${ExistingDevice},${ExistingExtension}):remove_device)
    same => n,GotoIf($[${GOSUB_RETVAL} = 0]?loggedoff)
    same => n,Playback(silence/1&an-error-has-occurred)
    same => n,Hangup()

    same => n(remove_device),NoOp()
    same => n,Set(Result=${DB_DELETE(HotDesk/${ExistingDevice})})
    same => n,Goto(loggedoff)

    same => n(loggedoff),Verbose(2,Existing device and extensions have been logged off prior to login)

    same => n(login),Verbose(2,Now logging in extension ${NewExtension} to device ${NewPeerName})
    same => n,GoSub(subDeviceLogin,1(${NewPeerName},${NewExtension}))
    same => n,GotoIf($[${GOSUB_RETVAL} = 0]?login_ok)
    same => n,Playback(silence/1&an-error-has-occurred)
    same => n,Hangup()

    same => n(login_ok),Playback(silence/1&agent-loginok)
    same => n,Hangup()

  exten => subDeviceLogoff,1,NoOp()
    same => n,Set(LOCAL(PeerName)=${ARG1})
    same => n,Set(LOCAL(Extension)=${ARG2})
    same => n,ExecIf($[${ISNULL(${LOCAL(PeerName)})} | ${ISNULL(${LOCAL(Extension)})}]?Return(-1))
    same => n,Set(PeerNameResult=${DB_DELETE(HotDesk/${LOCAL(PeerName)})})
    same => n,Set(ExtensionResult=${DB_DELETE(HotDesk/${LOCAL(Extension)})})
    same => n,Return(0)

  exten => subDeviceLogin,1,NoOp()
    same => n,Set(LOCAL(PeerName)=${ARG1})
    same => n,Set(LOCAL(Extension)=${ARG2})
    same => n,ExecIf($[${ISNULL(${LOCAL(PeerName)})} | ${ISNULL(${LOCAL(Extension)})}]?Return(-1))
    same => n,Set(DB(HotDesk/${LOCAL(PeerName)})=${LOCAL(Extension)})
    same => n,Set(DB(HotDesk/${LOCAL(Extension)})=${LOCAL(PeerName)})
    same => n,Set(ReturnResult=${IF($[${DB_EXISTS(HotDesk/${LOCAL(PeerName)})} & ${DB_EXISTS(HotDesk/${LOCAL(Extension)})}]?0:-1)})
    same => n,Return(${ReturnResult})

Asterisk : The Cookbook 食譜 033 - 資料庫 AstDB

Q033: 靜態設定看得差不多了,如果想做一些動態設定,是否有像 File, Database 這類的儲存空間?

是的,Asterisk 有支援各種模組,裡面可以加入像 MySQL, Postgre SQL 這類資料庫,事實上它也有內建的資料庫,Asterisk 比較接近 Sqlite,底下用實例來說明:

exten => 678,1,NoOp()
  same => n,Set(COUNT=${DB(test/count)})
  same => n,GotoIf($[${ISNULL(${COUNT})}]?:continue)
  same => n,Set(DB(test/count)=1)
  same => n,Goto(1)
  same => n(continue),NoOp()
  same => n,Playback(silence/1)
  same => n,SayNumber(${COUNT})
  same => n,Set(COUNT=$[${COUNT} + 1])
  same => n,Set(DB(test/count)=${COUNT})


  1. 讀資料 DB(FAMILY/KEY) 如上例的 ${DB(test/count)}
  2. 設定 Set(DB(FAMILY/KEY)=VALUE), 如上例的 Set(DB(test/count)=${COUNT})
  3. 刪資料:
    1. DB_DELETE(FAMILY/KEY)
    2. DBdeltree(FAMILY)
  4. 檢查資料是否存在 DB_EXIST(FAMILY/KEY),如果要順便讀取其值的話,可以在呼叫後由 DB_RESULT 變數取得
  5. 傳回所有的 KEYs DB_KEYS(PREFIX)
所謂的 FAMILY 可以想成『資料庫』、『檔案』或是「群組」

Asterisk : The Cookbook 食譜 032 - 在地通道

Q032: 我們知道可以同時撥接多個通道,可是如果想要多個通道是輪流撥接時該怎麼辦呢?

雖然同時撥接很有用,有時我們更需要輪流撥接接通其中一個通道就夠了,以免同時吵到多個好友。這個需求或許可以用傳回值來判斷處理,但是 Asterisk 提供一個更簡單的寫法,也相當有用。一樣用實例來說明:

首先我們定義一個 Context [TimeDelay] 如下,共定義三個在地通道 channel_1, channel_2, channel_3,三個在地通道等待時間是不同的,後面的也都加上了 Wait():

[TimeDelay]
  exten => channel_1,1,Verbose(2,Dialing the first channel)
    same => n,Dial(SIP/0000FFFF0001,20)
    same => n,Hangup()
  exten => channel_2,1,Verbose(2,Dialing the second channel with a delay)
    same => n,Wait(10)
    same => n,Dial(DAHDI/g0/14165551212)
  exten => channel_3,1,Verbose(2,Dialing the third channel with a delay)
    same => n,Wait(15)
    same => n,Dial(SIP/MyITSP/12565551212,15)
    same => n,Hangup()

在引用時如下,利用 Dial() 同時撥接 channel_1, channel_2, channel_3:

[LocalSets]
  exten => 107,1,Verbose(2,Dialing multiple locations with time delay)
    same => n,Dial(Local/channel_1@TimeDelay&Local/channel_2@TimeDelay
&Local/channel_3@TimeDelay,40)
    same => n,Hangup()

因為三個在地通道的逾時時間不同,後面的通道也加了 Wait(), 因此這三個的開始時間與結束時間並不相同。請自行用下圖來閱讀理解。


或是用下表來配合上面的設定來閱讀


Asterisk : The Cookbook 食譜 031 - 巨集 Macros + GoSub

Q031: 如果我們公司有數百個帳號要設定,某些條件要採用相同的設定,有沒有不要單純採用複製貼上而是比較快速的方法撰寫?

是的,就像程式語言中的自訂函數,或是試算表中的自訂巨集,Asterisk 也可以自訂巨集及函數。

[巨集]

我也不往複雜的說,簡單說 Macro 非常像『簡化版的 Context』,至於 Context 可以回頭找找 Q025。一樣,我們從實例看起:

[macro-voicemail]
  exten => s,1,NoOp()
    same => n,Dial(${ARG1},10)
    same => n,GotoIf($["${DIALSTATUS}" = "BUSY"]?busy:unavail)
    same => n(unavail),VoiceMail(${MACRO_EXTEN}@default,u)
    same => n,Hangup()
    same => n(busy),VoiceMail(${MACRO_EXTEN}@default,b)
    same => n,Hangup()

  1. 名稱一定要以 macro- 開頭
  2. 因為定義巨集時並不知道分機號碼,這邊採用 s 來代表呼叫者的分機號碼
  3. 底下介紹四個巨集內可以使用的變數:
    ${MACRO_CONTEXT}   呼叫者的 Context
    ${MACRO_EXTEN} 呼叫者的分機號
    ${MACRO_PRIORITY} 呼叫者所在的權值
    ${ARGn} 傳入的第 n 個參數,從 1 開始,例如 ${ARG1}, ${ARG2}
至於引用時很簡單:
exten => 101,1,Macro(voicemail,${JOHN})
exten => 102,1,Macro(voicemail,${JANE})
exten => 103,1,Macro(voicemail,${JACK})

比較一下巨集命名與呼叫時的引用名稱,參數,變數。
在巨集呼叫中的 ARGn, MACRO_EXTEN 會被取代成
  101, JOHN
  102, JANE
  103, JACK
請仔細閱讀這幾個呼叫範例就會明白巨集呼叫時參數用法,以及上面四個變數的用法

上面範例還有一個變形,可以比較用法的差異:
[macro-voicemail]
  exten => s,1,NoOp()
    same => n,Dial(${ARG1},20)
    same => n,Goto(s-${DIALSTATUS},1)
  exten => s-NOANSWER,1,VoiceMail(${MACRO_EXTEN}@default,u)
    same => n,Goto(incoming,s,1)
  exten => s-BUSY,1,VoiceMail(${MACRO_EXTEN}@default,b)
    same => n,Goto(incoming,s,1)
  exten => _s-.,1,NoOp()
    same => n,Goto(s-NOANSWER,1)
上例 Goto(s-XXX) 這樣的語法相當容易理解,把它想成字串就好,這樣的寫法應該更容易閱讀(如果沒有就用前一種寫法嘛)。Dial() 的傳回狀態有幾種:
ANSWER, BUSY, NOANSWER, CANCEL, CONGESTION, CHANUNAVAIL, DONTCALL, TORTURE, INVALIDARGS

如果仔細研讀上面的範例,其實可以修改成如下,讀不懂也沒關係喔,戲法人人會變,只要能正確作動,其實沒有什麼美醜好壞之別:
[macro-voicemail]
  exten => s,1,NoOp()
    same => n,Dial(${ARG1},20)
    same => n,VoiceMail(${MACRO_EXTEN}@default,${IF($[${DIALSTATUS}
= BUSY]?b:u)})

這邊沒有說到的內容是,像 Dial() 本身可以相當複雜,也可以引用巨集,有機會再另外說明 Dial() 時再說(應該沒體力寫了)

[函數]

函數看起來很像巨集,一樣直接看範例,就用上面最後一個簡化版本:

[subVoicemail]
  exten => start,1,NoOp()
    same => n,Dial(${ARG1},20)
    same => n,VoiceMail(${ARG2}@default,${IF($[${DIALSTATUS}
= BUSY]?b:u)})

有兩點不一樣:
  1. [macro-voicemail] 變成 [subVoicemail]
    GoSub() 的自訂函數其實沒有像巨集那樣的用法,比較像 Context,後面呼叫會看到,這邊的寫法是讓我們自己濾
  2. 分機號碼由 s 變成 start
  3. 巨集變數都不能用,只能取得參數 ${ARGn} 而已
  4. 不像巨集會自動返回呼叫者,GoSub() 必須自行指定是否返回,否則執行完函數就結束了
呼叫範例:
exten => 101,1,GoSub(subVoicemail,start,1(${JOHN},${EXTEN}))
exten => 102,1,GoSub(subVoicemail,start,1(${JANE},${EXTEN}))
exten => 103,1,GoSub(subVoicemail,start,1(${JACK},${EXTEN}))

為了說明返回,我們以底下的例子來說明:
[subDialer]
  exten => start,1,NoOp()
    same => n,Dial(${ARG1},${ARG2})
    same => n,Return()
[subVoicemail]
  exten => start,1,NoOp()
    same => n,VoiceMail(${ARG1}@${ARG2},${ARG3})
    same => n,Hangup()
呼叫時如下:
exten => 101,1,NoOp()
  same => n,GoSub(subDialer,start,1(${JOHN},30))
  same => n,GoSub(subVoicemail,start,1(${EXTEN},default,u))
如果自訂函數都沒有寫最後一行的 Return(), 那麼呼叫範例並不會執行最後一行的 GoSub(subVoicemail.....)
不過上例並沒有很好的把 Dial() 狀態傳回,底下是修改版本,而且 [subVoicemail] 以這個例子也不必 Return():

[subDialer]
  exten => start,1,NoOp()
    same => n,Dial(${ARG1},${ARG2})
    same => n,Return(${DIALSTATUS})
[subVoicemail]
  exten => start,1,NoOp()
    same => n,VoiceMail(${ARG1}@${ARG2},${ARG3})
    same => n,Hangup()
呼叫例使用 ${GOSUB_RETVAL} 來取得傳回值:

exten => 101,1,NoOp()
  same => n,GoSub(subDialer,start,1(${JOHN},30))
  same => n,Set(VoicemailMessage=${IF($[${GOSUB_RETVAL} = BUSY]?b:u)})
  same => n,GoSub(subVoicemail,start,1(${EXTEN},default,${VoicemailMessage}))

Asterisk : The Cookbook 食譜 030 - 表示法 Expressions

Q030: 在撥號計劃設定檔中有看到類似四則運算,可以說明是怎麼用的嗎?

前面幾則對撥號計劃的設定其實已經說的很多了,基本的使用照理也沒什麼大問題才對。但是要設計自動話務員這類複雜的使用環境的話還有一個課題要說,就是所謂的『表示法』,也就是類似程式語言中的各項運算。

在 Asterisk 的運算中,邏輯運算跟 C 語言一樣,以 0 為假,以非 0 為真,如果有疑問的話,可以隨便找一本程式語言的書來翻翻看。

還記得前面曾經提到一個內建變數,指向現有的分機號碼的 ${EXTEN} 嗎?還記得曾經提過可以用 Set() 設定變數嗎?是的,變數是可以運算的。為了簡少本文長度,直接以實例說明:

  same => n,Set(COUNT=3)
  same => n,Set(NEWCOUNT=$[${COUNT} + 1])
  same => n,SayNumber(${NEWCOUNT})

上面應該有看到變數運算要擺在 $[] 裡面, 而且仍然不變採用 ${} 來取得變數。同樣的,上面也看到了加法,底下就來說說除了加法以外的其它運算子,有些我就懶得打字了。

邏輯判斷  |, &, =, >, <, >=, <=, !=
  有發現嗎?等於的判斷只有一個等號
四則運算  +, -, *, /, %
  最後面 % 是『餘數』的意思
正規表示法 :, =~
  因為第一個是冒號 ':' 特別提出來,因為要學正規表示法篇幅會變很大,雖然它很強大,但是除非相當進階才會需要,這邊就略過。

先總結一下,上面的運算子在其他語言中也都很常見,只是感覺混合了 C, Shell script, 所以差異之處自己要記住。Asterisk 早期版本要求運算子前後一定要有空白,而且通常寫程式時也都會要求我們把式子寫的『明確』一點,請看下例:

  劣: same => n,Set(TEST=$[${COUNT}+1])
  優: same => n,Set(TEST=$[ ${COUNT} + 1 ])

上面 Set() 是 Application, 而 ${} 則是變數引用, $[] 是變數運算,其中大中小括號的配對請自己小心閱讀,不要有了左邊漏了右邊。

接下來,在變數引用裡面可以使用函數運算,底下直接用例子說明:
  same => n,SayNumber(${LEN(${TEST})})
上面的 LEN() 會計算後面參數字串 ${TEST}的長度

接下來介紹一個特別的函數,TIMEOUT(), 它會傳回一個變數,沒錯,直接看用法:

  same => n, Set(TIMEOUT(digit)=30)

因為 TIMEOUT() 傳回變數,所以它本身不必用 ${} 括住,其中 digit 是 TIMEOUT() 可以用的三個類型之一。

條件判斷也是透過函數來表達,底下也是以實例來說明:

GotoIf(expression?destination1:destination2)
=================================

exten => 345,1,NoOp()    ; 通常用複雜運算用 NoOp() 開頭
  same => n,Set(TEST=1)
  same => n,GotoIf($[${TEST} = 1]?weasels:iguanas)
  same => n(weasels),Playback(weasels-eaten-phonesys)
  same => n,Hangup()
  same => n(iguanas),Playback(office-iguanas)
  same => n,Hangup()

這邊先補充說明,在講權值時引入 same => n 這樣的表示法時沒說到的部份就是,因為 n 只代表繼續上一個權值,當我們需要參考到某個權值時,可以在 n 後面用 (LABEL) 的方式給予標記,就像上面例子那樣使用,就不多說了。比較特別的是下面的用法也是允許的,就是判斷成立反而不跳走而是繼續下一個,同理也可以像第二條:
same => n,GotoIf($[${TEST} = 1]?:iguanas)
same => n,GotoIf($[${TEST} != 1]?iguanas)

上面範例還有一種寫法更易閱讀,跳到分機號碼去了:
exten => 345,1,NoOp()
  same => n,Answer()
  same => n,Set(TEST=1)
  same => n,GotoIf($[${TEST} = 1]?weasels,1:iguanas,1)
exten => weasels,1,
  same => n,Playback(weasels-eaten-phonesys)
  same => n,Hangup()
exten => iguanas,1,NoOp()
  same => n,Playback(office-iguanas)
  same => n,Hangup()

講到這邊篇幅已經很長了,但是仍然不得不提醒一件事,去翻一下 Shell Script, 就可以發現字串最好用 "" 括住,例如:

劣: same => n,GotoIf($[${TEST} = invalid]?err_handle)
    當 ${TEST} 未設定或是空字串時就會出錯。
優: same => n,GotoIf($[ "${TEST}" = "invalid"]?err_handle)

同樣道理:

劣: same => n,GotoIf($[${TEST} < 1]?err_handle)
  當 ${TEST} 未設定或是其實它是空字中時就會出錯
優: same => n,GotoIf($[ 0${TEST} < 1]?err_handle)

一個良好的 Asterisk 服務,應該能夠根據不同日期時間進行不同的服務,因此底下再特別提出 GotoIfTime() 函數。

GotoIfTime(times,days_of_week,days_of_month,months?label)
實例: same => n,GotoIfTime(09:00-17:59,mon-fri,*,*?open,s,1)
==============================================
times 可能的實例:
  "5:00 PM", "09:00-17:00", "18:00-0900"
  最後的實例會相當於下班時間,系統會自行處理跨日
days_of_week 可能的實例:
  "mon", "tue", "wed", "the", "fri", "sat", "sun", "sun-mon", "sat&sun"
  所以像週一三五的話是 "mon&wed&fri"
days_of_mon 可能的實例:
  "1",...,"31",  "7-12", "5&15&25"
months 可能的實例:
  "jan", ..., "dec", "jan-apr&jun&oct-dec"
以上,如果要『全部』的話就用星號 "*"

函數就暫時說這麼多,詳細可以在 asterisk -rc 的 CLI 環境中以命令 'core show functions' 取得清單


2016/02/03

Asterisk : The Cookbook 食譜 029 - 重要設定檔

Q029: 如何客製化自己的 Asterisk 呢?

我把設定檔擺在這邊,是因為相信前面幾篇應該足夠大家使用 Asterisk 來撥號了。這篇主要說明三個重要的設定檔(設定檔都在 /etc/asterisk/ 中)。
為了不占太多篇幅,請自行參考系統中的檔案。這邊提醒一個最重要的注意事項,檔案的節後面若有 (!) 的話,表示這個只是草稿,如果你要讓它生效,請自行將它去掉。

==== asterisk.conf ====

asterisk.conf 就把它想成「系統設定」即可,在執行 Asterisk 時也可以指定它的路徑,沒指定時就是從預設路徑讀(/etc/asterisk/asterisk.conf):

/usr/sbin/asterisk -C /custom/path/to/asterisk.conf

這個檔有幾個重要的節:[directories], [options], [files], [compat]

[directories]

astetcdir 設定檔
astmoddir 模組檔
astvarlibdir 各種狀態資訊存放路徑
astdbdir Asterisk 內部資料庫存放路徑
astdatadir 資源檔存放路徑,例如各種錄音檔
astagidir 存放 agi-bin 的執行檔
astspooldir 存放各種對外緩衝資料,例如 voicemail, call recording....
astrundir 存放 linux socket, process ID
astlogdir 存放 log 檔

[options]

這邊的設定可以參考 man asterisk
比較需要知道的就是 runuser/rungroup 為執行時的帳號,通常就是 asterisk

[files]

用來設定 control socket 的控制權,指的是執行在背景,前景用 asterisk -r 的時候用:
astctlpermissions 0660
astctlowner root
astctlgroup apache
astctl asterisk.ctl

這邊都用預設即可,也就是全部都可以不寫出來

[compat]
這是為了向先前的版本相容時用的,一般都設定 1.6, 所以照預設值寫就好。

pbx_realtime=1.6
res_agi=1.6
app_set=1.6

==== modules.conf ====

假如只設定 autoload=yes 這一行的話,那就會預載所有 asterisk.conf 中指明路徑的所有模組。
所以如果你想控制載入的模組,可以用 autoload=yes, 然後把要放的模組放在指定的路徑即可。

如果要明確指定哪些要載入哪些不要載入,那麼就用 :
  • preload 指明一開始就要載入的模組,此設定跟 autoload 無關
  • load 指明要載入的模組,受 autoload 影響
  • noload 指明不要載入的模組,當然是在 autoload=yes 的情況下
  • require 這項跟 load 一樣,但是有更強烈的要求之意,如果載入失敗就離開 asterisk 不繼續執行
  • preload-require 這項跟 preload 一樣,當然也是載入失敗就離開
==== indications.conf ====

這個檔定義各種地區的音頻設定(tone indications),預設是用美國,可以修改成台灣
country=us 改成 country=tw
在撥號計劃中,也可以用底下的語法來明確指明某 Context 採用哪個 tone indications
Set(CHANNEL(tonezone)=[tw])
Set(CHANNEL(musicclass)=[tw])
這邊提到的 tonezone, musicclass 通常也都會在別的設定檔中指明,或是採用在 indications.conf 中指定的 [genera] 中的 country 設定項。

底下用一個比較有趣的設定,星際爭霸的音效:

例如在 indications.conf 設定底下一節:
[starwars](us)
description = Star Wars Theme Song
ring = 262/400,392/500,0/100,349/400,330/400,294/400,524/400,392/500,0/100,
349/400,330/400,294/400,524/400,392/500,0/100,349/400,330/400,349/400,
294/500,0/2000

然後在 extensions.conf 中引用:
exten => 500,1,Answer()
  same => n,Set(CHANNEL(tonezone)=starwars)
  same => n,Dial(SIP/0000FFFF0002)

寫到這邊,如果您的系統只是自用的話,就在 indications.conf 中指明預設國家就夠了。如果還要給不同國家使用,那就在撥號計劃中如上面那樣使用。

==== musichold.conf ====

撥號時通常有各種音頻設定,甚至可以讓使用者下載自己的音頻(來電答鈴)。可以參考 http://www.university-music-on-hold.com/

這邊要說明的是,播放格式不同會影響 CPU 負載,例如 mp3 就遠比 .wav 還要需要 CPU 來解壓音頻。另外就是若採用非預設音頻格式的話,就需要指定 application 來播放,例如用 mpg123 來播放 mp3 格式的音頻,而 mpg123 可能需要額外安裝。

音頻檔放置路徑定義在 asterisk.conf 的 astdatadir 中,採用編譯的預設值跟 Ubuntu 套件的預設值是不同的,前者在 /var/lib/asterisk 而後者在 /usr/share/asterisk 中

======================================
講完幾個重要的設定檔之後,要提醒大家的是 log 檔,按照 asterisk.conf 的設定,可以找到 /var/log/asterisk/message 檔,它會記錄的跟 asterisk -cvvv 中見到的類似。 
也許你在 log message 中會見到很多錯誤,很有可能就是你載入了太多模組,而這些模組在編譯時可能沒有被設定支援。你可以試著解決這些問題。







Asterisk : The Cookbook 食譜 028 - 撥號計劃的互動 3 Pattern Matching

Q028: 撥號計劃裡面常常見到一些怪符號,可以說明它們是什麼嗎?

除了前面提到的變數外,還會見到一些符號,通稱為樣式比對(否則就是錯誤輸入了)。

樣式比對通常需要很大的篇幅,這邊直接從範例來介紹:
exten => _NXX,1,Playback(silence/1&auth-thankyou)


  • _ 開頭告訴 Asterisk 這段字串是樣式比對
  • N 表示 2...9。就像前面的提到的 same => n ... 這段說的 n
  • X 表示 0..9
  • Z 表示 1..9
    接下來說明「list」符號 []. 如果您對 Shell, Perl. 甚至 C, Java 等有了解的話,可以視為 array.....意思就是 [] 括住的『其中之一』.舉例來說
  • [123] 就代表 1, 2, 3 其中之一都符合樣式
  • [1-9] 則代表 1..9 這九個數字都符合樣式
  • [15-7] 則代表 1, 5, 6, 7 這四個數字符合樣式
  • . 沒錯,就是一個點 '.' 代表萬用字元,包括英文字也都符合樣式,有些語言的樣式比對用的是 ?
  • ! 是的,驚嘆號 '!' 代表零個以上。有些語言的樣式比對用的是 *
如果您的樣式有英文,又想用到 NXZ 怎麼辦呢?可以考慮底下的方法
_[N]extNXZ 這樣的話,會比對到 Next999 這種字串。

另外,在北美19個國家中,共用一個 NANP 電話號碼表示法如下:
1-NPA-NXX-XXXX, 第一碼的 1 是給北美國家使用的。

說到這邊不得不提到另一項跟變數+樣式比對相關的問題:
如何在引用時引用『現在的分機號碼』?例如:
exten => _XXX,1,Answer()
  same => n,SayDigits(??????)

是的,這個問題能夠解決的話就能夠做出更彈性的設定。

答案是...${EXTEN} 代表目前的分機號

exten => _XXX,1,Answer()
  same => n,SayDigits(${EXTEN})

對於變數處理,知道 shell script 的人應該知道一種表示法,這邊先說一下,Asterisk 變數表達字串時,最前面的字元所處的索引值是 0, 跟 C 語言的陣列一樣。先來看看實例,如果 ${EXTEN} 值為 123456789 的話.....

${EXTEN:2} 其值為 3456789 表示索引值第 2 以後,所以傳回值是從 3 開始
${EXTEN:3:4} 其值為 4567 表示從索引值 3 開始,長度4個
${EXTEN:-4:2} 其值為 67,表示從後面數來第4個,長度 2個。這邊要知道的是從頭數是 0, 倒數則從 1 開始。
${EXTEN:1:-3} 其值為 23456,表示從索引值 1 開始,去掉後面3個

如果能了解上面幾項的話,應該會發現還算是夠強大了吧?

最後補充在這邊的是,include,沒錯,相當於把另一個 Context 載入到此的意思,語法如下:
include => Context

例如以這幾篇食譜來說,可以自訂一個 Context 如下:
[Custom]
include => TestMenu

還有一個類似的用法是載入檔案,用法如下:
#include "......" <-- c="" p="">


Asterisk : The Cookbook 食譜 027 - 撥號計劃的互動 2 Dial()

Q027: Dial() 在撥號計劃中扮演相當重要的角色,可以說明它的用法嗎?

先直接寫 Dial() 的語法: Dial(destination,[timeout, option, URI])
首先,必須要有目標,後面是逾時、選項、URI。

distination 就是目標,我們從實例來看:
Dial(DAHDI/1) 指的是用 DAHDI 技術(裝置),後面接其使用的資源 1(此處指的是通道)。
Dial(SIP/7001), 這個出現在前面的食譜中,指的是用 SIP 技術裡面的 [7001] Context。
Dial(DAHDI/1&SIP/7001&IAX2/Softphone), 這個範例是同時播到三個目標。可以想像老人機中,同時撥電話、傳簡訊、送 email ...

以 IAX2 協定來看的話,其語法可以像底下這樣:
Dial(technology/user[:password]@remote_host[:port][/remote_extension])
實例如 exten => 500,1,Dial(IAX2/guest@misery.digium.com/s)

以 DAHDI 裝置來看的話,可以如下:
Dial(DAHDI/[gGrR]channel_or_group[/remote_extension])
exten => 501,1,Dial(DAHDI/4/18005551212)

第二個參數是逾時秒數,直接看範例,底下等60秒逾時:
exten => 7001,n,Dial(SIP/7001,60)

第三個參數是 option, 這個是針對 Dial() 而非前面的 destination, 也是需要另外的篇幅說明,這邊就只提一個,'m'。m 選項需要在其他的 Context 中設定 Hold Music。一般在撥號時,對方會聽到嘟嘟聲,有了m 之後聽到設定中的錄音檔(就像來電答鈴)。底下是個範例:

exten => 502,1,Dial(DAHDI/1,10,m)
  same => n,Playback(vm-nobodyavail)
  same => n,Hangup()

第四個參數是根據前面的 destination 來決定的,如果 destination 需要一個 URI 傳入的話才會用到它,例如 XMPP 這種即時通訊才會用到。因為相當少用就不多說了。

允許某個參數不填,直接跳過即可,例如底下省略逾時:
exten => 1,1,Dial(DAHDI/1,,m)

底下先用一個範例來表達 Dial(),請自行研讀:
[TestMenu]
  exten => start,1,Answer()
    same => n,Background(enter-ext-of-person)
    same => n,WaitExten(5)
  exten => 1,1,Dial(SIP/0000FFFF0001,10)
    same => n,Playback(vm-nobodyavail)
    same => n,Hangup()
  exten => 2,1,Dial(SIP/0000FFFF0002,10)
    same => n,Playback(vm-nobodyavail)
    same => n,Hangup()
  exten => i,1,Playback(pbx-invalid)
    same => n,Goto(TestMenu,start,1)
  exten => t,1,Playback(vm-goodbye)
    same => n,Hangup()

最後講講變數,沒錯,撥號計畫也因為有了變數使得整個設定更加彈性。
變數是區分大小寫的,習慣上會把變數都指定成大寫。順便說一下,通道則習慣用 Pascal 命名法。

在撥號計劃中,有三大類的變數:整體變數、通道變數、環境變數

整體變數定義在 [globals] Context 中,可以往前翻翻 Q025 這篇。整體變數可以在所有通道任何時候引用。在 [globals] Context 定義變數不像上面那樣,是採用更直接的方式,範例如下:

[globals]
LEIF=SIP/0000FFFF0001

通道變數就讓我們在底下直接用範例來說明:
exten => 301,1,Set(LEIF=SIP/0000FFFF0001)
  same => n,Dial(${LEIF})
還記得嗎?我們說撥號計劃是一系列的步驟。上面第一行只是設定變數 LEIF 而已,在第二行引用時,必須用 ${} 括住。

環境變數則是在執行 Asterisk 的時候由系統產生的,因為你在設定檔中看不到,引用方式也比較特別,語法 ${ENV(var)},範例如下:
${ENV(PATH)}

後面還有一項更複雜的項目需要說明,樣式比對。

Asterisk : The Cookbook 食譜 026 - 撥號計劃的互動 Interactive Dialplan

Q026: 前面只提到簡單的撥號計劃,可以進一步談談撥號計劃如何互相引用嗎?

是的,前一則食譜介紹的是靜態的撥號計劃,用來完成固定的動作是夠了。但是如果要在不同情況做不同的動作,就必須要特別的機制,這邊先介紹三個 Applications(): Goto(), Background(), 及 WaitExten()

Goto() 語法: same => n,Goto(context,extension,priority)
Goto() 需要三個參數,譬如: exten => 201,1,Goto(TestMenu,start,1)
如果要讓上面的範例可以作動,必須再定義一個 Context 如下:

[TestMenu]
exten => start,1,Answer()
....

這邊也示範了 Extension 不一定是數字,此處是 start
這個範例應用在一個實際通話中要求使用者輸入轉接分機,當使用者輸入 201 時,就會轉到 TestMenu 分機中。

Background() 語法跟 Playback() 一樣,但是最大的差異是 Playback() 遇到按鍵就中斷了,而 Background() 則不會。在應用上可以像加密輸入,或是邊播放語音說明邊等使用者輸入其他選擇(或是分機),後面的用法就是語音選單。這兩種用法都可以是 DTMF 數字。底下用一個比較實用的範例來說明:

[TestMenu]
  exten => start,1,Answer()
    same => n,Background(enter-ext-of-person)
    same => n,WaitExten(5)
  exten => 1,1,Playback(digits/1)
  exten => 2,1,Playback(digits/2)
...

上面也用到了 WaitExten(),剛好用來做為語音選單的範例。WaitExten() 可以接等待的秒數當參數,沒給的話就用預設值(10秒)。上面範例剛好示範了當使用者在 Background() 期間輸入數字的話,會聽到該分機相對應的聲音。

以本則食譜來說,當使用者撥入 201 分機的話,首先會聽到英文的『請輸入分機號碼,我將為您轉接』,然後等你輸入 1, 或 2。

如果分機是多位數的話,可以參考底下的範例:
[TestMenu]
  exten => start,1,Answer()
    same => n,Background(enter-ext-of-person)
    same => n,WaitExten(5)
  exten => 1,1,Playback(digits/1)
    same => n,Goto(TestMenu,start,1)
  exten => 2,1,Playback(digits/2)
    same => n,Goto(TestMenu,start,1)

講到這邊,你可以知道怎麼設計複雜的分機或是自動轉接的撥號計劃了。
底下就來處理無效的輸入或是處理逾時。

先簡介一下兩個分機號碼,無效輸入對應到 i, 逾時對應到 t。接下來用範例來說明:

[TestMenu]
  exten => start,1,Answer()
    same => n,Background(enter-ext-of-person)
    same => n,WaitExten(5)
  exten => 1,1,Playback(digits/1)
    same => n,Goto(TestMenu,start,1)
  exten => 2,1,Playback(digits/2)
    same => n,Goto(TestMenu,start,1)
  exten => i,1,Playback(pbx-invalid)
    same => n,Goto(TestMenu,start,1)
  exten => t,1,Playback(vm-goodbye)
    same => n,Hangup()

上例中,當使用者輸入數字不在設定中的話,例如 3,就會選擇對應到無效輸入的 i, 此處是播『無效』的錄音檔,然後跳回去重新輸入。如果等太久遲遲沒有收到輸入的話,就會播『再見』的錄音檔,然後結束通話。

說到這邊雖然引入了兩個應用,要跟真人來對比仍然不足,所以接下來再引入另一個應用 Dial()。

我覺得 Dial() 是相當重要的,所以打算單獨來說,看前一篇的範例就知道,Dial() 幾乎是我們實際在設計分機時都會想要用的,而不像這邊提到的 Background(), WaitExten(), i, t, 等等。


Asterisk : The Cookbook 食譜 025 - 撥號計劃 Dialplan

Q025: 看前面幾條食譜,似乎撥號計劃是最核心的功能,可以進一步說明嗎?

愛因斯坦說過,任何事應該盡量抽絲撥簡,卻不要將之視為簡單(Everything should be made as simple as possible, but not simpler.)。這句話用來理解相對論似乎就說得通了,當我們看狹義相對論的時候,看似相當容易理解,可是背後的事情卻是相當深遠地影響全世界。

同樣的 Asterisk 也是如此。Asterisk 最核心的撥號計劃相當容易理解,能做到的事卻相當廣泛。簡單講,撥號計劃定義了系統如何處理一個撥號接入與轉出。根據這麼簡單的理解,在 Asterisk 前後的版本發展也容入了不同的草稿語法。話說回來,又因為簡單又有彈性的草稿語法,使得 Asterisk 對所有撥號可以客製化一個複雜的計劃(流程)。我可以大膽的聲稱,Asterisk 的能力取決於你對撥號計劃的了解多寡。

撥號計劃(extensions.conf)分成四個主要部份:Contexts, Extensions, Priorities, 及 Applications. 如果您從前一則食譜編譯 Asterisk 的話,範例應該是在 ~/pbx/asterisk-11.21.0/configs/extensions.conf.sample

[Contexts]

我們先以前則提到的內容來說,摘出一段如下:
[internal]
exten => 7001,1,Answer()
exten => 7001,2,Dial(SIP/7001,60)
exten => 7001,3,Playback(vm-nobodyavail)
exten => 7001,4,VoiceMail(7001@main)
exten => 7001,5,Hangup()

撥號計劃分成多個節,每一節稱之為一個 Context, 每一個 Context 看似獨立,卻是整個撥號計劃的一環,彼此互相連結而非單獨存在(就像眼耳手腳)。而 extension 翻譯成中文可以說是分機號碼或是電話號碼,在某一 Context 中定義時除非特別指明(引用),否則是與其他 Context 獨立無關的(也就是在別的 Context 中重複定義也沒關係)。

想像一下有兩個公司共用同一個 Asterisk 服務器(這很常見),假如將每一個公司的自動話務員放在各自的 Context 中,此時他們當然就各自獨立不互相千擾對方,也就是說各自公司都有相同的分機號碼是彼此不會干擾對方的。

有兩個特別的 Contexts: [general] 及 [globals]。[general] 定義一般的撥號計劃的設定,這邊只需要知道這兩個非一般的 Context 即可。

前文也提過「通道」的說法,一個通道不止包括 extensions.conf, 還包括 sip.conf, iax.conf, chan_dahdi.conf 等等等。通道定義中一個必要的參數就叫 context,我們就以圖來表示吧:

圖中 sip.conf 中有一通道 [0000FFFF0001] 定義,其中的 context 指向 extensions.conf 中的 [LocalSets] 這個 Context. 當然這邊的字眼既然採用同一個,能幫助記憶,也有點容易讓人混淆,就靠你自行理解記憶囉。

對 Context 一個最重要的用法其實是『加密』,系統如果沒有很好的加密的話,很有可能就變成駭客入侵最佳的平台。可以參考 Asterisk Wiki中的說明,後面也會提到。

[Extensions]
在電信世界中,分機號碼通常就只是一組號碼,但是在 Asterisk 中,要理解 Extesions 卻必須看成『用來回應一通撥號的一串步驟』(這也是為什麼要叫撥號計劃的原因)。

在撥號計劃中,Extensions 是以關鍵字 exten 來表示,後面接 => 來表達指定之意。另外要提的是,在 Asterisk 的觀念中,Extensions 並非單純的數字,它通常是數字+文字的組合,甚至可以是 email 的格式,這一點讓 Asterisk 更有彈性也讓 Asterisk 更加強大。

一個 Extesion 的每個步驟包括三個部份:分機名稱(或是號碼), 權值(也可以看成真正的順序),及 application(或是命令)。其形式就是 :
exten => name, priority, application()
以最上面顯示的來說,就像 "exten => 7001,1,Answer()"

[Priorities]
其中權值可以視為真正的順序,所以最好是由小到大遞增,這樣也有助於理解。但是因為權值用純粹的數字容易引發問題,就像早期的 basic 語言一樣,要在中間插入一條步驟的話就會有困擾,所以後來引入 n 這個權值,它代表就按出現的順序由上而下承接,要記住的是一定要從 1 開始不能省略。例如:

exten => 7001,1,Answer()
exten => 7001,n,Dial(SIP/7001,60)
exten => 7001,n,Playback(vm-nobodyavail)
exten => 7001,n,VoiceMail(7001@main)
exten => 7001,n,Hangup()

為了簡化撰寫,後來也引入了 same => 記號,上例又可以寫成如下:
exten => 7001,1,Answer()
  same => n,Dial(SIP/7001,60)
  same => n,Playback(vm-nobodyavail)
  same => n,VoiceMail(7001@main)
  same => n,Hangup()
右移內縮只是為了容易閱讀,有沒有內縮不影響設定。

權值還可以給予標記,以便後面引用,就像程式語言中的 goto,這邊暫時不提,只簡單寫出正確與錯誤的寫法:
[正確] exten => 123,n(label),application()
[錯誤] exten => 123,n,(label),application()

[Applications]
Applications 可以視為『命令』或是『動作』,用來完成一個通道的一系列回應動作。最常用的有兩個:Answer(), Hangup()。每個 Application 括號中可以加入參數(有可能沒有參數),請見上面的實例。

Answer() 用來告知通道正在『響鈴中』,不必給予參數,也不見得一定要求有此 application。

Playback() 用來播放一段錄音檔,此時撥號端的用戶輸入聲音會被忽略,所以在自動話務員中不應該使用 Playback()。錄音檔通常可以在 /var/lib/asterisk/sounds 中找到,如果是 Ubuntu 預設套件的話,應該是在 /usr/share/asterisk/sounds 中。上例的 Playback() 只需提供檔名,省略附檔名,也可以指定 full-path 格式,但是仍然省略附檔名。附檔名系統會自動找,因為有可能是 .wav, .g722,  或是 .gsm 等等等

Hangup() 則用來掛斷通道,每個 Context 的最後,應該都以 Hangup() 結尾。


2016/02/02

Asterisk : The Cookbook 食譜 024 - 在 Ubuntu 下的簡易安裝

Q024: 在 Ubuntu 下不想自己編譯 source code,有沒有更簡單的安裝方法?

是的,目前 Ubuntu  15.10 系統預設的 asterisk 套件版本是 13.1.0, 是可以直接透過安裝套件來達到目的,底下是簡單的指令:

1. 安裝套件
  $ sudo apt-get install asterisk asterisk-config asterisk-core-sounds-en asterisk-core-sounds-en-* asterisk-modules asterisk-moh-opsound-g722 asterisk-moh-opsound-gsm asterisk-moh-opsound-wav asterisk-mp3 asterisk-mysql asterisk-ooh323 asterisk-voicemail

2. 修改設定檔
  請按照 Q023 的設定檔,修改 asterisk.conf  extensions.conf  manager.conf  modules.conf  sip.conf

如果您啟動 asterisk 之後不知道原因無法連線的話,最需要檢查的三個檔是 extensions.conf, sip.conf 及 modules.conf
之所以會特別加這一段話,主要就是因為我在我的電腦 15.10 中工作,所以常常亂裝些套件,更動了不知道什麼設定之後,幾年來又常常把它更新到最新的 Ubuntu 版本。所以我在前前後後幾次裝 Asterisk 都失敗,嘗試了自己編譯或是 Ubuntu 內建套件安裝都無果,也就是我一直搞不懂失敗原因。也因為這樣的機緣,所以我才打算翻譯這本食譜,那時我還不認為我的電腦能夠解決問題,本打算重新安裝試試呢。
在寫這篇食譜之前,我用一台小筆電裝過是成功的,那本是用 FreePBX, 後來用虛擬機用 ubuntu 14.04 Server 安裝也是成功的,那台是用編譯的 Asterisk。
後來在寫這一則食譜(Q023)的時候,我剛剛好照著文件上的做(不是我寫的部份)其實是錯的,因為照原文的話是 ./configure --libdir=/lib64,造成系統找不到 modules, 但是反而能夠連通。也因為這樣的機緣,才讓我找到主要原因是 modules.conf 的問題。原文提到的設定,應該是在 asterisk.conf 中將 astdatadir 設成 /lib64/asterisk 才對。只有純粹的 ./configure 才會將模組裝在預設的 /var/lib/asterisk 下。

如果您發現 SIP client 跟 Asterisk 不通,而你又覺得設定檔都已經照著教學修改了卻仍然不通的話,很有可能是 modules 的設定。以上一則的範例來說,我是把所有 PJSIP 模組都拿掉了才通的。

總之,不是把所有模組都載入才是最好的,而是適合自己的才是最好的。

Asterisk : The Cookbook 食譜 023 - 在 Ubuntu 下的安裝

Q023: 能談談在 Ubuntu 下怎麼安裝 Asterisk 嗎?

其實不管 Ubuntu, Debian, Redhat, CentOS 等等,系統部份雖然不見得一樣,但是 Asterisk 的編譯部份是相差不多的。

1. 安裝編譯環境:
  $ sudo apt-get install build-essential subversion \
libncurses5-dev libssl-dev libxml2-dev libsqlite3-dev \
uuid-dev

2. 準備源碼
  $ mkdir ~/pbx; cd ~/pbx
  $ wget http://downloads.asterisk.org/pub/telephony/asterisk/asterisk-11-current.tar.gz
  我實驗的版本是 11-current, 解開後是 11.21.0,可以自行驗證 12, 13 等
  $ tar zxf asterisk-11-current.tar.gz; cd asterisk-11*
  $ contrib/scripts/get_mp3_source.sh
  $ sudo ./contrib/scripts/install_prereq install
  $ sudo ./contrib/scripts/install_prereq install-unpackaged
3. 編譯,以我的 Ubuntu 是 64 bit, 命令如下
  $ ./configure 即可

    目前的電腦大部份都是多核心的,底下 -j8 可以加速編譯速度,可以視系統的 CPU 核心數來決定。
  $ make -j8
  $ make menuselect
  在 make menuselect 時,我是盡可能都選了,也可以不做 make menuselect,這不影響本次實驗結果
  $ sudo make install && sudo make config && sudo make samples
4. 修改權限
  $ sudo adduser --home /var/lib/asterisk --shell /bin/false asterisk && sudo passwd asterisk
  $ sudo chown -R asterisk.asterisk /var/{lib,spool,log,run}/asterisk
5. 準備設定檔,存放在 /etc/asterisk/ 中
  這一段比較複雜,先以我實驗的實例提供大家參考

===== extensions.conf ======
[globals]
INTERNAL_DIAL_OPT=,30
[internal]
exten => 7001,1,Answer()
exten => 7001,2,Dial(SIP/7001,60)
exten => 7001,3,Playback(vm-nobodyavail)
exten => 7001,4,VoiceMail(7001@main)
exten => 7001,5,Hangup()
exten => 7002,1,Answer()
exten => 7002,2,Dial(SIP/7001,60)
exten => 7002,3,Playback(vm-nobodyavail)
exten => 7002,4,VoiceMail(7001@main)
exten => 7002,5,Hangup()
exten => 7003,1,Answer()
exten => 7003,2,Dial(SIP/7001,60)
exten => 7003,3,Playback(vm-nobodyavail)
exten => 7003,4,VoiceMail(7001@main)
exten => 7003,5,Hangup()
exten => 8001,1,VoicemailMain(7001@main)
exten => 8001,2,Hangup()
exten => 8002,1,VoicemailMain(7002@main)
exten => 8002,2,Hangup()
exten => 8003,1,VoicemailMain(7003@main)
exten => 8003,2,Hangup()

===== sip.conf ======
[general]
  context=internal
  allowguest=no
  allowoverlap=no
  bindaddr=0.0.0.0
  bindport=5060
  srvlookup=no
  allow=ulaw
  localnet=192.168.1.0/255.255.255.0
[7001]
  type=friend
  host=dynamic
  secret=123456
  context=internal
[7002]
  type=friend
  host=dynamic
  secret=123456
  context=internal
[7003]
  type=friend
  host=dynamic
  secret=123456
  context=internal

===== manager.conf ======
[general]
  enabled = yes
  webenabled = yes
  port = 5038
  bindaddr = 0.0.0.0
  displaysystemname = yes
  httptimeout = 60
[admin] 
  secret = 123456
  permit = 192.168.1.0/24
  read = system,call,log,verbose,agent,user,config,command,dtmf,reporting,cdr,dialplan,originate,message
  write = system,call,log,verbose,agent,user,config,command,reporting,originate,message

===== asterisk.conf ======
[directories]
astetcdir => /etc/asterisk
astmoddir => /usr/lib/asterisk/modules
astvarlibdir => /var/lib/asterisk
astdbdir => /var/lib/asterisk
astkeydir => /var/lib/asterisk
astdatadir => /var/lib/asterisk
astagidir => /var/lib/asterisk/agi-bin
astspooldir => /var/spool/asterisk
astrundir => /var/run/asterisk
astlogdir => /var/log/asterisk
astsbindir => /usr/sbin
[options]
runuser = asterisk
rungroup = asterisk
documentation_language = en_US
[compat]
pbx_realtime=1.6
res_agi=1.6
app_set=1.6

===== modules.conf ======
[modules]
  autoload=yes
  load => res_musiconhold.so
  noload => res_speech.so
  noload => res_phoneprov.so
  noload => pbx_gtkconsole.so
  noload => chan_alsa.so
  noload => chan_console.so
  noload => res_ael_share.so
  noload => res_clialiases.so
  noload => res_adsi.so
  noload => pbx_ael.so
  noload => pbx_dundi.so
  noload => chan_oss.so
  noload => chan_mgcp.so
  noload => chan_skinny.so
  noload => chan_phone.so
  noload => chan_agent.so
  noload => chan_unistim.so
  noload => app_nbscat.so
  noload => app_amd.so
  noload => app_minivm.so
  noload => app_zapateller.so
  noload => app_ices.so
  noload => app_sendtext.so
  noload => app_speech_utils.so
  noload => app_mp3.so
  noload => app_flash.so
  noload => app_getcpeid.so
  noload => app_setcallerid.so
  noload => app_adsiprog.so
  noload => app_forkcdr.so
  noload => app_sms.so
  noload => app_morsecode.so
  noload => app_followme.so
  noload => app_url.so
  noload => app_alarmreceiver.so
  noload => app_disa.so
  noload => app_dahdiras.so
  noload => app_senddtmf.so
  noload => app_sayunixtime.so
  noload => app_test.so
  noload => app_externalivr.so
  noload => app_image.so
  noload => app_dictate.so
  noload => app_festival.so
  noload => chan_pjsip.so
  noload => func_pjsip_aor.so
  noload => func_pjsip_contact.so
  noload => func_pjsip_endpoint.so
  noload => res_hep_pjsip.so
  noload => res_pjsip_acl.so
  noload => res_pjsip_authenticator_digest.so
  noload => res_pjsip_caller_id.so
  noload => res_pjsip_config_wizard.so
  noload => res_pjsip_dialog_info_body_generator.so
  noload => res_pjsip_diversion.so
  noload => res_pjsip_dlg_options.so
  noload => res_pjsip_dtmf_info.so
  noload => res_pjsip_endpoint_identifier_anonymous.so
  noload => res_pjsip_endpoint_identifier_ip.so
  noload => res_pjsip_endpoint_identifier_user.so
  noload => res_pjsip_exten_state.so
  noload => res_pjsip_header_funcs.so
  noload => res_pjsip_keepalive.so
  noload => res_pjsip_log_forwarder.so
  noload => res_pjsip_logger.so
  noload => res_pjsip_messaging.so
  noload => res_pjsip_multihomed.so
  noload => res_pjsip_mwi_body_generator.so
  noload => res_pjsip_mwi.so
  noload => res_pjsip_nat.so
  noload => res_pjsip_notify.so
  noload => res_pjsip_one_touch_record_info.so
  noload => res_pjsip_outbound_authenticator_digest.so
  noload => res_pjsip_outbound_publish.so
  noload => res_pjsip_outbound_registration.so
  noload => res_pjsip_path.so
  noload => res_pjsip_phoneprov_provider.so
  noload => res_pjsip_pidf_body_generator.so
  noload => res_pjsip_pidf_digium_body_supplement.so
  noload => res_pjsip_pidf_eyebeam_body_supplement.so
  noload => res_pjsip_publish_asterisk.so
  noload => res_pjsip_pubsub.so
  noload => res_pjsip_refer.so
  noload => res_pjsip_registrar_expire.so
  noload => res_pjsip_registrar.so
  noload => res_pjsip_rfc3326.so
  noload => res_pjsip_sdp_rtp.so
  noload => res_pjsip_send_to_voicemail.so
  noload => res_pjsip_session.so
  noload => res_pjsip_sips_contact.so
  noload => res_pjsip.so
  noload => res_pjsip_t38.so
  noload => res_pjsip_transport_websocket.so
  noload => res_pjsip_xpidf_body_generator.so

6. 啟動與停止
  $ sudo /usr/sbin/asterisk -cvvv
  $ CLI> core stop now
如果想直接在背景執行,就不加參數 $ sudo /usr/sbin/asterisk
如果想停止背景中的 asterisk, 可以如下:
  $ sudo /usr/sbin/asterisk -rc
  CLI> core stop now

PS: 我試過 asterisk-13.7.0, 11.21.0, 12.8.2 都是可以連線的
PS: 如果無法連線,有很多情況是模組載入問題,請試著修改 modules.conf 試試

2016/02/01

Asterisk : The Cookbook 食譜 022 - 其它 Asterisk 專案

A022: 當我在尋找 Asterisk 時,看到很多不同的專案,這份食譜會說明這些專案嗎?

因為 Asterisk 是相當開放的軟體專案,所以也受很多組織支援。譬如 FreePBX 就是其中最著名的一套,主要是以 Asterisk 為核心,包裝了管理的 GUI 界面,並且釋出 ISO 檔讓人很容易安裝到機器中。

因為不同的打包會有不同的安裝、使用方式,我只打算講解 Asterisk 這個核心功能,因為光 Asterisk 就已經需要大量的篇幅。當然,這些專案也都以 Asterisk 為核心,所以只有深入了解基礎部份,也對您了解並使用其它專案有很大的幫助。

底下摘要說明 FreePBX 專案及其衍生的專案:

FreePBX: 由 FreePBX 組織發行,主要整合 Web GUI + mysql 當資料庫管理 Asterisk。底下還有其它基於 FreePBX 而來的其它套件。

AsteriskNOW: 由 Digium 維護發行
Elastix
FreePBX ISO
PBX in a Flash

這邊要特別說明的是,單純的 Asterisk 就足夠您使用了,但是要簡化安裝管理的話,或許您還得繼續研讀 FreePBX. 但是,它們並非真正免費的,當您進一步使用這些專案就知道了。

Asterisk : The Cookbook 食譜 021 - Asterisk 的版本

Q021: Asterisk 的版本好像很複雜?感覺不是很一致,可以說明一下嗎?

一個一直維護的版本來自這兒, 底下是摘要,這邊先補充一句話,在 1.1x 以後的版本,就直接以 1x 為主版號:

Release SeriesRelease TypeRelease DateSecurity Fix OnlyEOL
1.2.X2005-11-212007-08-072010-11-21
1.4.XLTS2006-12-232011-04-212012-04-21
1.6.0.XStandard2008-10-012010-05-012010-10-01
1.6.1.XStandard2009-04-272010-05-012011-04-27
1.6.2.XStandard2009-12-182011-04-212012-04-21
1.8.XLTS2010-10-212014-10-212015-10-21
10.XStandard2011-12-152012-12-152013-12-15
11.xLTS2012-10-252016-10-252017-10-25
12.xStandard2013-12-202014-12-202015-12-20
13.xLTS2014-10-242018-10-242019-10-24
14.xStandard2016-10 (tentative)2017-10 (tentative)2018-10 (tentative)
15.xLTS2017-10 (tentative)2021-10 (tentative)2022-10 (tentative)

上面 1.8.x 存在的時間相當久,所以一般文件也都在講這個版本。另外 11.x 起始於 2012-10-25, 因為它被標註為 LTS, 所以也有文件以它為主。

再看看 13.x 系列是從 2014-10-24 才開始,因為它被標註為 LTS(長期支援版本),因此 Ubuntu 中的套件也是這個系列,但是它起始年份比較新,所以要找到專門的文件反而不多。

Asterisk Release Schedule

Asterisk : The Cookbook 食譜 020 - 支援的硬體

Q020: 請問 Asterisk 要成為真正的 PBX 需要哪些硬體?

Asterisk 相容於現有諸多不同的技術,也因此有很多公司生產支援的硬體,譬如 Digium, Sangoma, Rhino, OpenVox, Pika, Voicetronix, Junghanns, Dialogic, Xorcom, beroNet 等等等。前兩個是比較受歡迎的公司,但是在參考支援的硬體時,適不適合你使用才是最後決定性因素。

最受歡迎的硬體通常支援一個硬體界面稱為 Digium Asterisk Hardware Device Interface(DAHDI),  後面還有討論到它。

這邊要再補充一句就是,這些硬體都會有不同的安裝需求與檔案結構,請在購買之前先詢問好技術支援,確認你都懂了再買會比較保險。

Asterisk : The Cookbook 食譜 019 - 撥號計劃

Q019: 請問什麼是『撥號計劃』?

撥號計劃是 Asterisk 的心臟。在系統中收到的所有信號通道將被傳遞到撥號計劃,其中包含 通話腳本,它決定打進來的通話該如何被處理。

撥號計劃有三種表達方式如下:

  1. 使用傳統的 Asterisk 撥號計劃語法,它儲存在 /etc/asterisk/extensions.conf
  2. 使用新的 Asterisk 擴充邏輯(AEL),它儲存在 /etc/asterisk/extensions.ael
  3. 使用 Lua 語法,它儲存在 /etc/asterisk/extensions.lua
別急,後面會講到傳統語法,它很容易延展到另外兩個(AEL, Lua)