The modem core: technical details¶
This is the technical companion to The modem core, 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.
core/ is the part of the modem between the DTE and the data pumps.
(The DTE, data terminal equipment, is the program on the other side of
the serial port or pty.) It is C++, in namespace dcore, declared in
dcore.h.
What the core holds¶
The AT command interpreter¶
The interpreter is in at.cpp. at_commands.pdf is
the reference for every command.
| Group | Commands |
|---|---|
| Basic | A B D E H I L M N O P Q S T V X Y Z |
& |
&A &C &D &E &F &H &K &L &P &Q &R &S &T &V |
\ and % |
\A \N \V, %B %C %E |
| Others | *B, #CID #UD |
| Modulation | +MS (by number or by V.250's name); V.18 text telephones (+MV18S +MV18R +MV18AM +MV18P, +MBAUDOT) |
| Identity and country | +GCI +GCAP +GMI +GMM +GMR |
| Fax and voice | +FCLASS; class 1 fax (+FTS +FRS +FTM +FRM +FTH +FRH +FMI +FMM +FMR); class 2 and 2.0 fax (+FCLASS=2 and 2.0: +FLI +FPI +FCC +FIS +FCS +FBO +FCR +FPS +FLP +FSP +FHS +FDT +FDR +FET +FKS, and class 2's names, in at_fax2()); voice (+V... in +FCLASS=8, +VCID) |
| V.92 | modem-on-hold (+PMH +PMHT +PMHR) |
| Configurators | !C !CF !CT (idle and on hook), !R (the init string), !M (the settings model, for programs) |
Other modems' setup strings. The interpreter also accepts the commands that other modems' setup strings send (V.250's, Rockwell's and Conexant's, USRobotics'):
- the basic commands of
taken_cmds, which have no effect; - the extended parameters of
kept_params, which keep their values for?and=?. Most have no effect either. Some are D-Modem settings thatmodem.cppreads:+ES,+ESA,+ITFand+IFC(V.80),+DS,+DS44and+DSZ(compression),+B2,+CBSTand+CRC(ISDN clear channels),+FAA(adaptive answer) and+CVM(V.cvm).
Command lines. ; may separate commands. Command lines are edited
(backspace), echoed, and repeated with A/.
Settings and results¶
- The S-registers.
- The result codes: verbose or numeric, quiet, and the connect
messages of
&A,Xand\V. WithV0a CONNECT's number gives its rate, as Smart Link's did. - The country parameters (
homolog_data.cpp,+GCI).
The configurators¶
config.h holds one settings model (Config::model()) that AT!R,
AT!CF, AT!CT and AT!C share (config.cpp, config_tui.cpp,
config_cli.cpp). Each Setting has:
- its key on the command line, its group, a label and a line of help;
- its AT command (
form,%sthe value); - its values: choices (each with its AT text, a command line name and a
label), a number from
lotohi, or an extended parameter's list; - how to read it from the modem (
get).
The model covers the basic commands' settings, the S-registers with no
command of their own (timers, characters), +GCI, +MS (its four
values as four settings, ms 0 to 3), +FCLASS, #CID, +PMH,
+PMHT and every kept parameter (kept_params).
- Changing a setting runs its AT command (
Config::apply(), throughat_command()), so the configurators take exactly what the AT interpreter takes and nothing else (a value is checked against the model first, so a configurator never runs a command that is not a setting's). - The defaults (
Config::defaults()) are whatATZsets: the settings' fields are set assregs_init(),kept_init()andreset()set them for the time it takes to read them, then put back. S8 and S24 are the country's; the country's own default is the USA's. -
AT!R(Config::init_lines()) gives the commands of the settings whose values differ from their defaults, in the model's order (+GCIfirst, since it sets S8 and S24;+MSas one command with its four values), an extended command followed by;when another comes after it on the line, in lines of 63 characters (LINE_CHARS) at most. It runs in any state. -
AT!M(Modem::model_listing()) gives the model itself, for a program's own settings page (the desktop apps in D-Modem/gui): a linemodel, its format (1) and the number of settings, then for each setting a linesetting, key, kind (pick,number,text), value, default, lowest, highest (a number's), form, group, label and help, each pick followed by itschoicelines (value, name, label); the fields apart by tabs. It runs in any state.
Running one. AT!C, AT!CF and AT!CT are ERROR unless the modem
is idle and on hook (conf_idle(): no call, the line not started, on
hook, no voice, fax or loop test, no held dial). The rest of the line is
not read. While a configurator runs (conf_), dte_input() gives it the
DTE's characters one at a time (conf_feed()) instead of the AT
interpreter, and poll() its timers; leaving it reports OK, and what
the DTE typed after that is commands again. A result (a RING), hangup()
(DTR off) and the destructor end it at once (conf_close()), without
OK; a leased line's S0 answer waits until it ends.
Keys. Config::input() turns the characters into keys: ANSI's and
VT100's escape sequences (arrows, Home, End, PgUp, PgDn, Ins, Del, F1 to
F10, Shift-Tab), ESC and a character as Alt and it, a lone ESC after 120
ms, CR LF as one Enter, the cursor position report (ESC [r;cR).
AT!C (and AT!CF) send ESC [6n. With no answer in 1.5 s,
AT!C starts the command line and AT!CF the full screen at 80 by 24 in
UTF-8. With one, the cursor goes to row and column 999 and is asked
again (the terminal's size, 80 by 24 to 160 by 66), then a CR and an é in
UTF-8 and again: column 2 is a terminal that takes UTF-8, else the
screen is drawn in CP437.
AT!CF draws each frame into cells (a code point and a CGA colour)
and sends the cells that changed since the last frame, with the cursor
moves and SGR colours they need (never the last cell, which would
scroll some terminals). Changes are kept until F2 (or the question on
leaving) runs them, +MS's as one +MS; a value the modem refuses is
listed in a red message box.
AT!CT parses each line against the words its mode takes (exec,
configuration), a word cut short where only one begins so, with IOS's
errors (% Invalid input detected at '^' marker., % Ambiguous
command, % Incomplete command.), ? and Tab. A setting changes at
once.
The call's statistics¶
ATI11 and ATI6 print the same statistics (call_stats()).
AT&V1 prints them as Rockwell's modems do (rockwell_stats()): its
labels dot-padded to 28 columns, PCM rates rounded as Rockwell's are
(53333), the termination as Rockwell's reasons (LOCAL REQUEST,
GSTN CLEARDOWN, CARRIER LOSS, NO CONNECT, ...), the line quality
as NMSE x 128, and N/A for Rockwell's pump states and EQM sum.
AT#UD prints them as Unimodem diagnostics (unimodem_diag()).
What is kept. The statistics (CallStats) are kept from one call's
start to the next call's start:
- the modulation and the rates at CONNECT and at the call's end;
- the line's power each way while the call was up (measured on the
samples in
process()); - why the call ended (
Cause). The first event that says so wins: the DTE's hangup, the link's release or error, the pump's failure before or after CONNECT, orline_dropped()(dmodemd's call when the SIP call is over); - what the pumps fill in (
dp_stats_,MDMPRM_STATS): retrains, renegotiations, SNR, round trip delay, echoes; each direction's symbol rate and carrier; V.34's coding each way (trellis code, non-linear encoder, precoding, shaping); the receiver's carrier and clock offsets; and on a PCM call the network's law and, towards the analogue modem, its digital loss and robbed bits; - the link's counts.
Each call's end logs a line of them.
AT#UD follows Microsoft's Unimodem Diagnostics Command Reference
Specification 1.0 (docs/specs/umud10.rtf):
- each key is a line
DIAG <2A4D3263 key=value>, the key in hexadecimal; - the modulation, error control, compression and the reason the call ended use V.58's codes;
ATZclears the log. AfterATZhas ended a call, only the keys 0, 1 and60=52remain.
AT+TMO (V.250 6.9) reads the same facts as V.59's managed objects
(v59.cpp). Each object has a name and a four-digit tag: the high
two digits name its high-level object (V34Diag 0C, CnxDiag 2C,
CallProgress 2E, ...), and 40 to 5F in the low two are the "common"
objects every modulation may have (txDataHistory 4C: 0C4C for V.34).
+TMO 0=?lists the tags (+TMO 0N=?the names),+TMO 4=?gives(0,1)(names and tags both);+TMO 0Cor+TMO V34 Allgives every object of V.34's, a linetag{value}orname{value}each;+TMO txDataHistoryalone is the last call's modulation's;+TMOalone repeats the last one;- an object that does not apply to the last call is
NULL; the values are V.59's (its tables' codes, levels with their units,TRUEandFALSE, octets in hexadecimal separated by;); - the rates each way are kept from CONNECT as they change (the last 16),
for
txDataHistoryandrxDataHistory; - the objects made of exchanged sequences the core does not record (V.8's CI, V.34's INFO and MP, V.90's CP, V.92's QC, V.8 bis's) are not there.
The call's state machine¶
The state machine is in modem.cpp. A call goes through: idle,
connecting, the data link coming up, online, online command mode (after
+++), and disconnecting. The state machine also handles dialling and
answering, rings (a SIP call's, or ring signals on a line, with their
cadence) and auto answer, caller ID, and the fax and voice modes.
Adaptive answer (+FAA=1). The data answer runs, in data mode or in
fax mode, until the call turns out to be one of these:
- A fax call, told by any of:
- the caller's CNG (
dsp.h'sCngDetect), or a burst of it cut short as our answer tone starts; - the V.8 CM of a fax call, which the V.8 pump reports with
DPSTAT_FAX; - T.38 from the far end;
- a caller that stays silent through the whole automode chain. The
chain's last stage, Bell 103, reports
DPSTAT_FAXafter 3 s.
The modem then reports FAX and goes on in class 1. The fax engine's
CED is cut short because the answer tone has already been sent
(FAX_answer_tone()).
- A data call, told by its pump's CONNECT: DATA, then its CONNECT.
Options::fax_detect has the program take a fax caller itself
(libdmodem's fax receiver) through fax_caller(), while the modem
stays in data mode.
Voice mode.
- The voice engine reports calling tones:
<DLE>cfor CNG and<DLE>efor V.25's 1300 Hz tone (VOICE_tones()'scngandct). +FCLASS=0or1while off hook keeps the call (voice_hold(),voice_held_: the line runs on, silent) for anATAorATDon it (start_call(true)).- With
+FAA=1, a calling tone does that by itself in the voice command state (voice_calling_tones()).
A dial string's ; holds the call in command state once it is
dialled (call progress's DPSTAT_DIALED).
ATDthen dials more on it, or starts the handshake. Call progress readsdialing_(the dial string plus what was added), while SIP andATDLuse the number as the firstATDgave it.ATAanswers it (V.250's call-back).
The timers¶
The escape guard time and the ring cadence are deadlines on the
program's clock (poll(), next_timer_ms()). All other timers count
samples. There are no signals and no global timer list.
How it connects to the rest¶
The host. A program that runs a modem (dmodemd, libdmodem, a test)
is its Host. The host starts and stops the line's audio, gets the
characters for the DTE, and tells the time. It hands the modem the
DTE's characters (dte_input()) and the line's samples while the modem
is started (process()), and it calls poll().
The data pumps (../dsp) see the core through their C interface
(interface.cpp: modem_dp.h, modem_param.h, dsplibs.h): a
struct modem that leads back to the Modem.
The data link layer (../link: V.42 detection, LAPM, MNP 2 to 4,
V.44, V.42bis, MNP 5, async) runs between the DTE's characters and the
pumps' bits. On calls with no error correction, two more layers sit
between the link and the pumps:
- V.13's simulated carrier (
v13.cpp, S48); - V.54's loop tests with V.52's self-test (
v54.cpp,AT&T; and V.250's+Tcommands on V.58's test objects,v58.cpp).
Both are described in leased.md.
Logging. log.cpp writes the debug output (to stderr or a hook) and
the binary dmodem.log.
Where it came from¶
The behaviour started as that of Smart Link's slmodemd core (modem.c,
modem_at.c, modem_param.c): the same commands, responses, states and
timing. dmodemd, libdmodem (dmodem.h), modem_test and the interop
tests' modems run on it, and so does Smart Link's original DSP library
(half_smartlink, interop.md).
The old core is no longer in the tree. core_equiv compares this one
with the transcripts recorded from it. None of slmodemd's code is left:
at.cpp, modem.cpp, interface.cpp and the country data were written
anew in 2026 from specifications of what they do, and give the same
transcripts.
Where this core differs from the old one¶
This core goes beyond the old one where the old one fell short:
- Version banner. The banner is the program's to print
(
Modem::product,version); dmodemd and modem_test print it at the start. AT&A1in fax mode printsModulation: NONE. The old core dereferenced a null data pump there.- Each command answers for itself.
AT*B(blacklisted numbers; none are kept) andAT\A<0-3>(the MNP block size) answer OK, and anAT+M<x>the core does not know is ERROR. The old core answered each of these with the result of the command before it on the line (ERROR at the line's start). The+Mcommands the core knows are+MS,+MR(kept), and V.18's+MV18...and+MBAUDOT. - After a fax or voice command's data. Fax and voice data stops at
the DTE's
<DLE><ETX>; what the DTE sends after it is its next commands, and they run as the command ends. The old core ran them only when more characters came. - A call that ends online drops the data still waiting to go out. The old core kept it, and its AT interpreter later took it for commands: a line of the data that began with AT ran.
- The escape keeps the data still waiting to go out, with the
+++(esc_held_), for the line onceATOis back online; the AT interpreter starts with none of it. The old core dropped it, so whether the+++reached the remote depended on whether the line had taken it within the guard time (EscapeKeepsData). AT&Vlists the active settings (the old core: ERROR).AT\V1puts the protocol after CONNECT's rate. WithATV0, a data call's CONNECT is its rate's number (the old core gave 1 for every one).- S-register numbers.
ATS<n>?andATS<n>=<v>readnandvin decimal, a leading 0 too (ATS09?is S9), as V.250 has them. The old core read them as C'sstrtoul()does with base 0, octal after a leading 0, soATS08=1andATS09?were ERROR andATS0=010set 8. - Statistics.
ATI11gives the last call's statistics (the old core: ERROR). So doATI6(USRobotics'; in the old core, its second stored profile, empty) andAT&V1(in Rockwell's form; the old core: ERROR).AT#UDgives them as Unimodem's diagnostics.&A'sModulation:is the one the call came up with (V91between two D-Modems); the old core named the pump thatAT+MSchose (V92). - Dial string modifiers.
;(back to command state),W(wait for a dial tone) and@(wait for quiet after ringing) do what V.250 says. The old core skipped;and@, and paused 2 s forW.S=<n>(a stored number; there are none) is ERROR. +FAA=1answers adaptively: FAX or DATA, then the call goes on as a fax or a data call. The old core took only+FAA=0.- V.42's detection phase. Calling on a modulation older than V.8 (V.32bis, V.22bis, ...), the core sends its ODPs and waits for the answerer's ADP before LAPM, so a Courier answering takes V.42. The old core went to LAPM straight away; those calls connect about 120 ms later now. After V.8, on a leased line and on a clear channel it still goes straight to LAPM.
Tests¶
ctest --test-dir build -R Core
core_equiv¶
core_equiv (CoreEquiv) runs each script in test/scripts/ through
core_script: two modems back to back, driven as a DTE would drive
them. The transcript (what each modem tells its DTE, with the time in
20 ms steps) must equal the script's <name>.expect.
Most .expect files are the old core's transcripts, recorded from it
when it left the tree (the two cores gave the same transcripts then).
Where this core differs on purpose (the list above), the .expect is
this core's transcript instead.
| Script | What it covers |
|---|---|
call_v32bis, call_v22, call_v34 |
calls with data both ways, the escape and ATO. call_v22 also has V.21 and numeric CONNECTs; call_v34 has V.34/V.90 (&A7) and &A's modulation (V.91) |
misc |
a call nobody answers, a key that aborts dialling, ATDL, S7 running out, hangups from either side and while connecting |
rings |
a SIP call that rings and goes away, auto answer at the second ring, ring signals with their cadence |
fax |
class 1 fax: V.21 frames, a TCF at 9600 |
fax_dte |
frames and commands around <DLE><ETX> |
voice, voice_dial |
the voice commands; +VLS=1, whose OK the voice engine gives, and a tone after it; ATD in voice mode (+VRA, +VRN) |
at |
all the AT commands |
setup |
other modems' setup strings |
data_drop |
a call that ends with data waiting |
dial |
the dial string's ;, W and @, the call-back, numeric &A lines and \V1 |
stats |
ATI11, AT&V1, ATI6 and AT#UD before, during and after calls: V.34 with echoes, V.32bis with MNP and without error correction, and one with no answer |
adaptive |
+FAA=1: fax and data callers into data and fax mode, FAX or DATA, +FCLASS after each call |
The scripts whose .expect is this core's own are at, setup,
fax_dte, data_drop, call_v22, call_v32bis, dial, call_v34,
stats, rings, adaptive and voice.
Script commands (see the header of test/script.cpp): A <text>,
run, until, send, ring, signal/cancel, hangup, state,
and two that shape the line:
tone A 350+440, or with on and off times in ms, puts a dial tone or ringing on A's line;echo A -12 0 -30 40sends A's signal back to it, a block (20 ms) plus 0 ms and plus 40 ms later, at -12 and -30 dB.
In a script \\ is a backslash and \xNN any other character (\b is
not a backspace).
CORE_EXPECT_WRITE=1 core_equiv writes the .expect files anew, to be
read in git diff; an empty .expect starts a new script off.
CoreScript.FaxDteUntilsCome checks that every until of fax_dte
comes.
core_test¶
core_test (Core, CoreCall, CoreFax2, CoreText,
CoreContactId) tests this core by itself:
| Area | Tests |
|---|---|
| The host and options | Options (the country, ATI7, S-registers out of range), Countries, FailingLine (a line that cannot start or go off hook), LineDelay, Log |
| Data calls | CoreCall.DataBothWaysAndHangup at V.32bis, V.34 and V.22, byte for byte; EscapeKeepsData; RetrainOnRequest (ATO1, ATO3); V34MaxRate; ModemOnHold |
| V.80 | V80, V80Retrain |
| Dialling and caller ID | PulseDialling, CallerIdBeforeRing, CallerIdAfterLineReversal, CallerIdOnTheCall, CallerIdFromLine |
| Answering | AdaptiveAnswer, VoiceFaxCall, VoiceDataCallingTone |
| Voice | VoiceLineGone, LocalPhone, VoiceHandset, VoiceEvents |
| Class 2 fax | CoreFax2.TwoPages, Fax2Abort, Fax2Poll |
| Text telephones and alarms | CoreText.LineEachWay (V.18's modes), CoreContactId (Contact ID messages kissed off, a bad checksum NAKed), CoreSms (fixed-line SMS: submissions, a delivery, a call with nothing to send) |