diff --git a/configs/aarch64_defconfig b/configs/aarch64_defconfig index 3a933243..a6f80703 100644 --- a/configs/aarch64_defconfig +++ b/configs/aarch64_defconfig @@ -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 diff --git a/package/Config.in b/package/Config.in index 110d247b..9b04f462 100644 --- a/package/Config.in +++ b/package/Config.in @@ -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" diff --git a/package/feature-modem/Config.in b/package/feature-modem/Config.in new file mode 100644 index 00000000..ed888950 --- /dev/null +++ b/package/feature-modem/Config.in @@ -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). diff --git a/package/feature-modem/feature-modem.mk b/package/feature-modem/feature-modem.mk new file mode 100644 index 00000000..d5a7827b --- /dev/null +++ b/package/feature-modem/feature-modem.mk @@ -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)) diff --git a/package/modemd/Config.in b/package/modemd/Config.in new file mode 100644 index 00000000..0b8ddafe --- /dev/null +++ b/package/modemd/Config.in @@ -0,0 +1,6 @@ +config BR2_PACKAGE_MODEMD + bool "modemd" + select BR2_PACKAGE_MODEM_MANAGER + select BR2_PACKAGE_PYTHON3 + help + Daemon which manages modems. diff --git a/package/modemd/modemd.hash b/package/modemd/modemd.hash new file mode 100644 index 00000000..db4831e3 --- /dev/null +++ b/package/modemd/modemd.hash @@ -0,0 +1,2 @@ +# Locally calculated +sha256 25b33026a661c4c550374cfcba6890a4363bf19db0c1c31a6e65b5edd113ecf0 LICENSE diff --git a/package/modemd/modemd.mk b/package/modemd/modemd.mk new file mode 100644 index 00000000..8b84dfac --- /dev/null +++ b/package/modemd/modemd.mk @@ -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)) diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/modem-manager.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/modem-manager.conf new file mode 100644 index 00000000..8c060e2d --- /dev/null +++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/modem-manager.conf @@ -0,0 +1 @@ +service [2345789] ModemManager -- ModemManager daemon diff --git a/patches/libqmi/0001-Ignore-sysfs-for-download-mode.patch b/patches/libqmi/0001-Ignore-sysfs-for-download-mode.patch new file mode 100644 index 00000000..31d4ee19 --- /dev/null +++ b/patches/libqmi/0001-Ignore-sysfs-for-download-mode.patch @@ -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; diff --git a/patches/modem-manager/0000-udevdir.patch b/patches/modem-manager/0000-udevdir.patch new file mode 100644 index 00000000..139d208e --- /dev/null +++ b/patches/modem-manager/0000-udevdir.patch @@ -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') diff --git a/patches/modem-manager/0002-multiplex-interface-name.patch b/patches/modem-manager/0002-multiplex-interface-name.patch new file mode 100644 index 00000000..2a4ad976 --- /dev/null +++ b/patches/modem-manager/0002-multiplex-interface-name.patch @@ -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 diff --git a/patches/modem-manager/0003-port-probe-trust-ID_MM_PORT_TYPE_MBIM-udev-hint.patch b/patches/modem-manager/0003-port-probe-trust-ID_MM_PORT_TYPE_MBIM-udev-hint.patch new file mode 100644 index 00000000..f9672e5e --- /dev/null +++ b/patches/modem-manager/0003-port-probe-trust-ID_MM_PORT_TYPE_MBIM-udev-hint.patch @@ -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); diff --git a/src/confd/src/Makefile.am b/src/confd/src/Makefile.am index fc67edbb..979e56e7 100644 --- a/src/confd/src/Makefile.am +++ b/src/confd/src/Makefile.am @@ -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 \ diff --git a/src/confd/src/core.c b/src/confd/src/core.c index 956b5501..86db25fe 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -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: diff --git a/src/confd/src/core.h b/src/confd/src/core.h index b56c8bf3..dadfddfc 100644 --- a/src/confd/src/core.h +++ b/src/confd/src/core.h @@ -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_ */ diff --git a/src/confd/src/interfaces.c b/src/confd/src/interfaces.c index 132ccca8..7585aa29 100644 --- a/src/confd/src/interfaces.c +++ b/src/confd/src/interfaces.c @@ -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: diff --git a/src/confd/src/interfaces.h b/src/confd/src/interfaces.h index a7bb77c0..aece9e46 100644 --- a/src/confd/src/interfaces.h +++ b/src/confd/src/interfaces.h @@ -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); diff --git a/src/confd/src/modem.c b/src/confd/src/modem.c new file mode 100644 index 00000000..a7d83ddd --- /dev/null +++ b/src/confd/src/modem.c @@ -0,0 +1,330 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#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(®ex, "index='([0-9]+)'", REG_EXTENDED)) + return -1; + + ret = regexec(®ex, 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(®ex); + + 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; +} diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index 85082999..bf088c06 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -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" ) diff --git a/src/confd/yang/confd/infix-modem.yang b/src/confd/yang/confd/infix-modem.yang new file mode 100644 index 00000000..f0a52fe6 --- /dev/null +++ b/src/confd/yang/confd/infix-modem.yang @@ -0,0 +1,1089 @@ +/* + * Infix Modem YANG module + */ + +module infix-modem { + yang-version 1.1; + namespace "urn:infix:params:xml:ns:yang:infix-modem"; + prefix "infix-modem"; + + import ietf-inet-types { + prefix "inet"; + } + import ietf-interfaces { + prefix "if"; + } + import ietf-yang-types { + prefix yang; + } + import ietf-netconf-acm { + prefix nacm; + } + + organization "KernelKit"; + contact "kernelkit@googlegroups.com"; + description "YANG data model for modems."; + + revision 2024-03-15 { + description "Initial revision"; + reference "internal"; + } + + container modems { + description "Configuration of modems."; + + list modem { + key "index"; + unique sim; + description "List of modems"; + + leaf index { + type uint8; + description "Index of the modem."; + } + leaf enabled { + type boolean; + description "Enable or disable modem."; + default false; + } + leaf path { + type string; + description "Path to modem."; + config false; + } + leaf sim { + type int8; + description "Index of SIM card."; + } + leaf pin { + type string; + description "PIN code to unlock SIM card."; + nacm:default-deny-all; + } + leaf puk { + type string; + description "PUK code to unlock SIM card."; + nacm:default-deny-all; + } + + leaf preferred-mode { + type access-mode; + description "Preferred mode."; + } + list allowed-mode { + key "mode"; + description "List of allowed modes."; + + leaf mode { + type access-mode; + description "Allowed mode."; + } + } + leaf carrier { + type string; + description "Network carrier profile."; + } + list band { + key "band"; + description "List of enabled bands."; + + leaf band { + type band; + description "Enabled band."; + } + } + list bearer { + key "index"; + description "List of bearers."; + + leaf index { + type uint8; + description "Index of the bearer."; + } + leaf apn-type { + type bearer-apn-type; + description "APN type."; + } + leaf apn { + type string; + description "Access Point Name (APN)."; + } + leaf username { + type string; + description "User name (if any) required by the network."; + default ""; + } + leaf password { + type string; + description "Password (if any) required by the network."; + default ""; + nacm:default-deny-all; + } + leaf allow-roaming { + type boolean; + description "Flag to tell whether connection is allowed during roaming."; + default false; + } + leaf ip-type { + type ip-family; + description "IP addressing type."; + default ipv4v6; + } + leaf default-route { + type boolean; + description "Set default route for connection"; + default false; + } + leaf firewall-enabled { + type boolean; + description "Enable firewall for connection."; + default false; + } + leaf dns-enabled { + type boolean; + description "Enable DNS resolver for connection."; + default true; + } + } + container location { + leaf enabled { + type boolean; + description "Enable location gathering."; + default false; + } + + list source { + key "source"; + description "List of location sources."; + + leaf source { + type location-source; + description "Source used for location gathering."; + } + } + } + + container info { + description "Modem information."; + config false; + + leaf manufacturer { + type string; + description "Manufacturer of the modem."; + } + leaf model { + type string; + description "Model name of the modem."; + } + leaf hardware-revision { + type string; + description "Hardware revision of the modem."; + } + leaf firmware-version { + type string; + description "Firmware version of the modem."; + } + leaf-list supported-carrier { + type string; + description "List of supported carriers."; + } + leaf serial-number { + type string; + description "Serial number of the modem (IMEI)."; + } + leaf-list phone-number { + type string; + description "List of own phone numbers."; + } + leaf imsi { + type string; + description "International Mobile Subscriber Identity"; + } + leaf iccid { + type string; + description "ICCID of the SIM card."; + } + } + + container status { + description "Modem Status Information."; + config false; + + leaf sim-active { + type int8; + description "Index of active SIM card."; + } + leaf sim-present { + type boolean; + description "True if SIM card is present"; + } + leaf state { + type string; + description "Modem state."; + } + leaf state-failed-reason { + type string; + description "Reason of failed modem state."; + } + leaf selected-carrier { + type string; + description "Selected carrier."; + } + leaf signal-quality { + type uint8; + description "Signal strength in percent (0-100)."; + } + leaf signal-rssi { + type string; + description "RSSI (Received Signal Strength Indication) in dBm."; + } + leaf signal-rsrp { + type string; + description "RSRP (Reference Signal Received Power) in dBm."; + } + leaf signal-rsrq { + type string; + description "RSRQ (Reference Signal Received Quality) in dB."; + } + leaf signal-rscp { + type string; + description "RSCP (Received Signal Code Power) in dBm."; + } + leaf signal-snr { + type string; + description "SNR (Signal Noise Ratio) in dB."; + } + leaf signal-sinr { + type string; + description "SINR (Signal Interference Noise Ratio) ) in dB."; + } + + container cellular { + description "Cellular network information."; + + leaf registration-state { + description "Modem registration state."; + type registration-state; + } + leaf operator-name { + type string; + description "Name of the cellular network operator."; + } + leaf operator-id { + type string; + description "Identifier of the cellular network operator (MCC/MNC)."; + } + leaf network-type { + type string; + description "Type of cellular network (e.g. LTE)"; + } + leaf packet-service-state { + description "Modem packet service state."; + type string; + } + } + + list bearer { + description "Bearer information."; + key "path"; + + leaf path { + type string; + description "Path to bearer."; + } + leaf interface { + type if:interface-ref; + description "Network interface name for the bearer."; + } + leaf connected { + type boolean; + description "Indicates whether bearer is connected."; + } + leaf connection-failed-reason { + type string; + description "Reason of failed connection of bearer."; + } + leaf ipv4-address { + type inet:ipv4-address; + description "The IPv4 address of the bearer connection."; + } + leaf ipv4-prefix { + type uint16; + description "The IPv4 prefix of the bearer connection."; + } + leaf ipv6-address { + type inet:ipv6-address; + description "The IPv6 address of the bearer connection."; + } + leaf ipv6-prefix { + type uint16; + description "The IPv6 prefix of the bearer connection."; + } + leaf in-bytes { + type yang:counter64; + description "The number of received bytes of the bearer connection."; + } + leaf out-bytes { + type yang:counter64; + description "The number of sent bytes of the bearer connection."; + } + leaf total-in-bytes { + type yang:counter64; + description "Total number of received bytes of all connections."; + } + leaf total-out-bytes { + type yang:counter64; + description "Total number of sent bytes of all connections."; + } + leaf total-duration { + type string; + description "Total duration of all connections in seconds."; + } + } + + container location { + description "Location information."; + + leaf latitude { + type string; + description "Latitude in decimal degrees."; + } + leaf longitude { + type string; + description "Longitude in decimal degrees."; + } + leaf altitude { + type string; + description "Altitude above sea level in meters."; + } + leaf mcc { + type string; + description "Mobile Country Code of the operator."; + } + leaf mnc { + type string; + description "Mobile Network Code of the operator."; + } + leaf lac { + type string; + description "Location Area Code of the operator."; + } + leaf cid { + type string; + description "Cell Identifier of the operator."; + } + leaf tac { + type string; + description "Tracking Area Code of the operator."; + } + } + } + + notification status-update { + description "Notification for modem status updates."; + leaf desc { + type string; + description "Description of the status update."; + mandatory true; + } + } + } + } + + rpc restart { + description "Restart modem."; + input { + leaf index { + type string; + description "Modem index."; + } + } + } + rpc reset { + description "Reset the modem to factory defaults."; + input { + leaf index { + type string; + description "Modem index."; + } + } + } + rpc send-sms { + description "Send an SMS message."; + input { + leaf index { + type string; + description "Modem index."; + } + leaf phone-number { + type string; + description "Recipient's phone number."; + } + leaf message-text { + type string; + description "Text of the SMS message."; + } + } + } + + typedef bearer-apn-type { + description "APN type for bearer."; + + type enumeration { + enum initial { + description "APN used for the initial attach procedure."; + } + enum default { + description "Default connection APN providing access to the Internet."; + } + enum ims { + description "APN providing access to IMS services."; + } + enum mms { + description "APN providing access to MMS services."; + } + enum management { + description "APN providing access to over-the-air device management procedures."; + } + enum voice { + description "APN providing access to voice-over-IP services."; + } + enum emergency { + description "APN providing access to emergency services."; + } + enum private { + description "APN providing access to private networks."; + } + enum purchase { + description "APN providing access to over-the-air activation sites."; + } + enum video_share { + description "APN providing access to video sharing service."; + } + enum local { + description "APN providing access to a local connection with the device."; + } + enum app { + description "APN providing access to certain applications allowed by mobile operators."; + } + enum xcap { + description "APN providing access to XCAP provisioning on IMS services."; + } + enum tethering { + description "APN providing access to mobile hotspot tethering."; + } + } + } + + typedef ip-family { + description "IP address family."; + + type enumeration { + enum ipv4 { + description "IPv4 address family."; + } + enum ipv6 { + description "IPv6 address family."; + } + enum ipv4v6 { + description "IPv4 or IPv6 address family."; + } + } + } + + typedef access-mode { + type enumeration { + enum cs { + description "Circuit-switched technologies (e.g. CSD, GSM)."; + } + enum 2g { + description "2G technologies (e.g. GPRS, EDGE)."; + } + enum 3g { + description "3G technologies (e.g. UMTS, HSxPA)."; + } + enum 4g { + description "4G technologies (e.g. LTE)."; + } + enum 5g { + description "5G technologies (e.g. 5GNR)."; + } + enum any { + description "Any technologies."; + } + } + } + + typedef registration-state { + type enumeration { + enum idle { + description "Not registered, not searching for new operator to register."; + } + enum home { + description "Registered on home network."; + } + enum searching { + description "Not registered, searching for new operator to register with."; + } + enum denied { + description "Registration denied."; + } + enum roaming { + description "Registered on a roaming network."; + } + enum unknown { + description "Unknown registration state."; + } + } + } + + typedef band { + type enumeration { + enum any { + description "Any band."; + } + enum egsm { + description "GSM/GPRS/EDGE 900 MHz."; + } + enum dcs { + description "GSM/GPRS/EDGE 1800 MHz."; + } + enum pcs { + description "GSM/GPRS/EDGE 1900 MHz."; + } + enum g850 { + description "GSM/GPRS/EDGE 850 MHz."; + } + enum g450 { + description "GSM/GPRS/EDGE 450 MHz."; + } + enum g480 { + description "GSM/GPRS/EDGE 480 MHz."; + } + enum g750 { + description "GSM/GPRS/EDGE 750 MHz."; + } + enum g380 { + description "GSM/GPRS/EDGE 380 MHz."; + } + enum g410 { + description "GSM/GPRS/EDGE 410 MHz."; + } + enum g710 { + description "GSM/GPRS/EDGE 710 MHz."; + } + enum g810 { + description "GSM/GPRS/EDGE 810 MHz."; + } + enum utran-1 { + description "UMTS 2100 MHz (IMT, UTRAN band 1)."; + } + enum utran-2 { + description "UMTS 1900 MHz (PCS A-F, UTRAN band 2)."; + } + enum utran-3 { + description "UMTS 1800 MHz (DCS, UTRAN band 3)."; + } + enum utran-4 { + description "UMTS 1700 MHz (AWS A-F, UTRAN band 4)."; + } + enum utran-5 { + description "UMTS 850 MHz (CLR, UTRAN band 5)."; + } + enum utran-6 { + description "UMTS 800 MHz (UTRAN band 6)."; + } + enum utran-7 { + description "UMTS 2600 MHz (IMT-E, UTRAN band 7)."; + } + enum utran-8 { + description "UMTS 900 MHz (E-GSM, UTRAN band 8)."; + } + enum utran-9 { + description "UMTS 1700 MHz (UTRAN band 9)."; + } + enum utran-10 { + description "UMTS 1700 MHz (EAWS A-G, UTRAN band 10)."; + } + enum utran-11 { + description "UMTS 1500 MHz (LPDC, UTRAN band 11)."; + } + enum utran-12 { + description "UMTS 700 MHz (LSMH A/B/C, UTRAN band 12)."; + } + enum utran-13 { + description "UMTS 700 MHz (USMH C, UTRAN band 13)."; + } + enum utran-14 { + description "UMTS 700 MHz (USMH D, UTRAN band 14)."; + } + enum utran-19 { + description "UMTS 800 MHz (UTRAN band 19)."; + } + enum utran-20 { + description "UMTS 800 MHz (EUDD, UTRAN band 20)."; + } + enum utran-21 { + description "UMTS 1500 MHz (UPDC, UTRAN band 21)."; + } + enum utran-22 { + description "UMTS 3500 MHz (UTRAN band 22)."; + } + enum utran-25 { + description "UMTS 1900 MHz (EPCS A-G, UTRAN band 25)."; + } + enum utran-26 { + description "UMTS 850 MHz (ECLR, UTRAN band 26)."; + } + enum utran-32 { + description "UMTS 1500 MHz (L-band, UTRAN band 32)."; + } + enum eutran-1 { + description "E-UTRAN band 1."; + } + enum eutran-2 { + description "E-UTRAN band 2."; + } + enum eutran-3 { + description "E-UTRAN band 3."; + } + enum eutran-4 { + description "E-UTRAN band 4."; + } + enum eutran-5 { + description "E-UTRAN band 5."; + } + enum eutran-6 { + description "E-UTRAN band 6."; + } + enum eutran-7 { + description "E-UTRAN band 7."; + } + enum eutran-8 { + description "E-UTRAN band 8."; + } + enum eutran-9 { + description "E-UTRAN band 9."; + } + enum eutran-10 { + description "E-UTRAN band 10."; + } + enum eutran-11 { + description "E-UTRAN band 11."; + } + enum eutran-12 { + description "E-UTRAN band 12."; + } + enum eutran-13 { + description "E-UTRAN band 13."; + } + enum eutran-14 { + description "E-UTRAN band 14."; + } + enum eutran-17 { + description "E-UTRAN band 17."; + } + enum eutran-18 { + description "E-UTRAN band 18."; + } + enum eutran-19 { + description "E-UTRAN band 19."; + } + enum eutran-20 { + description "E-UTRAN band 20."; + } + enum eutran-21 { + description "E-UTRAN band 21."; + } + enum eutran-22 { + description "E-UTRAN band 22."; + } + enum eutran-23 { + description "E-UTRAN band 23."; + } + enum eutran-24 { + description "E-UTRAN band 24."; + } + enum eutran-25 { + description "E-UTRAN band 25."; + } + enum eutran-26 { + description "E-UTRAN band 26."; + } + enum eutran-27 { + description "E-UTRAN band 27."; + } + enum eutran-28 { + description "E-UTRAN band 28."; + } + enum eutran-29 { + description "E-UTRAN band 29."; + } + enum eutran-30 { + description "E-UTRAN band 30."; + } + enum eutran-31 { + description "E-UTRAN band 31."; + } + enum eutran-32 { + description "E-UTRAN band 32."; + } + enum eutran-33 { + description "E-UTRAN band 33."; + } + enum eutran-34 { + description "E-UTRAN band 34."; + } + enum eutran-35 { + description "E-UTRAN band 35."; + } + enum eutran-36 { + description "E-UTRAN band 36."; + } + enum eutran-37 { + description "E-UTRAN band 37."; + } + enum eutran-38 { + description "E-UTRAN band 38."; + } + enum eutran-39 { + description "E-UTRAN band 39."; + } + enum eutran-40 { + description "E-UTRAN band 40."; + } + enum eutran-41 { + description "E-UTRAN band 41."; + } + enum eutran-42 { + description "E-UTRAN band 42."; + } + enum eutran-43 { + description "E-UTRAN band 43."; + } + enum eutran-44 { + description "E-UTRAN band 44."; + } + enum eutran-45 { + description "E-UTRAN band 45."; + } + enum eutran-46 { + description "E-UTRAN band 46."; + } + enum eutran-47 { + description "E-UTRAN band 47."; + } + enum eutran-48 { + description "E-UTRAN band 48."; + } + enum eutran-49 { + description "E-UTRAN band 49."; + } + enum eutran-50 { + description "E-UTRAN band 50."; + } + enum eutran-51 { + description "E-UTRAN band 51."; + } + enum eutran-52 { + description "E-UTRAN band 52."; + } + enum eutran-53 { + description "E-UTRAN band 53."; + } + enum eutran-54 { + description "E-UTRAN band 54."; + } + enum eutran-55 { + description "E-UTRAN band 55."; + } + enum eutran-56 { + description "E-UTRAN band 56."; + } + enum eutran-57 { + description "E-UTRAN band 57."; + } + enum eutran-58 { + description "E-UTRAN band 58."; + } + enum eutran-59 { + description "E-UTRAN band 59."; + } + enum eutran-60 { + description "E-UTRAN band 60."; + } + enum eutran-61 { + description "E-UTRAN band 61."; + } + enum eutran-62 { + description "E-UTRAN band 62."; + } + enum eutran-63 { + description "E-UTRAN band 63."; + } + enum eutran-64 { + description "E-UTRAN band 64."; + } + enum eutran-65 { + description "E-UTRAN band 65."; + } + enum eutran-66 { + description "E-UTRAN band 66."; + } + enum eutran-67 { + description "E-UTRAN band 67."; + } + enum eutran-68 { + description "E-UTRAN band 68."; + } + enum eutran-69 { + description "E-UTRAN band 69."; + } + enum eutran-70 { + description "E-UTRAN band 70."; + } + enum eutran-71 { + description "E-UTRAN band 71."; + } + enum eutran-85 { + description "E-UTRAN band 85."; + } + enum cdma-bc0 { + description "CDMA Band Class 0 (US Cellular 850MHz)."; + } + enum cdma-bc1 { + description "CDMA Band Class 1 (US PCS 1900MHz)."; + } + enum cdma-bc2 { + description "CDMA Band Class 2 (UK TACS 900MHz)."; + } + enum cdma-bc3 { + description "CDMA Band Class 3 (Japanese TACS)."; + } + enum cdma-bc4 { + description "CDMA Band Class 4 (Korean PCS)."; + } + enum cdma-bc5 { + description "CDMA Band Class 5 (NMT 450MHz)."; + } + enum cdma-bc6 { + description "CDMA Band Class 6 (IMT2000 2100MHz)."; + } + enum cdma-bc7 { + description "CDMA Band Class 7 (Cellular 700MHz)."; + } + enum cdma-bc8 { + description "CDMA Band Class 8 (1800MHz)."; + } + enum cdma-bc9 { + description "CDMA Band Class 9 (900MHz)."; + } + enum cdma-bc10 { + description "CDMA Band Class 10 (US Secondary 800)."; + } + enum cdma-bc11 { + description "CDMA Band Class 11 (European PAMR 400MHz)."; + } + enum cdma-bc12 { + description "CDMA Band Class 12 (PAMR 800MHz)."; + } + enum cdma-bc13 { + description "CDMA Band Class 13 (IMT2000 2500MHz Expansion)."; + } + enum cdma-bc14 { + description "CDMA Band Class 14 (More US PCS 1900MHz)."; + } + enum cdma-bc15 { + description "CDMA Band Class 15 (AWS 1700MHz)."; + } + enum cdma-bc16 { + description "CDMA Band Class 16 (US 2500MHz)."; + } + enum cdma-bc17 { + description "CDMA Band Class 17 (US 2500MHz Forward Link Only)."; + } + enum cdma-bc18 { + description "CDMA Band Class 18 (US 700MHz Public Safety)."; + } + enum cdma-bc19 { + description "CDMA Band Class 19 (US Lower 700MHz)."; + } + enum ngran-1 { + description "NGRAN band 1."; + } + enum ngran-2 { + description "NGRAN band 2."; + } + enum ngran-3 { + description "NGRAN band 3."; + } + enum ngran-5 { + description "NGRAN band 5."; + } + enum ngran-7 { + description "NGRAN band 7."; + } + enum ngran-8 { + description "NGRAN band 8."; + } + enum ngran-12 { + description "NGRAN band 12."; + } + enum ngran-13 { + description "NGRAN band 13."; + } + enum ngran-14 { + description "NGRAN band 14."; + } + enum ngran-18 { + description "NGRAN band 18."; + } + enum ngran-20 { + description "NGRAN band 20."; + } + enum ngran-25 { + description "NGRAN band 25."; + } + enum ngran-26 { + description "NGRAN band 26."; + } + enum ngran-28 { + description "NGRAN band 28."; + } + enum ngran-29 { + description "NGRAN band 29."; + } + enum ngran-30 { + description "NGRAN band 30."; + } + enum ngran-34 { + description "NGRAN band 34."; + } + enum ngran-38 { + description "NGRAN band 38."; + } + enum ngran-39 { + description "NGRAN band 39."; + } + enum ngran-40 { + description "NGRAN band 40."; + } + enum ngran-41 { + description "NGRAN band 41."; + } + enum ngran-48 { + description "NGRAN band 48."; + } + enum ngran-50 { + description "NGRAN band 50."; + } + enum ngran-51 { + description "NGRAN band 51."; + } + enum ngran-53 { + description "NGRAN band 53."; + } + enum ngran-65 { + description "NGRAN band 65."; + } + enum ngran-66 { + description "NGRAN band 66."; + } + enum ngran-70 { + description "NGRAN band 70."; + } + enum ngran-71 { + description "NGRAN band 71."; + } + enum ngran-74 { + description "NGRAN band 74."; + } + enum ngran-75 { + description "NGRAN band 75."; + } + enum ngran-76 { + description "NGRAN band 76."; + } + enum ngran-77 { + description "NGRAN band 77."; + } + enum ngran-78 { + description "NGRAN band 78."; + } + enum ngran-79 { + description "NGRAN band 79."; + } + enum ngran-80 { + description "NGRAN band 80."; + } + enum ngran-81 { + description "NGRAN band 81."; + } + enum ngran-82 { + description "NGRAN band 82."; + } + enum ngran-83 { + description "NGRAN band 83."; + } + enum ngran-84 { + description "NGRAN band 84."; + } + enum ngran-86 { + description "NGRAN band 86."; + } + enum ngran-89 { + description "NGRAN band 89."; + } + enum ngran-90 { + description "NGRAN band 90."; + } + enum ngran-91 { + description "NGRAN band 91."; + } + enum ngran-92 { + description "NGRAN band 92."; + } + enum ngran-93 { + description "NGRAN band 93."; + } + enum ngran-94 { + description "NGRAN band 94."; + } + enum ngran-95 { + description "NGRAN band 95."; + } + enum ngran-257 { + description "NGRAN band 257."; + } + enum ngran-258 { + description "NGRAN band 258."; + } + enum ngran-260 { + description "NGRAN band 260."; + } + enum ngran-261 { + description "NGRAN band 261."; + } + } + } + + typedef location-source { + type enumeration { + enum gps { + description "GPS location source."; + } + enum agps-msa { + description "MSA A-GPS location source."; + } + enum agps-msb { + description "MSB A-GPS location source."; + } + enum 3gpp { + description "3GPP location source."; + } + enum cdma { + description "CDMA location source."; + } + } + } +} diff --git a/src/confd/yang/confd/infix-modem@2024-03-15.yang b/src/confd/yang/confd/infix-modem@2024-03-15.yang new file mode 120000 index 00000000..467b07a4 --- /dev/null +++ b/src/confd/yang/confd/infix-modem@2024-03-15.yang @@ -0,0 +1 @@ +infix-modem.yang \ No newline at end of file diff --git a/src/modemd/77-mm-dell-port-types.rules b/src/modemd/77-mm-dell-port-types.rules new file mode 100644 index 00000000..376b75f3 --- /dev/null +++ b/src/modemd/77-mm-dell-port-types.rules @@ -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" diff --git a/src/modemd/LICENSE b/src/modemd/LICENSE new file mode 100644 index 00000000..3db075fa --- /dev/null +++ b/src/modemd/LICENSE @@ -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. diff --git a/src/modemd/finit.conf b/src/modemd/finit.conf new file mode 100644 index 00000000..090a5191 --- /dev/null +++ b/src/modemd/finit.conf @@ -0,0 +1 @@ +service [2345] name:modemd /sbin/modemd -- Modem daemon diff --git a/src/modemd/modem-carrier b/src/modemd/modem-carrier new file mode 100755 index 00000000..d5cc8507 --- /dev/null +++ b/src/modemd/modem-carrier @@ -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) diff --git a/src/modemd/modem-command.c b/src/modemd/modem-command.c new file mode 100644 index 00000000..96a2f7aa --- /dev/null +++ b/src/modemd/modem-command.c @@ -0,0 +1,617 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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, ""); + } else if (*p == '\n') { + len += snprintf(quoted + len, sizeof(quoted) - len, ""); + } 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 Index of modem\n"); + fprintf(stderr, " --device TTY device of modem\n"); + fprintf(stderr, " --mode Serial mode for TTY (e.g. '115200 8N1')\n"); + fprintf(stderr, " --expect String to expect\n"); + fprintf(stderr, " --timeout 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; +} diff --git a/src/modemd/modem-info b/src/modemd/modem-info new file mode 100755 index 00000000..2f1684b2 --- /dev/null +++ b/src/modemd/modem-info @@ -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() diff --git a/src/modemd/modem-power b/src/modemd/modem-power new file mode 100755 index 00000000..9bcb67a0 --- /dev/null +++ b/src/modemd/modem-power @@ -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) diff --git a/src/modemd/modem-rpc b/src/modemd/modem-rpc new file mode 100755 index 00000000..5bc7ecd7 --- /dev/null +++ b/src/modemd/modem-rpc @@ -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) diff --git a/src/modemd/modem-scan-networks b/src/modemd/modem-scan-networks new file mode 100755 index 00000000..288e7fa6 --- /dev/null +++ b/src/modemd/modem-scan-networks @@ -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) diff --git a/src/modemd/modem-sms b/src/modemd/modem-sms new file mode 100755 index 00000000..7bcb797c --- /dev/null +++ b/src/modemd/modem-sms @@ -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) diff --git a/src/modemd/modem-udev b/src/modemd/modem-udev new file mode 100755 index 00000000..ba4ebfba --- /dev/null +++ b/src/modemd/modem-udev @@ -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) diff --git a/src/modemd/modem-update-firmware b/src/modemd/modem-update-firmware new file mode 100755 index 00000000..b8675108 --- /dev/null +++ b/src/modemd/modem-update-firmware @@ -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) diff --git a/src/modemd/modem-ussd b/src/modemd/modem-ussd new file mode 100755 index 00000000..e1b1e10c --- /dev/null +++ b/src/modemd/modem-ussd @@ -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) diff --git a/src/modemd/modemd b/src/modemd/modemd new file mode 100755 index 00000000..695a41a0 --- /dev/null +++ b/src/modemd/modemd @@ -0,0 +1,1618 @@ +#!/usr/bin/env python3 + +import threading +import subprocess +import argparse +import hashlib +import signal +import syslog +import socket +import shutil +import select +import json +import enum +import time +import os +import sys + +rundir = "/run/modemd" +smsdir = "/var/sms" +debug = False +threads = [] + +syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_SYSLOG) + +class Trigger(enum.Enum): + POWER = 1 + RESET = 2 + RESTART = 3 + + +class State(enum.Enum): + FAILED = -1 + DOWN = 0 + ENABLED = 1 + CONNECTING = 2 + CONNECTED = 3 + UP = 4 + + +def info(msg): + syslog.syslog(syslog.LOG_INFO, " %s" % msg) + + +def dbg(msg): + global debug + if debug: + syslog.syslog(syslog.LOG_INFO, " %s" % msg) + + +def err(msg): + syslog.syslog(syslog.LOG_ERR, " %s" % msg) + + +def fatal(msg): + syslog.syslog(syslog.LOG_ALERT, " %s" % msg) + sys.exit(1) + + +def opener(path, flags): + ret = os.open(path, flags, 0o664) + shutil.chown(path, user="root", group="wheel") + return ret + + +def rmf(path): + if os.path.exists(path): + os.unlink(path) + + +def rmrf(path): + for f in os.listdir(path): + p = os.path.join(path, f) + if os.path.isdir(p): + rmrf(p) + else: + os.remove(p) + + +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: + fatal("Unable to mkdir %s" % path) + + +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 fwrite(path, cont): + with open(path, "w", opener=opener) as fd: + if not fd.write(cont): + return False + return True + + +def fwritej(path, cont): + if fwrite(path, json.dumps(cont)): + return True + else: + return False + + +def runcmdp(cmd1, cmd2, check=True): + dbg("Running: %s | %s" % (" ".join(cmd1), " ".join(cmd2))) + ret = None + try: + res1 = subprocess.run(cmd1, check=check, text=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + res2 = subprocess.run(cmd2, input=res1.stdout, check=check, text=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if res1.returncode == 0 and res2.returncode == 0: + if res2.stdout: + ret = res2.stdout.strip() + else: + ret = True + except subprocess.CalledProcessError: + dbg("Piped command failed") + dbg(res1.stderr) + dbg(res2.stderr) + return None + finally: + return ret + + +def runcmd(cmd, check=True): + dbg("Running: %s" % " ".join(cmd)) + ret = None + try: + res = subprocess.run(cmd, check=check, 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 as e: + dbg("Command %s failed" % cmd[0]) + dbg(e) + return None + finally: + return ret + + +def runcmdj(cmd): + output = runcmd(cmd) + 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 list_sms(modem): + smslist = [] + + output = None + if modem: + output = runcmdj(["mmcli", "-J", "-m", modem, + "--messaging-list-sms"]) + if not output: + return smslist + + for path in output["modem.messaging.sms"]: + output = runcmdj(["mmcli", "-J", "-s", path]) + if not output: + continue + props = output["sms"]["properties"] + if props["pdu-type"] == "submit": + if props["state"] == "--": + smslist.append({"path": path, + "type": "notsent"}) + elif props["state"] == "sent": + smslist.append({"path": path, + "type": "sent"}) + elif props["pdu-type"] == "deliver": + if props["state"] == "received": + smslist.append({"path": path, + "type": "received", + "payload": output["sms"]}) + + return smslist + + +class RpcThread(threading.Thread): + path = None + server = None + + def __init__(self): + global rundir + super().__init__(daemon=True, name="rpc") + self.exited = False + self.stopevent = threading.Event() + self.sock = rundir + "/modemd.sock" + + def stop(self): + self.stopevent.set() + + def stopped(self): + return self.stopevent.is_set() + + def sendsms(self, path, data): + info("Sending sms to %s" % data["number"]) + if not path: + err("No modem path yet") + return False + + opts = [] + opts.append('number="%s"' % data["number"]) + opts.append('text="%s"' % data["text"]) + + if not runcmd(["mmcli", "-m", path, + "--messaging-create-sms=%s" % ",".join(opts)]): + err("Unable to create sms") + return False + + return True + + def rpc(self, rpc, data): + global threads + + info("Got rpc %s for modem%d" % (rpc, data["index"])) + for th in threads: + if th.name.startswith("modem") and th.index == data["index"]: + if rpc == "restart": + th.trigger = Trigger.RESTART + th.interrupt = True + elif rpc == "reset": + th.trigger = Trigger.RESET + th.interrupt = True + elif rpc == "send-sms": + self.sendsms(th.path, data) + return True + + def handle(self, msg): + if "rpc" not in msg: + err("No rpc in msg") + return False + elif "data" not in msg: + err("No data in msg") + return False + elif "index" not in msg["data"]: + err("No index") + return False + + if msg["rpc"] == "send-sms": + if "number" not in msg["data"]: + err("No sms number") + return False + if "text" not in msg["data"]: + err("No sms text") + return False + + return self.rpc(msg["rpc"], msg["data"]) + + def process(self): + conn, addr = self.server.accept() + try: + while not self.stopped(): + msg = conn.recv(1024) + if msg: + self.handle(json.loads(msg.decode())) + else: + break + finally: + conn.close() + return True + + def run(self): + rmf(self.sock) + self.server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.server.bind(self.sock) + self.server.listen(1) + + while not self.stopped(): + if not self.process(): + return False + + self.server.close() + rmf(self.sock) + + dbg("Exiting rpc") + self.exited = True + return True + + +class SimThread(threading.Thread): + def __init__(self): + super().__init__(daemon=True, name="sim") + self.exited = False + self.stopevent = threading.Event() + + def stop(self): + self.stopevent.set() + + def stopped(self): + return self.stopevent.is_set() + + def changed(self, index): + global threads + + path = "/sys/class/sim/sim%d/present" % index + present = int(fread(path)) + if present is None: + return + if present == 0: + info("sim%d disappeared" % index) + return + + info("sim%d appeared" % index) + for th in threads: + if th.name.startswith("modem") and th.sim["index"] == index: + th.trigger = Trigger.POWER + th.interrupt = True + + def run(self): + fd = os.open("/dev/simctrl", os.O_RDONLY) + while not self.stopped(): + res, *_ = select.select([fd], [], [], 10.0) + if fd in res: + index = int.from_bytes(os.read(fd, 1)) + self.changed(index) + + dbg("Exiting sim") + fd.close() + self.exited = True + return True + + +class ModemThread(threading.Thread): + index = None + cfg = None + + def __init__(self, cfg): + global rundir + + self.index = cfg["index"] + super().__init__(daemon=True, name="modem%d" % self.index) + self.stopevent = threading.Event() + self.exited = False + + self.cfg = cfg + self.prefix = "[modem%d] " % self.index + self.rundir = "%s/modem%d" % (rundir, self.index) + self.statedir = "%s/state" % self.rundir + self.locdir = "%s/location" % self.rundir + + mkdir(self.rundir) + mkdir(self.statedir) + mkdir(self.locdir) + + self.init() + + def stop(self): + self.stopevent.set() + + def stopped(self): + return self.stopevent.is_set() + + def dbg(self, msg): + dbg("%s%s" % (self.prefix, msg)) + + def info(self, msg): + info("%s%s" % (self.prefix, msg)) + + def err(self, msg): + err("%s%s" % (self.prefix, msg)) + + def fatal(self, msg): + fatal("%s%s" % (self.prefix, msg)) + + def init(self): + self.timeout = 1 + self.path = None + self.interfaces = [] + self.ifaces = [] + self.status = {} + self.state = None + self.trigger = None + self.iface = None + self.sim = None + self.failtime = None + self.detected = False + self.timer1 = 0 + self.timer1 = 0 + self.timer2 = 0 + self.updfail = 0 + self.connfail = 0 + self.location = {"enabled": False} + + self.set_state(State.DOWN) + self.configure() + + def configure(self): + self.bearers = { + "list": [], + "initial": None, + "active": [] + } + bearers = self.cfg.get("bearer") + if not bearers: + self.err("No bearers configured") + return False + + default = None + count = 0 + for bearer in sorted(bearers, key=lambda b: b["index"]): + bearer["pid"] = "--" + + apn = bearer.get("apn") + if not apn: + self.err("No APN configured") + return False + + apntype = bearer.get("apn-type") + if apntype == "initial": + if not self.bearers["initial"]: + self.bearers["initial"] = bearer + elif apntype == "default" and not default: + default = bearer + count += 1 + else: + self.bearers["list"].append(bearer) + count += 1 + + if default: + self.bearers["list"].insert(0, default) + + # at least one bearer is mandatory + if len(self.bearers["list"]) == 0: + self.err("No bearers evaluated") + return False + + # check for duplicate APNs + seen = [] + for bearer in self.bearers["list"]: + if bearer["apn"] not in seen: + seen.append(bearer["apn"]) + else: + self.err("Duplicate APN '%s'" % bearer["apn"]) + return False + + if not self.bearers["initial"]: + self.bearers["initial"] = self.bearers["list"][0] + + # check location + enabled = False + sources = [] + loc = self.cfg.get("location") + if loc and loc.get("enabled"): + enabled = True + v = loc.get("source") + if v: + sources = [s.get("source") for s in v] + + self.location = { + "enabled": enabled, + "sources": sources, + "state": "down"} + + return True + + def find_bearer(self, apn): + for bearer in self.bearers["active"]: + if bearer["apn"] == apn: + return bearer + return None + + def restart(self): + self.info("Restarting") + self.notif("RESTART") + self.pulldown() + + def power(self): + self.info("Power-cycling") + self.notif("POWER") + + self.pulldown() + + if runcmd(['/usr/libexec/modemd/modem-power', + '-i', str(self.index)]): + self.info("Waiting 10s") + time.sleep(10) + else: + self.err("Unable to power-cycle") + + self.init() + + def reset(self): + self.info("Resetting") + self.notif("RESET") + + self.pulldown() + + wait = 0 + if runcmd(["mmcli", "-m", self.path, + "--reset"]): + self.info("Performed a normal reset") + wait = 5 + elif runcmd(["mmcli", "-m", self.path, + "--factory-reset=000000"]): + self.info("Performed a factory reset") + wait = 10 + else: + self.err("Unable to reset") + + if wait > 0: + self.info("Waiting %ds" % wait) + time.sleep(wait) + + self.init() + + def notif(self, msg): + data = { + "infix-modem:modems": { + "modem": [{ + "index": self.index, + "status-update": {"desc": msg} + }] + } + } + note = "%s/notif" % self.rundir + fwritej(note, data) + if runcmd(["sysrepocfg", "-f", "json", "-N", note]): + self.dbg("Sent notification: %s" % msg) + rmf(note) + + def get_bearers(self): + bearers = [] + if not self.update(): + return bearers + for path in self.status.get("bearers", []): + output = runcmdj(["mmcli", "-J", "-m", self.path, "-b", path]) + if output and "bearer" in output: + bearers.append(output["bearer"]) + return bearers + + def get_profiles(self): + profiles = [] + output = runcmdj(["mmcli", "-J", "-m", self.path, + "--3gpp-profile-manager-list"]) + if not output: + return profiles + for entry in output["modem"]["3gpp"]["profile-manager"]["list"]: + profile = {} + for attr in entry.split(", "): + (key, value) = attr.split(": ") + profile[key] = value + profiles.append(profile) + return profiles + + def get_conn_failure(self, path): + for bearer in self.get_bearers(): + if bearer["dbus-path"] == path: + return bearer["status"]["connection-error"]["message"] + return "unknown" + + def update(self): + if not self.path: + return False + output = runcmdj(["mmcli", "-J", "-m", self.path]) + if output: + output = output.get("modem", {}) + output = output.get("generic", None) + if output: + self.status = output + self.updfail = 0 + return True + + self.updfail += 1 + if self.updfail > 5: + self.err("Reinit after too many failures") + time.sleep(10) + self.init() + + return False + + def iplink(self, iface, action): + dbg("Setting link of %s to %s" % (iface, action)) + if not runcmd(["ip", "link", "set", "dev", iface, action]): + self.err("Unable to set link of %s to %s" % (iface, action)) + return False + if not runcmd(["ip", "link", "set", "dev", iface, "state", action]): + self.err("Unable to set link state of %s to %s" % (iface, action)) + return False + return True + + def resolvconf(self, iface, action): + enabled = iface["bearer"].get("dns-enabled", False) + if enabled is False: + return True + + self.info("About to %s nameservers on %s" % (action, iface["name"])) + + output = runcmd(["resolvconf", "-i"], check=False) + if output: + for ifc in output.split(" "): + self.dbg("Removing nameservers from %s" % ifc) + runcmd(["resolvconf", "-d", ifc]) + + if action != "add": + return True + + count = 0 + data = "" + for dns in (iface["ipv4"]["dns"] + iface["ipv6"]["dns"]): + self.dbg("Adding nameserver %s" % dns) + data += "nameserver %s\n" % dns + count += 1 + + if count > 0: + path = "%s/resolv.conf" % self.rundir + fwrite(path, data) + if not runcmdp(["cat", path], ["resolvconf", "-a", iface["name"]]): + self.err("Unable to add nameservers on %s" % iface["name"]) + return False + + return True + + def ipgwroute(self, iface, action): + output = runcmdj(["ip", "-j", "route", + "show", "dev", iface["name"]]) + for rt in output: + if "scope" in rt and rt["scope"] == "link": + if action == "add": + return True + else: + runcmd(["ip", "route", + "del", rt["dst"], "dev", iface["name"]]) + + if action != "add": + return True + + if iface["ipv4"]["gateway"] != "--": + gw = iface["ipv4"]["gateway"] + elif iface["ipv6"]["gateway"] != "--": + gw = iface["ipv6"]["gateway"] + else: + self.err("No gateway address found") + return False + + cmd = ["ip", "route", "add", gw, "dev", iface["name"]] + if not runcmd(cmd): + self.err("Unable to add gateway route") + return False + + return True + + def ipdefroute(self, iface, action): + self.info("About to %s default route on %s" % (action, iface["name"])) + + output = runcmdj(["ip", "-j", "route", "show", "default"]) + if output: + for route in output: + if not runcmd(["ip", "route", "del", "default", + "dev", route["dev"]]): + self.err("Can't delete default route on %s" % route["dev"]) + + if not self.ipgwroute(iface, action): + return False + + if action != "add": + return True + + cmd = ["ip", "route", "add", "default", "dev", iface["name"]] + if iface["ipv4"]["gateway"] != "--": + cmd += ["via", iface["ipv4"]["gateway"]] + elif iface["ipv6"]["gateway"] != "--": + cmd += ["via", iface["ipv6"]["gateway"]] + + if not runcmd(cmd): + self.err("Unable to add default route on %s" % iface["name"]) + time.sleep(30) + return False + else: + return True + + def ifupdown(self, iface, action): + enabled = iface["bearer"].get("firewall-enabled", False) + if enabled is True: + self.info("Setting %s firewall on %s" % (action, self.iface)) + if not runcmd(["/etc/firewall/default", action, self.iface]): + return False + + path = "/var/ifupdown/%s" % self.iface + if os.path.isfile(path) or os.access(path, os.X_OK): + self.info("Running %s %s" % (path, action)) + if not runcmd([path, action]): + return False + + return True + + def linkdown(self, iface): + self.resolvconf(iface, "delete") + self.ipdefroute(iface, "delete") + self.ifupdown(iface, "down") + + output = runcmdj(["ip", "-j", "addr", "show", iface["name"]]) + for entry in output: + for addr in entry["addr_info"]: + runcmd(["ip", "addr", "del", + addr["local"] + "/" + str(addr["prefixlen"]), + "dev", iface["name"]]) + + self.iplink(iface["name"], "down") + + for i in range(len(self.ifaces)): + if self.ifaces[i]["name"] == iface["name"]: + del self.ifaces[i] + + return True + + def linkup(self, iface): + self.iplink(iface["name"], "up") + + addrs = [] + for ip in (iface["ipv4"], iface["ipv6"]): + if ip["address"] != "--" and ip["prefix"] != "--": + addrs.append("%s/%s" % (ip["address"], ip["prefix"])) + + if len(addrs) == 0: + self.err("No addresses yet") + return False + + for addr in addrs: + self.info("Setting address %s on %s" % (addr, iface["name"])) + if not runcmd(["ip", "addr", "change", + addr, "dev", iface["name"]]): + self.err("Unable to set address for %s" % iface["name"]) + return False + + if iface["bearer"].get("default-route"): + if not self.ipdefroute(iface, "add"): + return False + + if not self.ifupdown(iface, "up"): + return False + + if not self.resolvconf(iface, "add"): + return False + + self.ifaces.append(iface) + return True + + def poweron(self): + if self.status.get("power-state", "") == "off": + self.info("Powering on") + if not runcmd(["mmcli", "-m", self.path, "--set-power-state-on"]): + self.err("Unable to power on") + return False + return True + + def disable(self): + self.info("Disabling") + if not runcmd(["mmcli", "-m", self.path, "--disable"]): + self.err("Unable to disable") + return False + else: + return True + + def enable(self): + state = self.status.get("state", "unknown") + self.info("Enabling (state '%s')" % state) + + if not self.path: + return False + if state == "unknown" or state == "disabled": + if not runcmd(["mmcli", "-m", self.path, "--enable"]): + self.err("Unable to enable") + return False + return True + + def unlock(self): + cmd = ["mmcli", "-m", self.path, "--sim=%s" % + self.status.get("sim", "0")] + + if self.status.get("unlock-required", "") == "sim-pin": + self.info("Unlocking with PIN") + pin = self.cfg.get("pin") + if not pin: + self.err("No PIN defined") + return False + cmd.append("--pin=%s" % pin) + elif self.status.get("unlock-required", "") == "sim-puk": + self.info("Unlocking with PUK") + puk = self.cfg.get("puk") + if not puk: + self.err("No PUK defined") + return False + cmd.append("--puk=%s" % puk) + else: + self.err("Unsupported lock") + return False + + self.disable() + if not runcmd(cmd): + self.err("Unable to unlock") + return False + else: + return True + + def setcarrier(self): + path = "%s/carrier-support" % self.rundir + if os.path.exists(path): + supported = int(fread(path)) + else: + if runcmd(["/usr/libexec/modemd/modem-carrier", + "--manf", self.manf, + "--model", self.model, + "--check"]): + supported = 1 + else: + supported = 0 + + fwrite(path, "%d" % supported) + + if supported == 0: + self.info("Modem does not support setting a carrier") + return True + + path = "%s/supported-carriers" % self.rundir + if os.path.exists(path): + carriers = freadj(path) + else: + self.info("Getting supported carriers for %s %s" % + (self.manf, self.model)) + carriers = runcmdj(["/usr/libexec/modemd/modem-carrier", + "--manf", self.manf, + "--model", self.model, + "--index", str(self.index), + "--list"]) + if carriers: + fwritej(path, carriers) + + if not carriers: + self.err("Unable to list supported carriers") + return False + + path = "%s/current-carrier" % self.rundir + if os.path.exists(path): + current = freadj(path) + else: + output = runcmdj(["/usr/libexec/modemd/modem-carrier", + "--manf", self.manf, + "--model", self.model, + "--index", str(self.index), + "--get"]) + if output and isinstance(output, dict): + current = output.get("name") + if current: + fwritej(path, current) + + if not current: + self.err("Unable to get current carrier") + return False + + self.info("Current carrier is '%s'" % current) + + carrier = self.cfg.get("carrier", "default") + if carrier == current: + return True + + self.info("Setting carrier '%s' for modem%d" % + (carrier, self.index)) + + output = runcmdj(["/usr/libexec/modemd/modem-carrier", + "--manf", self.manf, + "--model", self.model, + "--index", str(self.index), + "--set", carrier]) + if not output: + self.err("Unable to set carrier '%s' on modem%d" % + (carrier, self.index)) + return False + + fwritej(path, carrier) + + if output.get("action", "") == "reset": + self.reset() + return False + else: + return True + + def initialize(self): + self.pulldown() + self.update() + self.poweron() + + if not self.setcarrier(): + return False + + if self.status.get("state", "") == "locked": + if not self.unlock(): + return False + + return self.enable() + + def get_profile_args(self, bearer): + args = [ + "profile-id=%s" % bearer["pid"], + "profile-name=profile%s" % bearer["pid"] + ] + v = bearer.get("apn-type") + if v: + args.append("apn-type=%s" % v) + v = bearer.get("apn") + if v: + args.append("apn=%s" % v) + v = bearer.get("ip-type") + if v: + args.append("ip-type=%s" % v) + v = bearer.get("username") + if v: + args.append("user=%s" % v) + v = bearer.get("password") + if v: + args.append("password=%s" % v) + + return ",".join(args) + + def get_bearer_args(self, bearer): + args = [] + pid = bearer.get("pid") + if pid and pid == "!-": + args.append("profile-id=%s" % pid) + else: + v = bearer.get("apn") + if v: + args.append("apn=%s" % v) + v = bearer.get("ip-type") + if v: + args.append("ip-type=%s" % v) + v = bearer.get("username") + if v: + args.append("user=%s" % v) + v = bearer.get("password") + if v: + args.append("password=%s" % v) + + v = bearer.get("allow-roaming") + if v: + args.append("allow-roaming=true") + else: + args.append("allow-roaming=false") + + return ",".join(args) + + def prepare_profile_bearers(self, bearers): + self.info("Preparing profile bearers") + + # wipe auxiliary profiles + for p in self.get_profiles(): + if p["profile-id"] != "1": + self.dbg("Deleting profile %s" % p["profile-id"]) + if not runcmd(["mmcli", "-m", self.path, + "--3gpp-profile-manager-delete=%s" % + p["profile-id"]]): + self.err("Unable to delete profile") + return False + + # create profiles and bearers + pid = 1 + for bearer in bearers: + bearer["pid"] = str(pid) + pid += 1 + + if pid > len(self.interfaces): + self.err("Max. number of bearers reached") + break + if bearer["pid"] != "1": + if not runcmd(["mmcli", "-m", self.path, + "--3gpp-profile-manager-set="]): + self.err("Unable to create profile") + return False + + args = self.get_profile_args(bearer) + if not runcmd(["mmcli", "-m", self.path, + "--3gpp-profile-manager-set=%s" % args]): + self.err("Unable to configure profile") + return False + + args = self.get_bearer_args(bearer) + if not runcmd(["mmcli", "-m", self.path, + "--create-bearer=%s" % args]): + self.err("Unable to create bearer") + return False + + self.info("Created profile bearer %s for '%s'" % + (bearer["pid"], bearer["apn"])) + self.bearers["active"].append(bearer) + return True + + def prepare_multiplex_bearers(self, bearers): + self.info("Preparing multiplex bearers") + + for bearer in bearers: + args = self.get_bearer_args(bearer) + if not runcmd(["mmcli", "-m", self.path, + "--create-bearer=multiplex=required,%s" % args]): + self.err("Unable to create bearer") + return False + self.info("Created multiplex bearer for '%s'" % bearer["apn"]) + self.bearers["active"].append(bearer) + return True + + def prepare_default_bearer(self, bearer): + if not runcmd(["mmcli", "-m", self.path, + "--create-bearer=%s" % + self.get_bearer_args(bearer)]): + self.err("Unable to create bearer") + return False + + self.info("Created default bearer '%s'" % bearer["apn"]) + self.bearers["active"].append(bearer) + return True + + def prepare_bearers(self): + self.info("Preparing bearers") + + # wipe existing bearers + for bearer in self.get_bearers(): + self.dbg("Deleting bearer %s" % bearer["dbus-path"]) + if not runcmd(["mmcli", "-m", self.path, + "--delete-bearer=%s" % bearer["dbus-path"]]): + self.err("Unable to delete bearer") + return False + + # configure initial bearer (used to attach to network) + initial = self.bearers.get("initial") + if initial: + self.info("Setting initial bearer '%s'" % initial["apn"]) + if not runcmd(["mmcli", "-m", self.path, + "--3gpp-set-initial-eps-bearer-settings=%s" % + self.get_bearer_args(initial)]): + self.err("Unable to set initial bearer") + + # configure dialup bearers + count = len(self.bearers["list"]) + self.info("Configuring %d bearers" % count) + + if len(self.bearers["list"]) == 1: + return self.prepare_default_bearer(self.bearers["list"][0]) + elif len(self.interfaces) == 1: + # use multiplex beares in case of 1 interface + return self.prepare_multiplex_bearers(self.bearers["list"]) + else: + # use profile beaeres in case of multiple interfaces + return self.prepare_profile_bearers(self.bearers["list"]) + + def prepare_bands(self): + self.info("Preparing bands") + + v = self.cfg.get("band", []) + bands = [k.get("band", "any") for k in v] + if len(bands) == 0 or "any" in bands: + return True + + if not runcmd(["mmcli", "-m", self.path, + "--set-current-bands=%s" % "|".join(bands)]): + self.err("Unable to set band") + return False + return True + + def prepare_modes(self): + self.info("Preparing modes") + + pref = self.cfg.get("preferred-mode") + if not pref or pref == "any": + pref = "none" + + allow = [] + v = self.cfg.get("allowed-mode", []) + allow = [k.get("mode", "any") for k in v] + if "any" in allow: + allow = [] + + if pref == "none" and len(allow) == 0: + runcmd(["mmcli", "-m", self.path, "--set-allowed-modes=any"], check=False) + return True + + mode = "allowed: %s; preferred: %s" % (", ".join(allow), pref) + supported = False + for m in self.status.get("supported-modes", []): + if m == mode: + supported = True + break + if not supported: + self.err("Mode '%s' is not supported" % mode) + return False + + current = self.status.get("current-modes", "") + if current == mode: + self.info("Mode '%s' already set" % mode) + return True + + self.info("Setting mode '%s'" % mode) + + cmd = ["mmcli", "-m", self.path] + if len(allow) > 0: + cmd.append("--set-allowed-modes=%s" % "|".join(allow)) + if pref != "none": + cmd.append("--set-preferred-mode=%s" % pref) + return runcmd(cmd) + + def prepare_location(self): + self.info("Preparing location") + + rmrf(self.locdir) + if not self.location["enabled"]: + fwrite("%s/disabled" % self.locdir, "1") + return True + + for source in ["gps", "agps-msa", "agps-msb", "3gpp", "cdma"]: + if self.location["enabled"] and source in self.location["sources"]: + self.info("Enabling location source '%s'" % source) + action = "enable" + else: + self.dbg("Disabling location source '%s'" % source) + action = "disable" + + args = [] + if source == "gps": + args = ["--location-%s-gps-nmea" % action, + "--location-%s-gps-raw" % action] + elif source == "agps-msa": + args = ["--location-%s-agps-msa" % action] + elif source == "agps-msb": + args = ["--location-%s-agps-msb" % action] + elif source == "3gpp": + args = ["--location-%s-3gpp" % action] + elif source == "cdma": + args = ["--location-%s-cdma-bs" % action] + else: + continue + + if not runcmd(["mmcli", "-m", self.path] + args): + self.err("Unable to %s location source" % action) + if self.location["enabled"]: + fwrite("%s/failed" % self.locdir, "1") + self.location["state"] = "failed" + return False + return True + + def prepare(self): + if not self.detected: + return False + + self.connfail = 0 + + if not self.prepare_bands(): + self.err("Unable to prepare bands") + return False + if not self.prepare_modes(): + self.err("Unable to prepare mode") + return False + if not self.prepare_bearers(): + self.err("Unable to prepare bearers") + return False + if not self.prepare_location(): + self.err("Unable to prepare location") + return False + + self.info("Waiting for network") + return True + + def connect(self): + for bearer in self.status.get("bearers", []): + self.info("Connecting %s" % bearer) + if not runcmd(["mmcli", "-m", self.path, + "--connect", + "--bearer=%s" % bearer + ]): + self.connfail += 1 + self.err("Unable to connect (%d failures)" % self.connfail) + self.err("Reason: %s" % self.get_conn_failure(bearer)) + return False + return True + + def pullup(self): + count = 0 + for bearer in self.get_bearers(): + iface = { + "name": bearer["status"]["interface"], + "connected": bearer["status"]["connected"], + "ipv4": bearer["ipv4-config"], + "ipv6": bearer["ipv6-config"], + "bearer": self.find_bearer(bearer["properties"]["apn"]) + } + if iface["bearer"] is None: + continue + if iface["name"] == "--": + continue + if iface["connected"] != "yes": + continue + if iface["ipv4"]["address"] == "--": + if iface["ipv6"]["address"] == "--": + continue + if not self.linkup(iface): + self.linkdown(iface) + return False + else: + count += 1 + + if count == len(self.bearers["list"]): + self.info("All bearers are up") + return True + else: + return False + + def pulldown(self): + for bearer in self.get_bearers(): + iface = { + "name": bearer["status"]["interface"], + "bearer": self.find_bearer(bearer["properties"]["apn"]) + } + + if bearer["status"]["connected"] == "yes": + self.info("Disconnecting %s" % bearer["dbus-path"]) + runcmd(["mmcli", "-m", self.path, + "--disconnect", "--bearer=%s" % bearer["dbus-path"]]) + for iface in self.ifaces: + self.linkdown(iface) + + self.set_state(State.DOWN) + + def set_state(self, state): + if state == self.state: + return False + + if state == State.UP: + self.notif(state.name) + elif state == State.DOWN and self.state == State.UP: + self.notif(state.name) + elif state == State.FAILED: + self.notif(state.name) + + rmrf(self.statedir) + fwrite("%s/%s" % (self.statedir, state.name.lower()), "1") + self.state = state + return True + + def check_state(self): + self.dbg("State %s (status %s)" % + (self.state.name, self.status.get("state"))) + + if self.detected and self.status.get("state", "") == "failed": + if self.set_state(State.FAILED): + self.err("In failed state because %s" % + self.status.get("state-failed-reason", "")) + self.failtime = time.time() + + # state failed + if self.state == State.FAILED: + elapsed = time.time() - self.failtime + if elapsed > 30: + self.err("Resetting after being 30s in state failed") + self.reset() + + # state down + elif self.state == State.DOWN: + if self.status.get("state", "") == "disabled": + self.enable() + self.timeout = 3 + elif self.status.get("state", "") != "initializing": + if self.prepare(): + self.set_state(State.ENABLED) + + # state enabled + elif self.state == State.ENABLED: + if self.status.get("state", "") == "registered": + if self.state != "connecting": + if self.connect(): + self.set_state(State.CONNECTING) + elif self.connfail > 5: + self.err("Resetting after 5 connection attempts") + self.reset() + + # state connecting + elif self.state == State.CONNECTING: + if self.status.get("state", "") == "connected": + self.set_state(State.CONNECTED) + + # state connected + elif self.state == State.CONNECTED: + if self.state != State.UP: + if self.pullup(): + # all bearers are up + self.set_state(State.UP) + + # state up + elif self.state == State.UP: + self.timeout = 10 + if self.status.get("state", "") != "connected": + self.pulldown() + + + def check_trigger(self): + if self.trigger is not None: + if self.trigger == Trigger.RESTART: + self.restart() + elif self.trigger == Trigger.RESET: + self.reset() + elif self.trigger == Trigger.POWER: + self.power() + + self.trigger = None + + def check_location(self): + if not self.location["enabled"]: + return + + output = runcmdj(["mmcli", "-J", "-m", self.path, + "--location-get"]) + if output: + lat = output["modem"]["location"]["gps"]["latitude"] + lon = output["modem"]["location"]["gps"]["longitude"] + else: + lat = "--" + lon = "--" + + if lat != "--" and lon != "--": + if self.location["state"] != "up": + self.info("Retrieved GPS location [%s, %s]" % (lat, lon)) + rmf("%s/down" % self.locdir) + fwrite("%s/up" % self.locdir, "1") + self.location["state"] = "up" + else: + rmf("%s/up" % self.locdir) + fwrite("%s/down" % self.locdir, "1") + self.location["state"] = "down" + + def save_sms(self, sms): + global smsdir + + payload = sms["payload"] + + string = "%s,%s,%s" % (payload["properties"]["timestamp"], + payload["content"]["number"], + payload["content"]["text"]) + m = hashlib.sha256() + m.update(string.encode()) + path = os.path.join(smsdir, m.hexdigest()) + + if not os.path.exists(path): + self.dbg("Saving SMS to %s" % sms["path"]) + data = {"modem": self.index, "payload": payload} + fwritej(path, data) + self.notif("SMS-RECEIVED %s" % os.path.basename(path)) + + storage = payload["properties"]["storage"].upper() + if storage != "--": + if not runcmd(["mmcli", "-m", self.path, + "--messaging-delete-sms=%s" % sms["path"]]): + self.err("Unable to delete SMS from %s storage" % storage) + else: + self.info("Deleted SMS from %s storage" % storage) + + def check_sms(self): + if self.state.value < State.ENABLED.value: + return + state = self.status.get("state", "") + if state != "registered" and state != "connected": + return + + for sms in list_sms(self.path): + if sms["type"] == "notsent": + self.dbg("Sending sms %s" % sms) + runcmd(["mmcli", "-m", self.path, "-s", sms["path"], "--send"]) + + elif sms["type"] == "sent": + self.notif("SMS-SENT") + runcmd(["mmcli", "-m", self.path, + "--messaging-delete-sms=%s" % sms["path"]]) + + elif sms["type"] == "received": + self.save_sms(sms) + + def check(self): + timeout = 10 if self.state == State.UP else 3 + elapsed = time.time() - self.timer1 + if elapsed > timeout: + self.check_trigger() + if self.update(): + self.check_state() + self.timer1 = time.time() + + elapsed = time.time() - self.timer2 + if elapsed > 30: + self.check_location() + self.check_sms() + self.timer2 = time.time() + + def lookup(self): + if self.iface and self.sim: + return True + + info = runcmdj(['/usr/libexec/modemd/modem-info', + '-i', str(self.index)]) + if not info: + self.err("No modem info") + return False + + ifaces = info.get("interfaces", []) + if len(info.get("interfaces", [])) == 0: + self.err("No modem interface") + return False + else: + self.iface = ifaces[0] + + self.sim = info.get("sim", None) + if not self.sim: + self.err("No modem sim") + return False + + if self.sim["index"] < 0: + self.err("Invalid sim index") + return False + + return True + + def query(self): + if self.path: + return True + + modem = None + output = runcmdj(['/usr/libexec/modemd/modem-info']) + if output: + for m in output: + if m.get("index", -1) == self.index: + modem = m + break + if modem is None: + self.err("Modem not present") + return False + + info = modem.get("info") + if not info: + self.err("Unable to obtain modem info") + return False + + path = "%s/manufacturer" % self.rundir + self.manf = info.get("manufacturer", "") + self.dbg("Manufacturer is '%s'" % self.manf) + fwrite(path, self.manf) + + path = "%s/model" % self.rundir + self.model = info.get("model", "") + self.dbg("Model is '%s'" % self.model) + fwrite(path, self.model) + + self.path = modem.get("path") + self.dbg("Got path %s" % self.path) + return True + + def detect(self): + self.info("Detecting") + + path = "%s/detected" % self.rundir + if os.path.exists(path): + os.remove(path) + + while not self.stopped(): + if self.lookup() and self.query(): + fwrite(path, "1") + self.info("Detected %s %s" % (self.manf, self.model)) + return True + else: + time.sleep(1) + + def run(self): + while not self.stopped(): + self.dbg("state %s, status '%s' (detected %s)" % + (self.state.name, + self.status.get("state", "unknown"), + str(self.detected))) + + if not self.detected: + if self.detect() and self.initialize(): + self.detected = True + else: + time.sleep(3) + + if self.detected: + self.check() + + time.sleep(1) + + self.pulldown() + + rmrf(self.statedir) + rmrf(self.locdir) + rmrf(self.rundir) + + self.dbg("Exiting") + self.exited = True + return True + + +def sighandler(signum, frame): + global threads + + for th in threads: + th.stop() + + # wait for modem threads + timeout = 0 + while timeout < 3: + exited = True + for th in threads: + if th.name.startswith("modem") and not th.exited: + exited = False + if exited: + break + time.sleep(1) + timeout += 1 + + if signum == 15: + sys.exit(0) + else: + fatal("Exiting on signal %d" % signum) + + +if __name__ == "__main__": + syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_DAEMON) + + if os.geteuid() != 0: + fatal("Must be root") + + parser = argparse.ArgumentParser(prog="modemd") + parser.add_argument("-d", action="store_true") + args = parser.parse_args() + + if args.d: + debug = True + else: + with open("/proc/cmdline", 'r') as fd: + cmdline = fd.read() + if "debug" in cmdline: + debug = True + + if debug: + runcmd(["mmcli", "-G", "debug"]) + + mkdir(rundir) + mkdir(smsdir) + signal.signal(signal.SIGINT, sighandler) + signal.signal(signal.SIGTERM, sighandler) + + try: + th = RpcThread() + th.start() + except Exception as e: + if debug: + print(e) + fatal("Rpc thread caught an exception") + + try: + th = SimThread() + th.start() + except Exception as e: + if debug: + print(e) + fatal("SIM thread caught an exception") + + config = runcmdj(["sysrepocfg", "-f", "json", "-X", "-m", "infix-modem"]) + if not config or "infix-modem:modems" not in config: + fatal("Unable to read config") + + if not runcmd(['/usr/libexec/modemd/sim-setup']): + fatal("Unable to setup SIMs") + + for cfg in config["infix-modem:modems"]["modem"]: + if not cfg["enabled"] or cfg["index"] < 0: + continue + + th = None + try: + th = ModemThread(cfg) + th.start() + except Exception as e: + if debug: + print(e) + fatal("Modem %d thread caught an exception" % cfg["index"]) + if th: + threads.append(th) + + if len(threads) == 0: + fatal("No modem threads are running") + + [th.join() for th in threads] + + fatal("Abnormal exit") diff --git a/src/modemd/modemd.modules-load b/src/modemd/modemd.modules-load new file mode 100644 index 00000000..45427903 --- /dev/null +++ b/src/modemd/modemd.modules-load @@ -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). diff --git a/src/modemd/modemd.rules b/src/modemd/modemd.rules new file mode 100644 index 00000000..4725c057 --- /dev/null +++ b/src/modemd/modemd.rules @@ -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" diff --git a/src/modemd/qmi-wwan-ids.rules b/src/modemd/qmi-wwan-ids.rules new file mode 100644 index 00000000..902f8d8f --- /dev/null +++ b/src/modemd/qmi-wwan-ids.rules @@ -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" diff --git a/src/modemd/sim-setup b/src/modemd/sim-setup new file mode 100755 index 00000000..be400f03 --- /dev/null +++ b/src/modemd/sim-setup @@ -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) diff --git a/src/statd/python/yanger/__main__.py b/src/statd/python/yanger/__main__.py index c88d4648..92d8a182 100644 --- a/src/statd/python/yanger/__main__.py +++ b/src/statd/python/yanger/__main__.py @@ -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) diff --git a/src/statd/python/yanger/infix_modem.py b/src/statd/python/yanger/infix_modem.py new file mode 100644 index 00000000..a2b0f6d4 --- /dev/null +++ b/src/statd/python/yanger/infix_modem.py @@ -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] + } + }