From 67fb5942ecbece45112645ae3eff2ee5a969ecdf Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Fri, 12 Dec 2025 16:55:31 +0000 Subject: [PATCH 01/20] improve error logging and fix typos --- src/services/hal.lua | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/services/hal.lua b/src/services/hal.lua index d00fb938..2fe5de6c 100644 --- a/src/services/hal.lua +++ b/src/services/hal.lua @@ -1,7 +1,3 @@ -local modem_manager = require "services.hal.managers.modemcard" -local ubus_manager = require "services.hal.managers.ubus" -local uci_manager = require "services.hal.managers.uci" -local wlan_managaer = require "services.hal.managers.wlan" local fiber = require "fibers.fiber" local queue = require "fibers.queue" local op = require "fibers.op" @@ -37,6 +33,11 @@ function hal_service:_register_device(device, capabilities) "Device Event: capability '%s' for device '%s' with id '%s' does not have an id", cap_name, device.type, device.id )) + elseif not cap.control then + log.error(string.format( + "Device Event: capability '%s' for device '%s' with id '%s' does not have a control field", + cap_name, device.type, device.id + )) else if not self.capabilities[cap_name] then self.capabilities[cap_name] = {} @@ -222,7 +223,7 @@ function hal_service:_handle_capability_control(request) end) end -function hal_service:_handle_capbility_info(data) +function hal_service:_handle_capability_info(data) if not data then return end if not data.type then @@ -272,7 +273,7 @@ end function hal_service:_apply_config(msg) log.trace(string.format( - '%s - %s: Recieved new HAL config', + "%s - %s: Received new HAL config", self.ctx:value("service_name"), self.ctx:value("fiber_name") )) @@ -309,10 +310,19 @@ function hal_service:_apply_config(msg) if manager then manager:apply_config(manager_config) else - local manager_pkg = require('services.hal.managers.' .. manager_name) - self.managers[manager_name] = manager_pkg.new() - self.managers[manager_name]:spawn(self.ctx, self.conn, self.device_event_q, self.capability_info_q) - self.managers[manager_name]:apply_config(manager_config) + local ok, manager_pkg = pcall(require, 'services.hal.managers.' .. manager_name) + if ok and type(manager_pkg) == "table" then + self.managers[manager_name] = manager_pkg.new() + self.managers[manager_name]:spawn(self.ctx, self.conn, self.device_event_q, self.capability_info_q) + self.managers[manager_name]:apply_config(manager_config) + else + log.error(string.format( + '%s - %s: Failed to load manager "%s"', + self.ctx:value("service_name"), + self.ctx:value("fiber_name"), + manager_name + )) + end end end end @@ -347,7 +357,7 @@ function hal_service:_control_main(ctx) config_sub:next_msg_op():wrap(function(msg) self:_apply_config(msg) end), cap_ctrl_sub:next_msg_op():wrap(function(msg) self:_handle_capability_control(msg) end), self.device_event_q:get_op():wrap(function(msg) self:_handle_device_connection_event(msg) end), - self.capability_info_q:get_op():wrap(function(msg) self:_handle_capbility_info(msg) end), + self.capability_info_q:get_op():wrap(function(msg) self:_handle_capability_info(msg) end), ctx:done_op() ):perform() end From 72d6b1c01131dbf6b836514961c2d465276bb753 Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Fri, 12 Dec 2025 16:55:55 +0000 Subject: [PATCH 02/20] Add dummy and dummy2 manager implementations and test harness for hal core --- .../harness/services/hal/managers/dummy.lua | 41 + .../harness/services/hal/managers/dummy2.lua | 41 + tests/hal/test_core.lua | 780 ++++++++++++++++++ 3 files changed, 862 insertions(+) create mode 100644 tests/hal/harness/services/hal/managers/dummy.lua create mode 100644 tests/hal/harness/services/hal/managers/dummy2.lua create mode 100644 tests/hal/test_core.lua diff --git a/tests/hal/harness/services/hal/managers/dummy.lua b/tests/hal/harness/services/hal/managers/dummy.lua new file mode 100644 index 00000000..2fb06f8d --- /dev/null +++ b/tests/hal/harness/services/hal/managers/dummy.lua @@ -0,0 +1,41 @@ +local service = require "service" +local op = require "fibers.op" +local sleep = require "fibers.sleep" +local new_msg = require 'bus'.new_msg + +local DummyManagement = {} +DummyManagement.__index = DummyManagement + +local function new() + local dummy_management = {} + return setmetatable(dummy_management, DummyManagement) +end + +function DummyManagement:apply_config(config) + self.test_arg = config.test_arg +end + +function DummyManagement:_manager(ctx, conn, device_event_q, capability_info_q) + conn:publish( + new_msg( + { "dummy", "status" }, + "running" + ) + ) + while not ctx:err() do + op.choice( + sleep.sleep_op(1), + ctx:done_op() + ):perform() + end +end + +function DummyManagement:spawn(ctx, conn, device_event_q, capability_info_q) + self.test_arg = nil + self.ctx = ctx + service.spawn_fiber("Dummy Manager", conn, ctx, function (fctx) + self:_manager(fctx, conn, device_event_q, capability_info_q) + end) +end + +return { new = new } diff --git a/tests/hal/harness/services/hal/managers/dummy2.lua b/tests/hal/harness/services/hal/managers/dummy2.lua new file mode 100644 index 00000000..eddab9ac --- /dev/null +++ b/tests/hal/harness/services/hal/managers/dummy2.lua @@ -0,0 +1,41 @@ +local service = require "service" +local op = require "fibers.op" +local sleep = require "fibers.sleep" +local new_msg = require 'bus'.new_msg + +local DummyManagement = {} +DummyManagement.__index = DummyManagement + +local function new() + local dummy_management = {} + return setmetatable(dummy_management, DummyManagement) +end + +function DummyManagement:apply_config(config) + self.test_arg = config.test_arg +end + +function DummyManagement:_manager(ctx, conn, device_event_q, capability_info_q) + conn:publish( + new_msg( + { "dummy2", "status" }, + "running" + ) + ) + while not ctx:err() do + op.choice( + sleep.sleep_op(1), + ctx:done_op() + ):perform() + end +end + +function DummyManagement:spawn(ctx, conn, device_event_q, capability_info_q) + self.test_arg = nil + self.ctx = ctx + service.spawn_fiber("Dummy Manager", conn, ctx, function (fctx) + self:_manager(fctx, conn, device_event_q, capability_info_q) + end) +end + +return { new = new } diff --git a/tests/hal/test_core.lua b/tests/hal/test_core.lua new file mode 100644 index 00000000..3ef21bc6 --- /dev/null +++ b/tests/hal/test_core.lua @@ -0,0 +1,780 @@ +-- Detect if this file is being run as the entry point +local this_file = debug.getinfo(1, "S").source:match("@?([^/]+)$") +local is_entry_point = arg and arg[0] and arg[0]:match("[^/]+$") == this_file + +if is_entry_point then + -- Match the test harness package.path setup (see tests/test.lua, + -- test_wifi.lua, test_metrics.lua, test_system.lua) + package.path = "../../src/lua-fibers/?.lua;" -- fibers submodule src + .. "../../src/lua-trie/src/?.lua;" -- trie submodule src + .. "../../src/lua-bus/src/?.lua;" -- bus submodule src + .. "../../src/?.lua;" -- main src tree + .. "./test_utils/?.lua;" -- shared test utilities + .. package.path + .. ";/usr/lib/lua/?.lua;/usr/lib/lua/?/init.lua;" + .. "./harness/?.lua;" + + _G._TEST = true -- Enable test exports in source code +end + +local luaunit = require 'luaunit' +local fiber = require 'fibers.fiber' +local context = require 'fibers.context' +local sleep = require 'fibers.sleep' +local queue = require 'fibers.queue' + +-- Test harness for HAL configuration, device events, and capability events. + +-- HAL config tests + +TestHalConfig = {} + +local function get_env_variables() + local bg_ctx = context.background() + + local ctx = context.with_cancel( + context.with_value(bg_ctx, "service_name", "hal") + ) + + local bus = require 'bus' + + package.loaded['services.hal'] = nil -- force reload to reset state between tests + package.loaded['services.hal.managers.dummy'] = nil -- force reload to reset state between tests + local hal = require 'services.hal' + return hal, ctx, bus.new(), bus.new_msg +end + +local function config_path() + return { 'config', 'hal' } +end + +local function new_hal_env() + local hal, ctx, bus, new_msg = get_env_variables() + local conn = bus:connect() + return hal, ctx, bus, conn, new_msg +end + +local function publish_config(conn, new_msg, payload) + conn:publish(new_msg(config_path(), payload, { retained = true })) +end + +local function assert_no_managers(hal, msg) + luaunit.assertNil(next(hal.managers), msg or "Expected HAL to have zero managers") +end + +function TestHalConfig:test_simple_config() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local test_arg = "test" + local config = { -- spawns a dummy manager with a test arg + managers = { + dummy = { + test_arg = test_arg + }, + }, + } + + -- Subscribe before starting HAL to avoid races with the + -- initial status publication from the dummy manager. + local dummy_manager_sub = conn:subscribe({ "dummy", "status" }) + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + luaunit.assertNotNil(dummy_manager_sub, "Expected to subscribe to dummy manager status messages") + local msg, err = dummy_manager_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + luaunit.assertNil(err, "Expected to receive dummy manager status message") + luaunit.assertNotNil(msg, "Expected to receive dummy manager status message") + luaunit.assertEquals(msg.payload, "running") + luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to have instantiated dummy manager") + luaunit.assertEquals(hal.managers.dummy.test_arg, test_arg, "Expected dummy manager to have applied config") + ctx:cancel("test complete") +end + +function TestHalConfig:test_nil_config() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local config = nil + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + + sleep.sleep(0.1) -- Give HAL some time to process the config + + assert_no_managers(hal) + ctx:cancel("test complete") +end + +function TestHalConfig:test_empty_managers_config() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local config = { + managers = { + -- empty managers + }, + } + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + + sleep.sleep(0.1) -- Give HAL some time to process the config + + assert_no_managers(hal) + ctx:cancel("test complete") +end + +function TestHalConfig:test_invalid_type_config() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local config = "This should be a table, not a string" + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + + sleep.sleep(0.1) -- Give HAL some time to process the config + + assert_no_managers(hal) + ctx:cancel("test complete") +end + +function TestHalConfig:test_no_managers_config() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local config = { + -- no managers key + some_other_config = true + } + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + + sleep.sleep(0.1) -- Give HAL some time to process the config + + assert_no_managers(hal) + ctx:cancel("test complete") +end + +function TestHalConfig:test_invalid_manager() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local config = { + managers = { + invalid_manager_name = { + some_arg = "some_value" + }, + }, + } + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + + sleep.sleep(0.1) -- Give HAL some time to process the config + + luaunit.assertNil(hal.managers.invalid_manager_name, "Expected HAL to ignore invalid manager name") + ctx:cancel("test complete") +end + +function TestHalConfig:test_remove_manager() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local test_arg = "test" + local config = { + managers = { + dummy = { + test_arg = test_arg + }, + }, + } + + local dummy_manager_sub = conn:subscribe({ "dummy", "status" }) + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + luaunit.assertNotNil(dummy_manager_sub, "Expected to subscribe to dummy manager status messages") + local msg, err = dummy_manager_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + luaunit.assertNil(err, "Expected to receive dummy manager status message" .. (err and (": " .. tostring(err)) or "")) + luaunit.assertNotNil(msg, "Expected to receive dummy manager status message") + luaunit.assertEquals(msg.payload, "running") + luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to have instantiated dummy manager") + + local done = false + local dummy_ctx = hal.managers.dummy.ctx + fiber.spawn(function() + dummy_ctx:done_op():perform() + done = true + end) + + -- Now remove the dummy manager from HAL + + local new_config = { + managers = { + -- empty managers + }, + } + + conn:publish( + new_msg( + config_path(), + new_config, + { retained = true } + ) + ) + + sleep.sleep(0.1) -- Give HAL some time to process the config + luaunit.assertNil(hal.managers.dummy, "Expected HAL to have removed dummy manager") + luaunit.assertNil(next(hal.managers), "Expected HAL to have zero managers") + luaunit.assertTrue(done, "Expected dummy manager context to be done") -- manager should revieve a cancel signal + ctx:cancel("test complete") +end + +function TestHalConfig:test_reconfigure_manager() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local test_arg = "initial_value" + local config = { + managers = { + dummy = { + test_arg = test_arg + }, + }, + } + + -- Subscribe before starting HAL so we reliably see the + -- initial status message published by the dummy manager. + local dummy_manager_sub = conn:subscribe({ "dummy", "status" }) + luaunit.assertNotNil(dummy_manager_sub, "Expected to subscribe to dummy manager status messages") + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + + local msg, err = dummy_manager_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + luaunit.assertNil(err, "Expected to receive dummy manager status message") + luaunit.assertNotNil(msg, "Expected to receive dummy manager status message") + luaunit.assertEquals(msg.payload, "running") + luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to have instantiated dummy manager") + luaunit.assertEquals(hal.managers.dummy.test_arg, test_arg, "Expected dummy manager to have applied initial config") + + -- Now reconfigure the dummy manager + + local new_test_arg = "updated_value" + local new_config = { + managers = { + dummy = { + test_arg = new_test_arg + }, + }, + } + + conn:publish( + new_msg( + config_path(), + new_config, + { retained = true } + ) + ) + + sleep.sleep(0.1) -- Give HAL some time to process the config + luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to still have dummy manager after reconfiguration") + luaunit.assertEquals(hal.managers.dummy.test_arg, new_test_arg, + "Expected dummy manager to have applied updated config") + ctx:cancel("test complete") +end + +function TestHalConfig:test_invalid_config_does_not_affect_managers() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local test_arg = "initial_value" + local config = { + managers = { + dummy = { + test_arg = test_arg + }, + }, + } + + local dummy_manager_sub = conn:subscribe({ "dummy", "status" }) + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + luaunit.assertNotNil(dummy_manager_sub, "Expected to subscribe to dummy manager status messages") + local msg, err = dummy_manager_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + luaunit.assertNil(err, "Expected to receive dummy manager status message") + luaunit.assertNotNil(msg, "Expected to receive dummy manager status message") + luaunit.assertEquals(msg.payload, "running") + luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to have instantiated dummy manager") + luaunit.assertEquals(hal.managers.dummy.test_arg, test_arg, "Expected dummy manager to have applied initial config") + + -- Now publish an invalid config type + + local invalid_config = "This should be a table, not a string" + + publish_config(conn, new_msg, invalid_config) + + sleep.sleep(0.1) -- Give HAL some time to process the config + luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to still have dummy manager after invalid config") + luaunit.assertEquals(hal.managers.dummy.test_arg, test_arg, + "Expected dummy manager to retain initial config after invalid config") + + -- Now publish a missing managers config + local missing_managers_config = { + some_other_config = true + } + publish_config(conn, new_msg, missing_managers_config) + sleep.sleep(0.1) -- Give HAL some time to process the config + luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to still have dummy manager after missing managers config") + luaunit.assertEquals(hal.managers.dummy.test_arg, test_arg, + "Expected dummy manager to retain initial config after missing managers config") + + ctx:cancel("test complete") +end + +function TestHalConfig:test_partial_manager_removal() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local config = { + managers = { + dummy = { + test_arg = "value1" + }, + dummy2 = { + test_arg = "value2" + }, + }, + } + + local dummy_manager_sub = conn:subscribe({ "dummy", "status" }) + local dummy2_manager_sub = conn:subscribe({ "dummy2", "status" }) + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + + -- Wait for dummy manager 1 + luaunit.assertNotNil(dummy_manager_sub, "Expected to subscribe to dummy manager status messages") + local msg, err = dummy_manager_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + luaunit.assertNil(err, "Expected to receive dummy manager status message") + luaunit.assertNotNil(msg, "Expected to receive dummy manager status message") + luaunit.assertEquals(msg.payload, "running") + luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to have instantiated dummy manager") + + -- Wait for dummy manager 2 + luaunit.assertNotNil(dummy2_manager_sub, "Expected to subscribe to dummy2 manager status messages") + local msg2, err2 = dummy2_manager_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + luaunit.assertNil(err2, "Expected to receive dummy2 manager status message") + luaunit.assertNotNil(msg2, "Expected to receive dummy2 manager status message") + luaunit.assertEquals(msg2.payload, "running") + luaunit.assertNotNil(hal.managers.dummy2, "Expected HAL to have instantiated dummy2 manager") + + -- Now remove only the dummy2 manager by omitting it from + -- the new config while keeping dummy present. + local new_config = { + managers = { + dummy = { + test_arg = "value2" + }, + }, + } + conn:publish( + new_msg( + config_path(), + new_config, + { retained = true } + ) + ) + sleep.sleep(0.1) -- Give HAL some time to process the config + -- HAL removes managers that are not present in the new + -- config, so dummy2 should be removed and dummy kept. + luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to still have dummy manager") + luaunit.assertEquals(hal.managers.dummy.test_arg, "value2", "Expected dummy manager to have applied updated config") + luaunit.assertNil(hal.managers.dummy2, "Expected HAL to have removed dummy2 manager") + ctx:cancel("test complete") +end + +-- Device event tests + +TestHalDeviceEvent = {} + +local function make_dummy_device_event(connected, id, capabilities) + return { + connected = connected, + type = 'dummy_device', + id_field = "field", + data = { + field = id + }, + capabilities = connected and capabilities or nil, -- only present on connected + device_control = connected and {} or nil, + } +end + +local function device_event_path(device_type, device_id) + return { 'hal', 'device', device_type, device_id } +end + +local function assert_no_device_event(device_event_q, hal_device_event_sub, ctx, event, description) + device_event_q:put(event) + local msg, err = hal_device_event_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + luaunit.assertNil(msg, + "Expected to not receive HAL device event message for " .. description) + luaunit.assertNotNil(err, + "Expected to receive timeout error for " .. description) +end + +function TestHalDeviceEvent:test_device_add_event() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_event_q = hal.device_event_q + local device_name = 'dummy1' + local device_add_event = make_dummy_device_event(true, device_name, {}) + + local hal_device_event_sub = conn:subscribe(device_event_path('dummy_device', device_name)) + + device_event_q:put(device_add_event) + + -- Next wait for a device event and check the value in the payload + local msg, err = hal_device_event_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + luaunit.assertNil(err, "Expected to receive HAL device event message") + luaunit.assertNotNil(msg, "Expected to receive HAL device event message") + luaunit.assertNotNil(msg.payload, "Expected HAL device event message to have data payload") + luaunit.assertEquals(msg.payload.connected, true, "Expected device connected state to be true") + luaunit.assertEquals(msg.payload.type, 'dummy_device', "Expected device type to be 'dummy_device'") + luaunit.assertEquals(msg.payload.index, device_name, "Expected device id to match") + ctx:cancel("test complete") +end + +function TestHalDeviceEvent:test_device_remove_event() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_event_q = hal.device_event_q + local device_name = 'dummy2' + local device_add_event = make_dummy_device_event(true, device_name, {}) + local device_remove_event = make_dummy_device_event(false, device_name, nil) + + local hal_device_event_sub = conn:subscribe(device_event_path('dummy_device', device_name)) + + device_event_q:put(device_add_event) + device_event_q:put(device_remove_event) + + -- First wait for the add event to be received + local msg, err = hal_device_event_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + luaunit.assertNil(err, "Expected to receive HAL device event message") + luaunit.assertNotNil(msg, "Expected to receive HAL device event message") + luaunit.assertNotNil(msg.payload, "Expected HAL device event message to have data payload") + luaunit.assertEquals(msg.payload.connected, true, "Expected device connected state to be true") + + -- Now wait for the remove event and check the value in the payload + local msg, err = hal_device_event_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + luaunit.assertNil(err, "Expected to receive HAL device event message") + luaunit.assertNotNil(msg, "Expected to receive HAL device event message") + luaunit.assertNotNil(msg.payload, "Expected HAL device event message to have data payload") + luaunit.assertEquals(msg.payload.connected, false, "Expected device connected state to be false") + luaunit.assertEquals(msg.payload.type, 'dummy_device', "Expected device type to be 'dummy_device'") + luaunit.assertEquals(msg.payload.index, device_name, "Expected device id to match") + ctx:cancel("test complete") +end + +function TestHalDeviceEvent:test_device_remove_nonexistent() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_event_q = hal.device_event_q + local device_name = 'dummy3' + local device_remove_event = make_dummy_device_event(false, device_name, nil) + + local hal_device_event_sub = conn:subscribe(device_event_path('dummy_device', device_name)) + + assert_no_device_event(device_event_q, hal_device_event_sub, ctx, device_remove_event, 'nonexistent device') + ctx:cancel("test complete") +end + +function TestHalDeviceEvent:test_device_add_event_invalid() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_event_q = hal.device_event_q + local device_name = 'dummy_invalid' + + local hal_device_event_sub = conn:subscribe(device_event_path('dummy_device', device_name)) + + -- Each invalid event starts from a helper-generated valid event + -- and then removes exactly one required field. + + -- No type field + local event_no_type = make_dummy_device_event(true, device_name, {}) + event_no_type.type = nil + assert_no_device_event(device_event_q, hal_device_event_sub, ctx, event_no_type, 'invalid event: no type field') + + -- No connected field + local event_no_connected = make_dummy_device_event(true, device_name, {}) + event_no_connected.connected = nil + assert_no_device_event(device_event_q, hal_device_event_sub, ctx, event_no_connected, + 'invalid event: no connected field') + + -- No id_field field + local event_no_id_field = make_dummy_device_event(true, device_name, {}) + event_no_id_field.id_field = nil + assert_no_device_event(device_event_q, hal_device_event_sub, ctx, event_no_id_field, + 'invalid event: no id_field field') + + -- No data field + local event_no_data = make_dummy_device_event(true, device_name, {}) + event_no_data.data = nil + assert_no_device_event(device_event_q, hal_device_event_sub, ctx, event_no_data, 'invalid event: no data field') + + -- No capabilities field (for a connected device) + local event_no_capabilities = make_dummy_device_event(true, device_name, {}) + event_no_capabilities.capabilities = nil + assert_no_device_event(device_event_q, hal_device_event_sub, ctx, event_no_capabilities, + 'invalid event: no capabilities field') + + ctx:cancel('test complete') +end + +-- Capability event tests + +TestHalDeviceCapabilityEvent = {} + +-- These helpers are kept for future capability control tests, +-- where capability endpoints will be invoked directly. + +local function wrap_result(...) + return { result = { ... }, err = nil } +end + +local function wrap_error(err_msg) + return { result = nil, err = err_msg } +end + +local function make_dummy_capability_list(length) + local capabilities = {} + for i = 1, length do + local cap = { + id = tostring(i), + control = { + no_args = function() + return wrap_result(i, "no_args_endpoint") + end, + single_arg = function(args) + return wrap_result(i, "single_arg_endpoint", args, #args) + end, + multi_arg = function(args) + return wrap_result(i, "multi_arg_endpoint", args, #args) + end, + error_fn = function() + return wrap_error("Capability function error") + end + } + } + capabilities["capability" .. i] = cap + end + return capabilities +end + +local function assert_capability_event(event, expected_event) + luaunit.assertNotNil(event, "Expected capability event to not be nil") + luaunit.assertEquals(event.connected, expected_event.connected, "Expected capability connected state to match") + luaunit.assertEquals(event.type, expected_event.type, "Expected capability type to match") + luaunit.assertEquals(event.index, expected_event.index, "Expected capability index to match") + luaunit.assertNotNil(event.device, "Expected capability event to have device field") + luaunit.assertEquals(event.device.type, expected_event.device.type, "Expected capability device type to match") + luaunit.assertEquals(event.device.index, expected_event.device.index, "Expected capability device index to match") +end + +local function make_expected_capability_event(device_name, capability_index, connected) + return { + connected = connected, + type = "capability" .. capability_index, + index = tostring(capability_index), + device = { + type = "dummy_device", + index = device_name, + }, + } +end + +local function capability_event_path(capability_index) + -- Topic used by HAL for capability connection events generated + -- as a side effect of device connection events. + return { 'hal', 'capability', 'capability' .. capability_index, tostring(capability_index) } +end + +local function expect_capability_event(sub, ctx, expected_event, label) + local suffix = label and (" " .. label) or "" + local msg, err = sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + luaunit.assertNil(err, "Expected to receive HAL capability event message" .. suffix) + luaunit.assertNotNil(msg, "Expected to receive HAL capability event message" .. suffix) + assert_capability_event(msg.payload, expected_event) +end + +local function assert_no_capability_event(ctx, sub, label) + local suffix = label and (" " .. label) or "" + local msg, err = sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + luaunit.assertNil(msg, "Expected to not receive HAL capability event message" .. suffix) + luaunit.assertNotNil(err, "Expected to receive timeout error" .. suffix) +end + +local function expect_retained_drop_event(ctx, sub) + local msg, err = sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + luaunit.assertNil(err, "Expected to receive a message") + luaunit.assertNotNil(msg, "Expected to receive a message") + luaunit.assertNil(msg.payload, "Expected retained drop message to have nil payload") +end + + +function TestHalDeviceCapabilityEvent:test_device_capability_add_event() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_name = 'dummy_capable_device' + local capabilities = make_dummy_capability_list(1) + local device_event = make_dummy_device_event(true, device_name, capabilities) + local hal_capability_info_sub = conn:subscribe(capability_event_path(1)) + hal.device_event_q:put(device_event) + + local expected_event = make_expected_capability_event(device_name, 1, true) + + -- Next wait for a capability info event and check the value in the payload + expect_capability_event(hal_capability_info_sub, ctx, expected_event, "for capability1") + ctx:cancel("test complete") +end + +function TestHalDeviceCapabilityEvent:test_device_multi_capability_event() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_name = 'dummy_multi_capable_device' + local capabilities = make_dummy_capability_list(3) + local device_event = make_dummy_device_event(true, device_name, capabilities) + local subs = {} + for i = 1, 3 do + subs[i] = conn:subscribe(capability_event_path(i)) + end + + hal.device_event_q:put(device_event) + + -- Next wait for capability info events and check the values in the payloads + for i, sub in ipairs(subs) do + local expected_event = make_expected_capability_event(device_name, i, true) + expect_capability_event(sub, ctx, expected_event, "for capability" .. i) + end + + ctx:cancel("test complete") +end + +function TestHalDeviceCapabilityEvent:test_device_capability_remove_event() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_name = 'dummy_capable_device_remove' + local capabilities = make_dummy_capability_list(1) + local device_add_event = make_dummy_device_event(true, device_name, capabilities) + local device_remove_event = make_dummy_device_event(false, device_name, nil) + local hal_capability_info_sub = conn:subscribe(capability_event_path(1)) + + hal.device_event_q:put(device_add_event) + + local expected_add_event = make_expected_capability_event(device_name, 1, true) + + -- Next wait for a capability info add event and check the value in the payload + expect_capability_event(hal_capability_info_sub, ctx, expected_add_event, "for capability1 add") + + -- Now send the remove event + hal.device_event_q:put(device_remove_event) + + local expected_remove_event = make_expected_capability_event(device_name, 1, false) + + -- A nil payload is sent first to remove retained values under the topic + expect_retained_drop_event(ctx, hal_capability_info_sub) + + -- Next wait for a capability info remove event and check the value in the payload + expect_capability_event(hal_capability_info_sub, ctx, expected_remove_event, "for capability1 remove") + + ctx:cancel("test complete") +end + +function TestHalDeviceCapabilityEvent:test_device_capability_invalid_event() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_name = 'dummy_invalid_capability_device' + + -- Start from two valid capabilities, then invalidate one of them + local capabilities = make_dummy_capability_list(2) + capabilities["capability2"].id = nil -- missing id should make this capability invalid + + local device_add_event = make_dummy_device_event(true, device_name, capabilities) + + local valid_cap_sub = conn:subscribe(capability_event_path(1)) + local invalid_cap_sub = conn:subscribe(capability_event_path(2)) + + -- Publish device add event with one valid and one invalid capability + hal.device_event_q:put(device_add_event) + + -- The valid capability should still produce an event + local expected_valid_event = make_expected_capability_event(device_name, 1, true) + expect_capability_event(valid_cap_sub, ctx, expected_valid_event, "for valid capability1") + + -- The invalid capability (missing id) should not produce any event + assert_no_capability_event(ctx, invalid_cap_sub, " for invalid capability2 (missing id)") + + ctx:cancel("test complete") +end + +function TestHalDeviceCapabilityEvent:test_device_capability_duplicate_id_event() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_name1 = 'dummy_dup_cap_device1' + local device_name2 = 'dummy_dup_cap_device2' + local capabilities1 = make_dummy_capability_list(1) + local capabilities2 = make_dummy_capability_list(1) + local device1_add_event = make_dummy_device_event(true, device_name1, capabilities1) + local device2_add_event = make_dummy_device_event(true, device_name2, capabilities2) + local cap_sub = conn:subscribe(capability_event_path(1)) + + -- Publish two add events with the same capability id; HAL should handle + -- the duplicate id by overwriting the existing entry and still publishing + -- capability events for both devices. + hal.device_event_q:put(device1_add_event) + hal.device_event_q:put(device2_add_event) + + local expected_event1 = make_expected_capability_event(device_name1, 1, true) + expect_capability_event(cap_sub, ctx, expected_event1, "for capability1 first add") + + local expected_event2 = make_expected_capability_event(device_name2, 1, true) + expect_capability_event(cap_sub, ctx, expected_event2, "for capability1 second add (duplicate id)") + + ctx:cancel("test complete") +end + +function TestHalDeviceCapabilityEvent:test_device_capability_nil_control_event() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_name = 'dummy_nil_control_device' + local capabilities = make_dummy_capability_list(1) + -- Invalidate the control field; this capability should be ignored. + capabilities["capability1"].control = nil + local device_add_event = make_dummy_device_event(true, device_name, capabilities) + local cap_sub = conn:subscribe(capability_event_path(1)) + + hal.device_event_q:put(device_add_event) + + -- No capability event should be published when control is nil. + assert_no_capability_event(ctx, cap_sub, " for capability1 with nil control") + + ctx:cancel("test complete") +end + +local function main() + fiber.spawn(function() + luaunit.LuaUnit.run() + fiber.stop() + end) +end + +-- Only run tests if this file is executed directly (not via dofile) +if is_entry_point then + main() + fiber.main() +end From e964f9879c0868c428cff9945d15b9a744041717 Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Tue, 16 Dec 2025 11:58:17 +0000 Subject: [PATCH 03/20] Managers now get their own contexts with cancellation --- src/services/hal.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/hal.lua b/src/services/hal.lua index 2fe5de6c..a4d50f27 100644 --- a/src/services/hal.lua +++ b/src/services/hal.lua @@ -1,6 +1,7 @@ local fiber = require "fibers.fiber" local queue = require "fibers.queue" local op = require "fibers.op" +local context = require "fibers.context" local service = require "service" local new_msg = require("bus").new_msg local log = require "services.log" @@ -313,7 +314,7 @@ function hal_service:_apply_config(msg) local ok, manager_pkg = pcall(require, 'services.hal.managers.' .. manager_name) if ok and type(manager_pkg) == "table" then self.managers[manager_name] = manager_pkg.new() - self.managers[manager_name]:spawn(self.ctx, self.conn, self.device_event_q, self.capability_info_q) + self.managers[manager_name]:spawn(context.with_cancel(self.ctx), self.conn, self.device_event_q, self.capability_info_q) self.managers[manager_name]:apply_config(manager_config) else log.error(string.format( From 390e006ef48e15e13a55494c8f9c27d3d7955d9d Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Tue, 16 Dec 2025 11:58:57 +0000 Subject: [PATCH 04/20] Added test abstractions: timeouts are now based on time indepentant yields. More tests and refactor --- tests/hal/harness.lua | 129 +++++++++ tests/hal/test_core.lua | 562 +++++++++++++++++++++++++++++++++++----- 2 files changed, 623 insertions(+), 68 deletions(-) create mode 100644 tests/hal/harness.lua diff --git a/tests/hal/harness.lua b/tests/hal/harness.lua new file mode 100644 index 00000000..907a61f7 --- /dev/null +++ b/tests/hal/harness.lua @@ -0,0 +1,129 @@ +local fiber = require 'fibers.fiber' +local context = require 'fibers.context' +local op = require 'fibers.op' + +local harness = {} + +-- Default maximum number of cooperative "ticks" to wait +-- before treating a wait as a timeout. +local DEFAULT_MAX_TICKS = 20 + +-- Environment helpers ------------------------------------------------------- + +function harness.get_env_variables() + local bg_ctx = context.background() + + local ctx = context.with_cancel( + context.with_value(bg_ctx, 'service_name', 'hal') + ) + + local bus = require 'bus' + + -- Force reload to reset state between tests + package.loaded['services.hal'] = nil + package.loaded['services.hal.managers.dummy'] = nil + local hal = require 'services.hal' + return hal, ctx, bus.new(), bus.new_msg +end + +function harness.config_path() + return { 'config', 'hal' } +end + +function harness.new_hal_env() + local hal, ctx, bus, new_msg = harness.get_env_variables() + local conn = bus:connect() + return hal, ctx, bus, conn, new_msg +end + +function harness.publish_config(conn, new_msg, payload) + conn:publish(new_msg(harness.config_path(), payload, { retained = true })) +end + +-- Tick-based waiting helpers ----------------------------------------------- + +-- Internal helper to build an alt function for perform_alt that: +-- - increments a tick counter +-- - yields the current fiber +-- - enforces a max tick budget +-- - bails out if the context is cancelled +local function make_alt_wait(ctx, max_ticks) + local ticks = 0 + max_ticks = max_ticks or DEFAULT_MAX_TICKS + + return function() + if ctx and ctx:err() then + return nil, 'context cancelled' + end + + ticks = ticks + 1 + if ticks > max_ticks then + return nil, 'timeout' + end + + fiber.yield() + -- Special error sentinel used by wait helpers to + -- distinguish an alt-path from a real error. + return nil, '__ALT__' + end +end + +-- Wait for a message on a subscriber using a non-blocking choice. +-- Returns (msg, err). If the alt path exhausts the tick budget, +-- returns (nil, 'timeout'). If the context is cancelled, returns +-- (nil, 'context cancelled'). +function harness.wait_for_msg(sub, ctx, max_ticks) + local alt = make_alt_wait(ctx, max_ticks) + while true do + local msg, err = sub:next_msg_op():perform_alt(alt) + if err ~= '__ALT__' then + return msg, err + end + end +end + +-- Ensure that no message is received on a subscriber within the +-- given tick budget. Returns true on success (no message), or +-- false, msg, err if a message or real error was observed. +function harness.ensure_no_msg(sub, ctx, max_ticks) + local alt = make_alt_wait(ctx, max_ticks) + while true do + local msg, err = sub:next_msg_op():perform_alt(alt) + if err == '__ALT__' then + -- Alt path taken; keep waiting. + else + -- Either a real message (err == nil) or a real error + -- (timeout/context cancelled). In both cases the caller + -- treats this as a failure of "no message expected". + return false, msg, err + end + end +end + +-- Wait until a predicate becomes true, yielding cooperatively +-- between checks. Returns true on success, or false, reason on +-- timeout or context cancellation. +function harness.wait_until(ctx, predicate, max_ticks) + local ticks = 0 + max_ticks = max_ticks or DEFAULT_MAX_TICKS + + while true do + if predicate() then + return true + end + + if ctx and ctx:err() then + return false, 'context cancelled' + end + + ticks = ticks + 1 + if ticks > max_ticks then + return false, 'timeout' + end + + fiber.yield() + end +end + +return harness + diff --git a/tests/hal/test_core.lua b/tests/hal/test_core.lua index 3ef21bc6..7b7ac1d4 100644 --- a/tests/hal/test_core.lua +++ b/tests/hal/test_core.lua @@ -9,19 +9,25 @@ if is_entry_point then .. "../../src/lua-trie/src/?.lua;" -- trie submodule src .. "../../src/lua-bus/src/?.lua;" -- bus submodule src .. "../../src/?.lua;" -- main src tree + .. "../../?.lua;" -- repo root (for tests.hal.harness) .. "./test_utils/?.lua;" -- shared test utilities .. package.path .. ";/usr/lib/lua/?.lua;/usr/lib/lua/?/init.lua;" .. "./harness/?.lua;" _G._TEST = true -- Enable test exports in source code + local log = require 'services.log' + local rxilog = require 'rxilog' + for _, mode in ipairs(rxilog.modes) do + log[mode.name] = function() end -- no-op logging during tests + end end local luaunit = require 'luaunit' local fiber = require 'fibers.fiber' -local context = require 'fibers.context' -local sleep = require 'fibers.sleep' -local queue = require 'fibers.queue' +local unpack = unpack or table.unpack + +local harness = require 'tests.hal.harness' -- Test harness for HAL configuration, device events, and capability events. @@ -29,34 +35,10 @@ local queue = require 'fibers.queue' TestHalConfig = {} -local function get_env_variables() - local bg_ctx = context.background() - - local ctx = context.with_cancel( - context.with_value(bg_ctx, "service_name", "hal") - ) - - local bus = require 'bus' - - package.loaded['services.hal'] = nil -- force reload to reset state between tests - package.loaded['services.hal.managers.dummy'] = nil -- force reload to reset state between tests - local hal = require 'services.hal' - return hal, ctx, bus.new(), bus.new_msg -end - -local function config_path() - return { 'config', 'hal' } -end - -local function new_hal_env() - local hal, ctx, bus, new_msg = get_env_variables() - local conn = bus:connect() - return hal, ctx, bus, conn, new_msg -end - -local function publish_config(conn, new_msg, payload) - conn:publish(new_msg(config_path(), payload, { retained = true })) -end +local new_hal_env = harness.new_hal_env +local config_path = harness.config_path +local publish_config = harness.publish_config +local channel = require 'fibers.channel' local function assert_no_managers(hal, msg) luaunit.assertNil(next(hal.managers), msg or "Expected HAL to have zero managers") @@ -82,7 +64,7 @@ function TestHalConfig:test_simple_config() publish_config(conn, new_msg, config) luaunit.assertNotNil(dummy_manager_sub, "Expected to subscribe to dummy manager status messages") - local msg, err = dummy_manager_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + local msg, err = harness.wait_for_msg(dummy_manager_sub, ctx) luaunit.assertNil(err, "Expected to receive dummy manager status message") luaunit.assertNotNil(msg, "Expected to receive dummy manager status message") luaunit.assertEquals(msg.payload, "running") @@ -100,8 +82,11 @@ function TestHalConfig:test_nil_config() publish_config(conn, new_msg, config) - sleep.sleep(0.1) -- Give HAL some time to process the config - + local ok, reason = harness.wait_until(ctx, function() + return next(hal.managers) ~= nil + end) + luaunit.assertFalse(ok, "Expected HAL to not create managers for nil config, but wait_until succeeded: " .. + tostring(reason)) assert_no_managers(hal) ctx:cancel("test complete") end @@ -119,8 +104,11 @@ function TestHalConfig:test_empty_managers_config() publish_config(conn, new_msg, config) - sleep.sleep(0.1) -- Give HAL some time to process the config - + local ok, reason = harness.wait_until(ctx, function() + return next(hal.managers) ~= nil + end) + luaunit.assertFalse(ok, "Expected HAL to not create managers for empty managers config, but wait_until succeeded: " .. + tostring(reason)) assert_no_managers(hal) ctx:cancel("test complete") end @@ -134,8 +122,11 @@ function TestHalConfig:test_invalid_type_config() publish_config(conn, new_msg, config) - sleep.sleep(0.1) -- Give HAL some time to process the config - + local ok, reason = harness.wait_until(ctx, function() + return next(hal.managers) ~= nil + end) + luaunit.assertFalse(ok, "Expected HAL to not create managers for invalid type config, but wait_until succeeded: " .. + tostring(reason)) assert_no_managers(hal) ctx:cancel("test complete") end @@ -152,8 +143,11 @@ function TestHalConfig:test_no_managers_config() publish_config(conn, new_msg, config) - sleep.sleep(0.1) -- Give HAL some time to process the config - + local ok, reason = harness.wait_until(ctx, function() + return next(hal.managers) ~= nil + end) + luaunit.assertFalse(ok, "Expected HAL to not create managers when managers key is missing, but wait_until succeeded: " .. + tostring(reason)) assert_no_managers(hal) ctx:cancel("test complete") end @@ -173,8 +167,11 @@ function TestHalConfig:test_invalid_manager() publish_config(conn, new_msg, config) - sleep.sleep(0.1) -- Give HAL some time to process the config - + local ok, reason = harness.wait_until(ctx, function() + return hal.managers.invalid_manager_name ~= nil + end) + luaunit.assertFalse(ok, + "Expected HAL to ignore invalid manager name, but wait_until succeeded: " .. tostring(reason)) luaunit.assertNil(hal.managers.invalid_manager_name, "Expected HAL to ignore invalid manager name") ctx:cancel("test complete") end @@ -197,7 +194,7 @@ function TestHalConfig:test_remove_manager() publish_config(conn, new_msg, config) luaunit.assertNotNil(dummy_manager_sub, "Expected to subscribe to dummy manager status messages") - local msg, err = dummy_manager_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + local msg, err = harness.wait_for_msg(dummy_manager_sub, ctx) luaunit.assertNil(err, "Expected to receive dummy manager status message" .. (err and (": " .. tostring(err)) or "")) luaunit.assertNotNil(msg, "Expected to receive dummy manager status message") luaunit.assertEquals(msg.payload, "running") @@ -226,7 +223,10 @@ function TestHalConfig:test_remove_manager() ) ) - sleep.sleep(0.1) -- Give HAL some time to process the config + local ok, reason = harness.wait_until(ctx, function() + return (next(hal.managers) == nil or hal.managers.dummy == nil) and done + end) + luaunit.assertTrue(ok, "Expected HAL to remove dummy manager, but wait_until failed: " .. tostring(reason)) luaunit.assertNil(hal.managers.dummy, "Expected HAL to have removed dummy manager") luaunit.assertNil(next(hal.managers), "Expected HAL to have zero managers") luaunit.assertTrue(done, "Expected dummy manager context to be done") -- manager should revieve a cancel signal @@ -254,7 +254,7 @@ function TestHalConfig:test_reconfigure_manager() publish_config(conn, new_msg, config) - local msg, err = dummy_manager_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + local msg, err = harness.wait_for_msg(dummy_manager_sub, ctx) luaunit.assertNil(err, "Expected to receive dummy manager status message") luaunit.assertNotNil(msg, "Expected to receive dummy manager status message") luaunit.assertEquals(msg.payload, "running") @@ -280,7 +280,11 @@ function TestHalConfig:test_reconfigure_manager() ) ) - sleep.sleep(0.1) -- Give HAL some time to process the config + local ok, reason = harness.wait_until(ctx, function() + return hal.managers.dummy ~= nil and hal.managers.dummy.test_arg == new_test_arg + end) + luaunit.assertTrue(ok, + "Expected HAL to reconfigure dummy manager, but wait_until failed: " .. tostring(reason)) luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to still have dummy manager after reconfiguration") luaunit.assertEquals(hal.managers.dummy.test_arg, new_test_arg, "Expected dummy manager to have applied updated config") @@ -305,7 +309,7 @@ function TestHalConfig:test_invalid_config_does_not_affect_managers() publish_config(conn, new_msg, config) luaunit.assertNotNil(dummy_manager_sub, "Expected to subscribe to dummy manager status messages") - local msg, err = dummy_manager_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + local msg, err = harness.wait_for_msg(dummy_manager_sub, ctx) luaunit.assertNil(err, "Expected to receive dummy manager status message") luaunit.assertNotNil(msg, "Expected to receive dummy manager status message") luaunit.assertEquals(msg.payload, "running") @@ -318,7 +322,12 @@ function TestHalConfig:test_invalid_config_does_not_affect_managers() publish_config(conn, new_msg, invalid_config) - sleep.sleep(0.1) -- Give HAL some time to process the config + local ok, reason = harness.wait_until(ctx, function() + return next(hal.managers) == nil + end) + luaunit.assertFalse(ok, + "Expected HAL to keep dummy manager after invalid config, but wait_until reported success: " .. + tostring(reason)) luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to still have dummy manager after invalid config") luaunit.assertEquals(hal.managers.dummy.test_arg, test_arg, "Expected dummy manager to retain initial config after invalid config") @@ -328,7 +337,13 @@ function TestHalConfig:test_invalid_config_does_not_affect_managers() some_other_config = true } publish_config(conn, new_msg, missing_managers_config) - sleep.sleep(0.1) -- Give HAL some time to process the config + + ok, reason = harness.wait_until(ctx, function() + return next(hal.managers) == nil + end) + luaunit.assertFalse(ok, + "Expected HAL to keep dummy manager after missing managers config, but wait_until reported success: " .. + tostring(reason)) luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to still have dummy manager after missing managers config") luaunit.assertEquals(hal.managers.dummy.test_arg, test_arg, "Expected dummy manager to retain initial config after missing managers config") @@ -359,7 +374,7 @@ function TestHalConfig:test_partial_manager_removal() -- Wait for dummy manager 1 luaunit.assertNotNil(dummy_manager_sub, "Expected to subscribe to dummy manager status messages") - local msg, err = dummy_manager_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + local msg, err = harness.wait_for_msg(dummy_manager_sub, ctx) luaunit.assertNil(err, "Expected to receive dummy manager status message") luaunit.assertNotNil(msg, "Expected to receive dummy manager status message") luaunit.assertEquals(msg.payload, "running") @@ -367,7 +382,7 @@ function TestHalConfig:test_partial_manager_removal() -- Wait for dummy manager 2 luaunit.assertNotNil(dummy2_manager_sub, "Expected to subscribe to dummy2 manager status messages") - local msg2, err2 = dummy2_manager_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + local msg2, err2 = harness.wait_for_msg(dummy2_manager_sub, ctx) luaunit.assertNil(err2, "Expected to receive dummy2 manager status message") luaunit.assertNotNil(msg2, "Expected to receive dummy2 manager status message") luaunit.assertEquals(msg2.payload, "running") @@ -389,7 +404,13 @@ function TestHalConfig:test_partial_manager_removal() { retained = true } ) ) - sleep.sleep(0.1) -- Give HAL some time to process the config + + local ok, reason = harness.wait_until(ctx, function() + return hal.managers.dummy ~= nil and hal.managers.dummy.test_arg == "value2" and + hal.managers.dummy2 == nil + end) + luaunit.assertTrue(ok, + "Expected HAL to apply partial manager removal, but wait_until failed: " .. tostring(reason)) -- HAL removes managers that are not present in the new -- config, so dummy2 should be removed and dummy kept. luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to still have dummy manager") @@ -421,11 +442,12 @@ end local function assert_no_device_event(device_event_q, hal_device_event_sub, ctx, event, description) device_event_q:put(event) - local msg, err = hal_device_event_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + local msg, err = harness.wait_for_msg(hal_device_event_sub, ctx) luaunit.assertNil(msg, "Expected to not receive HAL device event message for " .. description) - luaunit.assertNotNil(err, - "Expected to receive timeout error for " .. description) + luaunit.assertEquals(err, 'timeout', + "Expected timeout when no HAL device event should be received for " .. description .. + ", got: " .. tostring(err)) end function TestHalDeviceEvent:test_device_add_event() @@ -440,7 +462,7 @@ function TestHalDeviceEvent:test_device_add_event() device_event_q:put(device_add_event) -- Next wait for a device event and check the value in the payload - local msg, err = hal_device_event_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + local msg, err = harness.wait_for_msg(hal_device_event_sub, ctx) luaunit.assertNil(err, "Expected to receive HAL device event message") luaunit.assertNotNil(msg, "Expected to receive HAL device event message") luaunit.assertNotNil(msg.payload, "Expected HAL device event message to have data payload") @@ -464,14 +486,14 @@ function TestHalDeviceEvent:test_device_remove_event() device_event_q:put(device_remove_event) -- First wait for the add event to be received - local msg, err = hal_device_event_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + local msg, err = harness.wait_for_msg(hal_device_event_sub, ctx) luaunit.assertNil(err, "Expected to receive HAL device event message") luaunit.assertNotNil(msg, "Expected to receive HAL device event message") luaunit.assertNotNil(msg.payload, "Expected HAL device event message to have data payload") luaunit.assertEquals(msg.payload.connected, true, "Expected device connected state to be true") -- Now wait for the remove event and check the value in the payload - local msg, err = hal_device_event_sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + local msg, err = harness.wait_for_msg(hal_device_event_sub, ctx) luaunit.assertNil(err, "Expected to receive HAL device event message") luaunit.assertNotNil(msg, "Expected to receive HAL device event message") luaunit.assertNotNil(msg.payload, "Expected HAL device event message to have data payload") @@ -540,9 +562,6 @@ end TestHalDeviceCapabilityEvent = {} --- These helpers are kept for future capability control tests, --- where capability endpoints will be invoked directly. - local function wrap_result(...) return { result = { ... }, err = nil } end @@ -560,10 +579,10 @@ local function make_dummy_capability_list(length) no_args = function() return wrap_result(i, "no_args_endpoint") end, - single_arg = function(args) + single_arg = function(_, args) return wrap_result(i, "single_arg_endpoint", args, #args) end, - multi_arg = function(args) + multi_arg = function(_, args) return wrap_result(i, "multi_arg_endpoint", args, #args) end, error_fn = function() @@ -604,9 +623,14 @@ local function capability_event_path(capability_index) return { 'hal', 'capability', 'capability' .. capability_index, tostring(capability_index) } end +local function all_capability_event_path() + -- Topic used by HAL for all capability connection events + return { 'hal', 'capability', '+', '+' } +end + local function expect_capability_event(sub, ctx, expected_event, label) local suffix = label and (" " .. label) or "" - local msg, err = sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + local msg, err = harness.wait_for_msg(sub, ctx) luaunit.assertNil(err, "Expected to receive HAL capability event message" .. suffix) luaunit.assertNotNil(msg, "Expected to receive HAL capability event message" .. suffix) assert_capability_event(msg.payload, expected_event) @@ -614,13 +638,14 @@ end local function assert_no_capability_event(ctx, sub, label) local suffix = label and (" " .. label) or "" - local msg, err = sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + local msg, err = harness.wait_for_msg(sub, ctx) luaunit.assertNil(msg, "Expected to not receive HAL capability event message" .. suffix) - luaunit.assertNotNil(err, "Expected to receive timeout error" .. suffix) + luaunit.assertEquals(err, 'timeout', "Expected timeout when no capability event should be received" .. suffix .. + ", got: " .. tostring(err)) end local function expect_retained_drop_event(ctx, sub) - local msg, err = sub:next_msg_with_context(context.with_timeout(ctx, 0.1)) + local msg, err = harness.wait_for_msg(sub, ctx) luaunit.assertNil(err, "Expected to receive a message") luaunit.assertNotNil(msg, "Expected to receive a message") luaunit.assertNil(msg.payload, "Expected retained drop message to have nil payload") @@ -638,11 +663,28 @@ function TestHalDeviceCapabilityEvent:test_device_capability_add_event() local expected_event = make_expected_capability_event(device_name, 1, true) - -- Next wait for a capability info event and check the value in the payload + -- Next wait for a capability event and check the value in the payload expect_capability_event(hal_capability_info_sub, ctx, expected_event, "for capability1") ctx:cancel("test complete") end +function TestHalDeviceCapabilityEvent:test_device_no_capability() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_name = 'dummy_no_capability_device' + local capabilities = {} -- empty capabilities + local device_event = make_dummy_device_event(true, device_name, capabilities) + local hal_capability_info_sub = conn:subscribe(all_capability_event_path()) + + hal.device_event_q:put(device_event) + + -- No capability event should be published + assert_no_capability_event(ctx, hal_capability_info_sub, + "Expect no capability events for device with no capabilities") + + ctx:cancel("test complete") +end + function TestHalDeviceCapabilityEvent:test_device_multi_capability_event() local hal, ctx, bus, conn, new_msg = new_hal_env() hal:start(ctx, bus:connect()) @@ -748,6 +790,39 @@ function TestHalDeviceCapabilityEvent:test_device_capability_duplicate_id_event( ctx:cancel("test complete") end +function TestHalDeviceCapabilityEvent:test_add_remove_add_capability_event() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_name = 'dummy_add_remove_add_device' + local capabilities = make_dummy_capability_list(1) + local device_add_event = make_dummy_device_event(true, device_name, capabilities) + local device_remove_event = make_dummy_device_event(false, device_name, nil) + + local cap_sub = conn:subscribe(capability_event_path(1)) + + -- 1. Add event + hal.device_event_q:put(device_add_event) + + local expected_add_event = make_expected_capability_event(device_name, 1, true) + expect_capability_event(cap_sub, ctx, expected_add_event, "for capability1 add") + + -- 2. Remove event + hal.device_event_q:put(device_remove_event) + + -- A nil payload is sent first to remove retained values under the topic + expect_retained_drop_event(ctx, cap_sub) + + local expected_remove_event = make_expected_capability_event(device_name, 1, false) + expect_capability_event(cap_sub, ctx, expected_remove_event, "for capability1 remove") + + -- 3. Add event again + hal.device_event_q:put(device_add_event) + + expect_capability_event(cap_sub, ctx, expected_add_event, "for capability1 add again") + + ctx:cancel("test complete") +end + function TestHalDeviceCapabilityEvent:test_device_capability_nil_control_event() local hal, ctx, bus, conn, new_msg = new_hal_env() hal:start(ctx, bus:connect()) @@ -766,6 +841,357 @@ function TestHalDeviceCapabilityEvent:test_device_capability_nil_control_event() ctx:cancel("test complete") end +TestHalCapabilityControl = {} + +local function capability_control_path(capability_type, capability_index, endpoint) + return { 'hal', 'capability', capability_type, tostring(capability_index), 'control', endpoint } +end + +function TestHalCapabilityControl:test_capability_control_endpoints() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local capabilities = make_dummy_capability_list(1) + local device_event = make_dummy_device_event(true, "test_device", capabilities) + local cap_sub = conn:subscribe(capability_event_path(1)) + + hal.device_event_q:put(device_event) + + local expected_event = make_expected_capability_event("test_device", 1, true) + expect_capability_event(cap_sub, ctx, expected_event, "for capability1 add") -- wait for capability to appear + + -- 1. Test no_args endpoint + local cap_control_sub = conn:request(new_msg( + capability_control_path("capability1", 1, "no_args") + )) + + local response, err = harness.wait_for_msg(cap_control_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability control response") + local expected_result = wrap_result(1, "no_args_endpoint") + luaunit.assertEquals(response.payload, expected_result, "Expected capability control response to match") + + -- 2. Test single_arg endpoint + cap_control_sub = conn:request(new_msg( + capability_control_path("capability1", 1, "single_arg"), + { "arg1_value" } + )) + response, err = harness.wait_for_msg(cap_control_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability control response") + expected_result = wrap_result(1, "single_arg_endpoint", { "arg1_value" }, 1) + luaunit.assertEquals(response.payload, expected_result, "Expected capability control response to match") + + -- 3. Test multi_arg endpoint + cap_control_sub = conn:request(new_msg( + capability_control_path("capability1", 1, "multi_arg"), + { "arg1", "arg2", "arg3" } + )) + response, err = harness.wait_for_msg(cap_control_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability control response") + expected_result = wrap_result(1, "multi_arg_endpoint", { "arg1", "arg2", "arg3" }, 3) + luaunit.assertEquals(response.payload, expected_result, "Expected capability control response to match") + + -- 4. Test error_fn endpoint + cap_control_sub = conn:request(new_msg( + capability_control_path("capability1", 1, "error_fn") + )) + response, err = harness.wait_for_msg(cap_control_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability control response") + expected_result = wrap_error("Capability function error") + luaunit.assertEquals(response.payload, expected_result, "Expected capability control response to match") + + ctx:cancel("test complete") +end + +function TestHalCapabilityControl:test_invalid_capability_control_endpoints() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local capabilities = make_dummy_capability_list(1) + local device_event = make_dummy_device_event(true, "test_device_invalid_control", capabilities) + local cap_sub = conn:subscribe(capability_event_path(1)) + + hal.device_event_q:put(device_event) + + local expected_event = make_expected_capability_event("test_device_invalid_control", 1, true) + expect_capability_event(cap_sub, ctx, expected_event, "for capability1 add") -- wait for capability to appear + + -- 1. Non-existent function + local cap_control_sub = conn:request(new_msg( + capability_control_path("capability1", 1, "invalid_endpoint") + )) + + local response, err = harness.wait_for_msg(cap_control_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability control response") + local expected_result = wrap_error('endpoint does not exist') + luaunit.assertEquals(response.payload, expected_result, "Expected capability control response to match") + + -- 2. Non-existent capability index + cap_control_sub = conn:request(new_msg( + capability_control_path("capability1", 999, "no_args") + )) + + response, err = harness.wait_for_msg(cap_control_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability control response") + expected_result = wrap_error('capability instance does not exist') + luaunit.assertEquals(response.payload, expected_result, "Expected capability control response to match") + + -- 3. Non-existent capability type + cap_control_sub = conn:request(new_msg( + capability_control_path("invalid_capability", 1, "no_args") + )) + response, err = harness.wait_for_msg(cap_control_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability control response") + expected_result = wrap_error('capability does not exist') + luaunit.assertEquals(response.payload, expected_result, "Expected capability control response to match") + ctx:cancel("test complete") +end + +function TestHalCapabilityControl:test_no_endpoint_on_removal() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_name = 'dummy_control_remove_device' + local capabilities = make_dummy_capability_list(1) + local device_add_event = make_dummy_device_event(true, device_name, capabilities) + local device_remove_event = make_dummy_device_event(false, device_name, nil) + + local cap_sub = conn:subscribe(capability_event_path(1)) + + -- 1. Add event + hal.device_event_q:put(device_add_event) + + local expected_add_event = make_expected_capability_event(device_name, 1, true) + expect_capability_event(cap_sub, ctx, expected_add_event, "for capability1 add") + + -- 2. Remove event + hal.device_event_q:put(device_remove_event) + + -- A nil payload is sent first to remove retained values under the topic + expect_retained_drop_event(ctx, cap_sub) + + local expected_remove_event = make_expected_capability_event(device_name, 1, false) + expect_capability_event(cap_sub, ctx, expected_remove_event, "for capability1 remove") + + -- Now try to call an endpoint on the removed capability + local cap_control_sub = conn:request(new_msg( + capability_control_path("capability1", 1, "no_args") + )) + + local response, err = harness.wait_for_msg(cap_control_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability control response") + local expected_result = wrap_error('capability instance does not exist') + luaunit.assertEquals(response.payload, expected_result, "Expected capability control response to match") + + ctx:cancel("test complete") +end + +function TestHalCapabilityControl:test_publish_control() + local hal, ctx, bus, conn, new_msg = new_hal_env() + local ch = channel.new() + hal:start(ctx, bus:connect()) + local capabilities = make_dummy_capability_list(1) + -- New endpoint to detect run of endpoint + capabilities["capability1"].control["trigger_channel"] = function(_, args) + ch:put(args[1]) + return wrap_result("") -- we won't receive this + end + local device_event = make_dummy_device_event(true, "test_device_publish_control", capabilities) + local cap_sub = conn:subscribe(capability_event_path(1)) + + hal.device_event_q:put(device_event) + + local expected_event = make_expected_capability_event("test_device_publish_control", 1, true) + expect_capability_event(cap_sub, ctx, expected_event, "for capability1 add") -- wait for capability to appear + + -- Now publish a control message directly to the capability control topic + conn:publish(new_msg( + capability_control_path("capability1", 1, "trigger_channel"), + { 42 } + )) + -- Wait for the channel to be triggered using cooperative waiting + local received + fiber.spawn(function() + received = ch:get() + end) + local ok, reason = harness.wait_until(ctx, function() + return received ~= nil + end) + luaunit.assertTrue(ok, + "Expected capability control endpoint to trigger channel, but wait_until failed: " .. tostring(reason)) + luaunit.assertEquals(received, 42, "Expected capability control endpoint to trigger channel with argument") +end + +TestHalCapabilityInfo = {} + +local function capability_info_path(type, id, endpoints) + if endpoints == nil then endpoints = {} end + return { 'hal', 'capability', type, id, 'info', unpack(endpoints) } +end + +function TestHalCapabilityInfo:test_simple_info() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local info_q = hal.capability_info_q + local info_sub = conn:subscribe(capability_info_path("dummy", "1")) + + local info = "test" + info_q:put({ + type = "dummy", + id = "1", + sub_topic = {}, + endpoints = "single", + info = info, + }) + + local msg, err = harness.wait_for_msg(info_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability info message") + luaunit.assertNotNil(msg, "Expected to receive capability info message") + luaunit.assertEquals(msg.payload, info, "Expected capability info payload to match") + ctx:cancel("test complete") +end + +function TestHalCapabilityInfo:test_tabled_info() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local info_q = hal.capability_info_q + local info_sub = conn:subscribe(capability_info_path("dummy", "2")) + local no_info_sub_1 = conn:subscribe(capability_info_path("dummy", "2", { "field1" })) + local no_info_sub_2 = conn:subscribe(capability_info_path("dummy", "2", { "field2" })) + + local info = { + field1 = "value1", + field2 = 42, + } + info_q:put({ + type = "dummy", + id = "2", + sub_topic = {}, + endpoints = "single", + info = info, + }) + + local msg, err = harness.wait_for_msg(info_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability info message") + luaunit.assertNotNil(msg, "Expected to receive capability info message") + luaunit.assertEquals(msg.payload, info, "Expected capability info payload to match") + + local msg2, err2 = harness.wait_for_msg(no_info_sub_1, ctx) + luaunit.assertNil(msg2, "Expected to not receive capability info message with subtopic") + luaunit.assertEquals(err2, 'timeout', + "Expected timeout with subtopic for missing capability info, got: " .. tostring(err2)) + + local msg3, err3 = harness.wait_for_msg(no_info_sub_2, ctx) + luaunit.assertNil(msg3, "Expected to not receive capability info message with subtopic") + luaunit.assertEquals(err3, 'timeout', + "Expected timeout with subtopic for missing capability info, got: " .. tostring(err3)) + + ctx:cancel("test complete") +end + +function TestHalCapabilityInfo:test_info_with_subtopic() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local info_q = hal.capability_info_q + local info_sub = conn:subscribe(capability_info_path("dummy", "3", { "subtopic1", "subtopic2" })) + + local info = "subtopic_info" + info_q:put({ + type = "dummy", + id = "3", + sub_topic = { "subtopic1", "subtopic2" }, + endpoints = "single", + info = info, + }) + + local msg, err = harness.wait_for_msg(info_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability info message") + luaunit.assertNotNil(msg, "Expected to receive capability info message") + luaunit.assertEquals(msg.payload, info, "Expected capability info payload to match") + ctx:cancel("test complete") +end + +function TestHalCapabilityInfo:test_tabled_info_publish_multiple() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local info_q = hal.capability_info_q + local info_sub_1 = conn:subscribe(capability_info_path("dummy", "3", { "field1" })) + local info_sub_2 = conn:subscribe(capability_info_path("dummy", "3", { "field2" })) + local no_info_sub = conn:subscribe(capability_info_path("dummy", "3")) + + local info = { + field1 = "value1", + field2 = 42, + } + info_q:put({ + type = "dummy", + id = "3", + endpoints = "multiple", + info = info, + }) + + local msg, err = harness.wait_for_msg(info_sub_1, ctx) + luaunit.assertNil(err, "Expected to receive capability info message") + luaunit.assertNotNil(msg, "Expected to receive capability info message") + luaunit.assertEquals(msg.payload, info.field1, "Expected capability info payload to match") + + local msg2, err2 = harness.wait_for_msg(info_sub_2, ctx) + luaunit.assertNil(err2, "Expected to receive capability info message") + luaunit.assertNotNil(msg2, "Expected to receive capability info message") + luaunit.assertEquals(msg2.payload, info.field2, "Expected capability info payload to match") + + local msg3, err3 = harness.wait_for_msg(no_info_sub, ctx) + luaunit.assertNil(msg3, "Expected to not receive capability info message without subtopic") + luaunit.assertEquals(err3, 'timeout', + "Expected timeout without subtopic for missing capability info, got: " .. tostring(err3)) + + ctx:cancel("test complete") +end + +function TestHalCapabilityInfo:test_info_invalid() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local info_q = hal.capability_info_q + local info_sub = conn:subscribe(capability_info_path("dummy", "4")) + + -- Missing type + info_q:put({ + id = "4", + sub_topic = {}, + endpoints = "single", + info = "invalid_info", + }) + + local msg, err = harness.wait_for_msg(info_sub, ctx) + luaunit.assertNil(msg, "Expected to not receive capability info message with missing type") + luaunit.assertEquals(err, 'timeout', + "Expected timeout with missing type for capability info, got: " .. tostring(err)) + + -- Missing id + info_q:put({ + type = "dummy", + sub_topic = {}, + endpoints = "single", + info = "invalid_info", + }) + + msg, err = harness.wait_for_msg(info_sub, ctx) + luaunit.assertNil(msg, "Expected to not receive capability info message with missing id") + luaunit.assertEquals(err, 'timeout', + "Expected timeout with missing id for capability info, got: " .. tostring(err)) + + -- Missing endpoints + info_q:put({ + type = "dummy", + id = "4", + sub_topic = {}, + info = "invalid_info", + }) + + msg, err = harness.wait_for_msg(info_sub, ctx) + luaunit.assertNil(msg, "Expected to not receive capability info message with missing endpoints") + luaunit.assertEquals(err, 'timeout', + "Expected timeout with missing endpoints for capability info, got: " .. tostring(err)) + + ctx:cancel("test complete") +end + local function main() fiber.spawn(function() luaunit.LuaUnit.run() From bf4cd3802d09075db2e5bb9d40f5f084664a4852 Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Tue, 16 Dec 2025 12:05:06 +0000 Subject: [PATCH 05/20] small style improvements --- tests/hal/harness.lua | 19 ------------------- tests/hal/test_core.lua | 25 ++++++++----------------- 2 files changed, 8 insertions(+), 36 deletions(-) diff --git a/tests/hal/harness.lua b/tests/hal/harness.lua index 907a61f7..08ff2cf4 100644 --- a/tests/hal/harness.lua +++ b/tests/hal/harness.lua @@ -1,6 +1,5 @@ local fiber = require 'fibers.fiber' local context = require 'fibers.context' -local op = require 'fibers.op' local harness = {} @@ -82,24 +81,6 @@ function harness.wait_for_msg(sub, ctx, max_ticks) end end --- Ensure that no message is received on a subscriber within the --- given tick budget. Returns true on success (no message), or --- false, msg, err if a message or real error was observed. -function harness.ensure_no_msg(sub, ctx, max_ticks) - local alt = make_alt_wait(ctx, max_ticks) - while true do - local msg, err = sub:next_msg_op():perform_alt(alt) - if err == '__ALT__' then - -- Alt path taken; keep waiting. - else - -- Either a real message (err == nil) or a real error - -- (timeout/context cancelled). In both cases the caller - -- treats this as a failure of "no message expected". - return false, msg, err - end - end -end - -- Wait until a predicate becomes true, yielding cooperatively -- between checks. Returns true on success, or false, reason on -- timeout or context cancellation. diff --git a/tests/hal/test_core.lua b/tests/hal/test_core.lua index 7b7ac1d4..32b9b910 100644 --- a/tests/hal/test_core.lua +++ b/tests/hal/test_core.lua @@ -38,7 +38,6 @@ TestHalConfig = {} local new_hal_env = harness.new_hal_env local config_path = harness.config_path local publish_config = harness.publish_config -local channel = require 'fibers.channel' local function assert_no_managers(hal, msg) luaunit.assertNil(next(hal.managers), msg or "Expected HAL to have zero managers") @@ -85,8 +84,7 @@ function TestHalConfig:test_nil_config() local ok, reason = harness.wait_until(ctx, function() return next(hal.managers) ~= nil end) - luaunit.assertFalse(ok, "Expected HAL to not create managers for nil config, but wait_until succeeded: " .. - tostring(reason)) + luaunit.assertFalse(ok, "Expected HAL to not create managers for nil config") assert_no_managers(hal) ctx:cancel("test complete") end @@ -107,8 +105,7 @@ function TestHalConfig:test_empty_managers_config() local ok, reason = harness.wait_until(ctx, function() return next(hal.managers) ~= nil end) - luaunit.assertFalse(ok, "Expected HAL to not create managers for empty managers config, but wait_until succeeded: " .. - tostring(reason)) + luaunit.assertFalse(ok, "Expected HAL to not create managers for empty managers config") assert_no_managers(hal) ctx:cancel("test complete") end @@ -125,8 +122,7 @@ function TestHalConfig:test_invalid_type_config() local ok, reason = harness.wait_until(ctx, function() return next(hal.managers) ~= nil end) - luaunit.assertFalse(ok, "Expected HAL to not create managers for invalid type config, but wait_until succeeded: " .. - tostring(reason)) + luaunit.assertFalse(ok, "Expected HAL to not create managers for invalid type config") assert_no_managers(hal) ctx:cancel("test complete") end @@ -146,8 +142,7 @@ function TestHalConfig:test_no_managers_config() local ok, reason = harness.wait_until(ctx, function() return next(hal.managers) ~= nil end) - luaunit.assertFalse(ok, "Expected HAL to not create managers when managers key is missing, but wait_until succeeded: " .. - tostring(reason)) + luaunit.assertFalse(ok, "Expected HAL to not create managers when managers key is missing") assert_no_managers(hal) ctx:cancel("test complete") end @@ -170,8 +165,7 @@ function TestHalConfig:test_invalid_manager() local ok, reason = harness.wait_until(ctx, function() return hal.managers.invalid_manager_name ~= nil end) - luaunit.assertFalse(ok, - "Expected HAL to ignore invalid manager name, but wait_until succeeded: " .. tostring(reason)) + luaunit.assertFalse(ok, "Expected HAL to ignore invalid manager name") luaunit.assertNil(hal.managers.invalid_manager_name, "Expected HAL to ignore invalid manager name") ctx:cancel("test complete") end @@ -325,9 +319,7 @@ function TestHalConfig:test_invalid_config_does_not_affect_managers() local ok, reason = harness.wait_until(ctx, function() return next(hal.managers) == nil end) - luaunit.assertFalse(ok, - "Expected HAL to keep dummy manager after invalid config, but wait_until reported success: " .. - tostring(reason)) + luaunit.assertFalse(ok, "Expected HAL to keep dummy manager after invalid config") luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to still have dummy manager after invalid config") luaunit.assertEquals(hal.managers.dummy.test_arg, test_arg, "Expected dummy manager to retain initial config after invalid config") @@ -341,9 +333,7 @@ function TestHalConfig:test_invalid_config_does_not_affect_managers() ok, reason = harness.wait_until(ctx, function() return next(hal.managers) == nil end) - luaunit.assertFalse(ok, - "Expected HAL to keep dummy manager after missing managers config, but wait_until reported success: " .. - tostring(reason)) + luaunit.assertFalse(ok, "Expected HAL to keep dummy manager after missing managers config") luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to still have dummy manager after missing managers config") luaunit.assertEquals(hal.managers.dummy.test_arg, test_arg, "Expected dummy manager to retain initial config after missing managers config") @@ -984,6 +974,7 @@ end function TestHalCapabilityControl:test_publish_control() local hal, ctx, bus, conn, new_msg = new_hal_env() + local channel = require 'fibers.channel' local ch = channel.new() hal:start(ctx, bus:connect()) local capabilities = make_dummy_capability_list(1) From e120f093df51834159cfa9afbae5928b13476732 Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Wed, 17 Dec 2025 16:13:19 +0000 Subject: [PATCH 06/20] Changed mmcli and qmicli to be injectable for testing backends. Dummy command objects to allow direct hardware control from test level. Templates for generating large tables with mostly boilderplate --- src/services/hal/drivers/modem/mmcli.lua | 105 ++++++++-- src/services/hal/drivers/modem/qmicli.lua | 81 +++++++- tests/hal/harness/backends/mmcli.lua | 123 +++++++++++ tests/hal/harness/backends/qmicli.lua | 81 ++++++++ tests/hal/harness/devices/modem.lua | 99 +++++++++ tests/hal/templates.lua | 238 ++++++++++++++++++++++ tests/utils/ShimCommands.lua | 229 +++++++++++++++++++++ 7 files changed, 930 insertions(+), 26 deletions(-) create mode 100644 tests/hal/harness/backends/mmcli.lua create mode 100644 tests/hal/harness/backends/qmicli.lua create mode 100644 tests/hal/harness/devices/modem.lua create mode 100644 tests/hal/templates.lua create mode 100644 tests/utils/ShimCommands.lua diff --git a/src/services/hal/drivers/modem/mmcli.lua b/src/services/hal/drivers/modem/mmcli.lua index f127961f..efe67ba9 100644 --- a/src/services/hal/drivers/modem/mmcli.lua +++ b/src/services/hal/drivers/modem/mmcli.lua @@ -1,63 +1,133 @@ local exec = require "fibers.exec" -local function monitor_modems() +local backend = {} + +function backend.monitor_modems() return exec.command('mmcli', '-M') end -local function inhibit(device) +function backend.inhibit(device) return exec.command('mmcli', '-m', device, '--inhibit') end -local function connect(ctx, device, connection_string) +function backend.connect(ctx, device, connection_string) connection_string = string.format("--simple-connect=%s", connection_string) return exec.command_context(ctx, 'mmcli', '-m', device, connection_string) end -local function disconnect(ctx, device) +function backend.disconnect(ctx, device) return exec.command_context(ctx, 'mmcli', '-m', device, '--simple-disconnect') end -local function reset(ctx, device) +function backend.reset(ctx, device) return exec.command_context(ctx, 'mmcli', '-m', device, '-r') end -local function enable(ctx, device) +function backend.enable(ctx, device) return exec.command_context(ctx, 'mmcli', '-m', device, '-e') end -local function disable(ctx, device) +function backend.disable(ctx, device) return exec.command_context(ctx, 'mmcli', '-m', device, '-d') end -local function monitor_state(device) +function backend.monitor_state(device) return exec.command('mmcli', '-m', device, '-w') end -local function information(ctx, device) +function backend.information(ctx, device) return exec.command_context(ctx, 'mmcli', '-J', '-m', device) end -local function sim_information(ctx, device) +function backend.sim_information(ctx, device) return exec.command_context(ctx, 'mmcli', '-J', '-i', device) end -local function location_status(ctx, device) +function backend.location_status(ctx, device) return exec.command_context(ctx, 'mmcli', '-J', '-m', device, '--location-status') end -local function signal_setup(ctx, device, rate) +function backend.signal_setup(ctx, device, rate) return exec.command_context(ctx, 'mmcli', '-m', device, '--signal-setup=' .. tostring(rate)) end -local function signal_get(ctx, device) +function backend.signal_get(ctx, device) return exec.command_context(ctx, 'mmcli', '-J', '-m', device, '--signal-get') end -local function three_gpp_set_initial_eps_bearer_settings(ctx, device, settings) +function backend.three_gpp_set_initial_eps_bearer_settings(ctx, device, settings) local settings_string = string.format("--3gpp-set-initial-eps-bearer-settings=%s", settings) return exec.command_context(ctx, 'mmcli', '-m', device, settings_string) end -return { +local function monitor_modems() + return backend.monitor_modems() +end + +local function inhibit(ctx, device) + return backend.inhibit(device) +end + +local function connect(ctx, device, connection_string) + return backend.connect(ctx, device, connection_string) +end + +local function disconnect(ctx, device) + return backend.disconnect(ctx, device) +end + +local function reset(ctx, device) + return backend.reset(ctx, device) +end + +local function enable(ctx, device) + return backend.enable(ctx, device) +end + +local function disable(ctx, device) + return backend.disable(ctx, device) +end + +local function monitor_state(device) + return backend.monitor_state(device) +end + +local function information(ctx, device) + return backend.information(ctx, device) +end + +local function sim_information(ctx, device) + return backend.sim_information(ctx, device) +end + +local function location_status(ctx, device) + return backend.location_status(ctx, device) +end + +local function signal_setup(ctx, device, rate) + return backend.signal_setup(ctx, device, rate) +end + +local function signal_get(ctx, device) + return backend.signal_get(ctx, device) +end + +local function three_gpp_set_initial_eps_bearer_settings(ctx, device, settings) + return backend.three_gpp_set_initial_eps_bearer_settings(ctx, device, settings) +end + +local function use_backend(new_backend) + if not new_backend then + return "No backend provided" + end + for name, _ in pairs(backend) do + if not new_backend[name] then + return "New backend does not implement function: " .. name + end + end + backend = new_backend +end + +local mmcli_package = { monitor_modems = monitor_modems, inhibit = inhibit, connect = connect, @@ -72,4 +142,9 @@ return { signal_setup = signal_setup, signal_get = signal_get, three_gpp_set_initial_eps_bearer_settings = three_gpp_set_initial_eps_bearer_settings, + use_backend = use_backend -- function to swap out backend implementations } + +package.loaded['services.hal.drivers.modem.mmcli'] = mmcli_package -- singleton + +return mmcli_package diff --git a/src/services/hal/drivers/modem/qmicli.lua b/src/services/hal/drivers/modem/qmicli.lua index a27405df..490485ca 100644 --- a/src/services/hal/drivers/modem/qmicli.lua +++ b/src/services/hal/drivers/modem/qmicli.lua @@ -1,40 +1,94 @@ local exec = require "fibers.exec" -local function uim_get_card_status(ctx, port) +-- Default backend implementation using qmicli commands +local backend = {} + +function backend.uim_get_card_status(ctx, port) return exec.command_context(ctx, "qmicli", "-p", "-d", port, "--uim-get-card-status") end -local function uim_sim_power_off(ctx, port) +function backend.uim_sim_power_off(ctx, port) return exec.command_context(ctx, "qmicli", "-p", "-d", port, "--uim-sim-power-off=1") end -local function uim_sim_power_on(ctx, port) +function backend.uim_sim_power_on(ctx, port) return exec.command_context(ctx, "qmicli", "-p", "-d", port, "--uim-sim-power-on=1") end -local function uim_monitor_slot_status(port) +function backend.uim_monitor_slot_status(port) return exec.command('qmicli', '-p', '-d', port, '--uim-monitor-slot-status') end -local function uim_read_transparent(ctx, port, address_string) +function backend.uim_read_transparent(ctx, port, address_string) local addresses = string.format('--uim-read-transparent=%s', address_string) return exec.command_context(ctx, 'qmicli', '-p', '-d', port, addresses) end -local function nas_get_rf_band_info(ctx, port) +function backend.nas_get_rf_band_info(ctx, port) return exec.command_context(ctx, 'qmicli', '-p', '-d', port, '--nas-get-rf-band-info') end -local function nas_get_home_network(ctx, port) + +function backend.nas_get_home_network(ctx, port) return exec.command_context(ctx, 'qmicli', '-p', '-d', port, '--nas-get-home-network') end -local function nas_get_serving_system(ctx, port) +function backend.nas_get_serving_system(ctx, port) return exec.command_context(ctx, 'qmicli', '-p', '-d', port, '--nas-get-serving-system') end -local function nas_get_signal_info(ctx, port) + +function backend.nas_get_signal_info(ctx, port) return exec.command_context(ctx, 'qmicli', '-p', '-d', port, '--nas-get-signal-info') end -return { + +local function uim_get_card_status(ctx, port) + return backend.uim_get_card_status(ctx, port) +end + +local function uim_sim_power_off(ctx, port) + return backend.uim_sim_power_off(ctx, port) +end + +local function uim_sim_power_on(ctx, port) + return backend.uim_sim_power_on(ctx, port) +end + +local function uim_monitor_slot_status(port) + return backend.uim_monitor_slot_status(port) +end + +local function uim_read_transparent(ctx, port, address_string) + return backend.uim_read_transparent(ctx, port, address_string) +end + +local function nas_get_rf_band_info(ctx, port) + return backend.nas_get_rf_band_info(ctx, port) +end + +local function nas_get_home_network(ctx, port) + return backend.nas_get_home_network(ctx, port) +end + +local function nas_get_serving_system(ctx, port) + return backend.nas_get_serving_system(ctx, port) +end + +local function nas_get_signal_info(ctx, port) + return backend.nas_get_signal_info(ctx, port) +end + +local function use_backend(new_backend) + if not new_backend then + return "No backend provided" + end + for name, _ in pairs(backend) do + if not new_backend[name] then + return "New backend does not implement function: " .. name + end + end + backend = new_backend +end + +local qmicli_package = { uim_get_card_status = uim_get_card_status, uim_sim_power_off = uim_sim_power_off, uim_sim_power_on = uim_sim_power_on, @@ -44,5 +98,10 @@ return { nas_get_rf_band_info = nas_get_rf_band_info, nas_get_home_network = nas_get_home_network, nas_get_serving_system = nas_get_serving_system, - nas_get_signal_info = nas_get_signal_info + nas_get_signal_info = nas_get_signal_info, + + use_backend = use_backend -- function to swap out backend implementations } + +package.loaded['services.hal.drivers.modem.qmicli'] = qmicli_package -- singleton +return qmicli_package diff --git a/tests/hal/harness/backends/mmcli.lua b/tests/hal/harness/backends/mmcli.lua new file mode 100644 index 00000000..81c0d76a --- /dev/null +++ b/tests/hal/harness/backends/mmcli.lua @@ -0,0 +1,123 @@ +local channel = require 'fibers.channel' +local commands = require 'tests.utils.ShimCommands' + +local monitor_modems_cmd = commands.new_command() -- We only ever need one instance +local function monitor_modems() + return monitor_modems_cmd +end + +local inhibit_cmds = {} +local function inhibit(device) + inhibit_cmds[device] = commands.new_command() + return inhibit_cmds[device] +end + +local connect_cmds = {} +local function connect(ctx, device, connection_string) + if not connect_cmds[device] then + connect_cmds[device] = {} + end + table.insert(connect_cmds[device], commands.new_command()) + return connect_cmds[device][#connect_cmds[device]] +end + +local disconnect_cmds = {} +local function disconnect(ctx, device) + disconnect_cmds[device] = commands.new_command() + return disconnect_cmds[device] +end + +local reset_cmds = {} +local function reset(ctx, device) + reset_cmds[device] = commands.new_command() + return reset_cmds[device] +end + +local enable_cmds = {} +local function enable(ctx, device) + enable_cmds[device] = commands.new_command() + return enable_cmds[device] +end + +local disable_cmds = {} +local function disable(ctx, device) + disable_cmds[device] = commands.new_command() + return disable_cmds[device] +end + +local monitor_state_cmds = {} +local function monitor_state(device) + monitor_state_cmds[device] = commands.new_command() + return monitor_state_cmds[device] +end + +local information_cmds = {} +local function information(ctx, device) + print(device) + if not information_cmds[device] then + information_cmds[device] = commands.new_static_command() + end + return information_cmds[device] +end + +local sim_information_cmds = {} +local function sim_information(ctx, device) + sim_information_cmds[device] = commands.new_command() + return sim_information_cmds[device] +end + +local location_status_cmds = {} +local function location_status(ctx, device) + location_status_cmds[device] = commands.new_command() + return location_status_cmds[device] +end + +local signal_setup_cmds = {} +local function signal_setup(ctx, device, rate) + signal_setup_cmds[device] = commands.new_command() + return signal_setup_cmds[device] +end + +local signal_get_cmds = {} +local function signal_get(ctx, device) + signal_get_cmds[device] = commands.new_command() + return signal_get_cmds[device] +end + +local three_gpp_set_initial_eps_bearer_settings_cmds = {} +local function three_gpp_set_initial_eps_bearer_settings(ctx, device, settings) + local settings_string = string.format("--3gpp-set-initial-eps-bearer-settings=%s", settings) + three_gpp_set_initial_eps_bearer_settings_cmds[device] = commands.new_command() + return three_gpp_set_initial_eps_bearer_settings_cmds[device] +end + +return { + monitor_modems = monitor_modems, + inhibit = inhibit, + connect = connect, + disconnect = disconnect, + reset = reset, + enable = enable, + disable = disable, + monitor_state = monitor_state, + information = information, + sim_information = sim_information, + location_status = location_status, + signal_setup = signal_setup, + signal_get = signal_get, + three_gpp_set_initial_eps_bearer_settings = three_gpp_set_initial_eps_bearer_settings, + -- Exposed for test inspection + inhibit_cmds = inhibit_cmds, + connect_cmds = connect_cmds, + disconnect_cmds = disconnect_cmds, + reset_cmds = reset_cmds, + enable_cmds = enable_cmds, + disable_cmds = disable_cmds, + monitor_state_cmds = monitor_state_cmds, + information_cmds = information_cmds, + sim_information_cmds = sim_information_cmds, + location_status_cmds = location_status_cmds, + signal_setup_cmds = signal_setup_cmds, + signal_get_cmds = signal_get_cmds, + three_gpp_set_initial_eps_bearer_settings_cmds = three_gpp_set_initial_eps_bearer_settings_cmds +} diff --git a/tests/hal/harness/backends/qmicli.lua b/tests/hal/harness/backends/qmicli.lua new file mode 100644 index 00000000..3a39df54 --- /dev/null +++ b/tests/hal/harness/backends/qmicli.lua @@ -0,0 +1,81 @@ +local commands = require "tests.utils.ShimCommands" + +-- For each function we create and track shim commands per *port*. + +local uim_get_card_status_cmds = {} +local function uim_get_card_status(ctx, port) + uim_get_card_status_cmds[port] = commands.new_command() + return uim_get_card_status_cmds[port] +end + +local uim_sim_power_off_cmds = {} +local function uim_sim_power_off(ctx, port) + uim_sim_power_off_cmds[port] = commands.new_command() + return uim_sim_power_off_cmds[port] +end + +local uim_sim_power_on_cmds = {} +local function uim_sim_power_on(ctx, port) + uim_sim_power_on_cmds[port] = commands.new_command() + return uim_sim_power_on_cmds[port] +end + +local uim_monitor_slot_status_cmds = {} +local function uim_monitor_slot_status(port) + uim_monitor_slot_status_cmds[port] = commands.new_command() + return uim_monitor_slot_status_cmds[port] +end + +local uim_read_transparent_cmds = {} +local function uim_read_transparent(ctx, port, address_string) + uim_read_transparent_cmds[port] = commands.new_command() + return uim_read_transparent_cmds[port] +end + +local nas_get_rf_band_info_cmds = {} +local function nas_get_rf_band_info(ctx, port) + nas_get_rf_band_info_cmds[port] = commands.new_command() + return nas_get_rf_band_info_cmds[port] +end + +local nas_get_home_network_cmds = {} +local function nas_get_home_network(ctx, port) + nas_get_home_network_cmds[port] = commands.new_command() + return nas_get_home_network_cmds[port] +end + +local nas_get_serving_system_cmds = {} +local function nas_get_serving_system(ctx, port) + nas_get_serving_system_cmds[port] = commands.new_command() + return nas_get_serving_system_cmds[port] +end + +local nas_get_signal_info_cmds = {} +local function nas_get_signal_info(ctx, port) + nas_get_signal_info_cmds[port] = commands.new_command() + return nas_get_signal_info_cmds[port] +end + +return { + uim_get_card_status = uim_get_card_status, + uim_sim_power_off = uim_sim_power_off, + uim_sim_power_on = uim_sim_power_on, + uim_monitor_slot_status = uim_monitor_slot_status, + uim_read_transparent = uim_read_transparent, + + nas_get_rf_band_info = nas_get_rf_band_info, + nas_get_home_network = nas_get_home_network, + nas_get_serving_system = nas_get_serving_system, + nas_get_signal_info = nas_get_signal_info, + + -- Exposed for test inspection, keyed by port + uim_get_card_status_cmds = uim_get_card_status_cmds, + uim_sim_power_off_cmds = uim_sim_power_off_cmds, + uim_sim_power_on_cmds = uim_sim_power_on_cmds, + uim_monitor_slot_status_cmds = uim_monitor_slot_status_cmds, + uim_read_transparent_cmds = uim_read_transparent_cmds, + nas_get_rf_band_info_cmds = nas_get_rf_band_info_cmds, + nas_get_home_network_cmds = nas_get_home_network_cmds, + nas_get_serving_system_cmds = nas_get_serving_system_cmds, + nas_get_signal_info_cmds = nas_get_signal_info_cmds, +} diff --git a/tests/hal/harness/devices/modem.lua b/tests/hal/harness/devices/modem.lua new file mode 100644 index 00000000..f2aef5c5 --- /dev/null +++ b/tests/hal/harness/devices/modem.lua @@ -0,0 +1,99 @@ +local templates = require 'tests.hal.templates' + +-- Mock out external modem commands +local real_mmcli = require 'services.hal.drivers.modem.mmcli' +local mmcli = require 'tests.hal.harness.backends.mmcli' +local mock_err = real_mmcli.use_backend(mmcli) +if mock_err then + error("Failed to set mmcli backend: " .. mock_err) +end + +local real_qmicli = require 'services.hal.drivers.modem.qmicli' +local qmicli = require 'tests.hal.harness.backends.qmicli' +local mock_err = real_qmicli.use_backend(qmicli) +if mock_err then + error("Failed to set qmicli backend: " .. mock_err) +end + +local Modem = {} +Modem.__index = Modem + +local function make_full_address(index) + return string.format("/org/freedesktop/ModemManager1/Modem/%s", tostring(index)) +end + +local function make_monitor_event(is_added, address) + local sign = is_added and '(+)' or '(-)' + return string.format("%s %s [DUMMY MANAFACUTER] Dummy Modem Module", sign, + address) +end + +local function setup_mmcli_commands(commands) + commands.monitor_modems:stdout_pipe() -- create stdout pipe to share with modem manager +end + +function Modem:appear() + if not self.mmcli_data.address then + return "No address set for modem" + end + + local wr_err = self.mmcli_cmds.monitor_modems:write_out(make_monitor_event(true, self.mmcli_data.address)) + if wr_err then return wr_err end + + -- create info command output before modem is added + local information_cmd = mmcli.information(self.ctx, self.mmcli_data.address) + wr_err = information_cmd:write_out(self.mmcli_data.information) + return wr_err or nil +end + +function Modem:disappear() + if not self.mmcli_data.address then + return "No address set for modem" + end + + local wr_err = self.mmcli_cmds.monitor_modems:write_out(make_monitor_event(false, self.mmcli_data.address)) + if wr_err then return wr_err end +end + +function Modem:set_address_index(index) + self.mmcli_data.address = make_full_address(index) +end + +function Modem:set_mmcli_information(overrides) + self.mmcli_data.information = templates.make_modem_information(overrides) +end + +function Modem.new(ctx) + local self = {} + self.ctx = ctx + self.mmcli_data = {} + self.mmcli_data.information = templates.make_modem_information() + self.mmcli_cmds = { + monitor_modems = mmcli.monitor_modems() + } + setup_mmcli_commands(self.mmcli_cmds) + self.qmicli_data = {} + return setmetatable(self, Modem) +end + +local NoModem = {} +NoModem.__index = NoModem + +function NoModem.new() + local self = {} + self.mmcli_cmds = { + monitor_modems = mmcli.monitor_modems() + } + setup_mmcli_commands(self.mmcli_cmds) + return setmetatable(self, NoModem) +end + +function NoModem:appear() + local wr_err = self.mmcli_cmds.monitor_modems:write_out("No modems were found") + if wr_err then return wr_err end +end + +return { + new = Modem.new, + no_modem = NoModem.new +} diff --git a/tests/hal/templates.lua b/tests/hal/templates.lua new file mode 100644 index 00000000..532ded30 --- /dev/null +++ b/tests/hal/templates.lua @@ -0,0 +1,238 @@ +local json = require('dkjson') + +local function merge_tables(main, overrides) + local result = {} + for k, v in pairs(main) do + result[k] = v + end + for k, v in pairs(overrides) do + if type(v) == 'table' and type(result[k]) == 'table' then + result[k] = merge_tables(result[k], v) + else + result[k] = v + end + end + return result +end + +local function make_modem_information(overrides) + local base_information = { + modem = { + ["3gpp"] = { + ["5gnr"] = { + ["registration-settings"] = { + ["drx-cycle"] = "--", + ["mico-mode"] = "--", + }, + }, + ["enabled-locks"] = { + "fixed-dialing", + }, + eps = { + ["initial-bearer"] = { + ["dbus-path"] = "--", + settings = { + apn = "", + ["ip-type"] = "ipv4v6", + password = "--", + user = "--", + }, + }, + ["ue-mode-operation"] = "csps-2", + }, + imei = "867929068986654", + ["operator-code"] = "--", + ["operator-name"] = "--", + ["packet-service-state"] = "--", + pco = "--", + ["registration-state"] = "--", + }, + cdma = { + ["activation-state"] = "--", + ["cdma1x-registration-state"] = "--", + esn = "--", + ["evdo-registration-state"] = "--", + meid = "--", + nid = "--", + sid = "--", + }, + ["dbus-path"] = "/org/freedesktop/ModemManager1/Modem/14", + generic = { + ["access-technologies"] = {}, + bearers = {}, + ["carrier-configuration"] = "ROW_Generic_3GPP", + ["carrier-configuration-revision"] = "0501081F", + ["current-bands"] = { + "egsm", + "dcs", + "pcs", + "g850", + "utran-1", + "utran-4", + "utran-6", + "utran-5", + "utran-8", + "utran-2", + "eutran-1", + "eutran-2", + "eutran-3", + "eutran-4", + "eutran-5", + "eutran-7", + "eutran-8", + "eutran-12", + "eutran-13", + "eutran-18", + "eutran-19", + "eutran-20", + "eutran-25", + "eutran-26", + "eutran-28", + "eutran-38", + "eutran-39", + "eutran-40", + "eutran-41", + "utran-19", + }, + ["current-capabilities"] = { + "gsm-umts, lte", + }, + ["current-modes"] = "allowed: 2g, 3g, 4g; preferred: 4g", + device = "/sys/devices/platform/axi/1000120000.pcie/1f00300000.usb/xhci-hcd.1/usb3/3-1", + ["device-identifier"] = "a7591502473ae9ffc14e992ff1621f18cc4dd408", + drivers = { + "option1", + "qmi_wwan", + }, + ["equipment-identifier"] = "867929068986654", + ["hardware-revision"] = "10000", + manufacturer = "QUALCOMM INCORPORATED", + model = "QUECTEL Mobile Broadband Module", + ["own-numbers"] = {}, + physdev = "/sys/devices/platform/axi/1000120000.pcie/1f00300000.usb/xhci-hcd.1/usb3/3-1", + plugin = "quectel", + ports = { + "cdc-wdm0 (qmi)", + "ttyUSB0 (ignored)", + "ttyUSB1 (gps)", + "ttyUSB2 (at)", + "ttyUSB3 (at)", + "wwan0 (net)", + }, + ["power-state"] = "on", + ["primary-port"] = "cdc-wdm0", + ["primary-sim-slot"] = "1", + revision = "EG25GGBR07A08M2G", + ["signal-quality"] = { + recent = "yes", + value = "0", + }, + sim = "--", + ["sim-slots"] = { + "/org/freedesktop/ModemManager1/SIM/14", + "/", + }, + state = "disabled", + ["state-failed-reason"] = "--", + ["supported-bands"] = { + "egsm", + "dcs", + "pcs", + "g850", + "utran-1", + "utran-4", + "utran-6", + "utran-5", + "utran-8", + "utran-2", + "eutran-1", + "eutran-2", + "eutran-3", + "eutran-4", + "eutran-5", + "eutran-7", + "eutran-8", + "eutran-12", + "eutran-13", + "eutran-18", + "eutran-19", + "eutran-20", + "eutran-25", + "eutran-26", + "eutran-28", + "eutran-38", + "eutran-39", + "eutran-40", + "eutran-41", + "utran-19", + }, + ["supported-capabilities"] = { + "gsm-umts, lte", + }, + ["supported-ip-families"] = { + "ipv4", + "ipv6", + "ipv4v6", + }, + ["supported-modes"] = { + "allowed: 2g; preferred: none", + "allowed: 3g; preferred: none", + "allowed: 4g; preferred: none", + "allowed: 2g, 3g; preferred: 3g", + "allowed: 2g, 3g; preferred: 2g", + "allowed: 2g, 4g; preferred: 4g", + "allowed: 2g, 4g; preferred: 2g", + "allowed: 3g, 4g; preferred: 4g", + "allowed: 3g, 4g; preferred: 3g", + "allowed: 2g, 3g, 4g; preferred: 4g", + "allowed: 2g, 3g, 4g; preferred: 3g", + "allowed: 2g, 3g, 4g; preferred: 2g", + }, + ["unlock-required"] = "sim-pin2", + ["unlock-retries"] = { + "sim-pin (3)", + "sim-puk (10)", + "sim-pin2 (3)", + "sim-puk2 (10)", + }, + }, + }, + } + local merged = merge_tables(base_information, overrides or {}) + + return json.encode(merged), merged +end + +local function make_modem_device_event(overrides) + local base_event = { + connected = false, + data = { + device = "modemcard", + port = "/sys/devices/platform/axi/1000120000.pcie/1f00300000.usb/xhci-hcd.1/usb3/3-1" + }, + id_field = "port", + type = "usb" + } + if overrides and overrides.connected == true then + base_event.capabilities = { + modem = { + control = { + driver_q = { + buffer = { count = 0, first = 1, items = {} }, + buffer_size = 10, + getq = { count = 0, first = 1, items = {} }, + putq = { count = 0, first = 1, items = {} } + } + }, + id = "867929068986654" + } + } + base_event.device_control = {} + end + return merge_tables(base_event, overrides or {}) +end + +return { + make_modem_information = make_modem_information, + make_modem_device_event = make_modem_device_event, +} diff --git a/tests/utils/ShimCommands.lua b/tests/utils/ShimCommands.lua new file mode 100644 index 00000000..164aac0a --- /dev/null +++ b/tests/utils/ShimCommands.lua @@ -0,0 +1,229 @@ +local channel = require 'fibers.channel' +local context = require 'fibers.context' +local op = require 'fibers.op' + +local COMMAND_STATE = { + CREATED = 'created', + STARTED = 'started', + FLUSHED = 'flushed', + KILLED = 'killed' +} + +local Pipe = {} +Pipe.__index = Pipe + +function Pipe:read_line_op() + if not self.ch then + error("attempt to read from closed pipe") + end + return self.ch:get_op() +end + +function Pipe:close() + if self.parent_cmd.state ~= COMMAND_STATE.FLUSHED then + error("cannot close pipe before parent command is flushed") + end + self.ch = nil +end + +local function new_pipe(parent_cmd) + local self = { + parent_cmd = parent_cmd, + ch = channel.new() + } + return setmetatable(self, Pipe) +end + +local Command = {} +Command.__index = Command + +local function new_command() + local self = { + state = COMMAND_STATE.CREATED, + ctx = context.with_cancel(context.background()), + stdout = nil, + stderr = nil, + } + return setmetatable(self, Command) +end + +function Command:start() + self.state = COMMAND_STATE.STARTED + return nil +end + +function Command:setprdeathsig(sig) + -- pass +end + +function Command:setpgid() + -- pass +end + +function Command:stdout_pipe() + if not self.stdout then + self.stdout = new_pipe(self) + end + return self.stdout +end + +function Command:stderr_pipe() + if not self.stderr then + self.stderr = new_pipe(self) + end + return self.stderr +end + +function Command:combined_output() + local out = self:stdout_pipe() + local err = self:stderr_pipe() + + local buf = "" + local continue = true + local function push_data(data) + if data then + buf = buf .. data + else + continue = false + end + end + + while continue and not self.ctx:err() do + local read_op = op.choice( + out:read_line_op(), + err:read_line_op() + ):wrap(push_data) + op.choice( + read_op, + self.ctx:done_op() + ):perform() + end + + return buf, nil +end + +function Command:wait() + if self.state == COMMAND_STATE.KILLED then + self.state = COMMAND_STATE.FLUSHED + end +end + +function Command:kill() + self.ctx:cancel('killed') + self.state = COMMAND_STATE.KILLED +end + +function Command:close() + self.ctx:cancel('ended') +end + +function Command:write_out(data) + if not self.stdout then + return 'stdout pipe not set' + end + self.stdout.ch:put(data) +end + +function Command:write_err(data) + if not self.stderr then + return 'stderr pipe not set' + end + self.stderr.ch:put(data) +end + +local StaticCommand = {} +StaticCommand.__index = StaticCommand + +local function new_static_command() + local self = { + bse_cmd = new_command(), + out = "", + err = "", + } + return setmetatable(self, StaticCommand) +end + +function StaticCommand:start() + return self.bse_cmd:start() +end + +function StaticCommand:setprdeathsig(sig) + return self.bse_cmd:setprdeathsig(sig) +end + +function StaticCommand:setpgid() + return self.bse_cmd:setpgid() +end + +function StaticCommand:stdout_pipe() + error("unimplemented") +end + +function StaticCommand:stderr_pipe() + error("unimplemented") +end + +function StaticCommand:combined_output() + return self.out .. self.err +end + +function StaticCommand:wait() + return self.bse_cmd:wait() +end + +function StaticCommand:kill() + return self.bse_cmd:kill() +end + +function StaticCommand:close() + return self.bse_cmd:close() +end + +function StaticCommand:write_out(data) + self.out = data +end + +function StaticCommand:write_err(data) + self.err = data +end + +local BroadcastCommand = {} +BroadcastCommand.__index = BroadcastCommand + +function BroadcastCommand:new_child() + local child = new_command() + table.insert(self.children, child) + return child +end + +function BroadcastCommand:write_out(data) + for _, child in ipairs(self.children) do + local err = child:write_out(data) + if err then + return err + end + end +end + +function BroadcastCommand:write_err(data) + for _, child in ipairs(self.children) do + local err = child:write_err(data) + if err then + return err + end + end +end + +local function new_broadcast_command() + local self = { + children = {} + } + + return setmetatable(self, BroadcastCommand) +end + +return { + new_broadcast_command = new_broadcast_command, + new_static_command = new_static_command, + new_command = new_command +} From a0a7e9605b5804afc1be8f8ff461daeddf2cf7f3 Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Fri, 19 Dec 2025 16:01:10 +0000 Subject: [PATCH 07/20] Added hooks for calling commands directly --- tests/utils/ShimCommands.lua | 50 +++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/tests/utils/ShimCommands.lua b/tests/utils/ShimCommands.lua index 164aac0a..6b6816c7 100644 --- a/tests/utils/ShimCommands.lua +++ b/tests/utils/ShimCommands.lua @@ -20,9 +20,10 @@ function Pipe:read_line_op() end function Pipe:close() - if self.parent_cmd.state ~= COMMAND_STATE.FLUSHED then - error("cannot close pipe before parent command is flushed") - end + -- In the real exec implementation, pipes can be closed + -- independently of the process lifecycle. For the shim we + -- therefore allow close() regardless of the parent command + -- state and simply drop the underlying channel reference. self.ch = nil end @@ -43,12 +44,19 @@ local function new_command() ctx = context.with_cancel(context.background()), stdout = nil, stderr = nil, + -- Optional lifecycle hooks used by test backends to + -- trigger side-effects when commands are used. + on_start = nil, + on_kill = nil, } return setmetatable(self, Command) end function Command:start() self.state = COMMAND_STATE.STARTED + if self.on_start then + return self.on_start(self) + end return nil end @@ -75,8 +83,18 @@ function Command:stderr_pipe() end function Command:combined_output() - local out = self:stdout_pipe() - local err = self:stderr_pipe() + local out_pipe = self:stdout_pipe() + local err_pipe = self:stderr_pipe() + + -- In the real exec implementation, the process is started + -- before output is read. Mirror that behaviour here so that + -- any on_start side-effects are applied exactly once. + if self.state == COMMAND_STATE.CREATED then + local err = self:start() + if err then + return "", err + end + end local buf = "" local continue = true @@ -90,8 +108,8 @@ function Command:combined_output() while continue and not self.ctx:err() do local read_op = op.choice( - out:read_line_op(), - err:read_line_op() + out_pipe:read_line_op(), + err_pipe:read_line_op() ):wrap(push_data) op.choice( read_op, @@ -102,6 +120,21 @@ function Command:combined_output() return buf, nil end +function Command:run() + -- Simplified exec-style run: ensure the command has started and + -- propagate any start error. We do not currently accumulate + -- stdout/stderr here as existing callers only check the error. + if self.state == COMMAND_STATE.CREATED then + local err = self:start() + if err then + self.state = COMMAND_STATE.FLUSHED + return err + end + end + self.state = COMMAND_STATE.FLUSHED + return nil +end + function Command:wait() if self.state == COMMAND_STATE.KILLED then self.state = COMMAND_STATE.FLUSHED @@ -111,6 +144,9 @@ end function Command:kill() self.ctx:cancel('killed') self.state = COMMAND_STATE.KILLED + if self.on_kill then + self.on_kill(self) + end end function Command:close() From 28f7f4be9e3e5c3064daf9b48ec357de5fd7aa98 Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Fri, 19 Dec 2025 16:01:30 +0000 Subject: [PATCH 08/20] mmcli and qmicli use hooks to interact with sim modems --- tests/hal/harness/backends/mmcli.lua | 111 ++++++++++++++++++++++---- tests/hal/harness/backends/qmicli.lua | 40 ++++++++-- 2 files changed, 128 insertions(+), 23 deletions(-) diff --git a/tests/hal/harness/backends/mmcli.lua b/tests/hal/harness/backends/mmcli.lua index 81c0d76a..601b3545 100644 --- a/tests/hal/harness/backends/mmcli.lua +++ b/tests/hal/harness/backends/mmcli.lua @@ -1,5 +1,6 @@ local channel = require 'fibers.channel' local commands = require 'tests.utils.ShimCommands' +local modem_registry = require 'tests.hal.harness.devices.modem_registry' local monitor_modems_cmd = commands.new_command() -- We only ever need one instance local function monitor_modems() @@ -8,8 +9,22 @@ end local inhibit_cmds = {} local function inhibit(device) - inhibit_cmds[device] = commands.new_command() - return inhibit_cmds[device] + local cmd = commands.new_command() + inhibit_cmds[device] = cmd + + local modem = modem_registry.get_by_address(device) + if modem and modem.on_mmcli_inhibit_start then + cmd.on_start = function() + return modem:on_mmcli_inhibit_start(cmd) + end + end + if modem and modem.on_mmcli_inhibit_end then + cmd.on_kill = function() + modem:on_mmcli_inhibit_end(cmd) + end + end + + return cmd end local connect_cmds = {} @@ -17,43 +32,96 @@ local function connect(ctx, device, connection_string) if not connect_cmds[device] then connect_cmds[device] = {} end - table.insert(connect_cmds[device], commands.new_command()) - return connect_cmds[device][#connect_cmds[device]] + local cmd = commands.new_command() + table.insert(connect_cmds[device], cmd) + + local modem = modem_registry.get_by_address(device) + if modem and modem.on_mmcli_connect then + cmd.on_start = function() + return modem:on_mmcli_connect(cmd, connection_string) + end + end + + return cmd end local disconnect_cmds = {} local function disconnect(ctx, device) - disconnect_cmds[device] = commands.new_command() - return disconnect_cmds[device] + local cmd = commands.new_command() + disconnect_cmds[device] = cmd + + local modem = modem_registry.get_by_address(device) + if modem and modem.on_mmcli_disconnect then + cmd.on_start = function() + return modem:on_mmcli_disconnect(cmd) + end + end + + return cmd end local reset_cmds = {} local function reset(ctx, device) - reset_cmds[device] = commands.new_command() - return reset_cmds[device] + local cmd = commands.new_command() + reset_cmds[device] = cmd + + local modem = modem_registry.get_by_address(device) + if modem and modem.on_mmcli_reset then + cmd.on_start = function() + return modem:on_mmcli_reset(cmd) + end + end + + return cmd end local enable_cmds = {} local function enable(ctx, device) - enable_cmds[device] = commands.new_command() - return enable_cmds[device] + local cmd = commands.new_command() + enable_cmds[device] = cmd + + local modem = modem_registry.get_by_address(device) + if modem and modem.on_mmcli_enable then + cmd.on_start = function() + return modem:on_mmcli_enable(cmd) + end + end + + return cmd end local disable_cmds = {} local function disable(ctx, device) - disable_cmds[device] = commands.new_command() - return disable_cmds[device] + local cmd = commands.new_command() + disable_cmds[device] = cmd + + local modem = modem_registry.get_by_address(device) + if modem and modem.on_mmcli_disable then + cmd.on_start = function() + return modem:on_mmcli_disable(cmd) + end + end + + return cmd end local monitor_state_cmds = {} local function monitor_state(device) - monitor_state_cmds[device] = commands.new_command() - return monitor_state_cmds[device] + local cmd = commands.new_command() + monitor_state_cmds[device] = cmd + + local modem = modem_registry.get_by_address(device) + if modem and modem.on_mmcli_monitor_state_start then + cmd.on_start = function() + return modem:on_mmcli_monitor_state_start(cmd) + end + end + + return cmd end local information_cmds = {} local function information(ctx, device) - print(device) if not information_cmds[device] then information_cmds[device] = commands.new_static_command() end @@ -74,8 +142,17 @@ end local signal_setup_cmds = {} local function signal_setup(ctx, device, rate) - signal_setup_cmds[device] = commands.new_command() - return signal_setup_cmds[device] + local cmd = commands.new_command() + signal_setup_cmds[device] = cmd + + local modem = modem_registry.get_by_address(device) + if modem and modem.on_mmcli_signal_setup then + cmd.on_start = function() + return modem:on_mmcli_signal_setup(cmd, rate) + end + end + + return cmd end local signal_get_cmds = {} diff --git a/tests/hal/harness/backends/qmicli.lua b/tests/hal/harness/backends/qmicli.lua index 3a39df54..20f56b1f 100644 --- a/tests/hal/harness/backends/qmicli.lua +++ b/tests/hal/harness/backends/qmicli.lua @@ -1,4 +1,5 @@ local commands = require "tests.utils.ShimCommands" +local modem_registry = require 'tests.hal.harness.devices.modem_registry' -- For each function we create and track shim commands per *port*. @@ -10,20 +11,47 @@ end local uim_sim_power_off_cmds = {} local function uim_sim_power_off(ctx, port) - uim_sim_power_off_cmds[port] = commands.new_command() - return uim_sim_power_off_cmds[port] + local cmd = commands.new_command() + uim_sim_power_off_cmds[port] = cmd + + local modem = modem_registry.get_by_qmi_port(port) + if modem and modem.on_qmi_uim_sim_power_off then + cmd.on_start = function() + return modem:on_qmi_uim_sim_power_off(cmd, port) + end + end + + return cmd end local uim_sim_power_on_cmds = {} local function uim_sim_power_on(ctx, port) - uim_sim_power_on_cmds[port] = commands.new_command() - return uim_sim_power_on_cmds[port] + local cmd = commands.new_command() + uim_sim_power_on_cmds[port] = cmd + + local modem = modem_registry.get_by_qmi_port(port) + if modem and modem.on_qmi_uim_sim_power_on then + cmd.on_start = function() + return modem:on_qmi_uim_sim_power_on(cmd, port) + end + end + + return cmd end local uim_monitor_slot_status_cmds = {} local function uim_monitor_slot_status(port) - uim_monitor_slot_status_cmds[port] = commands.new_command() - return uim_monitor_slot_status_cmds[port] + local cmd = commands.new_command() + uim_monitor_slot_status_cmds[port] = cmd + + local modem = modem_registry.get_by_qmi_port(port) + if modem and modem.on_qmi_uim_monitor_start then + cmd.on_start = function() + return modem:on_qmi_uim_monitor_start(cmd, port) + end + end + + return cmd end local uim_read_transparent_cmds = {} From 9418deec780cea029e263ef39a14c0a80521a6ae Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Fri, 19 Dec 2025 16:02:05 +0000 Subject: [PATCH 09/20] simple registery for holding sim modems by qmi port and mmcli address --- tests/hal/harness/devices/modem_registry.lua | 39 ++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/hal/harness/devices/modem_registry.lua diff --git a/tests/hal/harness/devices/modem_registry.lua b/tests/hal/harness/devices/modem_registry.lua new file mode 100644 index 00000000..122dda1d --- /dev/null +++ b/tests/hal/harness/devices/modem_registry.lua @@ -0,0 +1,39 @@ +local M = {} + +-- Simple in-memory registry used by the test modem backends to +-- map mmcli modem addresses and QMI ports back to the dummy modem +-- instance that owns them. + +local by_address = {} +local by_qmi_port = {} + +local function clear_mapping(map, modem) + for k, v in pairs(map) do + if v == modem then + map[k] = nil + end + end +end + +function M.set_address(modem, address) + if not modem or not address then return end + -- Remove any previous mapping for this modem to avoid stale keys. + clear_mapping(by_address, modem) + by_address[address] = modem +end + +function M.set_qmi_port(modem, port) + if not modem or not port then return end + clear_mapping(by_qmi_port, modem) + by_qmi_port[port] = modem +end + +function M.get_by_address(address) + return by_address[address] +end + +function M.get_by_qmi_port(port) + return by_qmi_port[port] +end + +return M From f661b2af4d16c1f8bcfc63f0bdc8e29067facfeb Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Fri, 19 Dec 2025 16:02:48 +0000 Subject: [PATCH 10/20] waiting on channel and changed to sleep based yeild --- tests/hal/harness.lua | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/hal/harness.lua b/tests/hal/harness.lua index 08ff2cf4..003e1830 100644 --- a/tests/hal/harness.lua +++ b/tests/hal/harness.lua @@ -1,5 +1,6 @@ local fiber = require 'fibers.fiber' local context = require 'fibers.context' +local sleep = require 'fibers.sleep' local harness = {} @@ -60,7 +61,7 @@ local function make_alt_wait(ctx, max_ticks) return nil, 'timeout' end - fiber.yield() + sleep.sleep(0) -- yields -- Special error sentinel used by wait helpers to -- distinguish an alt-path from a real error. return nil, '__ALT__' @@ -81,6 +82,16 @@ function harness.wait_for_msg(sub, ctx, max_ticks) end end +function harness.wait_for_channel(ch, ctx, max_ticks) + local alt = make_alt_wait(ctx, max_ticks) + while true do + local val, err = ch:get_op():perform_alt(alt) + if err ~= '__ALT__' then + return val, err + end + end +end + -- Wait until a predicate becomes true, yielding cooperatively -- between checks. Returns true on success, or false, reason on -- timeout or context cancellation. From 27b3d7ff041031a47f732ed90357a760c8b1c78e Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Fri, 19 Dec 2025 16:03:25 +0000 Subject: [PATCH 11/20] Simplified modem info down to what is needed by our code only --- tests/hal/templates.lua | 275 +++++++++++++++++----------------------- 1 file changed, 117 insertions(+), 158 deletions(-) diff --git a/tests/hal/templates.lua b/tests/hal/templates.lua index 532ded30..20d1b03f 100644 --- a/tests/hal/templates.lua +++ b/tests/hal/templates.lua @@ -16,191 +16,147 @@ local function merge_tables(main, overrides) end local function make_modem_information(overrides) + -- Minimal mmcli -J -m output containing only the fields the + -- modem driver (and mode/model overrides) actually read. local base_information = { modem = { ["3gpp"] = { - ["5gnr"] = { - ["registration-settings"] = { - ["drx-cycle"] = "--", - ["mico-mode"] = "--", - }, - }, - ["enabled-locks"] = { - "fixed-dialing", - }, - eps = { - ["initial-bearer"] = { - ["dbus-path"] = "--", - settings = { - apn = "", - ["ip-type"] = "ipv4v6", - password = "--", - user = "--", - }, - }, - ["ue-mode-operation"] = "csps-2", - }, - imei = "867929068986654", - ["operator-code"] = "--", - ["operator-name"] = "--", - ["packet-service-state"] = "--", - pco = "--", ["registration-state"] = "--", }, - cdma = { - ["activation-state"] = "--", - ["cdma1x-registration-state"] = "--", - esn = "--", - ["evdo-registration-state"] = "--", - meid = "--", - nid = "--", - sid = "--", - }, - ["dbus-path"] = "/org/freedesktop/ModemManager1/Modem/14", generic = { - ["access-technologies"] = {}, - bearers = {}, - ["carrier-configuration"] = "ROW_Generic_3GPP", - ["carrier-configuration-revision"] = "0501081F", - ["current-bands"] = { - "egsm", - "dcs", - "pcs", - "g850", - "utran-1", - "utran-4", - "utran-6", - "utran-5", - "utran-8", - "utran-2", - "eutran-1", - "eutran-2", - "eutran-3", - "eutran-4", - "eutran-5", - "eutran-7", - "eutran-8", - "eutran-12", - "eutran-13", - "eutran-18", - "eutran-19", - "eutran-20", - "eutran-25", - "eutran-26", - "eutran-28", - "eutran-38", - "eutran-39", - "eutran-40", - "eutran-41", - "utran-19", - }, - ["current-capabilities"] = { - "gsm-umts, lte", - }, - ["current-modes"] = "allowed: 2g, 3g, 4g; preferred: 4g", - device = "/sys/devices/platform/axi/1000120000.pcie/1f00300000.usb/xhci-hcd.1/usb3/3-1", - ["device-identifier"] = "a7591502473ae9ffc14e992ff1621f18cc4dd408", + -- Drivers determine QMI/MBIM mode drivers = { - "option1", "qmi_wwan", }, - ["equipment-identifier"] = "867929068986654", - ["hardware-revision"] = "10000", - manufacturer = "QUALCOMM INCORPORATED", - model = "QUECTEL Mobile Broadband Module", - ["own-numbers"] = {}, - physdev = "/sys/devices/platform/axi/1000120000.pcie/1f00300000.usb/xhci-hcd.1/usb3/3-1", + -- Used to select manufacturer/model mapping plugin = "quectel", + model = "QUECTEL Mobile Broadband Module", + revision = "EG25GGBR07A08M2G", + + -- Identity + ["equipment-identifier"] = "867929068986654", + device = "/sys/devices/platform/axi/1000120000.pcie/1f00300000.usb/xhci-hcd.1/usb3/3-1", + + -- Ports used for QMI, AT and net stats ports = { "cdc-wdm0 (qmi)", - "ttyUSB0 (ignored)", - "ttyUSB1 (gps)", "ttyUSB2 (at)", - "ttyUSB3 (at)", "wwan0 (net)", }, - ["power-state"] = "on", ["primary-port"] = "cdc-wdm0", - ["primary-sim-slot"] = "1", - revision = "EG25GGBR07A08M2G", - ["signal-quality"] = { - recent = "yes", - value = "0", - }, + + -- SIM / state used by the driver polling logic sim = "--", - ["sim-slots"] = { - "/org/freedesktop/ModemManager1/SIM/14", - "/", - }, state = "disabled", - ["state-failed-reason"] = "--", - ["supported-bands"] = { - "egsm", - "dcs", - "pcs", - "g850", - "utran-1", - "utran-4", - "utran-6", - "utran-5", - "utran-8", - "utran-2", - "eutran-1", - "eutran-2", - "eutran-3", - "eutran-4", - "eutran-5", - "eutran-7", - "eutran-8", - "eutran-12", - "eutran-13", - "eutran-18", - "eutran-19", - "eutran-20", - "eutran-25", - "eutran-26", - "eutran-28", - "eutran-38", - "eutran-39", - "eutran-40", - "eutran-41", - "utran-19", + }, + }, + } + local merged = merge_tables(base_information, overrides or {}) + + return json.encode(merged), merged +end + +-- Minimal mmcli -J -i output structure. The driver only +-- requires the top-level `sim` table, so we keep this very small +-- while allowing overrides for tests that care about details. +local function make_sim_information(overrides) + local base_information = { + sim = { + ["active"] = true, + ["imsi"] = "001010123456789", + ["operator-id"] = "00101", + ["operator-name"] = "Test Operator", + }, + } + local merged = merge_tables(base_information, overrides or {}) + return json.encode(merged), merged +end + +-- Minimal mmcli --signal-get JSON. You can select one or more +-- access technologies that should carry real values via +-- `active_techs`; all other technologies have their metrics set +-- to "--" by default. +-- +-- active_techs: either a single string ("lte") or an array of +-- tech strings (e.g. {"lte", "5g"}). Allowed +-- values are "5g", "cdma1x", "evdo", "gsm", +-- "lte", "umts". +-- overrides: optional table keyed by tech name, each value a +-- table merged into that tech's metrics. +local function make_signal_information(active_techs, overrides) + local base_signal = { + modem = { + signal = { + ["5g"] = { + ["error-rate"] = "--", + rsrp = "--", + rsrq = "--", + snr = "--", }, - ["supported-capabilities"] = { - "gsm-umts, lte", + cdma1x = { + ecio = "--", + ["error-rate"] = "--", + rssi = "--", }, - ["supported-ip-families"] = { - "ipv4", - "ipv6", - "ipv4v6", + evdo = { + ecio = "--", + ["error-rate"] = "--", + io = "--", + rssi = "--", + sinr = "--", }, - ["supported-modes"] = { - "allowed: 2g; preferred: none", - "allowed: 3g; preferred: none", - "allowed: 4g; preferred: none", - "allowed: 2g, 3g; preferred: 3g", - "allowed: 2g, 3g; preferred: 2g", - "allowed: 2g, 4g; preferred: 4g", - "allowed: 2g, 4g; preferred: 2g", - "allowed: 3g, 4g; preferred: 4g", - "allowed: 3g, 4g; preferred: 3g", - "allowed: 2g, 3g, 4g; preferred: 4g", - "allowed: 2g, 3g, 4g; preferred: 3g", - "allowed: 2g, 3g, 4g; preferred: 2g", + gsm = { + ["error-rate"] = "--", + rssi = "--", }, - ["unlock-required"] = "sim-pin2", - ["unlock-retries"] = { - "sim-pin (3)", - "sim-puk (10)", - "sim-pin2 (3)", - "sim-puk2 (10)", + lte = { + ["error-rate"] = "--", + rsrp = "--", + rsrq = "--", + rssi = "--", + snr = "--", + }, + umts = { + ecio = "--", + ["error-rate"] = "--", + rscp = "--", + rssi = "--", + }, + refresh = { + rate = "0", + }, + threshold = { + ["error-rate"] = "no", + rssi = "0", }, }, }, } - local merged = merge_tables(base_information, overrides or {}) - return json.encode(merged), merged + -- Normalise active_techs to an array of tech names + local tech_list + if type(active_techs) == "string" or active_techs == nil then + tech_list = { active_techs or "lte" } + elseif type(active_techs) == "table" then + tech_list = active_techs + else + tech_list = { "lte" } + end + + overrides = overrides or {} + + for _, tech in ipairs(tech_list) do + local tech_table = base_signal.modem.signal[tech] + if tech_table then + local tech_overrides = overrides[tech] or overrides + if tech_overrides and next(tech_overrides) ~= nil then + base_signal.modem.signal[tech] = merge_tables(tech_table, tech_overrides) + end + end + end + + local encoded = json.encode(base_signal) + return encoded, base_signal end local function make_modem_device_event(overrides) @@ -234,5 +190,8 @@ end return { make_modem_information = make_modem_information, + make_sim_information = make_sim_information, + make_signal_information = make_signal_information, make_modem_device_event = make_modem_device_event, + merge_tables = merge_tables, } From fd1294f6103a4258f2f6a8c388eaba632eefd39b Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Fri, 19 Dec 2025 16:04:07 +0000 Subject: [PATCH 12/20] One test for proof of sim insertion detection via log output, realistic behaviour --- tests/hal/test_modem_driver.lua | 134 ++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 tests/hal/test_modem_driver.lua diff --git a/tests/hal/test_modem_driver.lua b/tests/hal/test_modem_driver.lua new file mode 100644 index 00000000..dc4fd7de --- /dev/null +++ b/tests/hal/test_modem_driver.lua @@ -0,0 +1,134 @@ +-- Standalone demo test: bring up a dummy modem with no SIM, +-- wait a bit, then insert a SIM and wait again so we can +-- observe modem logs on the console. + +local this_file = debug.getinfo(1, "S").source:match("@?([^/]+)$") +local is_entry_point = arg and arg[0] and arg[0]:match("[^/]+$") == this_file + +if is_entry_point then + package.path = "../../src/lua-fibers/?.lua;" -- fibers submodule src + .. "../../src/lua-trie/src/?.lua;" -- trie submodule src + .. "../../src/lua-bus/src/?.lua;" -- bus submodule src + .. "../../src/?.lua;" -- main src tree + .. "../../?.lua;" -- repo root (for tests.hal.harness) + .. "./test_utils/?.lua;" -- shared test utilities + .. package.path + .. ";/usr/lib/lua/?.lua;/usr/lib/lua/?/init.lua;" + .. "./harness/?.lua;" + + _G._TEST = true +end + +local luaunit = require 'luaunit' +local fiber = require 'fibers.fiber' +local context = require 'fibers.context' +local sleep = require 'fibers.sleep' + +local harness = require 'tests.hal.harness' +local dummy_modem = require 'tests.hal.harness.devices.modem' + +TestHalModemDriver = {} +TestHalModemSimInsert = {} + +-- function TestHalModemDriver:test_modem_enable() +-- local hal, ctx, bus, conn, new_msg = harness.new_hal_env() + +-- hal:start(ctx, bus:connect()) + +-- local config = { +-- managers = { +-- modemcard = {}, +-- }, +-- } +-- harness.publish_config(conn, new_msg, config) + +-- local modem = dummy_modem.new(context.with_cancel(ctx), 'failed') +-- modem:set_address_index("0") +-- modem:set_mmcli_information{ +-- modem = { +-- generic = { +-- device = "/fake/port0", +-- ["equipment-identifier"] = "123456789", +-- } +-- } +-- } +-- end + +function TestHalModemSimInsert:test_modem_sim_insert_logs() + -- Bring up a fresh HAL environment with the modemcard manager + -- running under HAL so we can call capability control endpoints + -- like sim_detect. + local hal, ctx, bus, conn, new_msg = harness.new_hal_env() + + -- Start HAL control loop. + hal:start(ctx, bus:connect()) + + -- Enable only the modemcard manager via HAL config. + local config = { + managers = { + modemcard = {}, + }, + } + harness.publish_config(conn, new_msg, config) + + -- Create a dummy modem with no SIM present. + local modem = dummy_modem.new(context.with_cancel(ctx)) + modem:set_address_index("0") + modem:set_mmcli_information{ + modem = { + generic = { + device = "/fake/port0", + ["equipment-identifier"] = "123456789", + } + } + } + + -- Make the modem appear on the bus so the manager and driver + -- start up and begin logging. + local wr_err = modem:appear() + luaunit.assertNil(wr_err, "Failed to write to monitor command stdout") + + -- Wait a bit with no SIM inserted so we can see the initial + -- modem logs and then trigger SIM detection via the HAL + -- capability control endpoint. + sleep.sleep(1) + + -- Call the modem sim_detect endpoint so the driver starts + -- its SIM detection / warm-swap logic. The modem capability + -- id is the IMEI we set above (123456789). + local bus_pkg = require 'bus' + local new_msg_fn = bus_pkg.new_msg or new_msg + local sim_detect_sub = conn:request(new_msg_fn( + { 'hal', 'capability', 'modem', '123456789', 'control', 'sim_detect' }, + {} + )) + -- We don't assert on the response; this is just to ensure the + -- request is consumed and does not block. + local _ = sim_detect_sub:next_msg_with_context_op(context.with_timeout(ctx, 5)):perform() + sleep.sleep(5) + + -- Now insert a SIM and wait again to observe the resulting + -- modem state changes and logs. + local sim = dummy_modem.new_sim() + sim:set_imsi("001010123456789") + sim:set_operator("00101", "Test Operator") + modem:insert_sim(sim) + + sleep.sleep(2) + + -- Cleanly shut down the environment. + ctx:cancel('test complete') + sleep.sleep(0) +end + +local function main() + fiber.spawn(function() + luaunit.LuaUnit.run() + fiber.stop() + end) +end + +if is_entry_point then + main() + fiber.main() +end From af45875bec6c80ba22645f6e69f5771f458faa43 Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Fri, 19 Dec 2025 16:04:30 +0000 Subject: [PATCH 13/20] Test detection of modem add and remove events --- tests/hal/test_manager_modemcard.lua | 121 +++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 tests/hal/test_manager_modemcard.lua diff --git a/tests/hal/test_manager_modemcard.lua b/tests/hal/test_manager_modemcard.lua new file mode 100644 index 00000000..f1b760d0 --- /dev/null +++ b/tests/hal/test_manager_modemcard.lua @@ -0,0 +1,121 @@ +-- Detect if this file is being run as the entry point +local this_file = debug.getinfo(1, "S").source:match("@?([^/]+)$") +local is_entry_point = arg and arg[0] and arg[0]:match("[^/]+$") == this_file + +if is_entry_point then + -- Match the test harness package.path setup (see tests/test.lua, + -- test_wifi.lua, test_metrics.lua, test_system.lua, test_core.lua) + package.path = "../../src/lua-fibers/?.lua;" -- fibers submodule src + .. "../../src/lua-trie/src/?.lua;" -- trie submodule src + .. "../../src/lua-bus/src/?.lua;" -- bus submodule src + .. "../../src/?.lua;" -- main src tree + .. "../../?.lua;" -- repo root (for tests.hal.harness) + .. "./test_utils/?.lua;" -- shared test utilities + .. package.path + .. ";/usr/lib/lua/?.lua;/usr/lib/lua/?/init.lua;" + .. "./harness/?.lua;" + + _G._TEST = true -- Enable test exports in source code + local log = require 'services.log' + local rxilog = require 'rxilog' + -- for _, mode in ipairs(rxilog.modes) do + -- log[mode.name] = function() end -- no-op logging during tests + -- end +end + +local luaunit = require 'luaunit' +local fiber = require 'fibers.fiber' +local channel = require 'fibers.channel' +local context = require 'fibers.context' +local sleep = require 'fibers.sleep' + +local harness = require 'tests.hal.harness' +local templates = require 'tests.hal.templates' +local dummy_modem = require 'tests.hal.harness.devices.modem' + +TestHalModemcardManager = {} + +function TestHalModemcardManager:test_modem_monitor_events() + local _, ctx, bus, conn, new_msg = harness.new_hal_env() + local manager = require 'services.hal.managers.modemcard'.new() + local device_event_q = channel.new() + local capability_info_q = channel.new() + manager:spawn(context.with_cancel(ctx), bus:connect(), device_event_q, capability_info_q) + + -- 1. No modems present + local nomodem = dummy_modem.no_modem() + local wr_err = nomodem:appear() + luaunit.assertNil(wr_err, "Failed to write to monitor command stdout") + + local result, err = harness.wait_for_channel(device_event_q, ctx, 20) -- wait for no add event + luaunit.assertNil(result, "Did not expect a device event") + luaunit.assertEquals(err, 'timeout') + + -- pre 2&3. Create dummy modem + local modem = dummy_modem.new(context.with_cancel(ctx)) + modem:set_address_index("0") + modem:set_mmcli_information{ + modem = { + generic = { + device = "/fake/port0", + ["equipment-identifier"] = "123456789", + } + } + } + + -- 2. Modem 0 added + wr_err = modem:appear() + luaunit.assertNil(wr_err, "Failed to write to monitor command stdout") + + + result, err = harness.wait_for_channel(device_event_q, ctx, 20) -- wait for add event + -- ignore control object + if result.capabilities.modem then result.capabilities.modem.control = "" end + -- build expected event + local expected_event = templates.make_modem_device_event{ + connected = true, + data = { + port = "/fake/port0" + }, + capabilities = { + modem = { + id = "123456789", + control = "" -- we don't care about the control object here + } + } + } + luaunit.assertNotNil(result, "Expected a device event") + luaunit.assertEquals(err, nil) + luaunit.assertEquals(result, expected_event) + + -- 3. Modem 0 removed + wr_err = modem:disappear() + luaunit.assertNil(wr_err, "Failed to write to monitor command stdout") + result, err = harness.wait_for_channel(device_event_q, ctx, 20) -- wait for remove event + -- expected_event = make_expected_device_event(false, make_full_address("0")) + expected_event = templates.make_modem_device_event{ + connected = false, + data = { + port = "/fake/port0" + } + } + luaunit.assertNotNil(result, "Expected a device event") + luaunit.assertEquals(err, nil) + luaunit.assertEquals(result, expected_event) + + ctx:cancel('test complete') + sleep.sleep(0) -- allow manager to exit +end + +local function main() + fiber.spawn(function() + luaunit.LuaUnit.run() + fiber.stop() + end) +end + +-- Only run tests if this file is executed directly (not via dofile) +if is_entry_point then + main() + fiber.main() +end From 7504a91a0ac34ce008f5c46877e004039fadd304 Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Fri, 19 Dec 2025 16:05:29 +0000 Subject: [PATCH 14/20] initial modem model to allow for responsive stateful reaction to driver code execution --- tests/hal/harness/devices/modem.lua | 418 +++++++++++++++++++++++++++- 1 file changed, 414 insertions(+), 4 deletions(-) diff --git a/tests/hal/harness/devices/modem.lua b/tests/hal/harness/devices/modem.lua index f2aef5c5..906f9116 100644 --- a/tests/hal/harness/devices/modem.lua +++ b/tests/hal/harness/devices/modem.lua @@ -1,4 +1,6 @@ local templates = require 'tests.hal.templates' +local fiber = require 'fibers.fiber' +local modem_registry = require 'tests.hal.harness.devices.modem_registry' -- Mock out external modem commands local real_mmcli = require 'services.hal.drivers.modem.mmcli' @@ -15,9 +17,56 @@ if mock_err then error("Failed to set qmicli backend: " .. mock_err) end +local MODEM_STATE = { + -- Upwards movement + FAILED = 0, + DISABLED = 1, + ENABLING = 2, + ENABLED = 3, + SEARCHING = 4, + REGISTERED = 5, + CONNECTING = 6, + CONNECTED = 7, + -- Downwards movement + DISCONNECTING = 8, + DISABLING = 9, +} + +local Sim = {} +Sim.__index = Sim + +function Sim.new() + local self = {} + self.active = true + return setmetatable(self, Sim) +end + +function Sim:set_imsi(imsi) + self.imsi = imsi +end + +function Sim:set_operator(operator_id, operator_name) + self.operator_id = operator_id + self.operator_name = operator_name +end + +function Sim:get_infomation() + local overrides = { + sim = { + ["active"] = self.active, + ["imsi"] = self.imsi, + ["operator-id"] = self.operator_id, + ["operator-name"] = self.operator_name, + }, + } + return templates.make_sim_information(overrides) +end + local Modem = {} Modem.__index = Modem +-- helper functions + local function make_full_address(index) return string.format("/org/freedesktop/ModemManager1/Modem/%s", tostring(index)) end @@ -32,6 +81,84 @@ local function setup_mmcli_commands(commands) commands.monitor_modems:stdout_pipe() -- create stdout pipe to share with modem manager end +-- Internal helpers --------------------------------------------------------- + +local function make_sim_dbus_path(index) + return string.format("/org/freedesktop/ModemManager1/SIM/%s", tostring(index)) +end + +local function clone_table(t) + local res = {} + for k, v in pairs(t or {}) do + if type(v) == 'table' then + res[k] = clone_table(v) + else + res[k] = v + end + end + return res +end + +-- Rebuild the mmcli -J -m information JSON based on current modem state. +function Modem:_refresh_mmcli_information() + local state = self.state + if type(state) ~= "string" then + state = tostring(state or "disabled") + end + + local sim_path = self.sim_path + if sim_path ~= nil and type(sim_path) ~= "string" then + sim_path = tostring(sim_path) + end + + local registration_state = self.registration_state + if type(registration_state) ~= "string" then + registration_state = tostring(registration_state or "--") + end + + local generic_overrides = { + state = state, + sim = sim_path or "--", + } + + local threegpp_overrides = { + ["registration-state"] = registration_state, + } + + local overrides = { + modem = { + generic = generic_overrides, + ["3gpp"] = threegpp_overrides, + } + } + + local encoded = templates.make_modem_information(overrides) + self.mmcli_data.information = encoded + + -- Update the existing static information command, if the backend has + -- already created one for this address. + local info_cmds = require('tests.hal.harness.backends.mmcli').information_cmds + local info_cmd = info_cmds[self.mmcli_data.address] + if info_cmd and info_cmd.write_out then + info_cmd:write_out(encoded) + end +end + +-- local function modem_state_machine(modem) +-- -- Placeholder: the real state machine will be event-driven based on +-- -- mmcli/qmicli shim commands. For now, we just initialise the +-- -- high-level state once; tests explicitly calling configuration +-- -- methods are responsible for refreshing information when state +-- -- changes. +-- modem.state = 'disabled' +-- modem.registration_state = "--" +-- -- Call as a plain function because the modem table has not yet +-- -- been given its metatable when this is invoked from Modem.new. +-- Modem._refresh_mmcli_information(modem) +-- end + +-- Modem hardware simulation methods + function Modem:appear() if not self.mmcli_data.address then return "No address set for modem" @@ -55,15 +182,282 @@ function Modem:disappear() if wr_err then return wr_err end end +function Modem:insert_sim(sim) + self.sim = sim + -- For now we always use SIM index 0 for the single + -- simulated SIM slot. + self.sim_path = make_sim_dbus_path(0) + -- When a SIM is inserted on real hardware, the modem does + -- not immediately transition into a registered state. + -- Instead, the SIM presence changes, the driver detects + -- this via QMI slot monitoring, and then power-cycles / + -- resets the modem which eventually comes back in a + -- disabled state. Reflect that by only updating SIM + -- presence here and leaving state transitions to the + -- driver-driven reset / power-cycle paths. + self:_refresh_mmcli_information() +end + +function Modem:remove_sim() + self.sim = nil + self.sim_path = nil + -- Clear SIM-related information but keep the current modem + -- state; any subsequent power-cycle/reset logic will drive + -- the state machine as in real hardware. + self.registration_state = "--" + self:_refresh_mmcli_information() + if self.qmi_slot_monitor_cmd then + self:_emit_sim_slot_status('absent') + end +end + +function Modem:block_signal() + -- TODO: make it so the modem cannot get past searching state +end + +function Modem:unblock_signal() + -- TODO: make it so the modem can get past searching state into registered state +end + +function Modem:block_connection() + -- TODO: make it so the modem cannot connect to network +end + +function Modem:unblock_connection() + -- TODO: make it so the modem can connect to network +end + +-- Modem configuration methods + function Modem:set_address_index(index) self.mmcli_data.address = make_full_address(index) + modem_registry.set_address(self, self.mmcli_data.address) end function Modem:set_mmcli_information(overrides) - self.mmcli_data.information = templates.make_modem_information(overrides) + -- Allow tests to set a custom base template; store both the raw + -- information and decoded fields we care about. + local information = templates.make_modem_information(overrides) + self.mmcli_data.information = information +end + +-- Optional: allow tests to explicitly bind a QMI port to this modem so +-- that qmicli backend commands can be routed back here. +function Modem:set_qmi_port(port) + self.qmi_port = port + modem_registry.set_qmi_port(self, port) +end + +-- Command handlers invoked from the mmcli/qmicli backends ------------- + +function Modem:on_mmcli_monitor_state_start(cmd) + -- Remember the state monitor command so that subsequent state + -- changes can emit updates. + self.state_monitor_cmd = cmd + local initial_state = self.state or 'disabled' + local line = string.format("Initial state: '%s'\n", initial_state) + cmd:stdout_pipe() -- ensure stdout exists + local err = cmd:write_out(line) + return err +end + +local function emit_state_change(modem, prev, curr) + if not modem.state_monitor_cmd then return end + if prev == curr then return end + local line = string.format("State changed: '%s' -> '%s'\n", prev, curr) + modem.state_monitor_cmd:write_out(line) +end + +function Modem:_set_state(new_state) + local prev = self.state or new_state + if prev == new_state then + return + end + self.state = new_state + fiber.spawn(function() + emit_state_change(self, prev, new_state) + self:_refresh_mmcli_information() + end) +end + +local function inhibited_error(cmd) + local err = "modem inhibited" + cmd:stderr_pipe() + cmd:write_err(err .. "\n") + return err +end + +local function failed_state_error(cmd) + local err = "WrongState: modem in failed state" + cmd:stderr_pipe() + cmd:write_err(err .. "\n") + return err +end + +function Modem:on_mmcli_enable(cmd) + if self.inhibited then + return inhibited_error(cmd) + end + if self.state == 'failed' then + return failed_state_error(cmd) + end + + if self.state == 'disabled' then + self.registration_state = 'home' + self:_set_state('registered') + end + + return nil +end + +function Modem:on_mmcli_disable(cmd) + if self.inhibited then + return inhibited_error(cmd) + end + if self.state == 'failed' then + return failed_state_error(cmd) + end + + self.registration_state = "--" + self:_set_state('disabled') + return nil +end + +function Modem:on_mmcli_connect(cmd, connection_string) + if self.inhibited then + return inhibited_error(cmd) + end + if self.state ~= 'registered' then + local err = string.format("cannot connect from state '%s'", tostring(self.state)) + cmd:stderr_pipe() + cmd:write_err(err .. "\n") + return err + end + + self:_set_state('connected') + cmd:stdout_pipe() + cmd:write_out("connected\n") + return nil +end + +function Modem:on_mmcli_disconnect(cmd) + if self.inhibited then + return inhibited_error(cmd) + end + if self.state ~= 'connected' then + local err = string.format("cannot disconnect from state '%s'", tostring(self.state)) + cmd:stderr_pipe() + cmd:write_err(err .. "\n") + return err + end + + self:_set_state('registered') + cmd:stdout_pipe() + cmd:write_out("disconnected\n") + return nil +end + +local function perform_reset(modem) + modem.registration_state = "--" + -- Model a drop-off and re-appearance of the modem on the + -- monitor_modems channel while keeping the same address. + fiber.spawn(function() + modem:disappear() + modem:_set_state('disabled') + modem:appear() + end) +end + +function Modem:on_mmcli_reset(cmd) + if self.inhibited then + return inhibited_error(cmd) + end + perform_reset(self) + return nil +end + +function Modem:on_mmcli_inhibit_start(cmd) + self.inhibited = true + return nil +end + +function Modem:on_mmcli_inhibit_end(cmd) + self.inhibited = false +end + +function Modem:on_mmcli_signal_setup(cmd, rate) + -- For now, just accept the requested rate; the driver will also + -- update its own refresh_rate_channel. + cmd:stdout_pipe() + cmd:write_out(string.format("signal setup %s\n", tostring(rate))) + return nil +end + +function Modem:on_qmi_uim_sim_power_off(cmd, port) + self.sim_powered = false + local powered_off_msg = string.format( + "[%s] Successfully performed SIM power off", tostring(port) + ) + fiber.spawn(function() + cmd:write_out(powered_off_msg) + cmd:write_out(nil) + end) + return nil +end + +function Modem:on_qmi_uim_sim_power_on(cmd, port) + self.sim_powered = true + + local powered_on_msg = string.format( + "[%s] Successfully performed SIM power on", tostring(port) + ) + fiber.spawn(function() + cmd:write_out(powered_on_msg) + cmd:write_out(nil) + end) + -- If a SIM is present when power is turned back on, model the + -- behaviour as a modem reset. + if self.sim then + -- Emit a QMI slot-status indication so any wait_for_sim + -- logic can observe the SIM becoming present. + fiber.spawn(function() + perform_reset(self) + end) + if self.qmi_slot_monitor_cmd then + fiber.spawn(function() + self:_emit_sim_slot_status('present') + end) + end + end + return nil +end + +-- Emit a minimal QMI slot status indication matching the format +-- expected by utils.parse_slot_monitor, using the simulated port +-- name from our QMI mapping. +function Modem:_emit_sim_slot_status(card_status) + if not self.qmi_slot_monitor_cmd or not self.qmi_port then return end + -- Keep this to a single logical line so that the + -- "Card status ... Slot status ..." pattern matches even + -- though Lua patterns do not make '.' span newlines. + local body = string.format( + "Card status: %s Slot status: active", + card_status + ) + -- self.qmi_slot_monitor_cmd:stdout_pipe() + self.qmi_slot_monitor_cmd:write_out(body) +end + +function Modem:on_qmi_uim_monitor_start(cmd, port) + self.qmi_slot_monitor_cmd = cmd + -- Real qmicli does not replay the last slot-status event on + -- monitor start; it only emits on subsequent changes. The + -- dummy will therefore emit status lines only when the SIM + -- state actually changes (insert/remove/power events). + return nil end -function Modem.new(ctx) +function Modem.new(ctx, initial_state) local self = {} self.ctx = ctx self.mmcli_data = {} @@ -71,9 +465,24 @@ function Modem.new(ctx) self.mmcli_cmds = { monitor_modems = mmcli.monitor_modems() } + self.state = initial_state or 'disabled' + self.registration_state = "--" + self.sim = nil + self.sim_path = nil + self.sim_powered = true + self.inhibited = false + self.signal_blocked = false + self.connection_blocked = false + self.qmi_slot_monitor_cmd = nil setup_mmcli_commands(self.mmcli_cmds) self.qmicli_data = {} - return setmetatable(self, Modem) + -- For now we hard-code the primary QMI port to match the + -- default modem information template used by tests. + self.qmi_port = "/dev/cdc-wdm0" + modem_registry.set_qmi_port(self, self.qmi_port) + local modem = setmetatable(self, Modem) + modem:_refresh_mmcli_information() + return modem end local NoModem = {} @@ -95,5 +504,6 @@ end return { new = Modem.new, - no_modem = NoModem.new + no_modem = NoModem.new, + new_sim = Sim.new } From c31b25ce50ae868dab886bfd6c35f6d5c4d99211 Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Fri, 19 Dec 2025 16:07:45 +0000 Subject: [PATCH 15/20] Deep modem state research, only done eg25 on new firmware --- .../command_outputs/collect_modem_snapshot.sh | 68 ++++++ .../collect_modem_transitions.sh | 155 ++++++++++++ .../connected/mmcli/location-status.json | 24 ++ .../eg25-new/state/connected/mmcli/modem.json | 189 +++++++++++++++ .../state/connected/mmcli/signal.json | 48 ++++ .../eg25-new/state/connected/mmcli/sim.json | 22 ++ .../connected/qmicli/nas-get-home-network.txt | 6 + .../connected/qmicli/nas-get-rf-band-info.txt | 12 + .../qmicli/nas-get-serving-system.txt | 29 +++ .../connected/qmicli/nas-get-signal-info.txt | 6 + .../connected/qmicli/uim-get-card-status.txt | 37 +++ .../qmicli/uim-read-transparent-gid1.txt | 7 + .../state/disabled/mmcli/location-status.json | 1 + .../eg25-new/state/disabled/mmcli/modem.json | 182 ++++++++++++++ .../eg25-new/state/disabled/mmcli/signal.json | 48 ++++ .../eg25-new/state/disabled/mmcli/sim.json | 22 ++ .../disabled/qmicli/nas-get-home-network.txt | 6 + .../disabled/qmicli/nas-get-rf-band-info.txt | 12 + .../qmicli/nas-get-serving-system.txt | 29 +++ .../disabled/qmicli/nas-get-signal-info.txt | 6 + .../disabled/qmicli/uim-get-card-status.txt | 37 +++ .../qmicli/uim-read-transparent-gid1.txt | 7 + .../failed-no-sim/mmcli/location-status.json | 1 + .../state/failed-no-sim/mmcli/modem.json | 175 +++++++++++++ .../state/failed-no-sim/mmcli/signal.json | 1 + .../state/failed-no-sim/mmcli/sim.json | 1 + .../qmicli/nas-get-home-network.txt | 1 + .../qmicli/nas-get-rf-band-info.txt | 9 + .../qmicli/nas-get-serving-system.txt | 26 ++ .../qmicli/nas-get-signal-info.txt | 4 + .../qmicli/uim-get-card-status.txt | 11 + .../qmicli/uim-read-transparent-gid1.txt | 1 + .../registered/mmcli/location-status.json | 24 ++ .../state/registered/mmcli/modem.json | 184 ++++++++++++++ .../state/registered/mmcli/signal.json | 48 ++++ .../eg25-new/state/registered/mmcli/sim.json | 22 ++ .../qmicli/nas-get-home-network.txt | 6 + .../qmicli/nas-get-rf-band-info.txt | 12 + .../qmicli/nas-get-serving-system.txt | 29 +++ .../registered/qmicli/nas-get-signal-info.txt | 6 + .../registered/qmicli/uim-get-card-status.txt | 37 +++ .../qmicli/uim-read-transparent-gid1.txt | 7 + .../mmcli/mm-disable.txt | 1 + .../mmcli/mm-enable.txt | 1 + .../qmicli/sim-power-off.txt | 1 + .../qmicli/sim-power-on.txt | 1 + .../mmcli/mm-disable.txt | 1 + .../mmcli/mm-enable.txt | 1 + .../qmicli/sim-power-off.txt | 1 + .../qmicli/sim-power-on.txt | 1 + .../mmcli/mm-disable.txt | 1 + .../disabled_modem_on_sim/mmcli/mm-enable.txt | 1 + .../qmicli/sim-power-off.txt | 1 + .../qmicli/sim-power-on.txt | 1 + .../failed_modem_off_sim/mmcli/mm-disable.txt | 1 + .../failed_modem_off_sim/mmcli/mm-enable.txt | 1 + .../qmicli/sim-power-off.txt | 1 + .../qmicli/sim-power-on.txt | 1 + .../failed_modem_on_sim/mmcli/mm-disable.txt | 1 + .../failed_modem_on_sim/mmcli/mm-enable.txt | 1 + .../qmicli/sim-power-off.txt | 1 + .../qmicli/sim-power-on.txt | 1 + .../mm-disable.txt | 1 + .../mm-enable.txt | 1 + .../sim-power-off.txt | 1 + .../sim-power-on.txt | 1 + .../mmcli/mm-disable.txt | 1 + .../mmcli/mm-enable.txt | 1 + .../qmicli/sim-power-off.txt | 1 + .../qmicli/sim-power-on.txt | 1 + .../mmcli/mm-disable.txt | 1 + .../mmcli/mm-enable.txt | 1 + .../qmicli/sim-power-off.txt | 1 + .../qmicli/sim-power-on.txt | 1 + .../eg25-new/transitions/sim_montior.txt | 20 ++ .../modem-research/notes/modem_behaviour.md | 229 ++++++++++++++++++ 76 files changed, 1839 insertions(+) create mode 100644 sprint-docs/modem-research/command_outputs/collect_modem_snapshot.sh create mode 100644 sprint-docs/modem-research/command_outputs/collect_modem_transitions.sh create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/location-status.json create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/modem.json create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/signal.json create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/sim.json create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-home-network.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-rf-band-info.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-serving-system.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-signal-info.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/uim-get-card-status.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/uim-read-transparent-gid1.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/location-status.json create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/modem.json create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/signal.json create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/sim.json create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-home-network.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-rf-band-info.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-serving-system.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-signal-info.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/uim-get-card-status.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/uim-read-transparent-gid1.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/location-status.json create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/modem.json create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/signal.json create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/sim.json create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-home-network.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-rf-band-info.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-serving-system.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-signal-info.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/uim-get-card-status.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/uim-read-transparent-gid1.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/location-status.json create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/modem.json create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/signal.json create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/sim.json create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-home-network.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-rf-band-info.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-serving-system.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-signal-info.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/uim-get-card-status.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/uim-read-transparent-gid1.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/mmcli/mm-disable.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/mmcli/mm-enable.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/qmicli/sim-power-off.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/qmicli/sim-power-on.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/mmcli/mm-disable.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/mmcli/mm-enable.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/qmicli/sim-power-off.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/qmicli/sim-power-on.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/mmcli/mm-disable.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/mmcli/mm-enable.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/qmicli/sim-power-off.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/qmicli/sim-power-on.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/mmcli/mm-disable.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/mmcli/mm-enable.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/qmicli/sim-power-off.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/qmicli/sim-power-on.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/mmcli/mm-disable.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/mmcli/mm-enable.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/qmicli/sim-power-off.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/qmicli/sim-power-on.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/mm-disable.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/mm-enable.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/sim-power-off.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/sim-power-on.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/mmcli/mm-disable.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/mmcli/mm-enable.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/qmicli/sim-power-off.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/qmicli/sim-power-on.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/mmcli/mm-disable.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/mmcli/mm-enable.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/qmicli/sim-power-off.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/qmicli/sim-power-on.txt create mode 100644 sprint-docs/modem-research/command_outputs/eg25-new/transitions/sim_montior.txt create mode 100644 sprint-docs/modem-research/notes/modem_behaviour.md diff --git a/sprint-docs/modem-research/command_outputs/collect_modem_snapshot.sh b/sprint-docs/modem-research/command_outputs/collect_modem_snapshot.sh new file mode 100644 index 00000000..d66e08fb --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/collect_modem_snapshot.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# collect_modem_snapshot.sh +# Usage: ./collect_modem_snapshot.sh /tmp/idle_run +# Env overrides (optional): MODEM, SIM, QMI_DEV + +set -u + +RUN_DIR="${1:-/tmp/modem_run}" +MODEM="${MODEM:-0}" +SIM="${SIM:-0}" +QMI_DEV="${QMI_DEV:-/dev/cdc-wdm0}" + +MMCLI_DIR="${RUN_DIR}/mmcli" +QMI_DIR="${RUN_DIR}/qmicli" + +mkdir -p "${MMCLI_DIR}" "${QMI_DIR}" + +echo "Snapshot in RUN_DIR='${RUN_DIR}', MODEM='${MODEM}', SIM='${SIM}', QMI_DEV='${QMI_DEV}'" >&2 + +############################################################################### +# mmcli read-only snapshots +############################################################################### + +# mmcli -J -m +mmcli -J -m "${MODEM}" \ + > "${MMCLI_DIR}/modem.json" 2>&1 || true + +# mmcli -J -i +mmcli -J -i "${SIM}" \ + > "${MMCLI_DIR}/sim.json" 2>&1 || true + +# mmcli -J -m --signal-get +mmcli -J -m "${MODEM}" --signal-get \ + > "${MMCLI_DIR}/signal.json" 2>&1 || true + +# mmcli -J -m --location-status +mmcli -J -m "${MODEM}" --location-status \ + > "${MMCLI_DIR}/location-status.json" 2>&1 || true + +############################################################################### +# qmicli read-only snapshots +############################################################################### + +# qmicli --uim-get-card-status +qmicli -p -d "${QMI_DEV}" --uim-get-card-status \ + > "${QMI_DIR}/uim-get-card-status.txt" 2>&1 || true + +# qmicli --uim-read-transparent=...6F3E (GID1) +qmicli -p -d "${QMI_DEV}" --uim-read-transparent=0x3F00,0x7FFF,0x6F3E \ + > "${QMI_DIR}/uim-read-transparent-gid1.txt" 2>&1 || true + +# qmicli --nas-get-rf-band-info +qmicli -p -d "${QMI_DEV}" --nas-get-rf-band-info \ + > "${QMI_DIR}/nas-get-rf-band-info.txt" 2>&1 || true + +# qmicli --nas-get-home-network +qmicli -p -d "${QMI_DEV}" --nas-get-home-network \ + > "${QMI_DIR}/nas-get-home-network.txt" 2>&1 || true + +# qmicli --nas-get-serving-system +qmicli -p -d "${QMI_DEV}" --nas-get-serving-system \ + > "${QMI_DIR}/nas-get-serving-system.txt" 2>&1 || true + +# qmicli --nas-get-signal-info +qmicli -p -d "${QMI_DEV}" --nas-get-signal-info \ + > "${QMI_DIR}/nas-get-signal-info.txt" 2>&1 || true + +echo "Snapshot done under '${RUN_DIR}'." >&2 diff --git a/sprint-docs/modem-research/command_outputs/collect_modem_transitions.sh b/sprint-docs/modem-research/command_outputs/collect_modem_transitions.sh new file mode 100644 index 00000000..2ba8b3ab --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/collect_modem_transitions.sh @@ -0,0 +1,155 @@ +#!/bin/sh +# collect_modem_transitions_matrix.sh +# +# Usage: +# MODEM=0 QMI_DEV=/dev/cdc-wdm0 \ +# sh collect_modem_transitions_matrix.sh \ +# /tmp/transitions enabled on +# +# Args: +# $1 = RUN_DIR (root output dir) +# $2 = INITIAL_MODEM_STATE: registered|disabled|connected +# $3 = INITIAL_SIM_STATE: on|off +# +# You should manually verify the modem really starts in this state. + +set -u + +RUN_DIR="${1:-/tmp/modem_transitions}" +INITIAL_MODEM_STATE="${2:-registered}" # registered|disabled|connected +INITIAL_SIM_STATE="${3:-on}" # on|off + +MODEM="${MODEM:-0}" +QMI_DEV="${QMI_DEV:-/dev/cdc-wdm0}" + +echo "RUN_DIR='${RUN_DIR}', INITIAL_MODEM_STATE='${INITIAL_MODEM_STATE}', INITIAL_SIM_STATE='${INITIAL_SIM_STATE}', MODEM='${MODEM}', QMI_DEV='${QMI_DEV}'" >&2 + +# Outputs go under: +# ${RUN_DIR}/${INITIAL_MODEM_STATE}_modem_${INITIAL_SIM_STATE}_sim/mmcli/.txt +# ${RUN_DIR}/${INITIAL_MODEM_STATE}_modem_${INITIAL_SIM_STATE}_sim/qmicli/.txt +INITIAL_STATE_DIR="${RUN_DIR}/${INITIAL_MODEM_STATE}_modem_${INITIAL_SIM_STATE}_sim" +MMCLI_DIR="${INITIAL_STATE_DIR}/mmcli" +QMI_DIR="${INITIAL_STATE_DIR}/qmicli" + +mkdir -p "${MMCLI_DIR}" "${QMI_DIR}" + +ensure_modem_state() { + case "$1" in + registered) + mmcli -m "${MODEM}" -e >/dev/null 2>&1 || true + sleep 1 + ;; + connected) + if [ -z "${CONNECTION_STRING:-}" ]; then + echo "CONNECTION_STRING is not set; cannot ensure 'connected' state." >&2 + return 1 + fi + mmcli -m "${MODEM}" --simple-connect="${CONNECTION_STRING}" + sleep 1 + ;; + disabled) + mmcli -m "${MODEM}" -d >/dev/null 2>&1 || true + ;; + *) + echo "Unknown modem state '$1' (expected registered|disabled|connected)" >&2 + ;; + esac +} + +ensure_sim_state() { + case "$1" in + on) + qmicli -p -d "${QMI_DEV}" --uim-sim-power-on=1 >/dev/null 2>&1 || true + ;; + off) + qmicli -p -d "${QMI_DEV}" --uim-sim-power-off=1 >/dev/null 2>&1 || true + ;; + *) + echo "Unknown SIM state '$1' (expected on|off)" >&2 + ;; + esac +} + +# Helper to run a single experiment: +# name: logical name (e.g. mm-enable, sim-power-off) +# kind: "modem" or "sim" (which aspect this experiment changes) +# forward: shell code for the transition command (no redirection) +run_experiment() { + name="$1" + kind="$2" + forward_cmd="$3" + + echo "=== Experiment '${name}' (initial state ${INITIAL_MODEM_STATE}/${INITIAL_SIM_STATE}) ===" >&2 + + # 1. Force the relevant initial state (modem *or* SIM, not both) + case "${kind}" in + modem) + ensure_modem_state "${INITIAL_MODEM_STATE}" + ;; + sim) + ensure_sim_state "${INITIAL_SIM_STATE}" + ;; + *) + echo "Unknown experiment kind '${kind}' (expected modem|sim)" >&2 + ;; + esac + + # 2. Run transition and capture output + # Choose output directory based on kind and write to .txt + case "${kind}" in + modem) + out_file="${MMCLI_DIR}/${name}.txt" + ;; + sim) + out_file="${QMI_DIR}/${name}.txt" + ;; + *) + out_file="${INITIAL_STATE_DIR}/${name}.txt" + ;; + esac + + # shellcheck disable=SC2086 + sh -c "${forward_cmd} > '${out_file}' 2>&1" || true + + # 3. Restore initial state for the same aspect (modem or SIM) + case "${kind}" in + modem) + ensure_modem_state "${INITIAL_MODEM_STATE}" + ;; + sim) + ensure_sim_state "${INITIAL_SIM_STATE}" + ;; + esac +} + +############################################################################### +# Define experiments +# +# You can comment out any you don’t care about. +############################################################################### + +# Modem enable (forward: -e, reverse: ensure initial state again) +run_experiment \ + "mm-enable" \ + "modem" \ + "mmcli -m '${MODEM}' -e" + +# Modem disable +run_experiment \ + "mm-disable" \ + "modem" \ + "mmcli -m '${MODEM}' -d" + +# SIM power off +run_experiment \ + "sim-power-off" \ + "sim" \ + "qmicli -p -d '${QMI_DEV}' --uim-sim-power-off=1" + +# SIM power on +run_experiment \ + "sim-power-on" \ + "sim" \ + "qmicli -p -d '${QMI_DEV}' --uim-sim-power-on=1" + +echo "All experiments done; each should have returned to initial state (best effort)." >&2 diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/location-status.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/location-status.json new file mode 100644 index 00000000..43f70bc0 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/location-status.json @@ -0,0 +1,24 @@ +{ + "modem": { + "location": { + "capabilities": [ + "3gpp-lac-ci", + "gps-raw", + "gps-nmea", + "gps-unmanaged", + "agps-msa", + "agps-msb" + ], + "enabled": [ + "3gpp-lac-ci" + ], + "gps": { + "assistance": [], + "assistance-servers": [], + "refresh-rate": "30", + "supl-server": "--" + }, + "signals": "no" + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/modem.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/modem.json new file mode 100644 index 00000000..be49085b --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/modem.json @@ -0,0 +1,189 @@ +{ + "modem": { + "3gpp": { + "5gnr": { + "registration-settings": { + "drx-cycle": "--", + "mico-mode": "--" + } + }, + "enabled-locks": [ + "fixed-dialing" + ], + "eps": { + "initial-bearer": { + "dbus-path": "/org/freedesktop/ModemManager1/Bearer/7", + "settings": { + "apn": "", + "ip-type": "ipv4", + "password": "--", + "user": "--" + } + }, + "ue-mode-operation": "csps-2" + }, + "imei": "867929068986514", + "operator-code": "23410", + "operator-name": "O2 - UK", + "packet-service-state": "attached", + "pco": "--", + "registration-state": "home" + }, + "cdma": { + "activation-state": "--", + "cdma1x-registration-state": "--", + "esn": "--", + "evdo-registration-state": "--", + "meid": "--", + "nid": "--", + "sid": "--" + }, + "dbus-path": "/org/freedesktop/ModemManager1/Modem/6", + "generic": { + "access-technologies": [ + "lte" + ], + "bearers": [ + "/org/freedesktop/ModemManager1/Bearer/6", + "/org/freedesktop/ModemManager1/Bearer/5", + "/org/freedesktop/ModemManager1/Bearer/4", + "/org/freedesktop/ModemManager1/Bearer/3" + ], + "carrier-configuration": "ROW_Generic_3GPP", + "carrier-configuration-revision": "0501081F", + "current-bands": [ + "egsm", + "dcs", + "pcs", + "g850", + "utran-1", + "utran-4", + "utran-6", + "utran-5", + "utran-8", + "utran-2", + "eutran-1", + "eutran-2", + "eutran-3", + "eutran-4", + "eutran-5", + "eutran-7", + "eutran-8", + "eutran-12", + "eutran-13", + "eutran-18", + "eutran-19", + "eutran-20", + "eutran-25", + "eutran-26", + "eutran-28", + "eutran-38", + "eutran-39", + "eutran-40", + "eutran-41", + "utran-19" + ], + "current-capabilities": [ + "gsm-umts, lte" + ], + "current-modes": "allowed: 2g, 3g, 4g; preferred: 4g", + "device": "/sys/devices/platform/axi/1000120000.pcie/1f00200000.usb/xhci-hcd.0/usb2/2-2/2-2.2", + "device-identifier": "b5bf9f66a67a0da6308a67fb858cdfef75d82565", + "drivers": [ + "option1", + "qmi_wwan" + ], + "equipment-identifier": "867929068986514", + "hardware-revision": "10000", + "manufacturer": "QUALCOMM INCORPORATED", + "model": "QUECTEL Mobile Broadband Module", + "own-numbers": [], + "physdev": "/sys/devices/platform/axi/1000120000.pcie/1f00200000.usb/xhci-hcd.0/usb2/2-2/2-2.2", + "plugin": "quectel", + "ports": [ + "cdc-wdm0 (qmi)", + "ttyUSB0 (ignored)", + "ttyUSB1 (gps)", + "ttyUSB2 (at)", + "ttyUSB3 (at)", + "wwan0 (net)" + ], + "power-state": "on", + "primary-port": "cdc-wdm0", + "primary-sim-slot": "1", + "revision": "EG25GGBR07A08M2G", + "signal-quality": { + "recent": "yes", + "value": "65" + }, + "sim": "/org/freedesktop/ModemManager1/SIM/2", + "sim-slots": [ + "/org/freedesktop/ModemManager1/SIM/2", + "/" + ], + "state": "connected", + "state-failed-reason": "--", + "supported-bands": [ + "egsm", + "dcs", + "pcs", + "g850", + "utran-1", + "utran-4", + "utran-6", + "utran-5", + "utran-8", + "utran-2", + "eutran-1", + "eutran-2", + "eutran-3", + "eutran-4", + "eutran-5", + "eutran-7", + "eutran-8", + "eutran-12", + "eutran-13", + "eutran-18", + "eutran-19", + "eutran-20", + "eutran-25", + "eutran-26", + "eutran-28", + "eutran-38", + "eutran-39", + "eutran-40", + "eutran-41", + "utran-19" + ], + "supported-capabilities": [ + "gsm-umts, lte" + ], + "supported-ip-families": [ + "ipv4", + "ipv6", + "ipv4v6" + ], + "supported-modes": [ + "allowed: 2g; preferred: none", + "allowed: 3g; preferred: none", + "allowed: 4g; preferred: none", + "allowed: 2g, 3g; preferred: 3g", + "allowed: 2g, 3g; preferred: 2g", + "allowed: 2g, 4g; preferred: 4g", + "allowed: 2g, 4g; preferred: 2g", + "allowed: 3g, 4g; preferred: 4g", + "allowed: 3g, 4g; preferred: 3g", + "allowed: 2g, 3g, 4g; preferred: 4g", + "allowed: 2g, 3g, 4g; preferred: 3g", + "allowed: 2g, 3g, 4g; preferred: 2g" + ], + "unlock-required": "sim-pin2", + "unlock-retries": [ + "sim-pin (3)", + "sim-puk (10)", + "sim-pin2 (3)", + "sim-puk2 (10)" + ] + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/signal.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/signal.json new file mode 100644 index 00000000..5d4fcadd --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/signal.json @@ -0,0 +1,48 @@ +{ + "modem": { + "signal": { + "5g": { + "error-rate": "--", + "rsrp": "--", + "rsrq": "--", + "snr": "--" + }, + "cdma1x": { + "ecio": "--", + "error-rate": "--", + "rssi": "--" + }, + "evdo": { + "ecio": "--", + "error-rate": "--", + "io": "--", + "rssi": "--", + "sinr": "--" + }, + "gsm": { + "error-rate": "--", + "rssi": "--" + }, + "lte": { + "error-rate": "--", + "rsrp": "--", + "rsrq": "--", + "rssi": "--", + "snr": "--" + }, + "refresh": { + "rate": "0" + }, + "threshold": { + "error-rate": "no", + "rssi": "0" + }, + "umts": { + "ecio": "--", + "error-rate": "--", + "rscp": "--", + "rssi": "--" + } + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/sim.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/sim.json new file mode 100644 index 00000000..050a27d5 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/sim.json @@ -0,0 +1,22 @@ +{ + "sim": { + "dbus-path": "/org/freedesktop/ModemManager1/SIM/2", + "properties": { + "active": "yes", + "eid": "--", + "emergency-numbers": [ + "999", + "00112" + ], + "esim-status": "--", + "gid1": "85FFFFFFFFFF47454E4945494E20202020202020", + "gid2": "FFFFFFFFFFFF2020202020202020202020202020", + "iccid": "8944110069073915392", + "imsi": "234103403359194", + "operator-code": "23410", + "operator-name": "--", + "removability": "--", + "sim-type": "--" + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-home-network.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-home-network.txt new file mode 100644 index 00000000..84351fca --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-home-network.txt @@ -0,0 +1,6 @@ +[/dev/cdc-wdm0] Successfully got home network: + Home network: + MCC: '234' + MNC: '10' + Description: 'O2 - UK' + Network name source: se13 diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-rf-band-info.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-rf-band-info.txt new file mode 100644 index 00000000..6a1adfe6 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-rf-band-info.txt @@ -0,0 +1,12 @@ +[/dev/cdc-wdm0] Successfully got RF band info +Band Information: + Radio Interface: 'lte' + Active Band Class: 'eutran-40' + Active Channel: '39250' +Band Information (Extended): + Radio Interface: 'lte' + Active Band Class: 'eutran-40' + Active Channel: '39250' +Bandwidth: + Radio Interface: 'lte' + Bandwidth: '20' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-serving-system.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-serving-system.txt new file mode 100644 index 00000000..b047d9cb --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-serving-system.txt @@ -0,0 +1,29 @@ +[/dev/cdc-wdm0] Successfully got serving system: + Registration state: 'registered' + CS: 'attached' + PS: 'attached' + Selected network: '3gpp' + Radio interfaces: '1' + [0]: 'lte' + Roaming status: 'off' + Data service capabilities: '1' + [0]: 'lte' + Current PLMN: + MCC: '234' + MNC: '10' + Description: 'O2 - UK' + Roaming indicators: '1' + [0]: 'off' (lte) + 3GPP location area code: '65534' + 3GPP cell ID: '153667976' + Detailed status: + Status: 'available' + Capability: 'cs-ps' + HDR Status: 'none' + HDR Hybrid: 'no' + Forbidden: 'no' + LTE tracking area code: '14768' + Full operator code info: + MCC: '234' + MNC: '10' + MNC with PCS digit: 'no' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-signal-info.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-signal-info.txt new file mode 100644 index 00000000..ddf57088 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-signal-info.txt @@ -0,0 +1,6 @@ +[/dev/cdc-wdm0] Successfully got signal info +LTE: + RSSI: '-73 dBm' + RSRQ: '-11 dB' + RSRP: '-103 dBm' + SNR: '10.4 dB' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/uim-get-card-status.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/uim-get-card-status.txt new file mode 100644 index 00000000..07aedb5e --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/uim-get-card-status.txt @@ -0,0 +1,37 @@ +[/dev/cdc-wdm0] Successfully got card status +Provisioning applications: + Primary GW: slot '1', application '1' + Primary 1X: session doesn't exist + Secondary GW: session doesn't exist + Secondary 1X: session doesn't exist +Slot [1]: + Card state: 'present' + UPIN state: 'not-initialized' + UPIN retries: '0' + UPUK retries: '0' + Application [1]: + Application type: 'usim (2)' + Application state: 'ready' + Application ID: + A0:00:00:00:87:10:02:FF:44:FF:FF:89:01:01:01:00 + Personalization state: 'ready' + UPIN replaces PIN1: 'no' + PIN1 state: 'disabled' + PIN1 retries: '3' + PUK1 retries: '10' + PIN2 state: 'enabled-not-verified' + PIN2 retries: '3' + PUK2 retries: '10' + Application [2]: + Application type: 'unknown (0)' + Application state: 'detected' + Application ID: + A0:00:00:00:63:50:4B:FF:34:FF:07:89:54:46:52:38 + Personalization state: 'unknown' + UPIN replaces PIN1: 'no' + PIN1 state: 'not-initialized' + PIN1 retries: '0' + PUK1 retries: '0' + PIN2 state: 'not-initialized' + PIN2 retries: '0' + PUK2 retries: '0' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/uim-read-transparent-gid1.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/uim-read-transparent-gid1.txt new file mode 100644 index 00000000..b0a6f712 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/uim-read-transparent-gid1.txt @@ -0,0 +1,7 @@ +[/dev/cdc-wdm0] Successfully read information from the UIM: +Card result: + SW1: '0x90' + SW2: '0x00' +Read result: + 85:FF:FF:FF:FF:FF:47:45:4E:49:45:49:4E:20:20:20:20:20:20:20 + diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/location-status.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/location-status.json new file mode 100644 index 00000000..8db81530 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/location-status.json @@ -0,0 +1 @@ +error: modem not enabled yet diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/modem.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/modem.json new file mode 100644 index 00000000..c2eec1f9 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/modem.json @@ -0,0 +1,182 @@ +{ + "modem": { + "3gpp": { + "5gnr": { + "registration-settings": { + "drx-cycle": "--", + "mico-mode": "--" + } + }, + "enabled-locks": [ + "fixed-dialing" + ], + "eps": { + "initial-bearer": { + "dbus-path": "--", + "settings": { + "apn": "internet", + "ip-type": "ipv4", + "password": "--", + "user": "--" + } + }, + "ue-mode-operation": "csps-2" + }, + "imei": "867929068986514", + "operator-code": "--", + "operator-name": "--", + "packet-service-state": "--", + "pco": "--", + "registration-state": "--" + }, + "cdma": { + "activation-state": "--", + "cdma1x-registration-state": "--", + "esn": "--", + "evdo-registration-state": "--", + "meid": "--", + "nid": "--", + "sid": "--" + }, + "dbus-path": "/org/freedesktop/ModemManager1/Modem/6", + "generic": { + "access-technologies": [], + "bearers": [], + "carrier-configuration": "ROW_Generic_3GPP", + "carrier-configuration-revision": "0501081F", + "current-bands": [ + "egsm", + "dcs", + "pcs", + "g850", + "utran-1", + "utran-4", + "utran-6", + "utran-5", + "utran-8", + "utran-2", + "eutran-1", + "eutran-2", + "eutran-3", + "eutran-4", + "eutran-5", + "eutran-7", + "eutran-8", + "eutran-12", + "eutran-13", + "eutran-18", + "eutran-19", + "eutran-20", + "eutran-25", + "eutran-26", + "eutran-28", + "eutran-38", + "eutran-39", + "eutran-40", + "eutran-41", + "utran-19" + ], + "current-capabilities": [ + "gsm-umts, lte" + ], + "current-modes": "allowed: 2g, 3g, 4g; preferred: 4g", + "device": "/sys/devices/platform/axi/1000120000.pcie/1f00200000.usb/xhci-hcd.0/usb2/2-2/2-2.2", + "device-identifier": "b5bf9f66a67a0da6308a67fb858cdfef75d82565", + "drivers": [ + "option1", + "qmi_wwan" + ], + "equipment-identifier": "867929068986514", + "hardware-revision": "10000", + "manufacturer": "QUALCOMM INCORPORATED", + "model": "QUECTEL Mobile Broadband Module", + "own-numbers": [], + "physdev": "/sys/devices/platform/axi/1000120000.pcie/1f00200000.usb/xhci-hcd.0/usb2/2-2/2-2.2", + "plugin": "quectel", + "ports": [ + "cdc-wdm0 (qmi)", + "ttyUSB0 (ignored)", + "ttyUSB1 (gps)", + "ttyUSB2 (at)", + "ttyUSB3 (at)", + "wwan0 (net)" + ], + "power-state": "on", + "primary-port": "cdc-wdm0", + "primary-sim-slot": "1", + "revision": "EG25GGBR07A08M2G", + "signal-quality": { + "recent": "yes", + "value": "0" + }, + "sim": "/org/freedesktop/ModemManager1/SIM/2", + "sim-slots": [ + "/org/freedesktop/ModemManager1/SIM/2", + "/" + ], + "state": "disabled", + "state-failed-reason": "--", + "supported-bands": [ + "egsm", + "dcs", + "pcs", + "g850", + "utran-1", + "utran-4", + "utran-6", + "utran-5", + "utran-8", + "utran-2", + "eutran-1", + "eutran-2", + "eutran-3", + "eutran-4", + "eutran-5", + "eutran-7", + "eutran-8", + "eutran-12", + "eutran-13", + "eutran-18", + "eutran-19", + "eutran-20", + "eutran-25", + "eutran-26", + "eutran-28", + "eutran-38", + "eutran-39", + "eutran-40", + "eutran-41", + "utran-19" + ], + "supported-capabilities": [ + "gsm-umts, lte" + ], + "supported-ip-families": [ + "ipv4", + "ipv6", + "ipv4v6" + ], + "supported-modes": [ + "allowed: 2g; preferred: none", + "allowed: 3g; preferred: none", + "allowed: 4g; preferred: none", + "allowed: 2g, 3g; preferred: 3g", + "allowed: 2g, 3g; preferred: 2g", + "allowed: 2g, 4g; preferred: 4g", + "allowed: 2g, 4g; preferred: 2g", + "allowed: 3g, 4g; preferred: 4g", + "allowed: 3g, 4g; preferred: 3g", + "allowed: 2g, 3g, 4g; preferred: 4g", + "allowed: 2g, 3g, 4g; preferred: 3g", + "allowed: 2g, 3g, 4g; preferred: 2g" + ], + "unlock-required": "sim-pin2", + "unlock-retries": [ + "sim-pin (3)", + "sim-puk (10)", + "sim-pin2 (3)", + "sim-puk2 (10)" + ] + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/signal.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/signal.json new file mode 100644 index 00000000..5d4fcadd --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/signal.json @@ -0,0 +1,48 @@ +{ + "modem": { + "signal": { + "5g": { + "error-rate": "--", + "rsrp": "--", + "rsrq": "--", + "snr": "--" + }, + "cdma1x": { + "ecio": "--", + "error-rate": "--", + "rssi": "--" + }, + "evdo": { + "ecio": "--", + "error-rate": "--", + "io": "--", + "rssi": "--", + "sinr": "--" + }, + "gsm": { + "error-rate": "--", + "rssi": "--" + }, + "lte": { + "error-rate": "--", + "rsrp": "--", + "rsrq": "--", + "rssi": "--", + "snr": "--" + }, + "refresh": { + "rate": "0" + }, + "threshold": { + "error-rate": "no", + "rssi": "0" + }, + "umts": { + "ecio": "--", + "error-rate": "--", + "rscp": "--", + "rssi": "--" + } + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/sim.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/sim.json new file mode 100644 index 00000000..050a27d5 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/sim.json @@ -0,0 +1,22 @@ +{ + "sim": { + "dbus-path": "/org/freedesktop/ModemManager1/SIM/2", + "properties": { + "active": "yes", + "eid": "--", + "emergency-numbers": [ + "999", + "00112" + ], + "esim-status": "--", + "gid1": "85FFFFFFFFFF47454E4945494E20202020202020", + "gid2": "FFFFFFFFFFFF2020202020202020202020202020", + "iccid": "8944110069073915392", + "imsi": "234103403359194", + "operator-code": "23410", + "operator-name": "--", + "removability": "--", + "sim-type": "--" + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-home-network.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-home-network.txt new file mode 100644 index 00000000..84351fca --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-home-network.txt @@ -0,0 +1,6 @@ +[/dev/cdc-wdm0] Successfully got home network: + Home network: + MCC: '234' + MNC: '10' + Description: 'O2 - UK' + Network name source: se13 diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-rf-band-info.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-rf-band-info.txt new file mode 100644 index 00000000..6a1adfe6 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-rf-band-info.txt @@ -0,0 +1,12 @@ +[/dev/cdc-wdm0] Successfully got RF band info +Band Information: + Radio Interface: 'lte' + Active Band Class: 'eutran-40' + Active Channel: '39250' +Band Information (Extended): + Radio Interface: 'lte' + Active Band Class: 'eutran-40' + Active Channel: '39250' +Bandwidth: + Radio Interface: 'lte' + Bandwidth: '20' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-serving-system.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-serving-system.txt new file mode 100644 index 00000000..b047d9cb --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-serving-system.txt @@ -0,0 +1,29 @@ +[/dev/cdc-wdm0] Successfully got serving system: + Registration state: 'registered' + CS: 'attached' + PS: 'attached' + Selected network: '3gpp' + Radio interfaces: '1' + [0]: 'lte' + Roaming status: 'off' + Data service capabilities: '1' + [0]: 'lte' + Current PLMN: + MCC: '234' + MNC: '10' + Description: 'O2 - UK' + Roaming indicators: '1' + [0]: 'off' (lte) + 3GPP location area code: '65534' + 3GPP cell ID: '153667976' + Detailed status: + Status: 'available' + Capability: 'cs-ps' + HDR Status: 'none' + HDR Hybrid: 'no' + Forbidden: 'no' + LTE tracking area code: '14768' + Full operator code info: + MCC: '234' + MNC: '10' + MNC with PCS digit: 'no' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-signal-info.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-signal-info.txt new file mode 100644 index 00000000..2d0fbcc0 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-signal-info.txt @@ -0,0 +1,6 @@ +[/dev/cdc-wdm0] Successfully got signal info +LTE: + RSSI: '-71 dBm' + RSRQ: '-11 dB' + RSRP: '-104 dBm' + SNR: '8.0 dB' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/uim-get-card-status.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/uim-get-card-status.txt new file mode 100644 index 00000000..07aedb5e --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/uim-get-card-status.txt @@ -0,0 +1,37 @@ +[/dev/cdc-wdm0] Successfully got card status +Provisioning applications: + Primary GW: slot '1', application '1' + Primary 1X: session doesn't exist + Secondary GW: session doesn't exist + Secondary 1X: session doesn't exist +Slot [1]: + Card state: 'present' + UPIN state: 'not-initialized' + UPIN retries: '0' + UPUK retries: '0' + Application [1]: + Application type: 'usim (2)' + Application state: 'ready' + Application ID: + A0:00:00:00:87:10:02:FF:44:FF:FF:89:01:01:01:00 + Personalization state: 'ready' + UPIN replaces PIN1: 'no' + PIN1 state: 'disabled' + PIN1 retries: '3' + PUK1 retries: '10' + PIN2 state: 'enabled-not-verified' + PIN2 retries: '3' + PUK2 retries: '10' + Application [2]: + Application type: 'unknown (0)' + Application state: 'detected' + Application ID: + A0:00:00:00:63:50:4B:FF:34:FF:07:89:54:46:52:38 + Personalization state: 'unknown' + UPIN replaces PIN1: 'no' + PIN1 state: 'not-initialized' + PIN1 retries: '0' + PUK1 retries: '0' + PIN2 state: 'not-initialized' + PIN2 retries: '0' + PUK2 retries: '0' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/uim-read-transparent-gid1.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/uim-read-transparent-gid1.txt new file mode 100644 index 00000000..b0a6f712 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/uim-read-transparent-gid1.txt @@ -0,0 +1,7 @@ +[/dev/cdc-wdm0] Successfully read information from the UIM: +Card result: + SW1: '0x90' + SW2: '0x00' +Read result: + 85:FF:FF:FF:FF:FF:47:45:4E:49:45:49:4E:20:20:20:20:20:20:20 + diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/location-status.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/location-status.json new file mode 100644 index 00000000..8db81530 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/location-status.json @@ -0,0 +1 @@ +error: modem not enabled yet diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/modem.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/modem.json new file mode 100644 index 00000000..401bef7c --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/modem.json @@ -0,0 +1,175 @@ +{ + "modem": { + "3gpp": { + "5gnr": { + "registration-settings": { + "drx-cycle": "--", + "mico-mode": "--" + } + }, + "enabled-locks": [], + "eps": { + "initial-bearer": { + "dbus-path": "--", + "settings": { + "apn": "--", + "ip-type": "--", + "password": "--", + "user": "--" + } + }, + "ue-mode-operation": "--" + }, + "imei": "867929068986514", + "operator-code": "--", + "operator-name": "--", + "packet-service-state": "--", + "pco": "--", + "registration-state": "--" + }, + "cdma": { + "activation-state": "--", + "cdma1x-registration-state": "--", + "esn": "--", + "evdo-registration-state": "--", + "meid": "--", + "nid": "--", + "sid": "--" + }, + "dbus-path": "/org/freedesktop/ModemManager1/Modem/5", + "generic": { + "access-technologies": [], + "bearers": [], + "carrier-configuration": "ROW_Generic_3GPP", + "carrier-configuration-revision": "0501081F", + "current-bands": [ + "egsm", + "dcs", + "pcs", + "g850", + "utran-1", + "utran-4", + "utran-6", + "utran-5", + "utran-8", + "utran-2", + "eutran-1", + "eutran-2", + "eutran-3", + "eutran-4", + "eutran-5", + "eutran-7", + "eutran-8", + "eutran-12", + "eutran-13", + "eutran-18", + "eutran-19", + "eutran-20", + "eutran-25", + "eutran-26", + "eutran-28", + "eutran-38", + "eutran-39", + "eutran-40", + "eutran-41", + "utran-19" + ], + "current-capabilities": [ + "gsm-umts, lte" + ], + "current-modes": "allowed: 2g, 3g, 4g; preferred: 4g", + "device": "/sys/devices/platform/axi/1000120000.pcie/1f00200000.usb/xhci-hcd.0/usb2/2-2/2-2.2", + "device-identifier": "b5bf9f66a67a0da6308a67fb858cdfef75d82565", + "drivers": [ + "option1", + "qmi_wwan" + ], + "equipment-identifier": "867929068986514", + "hardware-revision": "10000", + "manufacturer": "QUALCOMM INCORPORATED", + "model": "QUECTEL Mobile Broadband Module", + "own-numbers": [], + "physdev": "/sys/devices/platform/axi/1000120000.pcie/1f00200000.usb/xhci-hcd.0/usb2/2-2/2-2.2", + "plugin": "quectel", + "ports": [ + "cdc-wdm0 (qmi)", + "ttyUSB0 (ignored)", + "ttyUSB1 (gps)", + "ttyUSB2 (at)", + "ttyUSB3 (at)", + "wwan0 (net)" + ], + "power-state": "on", + "primary-port": "cdc-wdm0", + "primary-sim-slot": "1", + "revision": "EG25GGBR07A08M2G", + "signal-quality": { + "recent": "yes", + "value": "0" + }, + "sim": "--", + "sim-slots": [ + "/", + "/" + ], + "state": "failed", + "state-failed-reason": "sim-missing", + "supported-bands": [ + "egsm", + "dcs", + "pcs", + "g850", + "utran-1", + "utran-4", + "utran-6", + "utran-5", + "utran-8", + "utran-2", + "eutran-1", + "eutran-2", + "eutran-3", + "eutran-4", + "eutran-5", + "eutran-7", + "eutran-8", + "eutran-12", + "eutran-13", + "eutran-18", + "eutran-19", + "eutran-20", + "eutran-25", + "eutran-26", + "eutran-28", + "eutran-38", + "eutran-39", + "eutran-40", + "eutran-41", + "utran-19" + ], + "supported-capabilities": [ + "gsm-umts, lte" + ], + "supported-ip-families": [ + "ipv4", + "ipv6", + "ipv4v6" + ], + "supported-modes": [ + "allowed: 2g; preferred: none", + "allowed: 3g; preferred: none", + "allowed: 4g; preferred: none", + "allowed: 2g, 3g; preferred: 3g", + "allowed: 2g, 3g; preferred: 2g", + "allowed: 2g, 4g; preferred: 4g", + "allowed: 2g, 4g; preferred: 2g", + "allowed: 3g, 4g; preferred: 4g", + "allowed: 3g, 4g; preferred: 3g", + "allowed: 2g, 3g, 4g; preferred: 4g", + "allowed: 2g, 3g, 4g; preferred: 3g", + "allowed: 2g, 3g, 4g; preferred: 2g" + ], + "unlock-required": "--", + "unlock-retries": [] + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/signal.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/signal.json new file mode 100644 index 00000000..190b5bee --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/signal.json @@ -0,0 +1 @@ +error: modem has no extended signal capabilities diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/sim.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/sim.json new file mode 100644 index 00000000..2fbcda33 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/sim.json @@ -0,0 +1 @@ +error: couldn't find SIM diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-home-network.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-home-network.txt new file mode 100644 index 00000000..77b9a738 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-home-network.txt @@ -0,0 +1 @@ +error: couldn't get home network: QMI protocol error (37): 'UimUninitialized' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-rf-band-info.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-rf-band-info.txt new file mode 100644 index 00000000..9439faa2 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-rf-band-info.txt @@ -0,0 +1,9 @@ +[/dev/cdc-wdm0] Successfully got RF band info +Band Information: + Radio Interface: 'umts' + Active Band Class: 'wcdma-900' + Active Channel: '3025' +Band Information (Extended): + Radio Interface: 'umts' + Active Band Class: 'wcdma-900' + Active Channel: '3025' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-serving-system.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-serving-system.txt new file mode 100644 index 00000000..11968fcf --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-serving-system.txt @@ -0,0 +1,26 @@ +[/dev/cdc-wdm0] Successfully got serving system: + Registration state: 'not-registered-searching' + CS: 'detached' + PS: 'detached' + Selected network: '3gpp' + Radio interfaces: '1' + [0]: 'umts' + Roaming status: 'on' + Data service capabilities: '0' + Current PLMN: + MCC: '234' + MNC: '10' + Description: '' + Roaming indicators: '1' + [0]: 'on' (umts) + Detailed status: + Status: 'limited' + Capability: 'cs-ps' + HDR Status: 'none' + HDR Hybrid: 'no' + Forbidden: 'no' + UMTS primary scrambling code: '460' + Full operator code info: + MCC: '234' + MNC: '10' + MNC with PCS digit: 'no' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-signal-info.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-signal-info.txt new file mode 100644 index 00000000..96b2ad32 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-signal-info.txt @@ -0,0 +1,4 @@ +[/dev/cdc-wdm0] Successfully got signal info +WCDMA: + RSSI: '-80 dBm' + ECIO: '-8.5 dBm' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/uim-get-card-status.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/uim-get-card-status.txt new file mode 100644 index 00000000..ec5c84bc --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/uim-get-card-status.txt @@ -0,0 +1,11 @@ +[/dev/cdc-wdm0] Successfully got card status +Provisioning applications: + Primary GW: session doesn't exist + Primary 1X: session doesn't exist + Secondary GW: session doesn't exist + Secondary 1X: session doesn't exist +Slot [1]: + Card state: 'error: no-atr-received (3)' + UPIN state: 'not-initialized' + UPIN retries: '0' + UPUK retries: '0' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/uim-read-transparent-gid1.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/uim-read-transparent-gid1.txt new file mode 100644 index 00000000..32dbd275 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/uim-read-transparent-gid1.txt @@ -0,0 +1 @@ +error: couldn't read transparent file from the UIM: QMI protocol error (3): 'Internal' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/location-status.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/location-status.json new file mode 100644 index 00000000..43f70bc0 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/location-status.json @@ -0,0 +1,24 @@ +{ + "modem": { + "location": { + "capabilities": [ + "3gpp-lac-ci", + "gps-raw", + "gps-nmea", + "gps-unmanaged", + "agps-msa", + "agps-msb" + ], + "enabled": [ + "3gpp-lac-ci" + ], + "gps": { + "assistance": [], + "assistance-servers": [], + "refresh-rate": "30", + "supl-server": "--" + }, + "signals": "no" + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/modem.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/modem.json new file mode 100644 index 00000000..bbc20422 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/modem.json @@ -0,0 +1,184 @@ +{ + "modem": { + "3gpp": { + "5gnr": { + "registration-settings": { + "drx-cycle": "--", + "mico-mode": "--" + } + }, + "enabled-locks": [ + "fixed-dialing" + ], + "eps": { + "initial-bearer": { + "dbus-path": "/org/freedesktop/ModemManager1/Bearer/2", + "settings": { + "apn": "internet", + "ip-type": "ipv4", + "password": "--", + "user": "--" + } + }, + "ue-mode-operation": "csps-2" + }, + "imei": "867929068986514", + "operator-code": "23410", + "operator-name": "O2 - UK", + "packet-service-state": "attached", + "pco": "--", + "registration-state": "home" + }, + "cdma": { + "activation-state": "--", + "cdma1x-registration-state": "--", + "esn": "--", + "evdo-registration-state": "--", + "meid": "--", + "nid": "--", + "sid": "--" + }, + "dbus-path": "/org/freedesktop/ModemManager1/Modem/6", + "generic": { + "access-technologies": [ + "lte" + ], + "bearers": [], + "carrier-configuration": "ROW_Generic_3GPP", + "carrier-configuration-revision": "0501081F", + "current-bands": [ + "egsm", + "dcs", + "pcs", + "g850", + "utran-1", + "utran-4", + "utran-6", + "utran-5", + "utran-8", + "utran-2", + "eutran-1", + "eutran-2", + "eutran-3", + "eutran-4", + "eutran-5", + "eutran-7", + "eutran-8", + "eutran-12", + "eutran-13", + "eutran-18", + "eutran-19", + "eutran-20", + "eutran-25", + "eutran-26", + "eutran-28", + "eutran-38", + "eutran-39", + "eutran-40", + "eutran-41", + "utran-19" + ], + "current-capabilities": [ + "gsm-umts, lte" + ], + "current-modes": "allowed: 2g, 3g, 4g; preferred: 4g", + "device": "/sys/devices/platform/axi/1000120000.pcie/1f00200000.usb/xhci-hcd.0/usb2/2-2/2-2.2", + "device-identifier": "b5bf9f66a67a0da6308a67fb858cdfef75d82565", + "drivers": [ + "option1", + "qmi_wwan" + ], + "equipment-identifier": "867929068986514", + "hardware-revision": "10000", + "manufacturer": "QUALCOMM INCORPORATED", + "model": "QUECTEL Mobile Broadband Module", + "own-numbers": [], + "physdev": "/sys/devices/platform/axi/1000120000.pcie/1f00200000.usb/xhci-hcd.0/usb2/2-2/2-2.2", + "plugin": "quectel", + "ports": [ + "cdc-wdm0 (qmi)", + "ttyUSB0 (ignored)", + "ttyUSB1 (gps)", + "ttyUSB2 (at)", + "ttyUSB3 (at)", + "wwan0 (net)" + ], + "power-state": "on", + "primary-port": "cdc-wdm0", + "primary-sim-slot": "1", + "revision": "EG25GGBR07A08M2G", + "signal-quality": { + "recent": "yes", + "value": "68" + }, + "sim": "/org/freedesktop/ModemManager1/SIM/2", + "sim-slots": [ + "/org/freedesktop/ModemManager1/SIM/2", + "/" + ], + "state": "registered", + "state-failed-reason": "--", + "supported-bands": [ + "egsm", + "dcs", + "pcs", + "g850", + "utran-1", + "utran-4", + "utran-6", + "utran-5", + "utran-8", + "utran-2", + "eutran-1", + "eutran-2", + "eutran-3", + "eutran-4", + "eutran-5", + "eutran-7", + "eutran-8", + "eutran-12", + "eutran-13", + "eutran-18", + "eutran-19", + "eutran-20", + "eutran-25", + "eutran-26", + "eutran-28", + "eutran-38", + "eutran-39", + "eutran-40", + "eutran-41", + "utran-19" + ], + "supported-capabilities": [ + "gsm-umts, lte" + ], + "supported-ip-families": [ + "ipv4", + "ipv6", + "ipv4v6" + ], + "supported-modes": [ + "allowed: 2g; preferred: none", + "allowed: 3g; preferred: none", + "allowed: 4g; preferred: none", + "allowed: 2g, 3g; preferred: 3g", + "allowed: 2g, 3g; preferred: 2g", + "allowed: 2g, 4g; preferred: 4g", + "allowed: 2g, 4g; preferred: 2g", + "allowed: 3g, 4g; preferred: 4g", + "allowed: 3g, 4g; preferred: 3g", + "allowed: 2g, 3g, 4g; preferred: 4g", + "allowed: 2g, 3g, 4g; preferred: 3g", + "allowed: 2g, 3g, 4g; preferred: 2g" + ], + "unlock-required": "sim-pin2", + "unlock-retries": [ + "sim-pin (3)", + "sim-puk (10)", + "sim-pin2 (3)", + "sim-puk2 (10)" + ] + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/signal.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/signal.json new file mode 100644 index 00000000..5d4fcadd --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/signal.json @@ -0,0 +1,48 @@ +{ + "modem": { + "signal": { + "5g": { + "error-rate": "--", + "rsrp": "--", + "rsrq": "--", + "snr": "--" + }, + "cdma1x": { + "ecio": "--", + "error-rate": "--", + "rssi": "--" + }, + "evdo": { + "ecio": "--", + "error-rate": "--", + "io": "--", + "rssi": "--", + "sinr": "--" + }, + "gsm": { + "error-rate": "--", + "rssi": "--" + }, + "lte": { + "error-rate": "--", + "rsrp": "--", + "rsrq": "--", + "rssi": "--", + "snr": "--" + }, + "refresh": { + "rate": "0" + }, + "threshold": { + "error-rate": "no", + "rssi": "0" + }, + "umts": { + "ecio": "--", + "error-rate": "--", + "rscp": "--", + "rssi": "--" + } + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/sim.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/sim.json new file mode 100644 index 00000000..050a27d5 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/sim.json @@ -0,0 +1,22 @@ +{ + "sim": { + "dbus-path": "/org/freedesktop/ModemManager1/SIM/2", + "properties": { + "active": "yes", + "eid": "--", + "emergency-numbers": [ + "999", + "00112" + ], + "esim-status": "--", + "gid1": "85FFFFFFFFFF47454E4945494E20202020202020", + "gid2": "FFFFFFFFFFFF2020202020202020202020202020", + "iccid": "8944110069073915392", + "imsi": "234103403359194", + "operator-code": "23410", + "operator-name": "--", + "removability": "--", + "sim-type": "--" + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-home-network.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-home-network.txt new file mode 100644 index 00000000..84351fca --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-home-network.txt @@ -0,0 +1,6 @@ +[/dev/cdc-wdm0] Successfully got home network: + Home network: + MCC: '234' + MNC: '10' + Description: 'O2 - UK' + Network name source: se13 diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-rf-band-info.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-rf-band-info.txt new file mode 100644 index 00000000..6a1adfe6 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-rf-band-info.txt @@ -0,0 +1,12 @@ +[/dev/cdc-wdm0] Successfully got RF band info +Band Information: + Radio Interface: 'lte' + Active Band Class: 'eutran-40' + Active Channel: '39250' +Band Information (Extended): + Radio Interface: 'lte' + Active Band Class: 'eutran-40' + Active Channel: '39250' +Bandwidth: + Radio Interface: 'lte' + Bandwidth: '20' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-serving-system.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-serving-system.txt new file mode 100644 index 00000000..b047d9cb --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-serving-system.txt @@ -0,0 +1,29 @@ +[/dev/cdc-wdm0] Successfully got serving system: + Registration state: 'registered' + CS: 'attached' + PS: 'attached' + Selected network: '3gpp' + Radio interfaces: '1' + [0]: 'lte' + Roaming status: 'off' + Data service capabilities: '1' + [0]: 'lte' + Current PLMN: + MCC: '234' + MNC: '10' + Description: 'O2 - UK' + Roaming indicators: '1' + [0]: 'off' (lte) + 3GPP location area code: '65534' + 3GPP cell ID: '153667976' + Detailed status: + Status: 'available' + Capability: 'cs-ps' + HDR Status: 'none' + HDR Hybrid: 'no' + Forbidden: 'no' + LTE tracking area code: '14768' + Full operator code info: + MCC: '234' + MNC: '10' + MNC with PCS digit: 'no' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-signal-info.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-signal-info.txt new file mode 100644 index 00000000..2cef5e91 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-signal-info.txt @@ -0,0 +1,6 @@ +[/dev/cdc-wdm0] Successfully got signal info +LTE: + RSSI: '-71 dBm' + RSRQ: '-14 dB' + RSRP: '-102 dBm' + SNR: '8.2 dB' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/uim-get-card-status.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/uim-get-card-status.txt new file mode 100644 index 00000000..07aedb5e --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/uim-get-card-status.txt @@ -0,0 +1,37 @@ +[/dev/cdc-wdm0] Successfully got card status +Provisioning applications: + Primary GW: slot '1', application '1' + Primary 1X: session doesn't exist + Secondary GW: session doesn't exist + Secondary 1X: session doesn't exist +Slot [1]: + Card state: 'present' + UPIN state: 'not-initialized' + UPIN retries: '0' + UPUK retries: '0' + Application [1]: + Application type: 'usim (2)' + Application state: 'ready' + Application ID: + A0:00:00:00:87:10:02:FF:44:FF:FF:89:01:01:01:00 + Personalization state: 'ready' + UPIN replaces PIN1: 'no' + PIN1 state: 'disabled' + PIN1 retries: '3' + PUK1 retries: '10' + PIN2 state: 'enabled-not-verified' + PIN2 retries: '3' + PUK2 retries: '10' + Application [2]: + Application type: 'unknown (0)' + Application state: 'detected' + Application ID: + A0:00:00:00:63:50:4B:FF:34:FF:07:89:54:46:52:38 + Personalization state: 'unknown' + UPIN replaces PIN1: 'no' + PIN1 state: 'not-initialized' + PIN1 retries: '0' + PUK1 retries: '0' + PIN2 state: 'not-initialized' + PIN2 retries: '0' + PUK2 retries: '0' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/uim-read-transparent-gid1.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/uim-read-transparent-gid1.txt new file mode 100644 index 00000000..b0a6f712 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/uim-read-transparent-gid1.txt @@ -0,0 +1,7 @@ +[/dev/cdc-wdm0] Successfully read information from the UIM: +Card result: + SW1: '0x90' + SW2: '0x00' +Read result: + 85:FF:FF:FF:FF:FF:47:45:4E:49:45:49:4E:20:20:20:20:20:20:20 + diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/mmcli/mm-disable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/mmcli/mm-disable.txt new file mode 100644 index 00000000..302ea84f --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/mmcli/mm-disable.txt @@ -0,0 +1 @@ +successfully disabled the modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/mmcli/mm-enable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/mmcli/mm-enable.txt new file mode 100644 index 00000000..355006be --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/mmcli/mm-enable.txt @@ -0,0 +1 @@ +successfully enabled the modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/qmicli/sim-power-off.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/qmicli/sim-power-off.txt new file mode 100644 index 00000000..8dc98eda --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/qmicli/sim-power-off.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power off diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/qmicli/sim-power-on.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/qmicli/sim-power-on.txt new file mode 100644 index 00000000..b29806cd --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/qmicli/sim-power-on.txt @@ -0,0 +1 @@ +error: could not power on SIM: QMI protocol error (26): 'NoEffect' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/mmcli/mm-disable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/mmcli/mm-disable.txt new file mode 100644 index 00000000..302ea84f --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/mmcli/mm-disable.txt @@ -0,0 +1 @@ +successfully disabled the modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/mmcli/mm-enable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/mmcli/mm-enable.txt new file mode 100644 index 00000000..355006be --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/mmcli/mm-enable.txt @@ -0,0 +1 @@ +successfully enabled the modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/qmicli/sim-power-off.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/qmicli/sim-power-off.txt new file mode 100644 index 00000000..8dc98eda --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/qmicli/sim-power-off.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power off diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/qmicli/sim-power-on.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/qmicli/sim-power-on.txt new file mode 100644 index 00000000..0b8e19c7 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/qmicli/sim-power-on.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power on diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/mmcli/mm-disable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/mmcli/mm-disable.txt new file mode 100644 index 00000000..049e0b85 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/mmcli/mm-disable.txt @@ -0,0 +1 @@ +error: couldn't find modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/mmcli/mm-enable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/mmcli/mm-enable.txt new file mode 100644 index 00000000..049e0b85 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/mmcli/mm-enable.txt @@ -0,0 +1 @@ +error: couldn't find modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/qmicli/sim-power-off.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/qmicli/sim-power-off.txt new file mode 100644 index 00000000..8dc98eda --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/qmicli/sim-power-off.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power off diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/qmicli/sim-power-on.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/qmicli/sim-power-on.txt new file mode 100644 index 00000000..27c2c4ec --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/qmicli/sim-power-on.txt @@ -0,0 +1 @@ +error: could not power on SIM: QMI protocol error (3): 'Internal' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/mmcli/mm-disable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/mmcli/mm-disable.txt new file mode 100644 index 00000000..8b922995 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/mmcli/mm-disable.txt @@ -0,0 +1 @@ +error: couldn't disable the modem: 'GDBus.Error:org.freedesktop.ModemManager1.Error.Core.WrongState: modem in failed state' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/mmcli/mm-enable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/mmcli/mm-enable.txt new file mode 100644 index 00000000..668b1288 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/mmcli/mm-enable.txt @@ -0,0 +1 @@ +error: couldn't enable the modem: 'GDBus.Error:org.freedesktop.ModemManager1.Error.Core.WrongState: modem in failed state' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/qmicli/sim-power-off.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/qmicli/sim-power-off.txt new file mode 100644 index 00000000..8dc98eda --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/qmicli/sim-power-off.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power off diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/qmicli/sim-power-on.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/qmicli/sim-power-on.txt new file mode 100644 index 00000000..0b8e19c7 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/qmicli/sim-power-on.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power on diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/mmcli/mm-disable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/mmcli/mm-disable.txt new file mode 100644 index 00000000..8b922995 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/mmcli/mm-disable.txt @@ -0,0 +1 @@ +error: couldn't disable the modem: 'GDBus.Error:org.freedesktop.ModemManager1.Error.Core.WrongState: modem in failed state' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/mmcli/mm-enable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/mmcli/mm-enable.txt new file mode 100644 index 00000000..668b1288 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/mmcli/mm-enable.txt @@ -0,0 +1 @@ +error: couldn't enable the modem: 'GDBus.Error:org.freedesktop.ModemManager1.Error.Core.WrongState: modem in failed state' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/qmicli/sim-power-off.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/qmicli/sim-power-off.txt new file mode 100644 index 00000000..8dc98eda --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/qmicli/sim-power-off.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power off diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/qmicli/sim-power-on.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/qmicli/sim-power-on.txt new file mode 100644 index 00000000..27c2c4ec --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/qmicli/sim-power-on.txt @@ -0,0 +1 @@ +error: could not power on SIM: QMI protocol error (3): 'Internal' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/mm-disable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/mm-disable.txt new file mode 100644 index 00000000..8b922995 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/mm-disable.txt @@ -0,0 +1 @@ +error: couldn't disable the modem: 'GDBus.Error:org.freedesktop.ModemManager1.Error.Core.WrongState: modem in failed state' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/mm-enable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/mm-enable.txt new file mode 100644 index 00000000..668b1288 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/mm-enable.txt @@ -0,0 +1 @@ +error: couldn't enable the modem: 'GDBus.Error:org.freedesktop.ModemManager1.Error.Core.WrongState: modem in failed state' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/sim-power-off.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/sim-power-off.txt new file mode 100644 index 00000000..8dc98eda --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/sim-power-off.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power off diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/sim-power-on.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/sim-power-on.txt new file mode 100644 index 00000000..27c2c4ec --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/sim-power-on.txt @@ -0,0 +1 @@ +error: could not power on SIM: QMI protocol error (3): 'Internal' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/mmcli/mm-disable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/mmcli/mm-disable.txt new file mode 100644 index 00000000..049e0b85 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/mmcli/mm-disable.txt @@ -0,0 +1 @@ +error: couldn't find modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/mmcli/mm-enable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/mmcli/mm-enable.txt new file mode 100644 index 00000000..049e0b85 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/mmcli/mm-enable.txt @@ -0,0 +1 @@ +error: couldn't find modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/qmicli/sim-power-off.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/qmicli/sim-power-off.txt new file mode 100644 index 00000000..8dc98eda --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/qmicli/sim-power-off.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power off diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/qmicli/sim-power-on.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/qmicli/sim-power-on.txt new file mode 100644 index 00000000..0b8e19c7 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/qmicli/sim-power-on.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power on diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/mmcli/mm-disable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/mmcli/mm-disable.txt new file mode 100644 index 00000000..302ea84f --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/mmcli/mm-disable.txt @@ -0,0 +1 @@ +successfully disabled the modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/mmcli/mm-enable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/mmcli/mm-enable.txt new file mode 100644 index 00000000..355006be --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/mmcli/mm-enable.txt @@ -0,0 +1 @@ +successfully enabled the modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/qmicli/sim-power-off.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/qmicli/sim-power-off.txt new file mode 100644 index 00000000..8dc98eda --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/qmicli/sim-power-off.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power off diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/qmicli/sim-power-on.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/qmicli/sim-power-on.txt new file mode 100644 index 00000000..b29806cd --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/qmicli/sim-power-on.txt @@ -0,0 +1 @@ +error: could not power on SIM: QMI protocol error (26): 'NoEffect' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/sim_montior.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/sim_montior.txt new file mode 100644 index 00000000..36b5b041 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/sim_montior.txt @@ -0,0 +1,20 @@ +[/dev/cdc-wdm0] Received slot status indication: + Physical slot 1: + Card status: present + Slot status: active + Logical slot: 1 + ICCID: 8944110069073915392 + Protocol: uicc + Num apps: 0 + Is eUICC: no + Physical slot 2: + Card status: absent + Slot status: inactive +[/dev/cdc-wdm0] Received slot status indication: + Physical slot 1: + Card status: absent + Slot status: active + Logical slot: 1 + Physical slot 2: + Card status: absent + Slot status: inactive diff --git a/sprint-docs/modem-research/notes/modem_behaviour.md b/sprint-docs/modem-research/notes/modem_behaviour.md new file mode 100644 index 00000000..4593b54d --- /dev/null +++ b/sprint-docs/modem-research/notes/modem_behaviour.md @@ -0,0 +1,229 @@ +# EG25-new modem behaviour (for dummy modem modelling) + +This document summarises observed behaviour of an EG25 (new FW) modem from the mmcli/qmicli captures under +`scratchpad/modem-research/command_outputs/eg25-new`. It is intended to be the behavioural spec for the +test dummy modem. + +## High-level modem states + +There are two layers of "state": + +- **mmcli modem state**: `modem.generic.state` + - `failed` (e.g. `state-failed-reason = sim-missing`) + - `disabled` + - `registered` + - `connected` + +- **mmcli monitor state** (`mmcli -m X -w`, not fully captured here but implied by ModemManager docs and + behaviour): + - `enabling → enabled → searching → registered` + - `registered → connecting → connected` + - `connected → disconnecting → registered` + - SIM removal can be represented as a synthetic `no_sim` state at the HAL level. + +The HAL modem driver mainly looks at: + +- `modem.generic.state` (for failed vs non-failed) +- `modem["3gpp"]["registration-state"]` (for registered vs not) +- SIM presence via `modem.generic.sim` (`"--"` vs a SIM D-Bus path) + +## Data availability by state (mmcli) + +All observations below are from the JSON snapshots under `eg25-new/state/*/mmcli`. + +### `failed` (no SIM) + +Example: `state/failed-no-sim/mmcli`. + +- `mmcli -J -m` (modem.json) + - Succeeds. + - `modem.generic.state = "failed"`. + - `modem.generic["state-failed-reason"] = "sim-missing"`. + - `modem.generic.sim = "--"` (no SIM path). + - `modem["3gpp"]["registration-state"] = "--"`. + +- `mmcli --signal-get` (signal.json) + - Fails, output is a plain error string, not JSON: + - `error: modem has no extended signal capabilities` + - The HAL driver never calls `get_signal()` in this state because it gates on `generic.sim ~= "--"`. + +- `mmcli -J -i` (SIM info) + - Not applicable; there is no SIM D-Bus path in `modem.generic.sim`. + +### `disabled` (SIM present, modem disabled) + +Example: `state/disabled/mmcli`. + +- `mmcli -J -m` + - Succeeds. + - `modem.generic.state = "disabled"`. + - `modem.generic.sim` is a SIM path (e.g. `/org/freedesktop/ModemManager1/SIM/2`). + - `modem["3gpp"]["registration-state"] = "--"`. + +- `mmcli --signal-get` (signal.json) + - Succeeds and returns JSON with full `modem.signal` structure. + - All access technologies (`5g`, `cdma1x`, `evdo`, `gsm`, `lte`, `umts`) are present, but all metrics + are `"--"` (no meaningful signal values while disabled). + +- `mmcli -J -i` (SIM info) + - Succeeds when called with the SIM path from `modem.generic.sim`. + +### `registered` + +Example: `state/registered/mmcli`. + +- `mmcli -J -m` + - Succeeds. + - `modem.generic.state = "registered"`. + - `modem.generic.sim` is a SIM path. + - `modem["3gpp"]["registration-state"] = "home"`. + - `modem["3gpp"].operator-code` and `.operator-name` are populated (e.g. `23410`, `"O2 - UK"`). + - `modem["3gpp"]["packet-service-state"] = "attached"`. + - `modem.generic["access-technologies"]` contains `"lte"`. + +- `mmcli --signal-get` (signal.json) + - Succeeds with the same JSON structure as in `disabled`. + - For this firmware, extended signal metrics are still all `"--"`; HAL falls back to QMI NAS for + real RSRP/RSRQ/SNR via `nas-get-signal-info`. + +- `mmcli -J -i` (SIM info) + - Succeeds and returns SIM details. + +### `connected` + +Example: `state/connected/mmcli`. + +- `mmcli -J -m` + - Succeeds. + - `modem.generic.state = "connected"`. + - `modem.generic.bearers` contains one or more bearer paths. + - `modem["3gpp"]["registration-state"] = "home"` and `packet-service-state = "attached"`. + - `modem.generic["access-technologies"]` still includes `"lte"`. + +- `mmcli --signal-get` (signal.json) + - Same structure as `disabled`/`registered` and still all `"--"` for extended metrics. + +## Command behaviour by state (mmcli) + +From the transition captures under `eg25-new/transitions`: + +- `mmcli -m X -e` / `-d` while **modem is in `failed` state**: + - Both enable and disable fail with `WrongState` errors, e.g.: + - `error: couldn't enable the modem: 'GDBus.Error:org.freedesktop.ModemManager1.Error.Core.WrongState: modem in failed state'` + - `error: couldn't disable the modem: 'GDBus.Error:org.freedesktop.ModemManager1.Error.Core.WrongState: modem in failed state'` + - Dummy modem should refuse to change state when `enable`/`disable` are called in `FAILED`. + +- `mmcli -m X -e` / `-d` in **normal states**: + - From `connected` → `mmcli -d` succeeds and drives state towards `disabled`. + - From `disabled` → `mmcli -e` will eventually drive the monitor state sequence + `enabling → enabled → searching → registered` (not all steps are captured here but observed in HAL). + +## Data availability by state (qmicli) + +Using captures under `eg25-new/state/*/qmicli`. + +### UIM card status (SIM presence) + +- **Disabled (SIM present)** – `state/disabled/qmicli/uim-get-card-status.txt`: + - Slot [1]: `Card state: 'present'`. + - Full application list is reported (`Application [1]` usim ready, etc.). + - This is the "healthy SIM inserted" baseline. + +- **Failed, no SIM** – `state/failed-no-sim/qmicli/uim-get-card-status.txt`: + - Slot [1]: `Card state: 'error: no-atr-received (3)'`. + - No applications are listed. + - This aligns with `modem.generic.state = "failed"` and `state-failed-reason = "sim-missing"`. + +### UIM read transparent (GID1) + +- In non-failed, SIM-present states, `uim-read-transparent` returns a `Card result` and `Read result` + section with a hex string for GID1. +- In `failed`/no-SIM, this command is expected to fail or return no meaningful `Read result`. + - The HAL only calls `uim_get_gids()` when `modem.generic.state ~= 'failed'`. + +### NAS info (home network + signal) + +- `nas-get-home-network`: + - Only meaningful when `modem["3gpp"]["registration-state"] ~= "--"` (i.e. registered/connected). + - Returns MCC/MNC and description; the HAL uses this for MCC/MNC and operator info. + +- `nas-get-signal-info`: + - Returns LTE `RSSI`, `RSRQ`, `RSRP`, `SNR` when attached/registered/connected. + - This is the primary source of usable signal metrics for EG25-new, given that + `mmcli --signal-get` lacks extended values. + +## SIM power / presence behaviour + +From experiments scripted in `collect_modem_transitions.sh` and the user observations: + +- **SIM power-cycle with SIM inserted** (`qmicli --uim-sim-power-off=1` then `--uim-sim-power-on=1`): + - Causes the modem to effectively **disappear and reappear** from ModemManager's point of view. + - In practice this looks like a remove/add cycle on the USB device and a new D-Bus modem path. + - A modem that was in `failed` will come back as a **fresh modem in `disabled` state** once the SIM + is powered back on. + - This is the primary recovery path out of `failed(sim-missing)`. + +- **SIM removal** (physical SIM pulled out): + - Results in loss of SIM and subsequent `sim-missing` behaviour. + - In practice, the modem is observed to disappear and reappear from ModemManager, returning in a + `failed` state with `state-failed-reason = "sim-missing"` and `generic.sim = "--"`. + +For dummy modelling this implies: + +- When SIM power is toggled **off then on while a SIM is inserted**: + - Emit a modem `(-)` removal event on the mmcli monitor bus, then a `(+)` add event with a **new** + modem address. + - Reset the modem state machine to `DISABLED` with no bearer/registration state. + - Clear any previous failure reason. + +- When a SIM is **removed**: + - Emit a modem `(-)` removal event, then a `(+)` add event with a (potentially) new modem address. + - Recreate the modem in `FAILED` state with `state-failed-reason = "sim-missing"` and + `generic.sim = "--"`. + +## Driver-facing behavioural rules (what the dummy must respect) + +Summarising what the HAL modem driver expects from the underlying tools: + +- `get_modem_info()` (mmcli -J -m): + - Always succeeds in all states and reports at least: + - `modem.generic.state` + - `modem["3gpp"]["registration-state"]` + - `modem.generic.sim` (SIM path or `"--"`) + - Driver/mode/model fields (plugin, model, revision, drivers, ports, primary-port, equipment-identifier). + +- `get_sim_info()` (mmcli -J -i): + - Only called when `modem.generic.sim ~= "--"`. + - Should fail (or not be callable) when there is no SIM path. + +- `get_signal()` (mmcli --signal-get): + - Only called when `modem.generic.sim ~= "--"`. + - May legitimately fail or return no valid signals; driver will then report an error and move on. + - In `failed`/no-SIM state, the dummy should mimic `mmcli` by returning a non-JSON error string so + any accidental call fails cleanly. + +- `get_nas_info()` / `nas_get_rf_band_info()` / `uim_get_gids()` (QMI): + - `get_nas_info()` is only called when `modem["3gpp"]["registration-state"] ~= "--"`. + - `uim_get_gids()` is only called when `modem.generic.state ~= 'failed'`. + - The dummy should enforce the same gating and either return errors or empty results when invoked + outside these conditions. + +- `enable()` / `disable()` (mmcli -e/-d): + - Must fail with a `WrongState`-style error when current state is `FAILED`. + - Otherwise should drive the state-machine transitions described above via the monitor stream. + +These rules, plus the state/command availability matrices above, are what the dummy modem should +implement to behave like a realistic EG25-new instance. + +## QMI slot monitor + +The HAL uses `qmicli --uim-monitor-slot-status` (wrapped by `monitor_slot_status()` and parsed by +`utils.parse_slot_monitor`) to detect SIM insertion/removal events at runtime. + +- The dummy modem will need to emit slot-monitor lines that `parse_slot_monitor` can interpret as + transitions between "present" and "not present". +- Capturing some real `--uim-monitor-slot-status` output for: + - SIM present, + - SIM removed, + - SIM power-cycled, + would be useful to ensure the dummy's slot-monitor stream matches reality. From f28bf571c49f87aa4469f67bb89e9a2c754f422c Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Mon, 12 Jan 2026 14:27:55 +0000 Subject: [PATCH 16/20] temp change to sleep as yeild branch --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index 1e214d01..e4247eb7 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -FIBERS_VER=43a04d1 +FIBERS_VER=sleep-as-yield TRIE_VER=28b3572 BUS_VER=89af71a UI_VER=a8c5965 From 259037fc341f62814bab049962411c083ee38d20 Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Tue, 13 Jan 2026 16:42:12 +0000 Subject: [PATCH 17/20] Changed test bench to control modem events more verbosly --- tests/hal/harness/backends/mmcli.lua | 179 +++++---------------- tests/hal/harness/backends/qmicli.lua | 91 +++-------- tests/hal/test_manager_modemcard.lua | 205 ++++++++++++++++-------- tests/utils/ShimCommands.lua | 222 ++++++-------------------- tests/utils/mock.lua | 67 ++++++++ 5 files changed, 314 insertions(+), 450 deletions(-) create mode 100644 tests/utils/mock.lua diff --git a/tests/hal/harness/backends/mmcli.lua b/tests/hal/harness/backends/mmcli.lua index 601b3545..bac5093b 100644 --- a/tests/hal/harness/backends/mmcli.lua +++ b/tests/hal/harness/backends/mmcli.lua @@ -1,171 +1,79 @@ -local channel = require 'fibers.channel' -local commands = require 'tests.utils.ShimCommands' -local modem_registry = require 'tests.hal.harness.devices.modem_registry' +local commands = {} + +local function set_command(name, cmd) + commands[name] = cmd +end -local monitor_modems_cmd = commands.new_command() -- We only ever need one instance local function monitor_modems() - return monitor_modems_cmd + if not commands.monitor_modems then error("monitor_modems command not set up") end + return commands.monitor_modems end -local inhibit_cmds = {} local function inhibit(device) - local cmd = commands.new_command() - inhibit_cmds[device] = cmd - - local modem = modem_registry.get_by_address(device) - if modem and modem.on_mmcli_inhibit_start then - cmd.on_start = function() - return modem:on_mmcli_inhibit_start(cmd) - end - end - if modem and modem.on_mmcli_inhibit_end then - cmd.on_kill = function() - modem:on_mmcli_inhibit_end(cmd) - end - end - - return cmd + if not commands.inhibit then error("inhibit command not set up") end + return commands.inhibit end -local connect_cmds = {} local function connect(ctx, device, connection_string) - if not connect_cmds[device] then - connect_cmds[device] = {} - end - local cmd = commands.new_command() - table.insert(connect_cmds[device], cmd) - - local modem = modem_registry.get_by_address(device) - if modem and modem.on_mmcli_connect then - cmd.on_start = function() - return modem:on_mmcli_connect(cmd, connection_string) - end - end - - return cmd + if not commands.connect then error("connect command not set up") end + return commands.connect end -local disconnect_cmds = {} local function disconnect(ctx, device) - local cmd = commands.new_command() - disconnect_cmds[device] = cmd - - local modem = modem_registry.get_by_address(device) - if modem and modem.on_mmcli_disconnect then - cmd.on_start = function() - return modem:on_mmcli_disconnect(cmd) - end - end - - return cmd + if not commands.disconnect then error("disconnect command not set up") end + return commands.disconnect end -local reset_cmds = {} local function reset(ctx, device) - local cmd = commands.new_command() - reset_cmds[device] = cmd - - local modem = modem_registry.get_by_address(device) - if modem and modem.on_mmcli_reset then - cmd.on_start = function() - return modem:on_mmcli_reset(cmd) - end - end - - return cmd + if not commands.reset then error("reset command not set up") end + return commands.reset end -local enable_cmds = {} local function enable(ctx, device) - local cmd = commands.new_command() - enable_cmds[device] = cmd - - local modem = modem_registry.get_by_address(device) - if modem and modem.on_mmcli_enable then - cmd.on_start = function() - return modem:on_mmcli_enable(cmd) - end - end - - return cmd + if not commands.enable then error("enable command not set up") end + return commands.enable end -local disable_cmds = {} local function disable(ctx, device) - local cmd = commands.new_command() - disable_cmds[device] = cmd - - local modem = modem_registry.get_by_address(device) - if modem and modem.on_mmcli_disable then - cmd.on_start = function() - return modem:on_mmcli_disable(cmd) - end - end - - return cmd + if not commands.disable then error("disable command not set up") end + return commands.disable end -local monitor_state_cmds = {} local function monitor_state(device) - local cmd = commands.new_command() - monitor_state_cmds[device] = cmd - - local modem = modem_registry.get_by_address(device) - if modem and modem.on_mmcli_monitor_state_start then - cmd.on_start = function() - return modem:on_mmcli_monitor_state_start(cmd) - end - end - - return cmd + if not commands.monitor_state then error("monitor_state command not set up") end + return commands.monitor_state end -local information_cmds = {} local function information(ctx, device) - if not information_cmds[device] then - information_cmds[device] = commands.new_static_command() - end - return information_cmds[device] + if not commands.information then error("information command not set up") end + return commands.information end -local sim_information_cmds = {} local function sim_information(ctx, device) - sim_information_cmds[device] = commands.new_command() - return sim_information_cmds[device] + if not commands.sim_information then error("sim_information command not set up") end + return commands.sim_information end -local location_status_cmds = {} local function location_status(ctx, device) - location_status_cmds[device] = commands.new_command() - return location_status_cmds[device] + if not commands.location_status then error("location_status command not set up") end + return commands.location_status end -local signal_setup_cmds = {} local function signal_setup(ctx, device, rate) - local cmd = commands.new_command() - signal_setup_cmds[device] = cmd - - local modem = modem_registry.get_by_address(device) - if modem and modem.on_mmcli_signal_setup then - cmd.on_start = function() - return modem:on_mmcli_signal_setup(cmd, rate) - end - end - - return cmd + if not commands.signal_setup then error("signal_setup command not set up") end + return commands.signal_setup end -local signal_get_cmds = {} local function signal_get(ctx, device) - signal_get_cmds[device] = commands.new_command() - return signal_get_cmds[device] + if not commands.signal_get then error("signal_get command not set up") end + return commands.signal_get end -local three_gpp_set_initial_eps_bearer_settings_cmds = {} local function three_gpp_set_initial_eps_bearer_settings(ctx, device, settings) - local settings_string = string.format("--3gpp-set-initial-eps-bearer-settings=%s", settings) - three_gpp_set_initial_eps_bearer_settings_cmds[device] = commands.new_command() - return three_gpp_set_initial_eps_bearer_settings_cmds[device] + if not commands.three_gpp_set_initial_eps_bearer_settings then + error("three_gpp_set_initial_eps_bearer_settings command not set up") + end + return commands.three_gpp_set_initial_eps_bearer_settings end return { @@ -183,18 +91,7 @@ return { signal_setup = signal_setup, signal_get = signal_get, three_gpp_set_initial_eps_bearer_settings = three_gpp_set_initial_eps_bearer_settings, - -- Exposed for test inspection - inhibit_cmds = inhibit_cmds, - connect_cmds = connect_cmds, - disconnect_cmds = disconnect_cmds, - reset_cmds = reset_cmds, - enable_cmds = enable_cmds, - disable_cmds = disable_cmds, - monitor_state_cmds = monitor_state_cmds, - information_cmds = information_cmds, - sim_information_cmds = sim_information_cmds, - location_status_cmds = location_status_cmds, - signal_setup_cmds = signal_setup_cmds, - signal_get_cmds = signal_get_cmds, - three_gpp_set_initial_eps_bearer_settings_cmds = three_gpp_set_initial_eps_bearer_settings_cmds + + -- Test harness only + set_command = set_command, } diff --git a/tests/hal/harness/backends/qmicli.lua b/tests/hal/harness/backends/qmicli.lua index 20f56b1f..edf04a69 100644 --- a/tests/hal/harness/backends/qmicli.lua +++ b/tests/hal/harness/backends/qmicli.lua @@ -1,87 +1,52 @@ -local commands = require "tests.utils.ShimCommands" -local modem_registry = require 'tests.hal.harness.devices.modem_registry' +local commands = {} --- For each function we create and track shim commands per *port*. +local function set_command(name, cmd) + commands[name] = cmd +end -local uim_get_card_status_cmds = {} local function uim_get_card_status(ctx, port) - uim_get_card_status_cmds[port] = commands.new_command() - return uim_get_card_status_cmds[port] + if not commands.uim_get_card_status then error("uim_get_card_status command not set up") end + return commands.uim_get_card_status end -local uim_sim_power_off_cmds = {} local function uim_sim_power_off(ctx, port) - local cmd = commands.new_command() - uim_sim_power_off_cmds[port] = cmd - - local modem = modem_registry.get_by_qmi_port(port) - if modem and modem.on_qmi_uim_sim_power_off then - cmd.on_start = function() - return modem:on_qmi_uim_sim_power_off(cmd, port) - end - end - - return cmd + if not commands.uim_sim_power_off then error("uim_sim_power_off command not set up") end + return commands.uim_sim_power_off end -local uim_sim_power_on_cmds = {} local function uim_sim_power_on(ctx, port) - local cmd = commands.new_command() - uim_sim_power_on_cmds[port] = cmd - - local modem = modem_registry.get_by_qmi_port(port) - if modem and modem.on_qmi_uim_sim_power_on then - cmd.on_start = function() - return modem:on_qmi_uim_sim_power_on(cmd, port) - end - end - - return cmd + if not commands.uim_sim_power_on then error("uim_sim_power_on command not set up") end + return commands.uim_sim_power_on end -local uim_monitor_slot_status_cmds = {} local function uim_monitor_slot_status(port) - local cmd = commands.new_command() - uim_monitor_slot_status_cmds[port] = cmd - - local modem = modem_registry.get_by_qmi_port(port) - if modem and modem.on_qmi_uim_monitor_start then - cmd.on_start = function() - return modem:on_qmi_uim_monitor_start(cmd, port) - end - end - - return cmd + if not commands.uim_monitor_slot_status then error("uim_monitor_slot_status command not set up") end + return commands.uim_monitor_slot_status end -local uim_read_transparent_cmds = {} local function uim_read_transparent(ctx, port, address_string) - uim_read_transparent_cmds[port] = commands.new_command() - return uim_read_transparent_cmds[port] + if not commands.uim_read_transparent then error("uim_read_transparent command not set up") end + return commands.uim_read_transparent end -local nas_get_rf_band_info_cmds = {} local function nas_get_rf_band_info(ctx, port) - nas_get_rf_band_info_cmds[port] = commands.new_command() - return nas_get_rf_band_info_cmds[port] + if not commands.nas_get_rf_band_info then error("nas_get_rf_band_info command not set up") end + return commands.nas_get_rf_band_info end -local nas_get_home_network_cmds = {} local function nas_get_home_network(ctx, port) - nas_get_home_network_cmds[port] = commands.new_command() - return nas_get_home_network_cmds[port] + if not commands.nas_get_home_network then error("nas_get_home_network command not set up") end + return commands.nas_get_home_network end -local nas_get_serving_system_cmds = {} local function nas_get_serving_system(ctx, port) - nas_get_serving_system_cmds[port] = commands.new_command() - return nas_get_serving_system_cmds[port] + if not commands.nas_get_serving_system then error("nas_get_serving_system command not set up") end + return commands.nas_get_serving_system end -local nas_get_signal_info_cmds = {} local function nas_get_signal_info(ctx, port) - nas_get_signal_info_cmds[port] = commands.new_command() - return nas_get_signal_info_cmds[port] + if not commands.nas_get_signal_info then error("nas_get_signal_info command not set up") end + return commands.nas_get_signal_info end return { @@ -96,14 +61,6 @@ return { nas_get_serving_system = nas_get_serving_system, nas_get_signal_info = nas_get_signal_info, - -- Exposed for test inspection, keyed by port - uim_get_card_status_cmds = uim_get_card_status_cmds, - uim_sim_power_off_cmds = uim_sim_power_off_cmds, - uim_sim_power_on_cmds = uim_sim_power_on_cmds, - uim_monitor_slot_status_cmds = uim_monitor_slot_status_cmds, - uim_read_transparent_cmds = uim_read_transparent_cmds, - nas_get_rf_band_info_cmds = nas_get_rf_band_info_cmds, - nas_get_home_network_cmds = nas_get_home_network_cmds, - nas_get_serving_system_cmds = nas_get_serving_system_cmds, - nas_get_signal_info_cmds = nas_get_signal_info_cmds, + -- Test harness only + set_command = set_command, } diff --git a/tests/hal/test_manager_modemcard.lua b/tests/hal/test_manager_modemcard.lua index f1b760d0..0b28678b 100644 --- a/tests/hal/test_manager_modemcard.lua +++ b/tests/hal/test_manager_modemcard.lua @@ -18,93 +18,164 @@ if is_entry_point then _G._TEST = true -- Enable test exports in source code local log = require 'services.log' local rxilog = require 'rxilog' - -- for _, mode in ipairs(rxilog.modes) do - -- log[mode.name] = function() end -- no-op logging during tests - -- end + for _, mode in ipairs(rxilog.modes) do + log[mode.name] = function() end -- no-op logging during tests, comment out to see logs + end end local luaunit = require 'luaunit' local fiber = require 'fibers.fiber' -local channel = require 'fibers.channel' local context = require 'fibers.context' local sleep = require 'fibers.sleep' +local channel = require 'fibers.channel' local harness = require 'tests.hal.harness' -local templates = require 'tests.hal.templates' -local dummy_modem = require 'tests.hal.harness.devices.modem' +local mock = require 'tests.utils.mock' +local commands = require 'tests.utils.ShimCommands' + +local function make_monitor_event(is_added, address) + local sign = is_added and '(+)' or '(-)' + return string.format("%s %s [DUMMY MANAFACUTER] Dummy Modem Module", sign, + address) +end + +local function release(module_path) + package.loaded[module_path] = nil +end TestHalModemcardManager = {} -function TestHalModemcardManager:test_modem_monitor_events() - local _, ctx, bus, conn, new_msg = harness.new_hal_env() - local manager = require 'services.hal.managers.modemcard'.new() - local device_event_q = channel.new() - local capability_info_q = channel.new() - manager:spawn(context.with_cancel(ctx), bus:connect(), device_event_q, capability_info_q) +function TestHalModemcardManager:test_detector() + local ctx = context.with_cancel(context.background()) + + -- Setup mmcli backend command mocks + local mmcli = require 'tests.hal.harness.backends.mmcli' + local mmcli_mock = mock.new_module( + "services.hal.drivers.modem.mmcli", + mmcli + ) + mmcli_mock:apply() + + local monitor_modems_cmd = commands.new_command(ctx) + mmcli.set_command("monitor_modems", monitor_modems_cmd) + + local modem_manager_module = require 'services.hal.managers.modemcard' + local modem_manager = modem_manager_module.new() + local modem_detect_ch = modem_manager.modem_detect_channel + local modem_remove_ch = modem_manager.modem_remove_channel + fiber.spawn(function() + modem_manager:_detector(ctx) + end) + + local address = "/org/freedesktop/ModemManager1/Modem/0" + + -- Simulate modem addition + monitor_modems_cmd.stdout_ch:put(make_monitor_event(true, address)) + local detected_address, err = harness.wait_for_channel(modem_detect_ch, ctx) + luaunit.assertNil(err) + luaunit.assertEquals(detected_address, address) - -- 1. No modems present - local nomodem = dummy_modem.no_modem() - local wr_err = nomodem:appear() - luaunit.assertNil(wr_err, "Failed to write to monitor command stdout") - local result, err = harness.wait_for_channel(device_event_q, ctx, 20) -- wait for no add event - luaunit.assertNil(result, "Did not expect a device event") + -- Simulate modem removal + monitor_modems_cmd.stdout_ch:put(make_monitor_event(false, address)) + local removed_address, err = harness.wait_for_channel(modem_remove_ch, ctx) + luaunit.assertNil(err) + luaunit.assertEquals(removed_address, address) + + -- Simulate no modems + monitor_modems_cmd.stdout_ch:put("No modems found") + local no_event, err = harness.wait_for_channel(modem_detect_ch, ctx) + luaunit.assertNil(no_event) luaunit.assertEquals(err, 'timeout') - -- pre 2&3. Create dummy modem - local modem = dummy_modem.new(context.with_cancel(ctx)) - modem:set_address_index("0") - modem:set_mmcli_information{ - modem = { - generic = { - device = "/fake/port0", - ["equipment-identifier"] = "123456789", - } - } - } + -- Verify command call counts + luaunit.assertEquals(monitor_modems_cmd.calls.start, 1) - -- 2. Modem 0 added - wr_err = modem:appear() - luaunit.assertNil(wr_err, "Failed to write to monitor command stdout") - - - result, err = harness.wait_for_channel(device_event_q, ctx, 20) -- wait for add event - -- ignore control object - if result.capabilities.modem then result.capabilities.modem.control = "" end - -- build expected event - local expected_event = templates.make_modem_device_event{ - connected = true, - data = { - port = "/fake/port0" - }, - capabilities = { - modem = { - id = "123456789", - control = "" -- we don't care about the control object here - } - } + ctx:cancel('test complete') + -- Cleanup modules from cache + mmcli_mock:clear() + release "services.hal.managers.modemcard" + sleep.sleep(0) -- allow fiber to exit + + luaunit.assertEquals(monitor_modems_cmd.calls.wait, 1) + luaunit.assertEquals(monitor_modems_cmd.calls.kill, 1) + luaunit.assertEquals(monitor_modems_cmd.calls.close, 1) +end + +function TestHalModemcardManager:test_manager() + local ctx = context.with_cancel(context.background()) + + -- Setup modem driver mock (this will be a driver instance) + local modem_mock = mock.new_object { + init = { nil }, + apply_capabilities = { {}, nil }, + spawn = {} } - luaunit.assertNotNil(result, "Expected a device event") - luaunit.assertEquals(err, nil) - luaunit.assertEquals(result, expected_event) - - -- 3. Modem 0 removed - wr_err = modem:disappear() - luaunit.assertNil(wr_err, "Failed to write to monitor command stdout") - result, err = harness.wait_for_channel(device_event_q, ctx, 20) -- wait for remove event - -- expected_event = make_expected_device_event(false, make_full_address("0")) - expected_event = templates.make_modem_device_event{ - connected = false, - data = { - port = "/fake/port0" + + local modem_inst = modem_mock:create_instance() + + -- Setup modem driver module mock (to return the driver instance) + local modem_driver_module_mock = mock.new_module( + "services.hal.drivers.modem", + { + -- a mock can take a function for dynamic behavior or table of return values for static behavior + new = function(mctx, address) + modem_inst.address = address + modem_inst.device = "dummy" + modem_inst.ctx = mctx + return modem_inst + end } - } - luaunit.assertNotNil(result, "Expected a device event") - luaunit.assertEquals(err, nil) - luaunit.assertEquals(result, expected_event) + ) + modem_driver_module_mock:apply() + + -- Setup mmcli backend command mocks + local mmcli = require 'tests.hal.harness.backends.mmcli' + local mmcli_mock = mock.new_module( + "services.hal.drivers.modem.mmcli", + mmcli + ) + mmcli_mock:apply() + + local modem_manager_module = require 'services.hal.managers.modemcard' + local modem_manager = modem_manager_module.new() + local modem_detect_ch = modem_manager.modem_detect_channel + local modem_remove_ch = modem_manager.modem_remove_channel + local device_event_q = channel.new() + local capability_info_q = channel.new() + fiber.spawn(function() + modem_manager:_manager( + ctx, + nil, + device_event_q, + capability_info_q + ) + end) + local address = "/org/freedesktop/ModemManager1/Modem/0" + + -- Simulate modem detection + modem_detect_ch:put(address) + local device_event, err = harness.wait_for_channel(device_event_q, ctx) + luaunit.assertNil(err) + luaunit.assertEquals(device_event.connected, true) + luaunit.assertEquals(device_event.data.port, "dummy") + luaunit.assertEquals(modem_inst._calls.init, 1) + luaunit.assertEquals(modem_inst._calls.apply_capabilities, 1) + luaunit.assertEquals(modem_inst._calls.spawn, 1) + + -- Simulate modem removal + modem_remove_ch:put(address) + local device_event, err = harness.wait_for_channel(device_event_q, ctx) + luaunit.assertNil(err) + luaunit.assertEquals(device_event.connected, false) + luaunit.assertEquals(device_event.data.port, "dummy") ctx:cancel('test complete') - sleep.sleep(0) -- allow manager to exit + -- Cleanup modules from cache + modem_driver_module_mock:clear() + mmcli_mock:clear() + release "services.hal.managers.modemcard" + sleep.sleep(0) -- allow fiber to exit end local function main() diff --git a/tests/utils/ShimCommands.lua b/tests/utils/ShimCommands.lua index 6b6816c7..ee418bcb 100644 --- a/tests/utils/ShimCommands.lua +++ b/tests/utils/ShimCommands.lua @@ -1,6 +1,8 @@ local channel = require 'fibers.channel' local context = require 'fibers.context' local op = require 'fibers.op' +local unpack = table.unpack or unpack +local dispatcher = require 'tests.utils.dispatcher' local COMMAND_STATE = { CREATED = 'created', @@ -13,24 +15,17 @@ local Pipe = {} Pipe.__index = Pipe function Pipe:read_line_op() - if not self.ch then - error("attempt to read from closed pipe") - end return self.ch:get_op() end function Pipe:close() - -- In the real exec implementation, pipes can be closed - -- independently of the process lifecycle. For the shim we - -- therefore allow close() regardless of the parent command - -- state and simply drop the underlying channel reference. - self.ch = nil + self.parent_cmd.calls.close = self.parent_cmd.calls.close + 1 end -local function new_pipe(parent_cmd) +local function new_pipe(parent_cmd, ch) local self = { parent_cmd = parent_cmd, - ch = channel.new() + ch = ch } return setmetatable(self, Pipe) end @@ -38,63 +33,51 @@ end local Command = {} Command.__index = Command -local function new_command() +local function new_command(ctx, static_returns) local self = { - state = COMMAND_STATE.CREATED, - ctx = context.with_cancel(context.background()), - stdout = nil, - stderr = nil, - -- Optional lifecycle hooks used by test backends to - -- trigger side-effects when commands are used. - on_start = nil, - on_kill = nil, + ctx = context.with_cancel(ctx), + stdout_ch = channel.new(), + calls = { + setprdeathsig = 0, + setpgid = 0, + start = 0, + run = 0, + wait = 0, + kill = 0, + close = 0, + }, + static_returns = static_returns or {}, } return setmetatable(self, Command) end function Command:start() - self.state = COMMAND_STATE.STARTED - if self.on_start then - return self.on_start(self) + self.calls.start = self.calls.start + 1 + if self.static_returns.start then + return unpack(self.static_returns.start) end - return nil end function Command:setprdeathsig(sig) - -- pass + self.calls.setprdeathsig = self.calls.setprdeathsig + 1 + if self.static_returns.setprdeathsig then + return unpack(self.static_returns.setprdeathsig) + end end function Command:setpgid() - -- pass -end - -function Command:stdout_pipe() - if not self.stdout then - self.stdout = new_pipe(self) + self.calls.setpgid = self.calls.setpgid + 1 + if self.static_returns.setpgid then + return unpack(self.static_returns.setpgid) end - return self.stdout end -function Command:stderr_pipe() - if not self.stderr then - self.stderr = new_pipe(self) - end - return self.stderr +function Command:stdout_pipe() + return new_pipe(self, self.stdout_ch) end function Command:combined_output() local out_pipe = self:stdout_pipe() - local err_pipe = self:stderr_pipe() - - -- In the real exec implementation, the process is started - -- before output is read. Mirror that behaviour here so that - -- any on_start side-effects are applied exactly once. - if self.state == COMMAND_STATE.CREATED then - local err = self:start() - if err then - return "", err - end - end local buf = "" local continue = true @@ -108,8 +91,7 @@ function Command:combined_output() while continue and not self.ctx:err() do local read_op = op.choice( - out_pipe:read_line_op(), - err_pipe:read_line_op() + out_pipe:read_line_op() ):wrap(push_data) op.choice( read_op, @@ -121,145 +103,35 @@ function Command:combined_output() end function Command:run() - -- Simplified exec-style run: ensure the command has started and - -- propagate any start error. We do not currently accumulate - -- stdout/stderr here as existing callers only check the error. - if self.state == COMMAND_STATE.CREATED then - local err = self:start() - if err then - self.state = COMMAND_STATE.FLUSHED - return err - end + self.calls.run = self.calls.run + 1 + if self.static_returns.run then + return unpack(self.static_returns.run) end - self.state = COMMAND_STATE.FLUSHED - return nil end function Command:wait() - if self.state == COMMAND_STATE.KILLED then - self.state = COMMAND_STATE.FLUSHED + self.calls.wait = self.calls.wait + 1 + if self.static_returns.wait then + return unpack(self.static_returns.wait) end end function Command:kill() self.ctx:cancel('killed') - self.state = COMMAND_STATE.KILLED - if self.on_kill then - self.on_kill(self) - end -end - -function Command:close() - self.ctx:cancel('ended') -end - -function Command:write_out(data) - if not self.stdout then - return 'stdout pipe not set' + self.calls.kill = self.calls.kill + 1 + if self.static_returns.kill then + return unpack(self.static_returns.kill) end - self.stdout.ch:put(data) end -function Command:write_err(data) - if not self.stderr then - return 'stderr pipe not set' - end - self.stderr.ch:put(data) -end - -local StaticCommand = {} -StaticCommand.__index = StaticCommand - -local function new_static_command() - local self = { - bse_cmd = new_command(), - out = "", - err = "", - } - return setmetatable(self, StaticCommand) -end - -function StaticCommand:start() - return self.bse_cmd:start() -end - -function StaticCommand:setprdeathsig(sig) - return self.bse_cmd:setprdeathsig(sig) -end - -function StaticCommand:setpgid() - return self.bse_cmd:setpgid() -end - -function StaticCommand:stdout_pipe() - error("unimplemented") -end - -function StaticCommand:stderr_pipe() - error("unimplemented") -end - -function StaticCommand:combined_output() - return self.out .. self.err -end - -function StaticCommand:wait() - return self.bse_cmd:wait() -end - -function StaticCommand:kill() - return self.bse_cmd:kill() -end - -function StaticCommand:close() - return self.bse_cmd:close() -end - -function StaticCommand:write_out(data) - self.out = data -end - -function StaticCommand:write_err(data) - self.err = data -end - -local BroadcastCommand = {} -BroadcastCommand.__index = BroadcastCommand - -function BroadcastCommand:new_child() - local child = new_command() - table.insert(self.children, child) - return child -end - -function BroadcastCommand:write_out(data) - for _, child in ipairs(self.children) do - local err = child:write_out(data) - if err then - return err - end - end -end - -function BroadcastCommand:write_err(data) - for _, child in ipairs(self.children) do - local err = child:write_err(data) - if err then - return err - end - end -end - -local function new_broadcast_command() - local self = { - children = {} - } - - return setmetatable(self, BroadcastCommand) -end +-- function Command:close() +-- self.ctx:cancel('ended') +-- self.calls.close = self.calls.close + 1 +-- if self.static_returns.close then +-- return unpack(self.static_returns.close) +-- end +-- end return { - new_broadcast_command = new_broadcast_command, - new_static_command = new_static_command, new_command = new_command } diff --git a/tests/utils/mock.lua b/tests/utils/mock.lua new file mode 100644 index 00000000..b6ff82db --- /dev/null +++ b/tests/utils/mock.lua @@ -0,0 +1,67 @@ +local unpack = table.unpack or unpack + +local function build_mock_function(on_run) + return function(...) + if type(on_run) ~= "function" and type(on_run) ~= "table" then + error("Invalid on_run type for mock function") + end + if type(on_run) == "function" then + return on_run(...) + end + return unpack(on_run) + end +end + +local ModuleMock = {} +ModuleMock.__index = ModuleMock + +function ModuleMock:apply() + if not self.module_path then return end + package.loaded[self.module_path] = self +end + +function ModuleMock:clear() + if not self.module_path then return end + package.loaded[self.module_path] = nil +end + +local ObjectMock = {} +ObjectMock.__index = ObjectMock + +function ObjectMock:create_instance() + local instance = setmetatable({ _calls = {} }, ObjectMock) + for k, v in pairs(self.method_table) do + if k ~= "_calls" then + local fn = build_mock_function(v) + instance[k] = function(...) + instance._calls[k] = instance._calls[k] + 1 + return fn(...) + end + instance._calls[k] = 0 + end + end + return instance +end + +local function new_module(module_path, method_table) + local mock = setmetatable({ _calls = {}, module_path = module_path }, ModuleMock) + for k, v in pairs(method_table) do + local fn = build_mock_function(v) + mock[k] = function(...) + mock._calls[k] = mock._calls[k] + 1 + return fn(...) + end + mock._calls[k] = 0 + end + return mock +end + +local function new_object(method_table) + local mock = setmetatable({ method_table = method_table }, ObjectMock) + return mock +end + +return { + new_module = new_module, + new_object = new_object +} From e1e72ac2b93f16e9d9a70975fd3b98af045b805a Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Wed, 14 Jan 2026 09:05:49 +0000 Subject: [PATCH 18/20] Moved utils --- tests/{test_utils => utils}/SimuCommands.lua | 0 tests/{test_utils => utils}/assertions.lua | 0 tests/{test_utils => utils}/shim_shifter.lua | 0 tests/{test_utils => utils}/utils.lua | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename tests/{test_utils => utils}/SimuCommands.lua (100%) rename tests/{test_utils => utils}/assertions.lua (100%) rename tests/{test_utils => utils}/shim_shifter.lua (100%) rename tests/{test_utils => utils}/utils.lua (100%) diff --git a/tests/test_utils/SimuCommands.lua b/tests/utils/SimuCommands.lua similarity index 100% rename from tests/test_utils/SimuCommands.lua rename to tests/utils/SimuCommands.lua diff --git a/tests/test_utils/assertions.lua b/tests/utils/assertions.lua similarity index 100% rename from tests/test_utils/assertions.lua rename to tests/utils/assertions.lua diff --git a/tests/test_utils/shim_shifter.lua b/tests/utils/shim_shifter.lua similarity index 100% rename from tests/test_utils/shim_shifter.lua rename to tests/utils/shim_shifter.lua diff --git a/tests/test_utils/utils.lua b/tests/utils/utils.lua similarity index 100% rename from tests/test_utils/utils.lua rename to tests/utils/utils.lua From d228e56f08f57653a45f469a7b8051dae6f19503 Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Fri, 23 Jan 2026 14:41:52 +0000 Subject: [PATCH 19/20] backends --- src/services/hal/backends/at.lua | 71 ++++++++++ src/services/hal/backends/iw.lua | 40 ++++++ src/services/hal/backends/mmcli.lua | 150 ++++++++++++++++++++ src/services/hal/backends/qmicli.lua | 107 ++++++++++++++ src/services/hal/backends/ubus.lua | 42 ++++++ src/services/hal/backends/uci.lua | 202 +++++++++++++++++++++++++++ 6 files changed, 612 insertions(+) create mode 100644 src/services/hal/backends/at.lua create mode 100644 src/services/hal/backends/iw.lua create mode 100644 src/services/hal/backends/mmcli.lua create mode 100644 src/services/hal/backends/qmicli.lua create mode 100644 src/services/hal/backends/ubus.lua create mode 100644 src/services/hal/backends/uci.lua diff --git a/src/services/hal/backends/at.lua b/src/services/hal/backends/at.lua new file mode 100644 index 00000000..df54eb8b --- /dev/null +++ b/src/services/hal/backends/at.lua @@ -0,0 +1,71 @@ +package.path = '/usr/lib/lua/?.lua;/usr/lib/lua/?/init.lua;' .. package.path + +local file = require 'fibers.stream.file' +local op = require 'fibers.op' + +local function trim(input) + -- Pattern matches non-printable characters and spaces at the start and end of the string + -- %c matches control characters, %s matches all whitespace characters + -- %z matches the character with representation 0x00 (NUL byte) + return (input:gsub("^[%c%s%z]+", ""):gsub("[%c%s%z]+$", "")) +end + +local function send_with_context(ctx, port, command) + local reader, err = file.open(port, "r") + if not reader then return nil, "error opening AT read port: "..err end + + local writer = assert(file.open(port, "w")) + if not writer then return nil, "error opening AT write port: "..err end + + -- file write + op.choice( + writer:write_chars_op(command..'\r'), + ctx:done_op() + ):perform() + + writer:close() + + if ctx:err() then reader:close() return nil, ctx:err() end + + local res = {} + + while true do + local line = op.choice( + reader:read_line_op(), + ctx:done_op() + ):perform() + + if ctx:err() then reader:close() return nil, ctx:err() end + if not line then reader:close() return nil, 'unknown error' end + + line = trim(line) + + -- check for non-descriptive success/fail + if line:find("^OK$") then + reader:close() + return res, nil + elseif line:find("^ERROR$") then + reader:close() + return res, 'error' + else + -- check for descriptive fail + local error_code + error_code = line:match("^%+CME ERROR: (%d+)$") + if error_code then + reader:close() + return res, error_code + end + error_code = line:match("^%+CMS ERROR: (%d+)$") + if error_code then + reader:close() + return res, error_code + end + end + + if #line > 0 then table.insert(res, line) end + end +end + +return { + send_with_context = send_with_context +} \ No newline at end of file diff --git a/src/services/hal/backends/iw.lua b/src/services/hal/backends/iw.lua new file mode 100644 index 00000000..f155a4b8 --- /dev/null +++ b/src/services/hal/backends/iw.lua @@ -0,0 +1,40 @@ +local exec = require "fibers.exec" +local utils = require "services.hal.drivers.wireless.utils" + +local function get_iw_dev_info(ctx, interface) + local out, err = exec.command_context(ctx, "iw", "dev", interface, "info"):output() + if err then + return nil, err + end + + return utils.format_iw_dev_info(out) +end + +local function get_iw_event_stream(ctx) + return exec.command_context(ctx, "iw", "event") +end + +local function get_client_info(ctx, interface, mac) + local out, err = exec.command_context(ctx, "iw", "dev", interface, "station", "get", mac):output() + if err then + return nil, err + end + + return utils.format_iw_client_info(out) +end + +local function get_dev_noise(ctx, interface) + local out, err = exec.command_context(ctx, "iw", interface, "survey", "dump"):output() + if err then + return nil, err + end + + return utils.parse_dev_noise(out) +end + +return { + get_iw_dev_info = get_iw_dev_info, + get_iw_event_stream = get_iw_event_stream, + get_client_info = get_client_info, + get_dev_noise = get_dev_noise +} diff --git a/src/services/hal/backends/mmcli.lua b/src/services/hal/backends/mmcli.lua new file mode 100644 index 00000000..efe67ba9 --- /dev/null +++ b/src/services/hal/backends/mmcli.lua @@ -0,0 +1,150 @@ +local exec = require "fibers.exec" + +local backend = {} + +function backend.monitor_modems() + return exec.command('mmcli', '-M') +end + +function backend.inhibit(device) + return exec.command('mmcli', '-m', device, '--inhibit') +end + +function backend.connect(ctx, device, connection_string) + connection_string = string.format("--simple-connect=%s", connection_string) + return exec.command_context(ctx, 'mmcli', '-m', device, connection_string) +end + +function backend.disconnect(ctx, device) + return exec.command_context(ctx, 'mmcli', '-m', device, '--simple-disconnect') +end + +function backend.reset(ctx, device) + return exec.command_context(ctx, 'mmcli', '-m', device, '-r') +end + +function backend.enable(ctx, device) + return exec.command_context(ctx, 'mmcli', '-m', device, '-e') +end + +function backend.disable(ctx, device) + return exec.command_context(ctx, 'mmcli', '-m', device, '-d') +end + +function backend.monitor_state(device) + return exec.command('mmcli', '-m', device, '-w') +end + +function backend.information(ctx, device) + return exec.command_context(ctx, 'mmcli', '-J', '-m', device) +end + +function backend.sim_information(ctx, device) + return exec.command_context(ctx, 'mmcli', '-J', '-i', device) +end +function backend.location_status(ctx, device) + return exec.command_context(ctx, 'mmcli', '-J', '-m', device, '--location-status') +end + +function backend.signal_setup(ctx, device, rate) + return exec.command_context(ctx, 'mmcli', '-m', device, '--signal-setup=' .. tostring(rate)) +end + +function backend.signal_get(ctx, device) + return exec.command_context(ctx, 'mmcli', '-J', '-m', device, '--signal-get') +end + +function backend.three_gpp_set_initial_eps_bearer_settings(ctx, device, settings) + local settings_string = string.format("--3gpp-set-initial-eps-bearer-settings=%s", settings) + return exec.command_context(ctx, 'mmcli', '-m', device, settings_string) +end + +local function monitor_modems() + return backend.monitor_modems() +end + +local function inhibit(ctx, device) + return backend.inhibit(device) +end + +local function connect(ctx, device, connection_string) + return backend.connect(ctx, device, connection_string) +end + +local function disconnect(ctx, device) + return backend.disconnect(ctx, device) +end + +local function reset(ctx, device) + return backend.reset(ctx, device) +end + +local function enable(ctx, device) + return backend.enable(ctx, device) +end + +local function disable(ctx, device) + return backend.disable(ctx, device) +end + +local function monitor_state(device) + return backend.monitor_state(device) +end + +local function information(ctx, device) + return backend.information(ctx, device) +end + +local function sim_information(ctx, device) + return backend.sim_information(ctx, device) +end + +local function location_status(ctx, device) + return backend.location_status(ctx, device) +end + +local function signal_setup(ctx, device, rate) + return backend.signal_setup(ctx, device, rate) +end + +local function signal_get(ctx, device) + return backend.signal_get(ctx, device) +end + +local function three_gpp_set_initial_eps_bearer_settings(ctx, device, settings) + return backend.three_gpp_set_initial_eps_bearer_settings(ctx, device, settings) +end + +local function use_backend(new_backend) + if not new_backend then + return "No backend provided" + end + for name, _ in pairs(backend) do + if not new_backend[name] then + return "New backend does not implement function: " .. name + end + end + backend = new_backend +end + +local mmcli_package = { + monitor_modems = monitor_modems, + inhibit = inhibit, + connect = connect, + disconnect = disconnect, + reset = reset, + enable = enable, + disable = disable, + monitor_state = monitor_state, + information = information, + sim_information = sim_information, + location_status = location_status, + signal_setup = signal_setup, + signal_get = signal_get, + three_gpp_set_initial_eps_bearer_settings = three_gpp_set_initial_eps_bearer_settings, + use_backend = use_backend -- function to swap out backend implementations +} + +package.loaded['services.hal.drivers.modem.mmcli'] = mmcli_package -- singleton + +return mmcli_package diff --git a/src/services/hal/backends/qmicli.lua b/src/services/hal/backends/qmicli.lua new file mode 100644 index 00000000..490485ca --- /dev/null +++ b/src/services/hal/backends/qmicli.lua @@ -0,0 +1,107 @@ +local exec = require "fibers.exec" + +-- Default backend implementation using qmicli commands +local backend = {} + +function backend.uim_get_card_status(ctx, port) + return exec.command_context(ctx, "qmicli", "-p", "-d", port, "--uim-get-card-status") +end + +function backend.uim_sim_power_off(ctx, port) + return exec.command_context(ctx, "qmicli", "-p", "-d", port, "--uim-sim-power-off=1") +end + +function backend.uim_sim_power_on(ctx, port) + return exec.command_context(ctx, "qmicli", "-p", "-d", port, "--uim-sim-power-on=1") +end + +function backend.uim_monitor_slot_status(port) + return exec.command('qmicli', '-p', '-d', port, '--uim-monitor-slot-status') +end + +function backend.uim_read_transparent(ctx, port, address_string) + local addresses = string.format('--uim-read-transparent=%s', address_string) + return exec.command_context(ctx, 'qmicli', '-p', '-d', port, addresses) +end + +function backend.nas_get_rf_band_info(ctx, port) + return exec.command_context(ctx, 'qmicli', '-p', '-d', port, '--nas-get-rf-band-info') +end + +function backend.nas_get_home_network(ctx, port) + return exec.command_context(ctx, 'qmicli', '-p', '-d', port, '--nas-get-home-network') +end + +function backend.nas_get_serving_system(ctx, port) + return exec.command_context(ctx, 'qmicli', '-p', '-d', port, '--nas-get-serving-system') +end + +function backend.nas_get_signal_info(ctx, port) + return exec.command_context(ctx, 'qmicli', '-p', '-d', port, '--nas-get-signal-info') +end + +local function uim_get_card_status(ctx, port) + return backend.uim_get_card_status(ctx, port) +end + +local function uim_sim_power_off(ctx, port) + return backend.uim_sim_power_off(ctx, port) +end + +local function uim_sim_power_on(ctx, port) + return backend.uim_sim_power_on(ctx, port) +end + +local function uim_monitor_slot_status(port) + return backend.uim_monitor_slot_status(port) +end + +local function uim_read_transparent(ctx, port, address_string) + return backend.uim_read_transparent(ctx, port, address_string) +end + +local function nas_get_rf_band_info(ctx, port) + return backend.nas_get_rf_band_info(ctx, port) +end + +local function nas_get_home_network(ctx, port) + return backend.nas_get_home_network(ctx, port) +end + +local function nas_get_serving_system(ctx, port) + return backend.nas_get_serving_system(ctx, port) +end + +local function nas_get_signal_info(ctx, port) + return backend.nas_get_signal_info(ctx, port) +end + +local function use_backend(new_backend) + if not new_backend then + return "No backend provided" + end + for name, _ in pairs(backend) do + if not new_backend[name] then + return "New backend does not implement function: " .. name + end + end + backend = new_backend +end + +local qmicli_package = { + uim_get_card_status = uim_get_card_status, + uim_sim_power_off = uim_sim_power_off, + uim_sim_power_on = uim_sim_power_on, + uim_monitor_slot_status = uim_monitor_slot_status, + uim_read_transparent = uim_read_transparent, + + nas_get_rf_band_info = nas_get_rf_band_info, + nas_get_home_network = nas_get_home_network, + nas_get_serving_system = nas_get_serving_system, + nas_get_signal_info = nas_get_signal_info, + + use_backend = use_backend -- function to swap out backend implementations +} + +package.loaded['services.hal.drivers.modem.qmicli'] = qmicli_package -- singleton +return qmicli_package diff --git a/src/services/hal/backends/ubus.lua b/src/services/hal/backends/ubus.lua new file mode 100644 index 00000000..d97fb125 --- /dev/null +++ b/src/services/hal/backends/ubus.lua @@ -0,0 +1,42 @@ +local exec = require "fibers.exec" +local cjson = require "cjson.safe" + +local Ubus = {} + +-- Context-free variants +function Ubus.list(...) + return exec.command('ubus', 'list', ...) +end + +function Ubus.call(path, method, ...) + return exec.command('ubus', 'call', path, method, ...) +end + +function Ubus.listen(...) + return exec.command('ubus', 'listen', ...) +end + +function Ubus.send(type, message) + local encoded_message = cjson.encode(message) + return exec.command('ubus', 'send', type, encoded_message) +end + +-- Context-aware variants +function Ubus.list_with_context(ctx, ...) + return exec.command_context(ctx, 'ubus', 'list', ...) +end + +function Ubus.call_with_context(ctx, path, method, ...) + return exec.command_context(ctx, 'ubus', 'call', path, method, ...) +end + +function Ubus.listen_with_context(ctx, ...) + return exec.command_context(ctx, 'ubus', 'listen', ...) +end + +function Ubus.send_with_context(ctx, type, message) + local encoded_message = cjson.encode(message) + return exec.command_context(ctx, 'ubus', 'send', type, encoded_message) +end + +return Ubus diff --git a/src/services/hal/backends/uci.lua b/src/services/hal/backends/uci.lua new file mode 100644 index 00000000..1fe13d8a --- /dev/null +++ b/src/services/hal/backends/uci.lua @@ -0,0 +1,202 @@ +local queue = require "fibers.queue" +local channel = require "fibers.channel" +local fibers = require "fibers" +local uci_mod = require "uci" + +local Q_SIZE = 10 + +---@class UCI +---@field commit_q Queue +---@field cursor unknown +local UCI = { + commit_q = queue.new(Q_SIZE), + cursor = uci_mod.cursor() +} + +---@class Session +---@field _changes table[] +local Session = {} +Session.__index = Session + +--- Create a new UCI session +--- @return Session +function Session.new() + local self = setmetatable({}, Session) + self._changes = {} + self._commited = false + return self +end + +--- Get a value from the UCI configuration +--- @param config string +--- @param section string +--- @param option string +--- @return any value +--- @return string? error +function Session:get(config, section, option) + return UCI.get(config, section, option) +end + +--- Set a value in the UCI configuration +--- @param config string +--- @param section string +--- @param option string +--- @param value any +--- @return string? error +function Session:set(config, section, option, value) + if self._commited then + return "Cannot modify a committed session" + end + if config == nil or section == nil or option == nil then + return "Invalid arguments for UCI set operation" + end + local change + if value == nil then + change = {command = "set_section", config = config, section_name = section, section_type = option} + else + change = {command = "set_value", config = config, section = section, option = option, value = value} + end + table.insert(self._changes, change) +end + +function Session:delete(config, section, option) + if self._commited then + return "Cannot modify a committed session" + end + if config == nil or section == nil then + return "Invalid arguments for UCI delete operation" + end + local change + if option == nil then + change = {command = "delete_section", config = config, section = section} + else + change = {command = "delete_option", config = config, section = section, option = option} + end + table.insert(self._changes, change) +end + +function Session:add(config, section_type) + if self._commited then + return "Cannot modify a committed session" + end + if config == nil or section_type == nil then + return nil, "Invalid arguments for UCI add operation" + end + local change = {command = "add_section", config = config, section_type = section_type} + table.insert(self._changes, change) +end + +function Session:foreach(config, section_type, map_func) + if self._commited then + return "Cannot modify a committed session" + end + if config == nil or section_type == nil or map_func == nil then + return "Invalid arguments for UCI foreach operation" + end + if type(map_func) ~= "function" then + return "map_func must be a function" + end + table.insert(self._changes, {command = "foreach", config = config, section_type = section_type, map_func = map_func}) +end + +function Session:commit() + if self._commited then + return "Session has already been committed" + end + local reply_ch = channel.new() + UCI.commit_q:put({ changes = self._changes, reply_ch = reply_ch }) + self._changes = {} + local reply = reply_ch:get() + if not reply then + return nil, "No reply received for UCI commit" + end + self._commited = true + return reply.success, reply.err +end + +-- Create a new UCI session +--- @return Session +function UCI.new_session() + return Session.new() +end + +--- Get a value from the UCI configuration +--- @param config string +--- @param section string +--- @param option string +--- @return any value +--- @return string? error +function UCI.get(config, section, option) + return UCI.cursor:get(config, section, option) +end + +--- A switch-case utility function +--- @param key any +--- @return fun(cases: table): ... +local function switch(key) + return function(cases) + local func = cases[key] + if func then + return func() + else + if cases["default"] then + return cases["default"]() + else + error("No case matched and no default case provided") + end + end + end +end + +function UCI.reactor() + local this_is_where_a_scope_check_would_go = true + while this_is_where_a_scope_check_would_go do + local commit = UCI.commit_q:get() + + local ret, err + for _, change in ipairs(commit.changes) do + ret, err = switch(change.command) { + set_value = function() + local success = UCI.cursor:set(change.config, change.section, change.option, change.value) + return success, success == false and "Failed to set value" or nil + end, + set_section = function() + local success = UCI.cursor:set(change.config, change.section_name, change.section_type) + return success, success == false and "Failed to set section" or nil + end, + delete_option = function() + local success = UCI.cursor:delete(change.config, change.section, change.option) + return success, success == false and "Failed to delete option" or nil + end, + delete_section = function() + local success = UCI.cursor:delete(change.config, change.section) + return success, success == false and "Failed to delete section" or nil + end, + add_section = function() + local name = UCI.cursor:add(change.config, change.section_type) + return name, name == nil and "Failed to add section" or nil + end, + foreach = function() + local success = UCI.cursor:foreach(change.config, change.section_type, change.map_func) + return success, success == false and "Failed to foreach" or nil + end, + default = function() + return nil, "Unknown UCI command: " .. tostring(change.command) + end + } + + if err then + commit.reply_ch:put({ success = ret, err = err }) + break + end + end + commit.reply_ch:put({ success = ret, err = nil }) + end +end + + + +fibers.spawn(UCI.reactor) + +return UCI + From a308f19babda6350ea687157c2ae8fb00226aad4 Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Fri, 23 Jan 2026 14:41:59 +0000 Subject: [PATCH 20/20] types --- src/services/hal/types/core.lua | 249 ++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 src/services/hal/types/core.lua diff --git a/src/services/hal/types/core.lua b/src/services/hal/types/core.lua new file mode 100644 index 00000000..40df68a1 --- /dev/null +++ b/src/services/hal/types/core.lua @@ -0,0 +1,249 @@ +---@alias DeviceId string|integer +---@alias DeviceType string +---@alias CapabilityType string +---@alias CapabilityId string|integer +---@alias PublishMethod string +---@alias TopicEntry string|integer +---@alias SubTopic TopicEntry[] +---@alias Metadata table +---@alias Info table + +---@class Capability +---@field command_q Queue +---@field control_list string[] +---@field id CapabilityId + + +---@class DeviceConnectedEvent +---@field connected boolean +---@field type DeviceType +---@field id DeviceId +---@field data Metadata +---@field capabilities Capability[] +local DeviceConnectedEvent = {} +DeviceConnectedEvent.__index = DeviceConnectedEvent + +--- Build a new DeviceConnectedEvent +---@param dev_type DeviceType +---@param id_field string +---@param data Metadata +---@param capabilities Capability[] +---@return DeviceConnectedEvent? +---@return string? error +function DeviceConnectedEvent.new(dev_type, id_field, data, capabilities) + if dev_type == nil then + return nil, "dev_type must be provided" + end + if id_field == nil then + return nil, "id_field must be provided" + end + if data == nil then + return nil, "data must be provided" + end + if data[id_field] == nil then + return nil, "data must contain the id_field" + end + if type(capabilities) ~= "table" then + return nil, "capabilities must be a table" + end + local self = setmetatable({ + connected = true, + type = dev_type, + id = id_field, + data = data, + capabilities = capabilities + }, DeviceConnectedEvent) + return self, nil +end + +---@class DeviceDisconnectedEvent +---@field connected boolean +---@field type DeviceType +---@field id DeviceId +---@field data Metadata +local DeviceDisconnectedEvent = {} +DeviceDisconnectedEvent.__index = DeviceDisconnectedEvent + +--- Build a new DeviceDisconnectedEvent +---@param dev_type DeviceType +---@param id_field string +---@param data Metadata +---@return DeviceDisconnectedEvent? +---@return string? error +function DeviceDisconnectedEvent.new(dev_type, id_field, data) + if dev_type == nil then + return nil, "dev_type must be provided" + end + if id_field == nil then + return nil, "id_field must be provided" + end + if data == nil then + return nil, "data must be provided" + end + if data[id_field] == nil then + return nil, "data must contain the id_field" + end + local self = setmetatable({ + connected = false, + type = dev_type, + id = id_field, + data = data + }, DeviceDisconnectedEvent) + return self, nil +end + +---@alias DeviceConnectionEvent DeviceConnectedEvent|DeviceDisconnectedEvent + +---@class DeviceEvent +---@field connected boolean +---@field type DeviceType +---@field index DeviceId +---@field identity any +---@field metadata Metadata +local DeviceEvent = {} +DeviceEvent.__index = DeviceEvent + +--- Build a new DeviceEvent +---@param connected boolean +---@param dev_type DeviceType +---@param index DeviceId +---@param identity any +---@param metadata Metadata +---@return DeviceEvent? +---@return string? error +function DeviceEvent.new(connected, dev_type, index, identity, metadata) + if type(connected) ~= "boolean" then + return nil, "connected must be a boolean" + end + if dev_type == nil then + return nil, "dev_type must be provided" + end + if index == nil then + return nil, "index must be provided" + end + if identity == nil then + return nil, "identity must be provided" + end + if metadata == nil then + return nil, "metadata must be provided" + end + local self = setmetatable({ + connected = connected, + type = dev_type, + index = index, + identity = identity, + metadata = metadata + }, DeviceEvent) + return self, nil +end + +---@class CapabilityDevice +---@field type DeviceType +---@field id DeviceId + +---@class CapabilityEvent +---@field connected boolean +---@field type CapabilityType +---@field index CapabilityId +---@field device CapabilityDevice +local CapabilityEvent = {} +CapabilityEvent.__index = CapabilityEvent + +--- Build a new CapabilityEvent +---@param connected boolean +---@param cap_type CapabilityType +---@param index CapabilityId +---@param dev_type DeviceType +---@param device_id DeviceId +---@return CapabilityEvent? +---@return string? error +function CapabilityEvent.new(connected, cap_type, index, dev_type, device_id) + if type(connected) ~= "boolean" then + return nil, "connected must be a boolean" + end + if cap_type == nil then + return nil, "cap_type must be provided" + end + if index == nil then + return nil, "index must be provided" + end + if dev_type == nil then + return nil, "dev_type must be provided" + end + if device_id == nil then + return nil, "device_id must be provided" + end + local self = setmetatable({ + connected = connected, + type = cap_type, + index = index, + device = { type = dev_type, id = device_id } + }, CapabilityEvent) + return self, nil +end + +---@class InfoEvent +---@field type CapabilityType +---@field index CapabilityId +---@field sub_topic SubTopic +---@field publish_method PublishMethod +---@field info Info +local InfoEvent = {} +InfoEvent.__index = InfoEvent + +--- Build a new InfoEvent +---@param cap_type CapabilityType +---@param index CapabilityId +---@param sub_topic SubTopic +---@param publish_method PublishMethod +---@param info Info +---@return InfoEvent? +---@return string? error +function InfoEvent.new(cap_type, index, sub_topic, publish_method, info) + if cap_type == nil then + return nil, "cap_type must be provided" + end + if index == nil then + return nil, "index must be provided" + end + if type(sub_topic) ~= "table" then + return nil, "sub_topic must be a table" + end + if publish_method == nil then + return nil, "publish_method must be provided" + end + local self = setmetatable({ + type = cap_type, + index = index, + sub_topic = sub_topic, + publish_method = publish_method, + info = info + }, InfoEvent) + return self, nil +end + +---@class Reply +---@field result any +---@field error any +local Reply = {} +Reply.__index = Reply + +--- Build a new Reply +---@param result any +---@param error any +---@return Reply +function Reply.new(result, error) + return setmetatable({ + result = result, + error = error + }, Reply) +end + +return { + DeviceConnectedEvent = DeviceConnectedEvent, + DeviceDisconnectedEvent = DeviceDisconnectedEvent, + DeviceEvent = DeviceEvent, + CapabilityEvent = CapabilityEvent, + InfoEvent = InfoEvent, + Reply = Reply +}