Skip to content

Refactor 260702 - #2

Merged
koreapyj merged 25 commits into
mainfrom
refactor-260702
Jul 3, 2026
Merged

koreapyj merged 25 commits into
mainfrom
refactor-260702

Conversation

@koreapyj

@koreapyj koreapyj commented Jul 3, 2026

Copy link
Copy Markdown
Owner

제곧내

koreapyj and others added 25 commits July 2, 2026 15:33
- Replace hardcoded Ubuntu x86_64 include paths with pkg-config discovery
  (glib-2.0, gio-2.0, gobject-2.0, mm-glib, portaudio-2.0, alsa); CC,
  PKG_CONFIG, ASTERISK_INCLUDE, MODULES_DIR, ASTETCDIR, DESTDIR all
  overridable for cross builds.
- Link with $(CC) -shared and a version script exporting only the module
  self-symbol; add -MMD dependency tracking, -Wall -Wextra, install target.
- Delete res_mmsd.c: it was compiled with chan_modemmanager's
  AST_MODULE_SELF_SYM and both modules exported colliding non-static
  globals (dbus/loop/ptmainloop) with AST_MODFLAG_GLOBAL_SYMBOLS set.
  MMS returns as a native subsystem inside chan_modemmanager in a later
  commit, without the mmsd-tng/session-dbus dependency.
- Fix the two compile blockers under modern GCC defaults
  (-Wincompatible-pointer-types as error): ao2_callback teardown helpers
  now use the correct ao2_callback_fn signature (also clearing device to
  avoid double-unref with the destructor), and mm_call_send_dtmf gets its
  GAsyncReadyCallback without the bogus G_CALLBACK cast.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Capture: one blocking-poll thread per active call (snd_pcm_wait with a
  100ms timeout + snd_pcm_readi), queueing one 20ms voice frame per
  period; stop_stream only sets a flag and joins, so no cross-thread
  snd_pcm calls are needed. Capture streams are explicitly started (and
  restarted after xrun recovery) since snd_pcm_wait never auto-starts
  them -- verified against snd-aloop.
- Playback: inline snd_pcm_writei from modemmanager_write with
  xrun/suspend recovery and a single retry.
- Devices are opened at channel creation, requesting exactly 8000 then
  16000 Hz; the channel format follows whichever rate opened, so the old
  bug where a card with any other default rate silently produced
  ast_format_none is structurally gone. Both opens and both param sets
  are error-checked (the PortAudio version discarded the first open's
  result). Prefer plug-capable device strings so the ALSA plug layer
  absorbs 48k-native USB codecs.
- input_device/output_device config values are now ALSA PCM names
  (documented breaking change; key names unchanged).
- CLI lists ALSA PCM devices via snd_device_name_hint.
- Drop portaudio from the Makefile, CI deps and MODULEINFO.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New audio_detect.c (pure libc, no Asterisk/GLib deps) extracts the
kernel's busnum-port[.port...] USB device address from both the modem's
physical device path (mm_modem_get_device) and each
/sys/class/sound/cardN/device parent chain; the deepest address wins so
hubs don't false-match, interface components (1-2:1.4) are ignored, and
a usbN root component is required. Exactly one matching card resolves to
plughw:N,0, cached per modem; zero or multiple matches log an actionable
diagnostic and audio is refused rather than guessed.

input_device/output_device now default to "auto" (autodetect); any
explicit value, including "default", is used verbatim.

Covered by fixture-based host tests (tests/test_audio_detect.c, run via
make check, wired into CI): plain device, behind-hub, PCI-only path,
missing usbN root, unknown device, and ambiguous two-card cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Layout: chan_modemmanager.c is module lifecycle only; mm_bus.c owns the
D-Bus connection, MMManager and GMainLoop thread; modem.c/sim.c the pvt
containers and MM object attachment; channel.c the channel tech; call.c
the MMCall lifecycle; sms.c messaging; audio_alsa.c/audio_detect.c the
audio backend; cli.c/config.c the rest. One .so, one self-symbol.

Threading model:
- Private GMainContext instead of the global default context: other
  modules in the same Asterisk process may use GLib too, and two threads
  cannot iterate one context. All proxies are bound to it (created under
  push_thread_default, incl. mm_bus_push/pop_context wraps around
  list_calls/create_call/list_messages/get_sim) so every signal provably
  dispatches on our loop thread.
- Signal handlers never block: they snapshot and push tasks onto
  per-modem serializers (ast_threadpool_serializer over a shared pool),
  preserving per-modem ordering without cross-modem head-of-line
  blocking. A separate serializer handles MMManager hotplug
  (object-added/removed), replacing load-time-only device resolution.
