Skip to content

Commit f1d7152

Browse files
committed
enhance modem handling: add SIM presence polling and lifecycle monitoring
1 parent a6d1f53 commit f1d7152

3 files changed

Lines changed: 142 additions & 32 deletions

File tree

src/services/hal/backends/modem/models/quectel.lua

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,29 @@ local exec = require "fibers.io.exec"
22
local fibers = require "fibers"
33
local sleep = require "fibers.sleep"
44
local op = require "fibers.op"
5+
local scope = require "fibers.scope"
56

67
local at = require "services.hal.backends.modem.at"
78

89
local DEFAULT_TIMEOUT = 5
10+
local SIM_POLL_INTERVAL = 5
11+
12+
local function firmware_version_string(fwversion)
13+
if not fwversion then return nil end
14+
local version_string = string.match(fwversion, "^%w+_(%w+%.%w+)")
15+
return version_string
16+
end
17+
18+
local function firmware_version_code(version_str)
19+
local major, minor = string.match(version_str or "", "^(%w+)%.(%w+)")
20+
local major_num = tonumber(major, 16)
21+
local minor_num = tonumber(minor)
22+
if major_num and minor_num then
23+
return major_num * 1000 + minor_num
24+
else
25+
return nil, "Invalid firmware version format"
26+
end
27+
end
928

1029
local funcs = {
1130
-- This is a special case for the RM520N, it has a initial bearer which will always cause a
@@ -85,6 +104,54 @@ local funcs = {
85104
end
86105
end
87106
end
107+
},
108+
{
109+
-- On em06 (all firmware) and eg25-g (≤ 01.002) the UIM slot monitor cannot be relied upon
110+
-- to fire on SIM removal or insertion. Override with a polling implementation using
111+
-- is_sim_present(). listen_for_sim() drives trigger_sim_presence_check() for insertion.
112+
name = 'wait_for_sim_present_op',
113+
conditionals = {
114+
function(backend, model, _)
115+
if model == 'em06' then return true end
116+
if model ~= 'eg25' then return false end
117+
if type(backend.firmware) ~= "function" then return false end
118+
for _ = 0, 2 do
119+
local firmware_version, err = backend:firmware()
120+
if err == "" and firmware_version then
121+
local version_code = firmware_version_code(firmware_version_string(firmware_version))
122+
return version_code ~= nil and version_code <= firmware_version_code("01.002")
123+
end
124+
sleep.sleep(1)
125+
end
126+
return false
127+
end
128+
},
129+
func = function(backend)
130+
return op.guard(function()
131+
return scope.run_op(function(s)
132+
while true do
133+
local present, err = backend:is_sim_present()
134+
if err ~= "" then
135+
error("Failed to poll SIM presence: " .. err)
136+
end
137+
if present ~= backend.last_sim_state then
138+
backend.last_sim_state = present
139+
return present, ""
140+
end
141+
s:perform(sleep.sleep_op(SIM_POLL_INTERVAL))
142+
end
143+
end)
144+
:wrap(function(st, _, ...)
145+
if st == 'ok' then
146+
return ...
147+
elseif st == 'cancelled' then
148+
return false, "cancelled"
149+
else
150+
return false, (... or "SIM poll failed")
151+
end
152+
end)
153+
end)
154+
end
88155
}
89156
-- Other functions
90157
}

src/services/hal/backends/modem/modes/qmi.lua

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ local function add_mode_funcs(ModemBackend)
135135
return true, ""
136136
end
137137

