Testing Cellular Features Using CellularPi: Implementation and Testing
Introduction
In our previous posts, we covered the physical setup of the A7670E modem and its driver implementation. This final post explores the implementation of our testing interface and the underlying system architecture that enables reliable cellular communication.
System Architecture Overview
Our implementation requirements demanded a system-wide solution for cellular connectivity:
- TCP/IP networking accessible to all applications
- Integration with the Linux networking stack
- Automated network interface configuration
- System-wide SMS messaging capabilities
ModemManager's Role
ModemManager serves as the central control point for modem operations. When our A7670E connects, ModemManager:
- Detects the modem on USB ports (ttyUSB0, ttyUSB1, ttyUSB2)
- Identifies the correct ports for:
- AT command interface
- Data communication
- Additional control channels
- Initializes the modem with proper AT command sequences
- Manages modem state and network registration
The key benefit is ModemManager's handling of all AT command interactions:
AT+CFUN=1 # Enable full functionality
AT+CPIN? # Check SIM status
AT+CREG? # Check network registration
AT+CGREG? # Check GPRS registration
AT+CGDCONT # Set up PDP context
Network Management Layer
NetworkManager integrates with ModemManager to:
- Create network interfaces when the modem connects
- Configure IP settings through DHCP
- Set up proper routing tables
- Manage DNS resolution
CellularPi Implementation
Our testing application interfaces with this architecture through three main components:
1. Modem Management Module
The ModemDBusManager class establishes communication with ModemManager:
ModemDBusManager::ModemDBusManager(QObject* parent)
: QObject(parent)
, m_dbusConnection(QDBusConnection::systemBus())
{
m_dbusInitTimer = std::make_unique<QTimer>(this);
m_dbusInitTimer->setInterval(DBUS_INIT_RETRY_INTERVAL);
// Watch for modem service availability
auto* watcher = new QDBusServiceWatcher(
"org.freedesktop.ModemManager1",
m_dbusConnection,
QDBusServiceWatcher::WatchForRegistration |
QDBusServiceWatcher::WatchForUnregistration,
this
);
}
Interface initialization includes:
bool ModemDBusManager::initializeDBusInterfaces()
{
QMutexLocker locker(&m_dbusInitMutex);
// Create ModemManager interface
QDBusInterface manager("org.freedesktop.ModemManager1",
"/org/freedesktop/ModemManager1",
"org.freedesktop.DBus.ObjectManager",
m_dbusConnection);
// Get managed objects and find our modem
QDBusMessage reply = manager.call("GetManagedObjects");
const QDBusArgument arg = reply.arguments().at(0).value<QDBusArgument>();
// Parse interfaces
arg.beginMap();
while (!arg.atEnd()) {
QString path;
QVariantMap interfaces;
arg.beginMapEntry();
arg >> path >> interfaces;
arg.endMapEntry();
if (interfaces.contains(
"org.freedesktop.ModemManager1.Modem.Messaging")) {
m_dbusInterfaces.messagingPath = path;
found = true;
break;
}
}
arg.endMap();
}
2. Message Queue Management
We implement a robust message queuing system:
void Modem::queueSMS(const QString &phoneNo, const QString &message) {
QMutexLocker locker(&m_mutex);
m_smsQueue.enqueue({phoneNo, message});
if (!m_isProcessing) {
QMetaObject::invokeMethod(this, "processSMSQueue",
Qt::QueuedConnection);
}
}
void Modem::processSMSQueue() {
QMutexLocker locker(&m_mutex);
if (m_isProcessing || m_smsQueue.isEmpty()) {
return;
}
m_isProcessing = true;
SMSData smsData = m_smsQueue.dequeue();
sendSMSOverDBus(smsData);
}
3. Error Handling and Retries
Our implementation includes comprehensive error handling:
void ModemDBusManager::handleCreateSMSResponse(
const QDBusPendingCallWatcher *watcher,
const QVariantMap &properties,
int retryCount)
{
QDBusPendingReply<QDBusObjectPath> reply = *watcher;
if (reply.isError()) {
if (retryCount < MAX_RETRY_ATTEMPTS &&
shouldRetryOperation(reply.error())) {
scheduleRetry(properties, retryCount + 1);
return;
}
emit smsResult(false);
return;
}
// Create SMS interface for sending
QDBusInterface sms("org.freedesktop.ModemManager1",
reply.value().path(),
"org.freedesktop.ModemManager1.Sms",
m_dbusConnection);
if (!sms.isValid()) {
if (retryCount < MAX_RETRY_ATTEMPTS) {
scheduleRetry(properties, retryCount + 1);
return;
}
emit smsResult(false);
return;
}
}
Testing Implementation
SMS Testing
- Message Creation:
void ModemDBusManager::sendSMS(const QString &phoneNumber,
const QString &message)
{
QVariantMap properties;
properties["number"] = phoneNumber;
properties["text"] = message;
auto* createWatcher = new QDBusPendingCallWatcher(
m_dbusInterfaces.messaging->asyncCall("Create", properties),
this
);
}
- Status Monitoring:
void Modem::handleSMSResult(bool success) {
if (success) {
emit smsSent(m_mostRecentRecipient);
} else {
emit smsFailed(m_mostRecentRecipient);
}
QMutexLocker locker(&m_mutex);
m_isProcessing = false;
if (!m_smsQueue.isEmpty()) {
QMetaObject::invokeMethod(this, "processSMSQueue",
Qt::QueuedConnection);
}
}
Network Testing
Once the cellular interface is up and running, our REST client operates just like any other networked application, utilizing the standard Linux networking stack.
REST Client Implementation
Our REST client provides a clean interface for testing network connectivity:
class RestClient : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(QUrl baseUrl READ baseUrl WRITE setBaseUrl NOTIFY baseUrlChanged)
Q_PROPERTY(bool sslSupported READ sslSupported CONSTANT)
public:
explicit RestClient(QObject *parent = nullptr);
Q_INVOKABLE void get(const QString &endpoint);
Q_INVOKABLE void post(const QString &endpoint, const QVariantMap &data);
Q_INVOKABLE void put(const QString &endpoint, const QVariantMap &data);
Q_INVOKABLE void deleteResource(const QString &endpoint);
signals:
void responseReceived(const QJsonObject &response);
void errorOccurred(const QString &error);
void requestStarted();
void requestFinished();
};
Request Processing
The client handles requests using Qt's networking classes:
void RestClient::get(const QString &endpoint)
{
try {
auto request = m_requestFactory->createRequest(endpoint);
request.setHeader(QNetworkRequest::ContentTypeHeader,
"application/json");
emit requestStarted();
auto *reply = m_qnam.get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
handleResponse(reply);
reply->deleteLater();
});
} catch (const std::exception &e) {
emit errorOccurred(QString("Request failed: %1").arg(e.what()));
emit requestFinished();
}
}
Response Handling
We process responses and convert them to a format suitable for our UI:
void RestClient::handleResponse(QNetworkReply *reply)
{
if (reply->error() == QNetworkReply::NoError) {
QByteArray data = reply->readAll();
QJsonDocument doc = QJsonDocument::fromJson(data);
if (doc.isObject()) {
emit responseReceived(doc.object());
} else if (doc.isArray()) {
QJsonObject response;
response["data"] = doc.array();
emit responseReceived(response);
}
} else {
emit errorOccurred(reply->errorString());
}
emit requestFinished();
}
Testing Interface
The user interface provides a simple way to test different endpoints and methods:
// Internet testing tab
ColumnLayout {
spacing: window.height * 0.02
GroupBox {
title: "REST API Test"
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
spacing: window.height * 0.02
ComboBox {
id: endpointCombo
Layout.fillWidth: true
model: ["/posts/1", "/users/1", "/todos/1"]
font.pixelSize: baseSize
}
Button {
text: "GET Request"
Layout.fillWidth: true
onClicked: RestClient.get(endpointCombo.currentText)
}
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
TextArea {
id: responseArea
readOnly: true
font.family: "Monospace"
placeholderText: "Response will appear here..."
}
}
}
}
}
Example Test Scenarios
- Basic GET Request:
// Test basic connectivity
RestClient.baseUrl = "https://jsonplaceholder.typicode.com"
RestClient.get("/posts/1")
Expected response:
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident",
"body": "quia et suscipit\nsuscipit recusandae..."
}
- POST Request with Data:
RestClient.post("/posts", {
"title": "Test Post",
"body": "Testing cellular connectivity",
"userId": 1
})
- Error Handling Test:
// Test invalid endpoint
RestClient.get("/invalid-endpoint")
Performance Considerations
- Request Timeout Settings:
void RestClient::setupRequestDefaults()
{
// Set reasonable timeouts for cellular networks
m_qnam.setTransferTimeout(30000); // 30 seconds
// Configure proxy if needed
QNetworkProxy proxy = QNetworkProxy::applicationProxy();
if (proxy.type() != QNetworkProxy::NoProxy) {
m_qnam.setProxy(proxy);
}
}
- Connection Monitoring:
void RestClient::monitorNetworkState()
{
connect(&m_qnam, &QNetworkAccessManager::networkAccessible,
this, [this](QNetworkAccessManager::NetworkAccessibility accessible) {
if (accessible == QNetworkAccessManager::NotAccessible) {
emit errorOccurred("Network connection lost");
}
});
}
The REST client provides a clean, reliable way to test network connectivity without needing to know about the underlying cellular connection. This abstraction allows us to focus on testing application-level functionality while the Linux networking stack handles the complexities of the cellular data connection.
Testing Results and Analysis
Basic Connectivity Testing
During our testing with Safaricom network in Kenya, we conducted a series of HTTP requests to verify cellular data functionality:
- DNS Resolution Test
$ ping -c 4 google.com
PING google.com (142.250.196.142) 56(84) bytes of data.
64 bytes from nbo05s06-in-f142.1e100.net (142.250.196.142): icmp_seq=1 ttl=105 time=89.6 ms
64 bytes from nbo05s06-in-f142.1e100.net (142.250.196.142): icmp_seq=2 ttl=105 time=88.4 ms
64 bytes from nbo05s06-in-f142.1e100.net (142.250.196.142): icmp_seq=3 ttl=105 time=87.9 ms
64 bytes from nbo05s06-in-f142.1e100.net (142.250.196.142): icmp_seq=4 ttl=105 time=88.2 ms
--- google.com ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3004ms
rtt min/avg/max/mdev = 87.912/88.525/89.562/0.608 ms
- Network Interface Statistics
$ ifconfig wwan0
wwan0: flags=4305<UP,POINTOPOINT,RUNNING,NOARP,MULTICAST> mtu 1500
inet 10.158.x.x netmask 255.255.255.252 destination 10.158.x.x
inet6 fe80::c851:95ff:fe5d:c516 prefixlen 64 scopeid 0x20<link>
unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 txqueuelen 1000
RX packets 8562 bytes 9519432 (9.5 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 7245 bytes 845623 (845.6 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
Recommendations for Production Use
Based on our testing results, we recommend:
-
Network Resilience
- Implement progressive retry delays
- Cache successful responses where appropriate
- Monitor signal strength trends
-
Resource Management
- Limit concurrent requests to 5
- Implement request queuing
- Monitor memory usage
-
Error Handling
- Log network type changes
- Implement automatic reconnection
- Cache failed requests for retry
These findings will inform our future development and deployment strategies for cellular-based IoT applications.
Part 1: The Implementation Chronicles: A7670E USB Modem
Part 2: Driver Support for A7670E: Kernel Module Implementation