- pvt lock guards metadata only; owner access is snapshot+chan-ref
  (modem_grab_owner); ast_pbx_start never runs under a pvt lock.
- GObject refs: exactly-once via attach/detach helpers with stored
  signal-handler ids, disconnect-before-reconnect (reload can no longer
  double-connect); signal user_data sims are ref'd via closure notify.
- Ordered unload: unregister -> unwatch -> detach (joins capture
  threads) -> quit+join loop thread -> drop manager/bus -> containers.

Bug fixes landed with the code they live in:
- sms.c: alloca off-by-one stack overflow; find_sim result checked (was
  checking the wrong vars), ref released on all paths, modem/messaging
  NULL-guarded.
- channel.c: failed mm_call_start_sync now tears the channel down
  instead of returning it; sim->modem NULL checks; hangup no longer
  touches pvt state unlocked.
- call.c: MMCall is detached and deleted via the TERMINATED task (never
  leaked); all enum states handled; busy modems reject a second
  incoming call.
- modem.c: jbconf struct assignment (was memcpy of sizeof(pointer));
  own_numbers[0] guarded; reload prunes deconfigured pvts (active calls
  survive until hangup); load_config's unconditional unref of maybe
  unassigned objects is gone.
- pvt_container.h: OBJ_SEARCH_KEY lookups replace the fake stack-pvt
  hashing trick.
- Logging: ast_debug for tracing, ast_verb reserved for real events;
  g_print-to-stdout removed.

Tree builds with zero warnings under -Wall -Wextra.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Quectel-class modems need e.g. AT+QPCMV=1,2 after every boot before
their USB audio function carries call audio. MM's Command() D-Bus API is
compiled out of default ModemManager builds on every target distro, so
commands go directly to an AT port instead — scoped as the bring-up step
of the audio side-channel this driver already owns (like the sysfs ALSA
autodetect), not a general provisioning interface.

Config ([modemN]): init_commands=AT+QPCMV=1,2;... and optional
init_port=/dev/ttyUSBx.

Collision handling with ModemManager's port ownership, layered:
runs only once the MM modem object exists (probing settled) and reaches
ENABLED, once per appearance (flag reset on device re-attach); candidates
are MM-reported AT ports minus MM's primary port; ports MM holds with
TIOCEXCL fail open() and self-identify; every candidate must answer a
bare "AT" liveness probe before real commands are sent, so a port with
a competing reader is abandoned rather than fought over; we take
TIOCEXCL only for the short session. Failures log once — no retry storms.

The tty session primitives live in at_tty.c (pure libc) with pty-based
unit tests in tests/test_at_tty.c (OK/ERROR/+CME, URC interleaving,
split-line and timeout cases), wired into make check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
src/mms/vendor/ carries wsputil.c/.h and mmsutil.c/.h from mmsd-tng
(gitlab.com/kop316/mmsd @ 341117141f8d, GPL-2 like this project) with
upstream copyright headers intact and a provenance block in each file
listing every local change: wsputil.c is byte-for-byte verbatim; the
headers gained include guards + their own includes; mmsutil.c's daemon
log dependency is replaced by vendor_shim.h (DBG -> g_log). One upstream
-Wenum-conversion warning is suppressed for vendor objects only.

src/mms/mms_codec.{h,c} is the boundary the driver consumes:
- mms_codec_wap_push_extract(): defensive WSP Push parse (with and
  without a leading GSM UDH; port-addressing IEI 0x04/0x05), accepting
  only application/vnd.wap.mms-message (binary 0xBE or literal text) --
  SI/SL/OTA pushes and garbage are cleanly rejected. Zero-copy: the
  returned body points into the caller's buffer.
- mms_codec_decode_notification()/decode_retrieve(): wrappers over the
  vendored mms_message_decode() enforcing the expected message type.
  Documented ownership quirk: attachment offset/length index into the
  ORIGINAL pdu buffer, which must outlive the decoded struct.
- mms_codec_message_free(): NULL-safe free.

tests/test_mms_codec.c (make check, 3rd host binary): hand-crafted
byte-commented fixtures (push with/without UDH, wrong-port UDH, SI push
rejected, garbage rejected, minimal retrieve-conf with one text/plain
part) plus two fixtures reused from upstream unit/test-mmsutil.c with
provenance notes. Also stop tracking built test binaries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
contrib/openwrt/asterisk-chan-modemmanager: plain-make package modeled
on asterisk-chan-lantiq; DEPENDS asterisk +glib2 +modemmanager
+alsa-lib +libcurl (dbus arrives via modemmanager); compiles the repo
Makefile with TARGET_CC and the staging pkg-config wrapper against
STAGING_DIR asterisk headers; installs the module to
/usr/lib/asterisk/modules and the sample as /etc/asterisk/
modemmanager.conf (conffile). Source URL/commit are overridable for
local src-link development; header documents the feeds wiring.

