From 8b350409e24c3c8a4968753a120edc9fd2f8b243 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 13:31:30 +0000 Subject: [PATCH 1/8] libdmodemctl: the core the desktop apps share A plain C library with no I/O of its own, so the Windows, macOS and Linux apps can each be only views: the AT session (commands, their responses, the call's state from the result codes, hanging up through the escape), the settings model from AT!M, the call statistics from AT#UD, a small ANSI terminal screen for the configurator, and reading dmodemd's start-up lines. Tested against dmodemd's own output. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01B82eRVTPinTtLUajudobBC --- .gitignore | 11 + CMakeLists.txt | 27 ++ core/CMakeLists.txt | 26 ++ core/include/dmodemctl.h | 295 ++++++++++++++ core/src/daemon.c | 52 +++ core/src/diag.c | 435 +++++++++++++++++++++ core/src/model.c | 333 ++++++++++++++++ core/src/session.c | 761 +++++++++++++++++++++++++++++++++++++ core/src/util.c | 136 +++++++ core/src/util.h | 43 +++ core/src/vt.c | 756 ++++++++++++++++++++++++++++++++++++ core/test/CMakeLists.txt | 4 + core/test/diag-none.txt | 5 + core/test/dmodemctl_test.c | 480 +++++++++++++++++++++++ core/test/model.txt | 255 +++++++++++++ 15 files changed, 3619 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 core/CMakeLists.txt create mode 100644 core/include/dmodemctl.h create mode 100644 core/src/daemon.c create mode 100644 core/src/diag.c create mode 100644 core/src/model.c create mode 100644 core/src/session.c create mode 100644 core/src/util.c create mode 100644 core/src/util.h create mode 100644 core/src/vt.c create mode 100644 core/test/CMakeLists.txt create mode 100644 core/test/diag-none.txt create mode 100644 core/test/dmodemctl_test.c create mode 100644 core/test/model.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b821bea --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +build/ +build-*/ +.vs/ +bin/ +obj/ +*.user +.build/ +.swiftpm/ +*.xcodeproj/xcuserdata/ +DerivedData/ +.flatpak-builder/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..79fd4a9 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,27 @@ +# D-Modem's desktop apps: the shared C core (libdmodemctl), and the +# Linux app (GTK 4 and libadwaita) when its libraries are there. The +# Windows app (windows/) and the macOS app (macos/) build with their own +# tools and take the core from here. +cmake_minimum_required(VERSION 3.16) +project(dmodem-gui VERSION 1.0.0 LANGUAGES C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +option(DMODEM_GUI_LINUX "Build the GTK 4 app (linux/)" ON) + +add_subdirectory(core) +enable_testing() +add_subdirectory(core/test) + +if(DMODEM_GUI_LINUX AND UNIX AND NOT APPLE) + find_package(PkgConfig) + if(PkgConfig_FOUND) + pkg_check_modules(GTK IMPORTED_TARGET gtk4>=4.10 libadwaita-1>=1.4 vte-2.91-gtk4) + endif() + if(GTK_FOUND) + add_subdirectory(linux) + else() + message(STATUS "gtk4, libadwaita-1 or vte-2.91-gtk4 not found: the Linux app is not built") + endif() +endif() diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt new file mode 100644 index 0000000..02d1da5 --- /dev/null +++ b/core/CMakeLists.txt @@ -0,0 +1,26 @@ +# libdmodemctl: what the desktop apps share, in plain C with no +# dependencies. Static for the Linux app; a DLL for the Windows app's +# P/Invoke (-DDMODEMCTL_SHARED=ON). +option(DMODEMCTL_SHARED "Build libdmodemctl as a shared library" OFF) + +set(DMODEMCTL_SOURCES + src/session.c + src/model.c + src/diag.c + src/vt.c + src/daemon.c + src/util.c) + +if(DMODEMCTL_SHARED) + add_library(dmodemctl SHARED ${DMODEMCTL_SOURCES}) + target_compile_definitions(dmodemctl PRIVATE DMC_BUILD_DLL) + set_target_properties(dmodemctl PROPERTIES C_VISIBILITY_PRESET hidden) +else() + add_library(dmodemctl STATIC ${DMODEMCTL_SOURCES}) +endif() +target_include_directories(dmodemctl PUBLIC include) +if(MSVC) + target_compile_options(dmodemctl PRIVATE /W4 /wd4996) +else() + target_compile_options(dmodemctl PRIVATE -Wall -Wextra -Wshadow) +endif() diff --git a/core/include/dmodemctl.h b/core/include/dmodemctl.h new file mode 100644 index 0000000..1952b52 --- /dev/null +++ b/core/include/dmodemctl.h @@ -0,0 +1,295 @@ +/* + * SPDX-License-Identifier: BSD-3-Clause + * Copyright (c) 2026, D-Modem + * + * dmodemctl.h - the part of D-Modem's desktop apps that is not drawing. + * + * The Windows, macOS and Linux apps are thin native shells over this + * small C library, so that each of them is only views. It does no I/O + * of its own and starts no threads: each app opens the modem's DTE (a + * pty, a TCP socket) in its own way, hands the bytes it reads to + * dmc_feed(), writes what dmc_output() gives it, and calls dmc_poll() + * from a timer. That keeps it simple to call from C (GTK), C# + * (P/Invoke) and Swift. + * + * Its parts: + * - dmc_session: the AT conversation with dmodemd: commands and their + * responses, the call's state from the result codes (RING, CONNECT, + * NO CARRIER), the escape to command mode and hanging up, and the + * stream for the app's terminal view. + * - dmc_model: the modem's settings, from AT!M, for a native settings + * page. + * - dmc_diag: the last call's statistics, from AT#UD (Microsoft's + * Unimodem diagnostics), as labelled rows. + * - dmc_vt: a small ANSI terminal screen, for the apps whose toolkit + * has no terminal widget (Windows and macOS): the AT console and the + * full-screen configurator (AT!CF). + * - dmc_daemon_*: starting dmodemd and reading what it prints. + * + * Strings are UTF-8. A const char * the library returns stays valid + * until the next call on the same object. + */ +#ifndef DMODEMCTL_H +#define DMODEMCTL_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(_WIN32) && defined(DMC_BUILD_DLL) +#define DMC_API __declspec(dllexport) +#elif defined(_WIN32) && defined(DMC_USE_DLL) +#define DMC_API __declspec(dllimport) +#elif defined(__GNUC__) +#define DMC_API __attribute__((visibility("default"))) +#else +#define DMC_API +#endif + +/* the library's version: major * 10000 + minor * 100 + patch */ +#define DMC_VERSION 10000 +DMC_API int dmc_version(void); + +/* + * The session + */ + +typedef struct dmc_session dmc_session; + +/* the call's state, as the result codes tell it */ +enum dmc_state { + DMC_CLOSED = 0, /* no DTE open */ + DMC_IDLE, /* command mode, on hook */ + DMC_DIALING, /* ATD sent, no result yet */ + DMC_RINGING, /* RING heard, not answered */ + DMC_ANSWERING, /* ATA sent, no result yet */ + DMC_ONLINE, /* CONNECT: data mode */ + DMC_ONLINE_COMMAND, /* escaped (+++) during a call */ + DMC_HANGING_UP, /* going back on hook */ + DMC_CONFIGURATOR /* AT!C, AT!CF or AT!CT runs */ +}; + +/* what dmc_next_event() reports */ +enum dmc_event_type { + DMC_EV_NONE = 0, + /* a command of dmc_command()'s is done: tag, ok, text (its lines, + apart by '\n', without the echo and the final result), result + (the final result: OK, ERROR, CONNECT 33600..., or "timeout") */ + DMC_EV_RESPONSE, + /* the state changed: state */ + DMC_EV_STATE, + /* a result code nobody waited for (RING, NO CARRIER...): result */ + DMC_EV_RESULT, + /* caller ID after a RING: text "NMBR=5551234" and the like */ + DMC_EV_CALLER +}; + +typedef struct dmc_event { + int type; + int tag; + int ok; + int state; + const char *text; + const char *result; +} dmc_event; + +/* flags of dmc_command() */ +enum { + /* its echo and response kept out of the terminal stream (the app's + own queries: AT!M, AT#UD) */ + DMC_QUIET = 1 +}; + +DMC_API dmc_session *dmc_session_new(void); +DMC_API void dmc_session_free(dmc_session *s); + +/* the DTE opened (state IDLE) or closed (state CLOSED); either clears + what was queued */ +DMC_API void dmc_open(dmc_session *s, int64_t now_ms); +DMC_API void dmc_close(dmc_session *s); + +/* bytes the modem sent */ +DMC_API void dmc_feed(dmc_session *s, const char *data, int n); +/* bytes the user typed in the terminal view, sent as they are (the + session reads the command lines in them, to follow ATD, ATA, ATH and + AT!C typed by hand) */ +DMC_API void dmc_type(dmc_session *s, const char *data, int n); +/* the time now: runs the timers (a command's timeout, the escape's + guard times); returns the ms until it next needs to be called, -1 if + only when something happens */ +DMC_API int dmc_poll(dmc_session *s, int64_t now_ms); + +/* bytes to write to the modem: up to max into buf, how many */ +DMC_API int dmc_output(dmc_session *s, char *buf, int max); +/* bytes for the terminal view: up to max into buf, how many (what the + modem sent, less the DMC_QUIET commands') */ +DMC_API int dmc_terminal(dmc_session *s, char *buf, int max); +/* the next event into *ev: 1, or 0 for none */ +DMC_API int dmc_next_event(dmc_session *s, dmc_event *ev); + +/* an AT command (with or without the AT, no CR), queued behind any + other; timeout_ms 0 for the usual (5 s, ATD and ATA two minutes); + returns its tag (above 0) for the DMC_EV_RESPONSE, or -1 (closed, + or online) */ +DMC_API int dmc_command(dmc_session *s, const char *cmd, int flags, + int timeout_ms); +/* dial a number (ATD), answer (ATA): a tag as dmc_command()'s */ +DMC_API int dmc_dial(dmc_session *s, const char *number); +DMC_API int dmc_answer(dmc_session *s); +/* hang up whatever the state: online, the escape (+++, with its guard + times) and ATH; dialling, a key to stop it; else ATH */ +DMC_API void dmc_hangup(dmc_session *s); + +DMC_API int dmc_state(const dmc_session *s); +/* the state's name for people: "Idle", "Dialling", "Online" */ +DMC_API const char *dmc_state_name(int state); +/* the last CONNECT's text ("CONNECT 33600/ARQ/V34/LAPM/V42BIS"), its + rate (0 none), the caller ID's number and name of the last RING */ +DMC_API const char *dmc_connect_text(const dmc_session *s); +DMC_API int dmc_connect_rate(const dmc_session *s); +DMC_API const char *dmc_caller_number(const dmc_session *s); +DMC_API const char *dmc_caller_name(const dmc_session *s); + +/* + * The settings model (AT!M) + */ + +typedef struct dmc_model dmc_model; + +enum dmc_kind { DMC_PICK = 0, DMC_NUMBER, DMC_TEXT }; + +/* the model from AT!M's response text (dmc_event.text); NULL if it is + not one */ +DMC_API dmc_model *dmc_model_parse(const char *text); +DMC_API void dmc_model_free(dmc_model *m); + +DMC_API int dmc_model_count(const dmc_model *m); +/* setting i's fields */ +DMC_API const char *dmc_setting_key(const dmc_model *m, int i); +DMC_API int dmc_setting_kind(const dmc_model *m, int i); +DMC_API const char *dmc_setting_value(const dmc_model *m, int i); +DMC_API const char *dmc_setting_default(const dmc_model *m, int i); +DMC_API long dmc_setting_lo(const dmc_model *m, int i); +DMC_API long dmc_setting_hi(const dmc_model *m, int i); +DMC_API const char *dmc_setting_form(const dmc_model *m, int i); +DMC_API const char *dmc_setting_group(const dmc_model *m, int i); +DMC_API const char *dmc_setting_label(const dmc_model *m, int i); +DMC_API const char *dmc_setting_help(const dmc_model *m, int i); +DMC_API int dmc_setting_choices(const dmc_model *m, int i); +DMC_API const char *dmc_choice_value(const dmc_model *m, int i, int c); +DMC_API const char *dmc_choice_name(const dmc_model *m, int i, int c); +DMC_API const char *dmc_choice_label(const dmc_model *m, int i, int c); +/* the choice of setting i whose value is v, -1 none */ +DMC_API int dmc_setting_choice_of(const dmc_model *m, int i, + const char *v); +/* the setting whose key is key, -1 none */ +DMC_API int dmc_model_find(const dmc_model *m, const char *key); + +/* the groups, in the order the settings list them */ +DMC_API int dmc_model_groups(const dmc_model *m); +DMC_API const char *dmc_model_group(const dmc_model *m, int g); + +/* "" if setting i takes value, else why not */ +DMC_API const char *dmc_setting_check(const dmc_model *m, int i, + const char *value); +/* the command that sets setting i to value ("AT" and its form, the + value in it), "" if it does not take it */ +DMC_API const char *dmc_setting_command(const dmc_model *m, int i, + const char *value); +/* setting i's value now value (after its command's OK) */ +DMC_API void dmc_setting_set(dmc_model *m, int i, const char *value); + +/* + * The last call's statistics (AT#UD) + */ + +typedef struct dmc_diag dmc_diag; + +/* the statistics from AT#UD's response text; never NULL */ +DMC_API dmc_diag *dmc_diag_parse(const char *text); +DMC_API void dmc_diag_free(dmc_diag *d); +/* how many rows, each its key (the specification's, hexadecimal), its + label and its value for people ("33600 bit/s", "V.34") */ +DMC_API int dmc_diag_count(const dmc_diag *d); +DMC_API int dmc_diag_key(const dmc_diag *d, int i); +DMC_API const char *dmc_diag_label(const dmc_diag *d, int i); +DMC_API const char *dmc_diag_value(const dmc_diag *d, int i); +/* the raw value of key, NULL if it was not reported */ +DMC_API const char *dmc_diag_raw(const dmc_diag *d, int key); +/* there was a call to report (key 1 not "no previous call") */ +DMC_API int dmc_diag_has_call(const dmc_diag *d); + +/* + * The terminal screen + */ + +typedef struct dmc_vt dmc_vt; + +/* a cell's attributes */ +enum { + DMC_BOLD = 1, + DMC_UNDERLINE = 2, + DMC_REVERSE = 4 +}; + +/* the keys dmc_vt_key() encodes */ +enum dmc_key { + DMC_KEY_UP = 1, DMC_KEY_DOWN, DMC_KEY_RIGHT, DMC_KEY_LEFT, + DMC_KEY_HOME, DMC_KEY_END, DMC_KEY_PGUP, DMC_KEY_PGDN, + DMC_KEY_INSERT, DMC_KEY_DELETE, DMC_KEY_BACKTAB, + DMC_KEY_F1, DMC_KEY_F2, DMC_KEY_F3, DMC_KEY_F4, DMC_KEY_F5, + DMC_KEY_F6, DMC_KEY_F7, DMC_KEY_F8, DMC_KEY_F9, DMC_KEY_F10, + DMC_KEY_ENTER, DMC_KEY_BACKSPACE, DMC_KEY_TAB, DMC_KEY_ESCAPE +}; + +/* a screen of cols by rows, with scrollback lines kept above it */ +DMC_API dmc_vt *dmc_vt_new(int cols, int rows, int scrollback); +DMC_API void dmc_vt_free(dmc_vt *t); +DMC_API void dmc_vt_resize(dmc_vt *t, int cols, int rows); +DMC_API void dmc_vt_reset(dmc_vt *t); +DMC_API int dmc_vt_cols(const dmc_vt *t); +DMC_API int dmc_vt_rows(const dmc_vt *t); +/* bytes from the modem (dmc_terminal()'s) */ +DMC_API void dmc_vt_feed(dmc_vt *t, const char *data, int n); +/* the screen's answers to the modem's questions (where the cursor is: + ESC [6n), to write to the modem as if typed */ +DMC_API int dmc_vt_reply(dmc_vt *t, char *buf, int max); +/* how many lines of scrollback there are now */ +DMC_API int dmc_vt_history(const dmc_vt *t); +/* a cell: row 0 the screen's top, -1 the newest scrollback line and so + on; its character (a Unicode code point, 32 for none), colours (0 to + 15 the ANSI palette, -1 the default) and attributes */ +DMC_API uint32_t dmc_vt_char(const dmc_vt *t, int row, int col); +DMC_API int dmc_vt_fg(const dmc_vt *t, int row, int col); +DMC_API int dmc_vt_bg(const dmc_vt *t, int row, int col); +DMC_API int dmc_vt_attr(const dmc_vt *t, int row, int col); +/* a row as UTF-8 text, trailing blanks dropped */ +DMC_API const char *dmc_vt_text(dmc_vt *t, int row); +DMC_API int dmc_vt_cursor_row(const dmc_vt *t); +DMC_API int dmc_vt_cursor_col(const dmc_vt *t); +DMC_API int dmc_vt_cursor_visible(const dmc_vt *t); +/* the screen changed since the last call (and clears the mark) */ +DMC_API int dmc_vt_changed(dmc_vt *t); +/* the bytes a key sends, into buf: how many */ +DMC_API int dmc_vt_key(const dmc_vt *t, int key, char *buf, int max); +/* the RGB of palette colour c (0 to 15), as 0xRRGGBB */ +DMC_API uint32_t dmc_vt_palette(int c); + +/* + * dmodemd + */ + +/* a line dmodemd printed when it started: 1 and its DTE (a pty's + path, "TCP 127.0.0.1:2323", "pipe \\.\pipe\dmodem") into buf if it is + the line that says the modem is ready, else 0 */ +DMC_API int dmc_daemon_ready(const char *line, char *buf, int max); +/* a line that is an error of dmodemd's ("error: ..."): 1 */ +DMC_API int dmc_daemon_error(const char *line); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/core/src/daemon.c b/core/src/daemon.c new file mode 100644 index 0000000..7782699 --- /dev/null +++ b/core/src/daemon.c @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: BSD-3-Clause + * Copyright (c) 2026, D-Modem + * + * daemon.c - reading what dmodemd prints as it starts (dmodemctl.h) + * + * On Linux, macOS, BSD and Haiku it says + * Use `/dev/ttyDM0' as modem device, Ctrl+C for termination. + * (the pty's own path, /dev/pts/3, when it may not make the link), and + * on Windows + * Use TCP 127.0.0.1:2323 as the modem, Ctrl+C for termination. + */ +#include + +#include "dmodemctl.h" + +static int copy(const char *from, size_t n, char *buf, int max) +{ + if (!buf || max <= 0) + return 1; + if (n >= (size_t)max) + n = (size_t)max - 1; + memcpy(buf, from, n); + buf[n] = '\0'; + return 1; +} + +int dmc_daemon_ready(const char *line, char *buf, int max) +{ + if (!line) + return 0; + const char *p = strstr(line, "Use `"); + if (p) { + p += 5; + const char *e = strchr(p, '\''); + if (e && strstr(e, "as modem device")) + return copy(p, (size_t)(e - p), buf, max); + } + p = strstr(line, "Use "); + if (p) { + p += 4; + const char *e = strstr(p, " as the modem"); + if (e) + return copy(p, (size_t)(e - p), buf, max); + } + return 0; +} + +int dmc_daemon_error(const char *line) +{ + return line && !strncmp(line, "error:", 6); +} diff --git a/core/src/diag.c b/core/src/diag.c new file mode 100644 index 0000000..df4aba9 --- /dev/null +++ b/core/src/diag.c @@ -0,0 +1,435 @@ +/* + * SPDX-License-Identifier: BSD-3-Clause + * Copyright (c) 2026, D-Modem + * + * diag.c - the last call's statistics from AT#UD (dmodemctl.h) + * + * AT#UD follows Microsoft's Unimodem Diagnostics Command Reference + * Specification, version 1.0 (D-Modem's docs/specs/umud10.rtf): lines + * "DIAG <2A4D3263 key=value ...>", the keys and values in hexadecimal + * and the code tables V.58's. Each key becomes a row with its label + * and its value for people. + */ +#include +#include +#include + +#include "dmodemctl.h" +#include "util.h" + +/* how a key's value reads */ +enum fmt { + F_HEX, /* as it is */ + F_NUM, /* a count */ + F_BPS, /* bit/s */ + F_DBM, /* -dBm */ + F_DB, /* dB */ + F_MS, /* ms */ + F_HZ, /* Hz */ + F_BAUD, /* symbols a second */ + F_BYTES, /* bytes */ + F_TABLE, /* one of the code tables */ + F_STRING, /* quoted octets */ + F_VERSION /* digit.digit */ +}; + +struct code { + unsigned v; + const char *label; +}; + +static const struct code setup[] = { + { 0x0, "No previous call" }, + { 0x1, "No dial tone" }, + { 0x2, "Network busy (reorder)" }, + { 0x3, "Busy" }, + { 0x4, "No signal recognised" }, + { 0x5, "Voice" }, + { 0x6, "Text telephone" }, + { 0x7, "Data answer tone heard" }, + { 0x8, "Data calling tone heard" }, + { 0x9, "Fax answer tone heard" }, + { 0xA, "Fax calling tone heard" }, + { 0xB, "V.8bis" }, + { 0, NULL } +}; + +static const struct code media[] = { + { 0x0, "Data" }, + { 0x1, "Fax" }, + { 0x2, "Voice" }, + { 0x3, "VoiceView" }, + { 0x4, "ASVD (V.61)" }, + { 0x5, "ASVD (V.34Q)" }, + { 0x6, "DSVD (Multi-Tech)" }, + { 0x7, "DSVD 1.2" }, + { 0x8, "DSVD (V.70)" }, + { 0x9, "Video telephony (H.324)" }, + { 0xA, "Other V.80 call" }, + { 0, NULL } +}; + +static const struct code dte_mode[] = { + { 0x0, "Asynchronous" }, + { 0x1, "V.80 transparent synchronous" }, + { 0x2, "V.80 framed synchronous" }, + { 0, NULL } +}; + +/* V.58's gstnModulationSchemeActive, and the manufacturers' */ +static const struct code modulation[] = { + { 0x0, "V.17" }, { 0x1, "V.21" }, { 0x2, "V.22" }, + { 0x3, "V.22bis" }, { 0x4, "V.23" }, { 0x5, "V.23 (half duplex)" }, + { 0x6, "V.26bis" }, { 0x7, "V.26ter" }, { 0x8, "V.27ter" }, + { 0x9, "V.29" }, { 0xA, "V.32" }, { 0xB, "V.32bis" }, + { 0xC, "V.34" }, { 0xD, "V.34 half duplex" }, + { 0xE, "V.90 (PCM)" }, { 0xF, "V.91 (PCM both ways)" }, + { 0x80, "X2" }, { 0x81, "K56flex" }, { 0x82, "V.FC" }, + { 0x83, "V.32terbo" }, { 0x84, "Bell 212A" }, { 0x85, "Bell 103" }, + { 0, NULL } +}; + +static const struct code error_control[] = { + { 0x0, "None" }, + { 0x1, "V.42 LAPM" }, + { 0x2, "MNP" }, + { 0x80, "MNP 10" }, + { 0x81, "ECP" }, + { 0x82, "ETC" }, + { 0, NULL } +}; + +static const struct code compression[] = { + { 0x0, "None" }, + { 0x1, "V.42bis" }, + { 0x80, "MNP 5" }, + { 0x81, "V.44" }, + { 0x82, "zstd" }, + { 0, NULL } +}; + +static const struct code flow[] = { + { 0x0, "Off" }, + { 0x1, "XON/XOFF" }, + { 0x2, "RTS/CTS" }, + { 0, NULL } +}; + +/* V.58's callCleared */ +static const struct code cleared[] = { + { 0x0, "Cause unknown" }, + { 0x1, "No previous call" }, + { 0x2, "Call in progress" }, + { 0x3, "Call waiting signal" }, + { 0x4, "Delayed" }, + { 0x19, "Inactivity timer" }, + { 0x1F, "DTR off stops the dial" }, + { 0x20, "DTR turned off" }, + { 0x28, "No number" }, + { 0x29, "Blacklisted number" }, + { 0x2A, "Too many attempts" }, + { 0x2B, "Extension off hook" }, + { 0x2C, "No answer in time (S7)" }, + { 0x2D, "Incoming call while dialling" }, + { 0x2E, "Loop current lost" }, + { 0x2F, "No dial tone" }, + { 0x30, "Voice" }, + { 0x31, "Reorder tone" }, + { 0x32, "SIT tone" }, + { 0x33, "Busy" }, + { 0x34, "Long space" }, + { 0x3C, "Carrier lost" }, + { 0x3D, "Training failed" }, + { 0x3E, "No modulation in common" }, + { 0x3F, "Retrain failed" }, + { 0x40, "Too many retrains" }, + { 0x41, "The other modem hung up" }, + { 0x42, "Fax detected" }, + { 0x50, "Stopped by a key" }, + { 0x51, "Hung up (ATH)" }, + { 0x52, "Reset (ATZ)" }, + { 0x5A, "Frame reject" }, + { 0x5B, "No error control" }, + { 0x5C, "Protocol violation" }, + { 0x5D, "Too many retransmissions" }, + { 0x5E, "Negotiation failed" }, + { 0x5F, "Disconnect frame received" }, + { 0x60, "SABME frame received" }, + { 0x64, "Lost synchronisation" }, + { 0, NULL } +}; + +struct keyinfo { + int key; + const char *label; + int fmt; + const struct code *table; +}; + +static const struct keyinfo keys[] = { + { 0x00, "Report version", F_VERSION, NULL }, + { 0x01, "Call setup", F_TABLE, setup }, + { 0x02, "Mode", F_TABLE, media }, + { 0x03, "DTE interface", F_TABLE, dte_mode }, + { 0x04, "V.8 CM", F_STRING, NULL }, + { 0x05, "V.8 JM", F_STRING, NULL }, + { 0x10, "Received level", F_DBM, NULL }, + { 0x11, "Transmit level", F_DBM, NULL }, + { 0x12, "Noise level", F_DBM, NULL }, + { 0x13, "Mean squared error", F_NUM, NULL }, + { 0x14, "Near echo loss", F_DB, NULL }, + { 0x15, "Far echo loss", F_DB, NULL }, + { 0x16, "Far echo delay", F_MS, NULL }, + { 0x17, "Round trip delay", F_MS, NULL }, + { 0x18, "V.34 INFO bits", F_HEX, NULL }, + { 0x20, "Transmit modulation", F_TABLE, modulation }, + { 0x21, "Receive modulation", F_TABLE, modulation }, + { 0x22, "Transmit symbol rate", F_BAUD, NULL }, + { 0x23, "Receive symbol rate", F_BAUD, NULL }, + { 0x24, "Transmit carrier", F_HZ, NULL }, + { 0x25, "Receive carrier", F_HZ, NULL }, + { 0x26, "First transmit rate", F_BPS, NULL }, + { 0x27, "First receive rate", F_BPS, NULL }, + { 0x30, "Carrier losses", F_NUM, NULL }, + { 0x31, "Rate renegotiations", F_NUM, NULL }, + { 0x32, "Retrains asked for", F_NUM, NULL }, + { 0x33, "Retrains granted", F_NUM, NULL }, + { 0x34, "Last transmit rate", F_BPS, NULL }, + { 0x35, "Last receive rate", F_BPS, NULL }, + { 0x40, "Error control", F_TABLE, error_control }, + { 0x41, "Frame size", F_BYTES, NULL }, + { 0x42, "Link timeouts", F_NUM, NULL }, + { 0x43, "NAKs received", F_NUM, NULL }, + { 0x44, "Compression", F_TABLE, compression }, + { 0x45, "Dictionary size", F_BYTES, NULL }, + { 0x50, "Transmit flow control", F_TABLE, flow }, + { 0x51, "Receive flow control", F_TABLE, flow }, + { 0x52, "Characters sent", F_NUM, NULL }, + { 0x53, "Characters received", F_NUM, NULL }, + { 0x54, "Characters lost sending", F_NUM, NULL }, + { 0x55, "Characters lost receiving", F_NUM, NULL }, + { 0x56, "Frames sent", F_NUM, NULL }, + { 0x57, "Frames received", F_NUM, NULL }, + { 0x58, "Frames sent again", F_NUM, NULL }, + { 0x59, "Frames received in error", F_NUM, NULL }, + { 0x60, "Why the call ended", F_TABLE, cleared }, + { 0x61, "Call waiting events", F_NUM, NULL }, + { -1, NULL, 0, NULL } +}; + +struct row { + int key; + char *raw, *label, *value; +}; + +struct dmc_diag { + struct row *rows; + int n; +}; + +static const struct keyinfo *info(int key) +{ + for (const struct keyinfo *k = keys; k->label; k++) + if (k->key == key) + return k; + return NULL; +} + +static const char *code_label(const struct code *t, unsigned v) +{ + for (; t->label; t++) + if (t->v == v) + return t->label; + return NULL; +} + +/* a number with thousands apart by commas */ +static void grouped(char *out, size_t n, unsigned long v) +{ + char digits[32]; + snprintf(digits, sizeof(digits), "%lu", v); + size_t len = strlen(digits), o = 0; + for (size_t i = 0; i < len && o + 2 < n; i++) { + if (i && (len - i) % 3 == 0) + out[o++] = ','; + out[o++] = digits[i]; + } + out[o] = '\0'; +} + +static char *shown(const struct keyinfo *k, const char *raw) +{ + char out[160]; + unsigned long v = strtoul(raw, NULL, 16); + char num[40]; + grouped(num, sizeof(num), v); + switch (k ? k->fmt : F_HEX) { + case F_NUM: + snprintf(out, sizeof(out), "%s", num); + break; + case F_BPS: + snprintf(out, sizeof(out), "%s bit/s", num); + break; + case F_DBM: + snprintf(out, sizeof(out), "%s%lu dBm", v ? "-" : "", v); + break; + case F_DB: + snprintf(out, sizeof(out), "%lu dB", v); + break; + case F_MS: + snprintf(out, sizeof(out), "%lu ms", v); + break; + case F_HZ: + snprintf(out, sizeof(out), "%lu Hz", v); + break; + case F_BAUD: + snprintf(out, sizeof(out), "%lu baud", v); + break; + case F_BYTES: + snprintf(out, sizeof(out), "%s bytes", num); + break; + case F_TABLE: { + const char *l = code_label(k->table, (unsigned)v); + if (l) + snprintf(out, sizeof(out), "%s", l); + else + snprintf(out, sizeof(out), "Code %lX", v); + break; + } + case F_VERSION: + if (strlen(raw) == 2) + snprintf(out, sizeof(out), "%c.%c", raw[0], raw[1]); + else + snprintf(out, sizeof(out), "%s", raw); + break; + case F_STRING: { + /* the octets, apart by spaces */ + size_t o = 0; + for (const char *p = raw; *p && o + 4 < sizeof(out); p++) { + if (*p == '"') + continue; + if (o && (o + 1) % 3 == 0) + out[o++] = ' '; + out[o++] = *p; + } + out[o] = '\0'; + break; + } + default: + snprintf(out, sizeof(out), "%s", raw); + break; + } + return dup_str(out); +} + +static void add(dmc_diag *d, int key, const char *raw, size_t n) +{ + /* a key again: its last value */ + for (int i = 0; i < d->n; i++) + if (d->rows[i].key == key) { + free(d->rows[i].raw); + free(d->rows[i].value); + d->rows[i].raw = dup_n(raw, n); + d->rows[i].value = shown(info(key), d->rows[i].raw); + return; + } + struct row *r = realloc(d->rows, sizeof(*r) * (size_t)(d->n + 1)); + if (!r) + abort(); + d->rows = r; + r = &d->rows[d->n++]; + const struct keyinfo *k = info(key); + char label[32]; + if (!k) + snprintf(label, sizeof(label), "Key %X", key); + r->key = key; + r->raw = dup_n(raw, n); + r->label = dup_str(k ? k->label : label); + r->value = shown(k, r->raw); +} + +dmc_diag *dmc_diag_parse(const char *text) +{ + dmc_diag *d = calloc(1, sizeof(*d)); + if (!d) + abort(); + const char *p = text ? text : ""; + while ((p = strstr(p, "DIAG <")) != NULL) { + p += 6; + const char *end = strchr(p, '>'); + if (!end) + break; + /* the tag (2A4D3263), then key=value pairs */ + while (p < end && *p != ' ') + p++; + while (p < end) { + while (p < end && *p == ' ') + p++; + const char *eq = p; + while (eq < end && *eq != '=') + eq++; + if (eq >= end) + break; + int key = (int)strtol(p, NULL, 16); + const char *v = eq + 1, *ve = v; + if (*v == '"') { + ve = strchr(v + 1, '"'); + ve = ve && ve < end ? ve + 1 : end; + } else { + while (ve < end && *ve != ' ') + ve++; + } + add(d, key, v, (size_t)(ve - v)); + p = ve; + } + p = end + 1; + } + return d; +} + +void dmc_diag_free(dmc_diag *d) +{ + if (!d) + return; + for (int i = 0; i < d->n; i++) { + free(d->rows[i].raw); + free(d->rows[i].label); + free(d->rows[i].value); + } + free(d->rows); + free(d); +} + +int dmc_diag_count(const dmc_diag *d) +{ + return d ? d->n : 0; +} + +int dmc_diag_key(const dmc_diag *d, int i) +{ + return d && i >= 0 && i < d->n ? d->rows[i].key : -1; +} + +const char *dmc_diag_label(const dmc_diag *d, int i) +{ + return d && i >= 0 && i < d->n ? d->rows[i].label : ""; +} + +const char *dmc_diag_value(const dmc_diag *d, int i) +{ + return d && i >= 0 && i < d->n ? d->rows[i].value : ""; +} + +const char *dmc_diag_raw(const dmc_diag *d, int key) +{ + for (int i = 0; d && i < d->n; i++) + if (d->rows[i].key == key) + return d->rows[i].raw; + return NULL; +} + +int dmc_diag_has_call(const dmc_diag *d) +{ + const char *v = dmc_diag_raw(d, 1); + return v && strtoul(v, NULL, 16) != 0; +} diff --git a/core/src/model.c b/core/src/model.c new file mode 100644 index 0000000..e8b2b74 --- /dev/null +++ b/core/src/model.c @@ -0,0 +1,333 @@ +/* + * SPDX-License-Identifier: BSD-3-Clause + * Copyright (c) 2026, D-Modem + * + * model.c - the modem's settings from AT!M (dmodemctl.h) + * + * AT!M's lines, their fields apart by tabs: + * + * model 1 + * setting
+ *