Add cellular modem support via modemd daemon

Integrates the modem management subsystem, donated by Avvero Pty Ltd,
adapted to work on any platform without proprietary hardware (simctrl,
modules.json, /sys/class/sim/).

Components:
- package/feature-modem: Buildroot feature package pulling in modemd,
  ModemManager, libmbim, libqmi, and USB serial/MBIM/QMI kernel drivers
- package/modemd: Buildroot package for the modemd daemon and helpers
- src/modemd: modem management daemon — detects modems via ModemManager,
  connects bearers, configures IP addresses, handles SMS and location
- src/confd/src/modem.c: confd plugin — enables/disables modemd and
  modem-manager via sysrepo, forwards RPCs (restart/reset/send-sms)
- src/confd/src/interfaces.{c,h}: IFT_MODEM type for wwan* interfaces;
  confd manages address/route assignment, modem daemon owns link state
- src/confd/yang/confd/infix-modem.yang: YANG model for modem config and
  operational state (bearers, APN, bands, modes, location, SMS)
- src/statd/python/yanger/infix_modem.py: operational state collector
- patches/modem-manager: upstream patches for udev install path,
  multiplexed interface naming, and MBIM port trust from udev hint
  (the last fixes Dell DW5811e / Sierra EM7455 on cold boot)
- patches/libqmi: ignore sysfs in download mode

Platform portability fixes for boards without simctrl hardware:
- modem-info: fall back to /run/modems.json when /run/modules.json is
  absent; discover wwan interfaces via sysfs scan; provide synthetic SIM
  entry when /sys/class/sim/ is not present; fix dbg() to respect flag
- modemd: make --set-allowed-modes=any best-effort (some modems only
  support one mode combination and reject any change); skip mode set
  when current mode already matches requested mode