debian/: debhelper-compat 13, dh --with asterisk (asterisk-dev's
dh_asterisk resolves the asterisk-abi-<buildsum> dependency via
${asterisk:Depends} -- verified in the built .deb); module path
computed from dpkg-architecture DEB_HOST_MULTIARCH matching Debian's
astmoddir (/usr/lib/<multiarch>/asterisk/modules, verified from
asterisk-config); make check wired into dh_auto_test; copyright
documents the GPL-2 vendored mmsd-tng codec. dpkg-buildpackage -us -uc
-b produces a lintian-clean package.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the mmsd-tng dependency (which requires systemd, unusable on
OpenWrt) with an in-driver pipeline on top of the previously vendored
codec:

- Intake (sms.c -> mms_on_wap_push_sms): binary SMS payloads are parsed
  as WSP push; non-MMS pushes are ignored, notifications are decoded,
  deduplicated by (sim, transaction id) and queued. SIMs without an
  mmsc configured log one notice and leave the SMS on the modem.
- Fetch: dedicated MMS worker thread (condvar-paced, never a modem
  serializer or the GMainLoop thread) drives libcurl transfers with
  per-SIM proxy/interface/User-Agent/timeout settings; the size cap is
  enforced inside the write callback because carriers lie about
  Content-Length; bare-path content locations resolve against the mmsc
  base; bounded retries with 30s/2m/10m backoff; HTTP 200 bodies are
  decode-validated (error PDUs wrapped in 200 fail after one retry);
  X-Mms-Expiry honored.
- Delivery: text/plain parts concatenated (SMIL skipped) into one
  message on the mms_context -> message_context -> context chain; other
  parts spooled under <mms_spool>/<txn>/ with MMS_ATTACHMENT_COUNT/
  FILE_n/TYPE_n, MMS_SUBJECT and MMS_TRANSACTION_ID variables;
  photo-only MMS delivers a placeholder body instead of vanishing.
- Ack: best-effort hand-encoded M-NotifyResp.ind (WAP-209) POSTed to
  the MMSC so the carrier stops re-sending; failures never affect
  delivery.
