aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/meson.build4
-rw-r--r--src/mm-log-object.c13
-rw-r--r--src/mm-log-object.h1
-rw-r--r--src/mm-port-scheduler-rr.c393
-rw-r--r--src/mm-port-scheduler-rr.h53
-rw-r--r--src/mm-port-scheduler.c74
-rw-r--r--src/mm-port-scheduler.h71
-rw-r--r--src/mm-port-serial.c118
-rw-r--r--src/mm-port-serial.h1
-rw-r--r--src/mm-port.c3
-rw-r--r--src/tests/meson.build1
-rw-r--r--src/tests/test-port-scheduler.c640
12 files changed, 1361 insertions, 11 deletions
diff --git a/src/meson.build b/src/meson.build
index 6e1aae89..10838cf1 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -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]
diff --git a/src/mm-log-object.c b/src/mm-log-object.c
index ed41355f..64e4b407 100644
--- a/src/mm-log-object.c
+++ b/src/mm-log-object.c
@@ -63,7 +63,7 @@ mm_log_object_get_id (MMLogObject *self)
gchar *self_id;
self_id = MM_LOG_OBJECT_GET_IFACE (self)->build_id (self);
- if (priv->owner_id) {
+ if (self_id && priv->owner_id) {
priv->id = g_strdup_printf ("%s/%s", priv->owner_id, self_id);
g_free (self_id);
} else
@@ -81,6 +81,17 @@ mm_log_object_set_owner_id (MMLogObject *self,
priv = get_private (self);
g_free (priv->owner_id);
priv->owner_id = g_strdup (owner_id);
+
+ mm_log_object_reset_id (self);
+}
+
+void
+mm_log_object_reset_id (MMLogObject *self)
+{
+ Private *priv;
+
+ priv = get_private (self);
+ g_clear_pointer (&priv->id, g_free);
}
static void
diff --git a/src/mm-log-object.h b/src/mm-log-object.h
index 21ffc05e..9ddc7832 100644
--- a/src/mm-log-object.h
+++ b/src/mm-log-object.h
@@ -34,5 +34,6 @@ struct _MMLogObjectInterface
const gchar *mm_log_object_get_id (MMLogObject *self);
void mm_log_object_set_owner_id (MMLogObject *self,
const gchar *owner_id);
+void mm_log_object_reset_id (MMLogObject *self);
#endif /* MM_LOG_OBJECT_H */
diff --git a/src/mm-port-scheduler-rr.c b/src/mm-port-scheduler-rr.c
new file mode 100644
index 00000000..cfa3a28a
--- /dev/null
+++ b/src/mm-port-scheduler-rr.c
@@ -0,0 +1,393 @@
+/* -*- 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);
+static void log_object_iface_init (MMLogObjectInterface *iface);
+
+struct _MMPortSchedulerRRPrivate {
+ guint instance_id;
+ 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;
+static guint instance_id_last = 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)
+ G_IMPLEMENT_INTERFACE (MM_TYPE_LOG_OBJECT,
+ log_object_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);
+
+ g_assert_cmpint (self->priv->sources->len, <, UINT_MAX);
+ mm_obj_dbg (self, "[%s] source id %p registered", tag, source_id);
+ mm_log_object_reset_id (MM_LOG_OBJECT (self));
+ }
+}
+
+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) {
+ mm_obj_dbg (self, "[%s] source id %p unregistered", s->tag, s->id);
+ g_ptr_array_remove_index (self->priv->sources, idx);
+ mm_log_object_reset_id (MM_LOG_OBJECT (self));
+
+ /* 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, "[%s] notified command-done but not active source", s->tag);
+ 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 gchar *
+log_object_build_id (MMLogObject *_self)
+{
+ MMPortSchedulerRR *self = MM_PORT_SCHEDULER_RR (_self);
+ g_autoptr(GString) str;
+ guint i;
+
+ str = g_string_sized_new (16);
+ for (i = 0; i < self->priv->sources->len; i++) {
+ Source *s;
+
+ s = g_ptr_array_index (self->priv->sources, i);
+ if (str->len)
+ g_string_append_c (str, ',');
+ g_string_append (str, s->tag);
+ }
+
+ return g_strdup_printf ("scheduler-%u (%s)", self->priv->instance_id, str->str);
+}
+
+static void
+log_object_iface_init (MMLogObjectInterface *iface)
+{
+ iface->build_id = log_object_build_id;
+}
+
+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;
+ self->priv->instance_id = instance_id_last++;
+}
+
+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));
+}
diff --git a/src/mm-port-scheduler-rr.h b/src/mm-port-scheduler-rr.h
new file mode 100644
index 00000000..6e750872
--- /dev/null
+++ b/src/mm-port-scheduler-rr.h
@@ -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_ */
diff --git a/src/mm-port-scheduler.c b/src/mm-port-scheduler.c
new file mode 100644
index 00000000..c1790a1c
--- /dev/null
+++ b/src/mm-port-scheduler.c
@@ -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);
+}
diff --git a/src/mm-port-scheduler.h b/src/mm-port-scheduler.h
new file mode 100644
index 00000000..d67ddc24
--- /dev/null
+++ b/src/mm-port-scheduler.h
@@ -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 */
diff --git a/src/mm-port-serial.c b/src/mm-port-serial.c
index 8b9bef98..d1976972 100644
--- a/src/mm-port-serial.c
+++ b/src/mm-port-serial.c
@@ -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);
@@ -878,6 +887,21 @@ port_serial_queue_process (gpointer data)
}
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)
{
GError *error = NULL;
@@ -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)
{
@@ -1962,6 +2017,16 @@ mm_port_serial_init (MMPortSerial *self)
}
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,
const GValue *value,
@@ -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_hash_table_destroy (self->priv->reply_cache);
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_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",
diff --git a/src/mm-port-serial.h b/src/mm-port-serial.h
index 59daadd4..f050bdc3 100644
--- a/src/mm-port-serial.h
+++ b/src/mm-port-serial.h
@@ -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,
diff --git a/src/mm-port.c b/src/mm-port.c
index da37f902..7102770d 100644
--- a/src/mm-port.c
+++ b/src/mm-port.c
@@ -132,6 +132,9 @@ log_object_build_id (MMLogObject *_self)
MMPort *self;
self = MM_PORT (_self);
+ if (!self->priv->device || !self->priv->ptype)
+ return NULL;
+
return g_strdup_printf ("%s/%s",
mm_port_get_device (self),
mm_port_type_get_string (mm_port_get_port_type (self)));
diff --git a/src/tests/meson.build b/src/tests/meson.build
index ea9ea3a3..5c6764b9 100644
--- a/src/tests/meson.build
+++ b/src/tests/meson.build
@@ -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,
diff --git a/src/tests/test-port-scheduler.c b/src/tests/test-port-scheduler.c
new file mode 100644
index 00000000..8f8fbeaa
--- /dev/null
+++ b/src/tests/test-port-scheduler.c
@@ -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;
+}