138-
--- Make an op to listen for sim presence
138+
--- Make an op that completes when the UIM slot status changes to reflect a new SIM state.
139+
--- Behaviour varies by modem and firmware — on some models the slot monitor cannot be relied
140+
--- upon to fire on removal or insertion. See models/quectel.lua for the polling override.
139141
---@return Op
140142
function ModemBackend:wait_for_sim_present_op()
141143
return op.guard(function()

src/services/hal/drivers/modem.lua

Lines changed: 72 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ local pulse = require "fibers.pulse"
2929
---@field initialised boolean
3030
---@field caps_applied boolean
3131
---@field state_pulse Pulse
32+
---@field sim_inserted_pulse Pulse
33+
---@field sim_state_ch Channel
3234
local Modem = {}
3335
Modem.__index = Modem
3436

@@ -45,6 +47,7 @@ local D_LOG_EMITTER = false
4547

4648
local DEFAULT_STOP_TIMEOUT = 5
4749
local DEFAULT_CACHE_TIMEOUT = 10
50+
local LISTEN_TRIGGER_INTERVAL = 1
4851

4952
local CONTROL_Q_LEN = 8
5053

@@ -337,39 +340,37 @@ function Modem:listen_for_sim()
337340
end
338341
self.listening_for_sim = true
339342
local ok, err = fibers.current_scope():spawn(function()
340-
fibers.run_scope(function()
341-
self:_emit_state("sim_listener", "open")
342-
343-
fibers.current_scope():finally(function()
344-
self.listening_for_sim = false
345-
self:_emit_state("sim_listener", "closed")
346-
end)
347-
348-
while true do
349-
--- out returns true if SIM is present, false if not
350-
local source, out, err = fibers.perform(op.named_choice {
351-
sim_present = self.backend:wait_for_sim_present_op(),
352-
timeout = sleep.sleep_op(DEFAULT_CACHE_TIMEOUT)
353-
})
354-
if source == "sim_present" then
355-
if err ~= "" then
356-
log.error("Modem Driver", self.imei,
357-
"listen_for_sim: error waiting for SIM presence:", err)
358-
self:_emit_event("sim_listen_error", err)
359-
end
360-
if out then
361-
self.state_pulse:signal()
362-
break
363-
end
364-
elseif source == "timeout" then
365-
local ok, check_err = self.backend:trigger_sim_presence_check()
366-
if not ok then
367-
log.error("Modem Driver", self.imei,
368-
"listen_for_sim: failed to trigger SIM presence check:", check_err)
369-
end
343+
self:_emit_state("sim_listener", "open")
344+
345+
fibers.current_scope():finally(function()
346+
self.listening_for_sim = false
347+
self:_emit_state("sim_listener", "closed")
348+
end)
349+
350+
-- Capture pulse version before reading state to close the insertion race window.
351+
local last_seen = self.sim_inserted_pulse:version()
352+
local sim_present = self.sim_state_ch:get()
353+
354+
while sim_present ~= true do
355+
local source, _, primary = fibers.perform(op.named_choice {
356+
inserted = self.sim_inserted_pulse:changed_op(last_seen),
357+
trigger = sleep.sleep_op(LISTEN_TRIGGER_INTERVAL),
358+
failed = self.scope:fault_op(),
359+
})
360+
if source == "inserted" then
361+
break
362+
elseif source == "trigger" then
363+
local trigger_ok, check_err = self.backend:trigger_sim_presence_check()
364+
if not trigger_ok then
365+
log.error("Modem Driver", self.imei,
366+
"listen_for_sim: failed to trigger SIM presence check:", check_err)
370367
end
368+
elseif source == "failed" then
369+
log.error("Modem Driver", self.imei,
370+
"listen_for_sim: modem scope faulted:", tostring(primary))
371+
break
371372
end
372-
end)
373+
end
373374
end)
374375
if not ok then
375376
return return_error("listen_for_sim spawn failed: " .. tostring(err), 1)
@@ -520,6 +521,43 @@ function Modem:control_manager()
520521
end
521522
end
522523

524+
function Modem:sim_lifecycle_monitor()
525+
log.trace("Modem Driver", self.imei, "sim_lifecycle_monitor: started")
526+
527+
fibers.current_scope():finally(function()
528+
log.trace("Modem Driver", self.imei, "sim_lifecycle_monitor: exiting")
529+
end)
530+
531+
local sim_present = nil
532+
while true do
533+
local source, v1, v2 = fibers.perform(op.named_choice {
534+
sim_change = self.backend:wait_for_sim_present_op(),
535+
send = self.sim_state_ch:put_op(sim_present),
536+
})
537+
if source == "sim_change" then
538+
local present, err = v1, v2
539+
if err == "cancelled" or err == "Stream closed" or err == "Command closed" then
540+
log.trace("Modem Driver", self.imei, "sim_lifecycle_monitor:", err)
541+
break
542+
elseif err ~= "" then
543+
log.error("Modem Driver", self.imei, "sim_lifecycle_monitor: error polling SIM presence:", err)
544+
sleep.sleep(DEFAULT_CACHE_TIMEOUT)
545+
elseif present then
546+
log.trace("Modem Driver", self.imei, "sim_lifecycle_monitor: SIM inserted")
547+
sim_present = true
548+
self.sim_inserted_pulse:signal()
549+
self.state_pulse:signal()
550+
self:_emit_state("sim_status", "present")
551+
else
552+
log.trace("Modem Driver", self.imei, "sim_lifecycle_monitor: SIM removed")
553+
sim_present = false
554+
self:_emit_state("sim_status", "absent")
555+
end
556+
end
557+
-- source == "send": listener consumed the state, loop to offer it again
558+
end
559+
end
560+
523561
---- Driver Functions ----
524562

525563
--- Spawn driver services
@@ -536,6 +574,7 @@ function Modem:start()
536574
self.scope:spawn(function() self:state_monitor() end)
537575
self.scope:spawn(function() self:control_manager() end)
538576
self.scope:spawn(function() self:emitter() end)
577+
self.scope:spawn(function() self:sim_lifecycle_monitor() end)
539578

540579
-- Signal initial pulse so emitter emits the initial state
541580
self.state_pulse:signal()
@@ -668,6 +707,8 @@ local function new(address)
668707
caps_applied = false, -- modem cannot start until capabilities applied
669708
listening_for_sim = false,
670709
state_pulse = pulse.new(),
710+
sim_inserted_pulse = pulse.new(),
711+
sim_state_ch = channel.new(),
671712
control_ch = control_ch
672713
}, Modem), ""
673714
end

0 commit comments

Comments
 (0)