FreePBX: the D-Modem module: technical details¶
This is the technical companion to FreePBX: the D-Modem module, the plain-English overview. Start there; this page keeps the full detail: the code's names, the Recommendations' clauses, the numbers and the test lists.
frontend/freepbx/dmodem/ is a FreePBX module (version 16.0.2, for FreePBX 16
and 17) for app_dmodem or dmodem-ari. It
provides:
- the modem's profiles, edited in FreePBX's web interface, each a destination for inbound routes, IVRs, time conditions and the like;
- calls out to a remote modem with a profile;
- the settings and profiles in FreePBX's Backup & Restore;
- a check of the PJSIP trunks and extensions for modem calls.
It adds Applications > D-Modem to the menu and the fwconsole dmodem
command. The module is PHP only; nothing in it is compiled.
Installing¶
- The modem. Choose one engine:
- dmodem-ari beside Asterisk (
build/ari/dmodem-ari), with an ARI (Asterisk REST Interface) user in Settings > Asterisk REST Interface Users. This is the default engine. - app_dmodem, on an Asterisk you build it for yourself. With
Asterisk's headers installed, run
cmake -B build -DDMODEM_APP_DMODEM=ON && cmake --build build, copyapp_dmodem.soto Asterisk's modules directory, then runasterisk -rx "module load app_dmodem.so". A built app_dmodem may not be passed on to anyone (LICENSING.md). - The module.
cmake --build build --target freepbx_modulemakesbuild/freepbx/dmodem-<version>.tgz. Upload it in Admin > Module Admin (Upload modules), or unpack it into FreePBX's modules directory and install it from the command line:
tar xzf dmodem-16.0.2.tgz -C /var/www/html/admin/modules/
fwconsole ma installlocal dmodem
fwconsole reload
The module is not signed, so FreePBX shows it as unsigned.
Using it¶
The module's page has five parts.
Profiles¶
A profile says where a call's data goes once the modems connect:
- a TCP server (a BBS);
- a server on a unix socket;
- a program on a pty (
pppdfor dial-up IP, a login, a door).
Each profile also has:
| field | default | notes |
|---|---|---|
| G.711 law | auto |
auto, ulaw or alaw. |
ISDN data calls (clear) |
auto |
auto: a clear channel for a call with a digital bearer; yes: every call; no: never. A CLEARMODE call is clear all the same. See isdn.md. |
Modem commands (init) |
empty | Extra AT commands, starting with AT, 200 characters at most. |
Answer command (answer) |
empty (ATA) |
For example AT+MS=132;A answers at V.32bis at most. |
| Connect timeout | 60 s | 5 to 600 s. |
Take fax calls (fax) |
on | Receive a calling fax machine as a fax. |
Call details first (header) |
off | TCP and unix-socket servers only. |
Hand over the line (handoff) |
off | Unix-socket servers with call details first only: the server gets the modem's line itself (a terminal) instead of a copy of its bytes. See the handoff. |
| Description | empty | One line, 100 characters at most; shown with the destination. |
A profile name has 1 to 40 characters (lower-case letters, digits, -
and _, starting with a letter or digit). The names general,
receivefax and sendfax are reserved.
A profile bbs is the destination app-dmodem,bbs,1, listed under
"D-Modem" in every destination picker. A profile keeps its name once
saved, because the name is its destination. A profile that a route or
another object still points at cannot be deleted.
Settings¶
- Engine: dmodem-ari's
Stasis()(the default), orDModem()in an Asterisk that has app_dmodem built for itself. - Log level: the modem's
debug, 0 to 3. - dmodem-ari (engine
arionly): the ARI URL (http://127.0.0.1:8088by default), user (dmodem), password (left empty on the form, the stored one is kept) and Stasis app name (dmodem). Optionally, T.38 through dmodem-ari's own SIP side, with its PJSIP endpoint (dmodem) and SIP address (127.0.0.1:5090). - Dialling out: the dial-out context (
from-internal), the caller ID, and the ring time (60 s; 5 to 300 s). - Fax, the fax settings of
[general]: - the directory received faxes go to (
/var/spool/asterisk/fax); - our fax number (up to 20 digits,
+and spaces); - the fastest rate (14400, 12000, 9600, 7200, 4800 or 2400 bit/s);
- error correction mode (on) and T.38 (on);
- Fax destination (on): a destination "D-Modem: receive a fax"
(
app-dmodem,receivefax,1) that answers as a fax machine.
Dial out¶
A call to a remote modem (a BBS, an ISP) with a profile:
- The number (digits,
*,#and+, 40 at most) is dialled in the dial-out context. That isfrom-internalby default, so your outbound routes pick the trunk, as for an extension's call. - The call uses the settings' caller ID (or the one given) and ring time.
- Once the far end answers, D-Modem is the calling modem:
DModem(<profile>,o), orStasis(<app>,<profile>,o)with dmodem-ari. The profile's server or program then talks to the remote end.
The module asks Asterisk's manager (AMI) to originate the call, with the
channel Local/<number>@<context> and the channel variable
DMODEM_DIALOUT=<profile>.
Line check¶
The line check reads each PJSIP endpoint (up to 200) as Asterisk has it now, through the manager, and reports what matters to a modem call:
- no G.711 at all (a warning), or codecs other than G.711 (a warning).
CLEARMODE (
codec_clearmode's) is listed apart, for ISDN data calls; - both laws allowed (a warning: a call can be converted between them, which costs V.90);
- DTMF detected in the audio (
inband, or a mode starting withauto: a warning); - fax detection on (a warning: the modem's 2100 Hz answer tone sounds like a fax machine's);
- direct media on, and T.38 on (notes).
It also shows whether app_dmodem is loaded (module show like
app_dmodem), or whether dmodem-ari is connected (its Stasis app in ari
show apps).
Generated files¶
This part shows what Apply Config writes.
What Apply Config writes¶
Apply Config writes /etc/asterisk/dmodem.conf (with FreePBX's header
first). Don't edit that file by hand: the next reload writes it again.
It also writes the app-dmodem context into
extensions_additional.conf:
[app-dmodem]
exten => bbs,1,NoOp(D-Modem profile bbs)
exten => bbs,n,DModem(bbs)
exten => bbs,n,NoOp(D-Modem bbs: ${DMODEMSTATUS} ${DMODEMRATE})
exten => bbs,n,Hangup()
exten => receivefax,1,NoOp(D-Modem fax)
exten => receivefax,n,DModemReceiveFax(/var/spool/asterisk/fax/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${FILTER(0-9A-Za-z+,${CALLERID(num)})}-${UNIQUEID}.tif)
...
With dmodem-ari as the engine, DModem(bbs) is Stasis(dmodem,bbs) and
the fax step is Stasis(dmodem,receivefax,<file>). The fax file is named
as DModem() names the faxes it receives by itself, except that an empty
caller ID leaves that part empty (where DModem() writes unknown).
app_dmodem reads dmodem.conf on each call, so a change applies from the
next call. dmodem-ari reads it when it starts, so restart it after Apply
Config.
Uninstalling the module empties its stored settings and profiles;
dmodem.conf is left for the next reload to drop.
fwconsole dmodem¶
fwconsole dmodem dial <profile> <number> [--callerid='"BBS" <5550100>']
fwconsole dmodem profiles
fwconsole dmodem export [file] # the settings and profiles, as JSON
fwconsole dmodem import <file> [--keep] # back in; --keep keeps the profiles
# that the file doesn't have
dialis the Dial out page for scripts (cron, a BBS's mailer).exportwithout a file prints the JSON. An export holds the ARI password, so a file it writes is readable by its owner alone (mode 0600).importchecks the settings and each profile as the forms do (see Backup and restore). After an import, runfwconsole reloadto write the files.
Backup and restore¶
The module takes part in FreePBX's Backup & Restore (Backup.php,
Restore.php). A backup that includes D-Modem keeps its settings and
profiles: the same data as fwconsole dmodem export.
A restore (like fwconsole dmodem import without --keep) checks the
data as the forms do:
- The settings are checked together. If any of them fails, none is restored, and the failing ones are named in the restore's log.
- Each profile is checked on its own. One that fails (for example a
profile named
general) is left out and named in the log; the others are restored. - Profiles that are not in the backup are deleted.
dmodem.conf and the dialplan are made again on the reload after the
restore. Received faxes are files in the fax directory, outside the
module's data, so a backup of the module does not include them.
The calls' path¶
V.90 needs the call's G.711 untouched from the network to the modem (asterisk.md). In FreePBX:
- In Settings > Asterisk SIP Settings, and on the trunk and the modem extension, allow one law only: the one your provider uses (ulaw in North America and Japan, alaw elsewhere). Your provider must not transcode either.
- On those endpoints: DTMF RFC 4733, Fax Detect off, and T.38 off where no fax is meant to use it.
- On the modem's inbound route: Detect Faxes no, no call recording, and no music on hold or announcement before the answer.
The line check shows what is still wrong.
The module's code¶
module.xml: the module's name, version (16.0.2), menu item, console command and FreePBX versions.Dmodem.class.php: the BMO (FreePBX's module object) class. It has the forms (doConfigPageInit()), the pages,genConfig()andwriteConfig()fordmodem.conf, the dialplan hook (doDialplanHook()), the destinations, the dial-out (dialOut()), the export and import (exportData(),importData()) and the line check (check()). The settings and profiles live in the module's kvstore.lib/Config.php: the settings' and profiles' defaults and checks, anddmodem.confmade from them. A;in a value is written\;, as Asterisk and dmodem-ari read it, and no line breaks get through.lib/Dialplan.php: theapp-dmodemcontext.lib/Apps.php: the dialplan step class (ext_dmodem_app) forDModem(),Stasis()andDModemReceiveFax(), which FreePBX's extensions class has no classes of its own for.lib/LineCheck.php: readspjsip show endpoints,pjsip show endpoint <name>,module showandari show apps, and works out what an endpoint's settings mean for a modem.lib/Dialout.php: a call out's Originate action, and the checks of its number and caller ID (nothing that would break the manager's lines).lib/Cli.php:fwconsole dmodem's subcommands (Console/Dmodem.class.phphands them over, so they work without Symfony).Backup.php,Restore.php: Backup & Restore (throughDmodem::exportData()andimportData()).functions.inc.php: FreePBX's destination hooks.page.dmodem.php,views/,assets/js/dmodem.js: the pages.
Tests¶
unit.php¶
frontend/freepbx/test/unit.php (ctest freepbx_unit, label quick, built when
PHP is installed) tests the module without FreePBX. It runs on stand-ins
for what the module uses (fpbx_env.php):
- the kvstore, with DB_Helper's rules (storing false deletes a key, and values come back as strings);
extensions::add()and its output, as FreePBX'sextensions.class.phphas them;- the manager, with canned CLI output;
- the destination usage.
It checks:
- the forms, with good and bad values;
dmodem.conf, read back as dmodem-ari reads it;- the dialplan for both engines;
- the destinations;
- the line check, on Asterisk 20's own CLI output (
test/fixtures/); - the dial-out (its Originate, and refused numbers and caller IDs);
fwconsole dmodem;- an export imported elsewhere;
- the module's own Backup and Restore classes, on stand-ins of the backup
module's base classes (
backup_env.php); - the pages.
FREEPBX_FRAMEWORK=<a checkout of FreePBX's framework> runs the dialplan
through FreePBX's own extensions.class.php instead of the stand-in.
e2e.sh¶
frontend/freepbx/test/e2e.sh runs the module's output in a private Asterisk
(ARI=1: with dmodem-ari as the engine):
gen.phpmakesdmodem.confand theapp-dmodemcontext the way the page does (its forms posted).- An inbound route sends 5551000 to
app-dmodem,bbs,1. dmodemd calls it over SIP and uses the BBS; the test checks that the call went throughapp-dmodem,bbs,1. (Two D-Modems on a clean G.711 path connect with V.91, at 62666/62666 bit/s.) - A call out:
dial.php(asfwconsole dmodem dial) has the manager originate it through the dial-out context (modem-outhere, the test's stand-in forfrom-internal) to dmodemd listening, withDModem(term,o)calling. The test checks thatLocal/5551000@modem-outwas used. - The line check runs on the live endpoints through the manager
(
linecheck.php,ami.php). The test's endpoint allows both laws, which the check must flag as a warning.