- sim-setup: guard simctrl open, return None when modules.json absent
  to skip SIM slot switching entirely on non-simctrl hardware

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-06-07 20:50:28 +02:00
parent 985b694e51
commit 314b0b425f
41 changed files with 6062 additions and 4 deletions
+1
View File
@@ -149,6 +149,7 @@ INFIX_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://www.kernelkit.org/infix/"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_FEATURE_GPS=y
BR2_PACKAGE_FEATURE_MODEM=y
BR2_PACKAGE_FEATURE_WIFI=y
BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y
BR2_PACKAGE_FEATURE_WIFI_QUALCOMM=y
+2
View File
@@ -2,6 +2,7 @@ menu "Packages"
comment "Hardware Support"
source "$BR2_EXTERNAL_INFIX_PATH/package/feature-gps/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/feature-modem/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/feature-wifi/Config.in"
comment "Software Packages"
@@ -30,6 +31,7 @@ source "$BR2_EXTERNAL_INFIX_PATH/package/landing/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/libsrx/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/lowdown/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/mcd/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/modemd/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/mdns-alias/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/netbrowse/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/onieprom/Config.in"
+19
View File
@@ -0,0 +1,19 @@
config BR2_PACKAGE_FEATURE_MODEM
bool "Feature Modem"
select BR2_PACKAGE_MODEMD
select BR2_PACKAGE_MODEM_MANAGER
select BR2_PACKAGE_MODEM_MANAGER_LIBMBIM
select BR2_PACKAGE_MODEM_MANAGER_LIBQMI
select BR2_PACKAGE_MODEM_MANAGER_LIBQRTR
help
Enables cellular modem support in Infix via ModemManager and
the modemd management daemon. Includes drivers for common
USB option modems and QMI/MBIM-based devices.
config BR2_PACKAGE_FEATURE_MODEM_QUALCOMM
bool "Qualcomm-based modems (QMI/QRTR/MHI)"
depends on BR2_PACKAGE_FEATURE_MODEM
help
Adds kernel support for Qualcomm-based cellular modems that use
the MHI bus and QRTR IPC router (e.g. Sierra Wireless EM7xxx,
Quectel EM/RMxxx, Telit LN9xx).
+25
View File
@@ -0,0 +1,25 @@
################################################################################
#
# Cellular modem support
#
################################################################################
FEATURE_MODEM_PACKAGE_VERSION = 1.0
FEATURE_MODEM_PACKAGE_LICENSE = MIT
define FEATURE_MODEM_LINUX_CONFIG_FIXUPS
$(call KCONFIG_ENABLE_OPT,CONFIG_USB_SERIAL)
$(call KCONFIG_ENABLE_OPT,CONFIG_USB_SERIAL_WWAN)
$(call KCONFIG_ENABLE_OPT,CONFIG_USB_SERIAL_OPTION)
$(call KCONFIG_ENABLE_OPT,CONFIG_USB_WDM)
$(call KCONFIG_ENABLE_OPT,CONFIG_USB_NET_QMI_WWAN)
$(call KCONFIG_ENABLE_OPT,CONFIG_USB_CDC_MBIM)
$(if $(filter y,$(BR2_PACKAGE_FEATURE_MODEM_QUALCOMM)),
$(call KCONFIG_SET_OPT,CONFIG_QRTR,m)
$(call KCONFIG_SET_OPT,CONFIG_MHI_BUS,m)
$(call KCONFIG_ENABLE_OPT,CONFIG_QRTR_MHI)
)
endef
$(eval $(generic-package))
+6
View File
@@ -0,0 +1,6 @@
config BR2_PACKAGE_MODEMD
bool "modemd"
select BR2_PACKAGE_MODEM_MANAGER
select BR2_PACKAGE_PYTHON3
help
Daemon which manages modems.
+2
View File
@@ -0,0 +1,2 @@
# Locally calculated
sha256 25b33026a661c4c550374cfcba6890a4363bf19db0c1c31a6e65b5edd113ecf0 LICENSE
+44
View File
@@ -0,0 +1,44 @@
################################################################################
#
# modemd
#
################################################################################
MODEMD_VERSION = 1.0
MODEMD_SITE_METHOD = local
MODEMD_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/modemd
MODEMD_LICENSE = BSD-3-Clause
MODEMD_LICENSE_FILES = LICENSE
MODEMD_REDISTRIBUTE = NO
MODEMD_DEPENDENCIES = modem-manager jansson python3
define MODEMD_BUILD_CMDS
$(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_LDFLAGS) \
$(MODEMD_DIR)/modem-command.c -o $(MODEMD_DIR)/modem-command -ljansson
endef
define MODEMD_INSTALL_TARGET_CMDS
mkdir -p $(TARGET_DIR)/usr/libexec/modemd
mkdir -p $(TARGET_DIR)/lib/udev/rules.d
mkdir -p $(FINIT_D)/available/
mkdir -p $(TARGET_DIR)/sbin
$(INSTALL) -D -m 0644 $(MODEMD_DIR)/finit.conf $(FINIT_D)/available/modemd.conf
install -m 644 $(MODEMD_DIR)/modemd.rules $(TARGET_DIR)/lib/udev/rules.d/90-modemd.rules
install -m 644 $(MODEMD_DIR)/qmi-wwan-ids.rules $(TARGET_DIR)/lib/udev/rules.d/91-qmi-wwan-ids.rules
install -m 644 $(MODEMD_DIR)/77-mm-dell-port-types.rules $(TARGET_DIR)/etc/udev/rules.d/77-mm-dell-port-types.rules
install -D -m 644 $(MODEMD_DIR)/modemd.modules-load $(TARGET_DIR)/etc/modules-load.d/modemd.conf
install -m 755 $(MODEMD_DIR)/modem-udev $(TARGET_DIR)/usr/libexec/modemd/
install -m 755 $(MODEMD_DIR)/modemd $(TARGET_DIR)/sbin/modemd
install -m 755 $(MODEMD_DIR)/modem-command $(TARGET_DIR)/sbin/modem-command
install -m 755 $(MODEMD_DIR)/modem-info $(TARGET_DIR)/usr/libexec/modemd/
install -m 755 $(MODEMD_DIR)/modem-rpc $(TARGET_DIR)/usr/libexec/modemd/
install -m 755 $(MODEMD_DIR)/modem-sms $(TARGET_DIR)/usr/libexec/modemd/
install -m 755 $(MODEMD_DIR)/modem-scan-networks $(TARGET_DIR)/usr/libexec/modemd/
install -m 755 $(MODEMD_DIR)/modem-update-firmware $(TARGET_DIR)/usr/libexec/modemd/
install -m 755 $(MODEMD_DIR)/modem-ussd $(TARGET_DIR)/usr/libexec/modemd/
install -m 755 $(MODEMD_DIR)/modem-carrier $(TARGET_DIR)/usr/libexec/modemd/
install -m 755 $(MODEMD_DIR)/modem-power $(TARGET_DIR)/usr/libexec/modemd/
install -m 755 $(MODEMD_DIR)/sim-setup $(TARGET_DIR)/usr/libexec/modemd/
endef
$(eval $(generic-package))
@@ -0,0 +1 @@
service [2345789] ModemManager -- ModemManager daemon
@@ -0,0 +1,21 @@
diff --git a/src/qmi-firmware-update/qfu-helpers-udev.c b/src/qmi-firmware-update/qfu-helpers-udev.c
index bda9106..40d648f 100644
--- a/src/qmi-firmware-update/qfu-helpers-udev.c
+++ b/src/qmi-firmware-update/qfu-helpers-udev.c
@@ -364,8 +364,14 @@ device_matches (GUdevDevice *device,
if (!device_sysfs_path)
goto out;
- if (g_strcmp0 (device_sysfs_path, sysfs_path) != 0)
- goto out;
+ /*
+ * don't compare sysfs path for download mode as it
+ * changes from USB4 to USB3 and thus may use a different host controller
+ */
+ if (mode != QFU_HELPERS_DEVICE_MODE_DOWNLOAD) {
+ if (g_strcmp0 (device_sysfs_path, sysfs_path) != 0)
+ goto out;
+ }
if (device_mode != mode)
return NULL;
+13
View File
@@ -0,0 +1,13 @@
diff --git a/meson.build b/meson.build
index 8cddf77..e97f862 100644
--- a/meson.build
+++ b/meson.build
@@ -188,7 +188,7 @@ endif
config_h.set('WITH_UDEV', enable_udev)
# udev base directory (required to install rules even when udev support is disabled)
-udev_udevdir = get_option('udevdir')
+udev_udevdir = '/lib/udev'
if udev_udevdir == ''
assert(enable_udev, 'udevdir must be explicitly given if udev support is disabled')
udev_udevdir = dependency('udev').get_pkgconfig_variable('udevdir')
@@ -0,0 +1,17 @@
diff --git a/src/mm-bearer-qmi.c b/src/mm-bearer-qmi.c
index 54f2e93..3c4df7b 100644
--- a/src/mm-bearer-qmi.c
+++ b/src/mm-bearer-qmi.c
@@ -2494,7 +2494,11 @@ load_settings_from_bearer (MMBearerQmi *self,
}
/* The link prefix hint given must be modem-specific */
- ctx->link_prefix_hint = g_strdup_printf ("qmapmux%u.", mm_base_modem_get_dbus_id (MM_BASE_MODEM (modem)));
+ const gchar *name = mm_port_get_device (MM_PORT (ctx->data));
+ if (name)
+ ctx->link_prefix_hint = g_strdup_printf ("%s.", name);
+ else
+ ctx->link_prefix_hint = g_strdup_printf ("qmapmux%u.", mm_base_modem_get_dbus_id (MM_BASE_MODEM (modem)));
}
/* If profile id is given, we'll launch the connection specifying the profile id in use
@@ -0,0 +1,29 @@
When a udev rule explicitly sets ID_MM_PORT_TYPE_MBIM=1 for a port,
treat it as definitive and skip the active MBIM open/probe. This
mirrors the way ID_MM_PORT_TYPE_GPS is handled (set via is_gps, a
direct result field rather than a "maybe" hint).
The active MBIM probe attempts to open the WDM device and send an
MBIM OPEN message. Some modems (e.g. Dell DW5811e / Sierra Wireless
EM7455 in USB composition 8) persistently send QMI/QMUX-framed
notifications on the MBIM control channel before ever responding to
MBIM OPEN, causing the probe to time out (~50 s). The probe failure
causes mm_port_probe_list_has_mbim_port() to return FALSE, so the
Dell plugin creates a non-MBIM modem object that cannot handle the
usbmisc/MBIM port, resulting in "unhandled port type".
Since the udev rule already encodes the vendor/product/interface
specificity required to identify the port, the active probe adds no
information in this case.
diff --git a/src/mm-port-probe.c b/src/mm-port-probe.c
--- a/src/mm-port-probe.c
+++ b/src/mm-port-probe.c
@@ -1529,6 +1529,7 @@ mm_port_probe_run (MMPortProbe *self,
/* If this is a port flagged as being a MBIM port, don't do any other probing */
if (self->priv->maybe_mbim) {
mm_obj_dbg (self, "no AT/QCDM/QMI probing in possible MBIM port");
+ mm_port_probe_set_result_mbim (self, TRUE);
mm_port_probe_set_result_at (self, FALSE);
mm_port_probe_set_result_qcdm (self, FALSE);
mm_port_probe_set_result_qmi (self, FALSE);
+1
View File
@@ -48,6 +48,7 @@ confd_plugin_la_SOURCES = \
if-vxlan.c \
if-wifi.c \
if-wireguard.c \
modem.c \
keystore.c \
system.c \
ntp.c \
+3 -1
View File
@@ -858,7 +858,9 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv)
rc = ntp_candidate_init(&confd);
if (rc)
goto err;
/* YOUR_INIT GOES HERE */
rc = modem_init(&confd);
if (rc)
goto err;
return SR_ERR_OK;
err:
+17
View File
@@ -133,6 +133,10 @@ typedef enum {
if ((rc = register_rpc(s, x, c, a, u))) \
goto fail
#define REGISTER_NOTIF(s,m,x,c,a,u) \
if ((rc = register_notif(s, m, x, c, a, u))) \
goto fail
struct confd {
sr_session_ctx_t *session; /* running datastore */
sr_session_ctx_t *startup; /* startup datastore */
@@ -192,6 +196,17 @@ static inline int register_rpc(sr_session_ctx_t *session, const char *xpath,
return rc;
}
static inline int register_notif(sr_session_ctx_t *session, const char *module, const char *xpath,
sr_event_notif_cb cb, void *arg, sr_subscription_ctx_t **sub)
{
int rc = sr_notif_subscribe(session, module, xpath,
NULL, NULL, /* forever */
cb, arg, SR_SUBSCR_NO_THREAD, sub);
if (rc)
ERROR("failed subscribing to %s notification: %s", xpath, sr_strerror(rc));
return rc;
}
/* core.c */
int finit_enable(const char *svc);
@@ -274,4 +289,6 @@ int ntp_candidate_init(struct confd *confd);
/* ptp.c */
int ptp_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd);
int modem_init(struct confd *confd);
#endif /* CONFD_CORE_H_ */
+16 -3
View File
@@ -80,6 +80,8 @@ static int ifchange_cand_infer_type(sr_session_ctx_t *session, const char *path)
if (!fnmatch("wifi+([0-9])*", ifname, FNM_EXTMATCH))
inferred.data.string_val = "infix-if-type:wifi";
else if (!fnmatch("wwan+([0-9-])", ifname, FNM_EXTMATCH))
inferred.data.string_val = "infix-if-type:modem";
else if (iface_is_phys(ifname))
inferred.data.string_val = "infix-if-type:ethernet";
else if (!fnmatch("br+([0-9])", ifname, FNM_EXTMATCH))
@@ -390,6 +392,8 @@ static int netdag_gen_afspec_add(sr_session_ctx_t *session, struct dagger *net,
return vlan_gen(NULL, cif, ip);
case IFT_VXLAN:
return vxlan_gen(NULL, cif, ip);
case IFT_MODEM:
return modem_gen(NULL, cif, net);
case IFT_WIFI:
return wifi_validate_secret(session, cif)
? : wifi_add_iface(cif, net);
@@ -434,6 +438,7 @@ static int netdag_gen_afspec_set(sr_session_ctx_t *session, struct dagger *net,
case IFT_DUMMY:
case IFT_GRE:
case IFT_GRETAP:
case IFT_MODEM:
case IFT_VETH:
case IFT_VXLAN:
case IFT_WIREGUARD:
@@ -474,6 +479,8 @@ static bool netdag_must_del(struct lyd_node *dif, struct lyd_node *cif)
return lydx_get_descendant(lyd_child(dif), "veth", NULL);
case IFT_VXLAN:
return lydx_get_descendant(lyd_child(dif), "vxlan", NULL);
case IFT_MODEM:
return false;
case IFT_WIREGUARD:
return lydx_get_descendant(lyd_child(dif), "wireguard", NULL);
case IFT_UNKNOWN:
@@ -551,6 +558,9 @@ static int netdag_gen_iface_del(struct dagger *net, struct lyd_node *dif,
case IFT_VETH:
veth_gen_del(dif, ip);
break;
case IFT_MODEM:
modem_gen_del(dif, net);
break;
case IFT_WIFI:
wifi_del_iface(dif, net);
break;
@@ -573,7 +583,8 @@ static int netdag_gen_iface_del(struct dagger *net, struct lyd_node *dif,
static sr_error_t netdag_gen_iface_timeout(struct dagger *net, const char *ifname, const char *iftype)
{
if (!strcmp(iftype, "infix-if-type:ethernet")) {
if (!strcmp(iftype, "infix-if-type:ethernet") ||
!strcmp(iftype, "infix-if-type:modem")) {
FILE *wait;
wait = dagger_fopen_net_init(net, ifname, NETDAG_INIT_TIMEOUT, "wait-interface.sh");
@@ -693,8 +704,9 @@ static sr_error_t netdag_gen_iface(sr_session_ctx_t *session, struct dagger *net
fprintf(ip, "link set alias \"%s\" dev %s\n", attr ?: "", ifname);
/* Bring interface back up, if enabled */
if (lydx_is_enabled(cif, "enabled"))
fprintf(ip, "link set dev %s up state up\n", ifname);
if (strcmp(iftype, "infix-if-type:modem") != 0)
if (lydx_is_enabled(cif, "enabled"))
fprintf(ip, "link set dev %s up state up\n", ifname);
err = err ? : netdag_gen_sysctl(net, cif, dif);
@@ -724,6 +736,7 @@ static int netdag_init_iface(struct lyd_node *cif)
return vlan_add_deps(cif);
case IFT_VETH:
return veth_add_deps(cif);
case IFT_MODEM:
case IFT_WIFI:
case IFT_DUMMY:
case IFT_ETH:
+5
View File
@@ -29,6 +29,7 @@
_map(IFT_GRETAP, "infix-if-type:gretap") \
_map(IFT_LAG, "infix-if-type:lag") \
_map(IFT_LO, "infix-if-type:loopback") \
_map(IFT_MODEM, "infix-if-type:modem") \
_map(IFT_VETH, "infix-if-type:veth") \
_map(IFT_VLAN, "infix-if-type:vlan") \
_map(IFT_VXLAN, "infix-if-type:vxlan") \
@@ -167,6 +168,10 @@ int ifchange_cand_infer_dhcp(sr_session_ctx_t *session, const char *path);
/* if-vxlan.c */
int vxlan_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip);
/* modem.c */
int modem_gen(struct lyd_node *dif, struct lyd_node *cif, struct dagger *net);
int modem_gen_del(struct lyd_node *dif, struct dagger *net);
/* infix-if-wireguard */
int wireguard_validate_peers(sr_session_ctx_t *session, struct lyd_node *cif);
int wireguard_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip, struct dagger *net);
+330
View File
@@ -0,0 +1,330 @@
/* SPDX-License-Identifier: BSD-3-Clause */
#include <assert.h>
#include <ctype.h>
#include <dirent.h>
#include <pwd.h>
#include <sys/utsname.h>
#include <sys/sysinfo.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <regex.h>
#include <srx/common.h>
#include <srx/lyx.h>
#include <srx/srx_val.h>
#include "core.h"
#define MAX_MODEMS 8
#define RUN_DIR "/run/modemd"
#define SOCK RUN_DIR "/modemd.sock"
#define MODULE "infix-modem"
#define ROOT_XPATH "/infix-modem:"
#define CFG_XPATH ROOT_XPATH "modems"
static int xpath_get_index(const char *xpath)
{
regmatch_t pmatch[2];
regex_t regex;
char buf[32];
int ret, len, i;
int index = -1;
if (regcomp(&regex, "index='([0-9]+)'", REG_EXTENDED))
return -1;
ret = regexec(&regex, xpath, 2, pmatch, 0);
if (!ret) {
len = (pmatch[1].rm_eo - pmatch[1].rm_so);
if (len < (int)(sizeof(buf)-1)) {
for (i = 0; i < len; i++)
buf[i] = xpath[pmatch[1].rm_so + i];
buf[i] = '\0';
index = (int) strtoul(buf, NULL, 10);
}
}
regfree(&regex);
return index;
}
static int node_index(struct lyd_node *node)
{
const char *s;
s = lydx_get_cattr(node, "index");
if (!s || !s[0])
return -1;
return (int) strtoul(s, NULL, 10);
}
static int disable(void)
{
NOTE("Disabling modemd");
/* disable modem-manager */
systemf("initctl -bfqn stop modem-manager");
systemf("initctl -bfqn disable modem-manager");
/* disable modemd */
systemf("initctl -bnq stop modemd");
systemf("initctl -bnq disable modemd");
return SR_ERR_OK;
}
static int enable(void)
{
int enabled, reload = 0;
NOTE("Enabling modemd");
/* enable modem-manager */
enabled = !systemf("initctl -bfq status modem-manager");
if (!enabled) {
systemf("initctl -bfqn enable modem-manager");
reload = 1;
}
/* enable modemd */
enabled = !systemf("initctl -bfq status modemd");
if (!enabled) {
systemf("initctl -bfqn enable modemd");
reload = 1;
}
/* reload if required */
if (reload)
systemf("initctl -b reload");
/* restart modem-manager */
systemf("initctl -bfqn restart modem-manager");
/* restart modemd */
systemf("initctl -bfqn restart modemd");
return SR_ERR_OK;
}
static int genconf(sr_data_t *cfg, struct lyd_node *diff)
{
struct lyd_node *node, *tree;
uint8_t enabled[MAX_MODEMS];
int index;
memset(enabled, 0, sizeof(enabled));
tree = lydx_get_descendant(cfg->tree, "modems", "modem", NULL);
LYX_LIST_FOR_EACH(tree, node, "modem") {
index = node_index(node);
if (index < MAX_MODEMS)
enabled[index] = lydx_get_bool(node, "enabled") ? 1 : 0;
}
tree = lydx_get_descendant(diff, "modems", "modem", NULL);
LYX_LIST_FOR_EACH(tree, node, "modem") {
index = node_index(node);
if (index < MAX_MODEMS && lydx_get_op(node) == LYDX_OP_DELETE)
enabled[index] = 0;
}
for (index = 0; index < MAX_MODEMS; index++) {
if (enabled[index]) {
if (enable() == SR_ERR_OK) {
return SR_ERR_OK;
} else {
ERROR("Cannot enable modem%d", index);
break;
}
}
}
return disable();
}
static int infix_modem_change(sr_session_ctx_t *session, uint32_t sub_id, const char *module,
const char *xpath, sr_event_t event, unsigned request_id, void *confd)
{
sr_data_t *cfg = NULL;
struct lyd_node *diff = NULL;
sr_error_t err;
if (event != SR_EV_DONE) {
return SR_ERR_OK;
}
err = sr_get_data(session, CFG_XPATH "//.", 0, 0, 0, &cfg);
if (err) {
ERROR("Can't get data");
goto out;
}
err = srx_get_diff(session, &diff);
if (err) {
ERROR("Can't get diff");
goto out;
}
err = genconf(cfg, diff);
if (err) {
ERROR("Can't gen conf");
goto out;
}
out:
if (diff) lyd_free_tree(diff);
if (cfg) sr_release_data(cfg);
return err;
}
static int infix_modem_rpcsend(char *msg, int len)
{
struct sockaddr_un addr;
struct timeval tv;
fd_set wfds;
int sock, ret = -1;
sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (sock < 0)
return -1;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, SOCK);
if (connect(sock, &addr, sizeof(addr)) == 0) {
tv.tv_sec = 5;
tv.tv_usec = 0;
FD_ZERO(&wfds);
FD_SET(sock, &wfds);
if (select(sock + 1, NULL, &wfds, NULL, &tv) > 0) {
if (write(sock, msg, len) == len) {
ret = 0;
}
}
}
close(sock);
return ret;
}
static int infix_modem_rpc(const char *xpath, const char *rpc, const char *data)
{
char msg[1024];
int len;
NOTE("Sending rpc %s to modemd", rpc);
len = snprintf(msg, sizeof(msg),
"{ \"rpc\" : \"%s\", \"data\" : %s }",
rpc, data ? data : "null");
if (infix_modem_rpcsend(msg, len) < 0) {
ERROR("Unable to send rpc");
return SR_ERR_INTERNAL;
}
return SR_ERR_OK;
}
static int infix_modem_sendsms(sr_session_ctx_t *session, uint32_t sub_id, const char *xpath,
const sr_val_t *input, const size_t input_cnt, sr_event_t event,
unsigned request_id, sr_val_t **output, size_t *output_cnt, void *priv)
{
char data[1024];
if (input_cnt < 3) {
ERROR("Not enough input parameters");
return SR_ERR_SYS;
}
snprintf(data, sizeof(data)-1,
"{ \"index\" : %s, \"number\" : \"%s\", \"text\" : \"%s\" }",
input[0].data.string_val,
input[1].data.string_val,
input[2].data.string_val);
return infix_modem_rpc(xpath, "send-sms", data);
}
static int infix_modem_restart(sr_session_ctx_t *session, uint32_t sub_id, const char *xpath,
const sr_val_t *input, const size_t input_cnt, sr_event_t event,
unsigned request_id, sr_val_t **output, size_t *output_cnt, void *priv)
{
char data[1024];
if (input_cnt < 1) {
ERROR("Not enough input parameters");
return SR_ERR_SYS;
}
snprintf(data, sizeof(data)-1,
"{ \"index\" : %s }", input[0].data.string_val);
return infix_modem_rpc(xpath, "restart", data);
}
static int infix_modem_reset(sr_session_ctx_t *session, uint32_t sub_id, const char *xpath,
const sr_val_t *input, const size_t input_cnt, sr_event_t event,
unsigned request_id, sr_val_t **output, size_t *output_cnt, void *priv)
{
char data[1024];
if (input_cnt < 1) {
ERROR("Not enough input parameters");
return SR_ERR_SYS;
}
snprintf(data, sizeof(data)-1,
"{ \"index\" : %s }", input[0].data.string_val);
return infix_modem_rpc(xpath, "reset", data);
}
static void infix_modem_notif (sr_session_ctx_t *session, uint32_t sub_id,
const sr_ev_notif_type_t notif_type, const char *xpath,
const sr_val_t *values, const size_t values_cnt,
struct timespec *timestamp, void *confd)
{
int index;
index = xpath_get_index(xpath);
if (index < 0) {
ERROR("No index");
return;
}
if (values_cnt < 1) {
ERROR("No values");
return;
}
NOTE("Notification from modem%d: %s",
index, values[0].data.string_val);
}
int modem_init(struct confd *confd)
{
int rc;
REGISTER_CHANGE(confd->session, MODULE, CFG_XPATH, 0, infix_modem_change, confd, &confd->sub);
REGISTER_NOTIF(confd->session, MODULE, CFG_XPATH "/modem/status-update", infix_modem_notif, confd, &confd->sub);
REGISTER_RPC(confd->session, ROOT_XPATH "restart", infix_modem_restart, NULL, &confd->sub);
REGISTER_RPC(confd->session, ROOT_XPATH "reset", infix_modem_reset, NULL, &confd->sub);
REGISTER_RPC(confd->session, ROOT_XPATH "send-sms", infix_modem_sendsms, NULL, &confd->sub);
return SR_ERR_OK;
fail:
ERROR("init failed: %s", sr_strerror(rc));
return rc;
}
int modem_gen(struct lyd_node *dif, struct lyd_node *cif, struct dagger *net)
{
return 0;
}
int modem_gen_del(struct lyd_node *dif, struct dagger *net)
{
return 0;
}
+1
View File
@@ -57,4 +57,5 @@ MODULES=(
"ieee1588-ptp-tt@2023-08-14.yang -e timestamp-correction"
"ieee802-dot1as-gptp@2025-12-10.yang"
"infix-ptp@2026-04-07.yang"
"infix-modem@2024-03-15.yang"
)
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
infix-modem.yang
+39
View File
@@ -0,0 +1,39 @@
# do not edit this file, it will be overwritten on update
ACTION!="add|change|move|bind", GOTO="mm_dell_port_types_end"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="413c", GOTO="mm_dell_vendorcheck"
GOTO="mm_dell_port_types_end"
LABEL="mm_dell_vendorcheck"
SUBSYSTEMS=="usb", ATTRS{bInterfaceNumber}=="?*", ENV{.MM_USBIFNUM}="$attr{bInterfaceNumber}"
# Dell DW5821e (default 0x81d7, with esim support 0x81e0)
# if 02: primary port
# if 03: secondary port
# if 04: raw NMEA port
# if 05: diag/qcdm port
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81d7", ENV{.MM_USBIFNUM}=="02", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}="1"
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81d7", ENV{.MM_USBIFNUM}=="03", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_SECONDARY}="1"
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81d7", ENV{.MM_USBIFNUM}=="04", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_GPS}="1"
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81d7", ENV{.MM_USBIFNUM}=="05", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_QCDM}="1"
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81e0", ENV{.MM_USBIFNUM}=="02", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}="1"
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81e0", ENV{.MM_USBIFNUM}=="03", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_SECONDARY}="1"
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81e0", ENV{.MM_USBIFNUM}=="04", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_GPS}="1"
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81e0", ENV{.MM_USBIFNUM}=="05", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_QCDM}="1"
# Dell DW5820e
# if 02: AT port
# if 04: debug port (ignore)
# if 06: AT port
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81d9", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_PORT_IGNORE}="1"
# Dell DW5811e (Sierra Wireless EM7455 rebadge)
# USB composition 8: MBIM-only (bInterfaceClass=02 bInterfaceSubClass=0e)
# Bound by cdc_mbim driver; no AT ports in this composition.
# if 0c: MBIM control port
# if 0d: data port (handled by cdc_mbim / cdc_ncm driver)
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81b6", ENV{.MM_USBIFNUM}=="0c", SUBSYSTEM=="usbmisc", ENV{ID_MM_PORT_TYPE_MBIM}="1"
GOTO="mm_dell_port_types_end"
LABEL="mm_dell_port_types_end"
+27
View File
@@ -0,0 +1,27 @@
Copyright (c) 2024 Avvero Pty Ltd
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of copyright holders nor the names of
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+1
View File
@@ -0,0 +1 @@
service [2345] name:modemd /sbin/modemd -- Modem daemon
+314
View File
@@ -0,0 +1,314 @@
#!/usr/bin/env python3
import subprocess
import argparse
import json
import syslog
import sys
import os
import re
syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_SYSLOG)
def log(msg):
syslog.syslog(syslog.LOG_INFO, msg)
def err(msg):
syslog.syslog(syslog.LOG_ERR, msg)
def fatal(msg):
syslog.syslog(syslog.LOG_ALERT, msg)
sys.exit(1)
def runcmd(cmd):
ret = None
try:
res = subprocess.run(cmd, check=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, text=True)
if res.returncode == 0:
if res.stdout:
ret = res.stdout.strip()
else:
ret = True
except subprocess.CalledProcessError:
return None
finally:
return ret
def runcmdj(cmd):
output = runcmd(cmd)
if not output:
return None
try:
data = json.loads(output)
except json.JSONDecodeError:
return None
finally:
return data
def read_file(path):
if os.path.exists(path):
with open(path, "r") as fd:
output = str(fd.read())
if output:
return output.strip()
return None
# Telit LE910
le910_carriers = [
{"num": 0, "name": "AT&T"},
{"num": 1, "name": "Verizon"},
{"num": 2, "name": "T-Mobile"},
{"num": 3, "name": "Bell"},
{"num": 4, "name": "Telus"},
{"num": 10, "name": "NTT-Docomo"},
{"num": 11, "name": "Telstra"},
{"num": 12, "name": "KDDI"},
{"num": 13, "name": "Softbank"},
{"num": 14, "name": "Vodafone New Zealand"},
{"num": 15, "name": "Spark New Zealand"},
{"num": 20, "name": "China Mobile"},
{"num": 21, "name": "China Unicom"},
{"num": 22, "name": "China Telecom"},
{"num": 30, "name": "Sprint"},
{"num": 31, "name": "SouthernLINC"},
{"num": 40, "name": "Global"},
{"num": 101, "name": "T-Mobile Germany"},
{"num": 102, "name": "AT&T Mexico"},
{"num": 103, "name": "Orange-WW"},
{"num": 104, "name": "Southern Linc USA"},
{"num": 105, "name": "Vodafone DE"}
]
def le910_carrier_bynum(num):
for carrier in le910_carriers:
if carrier["num"] == num:
return carrier
return None
def le910_carrier_byname(name):
for carrier in le910_carriers:
if carrier["name"] == name:
return carrier
return None
def le910_is_supported(variant):
if variant == "NS":
return True
elif variant == "AP":
return True
elif variant == "NF" or variant == "NFD":
return True
elif variant == "CN":
return True
elif variant == "APX":
return True
elif variant == "WWX" or variant == "WWXD":
return True
else:
return False
def le910_carrier_default(variant):
if variant == "NF" or variant == "WWX" or variant == "WWXD":
num = 0
elif variant == "AP" or variant == "APX":
num = 10
elif variant == "CN":
num = 20
elif variant == "NS":
num = 30
elif variant == "EU" or variant == "EUX":
num = 40
else:
return None
return le910_carrier_bynum(num)
def le910_command(index, command):
cmd = ['/sbin/modem-command',
'--index', str(index),
'--mode', '115200 8N1',
'--secondary',
'--timeout', '5',
command]
for attempt in range(1, 3):
output = runcmd(cmd)
if output:
return output
err("Command '%s' failed" % " ".join(cmd))
return None
def le910_list_carriers(index):
output = le910_command(index, 'AT#FWSWITCH=?')
if output is None:
return None
r = re.search(r"#FWSWITCH: \(([^\)]*)\)", output)
if not r:
err("Unable to list carriers on modem%d" % index)
return None
rlist = r.group(1)
nums = []
for rl in rlist.split(","):
r = re.search(r"(\d+)-(\d+)", rl)
if r:
start = int(r.group(1))
end = int(r.group(2))
for i in range(start, end + 1):
nums.append(i)
else:
nums.append(int(rl))
carriers = []
for num in nums:
carrier = le910_carrier_bynum(num)
if carrier:
carriers.append(carrier)
else:
err("Unknown carrier %d" % num)
return carriers
def le910_set_carrier(index, carrier):
output = le910_command(index, 'AT#FWSWITCH=%d' % carrier["num"])
if output is None:
return False
else:
return True
def le910_get_carrier(index):
output = le910_command(index, 'AT#FWSWITCH?')
if output is None:
return -1
r = re.search(r"#FWSWITCH: (\d+)", output)
if not r:
err("Unable to query carrier on modem%d" % index)
return -1
num = int(r.group(1))
return le910_carrier_bynum(num)
def runcheck(manf, model):
if manf == "Telit" and model[:5] == "LE910":
return le910_is_supported(model.split("-")[1])
else:
return False
def runset(index, manf, model, name):
if not runcheck(manf, model):
fatal("Unsupported modem")
if name == "default":
carrier = le910_carrier_default(model.split("-")[1])
else:
carrier = le910_carrier_byname(name)
if not carrier:
fatal("Invalid carrier '%s'" % name)
current = le910_get_carrier(index)
if not current:
fatal("Cannot get current carrier")
if current["name"] == carrier["name"]:
log("Carrier '%s' already set on modem%d" % (carrier["name"], index))
print(json.dumps({"action": "none"}))
sys.exit(0)
log("Setting carrier '%s' on modem%d" % (carrier["name"], index))
if not le910_set_carrier(index, carrier):
fatal("Unable to set carrier on modem%d" % index)
print(json.dumps({"action": "reset"}))
sys.exit(0)
def runget(index, manf, model):
if not runcheck(manf, model):
fatal("Unsupported modem")
carrier = le910_get_carrier(index)
if not carrier:
fatal("Unable to get carrier")
print(json.dumps(carrier))
sys.exit(0)
def runlist(index, manf, model):
if not runcheck(manf, model):
print(json.dumps([]))
sys.exit(0)
carriers = le910_list_carriers(index)
if not carriers:
fatal("No carriers")
names = []
for carrier in sorted(carriers, key=lambda c: c["name"]):
names.append(carrier["name"])
print(json.dumps(names))
sys.exit(0)
if __name__ == "__main__":
parser = argparse.ArgumentParser(prog='modem-carrier')
parser.add_argument("-i", "--index", default=0, help="Modem index")
parser.add_argument("-m", "--manf", default=0, help="Modem manufacturer")
parser.add_argument("-M", "--model", default=0, help="Modem model")
parser.add_argument("-s", "--set", help="Set carrier")
parser.add_argument("-g", "--get", help="Get carrier",
action="store_true")
parser.add_argument("-l", "--list", help="List supported carriers",
action="store_true")
parser.add_argument("-c", "--check", help="Check support for carriers",
action="store_true")
args = parser.parse_args()
if not args.manf:
fatal("No manufacturer given")
else:
manf = args.manf
if not args.model:
fatal("No model given")
else:
model = args.model
if args.index:
index = int(args.index)
else:
index = 0
if args.check:
if not runcheck(manf, model):
sys.exit(1)
elif args.list:
runlist(index, manf, model)
elif args.set:
runset(index, manf, model, args.set)
elif args.get:
runget(index, manf, model)
sys.exit(0)
+617
View File
@@ -0,0 +1,617 @@
/* SPDX-License-Identifier: BSD-3-Clause */
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <termios.h>
#include <getopt.h>
#include <fcntl.h>
#include <syslog.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <time.h>
#include <errno.h>
#include <jansson.h>
static int verbose = 0;
#define MODEMS_LOCK "/var/lock/modems.lock"
#define MODEMS_INFO "/run/modems.json"
#define TIMEOUT 3
#define DBG(fmt, a...) print(LOG_DEBUG, fmt, ##a)
#define SAY(fmt, a...) print(LOG_INFO, fmt, ##a)
#define ERR(fmt, a...) print(LOG_ERR, fmt, ##a)
#define FATAL(fmt, a...) print(LOG_ALERT, fmt, ##a)
static void print (int level, const char *fmt, ...)
{
char msg[1024];
va_list ap;
if (!fmt) return;
va_start(ap, fmt);
vsnprintf(msg, sizeof(msg) - 1, fmt, ap);
va_end(ap);
switch (level) {
case LOG_ERR:
fprintf(stderr, "ERROR: %s\n", msg);
break;
case LOG_ALERT:
fprintf(stderr, "ERROR: %s\n", msg);
unlink(MODEMS_LOCK);
exit(1);
break;
case LOG_DEBUG:
if (verbose) {
printf("DEBUG: %s\n", msg);
}
break;
default:
printf("%s\n", msg);
break;
}
}
static void printser (int dir, char *ser)
{
char quoted[1024];
char *p;
int len = 0;
for (p = ser; *p; p++) {
if (*p == '\r') {
len += snprintf(quoted + len, sizeof(quoted) - len, "<CR>");
} else if (*p == '\n') {
len += snprintf(quoted + len, sizeof(quoted) - len, "<NL>");
} else {
len += snprintf(quoted + len, sizeof(quoted) - len, "%c", *p);
}
}
if (dir == 0) {
DBG("Received: '%s'", quoted);
} else {
DBG("Sending '%s'", quoted);
}
}
struct ttymode {
int set;
int speed;
int parity;
int bits;
int stopbits;
};
static int parse_mode (const char *str, struct ttymode *mode)
{
char buf[256];
char *t1, *t2;
/* parse string */
if (strlen(str) >= sizeof(buf)) {
return -1;
}
strncpy(buf, str, sizeof(buf));
t1 = strtok(buf, " ");
if (!t1) {
ERR("Invalid format");
return -1;
}
t2 = strtok(NULL, " ");
if (!t2 || strlen(t2) != 3) {
ERR("Invalid format");
return -1;
}
/* parse speed */
switch (atoi(t1)) {
case 9600:
mode->speed = B9600;
break;
case 19200:
mode->speed = B19200;
break;
case 38400:
mode->speed = B38400;
break;
case 57600:
mode->speed = B57600;
break;
case 115200:
mode->speed = B115200;
break;
default:
ERR("Invalid baudrate");
return -1;
}
/* parse bits */
switch (t2[0]) {
case '5':
mode->bits = CS5;
break;
case '6':
mode->bits = CS6;
break;
case '7':
mode->bits = CS7;
break;
case '8':
mode->bits = CS8;
break;
default:
ERR("Invalid bits");
return -1;
}
/* parse parity */
switch (t2[1]) {
case 'N':
mode->parity = 0;
break;
case 'E':
mode->parity = PARENB;
break;
case 'O':
mode->parity = PARENB | PARODD;
break;
default:
ERR("Invalid parity");
return -1;
}
/* parse stopbits */
switch (t2[2]) {
case '1':
mode->stopbits = 0;
break;
case '2':
mode->stopbits = CSTOPB;
break;
default:
ERR("Invalid stop bits %c");
return -1;
}
return 0;
}
static int setup (const char *tty, int timeout, struct ttymode *mode)
{
struct termios stbuf;
struct stat st;
char dev[32];
int i, fd;
if (strncmp(tty, "/dev", 4) == 0) {
strncpy(dev, tty, sizeof(dev));
} else {
snprintf(dev, sizeof(dev), "/dev/%s", tty);
}
/* wait for device */
for (i = 0; i < timeout; i++) {
if (lstat(dev, &st) == 0) break;
sleep(1);
}
if (i >= timeout) {
ERR("Timeout waiting for %s", dev);
return -1;
}
DBG("Opening %s", tty);
fd = open(dev, O_RDWR | O_EXCL | O_NONBLOCK | O_NOCTTY);
if (fd < 0) {
ERR("Unable to open '%s'", dev);
return -1;
}
if (!mode->set) return fd;
memset(&stbuf, 0, sizeof (struct termios));
if (tcgetattr(fd, &stbuf) != 0) {
ERR("Unable to get serial attributes");
goto error;
}
/* default settings */
stbuf.c_cflag &= ~(CBAUD | CSIZE | CSTOPB | PARENB | PARODD | CRTSCTS);
stbuf.c_iflag &= ~(IGNCR | ICRNL | IUCLC | INPCK | IXON | IXOFF | IXANY );
stbuf.c_oflag &= ~(OPOST | OLCUC | OCRNL | ONLCR | ONLRET);
stbuf.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHONL);
stbuf.c_cc[VMIN] = 1;
stbuf.c_cc[VTIME] = 0;
stbuf.c_cc[VEOF] = 1;
/* ignore parity */
stbuf.c_iflag |= IGNPAR;
/* configure serial attributes */
stbuf.c_cflag |= (mode->bits | mode->parity | mode->stopbits | CLOCAL | CREAD);
/* disable XON/XOFF flow control */
stbuf.c_iflag &= ~(IXON | IXOFF | IXANY);
/* disable RTS/CTS flow control */
stbuf.c_cflag &= ~(CRTSCTS);
if (cfsetispeed(&stbuf, mode->speed) != 0 ||
cfsetospeed(&stbuf, mode->speed) != 0) {
ERR("Unable to set port speed");
goto error;
}
if (tcsetattr(fd, TCSANOW, &stbuf) != 0) {
ERR("Unable to set serial attributes");
goto error;
}
return fd;
error:
close(fd);
return -1;
}
static int serial_write (int fd, int timeout, char *buf, int len)
{
struct timeval tv;
fd_set fds;
FD_ZERO(&fds);
FD_SET(fd, &fds);
tv.tv_sec = timeout;
tv.tv_usec = 0;
if (select(fd + 1, NULL, &fds, NULL, &tv) <= 0) {
DBG("Cannot write to serial device");
return -1;
}
alarm(timeout);
tcflush(fd, TCIOFLUSH);
printser(1, buf);
if (write(fd, buf, len) != len) {
return -1;
}
tcdrain(fd);
alarm(0);
return 0;
}
static int serial_read (int fd, int timeout, char *buf, int size)
{
struct timeval tv;
fd_set fds;
int rc, len = 0;
FD_ZERO(&fds);
FD_SET(fd, &fds);
tv.tv_sec = timeout;
tv.tv_usec = 0;
memset(buf, 0, size);
rc = select(fd + 1, &fds, NULL, NULL, &tv);
if (rc <= 0) {
DBG("Cannot read from serial device (%d)", rc);
return -1;
}
do {
errno = 0;
alarm(timeout);
rc = read(fd, buf+len, size-len-1);
alarm(0);
if (rc > 0) {
len += rc;
} else {
if (rc < 0 && errno == -EAGAIN) {
continue;
} else {
break;
}
}
} while (1);
if (len > 0) buf[len] = '\0';
printser(0, buf);
return len;
}
static int execute (int fd, int timeout, const char *cmd, const char *expect)
{
char buf[1024];
time_t start;
long elapsed;
int len, ret = -1;
if (cmd) {
start = time(NULL);
len = snprintf(buf, sizeof(buf), "%s\r\n", cmd);
if (serial_write(fd, timeout, buf, len) < 0) {
DBG("Cannot write to serial device");
return -1;
}
elapsed = time(NULL) - start;
timeout -= elapsed;
}
DBG("Waiting for response");
while (timeout > 0) {
start = time(NULL);
len = serial_read(fd, timeout, buf, sizeof(buf));
if (len > 0) {
buf[len] = '\0';
SAY("%s", buf);
if (strstr(buf, expect)) {
ret = 0;
break;
}
}
elapsed = time(NULL) - start;
timeout -= elapsed;
}
return ret;
}
static int probe (int fd)
{
char buf[256];
int i, len;
DBG("Probing serial port");
/* try 3 times */
for (i = 3; i > 0; i--) {
len = snprintf(buf, sizeof(buf), "AT\r\n");
if (serial_write(fd, 1, buf, len) < 0) {
DBG("Cannot write to tty");
continue;
}
len = serial_read(fd, 1, buf, sizeof(buf));
if (len <= 0) {
DBG("Cannot read from tty");
continue;
}
if (strstr(buf, "OK")) {
return 0;
}
}
DBG("Probing failed");
return -1;
}
static int modemidx (const char *devpath)
{
json_t *json, *modules, *module, *mod, *paths, *obj;
const char *p;
int i, j, index = -1;
json = json_load_file("/run/modules.json", 0, NULL);
if (!json) {
return -1;
}
modules = json_object_get(json, "modules");
if (!modules || !json_is_array(modules)) {
goto out;
}
for (i = 0; i < json_array_size(modules); i++) {
module = json_array_get(modules, i);
if (!module) continue;
obj = json_object_get(module, "type");
if (!obj || !json_is_string(obj)) continue;
p = json_string_value(obj);
if (strcmp(p, "modem") != 0) continue;
obj = json_object_get(module, "index");
if (!obj || !json_is_integer(obj)) continue;
index = json_integer_value(obj);
paths = json_object_get(module, "paths");
if (!paths || !json_is_array(paths)) continue;
for (j = 0; j < json_array_size(paths); j++) {
obj = json_array_get(paths, j);
if (!obj) continue;
p = json_string_value(obj);
if (strstr(devpath, p)) {
/* found */
goto out;
}
}
}
index = -1;
out:
json_decref(json);
return index;
}
static char *modemdev (int index, int port)
{
json_t *json, *modems, *modem, *ports, *obj;
char *dev = NULL;
int i, p, fd;
fd = open(MODEMS_LOCK,
O_WRONLY | O_CREAT | O_EXCL,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (fd < 0) {
ERR("Cannot open lock");
return NULL;
}
if (flock(fd, LOCK_EX) < 0) {
ERR("Cannot to lock");
close(fd);
return NULL;
}
json = json_load_file(MODEMS_INFO, 0, NULL);
if (!json) {
goto out;
}
modems = json_object_get(json, "modems");
if (!modems || !json_is_array(modems)) {
goto out;
}
for (i = 0; i < json_array_size(modems); i++) {
modem = json_array_get(modems, i);
if (!modem)
continue;
obj = json_object_get(modem, "devpath");
if (!obj || !json_is_string(obj))
continue;
if (modemidx(json_string_value(obj)) != index)
continue;
ports = json_object_get(modem, "atports");
if (!ports || !json_is_array(ports) || json_array_size(ports) == 0)
goto out;
if (json_array_size(ports) >= port)
port = 0;
obj = json_array_get(ports, port);
if (!obj || !json_is_string(obj))
goto out;
/* found */
dev = strdup(json_string_value(obj));
goto out;
}
dev = NULL;
out:
if (json) json_decref(json);
unlink(MODEMS_LOCK);
flock(fd, LOCK_UN);
close(fd);
return dev;
}
static struct option options[] = {
{ "index", required_argument, 0, 'i' },
{ "device", required_argument, 0, 'd' },
{ "mode", required_argument, 0, 'm' },
{ "command", required_argument, 0, 'c' },
{ "expect", required_argument, 0, 'e' },
{ "timeout", required_argument, 0, 't' },
{ "primary", no_argument, 0, 'P' },
{ "secondary", no_argument, 0, 'S' },
{ "verbose", no_argument, 0, 'v' },
{ "help", no_argument, 0, 'h' },
{ 0, 0, 0, 0 }
};
static void usage (void)
{
fprintf(stderr, "Usage: modem-command [OPTIONS] [command]\n\n");
fprintf(stderr, " --index <idx> Index of modem\n");
fprintf(stderr, " --device <tty> TTY device of modem\n");
fprintf(stderr, " --mode <mode> Serial mode for TTY (e.g. '115200 8N1')\n");
fprintf(stderr, " --expect <str> String to expect\n");
fprintf(stderr, " --timeout <to> Timeout for operation\n");
fprintf(stderr, " --primary Use primary atport\n");
fprintf(stderr, " --secondary Use secondary atport\n");
fprintf(stderr, " --verbose Be verbose\n");
fprintf(stderr, " --help print help\n\n");
exit(1);
}
int main (int argc, char *argv[])
{
const char *cmd = NULL, *expect = "OK";
char *dev = NULL;
struct ttymode mode;
int timeout = TIMEOUT;
int index = 0, port = 0;
int opt, c, fd;
int ret = 1;
memset(&mode, 0, sizeof(struct ttymode));
while (1) {
c = getopt_long(argc, argv, "i:d:m:e:t:PSvh", options, &opt);
if (c < 0) break;
switch (c) {
case 'i':
index = atoi(optarg);
break;
case 'd':
dev = strdup(optarg);
break;
case 'm':
if (parse_mode(optarg, &mode) < 0) {
FATAL("Invalid serial mode specified");
}
mode.set = 1;
break;
case 'c':
cmd = optarg;
break;
case 'e':
expect = optarg;
break;
case 't':
timeout = atoi(optarg);
break;
case 'P':
port = 0;
break;
case 'S':
port = 1;
break;
case 'v':
verbose = 1;
break;
default:
usage();
break;
}
}
if (optind < argc) {
cmd = argv[optind];
if (strlen(cmd) == 0)
FATAL("Invalid command");
}
dev = modemdev(index, port);
if (!dev || strlen(dev) == 0) {
FATAL("No device found");
}
fd = setup(dev, timeout, &mode);
if (fd < 0) {
FATAL("Unable to setup device");
}
if (probe(fd) < 0) {
ERR("Unable to probe device");
goto abort;
}
if (execute(fd, timeout, cmd, expect) < 0) {
ERR("Command failed");
goto abort;
}
ret = 0;
abort:
close(fd);
if (dev) free(dev);
return ret;
}
+432
View File
@@ -0,0 +1,432 @@
#!/usr/bin/env python3
import subprocess
import argparse
import syslog
import json
import sys
import os
import re
debug = False
syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_SYSLOG)
def dbg(msg):
global debug
if debug:
syslog.syslog(syslog.LOG_INFO, msg)
def runcmd(cmd):
ret = None
try:
res = subprocess.run(cmd, check=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, text=True)
if res.returncode == 0:
if res.stdout:
ret = res.stdout.strip()
else:
ret = True
except subprocess.CalledProcessError:
return None
finally:
return ret
def runcmdj(cmd):
output = runcmd(cmd)
if not output:
return None
try:
data = json.loads(output)
except json.JSONDecodeError:
return None
finally:
return data
def vbool(val):
if val == "yes" or val == "true":
return True
else:
return False
def vnum(val):
if val != "--":
return int(val)
else:
return 0
def vnumstr(val):
if val != "" and val != "--":
return val
else:
return "0"
def vstr(val):
if val != "--":
return val
else:
return ""
def venum(val, enums, default):
enval = vstr(val)
for en in enums:
if enval == en:
return en
return default
def vip4(val):
val = vstr(val)
if val == "":
return "0.0.0.0"
else:
return val
def vip6(val):
val = vstr(val)
if val == "":
return "::"
else:
return val
def fread(path):
if os.path.exists(path):
with open(path, "r") as fd:
output = str(fd.read())
if output:
return output.strip()
return None
def freadj(path):
output = fread(path)
if not output:
return None
try:
data = json.loads(output)
except json.JSONDecodeError:
dbg("Unable to parse json output")
return None
finally:
return data
def get_manufacturer(index, gen):
path = "/run/modemd/modem%d/manufacturer" % index
manf = fread(path)
if manf:
return manf
manf = vstr(gen.get("manufacturer", ""))
return manf
def get_model(index, gen):
path = "/run/modemd/modem%d/model" % index
model = fread(path)
if model:
return model
model = vstr(gen.get("model", ""))
return model
def get_supported_carriers(index, manf, model):
path = "/run/modemd/modem%d/supported-carriers" % index
output = freadj(path)
if output:
return output
else:
return []
def get_current_carrier(index, manf, model):
path = "/run/modemd/modem%d/current-carrier" % index
output = freadj(path)
if output:
return output
else:
return "default"
def device_devpath(devpath):
path = devpath
while path != "/sys":
path = os.path.realpath("%s/.." % path)
subdir = os.path.basename(path)
r = re.search(r"usb\d", subdir)
if r:
return path
return None
def modem_get_module(devpath):
if not os.path.exists("/run/modules.json"):
if os.path.exists("/run/modems.json"):
with open("/run/modems.json", "r") as fd:
data = json.load(fd)
for index, modem in enumerate(data.get("modems", [])):
if devpath in modem["devpath"]:
return {"index": index, "slot": index, "paths": [modem["devpath"]], "type": "modem"}
return None
with open("/run/modules.json", "r") as fd:
data = json.load(fd)
for module in data["modules"]:
if module["type"] != "modem":
continue
for path in module["paths"]:
if path in devpath:
return module
return None
def modem_get_sim(module):
path = "/sys/class/sim"
for index in range(0, 5):
name = "sim%d" % index
if not os.path.exists("%s/%s" % (path, name)):
continue
slot = int(fread("%s/%s/slot" % (path, name)))
if slot != module["slot"]:
continue
present = int(fread("%s/%s/present" % (path, name)))
return {
"index": index,
"name": name,
"slot": slot,
"present": bool(present)
}
return None
def sysfs_interfaces(devpath):
ifaces = []
try:
for iface in os.listdir("/sys/class/net"):
p = os.path.realpath("/sys/class/net/%s" % iface)
if p.startswith(devpath):
ifaces.append(iface)
except OSError:
pass
return ifaces
def print_modem(index):
with open("/run/modems.json", "r") as fd:
data = json.load(fd)
for modem in data["modems"]:
module = modem_get_module(modem["devpath"])
if module["index"] == index:
modem["slot"] = module["slot"]
modem["sim"] = modem_get_sim(module) or {"index": 0, "name": "sim0", "slot": 0, "present": True}
if not modem.get("interfaces"):
modem["interfaces"] = sysfs_interfaces(modem["devpath"])
print(json.dumps(modem))
sys.exit(0)
print(json.dumps({}))
sys.exit(0)
def print_all():
modems = []
output = runcmdj(['mmcli', '-J', '-L'])
if not output:
dbg("mmcli command failed")
print(json.dumps(modems))
sys.exit(0)
for mpath in output['modem-list']:
output = runcmdj(['mmcli', '-J', '-m', mpath])
if not output:
dbg("no output for %s" % mpath)
continue
modem = output.get("modem")
if modem is None:
dbg("no modem for %s" % mpath)
continue
gen = modem.get("generic")
if gen is None:
dbg("no generic for %s" % mpath)
continue
gpp = modem.get("3gpp")
if gpp is None:
dbg("no 3gpp for %s" % mpath)
continue
devpath = device_devpath(gen["device"])
if devpath is None:
dbg("no devpath for %s" % mpath)
continue
module = modem_get_module(devpath)
if module is None:
dbg("no modem module for %s" % devpath)
continue
index = module["index"]
manf = get_manufacturer(index, gen)
if manf is None:
dbg("no manufacturer for modem%d" % index)
continue
model = get_model(index, gen)
if model is None:
dbg("no model for modem%d" % index)
continue
modem = {
"index": index,
"path": mpath
}
modem["info"] = {
"manufacturer": manf,
"model": model,
"supported-carrier": get_supported_carriers(index, manf, model),
"hardware-revision": vstr(gen["hardware-revision"]),
"firmware-version": vstr(gen["revision"]),
"serial-number": vstr(gen["equipment-identifier"]),
"phone-number": gen.get("own-numbers", []),
}
modem["status"] = {
"state": vstr(gen["state"]),
"selected-carrier": get_current_carrier(index, manf, model),
"signal-quality": vnum(gen["signal-quality"]["value"])
}
if gen["state"] == "failed":
reason = vstr(gen["state-failed-reason"])
if len(reason) > 0:
modem["status"]["state-failed-reason"] = reason
sim = modem_get_sim(module)
if sim is not None:
modem["status"]["sim-active"] = sim["index"]
modem["status"]["sim-present"] = sim["present"]
refresh = 0
output = runcmdj(['mmcli', '-J', '-m', mpath, '--signal-get'])
if output:
refresh = int(output["modem"]["signal"]["refresh"]["rate"])
if refresh == 0:
# enable extended signal info
runcmd(['mmcli', '-J', '-m', mpath, '--signal-setup=5'])
else:
signals = ["rssi", "rsrp", "rsrq", "rscp", "snr", "sinr"]
found = False
for tech in output["modem"]["signal"].keys():
for sig in signals:
v = output["modem"]["signal"][tech].get(sig, "--")
if v != "--":
modem["status"]["signal-%s" % sig] = v
found = True
if found:
break
modem["status"]["cellular"] = {
"registration-state":
venum(gpp["registration-state"],
["idle", "home", "searching", "denied", "roaming"],
"unknown"),
"operator-name": vstr(gpp["operator-name"]),
"operator-id": vstr(gpp["operator-code"]),
"network-type": ",".join(vstr(gen["access-technologies"])).upper(),
"service-state": vstr(gpp["packet-service-state"])
}
bearers = []
for bpath in gen["bearers"]:
output = runcmdj(['mmcli', '-J', '-m', mpath, '-b', bpath])
if not output:
continue
b = output["bearer"]
bearer = {
"path": vstr(bpath),
"connected": vbool(b["status"]["connected"]),
"ipv4-address": vip4(b["ipv4-config"]["address"]),
"ipv4-prefix": vnum(b["ipv4-config"]["prefix"]),
"ipv6-address": vip6(b["ipv6-config"]["address"]),
"ipv6-prefix": vnum(b["ipv6-config"]["prefix"]),
"in-bytes": vnumstr(b["stats"]["bytes-rx"]),
"out-bytes": vnumstr(b["stats"]["bytes-tx"]),
"total-in-bytes": vnumstr(b["stats"]["total-bytes-rx"]),
"total-out-bytes": vnumstr(b["stats"]["total-bytes-tx"]),
"total-duration": vnumstr(b["stats"]["total-duration"])
}
if b["status"]["connected"] != "yes":
reason = vstr(b["status"]["connection-error"]["message"])
if len(reason) > 0:
bearer["connection-failed-reason"] = reason
iface = vstr(b["status"]["interface"])
if len(iface) > 0:
bearer["interface"] = iface
bearers.append(bearer)
modem["status"]["bearer"] = bearers
output = runcmdj(['mmcli', '-J', '-m', mpath, '-i', gen["sim"]])
if output:
modem["info"]["imsi"] = output["sim"]["properties"]["imsi"]
modem["info"]["iccid"] = output["sim"]["properties"]["iccid"]
output = runcmdj(['mmcli', '-J', '-m', mpath, '--location-get'])
if output:
loc = output["modem"]["location"]
modem["status"]["location"] = {
"latitude": vstr(loc["gps"]["latitude"]),
"longitude": vstr(loc["gps"]["longitude"]),
"altitude": vstr(loc["gps"]["altitude"]),
"cid": vstr(loc["3gpp"]["cid"]),
"lac": vstr(loc["3gpp"]["lac"]),
"mcc": vstr(loc["3gpp"]["mcc"]),
"mnc": vstr(loc["3gpp"]["mnc"]),
"tac": vstr(loc["3gpp"]["tac"])
}
modems.append(modem)
modems = sorted(modems, key=lambda m: m['index'])
data = json.dumps(modems)
print(data)
if __name__ == "__main__":
parser = argparse.ArgumentParser(prog='modem-info')
parser.add_argument("-i", "--index", default=0, help="Modem index")
args = parser.parse_args()
with open("/proc/cmdline", 'r') as fd:
cmdline = fd.read()
if "debug" in cmdline:
debug = True
if args.index:
print_modem(int(args.index))
else:
print_all()
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env python3
import subprocess
import argparse
import json
import time
import syslog
import sys
syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_SYSLOG)
def log(msg):
syslog.syslog(syslog.LOG_INFO, msg)
def err(msg):
syslog.syslog(syslog.LOG_ERR, msg)
def fatal(msg):
syslog.syslog(syslog.LOG_ALERT, msg)
sys.exit(1)
def runcmd(cmd):
ret = None
try:
res = subprocess.run(cmd, check=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, text=True)
if res.returncode == 0:
if res.stdout:
ret = res.stdout.strip()
else:
ret = True
except subprocess.CalledProcessError:
return None
finally:
return ret
def runcmdj(cmd):
output = runcmd(cmd)
if not output:
return None
try:
data = json.loads(output)
except json.JSONDecodeError:
return None
finally:
return data
def reset(index, info):
log("Power-cycling modem %d" % index)
with open("%s/authorized" % info["devpath"], "w") as fd:
fd.write("0\n")
time.sleep(0.5)
path = "/sys/class/pcie/slot%d/pwrctl" % info["slot"]
with open(path, "w") as fd:
fd.write("cycle\n")
return True
if __name__ == "__main__":
parser = argparse.ArgumentParser(prog='modem-power')
parser.add_argument("-i", "--index", default=0, help="Modem index")
args = parser.parse_args()
if args.index:
index = int(args.index)
else:
index = 0
info = runcmdj(['/usr/libexec/modemd/modem-info',
'-i', str(index)])
if info is None:
fatal("Unable to obtain modem info")
if not reset(index, info):
fatal("Unable to power-cycle modem")
sys.exit(0)
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
import argparse
import json
import sys
import os
def sendrpc(index, rpc):
path = "/run/modemd/modem%d/rpc" % index
if os.path.exists(path):
sys.stderr.write("Error: another rpc is running\n")
return
with open(path, "w") as fd:
json.dump(rpc, fd)
if not os.system(['sysrepocfg', '-f', 'json', '-R', path]):
sys.stderr.write("Error: unable to restart\n")
os.unlink(path)
if __name__ == "__main__":
parser = argparse.ArgumentParser(prog='modem-rpc')
parser.add_argument("-i", "--index", default=0, help="Modem index")
parser.add_argument("-r", "--rpc", default=0, help="RPC command")
args = parser.parse_args()
if args.index:
index = int(args.index)
else:
index = 0
if not args.rpc:
sys.stderr.write("Error: no rpc command\n")
sys.exit(1)
print("Sending '%s' rpc to modem%d" % (args.rpc, index))
rpc = {"infix-modem:%s" % args.rpc: {"index": str(index)}}
sendrpc(index, rpc)
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
import argparse
import subprocess
import json
import sys
index = 0
def runcmd(cmd):
ret = None
try:
res = subprocess.run(cmd, check=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, text=True)
if res.returncode == 0:
if res.stdout:
ret = res.stdout.strip()
else:
ret = True
except subprocess.CalledProcessError:
return None
finally:
return ret
def runcmdj(cmd):
output = runcmd(cmd)
if not output:
return None
try:
data = json.loads(output)
except json.JSONDecodeError:
return None
finally:
return data
def scan(mpath):
output = runcmdj(['mmcli', '-J', '-m', mpath,
'--3gpp-scan', '--timeout', '60'])
if not output:
return False
networks = []
for entry in output["modem"]["3gpp"]["scan-networks"]:
net = {}
for attr in entry.split(", "):
(key, value) = attr.split(": ")
net[key] = value
if "operator-name" in net:
networks.append(net)
if len(networks) == 0:
print("No networks found")
return True
for net in networks:
print("Network '%s' %s (%s, %s)" % (
net.get("operator-name"),
net.get("operator-code"),
net.get("access-technologies"),
net.get("availability")))
return True
if __name__ == "__main__":
parser = argparse.ArgumentParser(prog='modem-scan-networks')
parser.add_argument("-i", "--index", default=0, help="Modem index")
args = parser.parse_args()
if args.index:
index = int(args.index)
for m in runcmdj(['/usr/libexec/modemd/modem-info']):
if m["index"] == index:
print("Scanning networks on modem%d, please stand by ..." % index)
if not scan(m["path"]):
print("Operation failed.", file=sys.stderr)
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env python3
from datetime import datetime
import argparse
import locale
import subprocess
import json
import glob
import re
import sys
import os
LOCALEPATH = "/usr/lib/locale"
def run_cmd(cmd):
try:
res = subprocess.run(cmd, capture_output=True, text=True)
return True if res.returncode == 0 else False
except subprocess.CalledProcessError:
return False
def send(index, number, text):
if index < 0:
# use first modem by default
index = 0
rpc = {
"infix-modem:send-sms": {
"index": str(index),
"phone-number": number,
"message-text": text
}
}
print("Sending SMS to modem%d" % index)
path = "/run/modemd/modem%d/rpc" % index
if os.path.exists(path):
sys.stderr.write("Error: another rpc is running\n")
return
with open(path, "w") as fd:
json.dump(rpc, fd)
if not run_cmd(['sysrepocfg', '-f', 'json', '-R', path]):
sys.stderr.write("Error: Unable to send SMS\n")
os.unlink(path)
def listsms():
smslist = []
files = glob.glob('/var/sms/*')
for f in files:
with open(f, "r") as fd:
sms = json.load(fd)
sms["path"] = f
t = sms["payload"]["properties"]["timestamp"]
t = re.sub("[+-][\\d:]+$", "", t)
sms["time"] = datetime.strptime(t, "%Y-%m-%dT%H:%M:%S")
smslist.append(sms)
return sorted(smslist, key=lambda sms: sms["time"])
def delete(index=-1):
count = 0
for sms in listsms():
if index > -1 and sms["modem"] == index:
os.remove(sms["path"])
count += 1
print("Deleted %d SMS files" % count)
def show(index=-1):
for sms in listsms():
if index > -1 and sms["modem"] != index:
continue
content = sms["payload"]["content"]
prop = sms["payload"]["properties"]
print("--- %s ---\n" % os.path.basename(sms["path"]))
print("From: %s" % content["number"])
print("SMSC: %s" % prop["smsc"])
print("Modem: %d" % sms["modem"])
print("Date: %s" % prop["timestamp"])
print("State: %s" % prop["state"])
print("\n%s\n" % content["text"])
def listlocale():
locales = []
for f in os.listdir(LOCALEPATH):
if os.path.isdir(os.path.join(LOCALEPATH, f)):
locales.append(f)
return ", ".join(locales)
if __name__ == "__main__":
parser = argparse.ArgumentParser(prog='modem-sms')
parser.add_argument("-d", action="store_true", help="Delete SMS")
parser.add_argument("-s", action="store_true", help="Send SMS")
parser.add_argument("-i", "--index", default=0, help="Modem index")
parser.add_argument("-n", "--number", default=None, help="Phone number")
parser.add_argument("-t", "--text", default=None, help="Message text")
parser.add_argument("-l", "--locale", default=None, help="Set locale")
args = parser.parse_args()
index = -1
if args.index:
index = int(args.index)
if args.locale:
try:
locale.setlocale(locale.LC_ALL, args.locale)
except locale.Error:
print("Error: Invalid locale (available: %s)" % listlocale())
sys.exit(1)
if args.s:
if not args.number:
sys.stderr.write("Error: need number\n")
elif not args.text:
sys.stderr.write("Error: need text\n")
else:
send(index, args.number, args.text)
elif args.d:
delete(index)
else:
show(index)
+294
View File
@@ -0,0 +1,294 @@
#!/usr/bin/env python3
import shutil
import errno
import time
import json
import fcntl
import sys
import re
import os
MODEMS = "/run/modems.json"
LOCK = "/var/lock/modems.lock"
LOGFILE = "/run/modemd/modem-udev.log"
debug = False
def log(msg):
ts = '{:010.3f}'.format(time.monotonic())
with open(LOGFILE, 'a') as fd:
fd.write("[%s] modem-udev (pid %d) %s\n" % (ts, os.getpid(), msg))
def dbg(msg):
global debug
if debug:
log(msg)
def info(msg):
log(msg)
def err(msg):
log("Error: %s" % msg)
def lock():
start = time.time()
while True:
elapsed = time.time() - start
if elapsed > 10:
err("Timeout waiting for lock")
break
try:
fd = open(LOCK, "a")
fcntl.flock(fd.fileno(), fcntl.LOCK_EX)
dbg("Acquired lock")
return fd
except IOError as e:
dbg("Unable to lock (%d)" % e.errno)
if e.errno != errno.EAGAIN:
err("Unable to lock")
return None
else:
time.sleep(1)
return None
def unlock(fd):
try:
fcntl.flock(fd.fileno(), fcntl.LOCK_UN)
fd.close()
dbg("Released lock")
return True
except IOError as e:
err("Unlock failed with %d" % e.errno)
return False
def mkdir(path):
if os.path.isdir(path):
return True
try:
os.mkdir(path, mode=0o755)
shutil.chown(path, user="root", group="wheel")
except OSError:
err("Unable to mkdir %s" % path)
return False
finally:
return True
def read_modem_data():
data = {}
if os.path.exists(MODEMS):
with open(MODEMS, "r") as fd:
data = json.load(fd)
return data
def write_modem_data(data):
with open(MODEMS + ".bak", 'w') as fd:
json.dump(data, fd)
os.rename(MODEMS + ".bak", MODEMS)
return True
def get_vendor(devpath):
path = "%s/idVendor" % devpath
if os.path.exists(path):
with open(path, "r") as fd:
return fd.readline().strip()
else:
return None
def get_product(devpath):
path = "%s/idProduct" % devpath
if os.path.exists(path):
with open(path, "r") as fd:
return fd.readline().strip()
else:
return None
def update(d):
modem = None
devpath = d.get("devpath")
data = read_modem_data()
if "modems" not in data:
data = {"modems": []}
else:
for m in data["modems"]:
if devpath == m.get("devpath"):
data["modems"].remove(m)
modem = m
break
if not modem:
modem = {"devpath": devpath}
vendor = get_vendor(devpath)
if vendor:
modem["vendor"] = vendor
product = get_product(devpath)
if product:
modem["product"] = product
atport = d.get("atport")
atports = modem.get("atports", [])
if atport and atport not in atports:
ptype = d.get("ptype", "default")
if ptype == "primary":
atports.insert(0, atport)
else:
atports.append(atport)
modem["atports"] = atports
qmiport = d.get("qmiport")
qmiports = modem.get("qmiports", [])
if qmiport and qmiport not in qmiports:
qmiports.append(qmiport)
modem["qmiports"] = qmiports
iface = d.get("iface")
ifaces = modem.get("interfaces", [])
if iface and iface not in ifaces:
ifaces.append(iface)
modem["interfaces"] = ifaces
data["modems"].append(modem)
write_modem_data(data)
info("Updated modem %s" % devpath)
def add_tty(devpath):
devpath = os.path.realpath("/sys/%s/../../../../" % devpath)
info("Adding tty device %s" % devpath)
atport = os.getenv("DEVNAME")
if not atport:
err("No DEVNAME")
return False
if os.getenv("ID_MM_PORT_TYPE_AT_PRIMARY"):
ptype = "primary"
elif os.getenv("ID_MM_PORT_TYPE_AT_SECONDARY"):
ptype = "secondary"
else:
ptype = "default"
info("Adding %s atport %s for %s" % (ptype, atport, devpath))
update({"devpath": devpath, "atport": atport, "ptype": ptype})
def add_net(devpath):
devpath = os.path.realpath("/sys/%s/../../../" % devpath)
info("Adding net device %s" % devpath)
iface = os.getenv("INTERFACE", None)
if not iface:
err("No INTERFACE")
return False
r = re.search(r"wwan(\d+)\.(\d+)", iface)
if r and r.group(2):
info("Skipping interface %s" % iface)
return False
info("Adding interface %s for %s" % (iface, devpath))
update({"devpath": devpath, "iface": iface})
try:
with open("/sys/class/net/%s/qmi/raw_ip" % iface, "w") as fd:
fd.write("Y\n")
except (IOError, OSError):
err("Unable to set raw-ip")
return False
info("Interface %s has been set to rawip" % iface)
def add_usbmisc(devpath):
devpath = os.path.realpath("/sys/%s/../../../" % devpath)
info("Adding usbmisc device %s" % devpath)
qmiport = os.getenv("DEVNAME", None)
if not qmiport:
err("No DEVNAME")
return False
info("Adding qmiport %s for %s" % (qmiport, devpath))
update({"devpath": devpath, "qmiport": qmiport})
def apply():
subsystem = os.getenv("SUBSYSTEM")
if not subsystem:
err("No SUBSYSTEM")
return False
devpath = os.getenv("DEVPATH")
if not devpath:
err("No DEVPATH")
return False
if "usb" not in devpath:
info("Skipping %s" % devpath)
return False
if subsystem == "net":
add_net(devpath)
elif subsystem == "tty":
add_tty(devpath)
elif subsystem == "usbmisc":
add_usbmisc(devpath)
else:
err("Subsystem %s unhandled" % subsystem)
return False
return True
if __name__ == "__main__":
with open("/proc/cmdline", 'r') as fd:
cmdline = fd.read()
if "debug" in cmdline:
debug = True
mkdir("/run/modemd")
dbg("started")
if debug:
for i, j in os.environ.items():
dbg("ENV: %s=%s" % (i, j))
else:
if os.path.exists(LOGFILE):
os.remove(LOGFILE)
fd = lock()
if fd is None:
sys.exit(2)
if apply():
rc = 0
else:
err("Unable to add modem device")
rc = 1
unlock(fd)
dbg("ended")
sys.exit(rc)
+389
View File
@@ -0,0 +1,389 @@
#!/usr/bin/env python3
import subprocess
import argparse
import urllib.parse
import hashlib
import syslog
import json
import time
import os
import sys
tempdir = "/tmp/modem-fwupdate"
debug = False
index = 0
syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_SYSLOG)
def dbg(msg):
global debug
if debug:
syslog.syslog(syslog.LOG_INFO, msg)
def info(msg):
syslog.syslog(syslog.LOG_INFO, msg)
def err(msg):
syslog.syslog(syslog.LOG_ERR, msg)
def fatal(msg):
syslog.syslog(syslog.LOG_ALERT, msg)
rmdir(tempdir)
sys.exit(1)
def runcmd(cmd):
ret = None
try:
res = subprocess.run(cmd, check=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, text=True)
if res.returncode == 0:
if res.stdout:
ret = res.stdout.strip()
else:
ret = True
except subprocess.CalledProcessError:
err("Command failed")
dbg(res.stderr)
return None
finally:
return ret
def runcmdj(cmd):
output = runcmd(cmd)
if not output:
return None
try:
data = json.loads(output)
except json.JSONDecodeError:
return None
finally:
return data
def exists(path):
try:
os.stat(path)
except OSError:
return False
return True
def rm(path):
if os.path.exists(path):
os.remove(path)
def rmdir(path):
if os.path.isdir(path):
for f in os.listdir(path):
p = os.path.join(path, f)
if os.path.isdir(p):
rmdir(p)
else:
os.remove(p)
def mkdir(path):
if os.path.isdir(path):
return True
try:
os.mkdir(path, mode=0o755)
except OSError:
fatal("Unable to mkdir %s" % path)
def initctl(cmd, svc):
output = runcmdj(['initctl', '-j', 'status'])
for ctl in output:
if ctl["identity"] == svc:
return runcmd(['initctl', cmd, svc])
return False
def get_path(index):
for modem in runcmdj(['/usr/libexec/modemd/modem-info']):
if modem["index"] == index:
return modem["path"]
def get_device_info(index):
path = get_path(index)
if path is None:
return None
output = runcmdj(['mmcli', '-J', '-m', path])
if not output:
return None
gen = output["modem"]["generic"]
info = {
"index": index,
"vendor": gen["manufacturer"],
"model": gen["model"],
"state": gen["state"],
"qmidev": [],
"atdev": []
}
for port in gen["ports"]:
if "(qmi)" in port:
info["qmidev"].append(port.split()[0])
elif "(at)" in port:
info["atdev"].append(port.split()[0])
return info
def wait_for_device(dev, appear):
if appear:
mode = "appear"
else:
mode = "disappear"
info("Waiting for modem%d to %s" % (dev["index"], mode))
path = "/dev/%s" % dev["qmidev"][0]
timeout = 30
while timeout > 0:
if mode == "appear":
if exists(path):
return True
elif mode == "disappear":
if not exists(path):
return True
time.sleep(1)
timeout -= 1
return False
def qmiupdate(dev):
if len(dev["qmidev"]) == 0:
err("No qmi device found")
return False
qmidev = "/dev/%s" % dev["qmidev"][0]
cwefile = None
nvufile = None
for f in os.scandir(tempdir):
if f.name.endswith('cwe'):
cwefile = "%s/%s" % (tempdir, f.name)
if f.name.endswith('nvu'):
nvufile = "%s/%s" % (tempdir, f.name)
if not cwefile:
err("No CWE file found")
return False
if not nvufile:
err("No NVU file found")
return False
info("Resetting modem")
if not runcmd(['qmicli', '-d', qmidev,
'--dms-set-operating-mode=offline']):
err("Unable to set modem offline")
return False
if not runcmd(['qmicli', '-d', qmidev,
'--dms-set-operating-mode=reset']):
err("Unable to reset modem")
return False
if not wait_for_device(dev, 0):
err("Device has not disappeared")
return False
if not wait_for_device(dev, 1):
err("Device has not appeared")
return False
info("Running qmi-firmware-update, please stand by...")
cmd = ['qmi-firmware-update', '-v', '--override-download',
'-w', qmidev, '-u', cwefile, nvufile]
return runcmd(cmd)
def firmware_update_sierra(dev, url, checksum):
if not download(url, checksum):
err("Unable to prepare firmware")
return False
info("Stopping modem services")
initctl("stop", "modemd")
initctl("stop", "modem-manager")
time.sleep(3)
ret = qmiupdate(dev)
info("Starting modem services")
initctl("restart", "modem-manager")
initctl("restart", "modemd")
return ret
def atcmd(dev, command, expect=None, timeout=None):
device = "/dev/%s" % dev["atdev"][0]
cmd = ['modem-command', '-d', device, command]
if expect:
cmd += ['-e', expect]
if timeout:
cmd += ['-t', str(timeout)]
return runcmd(cmd)
def firmware_update_telit(dev, url, checksum):
if 'LN920' not in dev["model"]:
err("Not supported")
return False
if len(dev["atdev"]) == 0:
err("No AT device found")
return False
if dev["state"] != "connected":
err("Modem is not connected")
return False
u = urllib.parse.urlparse(url)
if not u:
err("Unable to parse URL")
return False
if u.scheme != "ftp":
err("Not an FTP URL")
return False
hostname = ""
username = ""
password = ""
port = 21
if u.hostname:
hostname = u.hostname
if u.username:
username = u.username
if u.password:
password = u.password
if u.port:
port = int(u.port)
if u.path:
path = u.path
if hostname == "":
err("No hostname in URL")
return False
if path == "":
err("No path in URL")
return False
parms = (hostname, port, path, username, password)
cmd = 'AT#FTPGETOTAENH=%s,%d,%s,%s,%s' % parms
exp = '#DREL'
if not atcmd(dev, cmd, exp, 60):
err("Unable to download firmware")
return False
if not atcmd(dev, 'AT#OTAUP=2'):
err("Unable to update firmware")
return False
if not atcmd(dev, 'AT#ENHRST=1,3'):
err("Unable to reboot modem")
return False
time.sleep(5)
info("Restarting modem services")
initctl("restart", "modem-manager")
initctl("restart", "modemd")
return True
def verify(path, checksum):
if not checksum or checksum == "any":
return True
sha256 = hashlib.sha256()
with open(path, 'rb') as f:
while True:
data = f.read(1024)
if not data:
break
sha256.update(data)
calculated = sha256.hexdigest()
if checksum != calculated:
err("Checksum mismatch (%s vs. %s)" % (calculated, checksum))
return False
else:
info("Verified checksum")
return True
def download(url, checksum):
path = "%s/firmware.zip" % tempdir
info("Downloading %s" % url)
if not runcmd(['curl', '-s', '-L', url, '-o', path]):
err("Unable to download firmware")
return False
if not verify(path, checksum):
err("Checksum verification failed")
return False
if not runcmd(['unzip', '-d', tempdir, path]):
err("Unable to unpack firmware")
return False
return True
if __name__ == "__main__":
syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_DAEMON)
parser = argparse.ArgumentParser(prog='modem-firmware-update')
parser.add_argument("-i", "--index", default=None, help="Modem index")
parser.add_argument("-u", "--url", default=None, help="URL to firmware")
parser.add_argument("-c", "--checksum", default=None,
help="SHA256 checksum of firmware")
parser.add_argument('-d', action='store_true')
args = parser.parse_args()
if args.d:
debug = True
if args.index:
index = int(args.index)
if not args.url:
fatal("No firmware URL specified")
rmdir(tempdir)
mkdir(tempdir)
dev = get_device_info(index)
if dev is None:
fatal("No device found for modem%d" % index)
info("Starting firmware update for %s" % dev["model"])
if "Sierra" in dev["vendor"]:
ret = firmware_update_sierra(dev, args.url, args.checksum)
elif "Telit" in dev["vendor"]:
ret = firmware_update_telit(dev, args.url, args.checksum)
else:
fatal("Unsupported vendor")
if ret:
info("Firmware update succeeded")
else:
fatal("Firmware update failed")
rmdir(tempdir)
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
import argparse
import subprocess
import sys
verbose = False
def runcmd(cmd):
if verbose:
print("Running: %s" % " ".join(cmd))
ret = None
try:
res = subprocess.run(cmd, capture_output=True, text=True)
if verbose:
print(res.stdout)
if res.returncode == 0:
if res.stdout:
ret = res.stdout.strip()
else:
ret = True
except subprocess.CalledProcessError:
return None
finally:
return ret
def ussd(index, code):
cmd = ['/sbin/modem-command',
'--index', str(index),
'--expect', '+CUSD',
'--timeout', '60',
'AT+CUSD=1,"%s",15' % code]
if verbose:
cmd.append('--verbose')
output = runcmd(cmd)
if output is None:
sys.stderr.write("ERROR: Command failed\n")
return False
resp = output.split('"')
if len(resp) == 3:
print(resp[1])
return True
if __name__ == "__main__":
parser = argparse.ArgumentParser(prog='modem-ussd')
parser.add_argument("-i", "--index", default=0, help="Modem index")
parser.add_argument("-c", "--code", default=None, help="USSD codec")
parser.add_argument("-v", "--verbose", action="store_true")
args = parser.parse_args()
if args.verbose:
verbose = True
index = 0
if args.index:
index = int(args.index)
if not args.code:
sys.stderr.write("Error: need USSD code\n")
else:
if ussd(index, args.code):
sys.exit(0)
sys.exit(1)
+1618
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
# Placeholder — add module names here if any modem drivers need explicit
# loading (e.g. when device IDs are not yet upstream in the driver table).
+8
View File
@@ -0,0 +1,8 @@
ACTION!="add", GOTO="modemd_end"
SUBSYSTEM=="net", ENV{INTERFACE}=="wwan[0-9]*", RUN+="/usr/libexec/modemd/modem-udev"
SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}=="1", RUN+="/usr/libexec/modemd/modem-udev"
SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_SECONDARY}=="1", RUN+="/usr/libexec/modemd/modem-udev"
SUBSYSTEM=="usbmisc", KERNEL=="cdc-wdm[0-9]*",, RUN+="/usr/libexec/modemd/modem-udev"
LABEL="modemd_end"
+7
View File
@@ -0,0 +1,7 @@
# Bind USB modem devices whose IDs are not yet upstream in qmi_wwan.
# Required until the IDs are accepted into the kernel driver.
ACTION!="add", GOTO="qmi_wwan_ids_end"
SUBSYSTEM!="usb", GOTO="qmi_wwan_ids_end"
ENV{DEVTYPE}!="usb_device", GOTO="qmi_wwan_ids_end"
LABEL="qmi_wwan_ids_end"
+240
View File
@@ -0,0 +1,240 @@
#!/usr/bin/env python3
import subprocess
import json
import fcntl
import struct
import time
import syslog
import sys
import os
syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_SYSLOG)
def log(msg):
syslog.syslog(syslog.LOG_INFO, msg)
def err(msg):
syslog.syslog(syslog.LOG_ERR, msg)
def fatal(msg):
syslog.syslog(syslog.LOG_ALERT, msg)
sys.exit(1)
def runcmd(cmd):
ret = None
try:
res = subprocess.run(cmd, check=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, text=True)
if res.returncode == 0:
if res.stdout:
ret = res.stdout.strip()
else:
ret = True
except subprocess.CalledProcessError:
return None
finally:
return ret
def runcmdj(cmd):
output = runcmd(cmd)
if not output:
return None
try:
data = json.loads(output)
except json.JSONDecodeError:
return None
finally:
return data
def fread(path):
if os.path.exists(path):
with open(path, "r") as fd:
output = str(fd.read())
if output:
return output.strip()
return None
def freadj(path):
output = fread(path)
if not output:
return None
try:
data = json.loads(output)
except json.JSONDecodeError:
err("Unable to parse json output")
return None
finally:
return data
def find_modem(setup, index):
for modem in setup["modems"]:
if modem["index"] == index:
return modem
return None
def find_sim(setup, index):
for sim in setup["sims"]:
if sim["index"] == index:
return sim
return None
def pwrctl(setup, state):
for modem in setup["modems"]:
path = "/sys/class/pcie/slot%d/pwrctl" % modem["slot"]
if not os.path.exists(path):
continue
with open(path, "w") as fd:
fd.write("%s\n" % state)
def sim_ctrl(cmd, slots):
req = struct.pack('2i', *slots)
if not os.path.exists("/dev/simctrl"):
return None
try:
fd = os.open("/dev/simctrl", os.O_RDWR)
except OSError:
fatal("Cannot open sim ctrl")
try:
resp = fcntl.ioctl(fd, cmd, req)
except OSError:
fatal("Cannot run sim ctrl ioctl")
if not resp:
return None
else:
return struct.unpack('2i', resp)
def sim_get_slots():
# NM_SIMIOC_SLOT_GET is 0x80085300 (see linux/nm-sim.h)
return sim_ctrl(0x80085300, (0, 0))
def sim_set_slots(slots):
# NM_SIMIOC_SLOT_SET is 0x40085301 (see linux/nm-sim.h)
return sim_ctrl(0x40085301, slots)
def slot_setup(setup):
slots = []
for sim in setup["sims"]:
log("Connecting sim%d to slot %d" % (sim["index"], sim["newslot"]))
slots.append(sim["newslot"])
if sim_set_slots(tuple(slots)) is None:
return False
else:
return True
def change_setup(setup):
log("Changing SIM setup")
runcmd(["initctl", "stop", "modem-manager"])
time.sleep(1)
pwrctl(setup, "down")
time.sleep(1)
if slot_setup(setup) is False:
err("Unable to set up sim slots")
pwrctl(setup, "up")
time.sleep(1)
runcmd(["initctl", "restart", "modem-manager"])
return True
def swapslot(slot):
if slot == 0:
return 1
elif slot == 1:
return 0
else:
return -1
def sim_setup(setup):
changed = False
config = runcmdj(["sysrepocfg", "-f", "json", "-X", "-m", "infix-modem"])
if not config or "infix-modem:modems" not in config:
err("Cannot read config")
return False
for cfg in config["infix-modem:modems"]["modem"]:
if not cfg.get("enabled", False) or cfg.get("index", -1) < 0:
continue
modem = find_modem(setup, cfg["index"])
sim = find_sim(setup, cfg.get("sim", -1))
if modem and sim:
sim["newslot"] = modem["slot"]
if sim["newslot"] != sim["slot"]:
changed = True
if not changed:
log("SIMs already set up")
return True
if len(setup["modems"]) == 2 and len(setup["sims"]) == 2:
for i in range(0, 2):
o = 1 if i == 0 else 0
if setup["sims"][i]["newslot"] == -1:
log("Swapping slot of sim%d" % setup["sims"][i]["index"])
setup["sims"][i]["newslot"] = swapslot(setup["sims"][o]["newslot"])
return change_setup(setup)
def read_setup():
setup = {
"modems": [],
"sims": []
}
modules = freadj("/run/modules.json")
if not modules:
log("No modules.json found, skipping SIM setup")
return None
for module in modules.get("modules", []):
if module["slot"] > -1 and module["type"] == "modem":
setup["modems"].append({"index": module["index"],
"slot": module["slot"]})
slots = sim_get_slots()
if slots:
for index in range(0, len(slots)):
setup["sims"].append({"index": index,
"slot": slots[index],
"newslot": -1})
return setup
if __name__ == "__main__":
setup = read_setup()
if setup is None:
sys.exit(0)
if sim_setup(setup) is False:
fatal("Unable set up SIMs")
sys.exit(0)
+3
View File
@@ -126,6 +126,9 @@ def main():
elif model == 'ieee1588-ptp-tt':
from . import ieee1588_ptp
yang_data = ieee1588_ptp.operational()
elif model == 'infix-modem':
from . import infix_modem
yang_data = infix_modem.operational()
else:
common.LOG.warning("Unsupported model %s", model)
sys.exit(1)
+12
View File
@@ -0,0 +1,12 @@
from .common import LOG
from .host import HOST
def operational():
modems = HOST.run_json(['/usr/libexec/modemd/modem-info'], [])
return {
"infix-modem:modems": {
"modem": [m for m in modems]
}
}