- Durability: the notification SMS is deleted from the modem (via the
  owning modem's serializer) only on terminal states, so a restart
  re-discovers pending notifications; state itself is memory-only.

New per-SIM config: mmsc, mms_proxy, mms_interface, mms_context,
mms_spool, mms_max_size, mms_fetch_timeout, mms_max_retries, mms_ack,
mms_user_agent (documented in the sample config).

tests/test_mms_fetch.c (4th make check binary) exercises the curl layer
against a local one-shot HTTP stub: byte-exact roundtrip + decode,
mid-stream size-cap abort, non-200 handling, and the ack POST shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nWrt SDK CI jobs

CI: debian:trixie dpkg-buildpackage + lintian job, and an OpenWrt 25.12.5
SDK cross-build job (release-pinned feeds, src-linked package, dl/build
caching) uploading the built artifacts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
11 findings confirmed by independent verification, all fixed:

- critical: module usecount was never taken, so 'module unload' during
  an active call dlclose'd the module under a live channel -> crash.
  modemmanager_new() now takes ast_module_ref(AST_MODULE_SELF), released
  in modemmanager_hangup().
- critical/major (locking contract): the pvt ao2 lock was held across
  blocking ALSA I/O (open_stream from modemmanager_new/task_call_added/
  start_stream; snd_pcm_writei in alsa_write_frame), contradicting the
  documented invariant. open_stream() now takes its own locks and runs
  device opens lock-free; callers open before locking; a new dedicated
  pcm_lock guards handle pointers and the playback write path; the ao2
  lock never wraps ALSA I/O anywhere.
- major: sim->modem was read bare all over channel.c/call.c/sms.c/
  mms_core.c, racing resolve_object()/sim_detach_all() reassignment.
  New sim_grab_modem() (lock+ref+unlock) used everywhere, with GObject
  snapshots (device/voice/messaging) taken under the modem lock;
  modemmanager_new() now receives the caller's grabbed modem instead of
  re-reading sim->modem.
- critical: resolve path dereferenced NULL DeviceIdentifier/
  SimIdentifier from modems still initializing; now guarded and skipped
  until the identifiers appear.
- major: PCM handles leaked when channel allocation failed after a
  successful open; failure paths now stop_stream() to close them.
- major: bare-path X-Mms-Content-Location was appended to the full MMSC
  URL; absolute paths now resolve against the MMSC origin
  (scheme://host[:port]) and only relative paths append to the base.
- major: ubuntu CI job lacked libcurl4-openssl-dev.
- major: debhelper build artifacts (including a prebuilt ELF and stale
  DEBIAN/control) were committed; untracked and gitignored.
- minor: test_mms_fetch make rule missed vendor header prerequisites.

Tree still builds warning-free; all 68 test assertions pass; the module
still exports only its self-symbol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
It slipped into the step-8 commit before its .gitignore entry existed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
OpenWrt 25.12 packages with APK (USE_APK default y), not opkg. Set an
explicit PKG_VERSION so the package packs as APK-conventional 1.0.0-r1
instead of a bare '1' (without PKG_VERSION the version degenerates to
PKG_RELEASE alone), and document the .apk output in the README. The
conffiles stanza needs no change: include/package-pack.mk translates it
to APK's conffiles mechanism. CI artifact globs were already
extension-agnostic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verified on OpenWrt 25.12.5 x86_64 with an RM500Q-GL:
- the asterisk user needs the audio group (/dev/snd for call audio) and
  dialout (init AT commands); postinst now says so, like chan_dongle's
  dialout reminder.
- ModemManager claims BOTH AT ports on a QMI-controlled RM500Q, so
  init_commands needs one port freed via an ID_MM_PORT_IGNORE rule (MM
  parses /lib/udev/rules.d itself on OpenWrt); README carries a working
  example rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…overy)

MMS durability depends on the notification SMS staying on the modem
until the message reaches a terminal state — but the 'added' signal only
fires for NEW messages, so a notification that arrived while the module
was not watching (restart, reload, modem replug) was never processed.
Verified live on LGU+: a WAP-push landed during a reload window and sat
stored, invisible.

Every modem (re)appearance now pushes a rescan task onto the modem's
serializer: stored RECEIVED messages with binary payloads are fed
through the MMS intake (the dedupe cache absorbs repeats); stored text
SMS are skipped since they were delivered on arrival.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
libmm-glib reports textless SMS with an EMPTY text string, not NULL, so
both the live added-signal path and the stored-message rescan treated
WAP-push notifications as text SMS and skipped the MMS intake. Verified
against a live LGU+ MMS notification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ModemManager sometimes fails to delete multipart notification SMS
(QMI 'Couldn't delete N parts' quirk, seen live on an RM500Q), so the
notification survives on the modem. Previously a delivered/given-up
transaction was unlinked from the dedupe container immediately, and the
next rescan or carrier re-send re-fetched and re-delivered the same MMS
(observed on LGU+). Terminal transactions now stay linked as tombstones
until notification expiry plus a 24h grace, swept by the worker loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All verified live on LGU+: the MMSC's native AAAA silently black-holes
while the A record through the NAT64 prefix (RFC 7050 discovery) serves
m-retrieve.conf; MMS metadata reads via MESSAGE_DATA(); OpenWrt dl-cache
and apk same-version pitfalls when iterating locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pvts were created with identifier = the config SECTION name, linked into
the ao2 hash under that key, and then store_config overwrote the
identifier field with the configured value — corrupting bucket placement
and making every reload miss the existing pvt. Each reload therefore
recreated pvts (prune + re-resolve churn) and orphaned the live GObject
signal closures: verified on hardware, MMModemMessaging 'added' events
stopped arriving after any 'module reload' while working fine on a fresh
load.

build_modem/build_sim now read the section's identifier value up front
(sections without one are skipped with a warning), look up existing pvts
by it, and link new pvts only after the config vars are stored, so the
hash key never mutates post-link. Reload now updates pvts in place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verified live: MM emits Call.StateChanged on the bus (dbus-monitor saw
three transitions during a driver call) but the standalone MMCall proxy
created via mm_modem_voice_create_call_sync never delivered the GObject
'state-changed' signal to the handler, while proxies obtained from the
object manager (e.g. messaging 'added') deliver fine. The property-cache
path (PropertiesChanged -> notify::state) is a second, independent
delivery mechanism, so call_attach now subscribes to both and a
last_call_state tracker dedupes whichever arrives; handlers funnel into
one handle_call_state() that also guards against stale MMCall instances.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Driver fixes proven on hardware (OpenWrt 25.12.5, RM500Q-GL, LGU+):

- Subscribe to Call StateChanged at the connection level and execute
  the subscription on the GLib loop thread via g_main_context_invoke.
  Proxies and subscriptions created off-loop bind a dead thread-default
  context and never fire: g_main_context_push_thread_default silently
  no-ops ('acquired_context' assertion) once g_main_loop_run owns the
  context. The broken push/pop context API is removed; call state is
  additionally driven from the property cache (notify::state) as belt
  and braces.

- Open ALSA playback with SND_PCM_NONBLOCK and drop frames on -EAGAIN.
  A blocking snd_pcm_writei wedged the Asterisk bridge thread when the
  modem's USB audio function stopped draining ('Exceptionally long
  voice queue length'), muting calls.

- OpenWrt package: depend on kmod-usb-audio (/dev/snd) and the
  asterisk bridge/codec sub-packages every real call needs
  (bridge-simple/native-rtp/softmix, codec-ulaw/alaw/resample) -
  without them calls connect but carry no audio ('No translator
  path', 'Could not create class basic'). Gate the package with
  @AUDIO_SUPPORT @USB_SUPPORT: alsa-lib is @AUDIO_SUPPORT-gated
  upstream, so audio-less targets (e.g. at91/sama7) can neither build
  nor run the driver.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rewrite the workflow around distro-matched packages:
- test: build with -Werror + unit tests + symbol/install checks on
  the current Ubuntu LTS runner.
- deb: dpkg-buildpackage + lintian in ubuntu:24.04 and ubuntu:26.04
  containers, each against that release's own asterisk-dev (asterisk
  20 and 22), replacing the dead debian:trixie job (Debian stable
  ships no asterisk since 2023) and the fragile /__w artifact glob.
- openwrt: one job per official 25.12 package architecture that can
  support the driver (32; arm_cortex-a7_vfpv4 is excluded because its
  only target has no audio support, hence no alsa-lib in its feed).
  Each job uses the release SDK of a representative target verified to
  ship kmod-usb-audio, with feeds.conf built from the release's
  feeds.buildinfo - the exact commits the stock packages were built
  from - and default package config, matching the phase-2 buildbot by
  construction. A guard fails at defconfig with a clear message if the
  package is ever deselected. SDK dl/build_dir/staging cached per
  arch; OPENWRT_RELEASE is the single bump point.
- release: v* tags attach every .deb/.apk to a GitHub Release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
README now covers what users need: prebuilt-package install for
OpenWrt 25.12 and Ubuntu 24.04/26.04 (with the Debian-has-no-asterisk
note), one-time permissions setup, annotated minimal configuration,
MMS incl. the NAT64 runbook, dialplan usage, a troubleshooting table
from failures seen in real testing, and compatibility (including the
audio-less-target exclusion). It also states the guarantee that no
non-default build options are required anywhere.

Build-from-source, architecture/threading notes, packaging gotchas,
CI layout, the arch-matrix regeneration procedure (with the full
runtime-dependency sweep) and the release procedure move to
DEV_README.md, linked from the README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three defects found running as a 24/7 gateway on real traffic:

- Messaging.Added fires when ModemManager creates the SMS object,
  which can be before all parts are read off the modem (state
  'receiving'); the flip to 'received' is only a property change with
  no second signal. The intake task silently skipped such messages and
  nothing ever looked again -- observed live with WAP-push MMS
  notifications arriving during an active voice call (busy QMI channel
  widens the window). Poll the message up to 15 times at 2s intervals
  via a new mm_bus_timeout_add() loop-thread timer before giving up.

- Delivered text SMS were never deleted from the modem, so storage
  filled up (29 messages accumulated in two days of testing); a full
  store eventually rejects new SMS. Delete after successful dialplan
  queueing; undeliverable messages stay stored.

- MMS fetch failures while a voice call is active no longer count
  against the retry budget, and call termination kicks the worker to
  make waiting transactions due immediately (mms_kick). With a
  correctly configured MMS bearer fetches work fine during calls, but
  when the bearer shares the call's IMS PDN the modem flow-controls
  the host data path until hangup (measured on RM500Q/LGU+): every
  attempt failed, and a long call could exhaust the budget and drop
  the message for good.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Giving up used to delete the notification from the modem, permanently
destroying the message: when a broken bearer ate all four attempts, the
MMS was unrecoverable even though it still sat on the MMSC (observed
live -- a message was lost exactly this way). Keep the SMS unless the
notification has expired; a restart's stored-message rescan then
retries with a fresh budget. Expired or permanently undecodable
notifications are still deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@koreapyj
koreapyj merged commit 8be6e7a into main Jul 3, 2026
71 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant