Cross-platform GSM/UMTS modem hardware abstraction for Flutter: ttyUSB
AT-command modems (Linux/Windows/macOS) and native telephony (Android).
Split out of flutter_gsmsip (see flows/sdd-flutter_gsm)
so raw GSM hardware access doesn't require pulling in SIP/SMPP bridging
logic β flutter_gsmsip now depends on this package for its GSM leg,
alongside flutter_nmsip for its SIP leg.
| Platform | Status |
|---|---|
| Android | Real call control via flutter_dialer+flutter_tele (dial/answer/hangup/hold/mute/speaker); AT-command/firmware/diagnostic methods correctly unsupported (no Android equivalent) |
| Linux | Real ttyUSB/AT-command driver β LinuxFlutterGsm delegates via dart:ffi to libsimbox (built by sdd-asterisk-chan-simbox), which drives chan_svistok's real, unmodified AT-command logic without a running Asterisk instance. See flows/sdd-flutter_gsm-ffi for the binding design and Native Library Loading below for how libsimbox is located at runtime. setNetworkMode's auto/wcdmaOnly modes and setDiagMode(enabled: false)/changeImei are flagged gaps β see Known Issues |
| Windows / macOS | Interface registered, stub β dart:ffi binding is Linux-only this iteration (see sdd-flutter_gsm-ffi's Non-Goals); libsimbox's own Makefile already has a Darwin build branch, so a similar binding is a smaller follow-up than starting from scratch |
| OpenWRT | Native-core cross-compile target (embedded Linux, headless β not a Flutter UI platform) β tracked in sdd-asterisk-chan-simbox |
Linux/Windows/macOS telephony is modem-based (direct AT-command
communication with USB GSM/UMTS dongles over /dev/ttyUSBx or
platform-equivalent serial ports, chan_svistok-derived logic re-hosted
without Asterisk), architecturally distinct from Android's native
telecom path β see flows/sdd-flutter_gsmsip-interface/ and
flows/sdd-flutter_gsm/ for the full design rationale.
- Modem Discovery & State β cross-platform
ModemDevice/ModemRepositoryabstraction, push-styleModemEventstream - Calls & SMS β dial/answer/hangup/hold/mute/speaker, SMS send (Android via
flutter_smsussd) - AT-Command / Diagnostics β raw AT command passthrough, diag mode (real on Linux via
libsimbox; Windows/macOS pending) - Firmware / Recovery β Huawei DIAG-mode firmware flashing and bricked-modem recovery (real on Linux via
libsimbox; Windows/macOS pending) - Error Handling β typed
ModemExceptionhierarchy, distinguishes "no device" from "driver not available yet"
SIPβGSM call routing, SMPP SMS gateway, and voice-bridging logic live in
flutter_gsmsip, which depends on this package.
Add this to your Flutter project's pubspec.yaml:
dependencies:
flutter_gsm: ^0.1.0Or use a local path for development:
dependencies:
flutter_gsm:
path: ../flutter_gsmThen run:
flutter pub getEnsure your AndroidManifest.xml includes the required permissions:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CALL_PHONE"/>
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>import 'package:flutter_gsm/flutter_gsm.dart';
final ModemRepository modems = ModemRepositoryImpl();
final devices = await modems.listModems();
for (final device in devices) {
print('${device.id}: ${device.portPath ?? device.displayName} β ${device.state}');
}
modems.modemEvents.listen((event) {
switch (event) {
case ModemAttached(:final device):
print('Modem attached: ${device.id}');
case ModemDetached(:final modemId):
print('Modem detached: $modemId');
case ModemCallStateChanged(:final call):
print('Call ${call.id}: ${call.state}');
case ModemSmsReceived(:final from, :final text):
print('SMS from $from: $text');
default:
break;
}
});final call = await modems.dial(device.id, '+1234567890');
await modems.answerCall(call.id);
// ...
await modems.hangupCall(call.id);await modems.sendSms(device.id, '+1234567890', 'Hello from flutter_gsm');
final response = await modems.sendUssd(device.id, '*100#');try {
final devices = await modems.listModems();
if (devices.isEmpty) {
print('No modems attached.');
}
} on ModemDriverNotAvailableException {
print('Modem driver not available on this platform yet.');
}| Method | Description |
|---|---|
listModems() / getModem(id) |
Discovery & lookup |
modemEvents |
Stream<ModemEvent> β attach/detach, state/signal/registration changes, call state, SMS/USSD, errors |
sendAtCommand(modemId, command) |
Raw AT command passthrough (desktop) |
setPower(modemId, on:) / restartModem(modemId, mode:) |
Power / lifecycle |
changeImei(modemId, imei) / setNetworkMode(modemId, mode) / setGroup(modemId, groupId) |
Identity / network |
dial(modemId, number) / hangupCall(callId) / answerCall(callId) |
Calling |
sendSms(modemId, number, text) / sendUssd(modemId, code) |
SMS / USSD |
setDiagMode(modemId, enabled) |
Diagnostics (desktop, firmware-adjacent) |
See platform support above for which methods are real vs. UnsupportedError/UnimplementedError per platform.
ModemDevice, ModemCall, ModemEvent (sealed: ModemAttached, ModemDetached, ModemStateChanged, ModemSignalChanged, ModemRegistrationChanged, ModemCallStateChanged, ModemSmsReceived, ModemUssdReceived, ModemErrorOccurred), CarrierProfile + CarrierProfileRegistry, ModemGroupConfig, AtCommandResult, ModemState, NetworkMode, RestartMode, RegistrationState.
Typed exceptions (implements Exception), not Either/Failure:
ModemDriverNotAvailableExceptionβ platform implementation exists but has no real driver yet (distinct from an empty device list)ModemNotFoundExceptionβ no modem with the given idModemNotDefaultDialerException(Android) β app isn't the default dialer yet, needed before call control works
flutter_gsm/
βββ lib/
β βββ flutter_gsm.dart # Main export
β βββ flutter_gsm_platform_interface.dart
β βββ flutter_gsm_method_channel.dart # Android (method-channel fallback)
β βββ src/
β βββ domain/
β β βββ entities/ # ModemDevice, ModemCall, CarrierProfile, ModemGroupConfig
β β βββ models/ # ModemEvent, ModemState, AtCommandResult, ...
β β βββ repositories/ # ModemRepository (interface)
β β βββ exceptions/ # ModemException hierarchy
β βββ data/repositories/ # ModemRepositoryImpl
β βββ android/ # AndroidFlutterGsm (flutter_dialer/flutter_tele backed)
β βββ ffi/ # ffigen SimboxBindings, DynamicLibrary loader
β βββ linux/ # LinuxFlutterGsm + SimboxModemRepository (libsimbox via dart:ffi)
βββ android/ # Native Kotlin plugin code
β βββ src/main/kotlin/org/telon/flutter_gsm/
βββ pubspec.yaml
SIPβGSM call routing, SMPP SMS gateway, and voice-bridging orchestration
live in flutter_gsmsip, which depends on this
package for its GSM leg and on
flutter_nmsip for its SIP leg.
LinuxFlutterGsm binds to libsimbox (built by libsCpp/asterisk_chan_simbox)
via dart:ffi. This is a pragmatic dev-mode loading strategy, not a
packaged/redistributable one yet β a real linux/CMakeLists.txt
build-and-bundle step is a flagged follow-up (flutter_gsm's linux:
pubspec entry is currently pure-Dart, dartPluginClass-registered, with
no native CMake scaffold). Resolution order:
FLUTTER_GSM_SIMBOX_LIBenvironment variable, if set β tried directly with no fallback (an explicit override that fails to load surfaces loudly, not silently).libsimbox.so/libsimbox.dylib(system-installed names).../../libsCpp/asterisk_chan_simbox/libsimbox.so/.dylib(monorepo-relative dev path β works when running from within this workspace, not reliable across arbitrary build outputs).
If none load, SimboxModemRepository/LinuxFlutterGsm throw
ModemDriverNotAvailableException at first use (not at plugin
registration), matching this package's existing "driver not available"
convention elsewhere β so a dev machine without libsimbox built still
starts up normally, it just can't list/control modems.
The example/ directory contains a working app demonstrating modem
discovery, event streaming, and call control against ModemRepository.
To run the example:
cd example
flutter pub get
flutter run- Flutter: >=3.3.0
- Dart: ^3.10.8
- Android: API level 21+ (Android 5.0)
- Kotlin: 1.7.0+
See the GitHub Issues for known issues and roadmap.
- Windows/macOS driver pending: only Linux is bound to
libsimboxviadart:ffiso far β seesdd-flutter_gsm-ffi. Windows/macOS stay stubbed. - Linux
setNetworkMode: onlyNetworkMode.gsmOnly(AT^SYSCFG=13,...) is confirmed from chan_svistok's own reference source βauto/wcdmaOnlythrowUnsupportedErrorrather than guessing anAT^SYSCFGcode that could lock a real modem to an unreachable network. Confirm the real codes against attached hardware/vendor AT reference to wire them up. - Linux
setDiagMode(enabled: false): no "exit DIAG mode" function exists inlibsimboxβ throwsUnsupportedError, matching this package's existing "no equivalent exists" convention. - Linux
changeImei: genuinely blocked upstream, not an adapter gap β chan_svistok's real IMEI-change path (ttyprog_changeimei) is called in three places across its source but defined nowhere in the checked-in tree. Surfaces as aModemExceptionexplaining the gap rather than a fabricated success. - Linux native library loading is dev-mode only: see Native Library Loading above β no packaged/bundled distribution yet.
- Android AT-command/firmware surface: correctly unsupported β no Android equivalent exists (use the modem-hardware path via desktop platforms for those operations).
- iOS: not implemented.
This project is licensed under the NativeMindNONC License β see the LICENSE file for details.
Key Terms:
- β Free for non-commercial use (education, research, personal learning)
β οΈ Commercial use requires written permission from the copyright holder- π ShareAlike: Derivative works must be published as GitHub Forks under the same license
- π Attribution required: Credit the original authors with link to repository
Package: flutter_gsm
Version: 0.1.0
License: NativeMindNONC
Homepage: https://github.com/telon/flutter_gsm
Issues: https://github.com/telon/flutter_gsm/issues