port-serial: add serial port command scheduler

Add an interface and implementation for a port scheduler that round-
robins between ports the scheduler is attached to, serializing command
execution among one or more MMPortSerial instances.

Theory of operation:

Sources (e.g. MMPort subclasses) register themselves with the scheduler.

Each source notifies the scheduler whenever its command queue depth changes,
for example when new commands are submitted, when commands are completed,
or when commands are canceled.

The scheduler will round-robin between all sources with pending commands,
sleeping when there are no pending commands from any source.

For each source with a pending command the scheduler will emit the
'send-command' signal with that source's ID. The given source should
send the next command in its queue to the modem.

When that command is finished (either successfully or with an error/timeout)
the source must call mm_port_scheduler_notify_command_done() to notify the
scheduler that it may advance to the next source with a pending command, if
any. If the 'send-command' signal and the notify_command_done() call are not
balanced the scheduler may stall.

Signed-off-by: Dan Williams <dan@ioncontrol.co>
This commit is contained in:
Dan Williams
2025-05-23 18:46:53 -05:00
parent b24615d801
commit a769bbed6d
9 changed files with 1309 additions and 10 deletions
+4
View File
@@ -134,6 +134,8 @@ headers = files(
'mm-iface-port-at.h',
'mm-port.h',
'mm-port-serial-at.h',
'mm-port-scheduler.h',
'mm-port-scheduler-rr.h',
)
sources = files(
@@ -146,6 +148,8 @@ sources = files(
'mm-port-serial-gps.c',
'mm-port-serial-qcdm.c',
'mm-serial-parsers.c',
'mm-port-scheduler.c',
'mm-port-scheduler-rr.c',
)
deps = [libkerneldevice_dep]
+357
View File
@@ -0,0 +1,357 @@
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details:
*
* Copyright (C) 2025 Dan Williams <dan@ioncontrol.co>
*/
#include "mm-port-scheduler-rr.h"
#include "mm-log-object.h"
/* Theory of operation:
*
* Sources (e.g. MMPort subclasses) register themselves with the scheduler.
*
* Each source notifies the scheduler whenever its command queue depth changes,
* for example when new commands are submitted, when commands are completed,
* or when commands are canceled.
*
* The scheduler will round-robin between all sources with pending commands,
* sleeping when there are no pending commands from any source.
*
* For each source with a pending command the scheduler will emit the
* 'send-command' signal with that source's ID. The given source should
* send the next command in its queue to the modem.
*
* When that command is finished (either successfully or with an error/timeout)
* the source must call mm_port_scheduler_notify_command_done() to notify the
* scheduler that it may advance to the next source with a pending command, if
* any. If the 'send-command' signal and the notify_command_done() call are not
* balanced the scheduler may stall.
*/
static void mm_port_scheduler_iface_init (MMPortSchedulerInterface *iface);
struct _MMPortSchedulerRRPrivate {
GPtrArray *sources;
guint cur_source;
gboolean in_command;
guint next_pending_id;
/* Delay between allowing ports to send commands, in ms */
guint inter_port_delay;
};
enum {
PROP_0,
PROP_INTER_PORT_DELAY,
LAST_PROP
};
static guint send_command_signal = 0;
G_DEFINE_TYPE_WITH_CODE (MMPortSchedulerRR, mm_port_scheduler_rr, G_TYPE_OBJECT,
G_ADD_PRIVATE (MMPortSchedulerRR)
G_IMPLEMENT_INTERFACE (MM_TYPE_PORT_SCHEDULER,
mm_port_scheduler_iface_init))
/*****************************************************************************/
typedef struct {
gpointer id;
gchar *tag; /* e.g. port name */
guint num_pending;
} Source;
static void
source_free (Source *s)
{
g_free (s->tag);
g_slice_free (Source, s);
}
static Source *
find_source (MMPortSchedulerRR *self,
gpointer source_id,
guint *out_idx)
{
guint i;
for (i = 0; i < self->priv->sources->len; i++) {
Source *s;
s = g_ptr_array_index (self->priv->sources, i);
if (s->id == source_id) {
if (out_idx)
*out_idx = i;
return s;
}
}
return NULL;
}
static Source *
find_next_source (MMPortSchedulerRR *self,
guint *out_idx)
{
guint i, idx;
/* Starting at the source *after* the current source, advance through
* the entire array and back to the current source (in case only the
* current source has pending commands) to find the next source with
* a pending command.
*/
for (i = 0, idx = self->priv->cur_source + 1;
i < self->priv->sources->len;
i++, idx++) {
Source *s;
/* Wrap around */
if (idx >= self->priv->sources->len)
idx = 0;
s = g_ptr_array_index (self->priv->sources, idx);
if (s->num_pending > 0) {
if (out_idx)
*out_idx = idx;
return s;
}
}
return NULL;
}
static void schedule_next_command (MMPortSchedulerRR *self);
static gboolean
run_next_command (MMPortSchedulerRR *self)
{
self->priv->next_pending_id = 0;
if (find_next_source (self, &self->priv->cur_source)) {
Source *s;
s = g_ptr_array_index (self->priv->sources, self->priv->cur_source);
/* If this source has a pending command, run it. */
self->priv->in_command = TRUE;
g_signal_emit (MM_PORT_SCHEDULER (self),
send_command_signal,
0,
s->id);
}
return G_SOURCE_REMOVE;
}
static void
schedule_next_command (MMPortSchedulerRR *self)
{
guint next_idx = 0;
guint delay = 0;
if (self->priv->next_pending_id || self->priv->in_command || !find_next_source (self, &next_idx))
return;
/* Only delay next command if we change sources and this isn't the
* first time we're running a command.
*/
if (next_idx != self->priv->cur_source && self->priv->cur_source < self->priv->sources->len)
delay = self->priv->inter_port_delay;
self->priv->next_pending_id = g_timeout_add (delay, (GSourceFunc) run_next_command, self);
}
static void
register_source (MMPortScheduler *scheduler,
gpointer source_id,
const gchar *tag)
{
MMPortSchedulerRR *self = MM_PORT_SCHEDULER_RR (scheduler);
Source *s;
g_assert (source_id != NULL);
s = find_source (self, source_id, NULL);
if (!s) {
s = g_slice_new0 (Source);
s->id = source_id;
s->tag = g_strdup (tag);
g_ptr_array_add (self->priv->sources, s);
}
}
static void
unregister_source (MMPortScheduler *scheduler, gpointer source_id)
{
MMPortSchedulerRR *self = MM_PORT_SCHEDULER_RR (scheduler);
Source *s;
guint idx = 0;
g_assert (source_id != NULL);
s = find_source (self, source_id, &idx);
if (s) {
g_ptr_array_remove_index (self->priv->sources, idx);
/* If we just removed the current source, advance to the next one */
if (self->priv->cur_source == idx)
schedule_next_command (self);
}
}
static void
notify_num_pending (MMPortScheduler *scheduler,
gpointer source_id,
guint num_pending)
{
MMPortSchedulerRR *self = MM_PORT_SCHEDULER_RR (scheduler);
Source *s;
g_assert (source_id != NULL);
s = find_source (self, source_id, NULL);
if (s && s->num_pending != num_pending) {
s->num_pending = num_pending;
schedule_next_command (self);
}
}
static void
notify_command_done (MMPortScheduler *scheduler,
gpointer source_id,
guint num_pending)
{
MMPortSchedulerRR *self = MM_PORT_SCHEDULER_RR (scheduler);
Source *s;
guint idx = 0;
g_assert (source_id != NULL);
s = find_source (self, source_id, &idx);
if (!s) {
mm_obj_warn (self, "unknown source %p notified command-done", source_id);
return;
}
/* Only the current source gets to call this function */
if (self->priv->cur_source != idx) {
mm_obj_warn (self,
"source %p notified command-done but not active source",
source_id);
return;
}
self->priv->in_command = FALSE;
s->num_pending = num_pending;
schedule_next_command (self);
}
/*****************************************************************************/
MMPortSchedulerRR *
mm_port_scheduler_rr_new (void)
{
return MM_PORT_SCHEDULER_RR (g_object_new (MM_TYPE_PORT_SCHEDULER_RR, NULL));
}
static void
get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
MMPortSchedulerRR *self = MM_PORT_SCHEDULER_RR (object);
switch (prop_id) {
case PROP_INTER_PORT_DELAY:
g_value_set_uint (value, self->priv->inter_port_delay);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
MMPortSchedulerRR *self = MM_PORT_SCHEDULER_RR (object);
switch (prop_id) {
case PROP_INTER_PORT_DELAY:
self->priv->inter_port_delay = g_value_get_uint (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
mm_port_scheduler_iface_init (MMPortSchedulerInterface *scheduler_iface)
{
scheduler_iface->register_source = register_source;
scheduler_iface->unregister_source = unregister_source;
scheduler_iface->notify_num_pending = notify_num_pending;
scheduler_iface->notify_command_done = notify_command_done;
send_command_signal = g_signal_lookup (MM_PORT_SCHEDULER_SIGNAL_SEND_COMMAND,
MM_TYPE_PORT_SCHEDULER);
}
static void
mm_port_scheduler_rr_init (MMPortSchedulerRR *self)
{
self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
MM_TYPE_PORT_SCHEDULER_RR,
MMPortSchedulerRRPrivate);
self->priv->sources = g_ptr_array_new_full (2, (GDestroyNotify) source_free);
self->priv->cur_source = G_MAXUINT32;
}
static void
dispose (GObject *object)
{
MMPortSchedulerRR *self = MM_PORT_SCHEDULER_RR (object);
if (self->priv->next_pending_id) {
g_source_remove (self->priv->next_pending_id);
self->priv->next_pending_id = 0;
}
g_assert (self->priv->sources->len == 0);
g_ptr_array_free (self->priv->sources, TRUE);
G_OBJECT_CLASS (mm_port_scheduler_rr_parent_class)->dispose (object);
}
static void
mm_port_scheduler_rr_class_init (MMPortSchedulerRRClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
/* Virtual methods */
object_class->set_property = set_property;
object_class->get_property = get_property;
object_class->dispose = dispose;
g_object_class_install_property
(object_class, PROP_INTER_PORT_DELAY,
g_param_spec_uint (MM_PORT_SCHEDULER_RR_INTER_PORT_DELAY,
"Inter-port Delay",
"Inter-port delay in ms",
0, G_MAXUINT, 0,
G_PARAM_READWRITE));
}
+53
View File
@@ -0,0 +1,53 @@
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details:
*
* Copyright (C) 2025 Dan Williams <dan@ioncontrol.co>
*/
#ifndef _MM_PORT_SCHEDULER_RR_H_
#define _MM_PORT_SCHEDULER_RR_H_
#include <glib-object.h>
#include <gio/gio.h>
#include "mm-port-scheduler.h"
#define MM_TYPE_PORT_SCHEDULER_RR (mm_port_scheduler_rr_get_type ())
#define MM_PORT_SCHEDULER_RR(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), MM_TYPE_PORT_SCHEDULER_RR, MMPortSchedulerRR))
#define MM_PORT_SCHEDULER_RR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), MM_TYPE_PORT_SCHEDULER_RR, MMPortSchedulerRRClass))
#define MM_IS_PORT_SCHEDULER_RR(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MM_TYPE_PORT_SCHEDULER_RR))
#define MM_IS_PORT_SCHEDULER_RR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), MM_TYPE_PORT_SCHEDULER_RR))
#define MM_PORT_SCHEDULER_RR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), MM_TYPE_PORT_SCHEDULER_RR, MMPortSchedulerRRClass))
#define MM_PORT_SCHEDULER_RR_INTER_PORT_DELAY "inter-port-delay"
typedef struct _MMPortSchedulerRR MMPortSchedulerRR;
typedef struct _MMPortSchedulerRRClass MMPortSchedulerRRClass;
typedef struct _MMPortSchedulerRRPrivate MMPortSchedulerRRPrivate;
struct _MMPortSchedulerRR {
/*< private >*/
GObject parent;
MMPortSchedulerRRPrivate *priv;
};
struct _MMPortSchedulerRRClass {
/*< private >*/
GObjectClass parent;
};
GType mm_port_scheduler_rr_get_type (void);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (MMPortSchedulerRR, g_object_unref)
MMPortSchedulerRR *mm_port_scheduler_rr_new (void);
#endif /* _MM_PORT_SCHEDULER_RR_H_ */
+74
View File
@@ -0,0 +1,74 @@
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details:
*
* Copyright (C) 2025 Dan Williams <dan@ioncontrol.co>
*/
#include "mm-port-scheduler.h"
G_DEFINE_INTERFACE (MMPortScheduler, mm_port_scheduler, G_TYPE_OBJECT)
enum {
SIGNAL_SEND_COMMAND,
SIGNAL_LAST
};
static guint signals[SIGNAL_LAST] = { 0 };
/*****************************************************************************/
void
mm_port_scheduler_register_source (MMPortScheduler *self,
gpointer source,
const gchar *tag)
{
MM_PORT_SCHEDULER_GET_INTERFACE (self)->register_source (self, source, tag);
}
void
mm_port_scheduler_unregister_source (MMPortScheduler *self,
gpointer source)
{
MM_PORT_SCHEDULER_GET_INTERFACE (self)->unregister_source (self, source);
}
void
mm_port_scheduler_notify_num_pending (MMPortScheduler *self,
gpointer source,
guint num_pending)
{
MM_PORT_SCHEDULER_GET_INTERFACE (self)->notify_num_pending (self, source, num_pending);
}
void
mm_port_scheduler_notify_command_done (MMPortScheduler *self,
gpointer source,
guint num_pending)
{
MM_PORT_SCHEDULER_GET_INTERFACE (self)->notify_command_done (self, source, num_pending);
}
/*****************************************************************************/
static void
mm_port_scheduler_default_init (MMPortSchedulerInterface *iface)
{
signals[SIGNAL_SEND_COMMAND] =
g_signal_new (MM_PORT_SCHEDULER_SIGNAL_SEND_COMMAND,
MM_TYPE_PORT_SCHEDULER,
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (MMPortSchedulerInterface, send_command),
NULL, NULL,
NULL,
G_TYPE_NONE, 1,
G_TYPE_POINTER);
}
+71
View File
@@ -0,0 +1,71 @@
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details:
*
* Copyright (C) 2025 Dan Williams <dan@ioncontrol.co>
*/
#ifndef MM_PORT_SCHEDULER_H
#define MM_PORT_SCHEDULER_H
#include <glib.h>
#include <glib-object.h>
#define MM_TYPE_PORT_SCHEDULER mm_port_scheduler_get_type ()
#define MM_PORT_SCHEDULER_GET_INTERFACE(o) (G_TYPE_INSTANCE_GET_INTERFACE ((o), MM_TYPE_PORT_SCHEDULER, MMPortSchedulerInterface))
G_DECLARE_INTERFACE (MMPortScheduler, mm_port_scheduler, MM, PORT_SCHEDULER, GObject)
#define MM_PORT_SCHEDULER_SIGNAL_SEND_COMMAND "send-command"
typedef struct _MMPortSchedulerInterface MMPortSchedulerInterface;
struct _MMPortSchedulerInterface
{
GTypeInterface g_iface;
/* Signals */
void (*send_command) (MMPortScheduler *self,
gpointer source);
/* Methods */
void (*register_source) (MMPortScheduler *self,
gpointer source,
const gchar *tag);
void (*unregister_source) (MMPortScheduler *self,
gpointer source);
void (*notify_num_pending) (MMPortScheduler *self,
gpointer source,
guint num_pending);
void (*notify_command_done) (MMPortScheduler *self,
gpointer source,
guint num_pending);
};
void mm_port_scheduler_register_source (MMPortScheduler *self,
gpointer source,
const gchar *tag);
void mm_port_scheduler_unregister_source (MMPortScheduler *self,
gpointer source);
void mm_port_scheduler_notify_num_pending (MMPortScheduler *self,
gpointer source,
guint num_pending);
void mm_port_scheduler_notify_command_done (MMPortScheduler *self,
gpointer source,
guint num_pending);
#endif /* MM_PORT_SCHEDULER_H */
+108 -10
View File
@@ -36,6 +36,8 @@
#include "mm-port-serial.h"
#include "mm-log-object.h"
#include "mm-helper-enums-types.h"
#include "mm-port-scheduler.h"
#include "mm-port-scheduler-rr.h"
static gboolean port_serial_queue_process (gpointer data);
static void port_serial_schedule_queue_process (MMPortSerial *self,
@@ -59,6 +61,7 @@ enum {
PROP_FD,
PROP_SPEW_CONTROL,
PROP_FLASH_OK,
PROP_SCHEDULER,
LAST_PROP
};
@@ -81,6 +84,10 @@ struct _MMPortSerialPrivate {
GQueue *queue;
GByteArray *response;
/* Command scheduler */
MMPortScheduler *scheduler;
guint scheduler_send_id;
/* For real ports, iochannel, and we implement the eagain limit */
GIOChannel *iochannel;
guint iochannel_id;
@@ -89,7 +96,6 @@ struct _MMPortSerialPrivate {
GSocket *socket;
GSource *socket_source;
guint baud;
guint bits;
char parity;
@@ -194,8 +200,9 @@ mm_port_serial_command (MMPortSerial *self,
else
g_queue_push_tail (self->priv->queue, task);
if (g_queue_get_length (self->priv->queue) == 1)
port_serial_schedule_queue_process (self, 0);
mm_port_scheduler_notify_num_pending (self->priv->scheduler,
self,
g_queue_get_length (self->priv->queue));
}
/*****************************************************************************/
@@ -737,10 +744,11 @@ port_serial_got_response (MMPortSerial *self,
}
g_object_unref (task);
}
if (!g_queue_is_empty (self->priv->queue))
port_serial_schedule_queue_process (self, 0);
mm_port_scheduler_notify_command_done (self->priv->scheduler,
self,
g_queue_get_length (self->priv->queue));
}
}
g_object_unref (self);
g_clear_error (&error);
@@ -805,6 +813,7 @@ port_serial_queue_process (gpointer data)
GCancellable *cancellable;
GError *error = NULL;
g_assert (self->priv->timeout_id == 0);
self->priv->queue_id = 0;
task = g_queue_peek_head (self->priv->queue);
@@ -877,6 +886,21 @@ port_serial_queue_process (gpointer data)
return G_SOURCE_REMOVE;
}
static void
scheduler_send_command (MMPortScheduler *scheduler,
gpointer source,
gpointer user_data)
{
MMPortSerial *self = MM_PORT_SERIAL (user_data);
/* Must be for us */
if (source == self) {
g_assert (self->priv->queue_id == 0);
g_assert (self->priv->timeout_id == 0);
port_serial_queue_process (self);
}
}
static void
parse_response_buffer (MMPortSerial *self)
{
@@ -1444,6 +1468,8 @@ _close_internal (MMPortSerial *self, gboolean force)
}
g_queue_clear (self->priv->queue);
mm_port_scheduler_notify_num_pending (self->priv->scheduler, self, 0);
if (self->priv->timeout_id) {
g_source_remove (self->priv->timeout_id);
self->priv->timeout_id = 0;
@@ -1889,6 +1915,35 @@ mm_port_serial_get_flow_control (MMPortSerial *self)
/*****************************************************************************/
static void
scheduler_setup (MMPortSerial *self, MMPortScheduler *scheduler)
{
self->priv->scheduler = scheduler;
if (self->priv->scheduler) {
const gchar *port_name;
port_name = mm_port_get_device (MM_PORT (self));
mm_port_scheduler_register_source (self->priv->scheduler, self, port_name);
self->priv->scheduler_send_id = g_signal_connect (self->priv->scheduler,
MM_PORT_SCHEDULER_SIGNAL_SEND_COMMAND,
G_CALLBACK (scheduler_send_command),
self);
}
}
static void
scheduler_cleanup (MMPortSerial *self)
{
if (self->priv->scheduler) {
mm_port_scheduler_unregister_source (self->priv->scheduler, self);
g_signal_handler_disconnect (self->priv->scheduler, self->priv->scheduler_send_id);
self->priv->scheduler_send_id = 0;
}
g_clear_object (&self->priv->scheduler);
}
/*****************************************************************************/
MMPortSerial *
mm_port_serial_new (const char *name, MMPortType ptype)
{
@@ -1961,6 +2016,16 @@ mm_port_serial_init (MMPortSerial *self)
self->priv->response = g_byte_array_sized_new (500);
}
static void
constructed (GObject *object)
{
MMPortSerial *self = MM_PORT_SERIAL (object);
/* Add a default scheduler if none was set via GObject properties */
if (!self->priv->scheduler)
scheduler_setup (self, MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ()));
}
static void
set_property (GObject *object,
guint prop_id,
@@ -1997,6 +2062,10 @@ set_property (GObject *object,
case PROP_FLASH_OK:
self->priv->flash_ok = g_value_get_boolean (value);
break;
case PROP_SCHEDULER:
scheduler_cleanup (self);
scheduler_setup (self, g_value_dup_object (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@@ -2039,6 +2108,9 @@ get_property (GObject *object,
case PROP_FLASH_OK:
g_value_set_boolean (value, self->priv->flash_ok);
break;
case PROP_SCHEDULER:
g_value_set_object (value, self->priv->scheduler);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@@ -2046,7 +2118,7 @@ get_property (GObject *object,
}
static void
finalize (GObject *object)
dispose (GObject *object)
{
MMPortSerial *self = MM_PORT_SERIAL (object);
@@ -2059,14 +2131,30 @@ finalize (GObject *object)
g_assert (self->priv->socket == NULL);
g_assert (self->priv->socket_source == NULL);
if (self->priv->timeout_id)
if (self->priv->timeout_id) {
g_source_remove (self->priv->timeout_id);
self->priv->timeout_id = 0;
}
if (self->priv->queue_id)
if (self->priv->queue_id) {
g_source_remove (self->priv->queue_id);
self->priv->queue_id = 0;
}
g_byte_array_unref (self->priv->response);
self->priv->response = NULL;
scheduler_cleanup (self);
G_OBJECT_CLASS (mm_port_serial_parent_class)->dispose (object);
}
static void
finalize (GObject *object)
{
MMPortSerial *self = MM_PORT_SERIAL (object);
g_hash_table_destroy (self->priv->reply_cache);
g_byte_array_unref (self->priv->response);
g_queue_free (self->priv->queue);
G_OBJECT_CLASS (mm_port_serial_parent_class)->finalize (object);
@@ -2080,8 +2168,10 @@ mm_port_serial_class_init (MMPortSerialClass *klass)
g_type_class_add_private (object_class, sizeof (MMPortSerialPrivate));
/* Virtual methods */
object_class->constructed = constructed;
object_class->set_property = set_property;
object_class->get_property = get_property;
object_class->dispose = dispose;
object_class->finalize = finalize;
klass->config_fd = real_config_fd;
@@ -2161,6 +2251,14 @@ mm_port_serial_class_init (MMPortSerialClass *klass)
TRUE,
G_PARAM_READWRITE));
g_object_class_install_property
(object_class, PROP_SCHEDULER,
g_param_spec_object (MM_PORT_SERIAL_SCHEDULER,
"Scheduler",
"Command scheduler object (optional)",
MM_TYPE_PORT_SCHEDULER,
G_PARAM_READWRITE));
/* Signals */
signals[BUFFER_FULL] =
g_signal_new ("buffer-full",
+1
View File
@@ -40,6 +40,7 @@
#define MM_PORT_SERIAL_FD "fd" /* Construct-only */
#define MM_PORT_SERIAL_SPEW_CONTROL "spew-control"
#define MM_PORT_SERIAL_FLASH_OK "flash-ok"
#define MM_PORT_SERIAL_SCHEDULER "scheduler"
typedef enum {
MM_PORT_SERIAL_RESPONSE_NONE,
+1
View File
@@ -8,6 +8,7 @@ test_units = {
'error-helpers': libhelpers_dep,
'kernel-device-helpers': libkerneldevice_dep,
'modem-helpers': libhelpers_dep,
'port-scheduler': libport_dep,
'sms-part-3gpp': libhelpers_dep,
'sms-part-cdma': libhelpers_dep,
'sms-list': libsms_dep,
+640
View File
@@ -0,0 +1,640 @@
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details:
*
* Copyright (C) 2025 Dan Williams <dan@ioncontrol.co>
*/
#include <glib.h>
#include <string.h>
#include <locale.h>
#include <stdio.h>
#include "mm-port-scheduler-rr.h"
#include "mm-log-test.h"
static GMainLoop *loop;
/*****************************************************************************/
typedef struct {
MMPortScheduler *sched;
guint sig_id;
gpointer source_id;
gint num_pending;
gboolean immediate;
guint idle_id;
gpointer data;
gpointer data2;
} TestSourceCtx;
static void
test_source_setup (TestSourceCtx *ctx,
MMPortScheduler *sched,
GCallback send_cmd_func)
{
ctx->sched = g_object_ref (sched);
mm_port_scheduler_register_source (sched, ctx->source_id, "test");
ctx->sig_id = g_signal_connect (sched,
MM_PORT_SCHEDULER_SIGNAL_SEND_COMMAND,
send_cmd_func,
ctx);
mm_port_scheduler_notify_num_pending (ctx->sched, ctx->source_id, ctx->num_pending);
}
static void
test_source_cleanup (TestSourceCtx *ctx)
{
g_assert_cmpint (ctx->num_pending, ==, 0);
g_signal_handler_disconnect (ctx->sched, ctx->sig_id);
mm_port_scheduler_unregister_source (ctx->sched, ctx->source_id);
g_object_unref (ctx->sched);
}
/*****************************************************************************/
static gboolean
test_ss_command_done (TestSourceCtx *ctx)
{
ctx->idle_id = 0;
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending);
if (ctx->num_pending == 0)
g_main_loop_quit (loop);
return G_SOURCE_REMOVE;
}
static void
test_ss_send_command (MMPortScheduler *scheduler,
gpointer source,
TestSourceCtx *ctx)
{
g_assert (scheduler == ctx->sched);
g_assert (source == ctx->source_id);
ctx->num_pending--;
g_assert_cmpint (ctx->num_pending, >=, 0);
if (ctx->immediate) {
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending);
if (ctx->num_pending == 0)
g_main_loop_quit (loop);
} else
ctx->idle_id = g_idle_add ((GSourceFunc)test_ss_command_done, ctx);
}
static void
test_ss_done (gboolean immediate)
{
MMPortScheduler *sched;
TestSourceCtx ctx = {
.source_id = GUINT_TO_POINTER (0x1),
.num_pending = 10,
.immediate = immediate,
};
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
test_source_setup (&ctx, sched, G_CALLBACK (test_ss_send_command));
g_main_loop_run (loop);
test_source_cleanup (&ctx);
g_object_unref (sched);
}
/*****************************************************************************/
static void
test_ds_send_command (MMPortScheduler *scheduler,
gpointer source,
TestSourceCtx *ctx)
{
guint *counter = ctx->data;
g_assert (scheduler == ctx->sched);
if (source != ctx->source_id)
return;
ctx->num_pending--;
g_assert_cmpuint (ctx->num_pending, >=, 0);
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending);
/* assert that the scheduler alternates between the two sources */
g_assert_cmpuint ((*counter) % 2, ==, ctx->idle_id);
(*counter)--;
if (*counter == 0)
g_main_loop_quit (loop);
}
static void
test_ds_ordering (void)
{
MMPortScheduler *sched;
guint counter;
TestSourceCtx ctx1 = {
.source_id = GUINT_TO_POINTER (0x1),
.num_pending = 10,
.data = &counter,
.idle_id = 0, /* expected value of (counter % 2) */
};
TestSourceCtx ctx2 = {
.source_id = GUINT_TO_POINTER (0x2),
.num_pending = 10,
.data = &counter,
.idle_id = 1, /* expected value of (counter % 2) */
};
counter = ctx1.num_pending + ctx2.num_pending;
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_send_command));
test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_send_command));
g_main_loop_run (loop);
test_source_cleanup (&ctx1);
test_source_cleanup (&ctx2);
g_object_unref (sched);
}
/*****************************************************************************/
static void
test_ds_uneven_send_command (MMPortScheduler *scheduler,
gpointer source,
TestSourceCtx *ctx)
{
guint *counter = ctx->data;
g_assert (scheduler == ctx->sched);
if (source != ctx->source_id)
return;
ctx->num_pending--;
/* Test that the scheduler only calls each source for the number of pending
* commands it has notified the scheduler are in its queue.
*/
g_assert_cmpint (ctx->num_pending, >=, 0);
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending);
(*counter)--;
if (*counter == 0)
g_main_loop_quit (loop);
}
static void
test_ds_uneven_num_pending (void)
{
MMPortScheduler *sched;
guint counter;
TestSourceCtx ctx1 = {
.source_id = GUINT_TO_POINTER (0x1),
.num_pending = 10,
.data = &counter,
};
TestSourceCtx ctx2 = {
.source_id = GUINT_TO_POINTER (0x2),
.num_pending = 5,
.data = &counter,
};
counter = ctx1.num_pending + ctx2.num_pending;
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_uneven_send_command));
test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_uneven_send_command));
g_main_loop_run (loop);
test_source_cleanup (&ctx1);
test_source_cleanup (&ctx2);
g_object_unref (sched);
}
/*****************************************************************************/
static gboolean
ds_later_notify_pending (TestSourceCtx *ctx)
{
mm_port_scheduler_notify_num_pending (ctx->sched, ctx->source_id, ctx->num_pending);
return G_SOURCE_REMOVE;
}
static void
test_ds_later_send_command (MMPortScheduler *scheduler,
gpointer source,
TestSourceCtx *ctx)
{
guint *counter = ctx->data;
g_assert (scheduler == ctx->sched);
if (source != ctx->source_id)
return;
ctx->num_pending--;
g_assert_cmpint (ctx->num_pending, >=, 0);
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending);
/* After we've reached zero pending commands wait a short time and then
* add more to make sure the scheduler wakes up and processes the new pending
* requests */
if (ctx->num_pending == 0 && ctx->idle_id > 0) {
ctx->num_pending = ctx->idle_id;
ctx->idle_id = 0;
g_timeout_add_seconds (GPOINTER_TO_UINT (ctx->source_id),
(GSourceFunc)ds_later_notify_pending,
ctx);
}
(*counter)--;
if (*counter == 0)
g_main_loop_quit (loop);
}
static void
test_ds_num_pending_later (void)
{
MMPortScheduler *sched;
guint counter;
TestSourceCtx ctx1 = {
.source_id = GUINT_TO_POINTER (0x1),
.num_pending = 5,
.data = &counter,
.idle_id = 2, /* num pending to add after original num_pending reaches 0 */
};
TestSourceCtx ctx2 = {
.source_id = GUINT_TO_POINTER (0x2),
.num_pending = 4,
.data = &counter,
.idle_id = 1, /* num pending to add after original num_pending reaches 0 */
};
counter = ctx1.num_pending + ctx2.num_pending + ctx1.idle_id + ctx2.idle_id;
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_later_send_command));
test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_later_send_command));
g_main_loop_run (loop);
test_source_cleanup (&ctx1);
test_source_cleanup (&ctx2);
g_object_unref (sched);
}
/*****************************************************************************/
static gboolean
quit_loop (void)
{
g_main_loop_quit (loop);
return G_SOURCE_REMOVE;
}
static void
test_ds_bad_notify_send_command (MMPortScheduler *scheduler,
gpointer source,
TestSourceCtx *ctx)
{
g_assert (scheduler == ctx->sched);
if (source != ctx->source_id)
return;
if (GPOINTER_TO_UINT (source) == 0x2) {
/* Second source without any pending commands tries to call
* notify_command_done but this should have no effect; the scheduler
* should ignore the num_pending given here.
*/
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, 15);
return;
}
g_assert_cmpuint (GPOINTER_TO_UINT (source), ==, 0x1);
ctx->num_pending--;
g_assert_cmpint (ctx->num_pending, >=, 0);
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending);
if (ctx->num_pending == 0) {
/* Schedule a timeout to quit the mainloop to give enough time for
* the scheduler to mess up (which we don't expect).
*/
g_timeout_add_seconds (1, (GSourceFunc)quit_loop, NULL);
}
}
static gboolean
assert_not_reached (void)
{
g_assert_not_reached ();
return G_SOURCE_REMOVE;
}
static void
test_ds_bad_notify_done (void)
{
MMPortScheduler *sched;
guint timeout_id;
TestSourceCtx ctx1 = {
.source_id = GUINT_TO_POINTER (0x1),
.num_pending = 5,
};
TestSourceCtx ctx2 = {
.source_id = GUINT_TO_POINTER (0x2),
.num_pending = 0,
/* This source just hammers notify_done even though it never has any
* pending commands.
*/
};
timeout_id = g_timeout_add_seconds (3, (GSourceFunc)assert_not_reached, NULL);
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_bad_notify_send_command));
test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_bad_notify_send_command));
g_main_loop_run (loop);
g_source_remove (timeout_id);
test_source_cleanup (&ctx1);
test_source_cleanup (&ctx2);
g_object_unref (sched);
}
/*****************************************************************************/
static void
test_ds_delay_notify_send_command (MMPortScheduler *scheduler,
gpointer source,
TestSourceCtx *ctx)
{
guint *counter = ctx->data;
gint64 *last_call = ctx->data2;
gint64 now;
g_assert (scheduler == ctx->sched);
if (source != ctx->source_id)
return;
/* Ensure there was at least the inter-port delay time since the last call */
now = g_get_monotonic_time ();
g_assert_cmpint (now - *last_call, >=, 500);
*last_call = now;
ctx->num_pending--;
g_assert_cmpuint (ctx->num_pending, >=, 0);
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending);
(*counter)--;
if (*counter == 0)
g_main_loop_quit (loop);
}
static void
test_ds_inter_port_delay (void)
{
MMPortScheduler *sched;
guint counter;
gint64 last_call = 0;
TestSourceCtx ctx1 = {
.source_id = GUINT_TO_POINTER (0x1),
.data = &counter,
.data2 = &last_call,
.num_pending = 5,
};
TestSourceCtx ctx2 = {
.source_id = GUINT_TO_POINTER (0x2),
.data = &counter,
.data2 = &last_call,
.num_pending = 5,
};
counter = ctx1.num_pending + ctx2.num_pending;
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
g_object_set (sched, MM_PORT_SCHEDULER_RR_INTER_PORT_DELAY, 500, NULL);
test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_delay_notify_send_command));
test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_delay_notify_send_command));
g_main_loop_run (loop);
test_source_cleanup (&ctx1);
test_source_cleanup (&ctx2);
g_object_unref (sched);
}
/*****************************************************************************/
static void
test_ds_no_delay_notify_send_command (MMPortScheduler *scheduler,
gpointer source,
TestSourceCtx *ctx)
{
guint *counter = ctx->data;
gint64 *last_call = ctx->data2;
gint64 now;
g_assert (scheduler == ctx->sched);
if (source != ctx->source_id)
return;
/* Since the second source has no pending commands, there should be
* no delay between calls since only one source is executing.
*/
now = g_get_monotonic_time ();
g_assert_cmpint (now - *last_call, <, 1000);
*last_call = now;
ctx->num_pending--;
g_assert_cmpuint (ctx->num_pending, >=, 0);
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending);
(*counter)--;
if (*counter == 0)
g_main_loop_quit (loop);
}
static void
test_ds_inter_port_no_delay (void)
{
MMPortScheduler *sched;
guint counter;
gint64 last_call;
TestSourceCtx ctx1 = {
.source_id = GUINT_TO_POINTER (0x1),
.data = &counter,
.data2 = &last_call,
.num_pending = 5,
};
TestSourceCtx ctx2 = {
.source_id = GUINT_TO_POINTER (0x2),
.data = &counter,
.data2 = &last_call,
.num_pending = 0,
};
counter = ctx1.num_pending + ctx2.num_pending;
last_call = g_get_monotonic_time ();
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_no_delay_notify_send_command));
test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_no_delay_notify_send_command));
g_main_loop_run (loop);
test_source_cleanup (&ctx1);
test_source_cleanup (&ctx2);
g_object_unref (sched);
}
/*****************************************************************************/
static void
test_ds_pending_during_done_notify_send_command (MMPortScheduler *scheduler,
gpointer source,
TestSourceCtx *ctx)
{
guint *counter = ctx->data;
g_assert (scheduler == ctx->sched);
if (source != ctx->source_id)
return;
ctx->num_pending--;
g_assert_cmpuint (ctx->num_pending, >=, 0);
/* Simulate command completion adding more pending commands before calling command-done */
if (ctx->idle_id > 0) {
ctx->idle_id--;
ctx->num_pending++; /* increase length of fake source's command queue */
mm_port_scheduler_notify_num_pending (ctx->sched, ctx->source_id, ctx->num_pending);
}
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending);
(*counter)--;
if (*counter == 0)
g_main_loop_quit (loop);
}
static void
test_ds_pending_during_done (void)
{
MMPortScheduler *sched;
guint counter;
TestSourceCtx ctx1 = {
.source_id = GUINT_TO_POINTER (0x1),
.data = &counter,
.idle_id = 5, /* additional to add during notify-command-done */
.num_pending = 5,
};
TestSourceCtx ctx2 = {
.source_id = GUINT_TO_POINTER (0x2),
.data = &counter,
.idle_id = 5, /* additional to add during notify-command-done */
.num_pending = 5,
};
counter = ctx1.num_pending + ctx2.num_pending + ctx1.idle_id + ctx2.idle_id;
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_pending_during_done_notify_send_command));
test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_pending_during_done_notify_send_command));
g_main_loop_run (loop);
test_source_cleanup (&ctx1);
test_source_cleanup (&ctx2);
g_object_unref (sched);
}
/*****************************************************************************/
static void
test_errors_bad_source_done (void)
{
MMPortScheduler *sched;
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
mm_port_scheduler_notify_command_done (sched, GUINT_TO_POINTER (0x1), 5);
g_object_unref (sched);
}
/*****************************************************************************/
static void
test_errors_source_done_before_loop (void)
{
MMPortScheduler *sched;
TestSourceCtx ctx = {
.source_id = GUINT_TO_POINTER (0x1),
};
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
test_source_setup (&ctx, sched, G_CALLBACK (assert_not_reached));
mm_port_scheduler_notify_command_done (sched, ctx.source_id, 5);
test_source_cleanup (&ctx);
g_object_unref (sched);
}
/*****************************************************************************/
int main (int argc, char **argv)
{
int ret;
setlocale (LC_ALL, "");
g_test_init (&argc, &argv, NULL);
loop = g_main_loop_new (NULL, FALSE);
g_test_add_data_func ("/MM/port-scheduler/single-source/done-immediate", GUINT_TO_POINTER (TRUE), (GTestDataFunc)test_ss_done);
g_test_add_data_func ("/MM/port-scheduler/single-source/done-idle", GUINT_TO_POINTER (FALSE), (GTestDataFunc)test_ss_done);
g_test_add_data_func ("/MM/port-scheduler/dual-source/ordering", NULL, (GTestDataFunc)test_ds_ordering);
g_test_add_data_func ("/MM/port-scheduler/dual-source/uneven-num-pending", NULL, (GTestDataFunc)test_ds_uneven_num_pending);
g_test_add_data_func ("/MM/port-scheduler/dual-source/num-pending-later", NULL, (GTestDataFunc)test_ds_num_pending_later);
g_test_add_data_func ("/MM/port-scheduler/dual-source/bad-notify-done", NULL, (GTestDataFunc)test_ds_bad_notify_done);
g_test_add_data_func ("/MM/port-scheduler/dual-source/inter-port-delay", NULL, (GTestDataFunc)test_ds_inter_port_delay);
g_test_add_data_func ("/MM/port-scheduler/dual-source/inter-port-no-delay", NULL, (GTestDataFunc)test_ds_inter_port_no_delay);
g_test_add_data_func ("/MM/port-scheduler/dual-source/pending-during-done", NULL, (GTestDataFunc)test_ds_pending_during_done);
g_test_add_data_func ("/MM/port-scheduler/errors/bad-source-done", NULL, (GTestDataFunc)test_errors_bad_source_done);
g_test_add_data_func ("/MM/port-scheduler/errors/source-done-before-loop", NULL, (GTestDataFunc)test_errors_source_done_before_loop);
ret = g_test_run();
g_main_loop_unref (loop);
return ret;
}