mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-04 01:43:56 +08:00
🎨 Sync 2026-10-12 21:43:19
This commit is contained in:
1 parent
3a82c347ab
commit
bfe30ffbef
51 files changed
+8523
-149
No files matched your search
+2
-2
@@ -5,13 +5,13 @@ PKG_SHORT_SHA:=bc76c13
|
|||||||
PKG_COMMIT_DATE:=2026.10.11
|
PKG_COMMIT_DATE:=2026.10.11
|
||||||
|
|
||||||
PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA)
|
PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA)
|
||||||
PKG_RELEASE:=10
|
PKG_RELEASE:=11
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz
|
||||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
|
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
|
||||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
|
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
|
||||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
|
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
|
||||||
PKG_SOURCE_VERSION:=29c93e5739479dc14c0dea398465f3bc17026944
|
PKG_SOURCE_VERSION:=fac50d4bfa20e397dc765616578d92522b208e3a
|
||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA)
|
PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=ksmbd-tools
|
||||||
|
PKG_VERSION:=3.5.7
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
|
PKG_SOURCE_URL:=https://github.com/cifsd-team/ksmbd-tools/releases/download/$(PKG_VERSION)
|
||||||
|
PKG_HASH:=skip
|
||||||
|
|
||||||
|
PKG_LICENSE:=GPL-2.0-or-later
|
||||||
|
PKG_LICENSE_FILES:=COPYING
|
||||||
|
|
||||||
|
PKG_BUILD_DEPENDS:=glib2
|
||||||
|
PKG_BUILD_FLAGS:=gc-sections
|
||||||
|
|
||||||
|
include $(INCLUDE_DIR)/package.mk
|
||||||
|
include $(INCLUDE_DIR)/nls.mk
|
||||||
|
include $(INCLUDE_DIR)/meson.mk
|
||||||
|
|
||||||
|
define Package/ksmbd-tools/Default
|
||||||
|
SECTION:=net
|
||||||
|
CATEGORY:=Network
|
||||||
|
SUBMENU:=Filesystem
|
||||||
|
TITLE:=Kernel SMB
|
||||||
|
URL:=https://github.com/cifsd-team/ksmbd-tools
|
||||||
|
DEPENDS:= $(ICONV_DEPENDS) $(INTL_DEPENDS)
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/ksmbd-tools/Default/description
|
||||||
|
Userspace tools to manage the SMB kernel fileserver (ksmbd.ko).
|
||||||
|
The config file location is /etc/ksmbd/ksmbd.conf
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/ksmbd-server
|
||||||
|
$(call Package/ksmbd-tools/Default)
|
||||||
|
TITLE+= server
|
||||||
|
DEPENDS+= +(LINUX_5_4||LINUX_5_10||LINUX_6_6):kmod-fs-cifsd +!(LINUX_5_4||LINUX_5_10||LINUX_6_6):kmod-fs-ksmbd +libnl-core +libnl-genl +umdns +flock
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/ksmbd-server/description
|
||||||
|
installs: ksmbd.mountd
|
||||||
|
|
||||||
|
This provides the basic fileserver service and is the minimum needed to serve 'guest only' file shares or use a existing user database file ksmbdpwd.db.
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/ksmbd-server/config
|
||||||
|
select PACKAGE_wsdd2
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/ksmbd-avahi-service
|
||||||
|
$(call Package/ksmbd-tools/Default)
|
||||||
|
TITLE+= (Avahi service)
|
||||||
|
DEPENDS:= +avahi-daemon
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/ksmbd-avahi-service/description
|
||||||
|
installs: smb.service
|
||||||
|
|
||||||
|
This package contains the service definition for announcing the
|
||||||
|
Ksmbd (smb/445) Daemon service via mDNS/DNS-SD.
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/ksmbd-hotplug
|
||||||
|
$(call Package/ksmbd-tools/Default)
|
||||||
|
TITLE+= hotplug script for auto sharing
|
||||||
|
DEPENDS:=+blockd
|
||||||
|
endef
|
||||||
|
|
||||||
|
MESON_ARGS += \
|
||||||
|
-Db_lto=true \
|
||||||
|
-Dkrb5=disabled
|
||||||
|
|
||||||
|
TARGET_LDFLAGS += -Wl,--as-needed
|
||||||
|
|
||||||
|
define Package/ksmbd-server/install
|
||||||
|
$(INSTALL_DIR) $(1)/usr/libexec
|
||||||
|
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/libexec/ksmbd.tools $(1)/usr/libexec/
|
||||||
|
$(INSTALL_DIR) $(1)/usr/sbin
|
||||||
|
$(CP) $(PKG_INSTALL_DIR)/usr/sbin/ksmbd.addshare $(1)/usr/sbin/
|
||||||
|
$(CP) $(PKG_INSTALL_DIR)/usr/sbin/ksmbd.adduser $(1)/usr/sbin/
|
||||||
|
$(CP) $(PKG_INSTALL_DIR)/usr/sbin/ksmbd.control $(1)/usr/sbin/
|
||||||
|
$(CP) $(PKG_INSTALL_DIR)/usr/sbin/ksmbd.mountd $(1)/usr/sbin/
|
||||||
|
$(INSTALL_DIR) $(1)/etc/config $(1)/etc/ksmbd $(1)/etc/init.d
|
||||||
|
$(INSTALL_CONF) ./files/ksmbd.config $(1)/etc/config/ksmbd
|
||||||
|
$(INSTALL_DATA) ./files/ksmbd.conf.template $(1)/etc/ksmbd/
|
||||||
|
$(INSTALL_BIN) ./files/ksmbd.init $(1)/etc/init.d/ksmbd
|
||||||
|
$(INSTALL_DIR) $(1)/etc/sysctl.d
|
||||||
|
$(INSTALL_CONF) ./files/ksmbd.sysctl $(1)/etc/sysctl.d/90-ksmbd.conf
|
||||||
|
$(INSTALL_DATA) ./files/shares.sh $(1)/etc/ksmbd/
|
||||||
|
# copy examples until we have a wiki page
|
||||||
|
$(INSTALL_DATA) ./files/ksmbd.config.example $(1)/etc/ksmbd/
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/ksmbd-avahi-service/install
|
||||||
|
$(INSTALL_DIR) $(1)/etc/avahi/services
|
||||||
|
$(INSTALL_DATA) ./files/smb.service $(1)/etc/avahi/services/
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/ksmbd-hotplug/install
|
||||||
|
$(INSTALL_DIR) $(1)/etc/hotplug.d/mount
|
||||||
|
$(INSTALL_CONF) ./files/ksmbd.hotplug $(1)/etc/hotplug.d/mount/60-ksmbd
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/ksmbd-server/conffiles
|
||||||
|
/etc/config/ksmbd
|
||||||
|
/etc/ksmbd/ksmbd.conf.template
|
||||||
|
/etc/ksmbd/ksmbd.conf
|
||||||
|
/etc/ksmbd/ksmbdpwd.db
|
||||||
|
/etc/sysctl.d/90-ksmbd.conf
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/ksmbd-avahi-service/conffiles
|
||||||
|
/etc/avahi/services/smb.service
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,ksmbd-server))
|
||||||
|
$(eval $(call BuildPackage,ksmbd-avahi-service))
|
||||||
|
$(eval $(call BuildPackage,ksmbd-hotplug))
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[global]
|
||||||
|
netbios name = |NAME|
|
||||||
|
server string = |DESCRIPTION|
|
||||||
|
workgroup = |WORKGROUP|
|
||||||
|
interfaces = |INTERFACES|
|
||||||
|
bind interfaces only = yes
|
||||||
|
ipc timeout = 20
|
||||||
|
deadtime = 15
|
||||||
|
map to guest = Bad User
|
||||||
|
server max protocol = SMB3_11
|
||||||
|
smb2 max read = 1MB
|
||||||
|
smb2 max write = 1MB
|
||||||
|
smb2 max trans = 1MB
|
||||||
|
cache read buffers = no
|
||||||
|
cache trans buffers = no
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
config globals
|
||||||
|
option enabled '0'
|
||||||
|
option 'workgroup' 'WORKGROUP'
|
||||||
|
option 'description' 'Ksmbd on OpenWrt'
|
||||||
|
option interface 'lan'
|
||||||
|
option allow_legacy_protocols '1'
|
||||||
|
option allow_guest_ipc '1'
|
||||||
|
option homes '0'
|
||||||
|
option autoshare '1'
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
config globals
|
||||||
|
option 'description' 'Ksmbd on OpenWrt'
|
||||||
|
|
||||||
|
config share
|
||||||
|
option name 'testshare'
|
||||||
|
option path '/tmp'
|
||||||
|
option read_only 'no'
|
||||||
|
option guest_ok 'yes'
|
||||||
|
option create_mask '0666'
|
||||||
|
option dir_mask '0777'
|
||||||
|
option force_root '1'
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
. /lib/functions.sh
|
||||||
|
case "$ACTION" in add|remove) ;; *) exit 0;; esac
|
||||||
|
/etc/init.d/ksmbd enabled || exit 0
|
||||||
|
|
||||||
|
enabled=0
|
||||||
|
autoshare=1
|
||||||
|
ksmbd_settings() {
|
||||||
|
config_get_bool enabled "$1" enabled 0
|
||||||
|
config_get_bool autoshare "$1" autoshare 1
|
||||||
|
}
|
||||||
|
config_load ksmbd
|
||||||
|
config_foreach ksmbd_settings globals
|
||||||
|
[ "$enabled" -eq 1 ] && [ "$autoshare" -eq 1 ] || exit 0
|
||||||
|
|
||||||
|
# Synchronize automatic UCI shares and rebuild from current mounts.
|
||||||
|
/etc/init.d/ksmbd reload
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
#!/bin/sh /etc/rc.common
|
||||||
|
|
||||||
|
START=98
|
||||||
|
STOP=05
|
||||||
|
USE_PROCD=1
|
||||||
|
|
||||||
|
SMBD_IFACE=""
|
||||||
|
KSMBD_ENABLED=0
|
||||||
|
KSMBD_HOMES=0
|
||||||
|
KSMBD_AUTOSHARE=1
|
||||||
|
|
||||||
|
config_get_sane()
|
||||||
|
{
|
||||||
|
config_get "$@"
|
||||||
|
set -- "$(echo "$1" | tr -d '<>[]{};%?=#\n')"
|
||||||
|
}
|
||||||
|
|
||||||
|
smb_header()
|
||||||
|
{
|
||||||
|
config_get_bool KSMBD_ENABLED "$1" enabled 0
|
||||||
|
config_get_bool KSMBD_HOMES "$1" homes 0
|
||||||
|
config_get_bool KSMBD_AUTOSHARE "$1" autoshare 1
|
||||||
|
config_get_sane SMBD_IFACE "$1" interface "lan"
|
||||||
|
|
||||||
|
# resolve interfaces
|
||||||
|
interfaces=$(
|
||||||
|
. /lib/functions/network.sh
|
||||||
|
|
||||||
|
for net in $SMBD_IFACE; do
|
||||||
|
network_is_up "$net" || continue
|
||||||
|
network_get_device device "$net"
|
||||||
|
printf "%s " "${device:-$net}"
|
||||||
|
done
|
||||||
|
)
|
||||||
|
|
||||||
|
config_get hostname "$1" name
|
||||||
|
case "$hostname" in
|
||||||
|
''|*[!a-zA-Z0-9_.-]*)
|
||||||
|
hostname="$(sed 's/\..*//' /proc/sys/kernel/hostname | tr -cd 'a-zA-Z0-9_-')"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
config_get_sane workgroup "$1" workgroup "WORKGROUP"
|
||||||
|
config_get_sane description "$1" description "Ksmbd on OpenWrt"
|
||||||
|
config_get_bool ALLOW_LEGACY_PROTOCOLS "$1" allow_legacy_protocols 1
|
||||||
|
config_get_bool ALLOW_GUEST_IPC "$1" allow_guest_ipc 1
|
||||||
|
|
||||||
|
sed -e "s#|NAME|#$hostname#g" \
|
||||||
|
-e "s#|WORKGROUP|#$workgroup#g" \
|
||||||
|
-e "s#|DESCRIPTION|#$description#g" \
|
||||||
|
-e "s#|INTERFACES|#$interfaces#g" \
|
||||||
|
/etc/ksmbd/ksmbd.conf.template > /var/etc/ksmbd/ksmbd.conf
|
||||||
|
|
||||||
|
{
|
||||||
|
printf "\n######### Dynamic written config options #########\n"
|
||||||
|
|
||||||
|
if [ "$ALLOW_LEGACY_PROTOCOLS" -eq 1 ]; then
|
||||||
|
logger -p daemon.info -t 'ksmbd' "Legacy Protocols allowed, don't use this option for secure environments!"
|
||||||
|
printf "\tserver min protocol = NT1\n"
|
||||||
|
printf "\tserver signing = disabled\n"
|
||||||
|
fi
|
||||||
|
printf "\n[IPC$]\n\tguest ok = %s\n" "$( [ "$ALLOW_GUEST_IPC" -eq 1 ] && echo yes || echo no )"
|
||||||
|
} >> /var/etc/ksmbd/ksmbd.conf
|
||||||
|
|
||||||
|
[ -e /etc/ksmbd/ksmbd.conf ] || ln -nsf /var/etc/ksmbd/ksmbd.conf /etc/ksmbd/ksmbd.conf
|
||||||
|
|
||||||
|
if [ ! -L /etc/ksmbd/ksmbd.conf ]; then
|
||||||
|
logger -p daemon.warn -t 'ksmbd' "Local custom /etc/ksmbd/ksmbd.conf file detected, all UCI/Luci config settings are ignored!"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
smb_add_share()
|
||||||
|
{
|
||||||
|
config_get_sane name "$1" name
|
||||||
|
config_get_sane path "$1" path
|
||||||
|
config_get_sane comment "$1" comment
|
||||||
|
config_get_sane users "$1" users
|
||||||
|
config_get_sane create_mask "$1" create_mask 0666
|
||||||
|
config_get_sane dir_mask "$1" dir_mask 0777
|
||||||
|
config_get_sane browseable "$1" browseable yes
|
||||||
|
config_get_sane read_only "$1" read_only no
|
||||||
|
config_get_sane writeable "$1" writeable
|
||||||
|
config_get_sane guest_ok "$1" guest_ok yes
|
||||||
|
config_get_bool force_root "$1" force_root 1
|
||||||
|
config_get_sane write_list "$1" write_list
|
||||||
|
config_get_sane read_list "$1" read_list
|
||||||
|
config_get_sane hide_dot_files "$1" hide_dot_files yes
|
||||||
|
config_get_sane veto_files "$1" veto_files
|
||||||
|
config_get_sane inherit_owner "$1" inherit_owner
|
||||||
|
config_get_sane force_create_mode "$1" force_create_mode
|
||||||
|
config_get_sane force_directory_mode "$1" force_directory_mode
|
||||||
|
|
||||||
|
[ -z "$name" ] || [ -z "$path" ] && return
|
||||||
|
|
||||||
|
{
|
||||||
|
printf "\n[%s]\n\tpath = %s\n" "$name" "$path"
|
||||||
|
[ -n "$comment" ] && printf "\tcomment = %s\n" "$comment"
|
||||||
|
|
||||||
|
if [ "$force_root" -eq 1 ]; then
|
||||||
|
printf "\tforce user = %s\n" "root"
|
||||||
|
printf "\tforce group = %s\n" "root"
|
||||||
|
fi
|
||||||
|
[ -n "$users" ] && printf "\tvalid users = %s\n" "$users"
|
||||||
|
|
||||||
|
[ -n "$create_mask" ] && printf "\tcreate mask = %s\n" "$create_mask"
|
||||||
|
[ -n "$dir_mask" ] && printf "\tdirectory mask = %s\n" "$dir_mask"
|
||||||
|
[ -n "$force_create_mode" ] && printf "\tforce create mode = %s\n" "$force_create_mode"
|
||||||
|
[ -n "$force_directory_mode" ] && printf "\tforce directory mode = %s\n" "$force_directory_mode"
|
||||||
|
|
||||||
|
[ -n "$browseable" ] && printf "\tbrowseable = %s\n" "$browseable"
|
||||||
|
[ -n "$read_only" ] && printf "\tread only = %s\n" "$read_only"
|
||||||
|
[ -n "$writeable" ] && printf "\twriteable = %s\n" "$writeable"
|
||||||
|
|
||||||
|
[ -n "$guest_ok" ] && printf "\tguest ok = %s\n" "$guest_ok"
|
||||||
|
[ -n "$inherit_owner" ] && printf "\tinherit owner = %s\n" "$inherit_owner"
|
||||||
|
|
||||||
|
[ -n "$write_list" ] && printf "\twrite list = %s\n" "$write_list"
|
||||||
|
[ -n "$read_list" ] && printf "\tread list = %s\n" "$read_list"
|
||||||
|
|
||||||
|
[ -n "$hide_dot_files" ] && printf "\thide dot files = %s\n" "$hide_dot_files"
|
||||||
|
[ -n "$veto_files" ] && printf "\tveto files = %s\n" "$veto_files"
|
||||||
|
} >> /var/etc/ksmbd/ksmbd.conf
|
||||||
|
}
|
||||||
|
|
||||||
|
init_config()
|
||||||
|
{
|
||||||
|
flock -x 9 || return 1
|
||||||
|
mkdir -p /var/etc/ksmbd
|
||||||
|
|
||||||
|
config_load ksmbd
|
||||||
|
config_foreach smb_header globals
|
||||||
|
. /etc/ksmbd/shares.sh
|
||||||
|
ksmbd_sync_mounts || return 1
|
||||||
|
config_load ksmbd
|
||||||
|
config_foreach smb_add_share share
|
||||||
|
[ "$KSMBD_HOMES" -eq 1 ] && ksmbd_add_homes
|
||||||
|
return 0
|
||||||
|
} 9>/var/lock/ksmbd-config.lock
|
||||||
|
|
||||||
|
service_triggers()
|
||||||
|
{
|
||||||
|
# PROCD_RELOAD_DELAY=1000
|
||||||
|
|
||||||
|
procd_add_reload_trigger "dhcp" "system" "ksmbd"
|
||||||
|
|
||||||
|
for i in $SMBD_IFACE; do
|
||||||
|
procd_add_reload_interface_trigger "$i"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
kill_server()
|
||||||
|
{
|
||||||
|
if [ -e /sys/module/ksmbd ]; then
|
||||||
|
if [ -e /sys/class/ksmbd-control/kill_server ]; then
|
||||||
|
logger -p daemon.info -t 'ksmbd' "triggering kill_server"
|
||||||
|
echo hard > /sys/class/ksmbd-control/kill_server
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
start_service()
|
||||||
|
{
|
||||||
|
init_config || return 1
|
||||||
|
if [ "$KSMBD_ENABLED" -ne 1 ]; then
|
||||||
|
stop_service
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -e /etc/ksmbd/ksmbd.conf ]; then
|
||||||
|
logger -p daemon.error -t 'ksmbd' "missing config /etc/ksmbd/ksmbd.conf!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# NOTE: We don't do a soft-reload via signal, since [global] ksmbd.conf setting changes will be ignored, so always reset hard.
|
||||||
|
[ -e /sys/module/ksmbd ] && stop_service
|
||||||
|
|
||||||
|
[ ! -e /sys/module/ksmbd ] && modprobe ksmbd 2> /dev/null
|
||||||
|
if [ ! -e /sys/module/ksmbd ]; then
|
||||||
|
logger -p daemon.error -t 'ksmbd' "modprobe of ksmbd module failed, can\'t start ksmbd!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
logger -p daemon.notice -t 'ksmbd' "Starting Ksmbd userspace service."
|
||||||
|
procd_open_instance
|
||||||
|
procd_add_mdns "smb" "tcp" "445" "daemon=ksmbd"
|
||||||
|
procd_set_param command /usr/sbin/ksmbd.mountd --n
|
||||||
|
procd_set_param file /etc/ksmbd/ksmbd.conf
|
||||||
|
procd_set_param limits nofile=16384
|
||||||
|
procd_close_instance
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_service()
|
||||||
|
{
|
||||||
|
logger -p daemon.notice -t 'ksmbd' "Stopping Ksmbd userspace service."
|
||||||
|
killall ksmbd.mountd > /dev/null 2>&1
|
||||||
|
|
||||||
|
[ -e /sys/module/ksmbd ] && rmmod ksmbd > /dev/null 2>&1
|
||||||
|
# kill server if we cant rmmod
|
||||||
|
[ -e /sys/module/ksmbd ] && kill_server
|
||||||
|
# next try
|
||||||
|
[ -e /sys/module/ksmbd ] && rmmod ksmbd > /dev/null 2>&1
|
||||||
|
|
||||||
|
if [ -e /sys/module/ksmbd ]; then
|
||||||
|
logger -p daemon.error -t 'ksmbd' "module still loaded after kill_server?"
|
||||||
|
fi
|
||||||
|
[ -f /tmp/ksmbd.lock ] && rm /tmp/ksmbd.lock
|
||||||
|
}
|
||||||
|
|
||||||
|
# reload_service() {
|
||||||
|
# restart "$@"
|
||||||
|
# }
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Bound buffered-write backlog to smooth sustained uploads on flash storage.
|
||||||
|
# These VM limits apply system-wide, including writers other than ksmbd.
|
||||||
|
# /etc/sysctl.conf is loaded afterwards and can override these values.
|
||||||
|
vm.dirty_background_bytes=4194304
|
||||||
|
vm.dirty_bytes=16777216
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Home shares remain private runtime entries; mount shares are stored in UCI.
|
||||||
|
ksmbd_share_exists()
|
||||||
|
{
|
||||||
|
KSMBD_SHARE_NAME="$1" KSMBD_SHARE_PATH="$2" awk '
|
||||||
|
/^\[/ { name = substr($0, 2, length($0)-2); if (tolower(name) == tolower(ENVIRON["KSMBD_SHARE_NAME"])) found=1 }
|
||||||
|
/^[ \t]*path[ \t]*=/ { path=$0; sub(/^[ \t]*path[ \t]*=[ \t]*/, "", path); if (path == ENVIRON["KSMBD_SHARE_PATH"]) found=1 }
|
||||||
|
END { exit !found }' /var/etc/ksmbd/ksmbd.conf
|
||||||
|
}
|
||||||
|
|
||||||
|
ksmbd_add_homes()
|
||||||
|
{
|
||||||
|
local user password uid gid gecos home shell
|
||||||
|
[ -r /etc/ksmbd/ksmbdpwd.db ] || return 0
|
||||||
|
while IFS=: read -r user password uid gid gecos home shell; do
|
||||||
|
case "$user" in ''|*[!a-zA-Z0-9_.-]*) continue;; esac
|
||||||
|
case "$home" in /|/var|/tmp|/dev|/proc|/sys|'') continue;; /*) ;; *) continue;; esac
|
||||||
|
[ -d "$home" ] || continue
|
||||||
|
# Private homes are only generated for registered SMB users.
|
||||||
|
awk -F: -v user="$user" '$1 == user { found=1 } END { exit !found }' /etc/ksmbd/ksmbdpwd.db || continue
|
||||||
|
ksmbd_share_exists "$user" "$home" && continue
|
||||||
|
{
|
||||||
|
printf '\n[%s]\n\tpath = %s\n' "$user" "$home"
|
||||||
|
printf '\tcomment = Home Directories\n\tvalid users = %s\n' "$user"
|
||||||
|
printf '\tbrowseable = no\n\tguest ok = no\n\tread only = no\n'
|
||||||
|
printf '\tcreate mask = 0750\n\tdirectory mask = 0750\n'
|
||||||
|
} >> /var/etc/ksmbd/ksmbd.conf
|
||||||
|
done < /etc/passwd
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use a private delta directory so commits do not include pending LuCI edits.
|
||||||
|
ksmbd_mount_uci()
|
||||||
|
{
|
||||||
|
uci ${UCI_CONFIG_DIR:+-c "$UCI_CONFIG_DIR"} -t "$ksmbd_delta" -q "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
ksmbd_prune_mount()
|
||||||
|
{
|
||||||
|
local section="$1" path auto_path fingerprint
|
||||||
|
config_get path "$section" path
|
||||||
|
config_get auto_path "$section" auto_path
|
||||||
|
[ -n "$auto_path" ] || return 0
|
||||||
|
if [ "$path" != "$auto_path" ]; then
|
||||||
|
# Editing the path converts an automatic share into a manual share.
|
||||||
|
ksmbd_mount_uci delete "ksmbd.$section.auto_path"
|
||||||
|
elif [ "$KSMBD_AUTOSHARE" -ne 1 ] || ! grep -Fxq "$path" "$ksmbd_mounts"; then
|
||||||
|
ksmbd_mount_uci delete "ksmbd.$section"
|
||||||
|
else
|
||||||
|
# Only remove identical generated entries; differing permissions are
|
||||||
|
# intentional configuration and must not be silently discarded.
|
||||||
|
fingerprint="$(ksmbd_mount_uci show "ksmbd.$section" |
|
||||||
|
sed '1d; s/^ksmbd\.[^.]*\.//' | LC_ALL=C sort | md5sum | cut -d ' ' -f 1)"
|
||||||
|
if grep -Fxq "$fingerprint" "$ksmbd_delta/seen"; then
|
||||||
|
ksmbd_mount_uci delete "ksmbd.$section"
|
||||||
|
else
|
||||||
|
printf '%s\n' "$fingerprint" >> "$ksmbd_delta/seen"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ksmbd_find_mount()
|
||||||
|
{
|
||||||
|
local section="$1" share_name share_path
|
||||||
|
config_get share_name "$section" name
|
||||||
|
config_get share_path "$section" path
|
||||||
|
if [ "$share_path" = "$target" ] ||
|
||||||
|
[ "$(printf '%s' "$share_name" | tr 'A-Z' 'a-z')" = "$(printf '%s' "$name" | tr 'A-Z' 'a-z')" ]; then
|
||||||
|
ksmbd_found=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ksmbd_scan_mounts()
|
||||||
|
{
|
||||||
|
local device target fstype options rest name
|
||||||
|
while read -r device target fstype options rest; do
|
||||||
|
case "$device" in /dev/sd*|/dev/hd*|/dev/mmcblk*|/dev/nvme*|/dev/md*) ;; *) continue;; esac
|
||||||
|
# Firmware images and other read-only mounts are not automatic shares.
|
||||||
|
case "$fstype" in squashfs|erofs|cramfs|romfs|iso9660) continue;; esac
|
||||||
|
case ",$options," in *,ro,*) continue;; esac
|
||||||
|
target="$(printf '%b' "$target")"
|
||||||
|
case "$target" in /|/rom|/overlay|/boot|/boot/*|/tmp|/tmp/*|/dev|/dev/*|/proc|/proc/*|/sys|/sys/*) continue;; esac
|
||||||
|
case "$target" in *'
|
||||||
|
'*|*' '*) continue;; esac
|
||||||
|
[ -d "$target" ] || continue
|
||||||
|
name="${target##*/}"
|
||||||
|
case "$name" in ''|*'['*|*']'*|*';'*|*'%'*) name="${device##*/}";; esac
|
||||||
|
printf '%s\t%s\tno\n' "$target" "$name"
|
||||||
|
done < /proc/mounts
|
||||||
|
}
|
||||||
|
|
||||||
|
ksmbd_sync_mounts()
|
||||||
|
(
|
||||||
|
local ksmbd_delta ksmbd_mounts target name readonly section ksmbd_found
|
||||||
|
flock -x 8 || return 1
|
||||||
|
# The caller's snapshot can predate another hotplug worker's commit.
|
||||||
|
config_load ksmbd
|
||||||
|
ksmbd_delta="$(mktemp -d /tmp/ksmbd-uci.XXXXXX)" || return 1
|
||||||
|
trap 'rm -rf "$ksmbd_delta"' EXIT
|
||||||
|
: > "$ksmbd_delta/seen"
|
||||||
|
ksmbd_mounts="$ksmbd_delta/paths"
|
||||||
|
ksmbd_scan_mounts > "$ksmbd_delta/mounts"
|
||||||
|
cut -f1 "$ksmbd_delta/mounts" > "$ksmbd_mounts"
|
||||||
|
config_foreach ksmbd_prune_mount share
|
||||||
|
if [ -n "$(ksmbd_mount_uci changes ksmbd)" ]; then
|
||||||
|
ksmbd_mount_uci commit ksmbd || return 1
|
||||||
|
fi
|
||||||
|
[ "$KSMBD_AUTOSHARE" -eq 1 ] || return 0
|
||||||
|
config_load ksmbd
|
||||||
|
while IFS="$(printf '\t')" read -r target name readonly; do
|
||||||
|
ksmbd_found=0
|
||||||
|
config_foreach ksmbd_find_mount share
|
||||||
|
[ "$ksmbd_found" -eq 0 ] || continue
|
||||||
|
section="$(ksmbd_mount_uci add ksmbd share)" || return 1
|
||||||
|
ksmbd_mount_uci set "ksmbd.$section.name=$name"
|
||||||
|
ksmbd_mount_uci set "ksmbd.$section.path=$target"
|
||||||
|
ksmbd_mount_uci set "ksmbd.$section.auto_path=$target"
|
||||||
|
ksmbd_mount_uci set "ksmbd.$section.browseable=yes"
|
||||||
|
ksmbd_mount_uci set "ksmbd.$section.guest_ok=yes"
|
||||||
|
ksmbd_mount_uci set "ksmbd.$section.read_only=$readonly"
|
||||||
|
ksmbd_mount_uci set "ksmbd.$section.force_root=1"
|
||||||
|
ksmbd_mount_uci set "ksmbd.$section.create_mask=0666"
|
||||||
|
ksmbd_mount_uci set "ksmbd.$section.dir_mask=0777"
|
||||||
|
ksmbd_mount_uci commit ksmbd || return 1
|
||||||
|
config_load ksmbd
|
||||||
|
done < "$ksmbd_delta/mounts"
|
||||||
|
) 8>/var/lock/ksmbd-autoshare.lock
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" standalone='no'?>
|
||||||
|
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
|
||||||
|
<service-group>
|
||||||
|
<name replace-wildcards="yes">%h</name>
|
||||||
|
<service>
|
||||||
|
<type>_smb._tcp</type>
|
||||||
|
<port>445</port>
|
||||||
|
</service>
|
||||||
|
</service-group>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
Subject: [PATCH] Link glib statically for OpenWrt
|
||||||
|
|
||||||
|
Signed-off-by: coolsnowwolf <coolsnowwolf@gmail.com>
|
||||||
|
|
||||||
|
--- a/meson.build
|
||||||
|
+++ b/meson.build
|
||||||
|
@@ -21,6 +21,7 @@
|
||||||
|
glib_dep = dependency(
|
||||||
|
'glib-2.0',
|
||||||
|
version: '>= 2.58',
|
||||||
|
+ static: true,
|
||||||
|
)
|
||||||
|
libnl_dep = dependency(
|
||||||
|
'libnl-genl-3.0',
|
||||||
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-clashoo
|
PKG_NAME:=luci-app-clashoo
|
||||||
PKG_VERSION:=1.31.0
|
PKG_VERSION:=1.31.0
|
||||||
PKG_RELEASE:=7
|
PKG_RELEASE:=8
|
||||||
PKG_MAINTAINER:=kenzok8
|
PKG_MAINTAINER:=kenzok8
|
||||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,18 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import { access, popen, glob, readfile, writefile, stat, lstat, unlink, open, mkdtemp, rename, rmdir } from 'fs';
|
import { access, popen, glob, readfile, writefile, stat, lstat, unlink, open, rename, rmdir } from 'fs';
|
||||||
import { cursor } from 'uci';
|
import { cursor } from 'uci';
|
||||||
|
|
||||||
|
// 兼容 ucode < 2026-10-10:原生缺少 mkdtemp,改用 mktemp -d
|
||||||
|
function mkdtemp(tpl) {
|
||||||
|
let p = popen("mktemp -d '" + tpl + "' 2>/dev/null");
|
||||||
|
if (!p) return null;
|
||||||
|
let r = trim(p.read('all') || '');
|
||||||
|
p.close();
|
||||||
|
return r || null;
|
||||||
|
}
|
||||||
|
|
||||||
const BINARY_PATHS = [
|
const BINARY_PATHS = [
|
||||||
'/usr/bin/mihomo-stable',
|
'/usr/bin/mihomo-stable',
|
||||||
'/usr/bin/mihomo',
|
'/usr/bin/mihomo',
|
||||||
@@ -1219,6 +1228,10 @@ if (!t) t = 'template';
|
|||||||
return '_merged_' + s + '__' + t + '.yaml';
|
return '_merged_' + s + '__' + t + '.yaml';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function is_generated_yaml(path) {
|
||||||
|
return system("grep -q '^x-clashoo:' " + shell_quote(path) + " 2>/dev/null") == 0;
|
||||||
|
}
|
||||||
|
|
||||||
function template_search_dirs() {
|
function template_search_dirs() {
|
||||||
return [TEMPLATE_DIR, '/usr/share/clashoo/config/template'];
|
return [TEMPLATE_DIR, '/usr/share/clashoo/config/template'];
|
||||||
}
|
}
|
||||||
@@ -1228,7 +1241,7 @@ name = ensure_yaml_name(name || '');
|
|||||||
if (!name) return '';
|
if (!name) return '';
|
||||||
for (let dir in template_search_dirs()) {
|
for (let dir in template_search_dirs()) {
|
||||||
let p = dir + '/' + name;
|
let p = dir + '/' + name;
|
||||||
if (access(p, 'r')) return p;
|
if (access(p, 'r') && !is_generated_yaml(p)) return p;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -2907,6 +2920,7 @@ for (let dir in template_search_dirs()) {
|
|||||||
for (let f in (glob(dir + '/*.yaml') || [])) {
|
for (let f in (glob(dir + '/*.yaml') || [])) {
|
||||||
let name = replace(f, dir + '/', '');
|
let name = replace(f, dir + '/', '');
|
||||||
if (index(name, '_merged_') == 0 || seen[name]) continue;
|
if (index(name, '_merged_') == 0 || seen[name]) continue;
|
||||||
|
if (is_generated_yaml(f)) continue;
|
||||||
seen[name] = 1;
|
seen[name] = 1;
|
||||||
let fi = stat(f);
|
let fi = stat(f);
|
||||||
push(files, { name, mtime: fi ? ('' + fi.mtime) : null, size: fi ? (int(fi.size / 1024) + ' KB') : null });
|
push(files, { name, mtime: fi ? ('' + fi.mtime) : null, size: fi ? (int(fi.size / 1024) + ' KB') : null });
|
||||||
@@ -2914,6 +2928,7 @@ push(files, { name, mtime: fi ? ('' + fi.mtime) : null, size: fi ? (int(fi.size
|
|||||||
for (let f in (glob(dir + '/*.yml') || [])) {
|
for (let f in (glob(dir + '/*.yml') || [])) {
|
||||||
let name = replace(f, dir + '/', '');
|
let name = replace(f, dir + '/', '');
|
||||||
if (index(name, '_merged_') == 0 || seen[name]) continue;
|
if (index(name, '_merged_') == 0 || seen[name]) continue;
|
||||||
|
if (is_generated_yaml(f)) continue;
|
||||||
seen[name] = 1;
|
seen[name] = 1;
|
||||||
let fi = stat(f);
|
let fi = stat(f);
|
||||||
push(files, { name, mtime: fi ? ('' + fi.mtime) : null, size: fi ? (int(fi.size / 1024) + ' KB') : null });
|
push(files, { name, mtime: fi ? ('' + fi.mtime) : null, size: fi ? (int(fi.size / 1024) + ' KB') : null });
|
||||||
@@ -3477,13 +3492,22 @@ if (!tpl_src)
|
|||||||
return { success: false, error: 'invalid_template', message: '模板来源不能为空' };
|
return { success: false, error: 'invalid_template', message: '模板来源不能为空' };
|
||||||
|
|
||||||
let pairs = [];
|
let pairs = [];
|
||||||
|
let names = {};
|
||||||
try {
|
try {
|
||||||
let parsed = json(pairs_raw || '[]');
|
let parsed = json(pairs_raw || '[]');
|
||||||
if (type(parsed) == 'array') {
|
if (type(parsed) != 'array')
|
||||||
|
return { success: false, error: 'invalid_pairs', message: '订阅参数格式错误' };
|
||||||
for (let p in parsed) {
|
for (let p in parsed) {
|
||||||
if (p && p.name && p.url && match(p.url, /^https?:\/\//))
|
if (!p || type(p.name) != 'string' || type(p.url) != 'string')
|
||||||
push(pairs, { name: trim(p.name), url: trim(p.url) });
|
return { success: false, error: 'invalid_pairs', message: '订阅名称和链接不能为空' };
|
||||||
}
|
let name = trim(p.name);
|
||||||
|
let url = trim(p.url);
|
||||||
|
if (!name || !match(url, /^https?:\/\//))
|
||||||
|
return { success: false, error: 'invalid_pairs', message: '订阅名称不能为空,链接必须以 http:// 或 https:// 开头' };
|
||||||
|
if (names[name])
|
||||||
|
return { success: false, error: 'duplicate_provider_name', message: '订阅名称不能重复' };
|
||||||
|
names[name] = 1;
|
||||||
|
push(pairs, { name, url });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { success: false, error: 'invalid_pairs', message: '订阅参数格式错误' };
|
return { success: false, error: 'invalid_pairs', message: '订阅参数格式错误' };
|
||||||
@@ -3507,11 +3531,7 @@ if (system(dl_cmd) != 0 || !access(tmp_tpl, 'r'))
|
|||||||
return { success: false, error: 'download_failed', message: '远程模板下载失败' };
|
return { success: false, error: 'download_failed', message: '远程模板下载失败' };
|
||||||
} else {
|
} else {
|
||||||
let safe = safe_name(tpl_src);
|
let safe = safe_name(tpl_src);
|
||||||
let local_path = null;
|
let local_path = find_template_path(safe);
|
||||||
let search_dirs = template_search_dirs();
|
|
||||||
for (let i = 0; i < length(search_dirs); i++) {
|
|
||||||
if (access(search_dirs[i] + '/' + safe, 'r')) { local_path = search_dirs[i] + '/' + safe; break; }
|
|
||||||
}
|
|
||||||
if (!local_path)
|
if (!local_path)
|
||||||
return { success: false, error: 'template_not_found', message: '本地模板文件不存在' };
|
return { success: false, error: 'template_not_found', message: '本地模板文件不存在' };
|
||||||
if (system('cp -f ' + shell_quote(local_path) + ' ' + shell_quote(tmp_tpl)) != 0)
|
if (system('cp -f ' + shell_quote(local_path) + ' ' + shell_quote(tmp_tpl)) != 0)
|
||||||
@@ -3528,17 +3548,27 @@ return { success: false, error: 'missing_yq', message: '缺少 yq,无法注入
|
|||||||
system("cp -f " + shell_quote(tmp_tpl) + " " + shell_quote(exploded));
|
system("cp -f " + shell_quote(tmp_tpl) + " " + shell_quote(exploded));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 模板必须有 proxy-providers 段 */
|
||||||
|
if (system("yq -e 'has(\"proxy-providers\")' " + shell_quote(exploded) + " >/dev/null 2>&1") != 0) {
|
||||||
|
system("rm -f " + shell_quote(exploded));
|
||||||
|
return { success: false, error: 'no_proxy_providers', message: '模板缺少 proxy-providers 段' };
|
||||||
|
}
|
||||||
|
if (system("yq -e '(.\"proxy-providers\" | tag) == \"!!map\" and (.\"proxy-providers\" | length) == 1 and (.\"proxy-providers\" | to_entries[0].value | tag) == \"!!map\" and (.\"proxy-providers\" | to_entries[0].value.type) == \"http\"' " + shell_quote(exploded) + " >/dev/null 2>&1") != 0) {
|
||||||
|
system("rm -f " + shell_quote(exploded));
|
||||||
|
return { success: false, error: 'invalid_proxy_providers', message: '面板模板只支持一个 type: http 的样板 provider;多 provider 配置请直接上传使用' };
|
||||||
|
}
|
||||||
|
|
||||||
/* pairs 写到临时 JSON 文件 */
|
/* pairs 写到临时 JSON 文件 */
|
||||||
writefile(tmp_pairs, sprintf('%.J', pairs));
|
writefile(tmp_pairs, sprintf('%.J', pairs));
|
||||||
|
|
||||||
let tpl_label = match(tpl_src, /^https?:\/\//) ? tpl_src : replace(tpl_src, /^.*\//, "");
|
let tpl_label = match(tpl_src, /^https?:\/\//) ? tpl_src : replace(tpl_src, /^.*\//, "");
|
||||||
|
let tpl_expr = length(pairs) > 1 ? '($entry.value | del(.path))' : '$entry.value';
|
||||||
|
|
||||||
let yq_expr = '(load("' + tmp_pairs + '")) as $pairs | '
|
let yq_expr = '(load("' + tmp_pairs + '")) as $pairs | '
|
||||||
+ '(.["proxy-providers"] | keys) as $oldkeys | '
|
+ '(.["proxy-providers"] | to_entries[0]) as $entry | '
|
||||||
+ '($oldkeys | to_entries | map(.key as $i | .value as $k | {"key": $k, "value": ($pairs[$i].name // $k)}) | from_entries) as $ren | '
|
+ tpl_expr + ' as $tpl | '
|
||||||
+ '($oldkeys | to_entries | map(select(.key < ($pairs | length)) | .key as $i | .value as $k | {"key": $k, "value": $pairs[$i].url}) | from_entries) as $reurl | '
|
+ '.["proxy-providers"] = ($pairs | map({"key": .name, "value": ($tpl + {"url": .url})}) | from_entries) | '
|
||||||
+ '.["proxy-providers"] |= with_entries(.key as $old | .key = ($ren[$old] // $old) | .value.url = ($reurl[$old] // .value.url)) | '
|
+ '(.["proxy-groups"][] | select(has("use")) | .use) |= (map(. as $name | ([$pairs[].name] | select($name == $entry.key)) // [$name]) | flatten) | '
|
||||||
+ '(.["proxy-groups"][] | select(has("use")) | .use) |= map($ren[.] // .) | '
|
|
||||||
+ '."x-clashoo" = {"template": strenv(TPL_LABEL)}';
|
+ '."x-clashoo" = {"template": strenv(TPL_LABEL)}';
|
||||||
|
|
||||||
let inject_cmd = "TPL_LABEL=" + shell_quote(tpl_label)
|
let inject_cmd = "TPL_LABEL=" + shell_quote(tpl_label)
|
||||||
@@ -3584,7 +3614,7 @@ system('rm -f ' + shell_quote(tmp_tpl));
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_core_bin) {
|
if (_core_bin) {
|
||||||
let _ret = system(_core_bin + " -t -d /etc/clashoo -f " + shell_quote(out_path) + " 2>/tmp/clashoo_tpl_verify.txt");
|
let _ret = system(_core_bin + " -t -d /etc/clashoo -f " + shell_quote(out_path) + " >/dev/null 2>/tmp/clashoo_tpl_verify.txt");
|
||||||
if (_ret != 0) {
|
if (_ret != 0) {
|
||||||
let _err = trim(readfile('/tmp/clashoo_tpl_verify.txt') || '配置校验失败');
|
let _err = trim(readfile('/tmp/clashoo_tpl_verify.txt') || '配置校验失败');
|
||||||
system('rm -f ' + shell_quote(out_path) + ' /tmp/clashoo_tpl_verify.txt 2>/dev/null');
|
system('rm -f ' + shell_quote(out_path) + ' /tmp/clashoo_tpl_verify.txt 2>/dev/null');
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-diskman
|
PKG_NAME:=luci-app-diskman
|
||||||
PKG_VERSION:=0.2.13
|
PKG_VERSION:=0.2.13
|
||||||
PKG_RELEASE:=2
|
PKG_RELEASE:=3
|
||||||
|
|
||||||
PKG_MAINTAINER:=lisaac <lisaac.cn@gmail.com>
|
PKG_MAINTAINER:=lisaac <lisaac.cn@gmail.com>
|
||||||
PKG_LICENSE:=AGPL-3.0
|
PKG_LICENSE:=AGPL-3.0
|
||||||
@@ -24,7 +24,7 @@ define Package/luci-app-diskman/config
|
|||||||
config PACKAGE_$(PKG_NAME)_INCLUDE_btrfs_progs
|
config PACKAGE_$(PKG_NAME)_INCLUDE_btrfs_progs
|
||||||
depends on PACKAGE_$(PKG_NAME)
|
depends on PACKAGE_$(PKG_NAME)
|
||||||
bool "Include btrfs-progs"
|
bool "Include btrfs-progs"
|
||||||
default y
|
default n
|
||||||
|
|
||||||
config PACKAGE_$(PKG_NAME)_INCLUDE_lsblk
|
config PACKAGE_$(PKG_NAME)_INCLUDE_lsblk
|
||||||
depends on PACKAGE_$(PKG_NAME)
|
depends on PACKAGE_$(PKG_NAME)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ d:option(DummyValue, "temp", translate("Temp"))
|
|||||||
d:option(DummyValue, "p_table", translate("Partition Table"))
|
d:option(DummyValue, "p_table", translate("Partition Table"))
|
||||||
d:option(DummyValue, "sata_ver", translate("SATA Version"))
|
d:option(DummyValue, "sata_ver", translate("SATA Version"))
|
||||||
-- d:option(DummyValue, "rota_rate", translate("Rotation Rate"))
|
-- d:option(DummyValue, "rota_rate", translate("Rotation Rate"))
|
||||||
d:option(DummyValue, "health_status", translate("Health") .. "<br/>" .. translate("Status"))
|
d:option(DummyValue, "health_status", translate("Health") .. " / " .. translate("Status"))
|
||||||
-- d:option(DummyValue, "status", translate("Status"))
|
-- d:option(DummyValue, "status", translate("Status"))
|
||||||
|
|
||||||
-- local btn_eject = d:option(Button, "_eject")
|
-- local btn_eject = d:option(Button, "_eject")
|
||||||
|
|||||||
@@ -25,6 +25,25 @@ m.reset = false
|
|||||||
local disk_info = dm.get_disk_info(dev, true)
|
local disk_info = dm.get_disk_info(dev, true)
|
||||||
local format_cmd = dm.get_format_cmd()
|
local format_cmd = dm.get_format_cmd()
|
||||||
|
|
||||||
|
local function is_firmware_partition(part)
|
||||||
|
local name = part.partition_name or ""
|
||||||
|
return name:match("^0:") or name:find("rootfs", 1, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
local first_partition_start
|
||||||
|
for _, part in ipairs(disk_info.partitions) do
|
||||||
|
local start = tonumber(part.sec_start)
|
||||||
|
if part.number > 0 and start then
|
||||||
|
first_partition_start = math.min(first_partition_start or start, start)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_leading_free_space(part)
|
||||||
|
-- Bootloader data may live in the gap before the first real partition.
|
||||||
|
return part.number == -1 and first_partition_start and
|
||||||
|
tonumber(part.sec_end) < first_partition_start
|
||||||
|
end
|
||||||
|
|
||||||
s = m:section(Table, {disk_info}, translate("Device Info"))
|
s = m:section(Table, {disk_info}, translate("Device Info"))
|
||||||
-- s:option(DummyValue, "key")
|
-- s:option(DummyValue, "key")
|
||||||
-- s:option(DummyValue, "value")
|
-- s:option(DummyValue, "value")
|
||||||
@@ -118,14 +137,15 @@ end
|
|||||||
|
|
||||||
-- partitions info
|
-- partitions info
|
||||||
if not disk_info.p_table:match("Raid") then
|
if not disk_info.p_table:match("Raid") then
|
||||||
s_partition_table = m:section(Table, disk_info.partitions, translate("Partitions Info"), translate("Default 2048 sector alignment, support +size{b,k,m,g,t} in End Sector"))
|
s_partition_table = m:section(Table, disk_info.partitions, translate("Partitions Info"), translate("Default 1 MiB alignment, support +size{b,k,m,g,t} in End Sector"))
|
||||||
|
|
||||||
-- s_partition_table:option(DummyValue, "number", translate("Number"))
|
-- s_partition_table:option(DummyValue, "number", translate("Number"))
|
||||||
s_partition_table:option(DummyValue, "name", translate("Name"))
|
s_partition_table:option(DummyValue, "name", translate("Name"))
|
||||||
|
s_partition_table:option(DummyValue, "partition_name", translate("Partition Name"))
|
||||||
local val_sec_start = s_partition_table:option(Value, "sec_start", translate("Start Sector"))
|
local val_sec_start = s_partition_table:option(Value, "sec_start", translate("Start Sector"))
|
||||||
val_sec_start.render = function(self, section, scope)
|
val_sec_start.render = function(self, section, scope)
|
||||||
-- could create new partition
|
-- could create new partition
|
||||||
if disk_info.partitions[section].number == -1 and disk_info.partitions[section].size > 1 * 1024 * 1024 then
|
if disk_info.partitions[section].number == -1 and not is_leading_free_space(disk_info.partitions[section]) and disk_info.partitions[section].size > 1 * 1024 * 1024 then
|
||||||
self.template = "cbi/value"
|
self.template = "cbi/value"
|
||||||
Value.render(self, section, scope)
|
Value.render(self, section, scope)
|
||||||
else
|
else
|
||||||
@@ -136,7 +156,7 @@ if not disk_info.p_table:match("Raid") then
|
|||||||
local val_sec_end = s_partition_table:option(Value, "sec_end", translate("End Sector"))
|
local val_sec_end = s_partition_table:option(Value, "sec_end", translate("End Sector"))
|
||||||
val_sec_end.render = function(self, section, scope)
|
val_sec_end.render = function(self, section, scope)
|
||||||
-- could create new partition
|
-- could create new partition
|
||||||
if disk_info.partitions[section].number == -1 and disk_info.partitions[section].size > 1 * 1024 * 1024 then
|
if disk_info.partitions[section].number == -1 and not is_leading_free_space(disk_info.partitions[section]) and disk_info.partitions[section].size > 1 * 1024 * 1024 then
|
||||||
self.template = "cbi/value"
|
self.template = "cbi/value"
|
||||||
Value.render(self, section, scope)
|
Value.render(self, section, scope)
|
||||||
else
|
else
|
||||||
@@ -192,8 +212,9 @@ if not disk_info.p_table:match("Raid") then
|
|||||||
disk_info.partitions[section]._fs = value
|
disk_info.partitions[section]._fs = value
|
||||||
end
|
end
|
||||||
val_fs.render = function(self, section, scope)
|
val_fs.render = function(self, section, scope)
|
||||||
-- use listvalue when partition not mounted
|
local part = disk_info.partitions[section]
|
||||||
if disk_info.partitions[section].mount_point == "-" and disk_info.partitions[section].number ~= -1 and disk_info.partitions[section].type ~= "extended" then
|
-- Keep firmware partitions read-only in the filesystem column.
|
||||||
|
if not is_firmware_partition(part) and part.mount_point == "-" and part.number ~= -1 and part.type ~= "extended" then
|
||||||
self.template = "diskman/cbi/format_button"
|
self.template = "diskman/cbi/format_button"
|
||||||
self.inputstyle = "reset"
|
self.inputstyle = "reset"
|
||||||
self.inputtitle = disk_info.partitions[section].fs == "raw" and translate("Format") or disk_info.partitions[section].fs
|
self.inputtitle = disk_info.partitions[section].fs == "raw" and translate("Format") or disk_info.partitions[section].fs
|
||||||
@@ -261,6 +282,12 @@ if not disk_info.p_table:match("Raid") then
|
|||||||
btn_action.forcewrite = true
|
btn_action.forcewrite = true
|
||||||
btn_action.template = "diskman/cbi/disabled_button"
|
btn_action.template = "diskman/cbi/disabled_button"
|
||||||
btn_action.render = function(self, section, scope)
|
btn_action.render = function(self, section, scope)
|
||||||
|
if is_firmware_partition(disk_info.partitions[section]) or is_leading_free_space(disk_info.partitions[section]) then
|
||||||
|
self.template = "cbi/dvalue"
|
||||||
|
DummyValue.render(self, section, scope)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self.template = "diskman/cbi/disabled_button"
|
||||||
-- if partition is mounted or the size < 1mb, then disable the add action
|
-- if partition is mounted or the size < 1mb, then disable the add action
|
||||||
if disk_info.partitions[section].mount_point ~= "-" or (disk_info.partitions[section].type ~= "extended" and disk_info.partitions[section].number == -1 and disk_info.partitions[section].size <= 1 * 1024 * 1024) then
|
if disk_info.partitions[section].mount_point ~= "-" or (disk_info.partitions[section].type ~= "extended" and disk_info.partitions[section].number == -1 and disk_info.partitions[section].size <= 1 * 1024 * 1024) then
|
||||||
self.view_disabled = true
|
self.view_disabled = true
|
||||||
@@ -282,16 +309,17 @@ if not disk_info.p_table:match("Raid") then
|
|||||||
Button.render(self, section, scope)
|
Button.render(self, section, scope)
|
||||||
end
|
end
|
||||||
btn_action.write = function(self, section, value)
|
btn_action.write = function(self, section, value)
|
||||||
|
if is_firmware_partition(disk_info.partitions[section]) or is_leading_free_space(disk_info.partitions[section]) then return end
|
||||||
if value == translate("New") then
|
if value == translate("New") then
|
||||||
local start_sec = disk_info.partitions[section]._sec_start and tonumber(disk_info.partitions[section]._sec_start) or tonumber(disk_info.partitions[section].sec_start)
|
local start_sec = disk_info.partitions[section]._sec_start and tonumber(disk_info.partitions[section]._sec_start) or tonumber(disk_info.partitions[section].sec_start)
|
||||||
local end_sec = disk_info.partitions[section]._sec_end
|
local end_sec = disk_info.partitions[section]._sec_end
|
||||||
|
local logic_sec = tonumber(disk_info.logic_sec)
|
||||||
|
local align = math.max(math.ceil(tonumber(disk_info.phy_sec) / logic_sec), math.ceil(1048576 / logic_sec))
|
||||||
|
|
||||||
if start_sec then
|
if start_sec then
|
||||||
-- for sector alignment
|
-- for sector alignment
|
||||||
local align = tonumber(disk_info.phy_sec) / tonumber(disk_info.logic_sec)
|
if start_sec < align then
|
||||||
align = (align < 2048) and 2048
|
start_sec = tostring(align) .. "s"
|
||||||
if start_sec < 2048 then
|
|
||||||
start_sec = "2048" .. "s"
|
|
||||||
elseif math.fmod( start_sec, align ) ~= 0 then
|
elseif math.fmod( start_sec, align ) ~= 0 then
|
||||||
start_sec = tostring(start_sec + align - math.fmod( start_sec, align )) .. "s"
|
start_sec = tostring(start_sec + align - math.fmod( start_sec, align )) .. "s"
|
||||||
else
|
else
|
||||||
@@ -306,14 +334,14 @@ if not disk_info.p_table:match("Raid") then
|
|||||||
if tonumber(end_size) and end_unit then
|
if tonumber(end_size) and end_unit then
|
||||||
local unit ={
|
local unit ={
|
||||||
B=1,
|
B=1,
|
||||||
S=512,
|
S=tonumber(disk_info.logic_sec),
|
||||||
K=1024,
|
K=1024,
|
||||||
M=1048576,
|
M=1048576,
|
||||||
G=1073741824,
|
G=1073741824,
|
||||||
T=1099511627776
|
T=1099511627776
|
||||||
}
|
}
|
||||||
end_unit = end_unit:upper()
|
end_unit = end_unit:upper()
|
||||||
end_sec = tostring(tonumber(end_size) * unit[end_unit] / unit["S"] + tonumber(start_sec:sub(1,-2)) - 1 ) .. "s"
|
end_sec = tostring(math.ceil(tonumber(end_size) * unit[end_unit] / logic_sec) + tonumber(start_sec:sub(1,-2)) - 1) .. "s"
|
||||||
elseif tonumber(end_sec) then
|
elseif tonumber(end_sec) then
|
||||||
end_sec = end_sec .. "s"
|
end_sec = end_sec .. "s"
|
||||||
else
|
else
|
||||||
@@ -325,8 +353,8 @@ if not disk_info.p_table:match("Raid") then
|
|||||||
if disk_info.p_table == "MBR" and disk_info["extended_partition_index"] then
|
if disk_info.p_table == "MBR" and disk_info["extended_partition_index"] then
|
||||||
if tonumber(disk_info.partitions[disk_info["extended_partition_index"]].sec_start) <= tonumber(start_sec:sub(1,-2)) and tonumber(disk_info.partitions[disk_info["extended_partition_index"]].sec_end) >= tonumber(end_sec:sub(1,-2)) then
|
if tonumber(disk_info.partitions[disk_info["extended_partition_index"]].sec_start) <= tonumber(start_sec:sub(1,-2)) and tonumber(disk_info.partitions[disk_info["extended_partition_index"]].sec_end) >= tonumber(end_sec:sub(1,-2)) then
|
||||||
part_type = "logical"
|
part_type = "logical"
|
||||||
if tonumber(start_sec:sub(1,-2)) - tonumber(disk_info.partitions[section].sec_start) < 2048 then
|
if tonumber(start_sec:sub(1,-2)) - tonumber(disk_info.partitions[section].sec_start) < align then
|
||||||
start_sec = tonumber(start_sec:sub(1,-2)) + 2048
|
start_sec = tonumber(start_sec:sub(1,-2)) + align
|
||||||
start_sec = start_sec .."s"
|
start_sec = start_sec .."s"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -363,4 +391,4 @@ if not disk_info.p_table:match("Raid") then
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return m
|
return m
|
||||||
@@ -9,6 +9,7 @@ local ver = require "luci.version"
|
|||||||
local CMD = {"parted", "mdadm", "blkid", "smartctl", "df", "btrfs", "lsblk"}
|
local CMD = {"parted", "mdadm", "blkid", "smartctl", "df", "btrfs", "lsblk"}
|
||||||
|
|
||||||
local d = {command ={}}
|
local d = {command ={}}
|
||||||
|
local SYSFS_SECTOR_SIZE = 512
|
||||||
for _, cmd in ipairs(CMD) do
|
for _, cmd in ipairs(CMD) do
|
||||||
local command = luci.sys.exec("/usr/bin/which " .. cmd)
|
local command = luci.sys.exec("/usr/bin/which " .. cmd)
|
||||||
d.command[cmd] = command:match("^.+"..cmd) or nil
|
d.command[cmd] = command:match("^.+"..cmd) or nil
|
||||||
@@ -33,7 +34,42 @@ function byte_format(byte)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function get_emmc_health(device)
|
||||||
|
if not device:match("^mmcblk%d+$") then return end
|
||||||
|
local path = "/sys/class/block/" .. device .. "/device/"
|
||||||
|
if not (nixio.fs.readfile(path .. "type") or ""):match("^MMC%s*$") then return end
|
||||||
|
local translate = require("luci.i18n").translate
|
||||||
|
local info = {}
|
||||||
|
local lifetime
|
||||||
|
-- Use the more worn of the supported A/B estimates; zero means undefined.
|
||||||
|
for value in (nixio.fs.readfile(path .. "life_time") or ""):gmatch("%S+") do
|
||||||
|
local estimate = tonumber(value)
|
||||||
|
if estimate and estimate >= 1 and estimate <= 11 then
|
||||||
|
lifetime = math.max(lifetime or 0, estimate)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if lifetime then
|
||||||
|
if lifetime == 11 then
|
||||||
|
info.health = translate("Estimated lifetime exceeded")
|
||||||
|
else
|
||||||
|
-- Report estimated remaining life as a range, not an exact percentage.
|
||||||
|
info.health = string.format("%d-%d%%", 100 - lifetime * 10, 110 - lifetime * 10)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local eol = tonumber(nixio.fs.readfile(path .. "pre_eol_info") or "")
|
||||||
|
if eol == 1 then
|
||||||
|
info.status = translate("Normal")
|
||||||
|
elseif eol == 2 then
|
||||||
|
info.status = translate("Warning")
|
||||||
|
elseif eol == 3 then
|
||||||
|
info.status = translate("Urgent")
|
||||||
|
end
|
||||||
|
return info
|
||||||
|
end
|
||||||
|
|
||||||
local get_smart_info = function(device)
|
local get_smart_info = function(device)
|
||||||
|
local emmc_info = get_emmc_health(device)
|
||||||
|
if emmc_info then return emmc_info end
|
||||||
local section
|
local section
|
||||||
local smart_info = {}
|
local smart_info = {}
|
||||||
for _, line in ipairs(luci.util.execl(d.command.smartctl .. " -H -A -i -n standby -f brief /dev/" .. device)) do
|
for _, line in ipairs(luci.util.execl(d.command.smartctl .. " -H -A -i -n standby -f brief /dev/" .. device)) do
|
||||||
@@ -188,6 +224,10 @@ local get_parted_info = function(device)
|
|||||||
elseif device:match("mmcblk") or device:match("md") or device:match("nvme") then
|
elseif device:match("mmcblk") or device:match("md") or device:match("nvme") then
|
||||||
partition_temp["name"] = device.."p"..partition_temp["number"]
|
partition_temp["name"] = device.."p"..partition_temp["number"]
|
||||||
end
|
end
|
||||||
|
-- Read the kernel partition name: older parted output does not escape colons.
|
||||||
|
local uevent = partition_temp["name"] ~= "-" and
|
||||||
|
nixio.fs.readfile("/sys/class/block/" .. partition_temp["name"] .. "/uevent") or ""
|
||||||
|
partition_temp["partition_name"] = (uevent or ""):match("\nPARTNAME=([^\n]+)") or "-"
|
||||||
if partition_temp["number"] > 0 and partition_temp["fs"] == "" and d.command.lsblk then
|
if partition_temp["number"] > 0 and partition_temp["fs"] == "" and d.command.lsblk then
|
||||||
partition_temp["fs"] = (luci.util.exec(d.command.lsblk .. " /dev/" .. partition_temp["name"] .. " -no fstype") or ""):match("([^%s]+)") or ""
|
partition_temp["fs"] = (luci.util.exec(d.command.lsblk .. " /dev/" .. partition_temp["name"] .. " -no fstype") or ""):match("([^%s]+)") or ""
|
||||||
end
|
end
|
||||||
@@ -230,7 +270,7 @@ local get_parted_info = function(device)
|
|||||||
elseif (p["number"] <= 4) and (p["number"] > 0) then
|
elseif (p["number"] <= 4) and (p["number"] > 0) then
|
||||||
local s = nixio.fs.readfile("/sys/block/"..device.."/"..p["name"].."/size")
|
local s = nixio.fs.readfile("/sys/block/"..device.."/"..p["name"].."/size")
|
||||||
if s then
|
if s then
|
||||||
local real_size_sec = tonumber(s) * tonumber(disk_temp.logic_sec)
|
local real_size_sec = tonumber(s) * SYSFS_SECTOR_SIZE
|
||||||
-- if size not equal, it's an extended
|
-- if size not equal, it's an extended
|
||||||
if real_size_sec ~= p["size"] then
|
if real_size_sec ~= p["size"] then
|
||||||
disk_temp["extended_partition_index"] = i
|
disk_temp["extended_partition_index"] = i
|
||||||
@@ -313,7 +353,7 @@ d.get_disk_info = function(device, wakeup)
|
|||||||
-- if only weakup == true, weakup the disk and check parted_info.
|
-- if only weakup == true, weakup the disk and check parted_info.
|
||||||
if smart_info.status ~= "STANDBY" or wakeup or (smart_info["p_table"] and not smart_info["p_table"]:match("Raid")) or device:match("^md") then
|
if smart_info.status ~= "STANDBY" or wakeup or (smart_info["p_table"] and not smart_info["p_table"]:match("Raid")) or device:match("^md") then
|
||||||
disk_info = get_parted_info(device)
|
disk_info = get_parted_info(device)
|
||||||
disk_info["sec_size"] = disk_info["logic_sec"] .. "/" .. disk_info["phy_sec"]
|
disk_info["sec_size"] = tostring(disk_info["logic_sec"] or "-") .. "/" .. tostring(disk_info["phy_sec"] or "-")
|
||||||
disk_info["size_formated"] = byte_format(tonumber(disk_info["size"]))
|
disk_info["size_formated"] = byte_format(tonumber(disk_info["size"]))
|
||||||
-- if status is standby, after get part info, the disk is weakuped, then get smart_info again for more informations
|
-- if status is standby, after get part info, the disk is weakuped, then get smart_info again for more informations
|
||||||
if smart_info.status ~= "ACTIVE" then smart_info = get_smart_info(device) end
|
if smart_info.status ~= "ACTIVE" then smart_info = get_smart_info(device) end
|
||||||
@@ -362,11 +402,9 @@ d.list_raid_devices = function()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local size = tonumber(fs.readfile(string.format("/sys/class/block/%s/size", mdpath)))
|
local size = tonumber(fs.readfile(string.format("/sys/class/block/%s/size", mdpath)))
|
||||||
local ss = tonumber(fs.readfile(string.format("/sys/class/block/%s/queue/logical_block_size", mdpath)))
|
|
||||||
|
|
||||||
device_info["path"] = "/dev/"..mdpath
|
device_info["path"] = "/dev/"..mdpath
|
||||||
device_info["size"] = size*ss
|
device_info["size"] = size * SYSFS_SECTOR_SIZE
|
||||||
device_info["size_formated"] = byte_format(size*ss)
|
device_info["size_formated"] = byte_format(device_info["size"])
|
||||||
device_info["active"] = active:upper()
|
device_info["active"] = active:upper()
|
||||||
device_info["level"] = level
|
device_info["level"] = level
|
||||||
device_info["members"] = members
|
device_info["members"] = members
|
||||||
@@ -418,19 +456,18 @@ d.list_devices = function()
|
|||||||
local device_info = {}
|
local device_info = {}
|
||||||
local device = "/dev/" .. bname
|
local device = "/dev/" .. bname
|
||||||
local size = tonumber(fs.readfile(string.format("/sys/class/block/%s/size", bname)) or "0")
|
local size = tonumber(fs.readfile(string.format("/sys/class/block/%s/size", bname)) or "0")
|
||||||
local ss = tonumber(fs.readfile(string.format("/sys/class/block/%s/queue/logical_block_size", bname)) or "0")
|
|
||||||
local model = fs.readfile(string.format("/sys/class/block/%s/device/model", bname))
|
local model = fs.readfile(string.format("/sys/class/block/%s/device/model", bname))
|
||||||
local partitions = {}
|
local partitions = {}
|
||||||
for part in nixio.fs.glob("/sys/block/" .. bname .."/" .. bname .. "*") do
|
for part in nixio.fs.glob("/sys/block/" .. bname .."/" .. bname .. "*") do
|
||||||
local pname = nixio.fs.basename(part)
|
local pname = nixio.fs.basename(part)
|
||||||
local psize = byte_format(tonumber(nixio.fs.readfile(part .. "/size"))*ss)
|
local psize = byte_format(tonumber(nixio.fs.readfile(part .. "/size")) * SYSFS_SECTOR_SIZE)
|
||||||
local mount_point = get_mount_point(pname)
|
local mount_point = get_mount_point(pname)
|
||||||
if mount_point then device_info["inuse"] = true end
|
if mount_point then device_info["inuse"] = true end
|
||||||
table.insert(partitions, {name = pname, size_formated = psize, inuse = mount_point})
|
table.insert(partitions, {name = pname, size_formated = psize, inuse = mount_point})
|
||||||
end
|
end
|
||||||
|
|
||||||
device_info["path"] = device
|
device_info["path"] = device
|
||||||
device_info["size_formated"] = byte_format(size*ss)
|
device_info["size_formated"] = byte_format(size * SYSFS_SECTOR_SIZE)
|
||||||
device_info["model"] = model
|
device_info["model"] = model
|
||||||
device_info["partitions"] = partitions
|
device_info["partitions"] = partitions
|
||||||
-- true or false
|
-- true or false
|
||||||
|
|||||||
@@ -1,7 +1,66 @@
|
|||||||
|
<style>
|
||||||
|
.diskman-table { table-layout: auto; width: 100%; }
|
||||||
|
.diskman-table > tbody > tr > th,
|
||||||
|
.diskman-table > tbody > tr > td {
|
||||||
|
padding-left: 6px;
|
||||||
|
padding-right: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-wrap: normal;
|
||||||
|
width: auto;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.diskman-table > tbody > tr > [data-name="path"],
|
||||||
|
.diskman-table > tbody > tr > [data-name="model"] {
|
||||||
|
width: auto;
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
white-space: normal;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.diskman-table > tbody > tr > .cbi-section-actions {
|
||||||
|
width: 6em;
|
||||||
|
min-width: 6em;
|
||||||
|
flex: 0 0 6em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.onload = function () {
|
window.onload = function () {
|
||||||
//disk partition info
|
//disk partition info
|
||||||
let p_colors = ["#c0c0ff", "#fbbd00", "#e97c30", "#a0e0a0", "#e0c0ff"]
|
let p_colors = ["#c0c0ff", "#fbbd00", "#e97c30", "#a0e0a0", "#e0c0ff"]
|
||||||
|
function renderPartitions(item, disk_info) {
|
||||||
|
if (!disk_info.partitions || !disk_info.partitions.length) return
|
||||||
|
let isTable = item.nodeName == "TR"
|
||||||
|
if (isTable) item.closest("table").classList.add("diskman-table")
|
||||||
|
let row = document.createElement(isTable ? "tr" : "div")
|
||||||
|
let cell = document.createElement(isTable ? "td" : "div")
|
||||||
|
if (isTable) cell.colSpan = item.children.length
|
||||||
|
else {
|
||||||
|
row.className = "tr cbi-section-table-row cbi-rowstyle-1"
|
||||||
|
cell.className = "td"
|
||||||
|
}
|
||||||
|
cell.style.cssText = "padding:0; border:0;"
|
||||||
|
let container = document.createElement("div")
|
||||||
|
container.style.cssText = "position:relative; height:1.8em; overflow:hidden;"
|
||||||
|
let bar = document.createElement("div")
|
||||||
|
// Keep partition labels out of the table's intrinsic width calculation.
|
||||||
|
bar.style.cssText = "position:absolute; inset:0; display:flex;"
|
||||||
|
disk_info.partitions.forEach((part, index) => {
|
||||||
|
let segment = document.createElement("div")
|
||||||
|
let label = (part.name != '-' && part.name || '') + ' ' + (part.fs != 'Free Space' && part.fs || '') + ' ' + part.size_formated + ' ' + (part.useage != '-' && part.useage || '')
|
||||||
|
let share = disk_info.size > 0 ? part.size / disk_info.size * 100 : 0
|
||||||
|
// Flex normalizes the weights even when there are many small partitions.
|
||||||
|
segment.style.cssText = "min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:center; color:#525F7F;"
|
||||||
|
segment.style.flex = Math.max(share || 0, 8) + " 1 0%"
|
||||||
|
segment.style.backgroundColor = p_colors[index % p_colors.length]
|
||||||
|
segment.title = label
|
||||||
|
segment.textContent = label
|
||||||
|
bar.appendChild(segment)
|
||||||
|
})
|
||||||
|
container.appendChild(bar)
|
||||||
|
cell.appendChild(container)
|
||||||
|
row.appendChild(cell)
|
||||||
|
item.parentNode.insertBefore(row, item.nextSibling)
|
||||||
|
}
|
||||||
let lines = document.querySelectorAll('[id^=cbi-disk-]')
|
let lines = document.querySelectorAll('[id^=cbi-disk-]')
|
||||||
lines.forEach((item) => {
|
lines.forEach((item) => {
|
||||||
let dev = item.id.match(/cbi-disk-(.*)/)[1]
|
let dev = item.id.match(/cbi-disk-(.*)/)[1]
|
||||||
@@ -19,7 +78,7 @@
|
|||||||
} else if (cell.getAttribute("data-name") == "sata_ver" || cell.childNodes[1] && cell.childNodes[1].id.match(/sata_ver/)) {
|
} else if (cell.getAttribute("data-name") == "sata_ver" || cell.childNodes[1] && cell.childNodes[1].id.match(/sata_ver/)) {
|
||||||
cell.innerText = disk_info.sata_ver || "-"
|
cell.innerText = disk_info.sata_ver || "-"
|
||||||
} else if (cell.getAttribute("data-name") == "health_status" || cell.childNodes[1] && cell.childNodes[1].id.match(/health_status/)) {
|
} else if (cell.getAttribute("data-name") == "health_status" || cell.childNodes[1] && cell.childNodes[1].id.match(/health_status/)) {
|
||||||
cell.innerText = (disk_info.health || "-") + "\n" + (disk_info.status || "-")
|
cell.innerText = (disk_info.health || "-") + " / " + (disk_info.status || "-")
|
||||||
} else if (cell.getAttribute("data-name") == "health" || cell.childNodes[1] && cell.childNodes[1].id.match(/health/)) {
|
} else if (cell.getAttribute("data-name") == "health" || cell.childNodes[1] && cell.childNodes[1].id.match(/health/)) {
|
||||||
cell.innerText = disk_info.health || "-"
|
cell.innerText = disk_info.health || "-"
|
||||||
} else if (cell.getAttribute("data-name") == "status" || cell.childNodes[1] && cell.childNodes[1].id.match(/status/)) {
|
} else if (cell.getAttribute("data-name") == "status" || cell.childNodes[1] && cell.childNodes[1].id.match(/status/)) {
|
||||||
@@ -27,44 +86,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// handle partitons info
|
renderPartitions(item, disk_info)
|
||||||
if (disk_info.partitions && disk_info.partitions.length > 0) {
|
|
||||||
let partitons_div
|
|
||||||
if (item.nodeName == "TR") {
|
|
||||||
partitons_div = '<tr width="100%" style="white-space:nowrap;"><td style="margin:0px; padding:0px; border:0px; white-space:nowrap;" colspan="15">'
|
|
||||||
} else if (item.nodeName == "DIV") {
|
|
||||||
partitons_div = '<div class="tr cbi-section-table-row cbi-rowstyle-1"><div style="white-space:nowrap; position:absolute; width:100%">'
|
|
||||||
}
|
|
||||||
let expand = 0
|
|
||||||
let need_expand = 0
|
|
||||||
disk_info.partitions.forEach((part) => {
|
|
||||||
let p = part.size / disk_info.size * 100
|
|
||||||
if (p <= 8) {
|
|
||||||
expand += 8
|
|
||||||
need_expand += p
|
|
||||||
part.part_percent = 8
|
|
||||||
}
|
|
||||||
})
|
|
||||||
let n = 0
|
|
||||||
disk_info.partitions.forEach((part) => {
|
|
||||||
let p = part.size / disk_info.size * 100
|
|
||||||
if (p > 8) {
|
|
||||||
part.part_percent = p * (100 - expand) / (100 - need_expand)
|
|
||||||
}
|
|
||||||
let part_percent = part.part_percent + '%'
|
|
||||||
let p_color = p_colors[n++]
|
|
||||||
if (n > 4) { n = 0 }
|
|
||||||
let inline_txt = (part.name != '-' && part.name || '') + ' ' + (part.fs != 'Free Space' && part.fs || '') + ' ' + part.size_formated + ' ' + (part.useage != '-' && part.useage || '')
|
|
||||||
let partiton_div = '<div title="' + inline_txt + '" style="color: #525F7F; display:inline-block; text-align:center;background-color:' + p_color + '; width:' + part_percent + '">' + inline_txt + '</div>'
|
|
||||||
partitons_div += partiton_div
|
|
||||||
})
|
|
||||||
if (item.nodeName == "TR") {
|
|
||||||
partitons_div += '</td></tr>'
|
|
||||||
} else if (item.nodeName == "DIV") {
|
|
||||||
partitons_div += '</div><div> </div></div>'
|
|
||||||
}
|
|
||||||
item.insertAdjacentHTML('afterend', partitons_div);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
//raid table
|
//raid table
|
||||||
@@ -82,28 +104,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// handle partitons info
|
renderPartitions(item, disk_info)
|
||||||
let partitons_div
|
|
||||||
if (item.nodeName == "TR") {
|
|
||||||
partitons_div = '<tr width="100%" style="white-space:nowrap;"><td style="margin:0px; padding:0px; border:0px; white-space:nowrap;" colspan="15">'
|
|
||||||
} else if (item.nodeName == "DIV") {
|
|
||||||
partitons_div = '<div class="tr cbi-section-table-row cbi-rowstyle-1"><div style="white-space:nowrap; position:absolute; width:100%">'
|
|
||||||
}
|
|
||||||
let n = 0
|
|
||||||
disk_info.partitions.forEach((part) => {
|
|
||||||
let part_percent = part.size / disk_info.size * 100 + '%'
|
|
||||||
let p_color = p_colors[n++]
|
|
||||||
if (n > 4) { n = 0 }
|
|
||||||
let inline_txt = (part.name != '-' && part.name || '') + ' ' + (part.fs != 'Free Space' && part.fs || '') + ' ' + part.size_formated + ' ' + (part.useage != '-' && part.useage || '')
|
|
||||||
let partiton_div = '<div title="' + inline_txt + '" style="display:inline-block; text-align:center;background-color:' + p_color + '; width:' + part_percent + '">' + inline_txt + '</div>'
|
|
||||||
partitons_div += partiton_div
|
|
||||||
})
|
|
||||||
if (item.nodeName == "TR") {
|
|
||||||
partitons_div += '</td></tr>'
|
|
||||||
} else if (item.nodeName == "DIV") {
|
|
||||||
partitons_div += '</div><div> </div></div>'
|
|
||||||
}
|
|
||||||
item.insertAdjacentHTML('afterend', partitons_div);
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,9 +85,9 @@ msgid "Data"
|
|||||||
msgstr "Data"
|
msgstr "Data"
|
||||||
|
|
||||||
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/partition.lua:121
|
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/partition.lua:121
|
||||||
msgid "Default 2048 sector alignment, support +size{b,k,m,g,t} in End Sector"
|
msgid "Default 1 MiB alignment, support +size{b,k,m,g,t} in End Sector"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Sektory wyrównywane są do wielokrotności 2048, sektor końcowy można podać "
|
"Domyślne wyrównanie wynosi 1 MiB, sektor końcowy można podać "
|
||||||
"jako rozmiar w postaci +size{b,k,m,g,t}"
|
"jako rozmiar w postaci +size{b,k,m,g,t}"
|
||||||
|
|
||||||
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/btrfs.lua:114
|
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/btrfs.lua:114
|
||||||
@@ -506,9 +506,3 @@ msgstr ""
|
|||||||
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/partition.lua:99
|
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/partition.lua:99
|
||||||
msgid "is in use! please unmount it first!"
|
msgid "is in use! please unmount it first!"
|
||||||
msgstr "aktualnie jest w użyciu, proszę najpierw odmontować!"
|
msgstr "aktualnie jest w użyciu, proszę najpierw odmontować!"
|
||||||
|
|
||||||
#~ msgid "Partition NOT found!"
|
|
||||||
#~ msgstr "Nie znaleziono partycji!"
|
|
||||||
|
|
||||||
#~ msgid "Filesystem NOT support!"
|
|
||||||
#~ msgstr "System plików nie jest obsługiwany!"
|
|
||||||
@@ -74,7 +74,7 @@ msgid "Data"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/partition.lua:121
|
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/partition.lua:121
|
||||||
msgid "Default 2048 sector alignment, support +size{b,k,m,g,t} in End Sector"
|
msgid "Default 1 MiB alignment, support +size{b,k,m,g,t} in End Sector"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/btrfs.lua:114
|
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/btrfs.lua:114
|
||||||
@@ -488,3 +488,18 @@ msgstr ""
|
|||||||
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/partition.lua:99
|
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/partition.lua:99
|
||||||
msgid "is in use! please unmount it first!"
|
msgid "is in use! please unmount it first!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Partition Name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Estimated lifetime exceeded"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Normal"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Warning"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Urgent"
|
||||||
|
msgstr ""
|
||||||
@@ -85,9 +85,9 @@ msgid "Data"
|
|||||||
msgstr "数据"
|
msgstr "数据"
|
||||||
|
|
||||||
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/partition.lua:121
|
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/partition.lua:121
|
||||||
msgid "Default 2048 sector alignment, support +size{b,k,m,g,t} in End Sector"
|
msgid "Default 1 MiB alignment, support +size{b,k,m,g,t} in End Sector"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"默认2048扇区对齐,【中止扇区】支持 +容量{b,k,m,g,t} 格式,例:+500m +10g +1t"
|
"默认按 1 MiB 对齐,【终止扇区】支持 +容量{b,k,m,g,t} 格式,例:+500m +10g +1t"
|
||||||
|
|
||||||
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/btrfs.lua:114
|
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/btrfs.lua:114
|
||||||
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/btrfs.lua:118
|
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/btrfs.lua:118
|
||||||
@@ -504,8 +504,17 @@ msgstr ""
|
|||||||
msgid "is in use! please unmount it first!"
|
msgid "is in use! please unmount it first!"
|
||||||
msgstr "正在被使用!请先卸载!"
|
msgstr "正在被使用!请先卸载!"
|
||||||
|
|
||||||
#~ msgid "Partition NOT found!"
|
msgid "Partition Name"
|
||||||
#~ msgstr "分区未找到!"
|
msgstr "分区名"
|
||||||
|
|
||||||
#~ msgid "Filesystem NOT support!"
|
msgid "Estimated lifetime exceeded"
|
||||||
#~ msgstr "文件系统不支持!"
|
msgstr "已超过预估寿命"
|
||||||
|
|
||||||
|
msgid "Normal"
|
||||||
|
msgstr "正常"
|
||||||
|
|
||||||
|
msgid "Warning"
|
||||||
|
msgstr "预警"
|
||||||
|
|
||||||
|
msgid "Urgent"
|
||||||
|
msgstr "紧急"
|
||||||
@@ -85,9 +85,9 @@ msgid "Data"
|
|||||||
msgstr "數據"
|
msgstr "數據"
|
||||||
|
|
||||||
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/partition.lua:121
|
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/partition.lua:121
|
||||||
msgid "Default 2048 sector alignment, support +size{b,k,m,g,t} in End Sector"
|
msgid "Default 1 MiB alignment, support +size{b,k,m,g,t} in End Sector"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"默認 2048 磁區對齊,【結束磁區】支持 +容量{b,k,m,g,t} 格式,例:+500m +10g "
|
"默認按 1 MiB 對齊,【結束磁區】支持 +容量{b,k,m,g,t} 格式,例:+500m +10g "
|
||||||
"+1t"
|
"+1t"
|
||||||
|
|
||||||
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/btrfs.lua:114
|
#: applications/luci-app-diskman/luasrc/model/cbi/diskman/btrfs.lua:114
|
||||||
@@ -505,8 +505,17 @@ msgstr ""
|
|||||||
msgid "is in use! please unmount it first!"
|
msgid "is in use! please unmount it first!"
|
||||||
msgstr "正在被使用!請先卸載!"
|
msgstr "正在被使用!請先卸載!"
|
||||||
|
|
||||||
#~ msgid "Partition NOT found!"
|
msgid "Partition Name"
|
||||||
#~ msgstr "分割區未找到!"
|
msgstr "分割區名稱"
|
||||||
|
|
||||||
#~ msgid "Filesystem NOT support!"
|
msgid "Estimated lifetime exceeded"
|
||||||
#~ msgstr "文件系統不支持!"
|
msgstr "已超過預估壽命"
|
||||||
|
|
||||||
|
msgid "Normal"
|
||||||
|
msgstr "正常"
|
||||||
|
|
||||||
|
msgid "Warning"
|
||||||
|
msgstr "預警"
|
||||||
|
|
||||||
|
msgid "Urgent"
|
||||||
|
msgstr "緊急"
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=luci-app-modem-manager-next
|
||||||
|
PKG_VERSION:=1.0.0
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
|
PKG_LICENSE:=MIT
|
||||||
|
PKG_MAINTAINER:=Dante <dante@seckv.com>
|
||||||
|
|
||||||
|
LUCI_TITLE:=LuCI support for Cellular Modem Management (Next-Gen)
|
||||||
|
LUCI_DESCRIPTION:=Provides a modern LuCI Web management interface for 4G/5G cellular modems.
|
||||||
|
PKG_DESCRIPTION:=Provides a modern LuCI Web management interface for 4G/5G cellular modems.
|
||||||
|
LUCI_DEPENDS:=+ucode +ucode-mod-fs +ucode-mod-ubus +ucode-mod-uci \
|
||||||
|
+kmod-usb-net +kmod-usb-serial +kmod-usb-serial-option \
|
||||||
|
+kmod-usb-net-cdc-ncm +kmod-usb-net-cdc-mbim +kmod-usb-net-cdc-ether +kmod-usb-net-qmi-wwan \
|
||||||
|
+umbim +uqmi +sms-tool
|
||||||
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)
|
||||||
|
$(call Package/luci/webtemplate)
|
||||||
|
TITLE:=$(LUCI_TITLE)
|
||||||
|
DEPENDS:=$(LUCI_DEPENDS)
|
||||||
|
PROVIDES:=luci-app-modem-manager
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/description
|
||||||
|
Provides a modern LuCI Web management interface for 4G/5G cellular modems.
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/conffiles
|
||||||
|
/etc/config/modem-manager
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/postinst
|
||||||
|
#!/bin/sh
|
||||||
|
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||||
|
chmod 0755 /etc/init.d/modem-manager 2>/dev/null || true
|
||||||
|
chmod 0755 /etc/hotplug.d/usb/20-modem-hotplug 2>/dev/null || true
|
||||||
|
chmod 0755 /usr/libexec/modem-at-executor /usr/libexec/modem-detect /usr/libexec/modem-collector 2>/dev/null || true
|
||||||
|
/etc/init.d/rpcd restart
|
||||||
|
/etc/init.d/modem-manager enable
|
||||||
|
/etc/init.d/modem-manager restart
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
endef
|
||||||
|
|
||||||
|
# call BuildPackage - OpenWrt buildroot signature
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
# luci-app-modem-manager-next
|
||||||
|
|
||||||
|
现代化、轻量级、高可靠的 OpenWrt 蜂窝模组管理器(Next-Gen),专为 4G / 5G 移动宽带模组设计。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 架构设计与背景
|
||||||
|
|
||||||
|
在传统 OpenWrt 路由器环境中,官方及社区方案通常面临两大难题:
|
||||||
|
1. **重型依赖与功能受限**:基于 D-Bus 和桌面级 ModemManager (`mmcli`) 的方案占用内存大、启动慢,且对移远(Quectel)、广和通(Fibocom)、中兴(ZTE)等主流 5G/4G 模组的私有特性(锁频段、5G SA/NSA 模式切换、私有驱动切换、载波聚合查询等)支持不足,甚至默认禁止 AT 命令直接交互;
|
||||||
|
2. **串口竞争与死锁风险**:AT 串口为单工互斥字符设备。若前端定时轮询信号指标,极易与用户 AT 命令交互或模组底层注册产生冲突,引发端口死锁、数据乱码或缓冲区溢出。
|
||||||
|
|
||||||
|
`luci-app-modem-manager-next` 采用**“物理 Tab 独立解耦架构 + ucode 原生 RPC + 串口排他并发调度网关 + 异步双缓冲状态采集引擎”**,全面解决上述痛点。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 系统核心拓扑与执行流程
|
||||||
|
|
||||||
|
```text
|
||||||
|
【内核层与热插拔】 【前端 LuCI2 界面】
|
||||||
|
+-----------------------------+ +-----------------------------+
|
||||||
|
| USB 模组物理插拔 / 模式切换 | | 前端 Overview 页面 (5秒轮询)|
|
||||||
|
+-----------------------------+ +-----------------------------+
|
||||||
|
| |
|
||||||
|
内核 UEvent 触发 ubus call (非阻塞)
|
||||||
|
v v
|
||||||
|
+-----------------------------+ +-----------------------------+
|
||||||
|
| /etc/hotplug.d/usb/ | | rpcd ucode 插件 (luci.modem)|
|
||||||
|
| 20-modem-hotplug 脚本 | | 直接秒级读取内存缓存 JSON |
|
||||||
|
+-----------------------------+ +-----------------------------+
|
||||||
|
| ^
|
||||||
|
异步触发 | 毫秒级零延迟读取
|
||||||
|
v |
|
||||||
|
+-----------------------------+ +-----------------------------+
|
||||||
|
| modem-detect (快速探测) | ── 更新设备缓存 ──> | /tmp/run/modem-manager/ |
|
||||||
|
+-----------------------------+ | devices.json & status.json |
|
||||||
|
+-----------------------------+
|
||||||
|
^
|
||||||
|
| 原子写入最新指标
|
||||||
|
+-----------------------------------------------------------+ |
|
||||||
|
| 【后台单向采集守护器 (modem-collector)】 | |
|
||||||
|
| | ──────+
|
||||||
|
| 1. 定时触发采集周期 (默认 5 秒) |
|
||||||
|
| 2. 检查终端活跃标记 (/var/lock/modem_terminal_active.lock) |
|
||||||
|
| ├─ [活跃中] -> 主动休眠避让,绝不碰串口 |
|
||||||
|
| └─ [空闲中] -> 带 flock 单线程安全读取 AT 状态指标 |
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
▲
|
||||||
|
│ 标记终端操作已介入 (保持避让窗口 10 秒)
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
| 【用户在 AT 终端页执行指令 (Terminal Tab)】 |
|
||||||
|
| 独占拥有最高优先级串口控制权,绝无后台轮询争抢导致死锁 |
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 核心机制实现
|
||||||
|
|
||||||
|
### 3.1 模块变动触发扫描机制 (Hotplug-driven Scan)
|
||||||
|
- 监听脚本:`/etc/hotplug.d/usb/20-modem-hotplug`;
|
||||||
|
- 行为:当模组发生物理插入、拔出,或在配置页面执行模式切换导致 USB 端点重新枚举时,内核 UEvent 异步唤醒脚本,触发 `/usr/libexec/modem-detect --cache` 重新建立设备拓扑清单,原子更新 `/tmp/run/modem-manager/devices.json`,并通过 ubus 广播事件通知前端视图。
|
||||||
|
|
||||||
|
### 3.2 初始状态扫描探测机制 (Boot & Startup Init)
|
||||||
|
- 引导期预热:在 `/etc/init.d/modem-manager` 启动引导期,后台异步执行一次 `modem-detect --cache`;
|
||||||
|
- 前端极速首屏:前端 RPC `get_devices` 优先读取内存缓存,实现 0 毫秒首屏呈现,彻底免除用户打开页面时的等待开销。
|
||||||
|
|
||||||
|
### 3.3 异步状态双缓冲与 AT 终端互斥主动避让 (Async Decoupled Polling & Active Yielding)
|
||||||
|
- **单一生产者**:后台常驻守护进程 `/usr/libexec/modem-collector` 作为与串口交互的唯一常规生产者,单线程采集 CSQ、CPIN、COPS、QENG、QTEMP 并原子更新 `/tmp/run/modem-manager/status.json`;
|
||||||
|
- **终端最高优先级**:用户通过控制台或快捷按钮发送指令(`send_at`)时,立即触碰激活 `/var/lock/modem_terminal_active.lock`;
|
||||||
|
- **主动退避**:后台采集器若检测到终端在过去 10 秒内有操作,立即放弃当前轮询并主动休眠,将串口控制权完全让渡给用户;
|
||||||
|
- **前端彻底解耦**:前端 Overview 页面的 5 秒轮询仅读取内存缓存文件,调用耗时为 `0.00s`,零串口开销,杜绝死锁与缓冲区阻塞。
|
||||||
|
|
||||||
|
### 3.4 纯 POSIX 原生串口执行器 (modem-at-executor)
|
||||||
|
- 零外部依赖:摆脱对 `chat`、`stty` 或专用工具的依赖,100% 兼容裁剪版 BusyBox;
|
||||||
|
- 互斥安全:基于 `flock -n -x 9` 非阻塞轮询实现文件级排他锁,防止多进程竞争;
|
||||||
|
- 超时保护与动态自适应:基于管道和时间窗口读取,普通指令 3 秒超时,长耗时指令(如 `AT+COPS=?` 全频段搜网)自适应动态提升至 60 秒超时,防止误截断。
|
||||||
|
|
||||||
|
### 3.5 AT 终端容错性与交互易用性增强
|
||||||
|
- **中英文标点智能转换**:自动将全角/中文引号(`“”`)、逗号(`,`)、问号(`?`)、冒号(`:`)、等号(`=`)、加号(`+`)智能清洗为标准半角 ASCII 字符,防止模组因多字节编码报错;
|
||||||
|
- **智能推测与模糊建议 ("Did you mean?")**:当指令执行失败时,利用编辑距离算法计算高频知识库,自动推测用户意图(例如误输 `AT+CPIM?` 时提示推测 `AT+CPIN?`,支持一键填入);
|
||||||
|
- **行内实时自动补全 (Autocomplete)**:打字时自动匹配候选指令及中文功能简介,支持 Esc 快速关闭;
|
||||||
|
- **CME / CMS 错误码中文语义化**:内置 3GPP TS 27.007 错误码字典,将晦涩的数字代码(如 `+CME ERROR: 10/11/13/14`)自动解析为可读中文释义;
|
||||||
|
- **高危破坏性指令拦截预警**:拦截 `AT&F`、`AT+RESTORE` 等可能导致出厂重置的高危操作,弹窗二次确认;
|
||||||
|
- **耗时测量与日志导出**:毫秒级精度测量下发耗时,支持自定义收藏夹管理与一键导出排障日志文件。
|
||||||
|
|
||||||
|
### 3.6 主控制串口用户接管与持久化机制 (User-Configured AT Port Override)
|
||||||
|
- **设置入口**:在【设置 (Settings)】页面中提供专属“串口与通信参数配置”卡片;
|
||||||
|
- **候选端口与自定义路径**:下拉列表枚举该模组暴露的所有物理串口,默认选中国际推荐口(标有 `[自动推荐]`),并支持选择 `[自定义设备路径]` 手动输入专用软链接(如 `/dev/modem_at` 或 `/dev/ttyACM0`);
|
||||||
|
- **配置与运行态全链路接管**:保存后写入 UCI `/etc/config/modem-manager` 的 `global.port`,后台采集器 `modem-collector`、前台总览页 `Overview` 与 RPC 接口均以用户指定端口作为最高优先级,彻底消除端口序号漂移和多串口设备误判风险。
|
||||||
|
|
||||||
|
### 3.7 物联网卡零干扰熔断机制与安全短信模块 (Safe SMS with IoT Guard)
|
||||||
|
- **默认全局熔断关闭**:考虑到部分物联网卡(IoT SIM)、企业遥测专用卡或定向纯流量卡对 SMS 业务存在特殊风控限制,若模组自动轮询或读取短信可能导致异常扣费、被运营商封卡或阻塞 AT 通道,系统在 UCI 中默认设置 `global.sms_enable=0`;
|
||||||
|
- **零串口开销**:在短信模块处于关闭状态时,后台采集守护器和 RPC 接口绝对不向模组发送任何短信读取、容量查询或配置指令;
|
||||||
|
- **显式激活与收发管理**:用户在【短信 (SMS)】页面可一键安全启用,支持 SIM 卡短信容量占用进度查看、收件箱列表阅读、UCS2/UTF-8 字符自适应解码、写信发送(带文本长度统计与手机号校验)及一键清空所有短信。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 目录与文件布局
|
||||||
|
|
||||||
|
```text
|
||||||
|
luci-app-modem-manager-next/
|
||||||
|
├── Makefile # 软件包构建与权限配置
|
||||||
|
├── README.md # 项目架构与设计文档
|
||||||
|
├── root/
|
||||||
|
│ ├── etc/
|
||||||
|
│ │ ├── config/modem-manager # UCI 配置文件 (含 sms_enable)
|
||||||
|
│ │ ├── hotplug.d/usb/20-modem-hotplug # USB 热插拔事件感知脚本
|
||||||
|
│ │ ├── init.d/modem-manager # 系统守护服务脚本
|
||||||
|
│ │ └── uci-defaults/99_modem-manager-init # 初始化脚本
|
||||||
|
│ └── usr/
|
||||||
|
│ ├── libexec/
|
||||||
|
│ │ ├── modem-at-executor # 纯 POSIX 串口排他安全执行器 (含 SMS 发送)
|
||||||
|
│ │ ├── modem-collector # 后台异步状态采集与避让守护器
|
||||||
|
│ │ └── modem-detect # sysfs 设备枚举与关联扫描工具
|
||||||
|
│ └── share/
|
||||||
|
│ ├── modem-manager/templates/ # 厂商预置模版库
|
||||||
|
│ │ ├── quectel.json # 移远模组规则 (RM500Q, RM500U, RG500U 等)
|
||||||
|
│ │ ├── fibocom.json # 广和通模组规则 (FM650, FG650, NL668 等)
|
||||||
|
│ │ ├── zte.json # 中兴微模组规则
|
||||||
|
│ │ └── generic.json # 通用 3GPP 模组规则
|
||||||
|
│ ├── luci/menu.d/
|
||||||
|
│ │ └── luci-app-modem-manager.json # 菜单路由定义 (含 SMS Tab)
|
||||||
|
│ └── rpcd/
|
||||||
|
│ ├── acl.d/luci-app-modem-manager.json # RPC 鉴权规则
|
||||||
|
│ └── ucode/luci.modem # ucode RPC 插件 (含 SMS 接口)
|
||||||
|
├── htdocs/luci-static/resources/view/modem/
|
||||||
|
│ ├── overview.js # 【总览】:信号仪表盘、基站详情、SIM状态
|
||||||
|
│ ├── settings.js # 【设置】:驱动协议切换、频段锁定、APN
|
||||||
|
│ ├── sms.js # 【短信】:物联网卡安全防护、短信收发展示
|
||||||
|
│ ├── templates.js # 【模版】:厂商模版查看与参数管理
|
||||||
|
│ ├── terminal.js # 【终端】:高级交互控制台与快捷预设指令
|
||||||
|
│ └── log.js # 【日志】:AT 交互与系统运行日志
|
||||||
|
└── po/
|
||||||
|
├── templates/modem.pot # 翻译模板
|
||||||
|
└── zh_Hans/modem.po # 中文简体翻译 (无 Emoji、无中英混杂)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 验证与兼容性
|
||||||
|
|
||||||
|
本软件包已在真实硬件环境中完成验证:
|
||||||
|
- **测试环境**:ImmortalWrt 25.12 (MediaTek Filogic, aarch64)
|
||||||
|
- **测试模组**:移远通信 RM500U-CNV (USB ID: `2c7c:0900`)
|
||||||
|
- **驱动协议**:`cdc_ncm` (`usb0`)
|
||||||
|
- **网络制式**:中国电信 5G 独立组网 (NR5G-SA, N78 频段)
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require rpc';
|
||||||
|
'require poll';
|
||||||
|
'require ui';
|
||||||
|
'require dom';
|
||||||
|
|
||||||
|
const callGetLog = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'get_log',
|
||||||
|
expect: { log: '' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const callClearLog = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'clear_log'
|
||||||
|
});
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null,
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
return L.resolveDefault(callGetLog(), '');
|
||||||
|
},
|
||||||
|
|
||||||
|
handleClearLog: function() {
|
||||||
|
const self = this;
|
||||||
|
callClearLog().then(function() {
|
||||||
|
const logArea = document.getElementById('modem_log_content');
|
||||||
|
if (logArea) logArea.value = '';
|
||||||
|
ui.addNotification(null, E('p', {}, _('Log buffer cleared successfully.')), 'info');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateLog: function() {
|
||||||
|
return callGetLog().then(function(res) {
|
||||||
|
const logArea = document.getElementById('modem_log_content');
|
||||||
|
if (logArea) {
|
||||||
|
let logText = (typeof res === 'string') ? res : ((res && res.log) ? res.log : '');
|
||||||
|
logArea.value = logText || _('No log entries recorded yet.');
|
||||||
|
logArea.scrollTop = logArea.scrollHeight;
|
||||||
|
}
|
||||||
|
}).catch(function() {});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(logData) {
|
||||||
|
const self = this;
|
||||||
|
let initialLog = (typeof logData === 'string') ? logData : ((logData && logData.log) ? logData.log : '');
|
||||||
|
if (!initialLog) initialLog = _('No log entries recorded yet.');
|
||||||
|
|
||||||
|
const container = E('div', { 'class': 'cbi-map' }, [
|
||||||
|
E('h2', {}, _('Cellular Modem Manager') + ' - ' + _('System Log')),
|
||||||
|
E('div', { 'class': 'cbi-map-descr' }, _('View background AT command traces, hardware detection events, and auto-reconnect activities.'))
|
||||||
|
]);
|
||||||
|
|
||||||
|
const logSection = E('fieldset', { 'class': 'cbi-section' }, [
|
||||||
|
E('legend', { 'style': 'display: flex; justify-content: space-between; align-items: center; width: 100%; box-sizing: border-box;' }, [
|
||||||
|
E('span', {}, _('Modem Manager Logs')),
|
||||||
|
E('span', { 'style': 'display: inline-flex; gap: 8px; align-items: center; margin-left: auto;' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'class': 'btn cbi-button cbi-button-reload',
|
||||||
|
'value': _('Refresh'),
|
||||||
|
'style': 'font-size: 11px;',
|
||||||
|
'click': ui.createHandlerFn(self, 'updateLog')
|
||||||
|
}),
|
||||||
|
E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'class': 'btn cbi-button cbi-button-reset',
|
||||||
|
'value': _('Clear Log'),
|
||||||
|
'style': 'font-size: 11px;',
|
||||||
|
'click': ui.createHandlerFn(self, 'handleClearLog')
|
||||||
|
})
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-section-node' }, [
|
||||||
|
E('textarea', {
|
||||||
|
'id': 'modem_log_content',
|
||||||
|
'class': 'cbi-input-textarea',
|
||||||
|
'style': 'width: 100%; min-height: 420px; font-family: monospace; font-size: 12px; background: #fafafa; border: 1px solid #ccc; box-sizing: border-box; padding: 10px;',
|
||||||
|
'readonly': 'readonly',
|
||||||
|
'wrap': 'off'
|
||||||
|
}, [ initialLog ])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
container.appendChild(logSection);
|
||||||
|
|
||||||
|
poll.add(function() {
|
||||||
|
return self.updateLog();
|
||||||
|
}, 5);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,430 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require rpc';
|
||||||
|
'require poll';
|
||||||
|
'require ui';
|
||||||
|
'require dom';
|
||||||
|
|
||||||
|
const callGetDevices = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'get_devices',
|
||||||
|
expect: { devices: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
const callGetStatus = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'get_status',
|
||||||
|
params: [ 'port', 'primary_net' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
const callRefreshStatus = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'refresh_status',
|
||||||
|
params: [ 'port', 'primary_net' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
const callDeviceAction = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'device_action',
|
||||||
|
params: [ 'port', 'action' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null,
|
||||||
|
|
||||||
|
selectedPort: null,
|
||||||
|
selectedNet: null,
|
||||||
|
devicesList: [],
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
return L.resolveDefault(callGetDevices(), []);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderProgressBar: function(percent, label, color) {
|
||||||
|
const bg = color || '#2bc1a4';
|
||||||
|
return E('div', { 'style': 'width: 220px; background: #eee; border-radius: 3px; height: 16px; position: relative; display: inline-block; vertical-align: middle; margin-right: 10px; overflow: hidden;' }, [
|
||||||
|
E('div', {
|
||||||
|
'style': 'height: 100%; width: ' + Math.min(100, Math.max(0, percent)) + '%; background: ' + bg + '; transition: width 0.3s;'
|
||||||
|
}),
|
||||||
|
E('span', {
|
||||||
|
'style': 'position: absolute; width: 100%; top: 0; left: 0; text-align: center; font-size: 11px; line-height: 16px; color: #333; font-weight: bold;'
|
||||||
|
}, label)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleAction: function(action) {
|
||||||
|
const self = this;
|
||||||
|
if (!self.selectedPort) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Please select a valid modem port first.')), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.showModal(_('Executing Action'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Sending action to cellular module, please wait...'))
|
||||||
|
]);
|
||||||
|
|
||||||
|
callDeviceAction(self.selectedPort, action).then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (res && res.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Action executed successfully.')), 'info');
|
||||||
|
self.forceRefresh();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Action failed: ') + (res ? res.output : _('Unknown error'))), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', {}, err.message), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
forceRefresh: function() {
|
||||||
|
const self = this;
|
||||||
|
if (!self.selectedPort) return;
|
||||||
|
return callRefreshStatus(self.selectedPort, self.selectedNet).then(function(res) {
|
||||||
|
self.applyStatusDOM(res);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateStatus: function() {
|
||||||
|
const self = this;
|
||||||
|
if (!self.selectedPort)
|
||||||
|
return Promise.resolve();
|
||||||
|
|
||||||
|
return callGetStatus(self.selectedPort, self.selectedNet).then(function(res) {
|
||||||
|
self.applyStatusDOM(res);
|
||||||
|
}).catch(function(e) {});
|
||||||
|
},
|
||||||
|
|
||||||
|
formatBytes: function(bytes) {
|
||||||
|
if (bytes === null || bytes === undefined || isNaN(bytes) || bytes < 0) return '0 B';
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
const k = 1024;
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
const val = (bytes / Math.pow(k, i)).toFixed(i === 0 ? 0 : 2);
|
||||||
|
return val + ' ' + units[i];
|
||||||
|
},
|
||||||
|
|
||||||
|
formatSpeed: function(bytesPerSec) {
|
||||||
|
if (bytesPerSec === null || bytesPerSec === undefined || isNaN(bytesPerSec) || bytesPerSec < 0) return '0 B/s';
|
||||||
|
return this.formatBytes(bytesPerSec) + '/s';
|
||||||
|
},
|
||||||
|
|
||||||
|
formatUptime: function(sec) {
|
||||||
|
if (!sec || sec <= 0) return '-';
|
||||||
|
const d = Math.floor(sec / 86400);
|
||||||
|
const h = Math.floor((sec % 86400) / 3600);
|
||||||
|
const m = Math.floor((sec % 3600) / 60);
|
||||||
|
const s = Math.floor(sec % 60);
|
||||||
|
let str = (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s;
|
||||||
|
if (d > 0) return d + ' ' + _('days') + ' ' + str;
|
||||||
|
return str;
|
||||||
|
},
|
||||||
|
|
||||||
|
applyStatusDOM: function(status) {
|
||||||
|
const self = this;
|
||||||
|
if (!status) return;
|
||||||
|
|
||||||
|
const csq = status.csq || {};
|
||||||
|
const cell = status.cell || {};
|
||||||
|
const net = status.network || {};
|
||||||
|
|
||||||
|
const tTemp = document.getElementById('modem_temp_val');
|
||||||
|
if (tTemp) dom.content(tTemp, (status.temperature !== null && status.temperature !== undefined) ? (status.temperature + ' °C') : _('Not Supported'));
|
||||||
|
|
||||||
|
const tOp = document.getElementById('modem_operator_val');
|
||||||
|
if (tOp) dom.content(tOp, status.operator || _('Searching'));
|
||||||
|
|
||||||
|
const tRat = document.getElementById('modem_rat_val');
|
||||||
|
if (tRat) dom.content(tRat, status.act || _('Unknown'));
|
||||||
|
|
||||||
|
const tBand = document.getElementById('modem_band_val');
|
||||||
|
if (tBand) dom.content(tBand, cell.band || _('None'));
|
||||||
|
|
||||||
|
const tFw = document.getElementById('modem_firmware_val');
|
||||||
|
if (tFw && status.firmware) dom.content(tFw, status.firmware);
|
||||||
|
|
||||||
|
const tImei = document.getElementById('modem_imei_val');
|
||||||
|
if (tImei && status.imei) dom.content(tImei, status.imei);
|
||||||
|
|
||||||
|
const tSim = document.getElementById('modem_sim_val');
|
||||||
|
if (tSim) {
|
||||||
|
let sText = _('Normal');
|
||||||
|
let sColor = 'green';
|
||||||
|
if (status.sim_status === 'PIN_LOCKED') { sText = _('PIN Locked'); sColor = 'orange'; }
|
||||||
|
else if (status.sim_status === 'PUK_LOCKED') { sText = _('PUK Locked'); sColor = 'red'; }
|
||||||
|
else if (status.sim_status === 'NOT_INSERTED') { sText = _('Not Detected'); sColor = 'red'; }
|
||||||
|
dom.content(tSim, E('span', { 'style': 'color: ' + sColor + '; font-weight: bold;' }, sText));
|
||||||
|
}
|
||||||
|
|
||||||
|
const sigCsq = document.getElementById('modem_sig_csq');
|
||||||
|
if (sigCsq) {
|
||||||
|
dom.content(sigCsq, [
|
||||||
|
self.renderProgressBar(csq.percent || 0, (csq.csq || 0) + ' / 31 (' + (csq.percent || 0) + '%)', '#2bc1a4'),
|
||||||
|
E('span', {}, (csq.rssi_dbm ? csq.rssi_dbm + ' dBm' : ''))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sigRsrp = document.getElementById('modem_sig_rsrp');
|
||||||
|
if (sigRsrp) {
|
||||||
|
let rsrpVal = (cell.rsrp !== null && cell.rsrp !== undefined && cell.rsrp < 0 && cell.rsrp >= -145) ? cell.rsrp : null;
|
||||||
|
let rsrpPercent = rsrpVal ? Math.max(0, Math.min(100, Math.floor((rsrpVal + 140) * (100 / 96)))) : 0;
|
||||||
|
let qualityText = '';
|
||||||
|
if (rsrpVal !== null) {
|
||||||
|
if (rsrpVal >= -85) qualityText = _('Excellent');
|
||||||
|
else if (rsrpVal >= -100) qualityText = _('Good');
|
||||||
|
else qualityText = _('Weak');
|
||||||
|
}
|
||||||
|
dom.content(sigRsrp, [
|
||||||
|
self.renderProgressBar(rsrpPercent, rsrpVal ? (rsrpVal + ' dBm') : _('Calculating'), '#4895ef'),
|
||||||
|
E('span', {}, qualityText)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sigSinr = document.getElementById('modem_sig_sinr');
|
||||||
|
if (sigSinr) {
|
||||||
|
let sinrVal = (cell.sinr !== null && cell.sinr !== undefined) ? cell.sinr : null;
|
||||||
|
let sinrPercent = sinrVal ? Math.max(0, Math.min(100, Math.floor((sinrVal + 10) * 2.5))) : 0;
|
||||||
|
dom.content(sigSinr, [
|
||||||
|
self.renderProgressBar(sinrPercent, sinrVal ? (sinrVal + ' dB') : _('Calculating'), '#4cc9f0')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cellPci = document.getElementById('modem_cell_pci');
|
||||||
|
if (cellPci) dom.content(cellPci, cell.pci || '-');
|
||||||
|
const cellId = document.getElementById('modem_cell_id');
|
||||||
|
if (cellId) dom.content(cellId, cell.cell_id || '-');
|
||||||
|
const cellTac = document.getElementById('modem_cell_tac');
|
||||||
|
if (cellTac) dom.content(cellTac, cell.tac || '-');
|
||||||
|
const cellArfcn = document.getElementById('modem_cell_arfcn');
|
||||||
|
if (cellArfcn) dom.content(cellArfcn, cell.earfcn || '-');
|
||||||
|
|
||||||
|
const netStatus = document.getElementById('modem_net_status');
|
||||||
|
if (netStatus) {
|
||||||
|
let isConn = !!net.connected;
|
||||||
|
let netText = isConn ? _('Connected') : _('Disconnected');
|
||||||
|
let netColor = isConn ? '#4caf50' : '#f44336';
|
||||||
|
let uptimeStr = (isConn && net.uptime) ? self.formatUptime(net.uptime) : '';
|
||||||
|
dom.content(netStatus, [
|
||||||
|
E('span', {
|
||||||
|
'style': 'background-color: ' + netColor + '; color: #fff; padding: 2px 8px; border-radius: 3px; font-weight: bold; font-size: 11px; margin-right: 8px;'
|
||||||
|
}, netText),
|
||||||
|
uptimeStr ? E('span', { 'style': 'color: #555; font-size: 12px;' }, _('Uptime: ') + uptimeStr) : ''
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const netPhys = document.getElementById('modem_net_phys');
|
||||||
|
if (netPhys) {
|
||||||
|
let devName = net.device || self.selectedNet || _('Not Registered');
|
||||||
|
dom.content(netPhys, E('span', { 'style': 'font-weight: bold; font-family: monospace; color: #0366d6;' }, devName));
|
||||||
|
}
|
||||||
|
|
||||||
|
const netIface = document.getElementById('modem_net_iface');
|
||||||
|
if (netIface) {
|
||||||
|
let ifaceName = net.interface || _('Unmanaged (Config in OpenWrt Interfaces)');
|
||||||
|
let protoStr = net.proto ? ' [' + net.proto.toUpperCase() + ']' : '';
|
||||||
|
dom.content(netIface, ifaceName + protoStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
const netIp4 = document.getElementById('modem_net_ip4');
|
||||||
|
if (netIp4) dom.content(netIp4, (net.ip4 && net.ip4.length > 0) ? net.ip4.join(', ') : '-');
|
||||||
|
|
||||||
|
const netIp6 = document.getElementById('modem_net_ip6');
|
||||||
|
if (netIp6) dom.content(netIp6, (net.ip6 && net.ip6.length > 0) ? net.ip6.join(', ') : '-');
|
||||||
|
|
||||||
|
const netTraffic = document.getElementById('modem_net_traffic');
|
||||||
|
if (netTraffic) {
|
||||||
|
const rxFormatted = self.formatBytes(net.rx_bytes || 0);
|
||||||
|
const txFormatted = self.formatBytes(net.tx_bytes || 0);
|
||||||
|
const totalFormatted = self.formatBytes((net.rx_bytes || 0) + (net.tx_bytes || 0));
|
||||||
|
const rxPackets = net.rx_packets || 0;
|
||||||
|
const txPackets = net.tx_packets || 0;
|
||||||
|
|
||||||
|
dom.content(netTraffic, [
|
||||||
|
E('div', { 'style': 'display: flex; gap: 24px; align-items: center; white-space: nowrap;' }, [
|
||||||
|
E('span', {}, [
|
||||||
|
E('strong', {}, _('Download') + ': '),
|
||||||
|
rxFormatted + (rxPackets > 0 ? ' / ' + rxPackets + ' ' + _('Packets') : '')
|
||||||
|
]),
|
||||||
|
E('span', {}, [
|
||||||
|
E('strong', {}, _('Upload') + ': '),
|
||||||
|
txFormatted + (txPackets > 0 ? ' / ' + txPackets + ' ' + _('Packets') : '')
|
||||||
|
]),
|
||||||
|
E('span', { 'style': 'color: #333;' }, [
|
||||||
|
E('strong', {}, _('Total Traffic') + ': '),
|
||||||
|
totalFormatted
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(devicesData) {
|
||||||
|
const self = this;
|
||||||
|
self.devicesList = Array.isArray(devicesData) ? devicesData : ((devicesData && Array.isArray(devicesData.devices)) ? devicesData.devices : []);
|
||||||
|
|
||||||
|
const container = E('div', { 'class': 'cbi-map' }, [
|
||||||
|
E('h2', {}, _('Cellular Modem Manager') + ' - ' + _('Overview')),
|
||||||
|
E('div', { 'class': 'cbi-map-descr' }, _('Real-time overview of cellular modem hardware, network registration, signal metrics, and data connection.'))
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (self.devicesList.length === 0) {
|
||||||
|
container.appendChild(E('div', { 'class': 'alert-message warning', 'style': 'margin-top: 20px; padding: 20px; border-left: 5px solid #ff9800; background: #fff8e1;' }, [
|
||||||
|
E('h4', { 'style': 'margin: 0 0 10px 0; color: #d32f2f; font-size: 16px;' }, _('No Active Cellular Modem Detected')),
|
||||||
|
E('p', { 'style': 'margin: 0 0 15px 0; line-height: 1.6; color: #555;' }, _('No active 4G or 5G cellular modem detected on the USB bus. Please verify hardware connection, power supply, or driver status.')),
|
||||||
|
E('div', {}, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'class': 'btn cbi-button cbi-button-action',
|
||||||
|
'value': _('Rescan Devices'),
|
||||||
|
'click': function() {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
])
|
||||||
|
]));
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.selectedPort = self.devicesList[0].primary_port;
|
||||||
|
self.selectedNet = self.devicesList[0].primary_net;
|
||||||
|
|
||||||
|
const firstDev = self.devicesList[0] || {};
|
||||||
|
const hwSection = E('fieldset', { 'class': 'cbi-section' }, [
|
||||||
|
E('legend', {}, _('Hardware Information')),
|
||||||
|
E('table', { 'class': 'table cbi-section-table' }, [
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td left', 'width': '16%' }, _('Manufacturer')),
|
||||||
|
E('td', { 'class': 'td left', 'width': '34%' }, firstDev.vendor || '-'),
|
||||||
|
E('td', { 'class': 'td left', 'width': '16%' }, _('Model')),
|
||||||
|
E('td', { 'class': 'td left', 'width': '34%' }, firstDev.model || '-')
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td left' }, _('Firmware Version')),
|
||||||
|
E('td', { 'class': 'td left', 'id': 'modem_firmware_val' }, '-'),
|
||||||
|
E('td', { 'class': 'td left' }, _('IMEI')),
|
||||||
|
E('td', { 'class': 'td left', 'id': 'modem_imei_val' }, '-')
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td left' }, _('USB Identifiers')),
|
||||||
|
E('td', { 'class': 'td left' }, (firstDev.vid && firstDev.pid) ? (firstDev.vid + ':' + firstDev.pid) : '-'),
|
||||||
|
E('td', { 'class': 'td left' }, _('Core Temperature')),
|
||||||
|
E('td', { 'class': 'td left', 'id': 'modem_temp_val' }, '-')
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td left' }, _('Driver Binding')),
|
||||||
|
E('td', { 'class': 'td left' }, firstDev.driver || _('Option Driver')),
|
||||||
|
E('td', { 'class': 'td left' }, _('SIM Status')),
|
||||||
|
E('td', { 'class': 'td left', 'id': 'modem_sim_val' }, '-')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
container.appendChild(hwSection);
|
||||||
|
|
||||||
|
const sigSection = E('fieldset', { 'class': 'cbi-section' }, [
|
||||||
|
E('legend', {}, _('Cellular Network and Signal Metrics')),
|
||||||
|
E('table', { 'class': 'table cbi-section-table' }, [
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td left', 'width': '16%' }, _('Operator')),
|
||||||
|
E('td', { 'class': 'td left', 'width': '34%', 'id': 'modem_operator_val' }, '-'),
|
||||||
|
E('td', { 'class': 'td left', 'width': '16%' }, _('Access Technology')),
|
||||||
|
E('td', { 'class': 'td left', 'width': '34%', 'id': 'modem_rat_val' }, '-')
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td left' }, _('Active Band')),
|
||||||
|
E('td', { 'class': 'td left', 'id': 'modem_band_val' }, '-'),
|
||||||
|
E('td', { 'class': 'td left' }, _('Physical Cell ID')),
|
||||||
|
E('td', { 'class': 'td left', 'id': 'modem_cell_pci' }, '-')
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td left' }, _('Cell ID')),
|
||||||
|
E('td', { 'class': 'td left', 'id': 'modem_cell_id' }, '-'),
|
||||||
|
E('td', { 'class': 'td left' }, _('Tracking Area Code')),
|
||||||
|
E('td', { 'class': 'td left', 'id': 'modem_cell_tac' }, '-')
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td left' }, _('Carrier Channel')),
|
||||||
|
E('td', { 'class': 'td left', 'id': 'modem_cell_arfcn' }, '-'),
|
||||||
|
E('td', { 'class': 'td left' }, _('Signal Strength CSQ')),
|
||||||
|
E('td', { 'class': 'td left', 'id': 'modem_sig_csq' }, '-')
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td left' }, _('Signal Power RSRP')),
|
||||||
|
E('td', { 'class': 'td left', 'id': 'modem_sig_rsrp' }, '-'),
|
||||||
|
E('td', { 'class': 'td left' }, _('Signal Quality SINR')),
|
||||||
|
E('td', { 'class': 'td left', 'id': 'modem_sig_sinr' }, '-')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
container.appendChild(sigSection);
|
||||||
|
|
||||||
|
const connSection = E('fieldset', { 'class': 'cbi-section' }, [
|
||||||
|
E('legend', {}, _('Network Interface Connection')),
|
||||||
|
E('table', { 'class': 'table cbi-section-table' }, [
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td left', 'width': '16%' }, _('Connection State')),
|
||||||
|
E('td', { 'class': 'td left', 'width': '34%', 'id': 'modem_net_status' }, '-'),
|
||||||
|
E('td', { 'class': 'td left', 'width': '16%' }, _('Physical & Logical Interface')),
|
||||||
|
E('td', { 'class': 'td left', 'width': '34%' }, [
|
||||||
|
E('div', {}, [
|
||||||
|
E('span', { 'style': 'color: #333; font-weight: bold; margin-right: 4px;' }, _('Physical:')),
|
||||||
|
E('span', { 'id': 'modem_net_phys' }, '-'),
|
||||||
|
E('span', { 'style': 'color: #888; margin: 0 8px;' }, '|'),
|
||||||
|
E('span', { 'style': 'color: #333; font-weight: bold; margin-right: 4px;' }, _('Logical:')),
|
||||||
|
E('span', { 'id': 'modem_net_iface' }, '-')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td left' }, _('Modem IPv4 Address')),
|
||||||
|
E('td', { 'class': 'td left', 'id': 'modem_net_ip4' }, '-'),
|
||||||
|
E('td', { 'class': 'td left' }, _('Modem IPv6 Address')),
|
||||||
|
E('td', { 'class': 'td left', 'id': 'modem_net_ip6' }, '-')
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td left' }, _('Traffic Statistics')),
|
||||||
|
E('td', { 'class': 'td left', 'colspan': '3', 'id': 'modem_net_traffic' }, '-')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'margin-top: 15px; display: flex; gap: 10px;' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'class': 'btn cbi-button cbi-button-reload',
|
||||||
|
'value': _('Refresh'),
|
||||||
|
'click': ui.createHandlerFn(self, 'forceRefresh')
|
||||||
|
}),
|
||||||
|
E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'class': 'btn cbi-button cbi-button-reset',
|
||||||
|
'value': _('Restart Modem'),
|
||||||
|
'click': ui.createHandlerFn(self, 'handleAction', 'reboot')
|
||||||
|
}),
|
||||||
|
E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'class': 'btn cbi-button cbi-button-action',
|
||||||
|
'value': _('Toggle Airplane Mode'),
|
||||||
|
'click': ui.createHandlerFn(self, 'handleAction', 'airplane_on')
|
||||||
|
}),
|
||||||
|
E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'class': 'btn cbi-button cbi-button-reload',
|
||||||
|
'value': _('Reset RF Radio'),
|
||||||
|
'click': ui.createHandlerFn(self, 'handleAction', 'reset_radio')
|
||||||
|
})
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
container.appendChild(connSection);
|
||||||
|
|
||||||
|
poll.add(function() {
|
||||||
|
return self.updateStatus();
|
||||||
|
}, 5);
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
self.updateStatus();
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,565 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require rpc';
|
||||||
|
'require ui';
|
||||||
|
|
||||||
|
const callGetDevices = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'get_devices',
|
||||||
|
expect: { devices: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
const callGetConfig = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'get_config',
|
||||||
|
expect: { port: '', baud: '115200' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const callSaveConfig = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'save_config',
|
||||||
|
params: [ 'port', 'baud' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
const callSetMode = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'set_mode',
|
||||||
|
params: [ 'port', 'mode' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
const callSetPreferences = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'set_preferences',
|
||||||
|
params: [ 'port', 'rat', 'bands_lte', 'bands_nr5g' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
const callSetApn = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'set_apn',
|
||||||
|
params: [ 'port', 'apn', 'pdp_type' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
const callGetApn = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'get_apn',
|
||||||
|
params: [ 'port' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
const callGetCellLock = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'get_cell_lock',
|
||||||
|
params: [ 'port' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
const callSetCellLock = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'set_cell_lock',
|
||||||
|
params: [ 'port', 'mode', 'earfcn', 'pci' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
const callGetStatus = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'get_status',
|
||||||
|
params: [ 'port', 'primary_net' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null,
|
||||||
|
|
||||||
|
selectedPort: null,
|
||||||
|
selectedNet: null,
|
||||||
|
devicesList: [],
|
||||||
|
configData: {},
|
||||||
|
cellLockData: {},
|
||||||
|
apnData: {},
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
const self = this;
|
||||||
|
return Promise.all([
|
||||||
|
L.resolveDefault(callGetDevices(), []),
|
||||||
|
L.resolveDefault(callGetConfig(), {})
|
||||||
|
]).then(function(results) {
|
||||||
|
const devs = results[0];
|
||||||
|
const cfg = results[1] || {};
|
||||||
|
const list = Array.isArray(devs) ? devs : ((devs && Array.isArray(devs.devices)) ? devs.devices : []);
|
||||||
|
const port = cfg.port || (list.length > 0 ? list[0].primary_port : null);
|
||||||
|
|
||||||
|
if (port) {
|
||||||
|
return Promise.all([
|
||||||
|
L.resolveDefault(callGetCellLock(port), {}),
|
||||||
|
L.resolveDefault(callGetApn(port), {})
|
||||||
|
]).then(function(extraResults) {
|
||||||
|
return [ devs, cfg, extraResults[0] || {}, extraResults[1] || {} ];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [ devs, cfg, {}, {} ];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleApplySettings: function() {
|
||||||
|
const self = this;
|
||||||
|
const portSelect = document.getElementById('modem_port_select');
|
||||||
|
const customPortInput = document.getElementById('modem_custom_port');
|
||||||
|
let targetPort = (portSelect && portSelect.value === 'custom') ? (customPortInput ? customPortInput.value.trim() : '') : (portSelect ? portSelect.value : '');
|
||||||
|
|
||||||
|
if (!targetPort) {
|
||||||
|
targetPort = self.selectedPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetPort) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Please specify or select an active AT communication port.')), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baudVal = document.getElementById('modem_baud_select').value || '115200';
|
||||||
|
|
||||||
|
const modeEl = document.querySelector('input[name="modem_mode"]:checked');
|
||||||
|
const modeVal = modeEl ? modeEl.value : 'ncm';
|
||||||
|
|
||||||
|
const ratEl = document.getElementById('modem_rat_select');
|
||||||
|
const ratVal = ratEl ? ratEl.value : 'AUTO';
|
||||||
|
|
||||||
|
const apnEl = document.getElementById('modem_apn');
|
||||||
|
const apnVal = apnEl ? apnEl.value.trim() : 'cmiot';
|
||||||
|
const pdpEl = document.getElementById('modem_pdp');
|
||||||
|
const pdpVal = pdpEl ? pdpEl.value : 'IPV4V6';
|
||||||
|
|
||||||
|
const lteEls = document.querySelectorAll('.modem-band-lte:checked');
|
||||||
|
const nrEls = document.querySelectorAll('.modem-band-nr:checked');
|
||||||
|
const bandsLte = [];
|
||||||
|
const bandsNr = [];
|
||||||
|
for (let i = 0; i < lteEls.length; i++) bandsLte.push(lteEls[i].value);
|
||||||
|
for (let i = 0; i < nrEls.length; i++) bandsNr.push(nrEls[i].value);
|
||||||
|
|
||||||
|
const cellModeEl = document.getElementById('modem_cell_lock_mode');
|
||||||
|
const cellLockMode = cellModeEl ? cellModeEl.value : 'none';
|
||||||
|
const cellEarfcnEl = document.getElementById('modem_cell_lock_earfcn');
|
||||||
|
const cellLockEarfcn = cellEarfcnEl ? cellEarfcnEl.value.trim() : '';
|
||||||
|
const cellPciEl = document.getElementById('modem_cell_lock_pci');
|
||||||
|
const cellLockPci = cellPciEl ? cellPciEl.value.trim() : '';
|
||||||
|
|
||||||
|
if (cellLockMode !== 'none') {
|
||||||
|
if (!cellLockEarfcn || !cellLockPci) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Both carrier frequency and physical cell ID are required for cell locking.')), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!/^[0-9]+$/.test(cellLockEarfcn) || !/^[0-9]+$/.test(cellLockPci)) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Carrier frequency and physical cell ID must be valid positive integers.')), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.showModal(_('Applying Configuration'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Saving port settings and sending configuration commands, please wait...'))
|
||||||
|
]);
|
||||||
|
|
||||||
|
callSaveConfig(targetPort, baudVal)
|
||||||
|
.then(function() {
|
||||||
|
return callSetMode(targetPort, modeVal);
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
return callSetPreferences(targetPort, ratVal, bandsLte, bandsNr);
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
return callSetApn(targetPort, apnVal, pdpVal);
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
return callSetCellLock(targetPort, cellLockMode, cellLockEarfcn, cellLockPci);
|
||||||
|
})
|
||||||
|
.then(function(lockRes) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (lockRes && lockRes.success === false) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Settings saved, but cell lock failed: ') + (lockRes.error || lockRes.output || '')), 'warning');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Modem settings successfully applied.')), 'info');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', {}, _('Failed to apply settings: ') + err.message), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(loadData) {
|
||||||
|
const self = this;
|
||||||
|
const devicesData = loadData ? loadData[0] : null;
|
||||||
|
self.configData = loadData ? loadData[1] : {};
|
||||||
|
self.cellLockData = loadData ? loadData[2] : {};
|
||||||
|
self.apnData = loadData ? loadData[3] : {};
|
||||||
|
|
||||||
|
self.devicesList = Array.isArray(devicesData) ? devicesData : ((devicesData && Array.isArray(devicesData.devices)) ? devicesData.devices : []);
|
||||||
|
|
||||||
|
if (self.devicesList.length > 0) {
|
||||||
|
self.selectedPort = self.configData.port || self.devicesList[0].primary_port;
|
||||||
|
self.selectedNet = self.devicesList[0].primary_net;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceOptions = [];
|
||||||
|
self.devicesList.forEach(function(dev, idx) {
|
||||||
|
let label = dev.vendor + ' ' + dev.model + ' (' + (dev.primary_port || _('No Port')) + ')';
|
||||||
|
const optAttrs = { value: dev.primary_port, 'data-net': dev.primary_net };
|
||||||
|
if (idx === 0) optAttrs.selected = '';
|
||||||
|
deviceOptions.push(E('option', optAttrs, label));
|
||||||
|
});
|
||||||
|
|
||||||
|
const container = E('div', { 'class': 'cbi-map' }, [
|
||||||
|
E('h2', {}, _('Cellular Modem Manager') + ' - ' + _('Settings')),
|
||||||
|
E('div', { 'class': 'cbi-map-descr' }, _('Configure driver protocols, lock preferred access technologies, customize APN credentials, and manage cellular network interfaces.'))
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (self.devicesList.length === 0) {
|
||||||
|
container.appendChild(E('div', { 'class': 'alert-message warning', 'style': 'margin-top: 15px; padding: 15px; border-left: 5px solid #ff9800; background: #fff8e1;' }, [
|
||||||
|
E('h4', { 'style': 'margin: 0 0 8px 0; color: #d32f2f;' }, _('No Cellular Modem Detected')),
|
||||||
|
E('p', { 'style': 'margin: 0; color: #555;' }, _('No active cellular modem detected. You can still pre-configure serial port path or refresh after connecting.'))
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Target Modem Selector
|
||||||
|
const selectorNode = E('fieldset', { 'class': 'cbi-section' }, [
|
||||||
|
E('legend', {}, _('Target Modem Selection')),
|
||||||
|
E('div', { 'class': 'cbi-section-node' }, [
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Target Modem')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
deviceOptions.length > 0 ? E('select', {
|
||||||
|
'id': 'modem_device_select',
|
||||||
|
'class': 'cbi-input-select',
|
||||||
|
'style': 'min-width: 320px;',
|
||||||
|
'change': function(ev) {
|
||||||
|
self.selectedPort = ev.target.value;
|
||||||
|
let opt = ev.target.options[ev.target.selectedIndex];
|
||||||
|
self.selectedNet = opt ? opt.getAttribute('data-net') : null;
|
||||||
|
}
|
||||||
|
}, deviceOptions) : E('span', { 'style': 'color: red;' }, _('No cellular modems detected.')),
|
||||||
|
E('div', { 'class': 'cbi-value-description' }, _('Select the active cellular module to configure.'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
container.appendChild(selectorNode);
|
||||||
|
|
||||||
|
// Serial Port Customization Section
|
||||||
|
const dev = self.devicesList[0] || {};
|
||||||
|
const availablePorts = dev.ports || [];
|
||||||
|
const activeConfigPort = self.configData.port || dev.primary_port || '';
|
||||||
|
|
||||||
|
const portSelectOptions = [];
|
||||||
|
let matchedInList = false;
|
||||||
|
|
||||||
|
availablePorts.forEach(function(p) {
|
||||||
|
let isSelected = (p === activeConfigPort);
|
||||||
|
if (isSelected) matchedInList = true;
|
||||||
|
let isAuto = (p === dev.primary_port);
|
||||||
|
let optLabel = p + (isAuto ? ' [' + _('Auto Recommended') + ']' : '');
|
||||||
|
const optAttrs = { value: p };
|
||||||
|
if (isSelected) optAttrs.selected = '';
|
||||||
|
portSelectOptions.push(E('option', optAttrs, optLabel));
|
||||||
|
});
|
||||||
|
|
||||||
|
const customOptAttrs = { value: 'custom' };
|
||||||
|
if (!matchedInList && activeConfigPort !== '') customOptAttrs.selected = '';
|
||||||
|
portSelectOptions.push(E('option', customOptAttrs, _('Manual Custom Path')));
|
||||||
|
|
||||||
|
const portSection = E('fieldset', { 'class': 'cbi-section' }, [
|
||||||
|
E('legend', {}, _('Serial Port and Communication Configuration')),
|
||||||
|
E('div', { 'class': 'cbi-section-node' }, [
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Primary AT Port')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('select', {
|
||||||
|
'id': 'modem_port_select',
|
||||||
|
'class': 'cbi-input-select',
|
||||||
|
'style': 'min-width: 280px;',
|
||||||
|
'change': function(ev) {
|
||||||
|
const customWrap = document.getElementById('modem_custom_port_wrap');
|
||||||
|
if (customWrap) {
|
||||||
|
customWrap.style.display = (ev.target.value === 'custom') ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, portSelectOptions),
|
||||||
|
E('div', {
|
||||||
|
'id': 'modem_custom_port_wrap',
|
||||||
|
'style': 'margin-top: 8px; display: ' + ((!matchedInList && activeConfigPort !== '') ? 'block' : 'none') + ';'
|
||||||
|
}, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'text',
|
||||||
|
'id': 'modem_custom_port',
|
||||||
|
'class': 'cbi-input-text',
|
||||||
|
'style': 'min-width: 280px;',
|
||||||
|
'placeholder': '/dev/ttyUSB2 or /dev/modem_at',
|
||||||
|
'value': (!matchedInList ? activeConfigPort : '')
|
||||||
|
}),
|
||||||
|
E('span', { 'style': 'margin-left: 10px; color: #888; font-size: 12px;' }, _('Specify custom node path'))
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value-description' }, _('Select or enter the dedicated AT control port for background status polling and control commands.'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Serial Baudrate')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('select', { 'id': 'modem_baud_select', 'class': 'cbi-input-select', 'style': 'min-width: 150px;' },
|
||||||
|
['9600', '38400', '57600', '115200', '230400', '460800', '921600'].map(function(b) {
|
||||||
|
const opt = { value: b };
|
||||||
|
if (b === (self.configData.baud || '115200')) opt.selected = '';
|
||||||
|
return E('option', opt, b);
|
||||||
|
})
|
||||||
|
),
|
||||||
|
E('div', { 'class': 'cbi-value-description' }, _('Standard cellular modem AT serial baudrate (Default: 115200).'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
container.appendChild(portSection);
|
||||||
|
|
||||||
|
// Protocol Mode Switching Section
|
||||||
|
const modeSection = E('fieldset', { 'class': 'cbi-section' }, [
|
||||||
|
E('legend', {}, _('Driver Protocol and Operation Mode')),
|
||||||
|
E('div', { 'class': 'cbi-section-node' }, [
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Driver Mode')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('label', { 'style': 'margin-right: 20px;' }, [
|
||||||
|
E('input', { 'type': 'radio', 'name': 'modem_mode', 'value': 'ncm', 'checked': 'checked' }), ' ',
|
||||||
|
_('NCM Mode')
|
||||||
|
]),
|
||||||
|
E('label', { 'style': 'margin-right: 20px;' }, [
|
||||||
|
E('input', { 'type': 'radio', 'name': 'modem_mode', 'value': 'mbim' }), ' ',
|
||||||
|
_('MBIM Mode')
|
||||||
|
]),
|
||||||
|
E('label', { 'style': 'margin-right: 20px;' }, [
|
||||||
|
E('input', { 'type': 'radio', 'name': 'modem_mode', 'value': 'qmi' }), ' ',
|
||||||
|
_('QMI Mode')
|
||||||
|
]),
|
||||||
|
E('label', {}, [
|
||||||
|
E('input', { 'type': 'radio', 'name': 'modem_mode', 'value': 'ecm' }), ' ',
|
||||||
|
_('ECM Mode')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value-description' }, _('Switching the operation mode requires restarting the modem to take effect.'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
container.appendChild(modeSection);
|
||||||
|
|
||||||
|
// Cell Locking Section
|
||||||
|
const isCellLocked = !!(self.cellLockData && self.cellLockData.locked);
|
||||||
|
const cellLockType = isCellLocked ? (self.cellLockData.mode || 'nr5g') : 'none';
|
||||||
|
|
||||||
|
const cellLockSection = E('fieldset', { 'class': 'cbi-section' }, [
|
||||||
|
E('legend', {}, _('Base Station Cell Locking')),
|
||||||
|
E('div', { 'class': 'cbi-section-node' }, [
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Cell Lock Mode')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('select', {
|
||||||
|
'id': 'modem_cell_lock_mode',
|
||||||
|
'class': 'cbi-input-select',
|
||||||
|
'style': 'min-width: 240px;',
|
||||||
|
'change': function(ev) {
|
||||||
|
const val = ev.target.value;
|
||||||
|
const inputsWrap = document.getElementById('modem_cell_lock_inputs');
|
||||||
|
if (inputsWrap) {
|
||||||
|
inputsWrap.style.display = (val === 'none') ? 'none' : 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
E('option', { 'value': 'none', 'selected': (cellLockType === 'none' ? '' : null) }, _('Automatic Selection')),
|
||||||
|
E('option', { 'value': 'nr5g', 'selected': (cellLockType === 'nr5g' ? '' : null) }, _('Lock 5G NR Cell')),
|
||||||
|
E('option', { 'value': 'lte', 'selected': (cellLockType === 'lte' ? '' : null) }, _('Lock 4G LTE Cell'))
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value-description' }, _('Lock cellular module to a specific base station cell ID and carrier frequency.'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'id': 'modem_cell_lock_inputs', 'style': 'display: ' + (isCellLocked ? 'block' : 'none') + ';' }, [
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Carrier Frequency')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'text',
|
||||||
|
'id': 'modem_cell_lock_earfcn',
|
||||||
|
'class': 'cbi-input-text',
|
||||||
|
'style': 'min-width: 240px;',
|
||||||
|
'placeholder': _('e.g. 627264 or 1850'),
|
||||||
|
'value': (self.cellLockData.earfcn || '')
|
||||||
|
}),
|
||||||
|
E('div', { 'class': 'cbi-value-description' }, _('Target carrier ARFCN or EARFCN.'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Physical Cell ID')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'text',
|
||||||
|
'id': 'modem_cell_lock_pci',
|
||||||
|
'class': 'cbi-input-text',
|
||||||
|
'style': 'min-width: 240px;',
|
||||||
|
'placeholder': _('e.g. 468'),
|
||||||
|
'value': (self.cellLockData.pci || '')
|
||||||
|
}),
|
||||||
|
E('div', { 'class': 'cbi-value-description' }, _('Target base station physical cell identifier PCI.'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Lock Actions')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'class': 'btn cbi-button cbi-button-action',
|
||||||
|
'value': _('Autofill Current Serving Cell'),
|
||||||
|
'click': function() {
|
||||||
|
if (!self.selectedPort) return;
|
||||||
|
ui.showIndicator('modem_query', _('Querying serving cell...'));
|
||||||
|
callGetStatus(self.selectedPort, self.selectedNet).then(function(st) {
|
||||||
|
ui.hideIndicator('modem_query');
|
||||||
|
if (st && st.cell && st.cell.earfcn && st.cell.pci) {
|
||||||
|
const cell = st.cell;
|
||||||
|
const earfcnInput = document.getElementById('modem_cell_lock_earfcn');
|
||||||
|
const pciInput = document.getElementById('modem_cell_lock_pci');
|
||||||
|
const modeSelect = document.getElementById('modem_cell_lock_mode');
|
||||||
|
if (earfcnInput) earfcnInput.value = cell.earfcn;
|
||||||
|
if (pciInput) pciInput.value = cell.pci;
|
||||||
|
if (modeSelect) {
|
||||||
|
modeSelect.value = cell.is_5g ? 'nr5g' : 'lte';
|
||||||
|
const inputsWrap = document.getElementById('modem_cell_lock_inputs');
|
||||||
|
if (inputsWrap) inputsWrap.style.display = 'block';
|
||||||
|
}
|
||||||
|
ui.addNotification(null, E('p', {}, _('Current cell values populated.')), 'info');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, _('No active serving cell details detected.')), 'warning');
|
||||||
|
}
|
||||||
|
}).catch(function(e) {
|
||||||
|
ui.hideIndicator('modem_query');
|
||||||
|
ui.addNotification(null, E('p', {}, e.message), 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'class': 'btn cbi-button cbi-button-reset',
|
||||||
|
'style': 'margin-left: 10px;',
|
||||||
|
'value': _('Clear Lock Immediately'),
|
||||||
|
'click': function() {
|
||||||
|
if (!self.selectedPort) return;
|
||||||
|
ui.showModal(_('Clearing Cell Lock'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Sending unlocking commands...'))
|
||||||
|
]);
|
||||||
|
callSetCellLock(self.selectedPort, 'none').then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
const modeSelect = document.getElementById('modem_cell_lock_mode');
|
||||||
|
if (modeSelect) modeSelect.value = 'none';
|
||||||
|
const inputsWrap = document.getElementById('modem_cell_lock_inputs');
|
||||||
|
if (inputsWrap) inputsWrap.style.display = 'none';
|
||||||
|
ui.addNotification(null, E('p', {}, _('Cell lock cleared successfully.')), 'info');
|
||||||
|
}).catch(function(e) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', {}, e.message), 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
container.appendChild(cellLockSection);
|
||||||
|
|
||||||
|
// Network RAT and Band Locking Section
|
||||||
|
const ratSection = E('fieldset', { 'class': 'cbi-section' }, [
|
||||||
|
E('legend', {}, _('Network Access Technology and Band Locking')),
|
||||||
|
E('div', { 'class': 'cbi-section-node' }, [
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Access Technology Preference')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('select', { 'id': 'modem_rat_select', 'class': 'cbi-input-select' }, [
|
||||||
|
E('option', { 'value': 'AUTO', 'selected': 'selected' }, _('Automatic Mode')),
|
||||||
|
E('option', { 'value': 'NR5G' }, _('5G Standalone Only')),
|
||||||
|
E('option', { 'value': 'NR5G:LTE' }, _('5G and 4G Hybrid')),
|
||||||
|
E('option', { 'value': 'LTE' }, _('4G LTE Only'))
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value-description' }, _('Select priority radio access technology for network registration.'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('4G LTE Bands')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 10px; padding-top: 5px;' },
|
||||||
|
['B1', 'B3', 'B5', 'B8', 'B34', 'B38', 'B39', 'B40', 'B41'].map(function(band) {
|
||||||
|
return E('label', {
|
||||||
|
'style': 'display: inline-flex; align-items: center; gap: 4px; cursor: pointer; min-width: 60px;'
|
||||||
|
}, [
|
||||||
|
E('input', { 'type': 'checkbox', 'class': 'cbi-input-checkbox modem-band-lte', 'value': band, 'checked': 'checked' }),
|
||||||
|
E('span', {}, band)
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('5G NR Bands')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 10px; padding-top: 5px;' },
|
||||||
|
['N1', 'N28', 'N41', 'N77', 'N78', 'N79'].map(function(band) {
|
||||||
|
return E('label', {
|
||||||
|
'style': 'display: inline-flex; align-items: center; gap: 4px; cursor: pointer; min-width: 60px;'
|
||||||
|
}, [
|
||||||
|
E('input', { 'type': 'checkbox', 'class': 'cbi-input-checkbox modem-band-nr', 'value': band, 'checked': 'checked' }),
|
||||||
|
E('span', {}, band)
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
container.appendChild(ratSection);
|
||||||
|
|
||||||
|
// APN Section
|
||||||
|
const activeApn = (self.apnData && self.apnData.apn) ? self.apnData.apn : '';
|
||||||
|
const activePdp = (self.apnData && self.apnData.pdp_type) ? self.apnData.pdp_type : 'IPV4V6';
|
||||||
|
|
||||||
|
const apnSection = E('fieldset', { 'class': 'cbi-section' }, [
|
||||||
|
E('legend', {}, _('Access Point Name (APN) Settings')),
|
||||||
|
E('div', { 'class': 'cbi-section-node' }, [
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('APN Name')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'text',
|
||||||
|
'id': 'modem_apn',
|
||||||
|
'class': 'cbi-input-text',
|
||||||
|
'placeholder': _('e.g. ctnet, cmiot, uninet'),
|
||||||
|
'value': activeApn
|
||||||
|
}),
|
||||||
|
E('div', { 'class': 'cbi-value-description' }, _('Cellular carrier APN profile name (e.g. cmiot, cbnet, uninet).'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('PDP Protocol Type')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('select', { 'id': 'modem_pdp', 'class': 'cbi-input-select' }, [
|
||||||
|
E('option', { 'value': 'IPV4V6', 'selected': (activePdp === 'IPV4V6' ? 'selected' : null) }, _('Dual Stack IPv4 and IPv6')),
|
||||||
|
E('option', { 'value': 'IP', 'selected': (activePdp === 'IP' ? 'selected' : null) }, _('IPv4 Only')),
|
||||||
|
E('option', { 'value': 'IPV6', 'selected': (activePdp === 'IPV6' ? 'selected' : null) }, _('IPv6 Only'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
container.appendChild(apnSection);
|
||||||
|
|
||||||
|
// Submit Section
|
||||||
|
const btnSection = E('div', { 'class': 'cbi-page-actions' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'class': 'btn cbi-button cbi-button-save',
|
||||||
|
'value': _('Save and Apply'),
|
||||||
|
'click': ui.createHandlerFn(self, 'handleApplySettings')
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
container.appendChild(btnSection);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,503 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require rpc';
|
||||||
|
'require ui';
|
||||||
|
|
||||||
|
const callGetDevices = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'get_devices',
|
||||||
|
expect: { devices: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
const callGetConfig = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'get_config',
|
||||||
|
expect: { port: '', baud: '115200' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const callGetSmsStatus = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'get_sms_status',
|
||||||
|
params: [ 'port' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
const callSetSmsEnable = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'set_sms_enable',
|
||||||
|
params: [ 'enabled', 'port' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
const callGetSmsList = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'get_sms_list',
|
||||||
|
params: [ 'port' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
const callSendSms = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'send_sms',
|
||||||
|
params: [ 'port', 'recipient', 'message' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
const callDeleteSms = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'delete_sms',
|
||||||
|
params: [ 'port', 'index' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null,
|
||||||
|
|
||||||
|
selectedPort: null,
|
||||||
|
devicesList: [],
|
||||||
|
smsEnabled: false,
|
||||||
|
smsCapacity: { used: 0, total: 0 },
|
||||||
|
smsList: [],
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
const self = this;
|
||||||
|
return Promise.all([
|
||||||
|
L.resolveDefault(callGetDevices(), []),
|
||||||
|
L.resolveDefault(callGetConfig(), {})
|
||||||
|
]).then(function(results) {
|
||||||
|
const devicesData = results[0];
|
||||||
|
const configData = results[1] || {};
|
||||||
|
let list = [];
|
||||||
|
if (Array.isArray(devicesData)) {
|
||||||
|
list = devicesData;
|
||||||
|
} else if (devicesData && Array.isArray(devicesData.devices)) {
|
||||||
|
list = devicesData.devices;
|
||||||
|
}
|
||||||
|
self.devicesList = list;
|
||||||
|
|
||||||
|
if (self.devicesList.length > 0) {
|
||||||
|
self.selectedPort = configData.port || self.devicesList[0].primary_port;
|
||||||
|
} else {
|
||||||
|
self.selectedPort = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self.selectedPort) {
|
||||||
|
self.smsEnabled = false;
|
||||||
|
self.smsCapacity = { used: 0, total: 0 };
|
||||||
|
self.smsList = [];
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return callGetSmsStatus(self.selectedPort).then(function(statusRes) {
|
||||||
|
self.smsEnabled = !!(statusRes && statusRes.enabled);
|
||||||
|
self.smsCapacity = {
|
||||||
|
used: statusRes?.used || 0,
|
||||||
|
total: statusRes?.total || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
if (self.smsEnabled) {
|
||||||
|
return callGetSmsList(self.selectedPort).then(function(listRes) {
|
||||||
|
self.smsList = (listRes && listRes.messages) ? listRes.messages : [];
|
||||||
|
}).catch(function() {
|
||||||
|
self.smsList = [];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.smsList = [];
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}).catch(function() {
|
||||||
|
self.smsEnabled = false;
|
||||||
|
self.smsCapacity = { used: 0, total: 0 };
|
||||||
|
self.smsList = [];
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
}).catch(function() {
|
||||||
|
self.devicesList = [];
|
||||||
|
self.selectedPort = null;
|
||||||
|
self.smsEnabled = false;
|
||||||
|
self.smsCapacity = { used: 0, total: 0 };
|
||||||
|
self.smsList = [];
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderSmsState: function() {
|
||||||
|
const self = this;
|
||||||
|
const mainArea = document.getElementById('sms_main_content');
|
||||||
|
if (!mainArea) return;
|
||||||
|
|
||||||
|
while (mainArea.firstChild) {
|
||||||
|
mainArea.removeChild(mainArea.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self.smsEnabled) {
|
||||||
|
// Disabled state: safety notice card
|
||||||
|
const disabledCard = E('div', {
|
||||||
|
'style': 'background: #fff; border: 1px solid #e1e4e8; border-radius: 6px; padding: 30px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.05); margin-top: 15px;'
|
||||||
|
}, [
|
||||||
|
E('div', { 'style': 'font-size: 18px; font-weight: bold; color: #333; margin-bottom: 12px;' }, _('SMS Feature is Currently Disabled')),
|
||||||
|
E('div', {
|
||||||
|
'style': 'max-width: 680px; margin: 0 auto 24px auto; color: #555; line-height: 1.6; font-size: 14px;'
|
||||||
|
}, _('To prevent IoT SIM cards, enterprise telemetry cards, or data-only SIM cards from unexpected billing, network carrier account suspensions, or serial AT buffer deadlocks, the SMS module is disabled by default. In this mode, no background polling or SMS commands will ever touch the cellular modem.')),
|
||||||
|
E('div', { 'style': 'display: flex; justify-content: center; gap: 15px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-action',
|
||||||
|
'style': 'min-width: 180px; font-weight: bold;',
|
||||||
|
'click': function() {
|
||||||
|
self.handleToggleSms(true);
|
||||||
|
}
|
||||||
|
}, _('Enable SMS Feature'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
mainArea.appendChild(disabledCard);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enabled state
|
||||||
|
const toolbar = E('div', {
|
||||||
|
'style': 'display: flex; justify-content: space-between; align-items: center; background: #fff; border: 1px solid #e1e4e8; border-radius: 6px; padding: 15px 20px; margin-top: 15px; box-shadow: 0 1px 3px rgba(0,0,0,0.05);'
|
||||||
|
}, [
|
||||||
|
E('div', { 'style': 'display: flex; align-items: center; gap: 20px;' }, [
|
||||||
|
E('div', {}, [
|
||||||
|
E('span', { 'style': 'color: #333; font-size: 13px;' }, _('SIM Card Storage Capacity: ')),
|
||||||
|
E('strong', { 'style': 'color: #333; font-size: 14px;' },
|
||||||
|
self.smsCapacity.total > 0 ? (self.smsCapacity.used + ' / ' + self.smsCapacity.total + ' ' + _('Messages')) : (self.smsList.length + ' ' + _('Messages')))
|
||||||
|
]),
|
||||||
|
E('span', {
|
||||||
|
'style': 'background: #e6ffed; color: #22863a; border: 1px solid #bef5cb; padding: 2px 8px; border-radius: 12px; font-size: 12px;'
|
||||||
|
}, _('Module Active'))
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'display: flex; gap: 10px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-save',
|
||||||
|
'click': function() {
|
||||||
|
self.handleOpenSendModal();
|
||||||
|
}
|
||||||
|
}, _('Compose Message')),
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-action',
|
||||||
|
'click': function() {
|
||||||
|
self.handleRefreshList();
|
||||||
|
}
|
||||||
|
}, _('Refresh List')),
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-reset',
|
||||||
|
'click': function() {
|
||||||
|
self.handleClearAll();
|
||||||
|
}
|
||||||
|
}, _('Clear All')),
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-neutral',
|
||||||
|
'style': 'color: #d73a49;',
|
||||||
|
'click': function() {
|
||||||
|
self.handleToggleSms(false);
|
||||||
|
}
|
||||||
|
}, _('Disable Feature'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
mainArea.appendChild(toolbar);
|
||||||
|
|
||||||
|
// SMS Message List
|
||||||
|
const listContainer = E('div', { 'style': 'margin-top: 15px;' });
|
||||||
|
|
||||||
|
if (!self.smsList || self.smsList.length === 0) {
|
||||||
|
listContainer.appendChild(E('div', {
|
||||||
|
'style': 'background: #fff; border: 1px dashed #d1d5db; border-radius: 6px; padding: 40px; text-align: center; color: #888;'
|
||||||
|
}, _('Inbox is empty. No messages stored on SIM card.')));
|
||||||
|
} else {
|
||||||
|
self.smsList.forEach(function(item) {
|
||||||
|
const isUnread = (item.status && item.status.indexOf('UNREAD') !== -1);
|
||||||
|
const card = E('div', {
|
||||||
|
'style': 'background: #fff; border: 1px solid #e1e4e8; border-left: 4px solid ' + (isUnread ? '#0366d6' : '#28a745') + '; border-radius: 4px; padding: 15px 20px; margin-bottom: 12px; box-shadow: 0 1px 2px rgba(0,0,0,0.04);'
|
||||||
|
}, [
|
||||||
|
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;' }, [
|
||||||
|
E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [
|
||||||
|
E('strong', { 'style': 'font-size: 15px; color: #24292e;' }, item.sender || _('Unknown Sender')),
|
||||||
|
E('span', {
|
||||||
|
'style': 'font-size: 11px; padding: 1px 6px; border-radius: 10px; background: ' + (isUnread ? '#dbedff; color: #0366d6;' : '#f1f8ff; color: #586069;')
|
||||||
|
}, isUnread ? _('Unread') : _('Read')),
|
||||||
|
E('span', { 'style': 'font-size: 11px; color: #999;' }, '#' + item.index)
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'color: #888; font-size: 12px;' }, item.time || '')
|
||||||
|
]),
|
||||||
|
E('div', {
|
||||||
|
'style': 'color: #333; font-size: 14px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; margin: 10px 0; background: #fafbfc; padding: 10px 12px; border-radius: 4px;'
|
||||||
|
}, item.content || _('[Empty Message Body]')),
|
||||||
|
E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 10px; margin-top: 8px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-action',
|
||||||
|
'style': 'font-size: 12px;',
|
||||||
|
'click': function() {
|
||||||
|
self.handleOpenSendModal(item.sender);
|
||||||
|
}
|
||||||
|
}, _('Reply')),
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-reset',
|
||||||
|
'style': 'font-size: 12px;',
|
||||||
|
'click': function() {
|
||||||
|
self.handleDeleteSingle(item.index);
|
||||||
|
}
|
||||||
|
}, _('Delete'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
listContainer.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mainArea.appendChild(listContainer);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleToggleSms: function(enable) {
|
||||||
|
const self = this;
|
||||||
|
const actionTitle = enable ? _('Enable SMS Feature') : _('Disable SMS Feature');
|
||||||
|
const actionText = enable ?
|
||||||
|
_('Enabling SMS allows sending, receiving, and reading messages. Ensure your SIM card plan supports SMS traffic.') :
|
||||||
|
_('Disabling SMS stops all SMS-related interactions and returns the cellular modem to pure data safety mode.');
|
||||||
|
|
||||||
|
ui.showModal(actionTitle, [
|
||||||
|
E('p', {}, actionText),
|
||||||
|
E('div', { 'class': 'cbi-page-actions' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-neutral',
|
||||||
|
'click': ui.hideModal
|
||||||
|
}, _('Cancel')),
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button ' + (enable ? 'cbi-button-save' : 'cbi-button-reset'),
|
||||||
|
'click': function() {
|
||||||
|
ui.showModal(actionTitle, [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Updating configuration, please wait...'))
|
||||||
|
]);
|
||||||
|
callSetSmsEnable(!!enable, self.selectedPort).then(function(res) {
|
||||||
|
self.smsEnabled = !!(res && (res.enabled || res.success));
|
||||||
|
if (self.smsEnabled) {
|
||||||
|
return Promise.all([
|
||||||
|
callGetSmsStatus(self.selectedPort),
|
||||||
|
callGetSmsList(self.selectedPort)
|
||||||
|
]).then(function(datas) {
|
||||||
|
const st = datas[0] || {};
|
||||||
|
const ls = datas[1] || {};
|
||||||
|
self.smsCapacity = { used: st.used || 0, total: st.total || 0 };
|
||||||
|
self.smsList = (ls && Array.isArray(ls.messages)) ? ls.messages : [];
|
||||||
|
ui.hideModal();
|
||||||
|
self.renderSmsState();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.smsList = [];
|
||||||
|
self.smsCapacity = { used: 0, total: 0 };
|
||||||
|
ui.hideModal();
|
||||||
|
self.renderSmsState();
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', {}, _('Failed to toggle SMS state: ') + err.message), 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, _('Confirm'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleRefreshList: function() {
|
||||||
|
const self = this;
|
||||||
|
if (!self.selectedPort) {
|
||||||
|
self.renderSmsState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ui.showModal(_('Refreshing Messages'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Reading SMS inbox from SIM card, please wait...'))
|
||||||
|
]);
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
callGetSmsStatus(self.selectedPort),
|
||||||
|
callGetSmsList(self.selectedPort)
|
||||||
|
]).then(function(results) {
|
||||||
|
const st = results[0] || {};
|
||||||
|
const ls = results[1] || {};
|
||||||
|
self.smsCapacity = { used: st.used || 0, total: st.total || 0 };
|
||||||
|
self.smsList = (ls && Array.isArray(ls.messages)) ? ls.messages : [];
|
||||||
|
ui.hideModal();
|
||||||
|
self.renderSmsState();
|
||||||
|
ui.addNotification(null, E('p', {}, _('SMS list refreshed successfully.')), 'info');
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', {}, _('Failed to refresh SMS list: ') + err.message), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleOpenSendModal: function(prefillRecipient) {
|
||||||
|
const self = this;
|
||||||
|
const recipientInput = E('input', {
|
||||||
|
'type': 'text',
|
||||||
|
'class': 'cbi-input-text',
|
||||||
|
'style': 'width: 100%; box-sizing: border-box; padding: 7px 10px; font-family: monospace;',
|
||||||
|
'placeholder': _('Target phone number, e.g. 10086 or +8613800000000'),
|
||||||
|
'value': prefillRecipient || ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const charCountEl = E('span', { 'style': 'color: #888; font-size: 12px;' }, '0 ' + _('characters'));
|
||||||
|
|
||||||
|
const messageInput = E('textarea', {
|
||||||
|
'class': 'cbi-input-textarea',
|
||||||
|
'style': 'width: 100%; height: 130px; box-sizing: border-box; padding: 8px 10px; font-family: sans-serif; resize: vertical;',
|
||||||
|
'placeholder': _('Enter SMS message content...'),
|
||||||
|
'input': function(ev) {
|
||||||
|
charCountEl.textContent = ev.target.value.length + ' ' + _('characters');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const formContainer = E('div', {
|
||||||
|
'style': 'min-width: 520px; max-width: 100%; box-sizing: border-box; padding: 4px 0;'
|
||||||
|
}, [
|
||||||
|
E('div', { 'style': 'margin-bottom: 16px;' }, [
|
||||||
|
E('label', {
|
||||||
|
'style': 'display: block; font-weight: bold; margin-bottom: 6px; color: #333; font-size: 13px;'
|
||||||
|
}, _('Recipient Phone Number')),
|
||||||
|
recipientInput
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'margin-bottom: 16px;' }, [
|
||||||
|
E('div', {
|
||||||
|
'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;'
|
||||||
|
}, [
|
||||||
|
E('label', {
|
||||||
|
'style': 'display: block; font-weight: bold; color: #333; font-size: 13px;'
|
||||||
|
}, _('Message Content')),
|
||||||
|
charCountEl
|
||||||
|
]),
|
||||||
|
messageInput
|
||||||
|
]),
|
||||||
|
E('div', {
|
||||||
|
'style': 'display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; padding-top: 15px; border-top: 1px solid #e1e4e8;'
|
||||||
|
}, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-neutral',
|
||||||
|
'click': ui.hideModal
|
||||||
|
}, _('Cancel')),
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-save',
|
||||||
|
'click': function() {
|
||||||
|
const to = recipientInput.value.trim();
|
||||||
|
const text = messageInput.value.trim();
|
||||||
|
if (!to) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Please specify a valid recipient phone number.')), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!text) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Message content cannot be empty.')), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.showModal(_('Sending Message'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Transmitting SMS message over cellular radio, please wait...'))
|
||||||
|
]);
|
||||||
|
|
||||||
|
callSendSms(self.selectedPort, to, text).then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (res && res.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Message transmitted successfully.')), 'info');
|
||||||
|
self.handleRefreshList();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Failed to transmit message: ') + (res?.output || _('Unknown cellular error'))), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', {}, _('Failed to transmit message: ') + err.message), 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, _('Send'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
ui.showModal(_('Compose Message'), [ formContainer ]);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleDeleteSingle: function(index) {
|
||||||
|
const self = this;
|
||||||
|
ui.showModal(_('Delete Message'), [
|
||||||
|
E('p', {}, _('Are you sure you want to delete message #') + index + '?'),
|
||||||
|
E('div', { 'class': 'cbi-page-actions' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-neutral',
|
||||||
|
'click': ui.hideModal
|
||||||
|
}, _('Cancel')),
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-reset',
|
||||||
|
'click': function() {
|
||||||
|
ui.showModal(_('Deleting Message'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Deleting message, please wait...'))
|
||||||
|
]);
|
||||||
|
callDeleteSms(self.selectedPort, index).then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (res && res.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Message deleted successfully.')), 'info');
|
||||||
|
self.handleRefreshList();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Failed to delete message: ') + (res?.output || _('Unknown error'))), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', {}, _('Failed to delete message: ') + err.message), 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, _('Delete'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleClearAll: function() {
|
||||||
|
const self = this;
|
||||||
|
ui.showModal(_('Clear All Messages'), [
|
||||||
|
E('p', {}, _('Are you sure you want to clear all SMS messages stored on the SIM card? This action cannot be undone.')),
|
||||||
|
E('div', { 'class': 'cbi-page-actions' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-neutral',
|
||||||
|
'click': ui.hideModal
|
||||||
|
}, _('Cancel')),
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-reset',
|
||||||
|
'click': function() {
|
||||||
|
ui.showModal(_('Clearing Messages'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Clearing all SMS messages, please wait...'))
|
||||||
|
]);
|
||||||
|
callDeleteSms(self.selectedPort, 'all').then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (res && res.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('All SMS messages cleared successfully.')), 'info');
|
||||||
|
self.handleRefreshList();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Failed to clear messages: ') + (res?.output || _('Unknown error'))), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', {}, _('Failed to clear messages: ') + err.message), 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, _('Confirm Clear'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
const container = E('div', { 'class': 'cbi-map' }, [
|
||||||
|
E('h2', {}, _('Cellular Modem Manager') + ' - ' + _('SMS Management')),
|
||||||
|
E('div', { 'class': 'cbi-map-descr' }, _('Read, compose, and manage cellular SMS messages stored on the SIM card.'))
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (self.devicesList.length === 0) {
|
||||||
|
container.appendChild(E('div', { 'class': 'alert-message warning', 'style': 'margin-top: 15px; padding: 15px; border-left: 5px solid #ff9800; background: #fff8e1;' }, [
|
||||||
|
E('h4', { 'style': 'margin: 0 0 8px 0; color: #d32f2f;' }, _('No Cellular Modem Detected')),
|
||||||
|
E('p', { 'style': 'margin: 0; color: #555;' }, _('No active cellular modem detected. Unable to manage SMS messages. Please check physical connection.'))
|
||||||
|
]));
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic SMS content container
|
||||||
|
const mainArea = E('div', { 'id': 'sms_main_content' });
|
||||||
|
container.appendChild(mainArea);
|
||||||
|
|
||||||
|
// Initial render after DOM is mounted
|
||||||
|
setTimeout(function() {
|
||||||
|
self.renderSmsState();
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require rpc';
|
||||||
|
'require ui';
|
||||||
|
|
||||||
|
const callGetTemplates = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'get_templates',
|
||||||
|
expect: { templates: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null,
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
return L.resolveDefault(callGetTemplates(), []);
|
||||||
|
},
|
||||||
|
|
||||||
|
showTemplateDetail: function(tpl) {
|
||||||
|
ui.showModal(tpl.name || _('Template Detail'), [
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Template ID')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, tpl.id || '-')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Target Vendor')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, tpl.vendor || '-')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('USB VID')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, tpl.vid || '-')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Mode Switch Command')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, (tpl.mode_switch && tpl.mode_switch.cmd) ? tpl.mode_switch.cmd : _('Not Defined'))
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Raw Configuration')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('pre', {
|
||||||
|
'style': 'background: #f7f7f7; padding: 10px; border: 1px solid #ddd; max-height: 250px; overflow-y: auto; font-size: 12px; font-family: monospace;'
|
||||||
|
}, JSON.stringify(tpl, null, 2))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'right', 'style': 'margin-top: 15px;' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'class': 'btn cbi-button',
|
||||||
|
'value': _('Close'),
|
||||||
|
'click': ui.hideModal
|
||||||
|
})
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(tplData) {
|
||||||
|
const self = this;
|
||||||
|
const templates = Array.isArray(tplData) ? tplData : ((tplData && Array.isArray(tplData.templates)) ? tplData.templates : []);
|
||||||
|
|
||||||
|
const container = E('div', { 'class': 'cbi-map' }, [
|
||||||
|
E('h2', {}, _('Cellular Modem Manager') + ' - ' + _('Configuration Templates')),
|
||||||
|
E('div', { 'class': 'cbi-map-descr' }, _('Manage vendor preset command profiles, private AT mappings, band lock masks, and driver switching templates.'))
|
||||||
|
]);
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
templates.forEach(function(tpl) {
|
||||||
|
rows.push(E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td left', 'style': 'font-weight: bold;' }, tpl.name || tpl.id),
|
||||||
|
E('td', { 'class': 'td left' }, tpl.vendor || '-'),
|
||||||
|
E('td', { 'class': 'td left' }, tpl.vid || '*'),
|
||||||
|
E('td', { 'class': 'td left' }, (tpl.mode_switch && tpl.mode_switch.cmd) ? tpl.mode_switch.cmd : '-'),
|
||||||
|
E('td', { 'class': 'td right' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'class': 'btn cbi-button cbi-button-action',
|
||||||
|
'value': _('View Details'),
|
||||||
|
'click': ui.createHandlerFn(self, 'showTemplateDetail', tpl)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
rows.push(E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td center', 'colspan': '5' }, _('No templates installed.'))
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableRows = [
|
||||||
|
E('tr', { 'class': 'tr table-titles' }, [
|
||||||
|
E('th', { 'class': 'th left' }, _('Template Name')),
|
||||||
|
E('th', { 'class': 'th left' }, _('Vendor')),
|
||||||
|
E('th', { 'class': 'th left' }, _('Vendor ID')),
|
||||||
|
E('th', { 'class': 'th left' }, _('Mode Switching Command')),
|
||||||
|
E('th', { 'class': 'th right' }, _('Actions'))
|
||||||
|
])
|
||||||
|
].concat(rows);
|
||||||
|
|
||||||
|
const listSection = E('fieldset', { 'class': 'cbi-section' }, [
|
||||||
|
E('legend', {}, _('Preset Template Library')),
|
||||||
|
E('table', { 'class': 'table cbi-section-table' }, tableRows)
|
||||||
|
]);
|
||||||
|
container.appendChild(listSection);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,643 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require rpc';
|
||||||
|
'require ui';
|
||||||
|
|
||||||
|
const callGetDevices = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'get_devices',
|
||||||
|
expect: { devices: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
const callSendAt = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'send_at',
|
||||||
|
params: [ 'port', 'cmd', 'timeout' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Standard 3GPP and Vendor AT Command Knowledge Base
|
||||||
|
const AT_DICTIONARY = [
|
||||||
|
{ cmd: 'ATI', desc: _('Query module manufacturer, model, and firmware version') },
|
||||||
|
{ cmd: 'AT+CPIN?', desc: _('Query SIM card lock and ready status') },
|
||||||
|
{ cmd: 'AT+CSQ', desc: _('Query received signal strength and bit error rate') },
|
||||||
|
{ cmd: 'AT+CCID', desc: _('Query SIM card ICCID number') },
|
||||||
|
{ cmd: 'AT+CFUN?', desc: _('Query current RF radio state (0:Min, 1:Full, 4:Airplane)') },
|
||||||
|
{ cmd: 'AT+CFUN=1', desc: _('Enable full RF function (Turn off airplane mode)') },
|
||||||
|
{ cmd: 'AT+CFUN=4', desc: _('Turn on airplane mode') },
|
||||||
|
{ cmd: 'AT+CFUN=1,1', desc: _('Reboot module processor and RF reset') },
|
||||||
|
{ cmd: 'AT+COPS?', desc: _('Query registered cellular operator and RAT') },
|
||||||
|
{ cmd: 'AT+COPS=?', desc: _('Scan all available cellular networks in air (Long timeout)') },
|
||||||
|
{ cmd: 'AT+CREG?', desc: _('Query 2G/3G network registration status') },
|
||||||
|
{ cmd: 'AT+CEREG?', desc: _('Query 4G LTE network registration status') },
|
||||||
|
{ cmd: 'AT+C5GREG?', desc: _('Query 5G NR network registration status') },
|
||||||
|
{ cmd: 'AT+CGDCONT?', desc: _('Query configured PDP contexts and APN profiles') },
|
||||||
|
{ cmd: 'AT+CGDCONT=1,"IPV4V6","cmiot"', desc: _('Configure PDP context 1 with APN') },
|
||||||
|
{ cmd: 'AT+QENG="servingcell"', desc: _('Quectel: Query current serving cell details (5G SA/LTE, PCI, TAC, Band, RSRP, SINR)') },
|
||||||
|
{ cmd: 'AT+QCAINFO', desc: _('Quectel: Query carrier aggregation (CA) state and secondary cells') },
|
||||||
|
{ cmd: 'AT+QTEMP', desc: _('Quectel: Query CPU and RF chip temperatures') },
|
||||||
|
{ cmd: 'AT+QNWINFO', desc: _('Quectel: Query current network band and channel info') },
|
||||||
|
{ cmd: 'AT+GTCCINFO?', desc: _('Fibocom: Query current cell info and radio metrics') },
|
||||||
|
{ cmd: 'AT+GTCAINFO?', desc: _('Fibocom: Query carrier aggregation info') },
|
||||||
|
{ cmd: 'AT+GTRNDIS?', desc: _('Fibocom: Query RNDIS network connection status') }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 3GPP TS 27.007 CME / CMS Error Descriptions
|
||||||
|
const CME_ERRORS = {
|
||||||
|
'0': _('Phone failure'),
|
||||||
|
'1': _('No connection to phone'),
|
||||||
|
'2': _('Phone-adaptor link reserved'),
|
||||||
|
'3': _('Operation not allowed'),
|
||||||
|
'4': _('Operation not supported'),
|
||||||
|
'5': _('PH-SIM PIN required'),
|
||||||
|
'10': _('SIM card not inserted or not detected'),
|
||||||
|
'11': _('SIM PIN required (Please enter PIN to unlock)'),
|
||||||
|
'12': _('SIM PUK required (Please enter PUK to unlock)'),
|
||||||
|
'13': _('SIM card failure or communication defect'),
|
||||||
|
'14': _('SIM card busy, please retry later'),
|
||||||
|
'15': _('SIM wrong password'),
|
||||||
|
'16': _('Incorrect PIN entered'),
|
||||||
|
'17': _('PIN2 required'),
|
||||||
|
'18': _('PUK2 required'),
|
||||||
|
'20': _('Memory storage full'),
|
||||||
|
'21': _('Invalid index or memory entry not found'),
|
||||||
|
'22': _('Not found'),
|
||||||
|
'24': _('Text string too long'),
|
||||||
|
'25': _('Invalid characters in text string'),
|
||||||
|
'26': _('Dial string too long'),
|
||||||
|
'27': _('Invalid characters in dial string'),
|
||||||
|
'30': _('No cellular network service available'),
|
||||||
|
'31': _('Network timeout'),
|
||||||
|
'32': _('Network not allowed emergency calls only'),
|
||||||
|
'100': _('Unknown error or unsupported extended command')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Chinese & Full-width punctuation replacement mapping
|
||||||
|
function sanitizeAtPunctuation(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
let res = str;
|
||||||
|
// Replace full-width quotes
|
||||||
|
res = res.replace(/[“”]/g, '"').replace(/[‘’]/g, "'");
|
||||||
|
// Replace full-width punctuation
|
||||||
|
res = res.replace(/,/g, ',')
|
||||||
|
.replace(/?/g, '?')
|
||||||
|
.replace(/:/g, ':')
|
||||||
|
.replace(/;/g, ';')
|
||||||
|
.replace(/=/g, '=')
|
||||||
|
.replace(/+/g, '+')
|
||||||
|
.replace(/-/g, '-')
|
||||||
|
.replace(/(/g, '(')
|
||||||
|
.replace(/)/g, ')')
|
||||||
|
.replace(/\u3000/g, ' '); // full-width space
|
||||||
|
|
||||||
|
// Ensure uppercase prefix for 'at'
|
||||||
|
if (/^at([+=?&]|\s|$)/i.test(res)) {
|
||||||
|
res = 'AT' + res.slice(2);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Levenshtein distance for fuzzy suggestion
|
||||||
|
function levenshteinDistance(s1, s2) {
|
||||||
|
const a = s1.toUpperCase();
|
||||||
|
const b = s2.toUpperCase();
|
||||||
|
const matrix = [];
|
||||||
|
for (let i = 0; i <= b.length; i++) matrix[i] = [i];
|
||||||
|
for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
|
||||||
|
|
||||||
|
for (let i = 1; i <= b.length; i++) {
|
||||||
|
for (let j = 1; j <= a.length; j++) {
|
||||||
|
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
||||||
|
matrix[i][j] = matrix[i - 1][j - 1];
|
||||||
|
} else {
|
||||||
|
matrix[i][j] = Math.min(
|
||||||
|
matrix[i - 1][j - 1] + 1, // substitution
|
||||||
|
matrix[i][j - 1] + 1, // insertion
|
||||||
|
matrix[i - 1][j] + 1 // deletion
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matrix[b.length][a.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
function findBestSuggestion(inputCmd) {
|
||||||
|
if (!inputCmd || inputCmd.length < 3) return null;
|
||||||
|
const clean = inputCmd.trim().toUpperCase();
|
||||||
|
let bestMatch = null;
|
||||||
|
let lowestDist = 999;
|
||||||
|
|
||||||
|
for (let i = 0; i < AT_DICTIONARY.length; i++) {
|
||||||
|
const target = AT_DICTIONARY[i].cmd.toUpperCase();
|
||||||
|
const baseTarget = target.split('=')[0]; // base command prefix
|
||||||
|
const baseClean = clean.split('=')[0];
|
||||||
|
|
||||||
|
const dist = levenshteinDistance(baseClean, baseTarget);
|
||||||
|
if (dist > 0 && dist <= 2 && dist < lowestDist) {
|
||||||
|
lowestDist = dist;
|
||||||
|
bestMatch = AT_DICTIONARY[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null,
|
||||||
|
|
||||||
|
selectedPort: null,
|
||||||
|
devicesList: [],
|
||||||
|
cmdHistory: [],
|
||||||
|
historyIndex: -1,
|
||||||
|
favorites: [],
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
return L.resolveDefault(callGetDevices(), []);
|
||||||
|
},
|
||||||
|
|
||||||
|
loadFavorites: function() {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('modem_at_favorites');
|
||||||
|
if (saved) this.favorites = JSON.parse(saved);
|
||||||
|
} catch(e) {}
|
||||||
|
if (!Array.isArray(this.favorites) || this.favorites.length === 0) {
|
||||||
|
this.favorites = [
|
||||||
|
{ cmd: 'ATI', label: _('Module Info') },
|
||||||
|
{ cmd: 'AT+CSQ', label: _('Signal CSQ') },
|
||||||
|
{ cmd: 'AT+CPIN?', label: _('SIM Status') },
|
||||||
|
{ cmd: 'AT+COPS?', label: _('Operator') },
|
||||||
|
{ cmd: 'AT+QENG="servingcell"', label: _('Serving Cell') }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
saveFavorites: function() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('modem_at_favorites', JSON.stringify(this.favorites));
|
||||||
|
} catch(e) {}
|
||||||
|
},
|
||||||
|
|
||||||
|
appendLog: function(text, type, durationMs) {
|
||||||
|
const out = document.getElementById('terminal_output');
|
||||||
|
if (!out) return;
|
||||||
|
|
||||||
|
const d = new Date();
|
||||||
|
const timeStr = d.toTimeString().split(' ')[0] + '.' + String(d.getMilliseconds()).padStart(3, '0');
|
||||||
|
|
||||||
|
const line = document.createElement('div');
|
||||||
|
line.style.whiteSpace = 'pre-wrap';
|
||||||
|
line.style.wordBreak = 'break-all';
|
||||||
|
line.style.fontFamily = 'monospace';
|
||||||
|
line.style.lineHeight = '1.4em';
|
||||||
|
line.style.marginBottom = '4px';
|
||||||
|
|
||||||
|
if (type === 'cmd') {
|
||||||
|
line.style.color = '#cc7832';
|
||||||
|
line.style.fontWeight = 'bold';
|
||||||
|
line.textContent = timeStr + ' >>> ' + text;
|
||||||
|
} else if (type === 'error') {
|
||||||
|
line.style.color = '#ff6b68';
|
||||||
|
line.textContent = timeStr + ' <<< ' + text;
|
||||||
|
} else if (type === 'meta') {
|
||||||
|
line.style.color = '#808080';
|
||||||
|
line.style.fontSize = '11px';
|
||||||
|
line.textContent = text;
|
||||||
|
} else {
|
||||||
|
line.style.color = '#a9b7c6';
|
||||||
|
line.textContent = timeStr + ' <<< ' + text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (durationMs !== undefined && durationMs !== null) {
|
||||||
|
const durSpan = document.createElement('span');
|
||||||
|
durSpan.style.float = 'right';
|
||||||
|
durSpan.style.color = '#629755';
|
||||||
|
durSpan.style.fontSize = '11px';
|
||||||
|
durSpan.textContent = '[' + _('Duration: ') + durationMs + ' ms]';
|
||||||
|
line.appendChild(durSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
out.appendChild(line);
|
||||||
|
|
||||||
|
// Parse CME / CMS Error Code Annotation
|
||||||
|
const mCme = text.match(/\+(CME|CMS) ERROR:\s*([0-9]+)/);
|
||||||
|
if (mCme) {
|
||||||
|
const errCode = mCme[2];
|
||||||
|
const explanation = CME_ERRORS[errCode];
|
||||||
|
if (explanation) {
|
||||||
|
const expLine = document.createElement('div');
|
||||||
|
expLine.style.color = '#e5c07b';
|
||||||
|
expLine.style.background = 'rgba(229, 192, 123, 0.1)';
|
||||||
|
expLine.style.padding = '2px 6px';
|
||||||
|
expLine.style.margin = '2px 0';
|
||||||
|
expLine.style.borderRadius = '3px';
|
||||||
|
expLine.style.fontSize = '12px';
|
||||||
|
expLine.textContent = '[' + _('Error Explanation') + '] ' + mCme[0] + ' -> ' + explanation;
|
||||||
|
out.appendChild(expLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.scrollTop = out.scrollHeight;
|
||||||
|
},
|
||||||
|
|
||||||
|
executeCommand: function(cmdStr) {
|
||||||
|
const self = this;
|
||||||
|
const inputEl = document.getElementById('terminal_input');
|
||||||
|
let raw = cmdStr || (inputEl ? inputEl.value : '');
|
||||||
|
|
||||||
|
// Auto sanitize Chinese/Full-width punctuation
|
||||||
|
const cleanCmd = sanitizeAtPunctuation(raw).trim();
|
||||||
|
if (!cleanCmd) return;
|
||||||
|
|
||||||
|
if (!self.selectedPort) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('No active communication port configured. Please configure an AT control port in Settings first.')), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security Guard: warn on dangerous destructive reset commands
|
||||||
|
if (/^AT&F|^AT\+RESTORE/i.test(cleanCmd)) {
|
||||||
|
if (!confirm(_('Warning: This command may perform a factory reset on the cellular modem. Are you sure to execute?'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.appendLog(cleanCmd, 'cmd');
|
||||||
|
|
||||||
|
if (self.cmdHistory.length === 0 || self.cmdHistory[self.cmdHistory.length - 1] !== cleanCmd) {
|
||||||
|
self.cmdHistory.push(cleanCmd);
|
||||||
|
}
|
||||||
|
self.historyIndex = self.cmdHistory.length;
|
||||||
|
|
||||||
|
if (inputEl) {
|
||||||
|
inputEl.value = '';
|
||||||
|
self.hideAutocomplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = document.getElementById('terminal_send_btn');
|
||||||
|
if (btn) btn.disabled = true;
|
||||||
|
|
||||||
|
// Calculate client-side timestamp fallback
|
||||||
|
const tStart = Date.now();
|
||||||
|
callSendAt(self.selectedPort, cleanCmd, 10).then(function(res) {
|
||||||
|
if (btn) btn.disabled = false;
|
||||||
|
const dur = (res && res.duration_ms) ? res.duration_ms : (Date.now() - tStart);
|
||||||
|
|
||||||
|
if (res && res.output) {
|
||||||
|
self.appendLog(res.output, res.success ? 'resp' : 'error', dur);
|
||||||
|
|
||||||
|
// Suggest command if failed
|
||||||
|
if (!res.success) {
|
||||||
|
const sugg = findBestSuggestion(cleanCmd);
|
||||||
|
if (sugg) {
|
||||||
|
self.renderSuggestionNotice(sugg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.appendLog(_('No response from modem (Timeout or device busy).'), 'error', dur);
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
if (btn) btn.disabled = false;
|
||||||
|
self.appendLog(_('Execution failed: ') + err.message, 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderSuggestionNotice: function(sugg) {
|
||||||
|
const self = this;
|
||||||
|
const out = document.getElementById('terminal_output');
|
||||||
|
if (!out) return;
|
||||||
|
|
||||||
|
const line = document.createElement('div');
|
||||||
|
line.style.color = '#61afef';
|
||||||
|
line.style.background = 'rgba(97, 175, 239, 0.1)';
|
||||||
|
line.style.padding = '3px 8px';
|
||||||
|
line.style.margin = '4px 0';
|
||||||
|
line.style.borderRadius = '3px';
|
||||||
|
line.style.fontSize = '12px';
|
||||||
|
|
||||||
|
const textSpan = document.createElement('span');
|
||||||
|
textSpan.textContent = _('Did you mean: ') + sugg.cmd + ' - ' + sugg.desc + '? ';
|
||||||
|
|
||||||
|
const fillBtn = document.createElement('a');
|
||||||
|
fillBtn.href = '#';
|
||||||
|
fillBtn.style.color = '#98c379';
|
||||||
|
fillBtn.style.marginLeft = '8px';
|
||||||
|
fillBtn.style.textDecoration = 'underline';
|
||||||
|
fillBtn.textContent = _('[Fill in]');
|
||||||
|
fillBtn.onclick = function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const inputEl = document.getElementById('terminal_input');
|
||||||
|
if (inputEl) {
|
||||||
|
inputEl.value = sugg.cmd;
|
||||||
|
inputEl.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
line.appendChild(textSpan);
|
||||||
|
line.appendChild(fillBtn);
|
||||||
|
out.appendChild(line);
|
||||||
|
out.scrollTop = out.scrollHeight;
|
||||||
|
},
|
||||||
|
|
||||||
|
showAutocomplete: function(query) {
|
||||||
|
const self = this;
|
||||||
|
const box = document.getElementById('terminal_autocomplete_box');
|
||||||
|
if (!box) return;
|
||||||
|
|
||||||
|
const clean = query.trim().toUpperCase();
|
||||||
|
if (!clean || clean.length < 2) {
|
||||||
|
box.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = AT_DICTIONARY.filter(function(item) {
|
||||||
|
return item.cmd.toUpperCase().indexOf(clean) !== -1;
|
||||||
|
}).slice(0, 6);
|
||||||
|
|
||||||
|
if (matches.length === 0) {
|
||||||
|
box.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
box.innerHTML = '';
|
||||||
|
matches.forEach(function(m) {
|
||||||
|
const itemEl = document.createElement('div');
|
||||||
|
itemEl.style.padding = '5px 10px';
|
||||||
|
itemEl.style.cursor = 'pointer';
|
||||||
|
itemEl.style.fontSize = '12px';
|
||||||
|
itemEl.style.borderBottom = '1px solid #eee';
|
||||||
|
itemEl.style.display = 'flex';
|
||||||
|
itemEl.style.justifyContent = 'space-between';
|
||||||
|
|
||||||
|
const cmdSpan = document.createElement('b');
|
||||||
|
cmdSpan.style.fontFamily = 'monospace';
|
||||||
|
cmdSpan.textContent = m.cmd;
|
||||||
|
|
||||||
|
const descSpan = document.createElement('span');
|
||||||
|
descSpan.style.color = '#888';
|
||||||
|
descSpan.style.marginLeft = '10px';
|
||||||
|
descSpan.textContent = m.desc;
|
||||||
|
|
||||||
|
itemEl.appendChild(cmdSpan);
|
||||||
|
itemEl.appendChild(descSpan);
|
||||||
|
|
||||||
|
itemEl.onmouseenter = function() { itemEl.style.background = '#f0f4f8'; };
|
||||||
|
itemEl.onmouseleave = function() { itemEl.style.background = '#fff'; };
|
||||||
|
itemEl.onclick = function() {
|
||||||
|
const inputEl = document.getElementById('terminal_input');
|
||||||
|
if (inputEl) {
|
||||||
|
inputEl.value = m.cmd;
|
||||||
|
inputEl.focus();
|
||||||
|
}
|
||||||
|
self.hideAutocomplete();
|
||||||
|
};
|
||||||
|
|
||||||
|
box.appendChild(itemEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
box.style.display = 'block';
|
||||||
|
},
|
||||||
|
|
||||||
|
hideAutocomplete: function() {
|
||||||
|
const box = document.getElementById('terminal_autocomplete_box');
|
||||||
|
if (box) box.style.display = 'none';
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshFavoritesUI: function() {
|
||||||
|
const self = this;
|
||||||
|
const favContainer = document.getElementById('terminal_favorites_list');
|
||||||
|
if (!favContainer) return;
|
||||||
|
|
||||||
|
favContainer.innerHTML = '';
|
||||||
|
self.favorites.forEach(function(f, idx) {
|
||||||
|
const btn = E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'class': 'btn cbi-button cbi-button-action',
|
||||||
|
'style': 'margin-right: 6px; margin-bottom: 6px; font-family: monospace; font-size: 12px;',
|
||||||
|
'value': f.label || f.cmd,
|
||||||
|
'click': function() {
|
||||||
|
const inputEl = document.getElementById('terminal_input');
|
||||||
|
if (inputEl) inputEl.value = f.cmd;
|
||||||
|
self.executeCommand(f.cmd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
favContainer.appendChild(btn);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
addCurrentToFavorites: function() {
|
||||||
|
const self = this;
|
||||||
|
const inputEl = document.getElementById('terminal_input');
|
||||||
|
const cmd = inputEl ? inputEl.value.trim() : '';
|
||||||
|
if (!cmd) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Please enter a command to favorite.')), 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = prompt(_('Enter a friendly name for this shortcut:'), cmd);
|
||||||
|
if (label !== null) {
|
||||||
|
self.favorites.push({ cmd: cmd, label: label.trim() || cmd });
|
||||||
|
self.saveFavorites();
|
||||||
|
self.refreshFavoritesUI();
|
||||||
|
ui.addNotification(null, E('p', {}, _('Command added to favorites.')), 'info');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
exportLogFile: function() {
|
||||||
|
const out = document.getElementById('terminal_output');
|
||||||
|
if (!out) return;
|
||||||
|
const text = out.innerText;
|
||||||
|
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
|
||||||
|
const a = document.createElement('a');
|
||||||
|
const d = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
a.download = 'modem_at_log_' + d + '.txt';
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(devicesData) {
|
||||||
|
const self = this;
|
||||||
|
self.devicesList = Array.isArray(devicesData) ? devicesData : ((devicesData && Array.isArray(devicesData.devices)) ? devicesData.devices : []);
|
||||||
|
self.loadFavorites();
|
||||||
|
|
||||||
|
if (self.devicesList.length > 0) {
|
||||||
|
self.selectedPort = self.devicesList[0].primary_port || (self.devicesList[0].ports ? self.devicesList[0].ports[0] : null);
|
||||||
|
} else {
|
||||||
|
self.selectedPort = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = E('div', { 'class': 'cbi-map' }, [
|
||||||
|
E('h2', {}, _('Cellular Modem Manager') + ' - ' + _('AT Command Terminal')),
|
||||||
|
E('div', { 'class': 'cbi-map-descr' }, _('Direct interactive terminal for sending AT commands with intelligent auto-punctuation correction, fuzzy suggestions, error translation, and custom favorites.'))
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!self.selectedPort) {
|
||||||
|
container.appendChild(E('div', { 'class': 'alert-message warning', 'style': 'margin-top: 15px; padding: 15px; border-left: 5px solid #ff9800; background: #fff8e1;' }, [
|
||||||
|
E('h4', { 'style': 'margin: 0 0 8px 0; color: #d32f2f;' }, _('No Serial Ports Available')),
|
||||||
|
E('p', { 'style': 'margin: 0; color: #555;' }, _('No ttyUSB or ttyACM serial communication ports detected. Please verify hardware connection and driver state.'))
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port Status Bar (Display Only)
|
||||||
|
const activePortLabel = self.selectedPort ? (self.selectedPort + ' @ 115200') : _('No Port Available');
|
||||||
|
const portStatusBar = E('div', {
|
||||||
|
'style': 'margin-top: 15px; margin-bottom: 15px; display: flex; align-items: center; justify-content: space-between; background: #f4f6f8; border: 1px solid #e1e4e8; border-radius: 4px; padding: 8px 16px;'
|
||||||
|
}, [
|
||||||
|
E('span', {}, [
|
||||||
|
E('strong', { 'style': 'color: #333;' }, _('Communication Port: ')),
|
||||||
|
E('span', { 'style': 'color: #0366d6; font-weight: bold; font-family: monospace; font-size: 13px;' }, activePortLabel)
|
||||||
|
]),
|
||||||
|
E('span', { 'style': 'color: #666; font-size: 12px;' }, _('Bound to configured primary AT control port'))
|
||||||
|
]);
|
||||||
|
container.appendChild(portStatusBar);
|
||||||
|
|
||||||
|
// Favorites and Presets Section
|
||||||
|
const presetsSection = E('fieldset', { 'class': 'cbi-section' }, [
|
||||||
|
E('legend', { 'style': 'display: flex; justify-content: space-between; align-items: center; width: 100%; box-sizing: border-box;' }, [
|
||||||
|
E('span', {}, _('Command Presets and Favorites')),
|
||||||
|
E('span', { 'style': 'display: inline-flex; gap: 8px; align-items: center; margin-left: auto;' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'class': 'btn cbi-button',
|
||||||
|
'style': 'font-size: 11px;',
|
||||||
|
'value': _('+ Add Current to Favorites'),
|
||||||
|
'click': ui.createHandlerFn(self, 'addCurrentToFavorites')
|
||||||
|
})
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-section-node', 'style': 'padding: 10px;' }, [
|
||||||
|
E('div', { 'style': 'margin-bottom: 8px;' }, [
|
||||||
|
E('b', { 'style': 'display: inline-block; width: 110px;' }, _('Favorites:')),
|
||||||
|
E('span', { 'id': 'terminal_favorites_list' })
|
||||||
|
]),
|
||||||
|
E('div', {}, [
|
||||||
|
E('b', { 'style': 'display: inline-block; width: 110px;' }, _('Advanced:')),
|
||||||
|
E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'class': 'btn cbi-button cbi-button-action',
|
||||||
|
'style': 'margin-right: 6px; margin-bottom: 6px; font-family: monospace; font-size: 12px;',
|
||||||
|
'value': 'AT+COPS=? - ' + _('Scan Networks'),
|
||||||
|
'click': function() { self.executeCommand('AT+COPS=?'); }
|
||||||
|
}),
|
||||||
|
E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'class': 'btn cbi-button cbi-button-action',
|
||||||
|
'style': 'margin-right: 6px; margin-bottom: 6px; font-family: monospace; font-size: 12px;',
|
||||||
|
'value': 'AT+CGDCONT? - ' + _('Query APN'),
|
||||||
|
'click': function() { self.executeCommand('AT+CGDCONT?'); }
|
||||||
|
})
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
container.appendChild(presetsSection);
|
||||||
|
|
||||||
|
// Autocomplete Dropdown Box
|
||||||
|
const autocompleteBox = E('div', {
|
||||||
|
'id': 'terminal_autocomplete_box',
|
||||||
|
'style': 'display: none; position: absolute; background: #fff; border: 1px solid #ccc; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 1000; max-height: 200px; overflow-y: auto; width: 550px;'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Command Input Bar (Positioned ABOVE Output Box)
|
||||||
|
const inputSection = E('fieldset', { 'class': 'cbi-section', 'style': 'margin-bottom: 15px; position: relative;' }, [
|
||||||
|
E('legend', {}, _('Command Execution')),
|
||||||
|
E('div', { 'class': 'cbi-section-node', 'style': 'padding: 10px;' }, [
|
||||||
|
E('div', { 'style': 'display: flex; gap: 10px; align-items: center;' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'text',
|
||||||
|
'id': 'terminal_input',
|
||||||
|
'class': 'cbi-input-text',
|
||||||
|
'style': 'flex: 1; font-family: monospace; font-size: 14px; padding: 6px 10px;',
|
||||||
|
'placeholder': _('Enter AT command (e.g. AT+CSQ)... Chinese punctuation auto-converted'),
|
||||||
|
'input': function(ev) {
|
||||||
|
self.showAutocomplete(ev.target.value);
|
||||||
|
},
|
||||||
|
'keydown': function(ev) {
|
||||||
|
if (ev.key === 'Enter') {
|
||||||
|
ev.preventDefault();
|
||||||
|
self.executeCommand();
|
||||||
|
} else if (ev.key === 'Escape') {
|
||||||
|
self.hideAutocomplete();
|
||||||
|
} else if (ev.key === 'ArrowUp') {
|
||||||
|
if (self.cmdHistory.length > 0 && self.historyIndex > 0) {
|
||||||
|
self.historyIndex--;
|
||||||
|
ev.target.value = self.cmdHistory[self.historyIndex];
|
||||||
|
}
|
||||||
|
} else if (ev.key === 'ArrowDown') {
|
||||||
|
if (self.historyIndex < self.cmdHistory.length - 1) {
|
||||||
|
self.historyIndex++;
|
||||||
|
ev.target.value = self.cmdHistory[self.historyIndex];
|
||||||
|
} else {
|
||||||
|
self.historyIndex = self.cmdHistory.length;
|
||||||
|
ev.target.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'id': 'terminal_send_btn',
|
||||||
|
'class': 'btn cbi-button cbi-button-action',
|
||||||
|
'value': _('Send Command'),
|
||||||
|
'click': function() {
|
||||||
|
self.executeCommand();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
autocompleteBox
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
container.appendChild(inputSection);
|
||||||
|
|
||||||
|
// Terminal Output Box (Positioned BELOW Input Bar)
|
||||||
|
const outputSection = E('fieldset', { 'class': 'cbi-section' }, [
|
||||||
|
E('legend', { 'style': 'display: flex; justify-content: space-between; align-items: center; width: 100%; box-sizing: border-box;' }, [
|
||||||
|
E('span', {}, _('Interactive Console Output')),
|
||||||
|
E('span', { 'style': 'display: inline-flex; gap: 8px; align-items: center; margin-left: auto;' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'class': 'btn cbi-button',
|
||||||
|
'value': _('Clear Output'),
|
||||||
|
'style': 'font-size: 11px;',
|
||||||
|
'click': function() {
|
||||||
|
const out = document.getElementById('terminal_output');
|
||||||
|
if (out) out.innerHTML = '';
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'class': 'btn cbi-button',
|
||||||
|
'value': _('Export Log'),
|
||||||
|
'style': 'font-size: 11px;',
|
||||||
|
'click': ui.createHandlerFn(self, 'exportLogFile')
|
||||||
|
}),
|
||||||
|
E('input', {
|
||||||
|
'type': 'button',
|
||||||
|
'class': 'btn cbi-button',
|
||||||
|
'value': _('Copy All'),
|
||||||
|
'style': 'font-size: 11px;',
|
||||||
|
'click': function() {
|
||||||
|
const out = document.getElementById('terminal_output');
|
||||||
|
if (out && navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(out.innerText);
|
||||||
|
ui.addNotification(null, E('p', {}, _('Terminal output copied to clipboard.')), 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', {
|
||||||
|
'id': 'terminal_output',
|
||||||
|
'style': 'background: #2b2b2b; color: #a9b7c6; font-family: monospace; font-size: 13px; min-height: 280px; max-height: 480px; overflow-y: auto; padding: 12px; border-radius: 4px; box-sizing: border-box; white-space: pre-wrap;'
|
||||||
|
}, [
|
||||||
|
E('div', { 'style': 'color: #808080;' }, _('System ready. Full-width punctuation will be automatically converted to standard ASCII.'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
container.appendChild(outputSection);
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
self.refreshFavoritesUI();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,614 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr "Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
|
||||||
|
msgid "Modem Manager"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Cellular Modem Manager"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Overview"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Templates"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Configuration Templates"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "AT Terminal"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "AT Command Terminal"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Log"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "System Log"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Modem Manager Logs"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Active Modem Selection"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Target Modem Selection"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Current Device:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Active Device:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Refresh"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No cellular modems detected on USB bus."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No cellular modems detected."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No serial communication ports detected."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Hardware Information"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Manufacturer"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Model"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "USB Identifiers"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Core Temperature"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Driver Binding"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Option Driver"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "SIM Status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Querying..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Not Supported"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Normal"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PIN Locked"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PUK Locked"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Not Detected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Cellular Network and Signal Metrics"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Operator"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Access Technology"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Active Band"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Physical Cell ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Cell ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Tracking Area Code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Carrier Channel"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Signal Strength (CSQ)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Signal Power (RSRP)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Signal Quality (SINR)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Excellent"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Good"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Weak"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Searching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Calculating"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Network Interface Connection"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Connection State"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Connected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Disconnected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Assigned IPv4"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Traffic Statistics"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Downlink: "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid " / Uplink: "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Restart Modem"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Toggle Airplane Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Reset RF Radio"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Executing Action"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Sending action to cellular module, please wait..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Action executed successfully."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Action failed: "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Driver Protocol and Operation Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Driver Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "MBIM Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "NCM Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "QMI Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ECM Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "RNDIS Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Switching the operation mode requires restarting the modem to take effect."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Sync OpenWrt Network"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Automatically update or create corresponding logical network interface in OpenWrt"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Interface Name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "The OpenWrt logical interface name to associate with this modem."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Network Access Technology and Band Locking"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Access Technology Preference"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Automatic Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "5G Standalone Only"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "5G and 4G Hybrid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "4G LTE Only"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Select priority radio access technology for network registration."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "4G LTE Bands"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "5G NR Bands"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Access Point Name (APN) Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "APN Name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Cellular carrier APN profile name (e.g. cmiot, cbnet, uninet)."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PDP Protocol Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Dual Stack IPv4 and IPv6"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "IPv4 Only"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "IPv6 Only"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Save and Apply"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Applying Configuration"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Sending configuration commands to modem, please wait..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Modem settings successfully applied."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Failed to apply settings: "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Preset Template Library"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Template Name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Vendor"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Vendor ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Mode Switching Command"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Actions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "View Details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Template Detail"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Template ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Target Vendor"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "USB VID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Mode Switch Command"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Raw Configuration"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Serial Port Selection"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Communication Port:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Baudrate: 115200 (Auto Locked)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Quick AT Command Presets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "General Info:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Network State:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Quectel Extra:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Fibocom Extra:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Interactive Console Output"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Clear Output"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Copy All"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Terminal output copied to clipboard."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "System ready. Enter an AT command and press Enter or Send to execute."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enter AT command (e.g. AT+CSQ)..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Send Command"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Clear Log"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Log buffer cleared successfully."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No log entries recorded yet."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No Active Cellular Modem Detected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No active 4G or 5G cellular modem detected on the USB bus. Please verify hardware connection, power supply, or driver status."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Rescan Devices"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No Serial Ports Available"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No ttyUSB or ttyACM serial communication ports detected. Please verify hardware connection and driver state."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No Cellular Modem Detected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No active cellular modem detected. Unable to manage SMS messages. Please check physical connection."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No active cellular modem detected. You can still pre-configure serial port path or refresh after connecting."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Signal Strength CSQ"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Signal Power RSRP"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Signal Quality SINR"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Configure PDP context 1 with APN"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Configure driver protocols, lock preferred access technologies, customize APN credentials, and manage cellular network interfaces."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Dial string too long"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Direct interactive terminal for sending AT commands with intelligent auto-punctuation correction, fuzzy suggestions, error translation, and custom favorites."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enable full RF function (Turn off airplane mode)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Execution failed: "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Fibocom: Query RNDIS network connection status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Fibocom: Query carrier aggregation info"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Fibocom: Query current cell info and radio metrics"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Incorrect PIN entered"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Invalid characters in dial string"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Invalid characters in text string"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Invalid index or memory entry not found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Manage vendor preset command profiles, private AT mappings, band lock masks, and driver switching templates."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Memory storage full"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Network not allowed emergency calls only"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Network timeout"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No Net"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No Port"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No cellular network service available"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No connection to phone"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No templates installed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "None"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Not Defined"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Not found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Operation not allowed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Operation not supported"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PH-SIM PIN required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PIN2 required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PUK2 required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Phone failure"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Phone-adaptor link reserved"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Please select a communication port first."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Please select a valid modem port first."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Quectel: Query CPU and RF chip temperatures"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Quectel: Query carrier aggregation (CA) state and secondary cells"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Quectel: Query current network band and channel info"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Quectel: Query current serving cell details (5G SA/LTE, PCI, TAC, Band, RSRP, SINR)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query 2G/3G network registration status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query 4G LTE network registration status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query 5G NR network registration status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query SIM card ICCID number"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query SIM card lock and ready status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query configured PDP contexts and APN profiles"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query current RF radio state (0:Min, 1:Full, 4:Airplane)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query module manufacturer, model, and firmware version"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query received signal strength and bit error rate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query registered cellular operator and RAT"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Real-time overview of cellular modem hardware, network registration, signal metrics, and data connection."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Reboot module processor and RF reset"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "SIM PIN required (Please enter PIN to unlock)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "SIM PUK required (Please enter PUK to unlock)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "SIM card busy, please retry later"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "SIM card failure or communication defect"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "SIM card not inserted or not detected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "SIM wrong password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Scan all available cellular networks in air (Long timeout)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Text string too long"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Turn on airplane mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Unknown"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Unknown error or unsupported extended command"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Uplink: "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "View background AT command traces, hardware detection events, and auto-reconnect activities."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Signal CSQ"
|
||||||
|
msgstr ""
|
||||||
@@ -0,0 +1,614 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr "Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
|
||||||
|
msgid "Modem Manager"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Cellular Modem Manager"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Overview"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Templates"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Configuration Templates"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "AT Terminal"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "AT Command Terminal"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Log"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "System Log"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Modem Manager Logs"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Active Modem Selection"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Target Modem Selection"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Current Device:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Active Device:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Refresh"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No cellular modems detected on USB bus."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No cellular modems detected."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No serial communication ports detected."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Hardware Information"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Manufacturer"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Model"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "USB Identifiers"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Core Temperature"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Driver Binding"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Option Driver"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "SIM Status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Querying..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Not Supported"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Normal"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PIN Locked"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PUK Locked"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Not Detected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Cellular Network and Signal Metrics"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Operator"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Access Technology"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Active Band"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Physical Cell ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Cell ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Tracking Area Code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Carrier Channel"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Signal Strength (CSQ)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Signal Power (RSRP)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Signal Quality (SINR)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Excellent"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Good"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Weak"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Searching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Calculating"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Network Interface Connection"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Connection State"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Connected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Disconnected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Assigned IPv4"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Traffic Statistics"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Downlink: "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid " / Uplink: "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Restart Modem"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Toggle Airplane Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Reset RF Radio"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Executing Action"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Sending action to cellular module, please wait..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Action executed successfully."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Action failed: "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Driver Protocol and Operation Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Driver Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "MBIM Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "NCM Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "QMI Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ECM Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "RNDIS Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Switching the operation mode requires restarting the modem to take effect."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Sync OpenWrt Network"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Automatically update or create corresponding logical network interface in OpenWrt"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Interface Name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "The OpenWrt logical interface name to associate with this modem."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Network Access Technology and Band Locking"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Access Technology Preference"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Automatic Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "5G Standalone Only"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "5G and 4G Hybrid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "4G LTE Only"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Select priority radio access technology for network registration."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "4G LTE Bands"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "5G NR Bands"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Access Point Name (APN) Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "APN Name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Cellular carrier APN profile name (e.g. cmiot, cbnet, uninet)."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PDP Protocol Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Dual Stack IPv4 and IPv6"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "IPv4 Only"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "IPv6 Only"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Save and Apply"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Applying Configuration"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Sending configuration commands to modem, please wait..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Modem settings successfully applied."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Failed to apply settings: "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Preset Template Library"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Template Name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Vendor"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Vendor ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Mode Switching Command"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Actions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "View Details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Template Detail"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Template ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Target Vendor"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "USB VID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Mode Switch Command"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Raw Configuration"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Serial Port Selection"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Communication Port:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Baudrate: 115200 (Auto Locked)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Quick AT Command Presets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "General Info:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Network State:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Quectel Extra:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Fibocom Extra:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Interactive Console Output"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Clear Output"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Copy All"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Terminal output copied to clipboard."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "System ready. Enter an AT command and press Enter or Send to execute."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enter AT command (e.g. AT+CSQ)..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Send Command"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Clear Log"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Log buffer cleared successfully."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No log entries recorded yet."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No Active Cellular Modem Detected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No active 4G or 5G cellular modem detected on the USB bus. Please verify hardware connection, power supply, or driver status."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Rescan Devices"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No Serial Ports Available"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No ttyUSB or ttyACM serial communication ports detected. Please verify hardware connection and driver state."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No Cellular Modem Detected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No active cellular modem detected. Unable to manage SMS messages. Please check physical connection."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No active cellular modem detected. You can still pre-configure serial port path or refresh after connecting."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Signal Strength CSQ"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Signal Power RSRP"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Signal Quality SINR"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Configure PDP context 1 with APN"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Configure driver protocols, lock preferred access technologies, customize APN credentials, and manage cellular network interfaces."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Dial string too long"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Direct interactive terminal for sending AT commands with intelligent auto-punctuation correction, fuzzy suggestions, error translation, and custom favorites."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enable full RF function (Turn off airplane mode)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Execution failed: "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Fibocom: Query RNDIS network connection status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Fibocom: Query carrier aggregation info"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Fibocom: Query current cell info and radio metrics"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Incorrect PIN entered"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Invalid characters in dial string"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Invalid characters in text string"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Invalid index or memory entry not found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Manage vendor preset command profiles, private AT mappings, band lock masks, and driver switching templates."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Memory storage full"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Network not allowed emergency calls only"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Network timeout"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No Net"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No Port"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No cellular network service available"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No connection to phone"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No templates installed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "None"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Not Defined"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Not found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Operation not allowed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Operation not supported"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PH-SIM PIN required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PIN2 required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PUK2 required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Phone failure"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Phone-adaptor link reserved"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Please select a communication port first."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Please select a valid modem port first."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Quectel: Query CPU and RF chip temperatures"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Quectel: Query carrier aggregation (CA) state and secondary cells"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Quectel: Query current network band and channel info"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Quectel: Query current serving cell details (5G SA/LTE, PCI, TAC, Band, RSRP, SINR)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query 2G/3G network registration status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query 4G LTE network registration status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query 5G NR network registration status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query SIM card ICCID number"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query SIM card lock and ready status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query configured PDP contexts and APN profiles"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query current RF radio state (0:Min, 1:Full, 4:Airplane)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query module manufacturer, model, and firmware version"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query received signal strength and bit error rate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Query registered cellular operator and RAT"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Real-time overview of cellular modem hardware, network registration, signal metrics, and data connection."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Reboot module processor and RF reset"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "SIM PIN required (Please enter PIN to unlock)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "SIM PUK required (Please enter PUK to unlock)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "SIM card busy, please retry later"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "SIM card failure or communication defect"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "SIM card not inserted or not detected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "SIM wrong password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Scan all available cellular networks in air (Long timeout)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Text string too long"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Turn on airplane mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Unknown"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Unknown error or unsupported extended command"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Uplink: "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "View background AT command traces, hardware detection events, and auto-reconnect activities."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Signal CSQ"
|
||||||
|
msgstr ""
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
zh_Hans
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,11 @@
|
|||||||
|
config global 'global'
|
||||||
|
option enabled '1'
|
||||||
|
option check_interval '15'
|
||||||
|
option auto_reconnect '1'
|
||||||
|
option target_host '223.5.5.5'
|
||||||
|
option default_interface 'modem_wan'
|
||||||
|
option log_level 'info'
|
||||||
|
option sms_enable '0'
|
||||||
|
|
||||||
|
config template_custom
|
||||||
|
option enabled '0'
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# USB Hotplug Event Handler for Cellular Modems
|
||||||
|
# Dante <dante@seckv.com>
|
||||||
|
|
||||||
|
[ "$DEVTYPE" = "usb_device" ] || exit 0
|
||||||
|
|
||||||
|
case "$ACTION" in
|
||||||
|
add|bind|remove)
|
||||||
|
# Delay slightly to allow sysfs device tree and tty nodes to settle
|
||||||
|
(
|
||||||
|
sleep 1
|
||||||
|
/usr/libexec/modem-detect --cache >/dev/null 2>&1
|
||||||
|
which ubus >/dev/null 2>&1 && ubus send modem.device.change "{\"action\":\"$ACTION\",\"dev\":\"$DEVICENAME\"}" 2>/dev/null || true
|
||||||
|
) &
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/sh /etc/rc.common
|
||||||
|
# Cellular Modem Manager Daemon
|
||||||
|
# Dante <dante@seckv.com>
|
||||||
|
|
||||||
|
USE_PROCD=1
|
||||||
|
START=95
|
||||||
|
STOP=10
|
||||||
|
|
||||||
|
PROG="/usr/libexec/modem-collector"
|
||||||
|
|
||||||
|
start_service() {
|
||||||
|
local enabled check_interval
|
||||||
|
config_load modem-manager
|
||||||
|
config_get_bool enabled global enabled 1
|
||||||
|
config_get check_interval global check_interval 15
|
||||||
|
|
||||||
|
[ "$enabled" -eq 1 ] || return 0
|
||||||
|
|
||||||
|
mkdir -p /tmp/run/modem-manager /var/log /var/lock
|
||||||
|
touch /var/log/modem-manager.log
|
||||||
|
|
||||||
|
# Warm up devices discovery cache at startup
|
||||||
|
/usr/libexec/modem-detect --cache >/dev/null 2>&1 &
|
||||||
|
|
||||||
|
procd_open_instance
|
||||||
|
procd_set_param command "$PROG"
|
||||||
|
procd_set_param respawn 3600 5 0
|
||||||
|
procd_set_param stdout 1
|
||||||
|
procd_set_param stderr 1
|
||||||
|
procd_close_instance
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_service() {
|
||||||
|
rm -f /var/lock/modem_terminal_active.lock 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
reload_service() {
|
||||||
|
stop
|
||||||
|
start
|
||||||
|
}
|
||||||
|
|
||||||
|
service_triggers() {
|
||||||
|
procd_add_reload_trigger "modem-manager"
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
chmod 0755 /usr/libexec/modem-at-executor /usr/libexec/modem-detect /usr/libexec/modem-collector 2>/dev/null || true
|
||||||
|
chmod 0755 /etc/init.d/modem-manager /etc/hotplug.d/usb/20-modem-hotplug 2>/dev/null || true
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Modem AT Command Safe Executor (Zero-dependency, Dynamic Timeout)
|
||||||
|
# Dante <dante@seckv.com>
|
||||||
|
|
||||||
|
DEVICE=""
|
||||||
|
COMMAND=""
|
||||||
|
TIMEOUT=3
|
||||||
|
BAUDRATE=115200
|
||||||
|
|
||||||
|
SMS_TO=""
|
||||||
|
SMS_MSG=""
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-d) DEVICE="$2"; shift 2 ;;
|
||||||
|
-c) COMMAND="$2"; shift 2 ;;
|
||||||
|
-t) TIMEOUT="$2"; shift 2 ;;
|
||||||
|
-b) BAUDRATE="$2"; shift 2 ;;
|
||||||
|
--sms-to) SMS_TO="$2"; shift 2 ;;
|
||||||
|
--sms-msg) SMS_MSG="$2"; shift 2 ;;
|
||||||
|
*) shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$DEVICE" ]; then
|
||||||
|
echo "Usage: $0 -d <device> [-c <command>] [--sms-to <number> --sms-msg <text>]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$COMMAND" ] && [ -z "$SMS_TO" ]; then
|
||||||
|
echo "Usage: Either -c <command> or --sms-to <number> must be specified" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$DEVICE" in
|
||||||
|
/dev/ttyUSB*|/dev/ttyACM*) ;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: Invalid device path $DEVICE" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ ! -c "$DEVICE" ]; then
|
||||||
|
echo "ERROR: Device $DEVICE not found" >&2
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Dynamic Timeout auto-detection: long-running commands like AT+COPS=? or scan
|
||||||
|
if [ -n "$SMS_TO" ]; then
|
||||||
|
[ "$TIMEOUT" -lt 15 ] && TIMEOUT=15
|
||||||
|
else
|
||||||
|
case "$COMMAND" in
|
||||||
|
*COPS=\?*|*COPS\?*|*QSCAN*|*GTCCINFO*|*SCAN*)
|
||||||
|
[ "$TIMEOUT" -lt 60 ] && TIMEOUT=60
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
LOCKFILE="/var/lock/modem_$(basename "$DEVICE").lock"
|
||||||
|
mkdir -p /var/lock
|
||||||
|
|
||||||
|
# Acquire exclusive file lock compatible with Busybox flock (-n -x FD)
|
||||||
|
exec 9>"$LOCKFILE"
|
||||||
|
locked=0
|
||||||
|
waited=0
|
||||||
|
while [ $waited -le "$TIMEOUT" ]; do
|
||||||
|
if flock -n -x 9 2>/dev/null; then
|
||||||
|
locked=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
waited=$((waited + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $locked -eq 0 ]; then
|
||||||
|
echo "ERROR: Device $DEVICE busy, lock timed out" >&2
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fast path: Prioritize native sms_tool engine if installed (zero echo loop, robust termios)
|
||||||
|
if command -v sms_tool >/dev/null 2>&1; then
|
||||||
|
if [ -n "$SMS_TO" ]; then
|
||||||
|
OUT=$(sms_tool -d "$DEVICE" -b "$BAUDRATE" send "$SMS_TO" "$SMS_MSG" 2>&1)
|
||||||
|
RC=$?
|
||||||
|
else
|
||||||
|
OUT=$(sms_tool -d "$DEVICE" -b "$BAUDRATE" at "$COMMAND" 2>&1)
|
||||||
|
RC=$?
|
||||||
|
fi
|
||||||
|
flock -u 9 2>/dev/null || true
|
||||||
|
CLEAN_OUT=$(echo "$OUT" | tr -d "\r")
|
||||||
|
if [ $RC -eq 0 ] && ! echo "$CLEAN_OUT" | grep -qE "ERROR|\+CME ERROR|\+CMS ERROR"; then
|
||||||
|
if [ -z "$CLEAN_OUT" ]; then
|
||||||
|
echo "OK"
|
||||||
|
else
|
||||||
|
echo "$CLEAN_OUT"
|
||||||
|
case "$CLEAN_OUT" in
|
||||||
|
*OK*) ;;
|
||||||
|
*) echo "OK" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "$CLEAN_OUT"
|
||||||
|
exit ${RC:-1}
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v stty >/dev/null 2>&1 && stty -F "$DEVICE" "$BAUDRATE" raw -echo -echoe -echok min 0 time 2 2>/dev/null || true
|
||||||
|
|
||||||
|
OUT_FILE="/tmp/at_out_$$.tmp"
|
||||||
|
rm -f "$OUT_FILE"
|
||||||
|
|
||||||
|
(
|
||||||
|
exec 3<>"$DEVICE"
|
||||||
|
# Drain stale residual buffer
|
||||||
|
dd if="$DEVICE" of=/dev/null bs=512 count=1 2>/dev/null || true
|
||||||
|
cat <&3 > "$OUT_FILE" &
|
||||||
|
CAT_PID=$!
|
||||||
|
|
||||||
|
if [ -n "$SMS_TO" ]; then
|
||||||
|
# Text mode SMS sending
|
||||||
|
printf "AT+CMGF=1\r\n" >&3
|
||||||
|
sleep 1
|
||||||
|
printf "AT+CMGS=\"%s\"\r\n" "$SMS_TO" >&3
|
||||||
|
sleep 1
|
||||||
|
printf "%s\x1A" "$SMS_MSG" >&3
|
||||||
|
|
||||||
|
elapsed=0
|
||||||
|
while [ $elapsed -lt "$TIMEOUT" ]; do
|
||||||
|
sleep 1
|
||||||
|
elapsed=$((elapsed + 1))
|
||||||
|
if [ -f "$OUT_FILE" ]; then
|
||||||
|
if grep -q "\+CMGS:" "$OUT_FILE" 2>/dev/null && grep -q "OK" "$OUT_FILE" 2>/dev/null; then
|
||||||
|
break
|
||||||
|
elif grep -qE "\+CMS ERROR|\+CME ERROR|ERROR" "$OUT_FILE" 2>/dev/null; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
printf "%s\r\n" "$COMMAND" >&3
|
||||||
|
|
||||||
|
elapsed=0
|
||||||
|
while [ $elapsed -lt "$TIMEOUT" ]; do
|
||||||
|
sleep 1
|
||||||
|
elapsed=$((elapsed + 1))
|
||||||
|
if [ -f "$OUT_FILE" ] && grep -qE "OK|ERROR|\+CME ERROR|\+CMS ERROR" "$OUT_FILE" 2>/dev/null; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
kill -9 "$CAT_PID" 2>/dev/null || true
|
||||||
|
exec 3>&-
|
||||||
|
)
|
||||||
|
|
||||||
|
flock -u 9 2>/dev/null || true
|
||||||
|
|
||||||
|
if [ -f "$OUT_FILE" ]; then
|
||||||
|
cat "$OUT_FILE" | tr -d "\r"
|
||||||
|
rm -f "$OUT_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Background Status Collector with Active Terminal Yielding
|
||||||
|
# Dante <dante@seckv.com>
|
||||||
|
|
||||||
|
RUN_DIR="/tmp/run/modem-manager"
|
||||||
|
CACHE_DEVICES="$RUN_DIR/devices.json"
|
||||||
|
CACHE_STATUS="$RUN_DIR/status.json"
|
||||||
|
TERMINAL_LOCK="/var/lock/modem_terminal_active.lock"
|
||||||
|
YIELD_WINDOW=10
|
||||||
|
|
||||||
|
mkdir -p "$RUN_DIR" /var/lock
|
||||||
|
|
||||||
|
is_terminal_active() {
|
||||||
|
if [ -f "$TERMINAL_LOCK" ]; then
|
||||||
|
local lock_mtime=$(date -r "$TERMINAL_LOCK" +%s 2>/dev/null || echo 0)
|
||||||
|
local now=$(date +%s)
|
||||||
|
local diff=$((now - lock_mtime))
|
||||||
|
if [ "$diff" -ge 0 ] && [ "$diff" -le "$YIELD_WINDOW" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_single() {
|
||||||
|
if is_terminal_active; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -f "$CACHE_DEVICES" ] || /usr/libexec/modem-detect --cache >/dev/null 2>&1
|
||||||
|
|
||||||
|
# Check user manually configured port from UCI first
|
||||||
|
local user_port=$(uci -q get modem-manager.global.port || uci -q get modem-manager.default.port)
|
||||||
|
|
||||||
|
ucode -e '
|
||||||
|
import * as fs from "fs";
|
||||||
|
let m = loadfile("/usr/share/rpcd/ucode/luci.modem")();
|
||||||
|
let plugin = m["luci.modem"];
|
||||||
|
let devs_data = plugin.get_devices.call();
|
||||||
|
let dev = (devs_data?.devices && length(devs_data.devices) > 0) ? devs_data.devices[0] : null;
|
||||||
|
let user_port = "'"$user_port"'";
|
||||||
|
let target_port = (user_port && user_port != "") ? user_port : (dev?.primary_port || null);
|
||||||
|
|
||||||
|
if (target_port) {
|
||||||
|
let status = plugin.collect_raw_status.call({ args: { port: target_port, primary_net: dev?.primary_net || "usb0" } });
|
||||||
|
if (status) {
|
||||||
|
let tmp = "/tmp/run/modem-manager/status.json.tmp";
|
||||||
|
fs.writefile(tmp, sprintf("%J\n", status));
|
||||||
|
fs.rename(tmp, "/tmp/run/modem-manager/status.json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
collect_single
|
||||||
|
INTERVAL=$(uci -q get modem-manager.global.check_interval || echo 15)
|
||||||
|
[ "$INTERVAL" -ge 5 ] 2>/dev/null || INTERVAL=15
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
done
|
||||||
+189
@@ -0,0 +1,189 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Cellular Modem Auto-Detector for OpenWrt
|
||||||
|
# Outputs discovered modems in JSON format and updates /tmp/run/modem-manager/devices.json
|
||||||
|
|
||||||
|
RUN_DIR="/tmp/run/modem-manager"
|
||||||
|
CACHE_FILE="$RUN_DIR/devices.json"
|
||||||
|
KNOWN_VENDORS="2c7c 2cb7 1e0e 12d1 19d2 1bbb 05c6 413c 0bdb 1199 2977 1508 1bc7 0e8d 1782 0489 1fac 2dee 33f8 3731 2df3 20a6"
|
||||||
|
|
||||||
|
get_sys_attr() {
|
||||||
|
local path="$1"
|
||||||
|
if [ -f "$path" ]; then
|
||||||
|
tr -d '\r\n' < "$path" 2>/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_all() {
|
||||||
|
echo "{"
|
||||||
|
echo ' "devices": ['
|
||||||
|
|
||||||
|
local FIRST=1
|
||||||
|
|
||||||
|
for dev in /sys/bus/usb/devices/*; do
|
||||||
|
[ -d "$dev" ] || continue
|
||||||
|
[ -f "$dev/idVendor" ] || continue
|
||||||
|
[ -f "$dev/idProduct" ] || continue
|
||||||
|
|
||||||
|
local VID=$(get_sys_attr "$dev/idVendor")
|
||||||
|
local PID=$(get_sys_attr "$dev/idProduct")
|
||||||
|
local DEVCLASS=$(get_sys_attr "$dev/bDeviceClass")
|
||||||
|
|
||||||
|
case "$VID" in
|
||||||
|
1d6b|2109|1d50) continue ;;
|
||||||
|
esac
|
||||||
|
[ "$DEVCLASS" = "09" ] && continue
|
||||||
|
|
||||||
|
local IS_MODEM=0
|
||||||
|
for kv in $KNOWN_VENDORS; do
|
||||||
|
if [ "$VID" = "$kv" ]; then
|
||||||
|
IS_MODEM=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
local DEVNAME=$(basename "$dev")
|
||||||
|
|
||||||
|
# Direct sysfs tree discovery for serial ports under this USB device
|
||||||
|
local TTYS=""
|
||||||
|
for t in $(find "$dev" -maxdepth 5 \( -name "ttyUSB*" -o -name "ttyACM*" \) 2>/dev/null); do
|
||||||
|
local b=$(basename "$t")
|
||||||
|
case "$b" in
|
||||||
|
ttyUSB*|ttyACM*)
|
||||||
|
if [ -e "/dev/$b" ]; then
|
||||||
|
case " $TTYS " in
|
||||||
|
*" /dev/$b "*) ;;
|
||||||
|
*) TTYS="$TTYS /dev/$b" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Fallback tty search via /sys/class/tty
|
||||||
|
if [ -z "$TTYS" ]; then
|
||||||
|
for t in /sys/class/tty/ttyUSB* /sys/class/tty/ttyACM*; do
|
||||||
|
[ -e "$t" ] || continue
|
||||||
|
local t_target=$(readlink "$t" 2>/dev/null || readlink -f "$t" 2>/dev/null)
|
||||||
|
case "$t_target" in
|
||||||
|
*"$DEVNAME"*)
|
||||||
|
local b=$(basename "$t")
|
||||||
|
[ -e "/dev/$b" ] && case " $TTYS " in *" /dev/$b "*) ;; *) TTYS="$TTYS /dev/$b" ;; esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
TTYS=$(echo "$TTYS" | sed 's/^ *//')
|
||||||
|
|
||||||
|
# Network interfaces discovery under this USB device
|
||||||
|
local NETS=""
|
||||||
|
for n in $(find "$dev" -maxdepth 5 -name "net" 2>/dev/null); do
|
||||||
|
[ -d "$n" ] || continue
|
||||||
|
for iface in "$n"/*; do
|
||||||
|
[ -d "$iface" ] || continue
|
||||||
|
local b=$(basename "$iface")
|
||||||
|
case "$b" in
|
||||||
|
lo|br-*|eth*|lan*|wan*|sfp*) ;;
|
||||||
|
*) case " $NETS " in *" $b "*) ;; *) NETS="$NETS $b" ;; esac ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# Fallback net search via /sys/class/net
|
||||||
|
if [ -z "$NETS" ]; then
|
||||||
|
for n in /sys/class/net/*; do
|
||||||
|
[ -d "$n" ] || continue
|
||||||
|
local n_target=$(readlink "$n" 2>/dev/null || readlink -f "$n" 2>/dev/null)
|
||||||
|
case "$n_target" in
|
||||||
|
*"$DEVNAME"*)
|
||||||
|
local b=$(basename "$n")
|
||||||
|
case "$b" in
|
||||||
|
lo|br-*|eth*|lan*|wan*|sfp*) ;;
|
||||||
|
*) case " $NETS " in *" $b "*) ;; *) NETS="$NETS $b" ;; esac ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
NETS=$(echo "$NETS" | sed 's/^ *//')
|
||||||
|
|
||||||
|
if [ -n "$TTYS" ] || [ -n "$NETS" ] || [ $IS_MODEM -eq 1 ]; then
|
||||||
|
IS_MODEM=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ $IS_MODEM -eq 1 ] || continue
|
||||||
|
|
||||||
|
local MFG=$(get_sys_attr "$dev/manufacturer")
|
||||||
|
local PROD=$(get_sys_attr "$dev/product")
|
||||||
|
local SER=$(get_sys_attr "$dev/serial")
|
||||||
|
|
||||||
|
local PRIMARY_TTY=""
|
||||||
|
for t in $TTYS; do
|
||||||
|
case "$t" in
|
||||||
|
*ttyUSB2|*ttyACM0)
|
||||||
|
PRIMARY_TTY="$t"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ -z "$PRIMARY_TTY" ] && PRIMARY_TTY=$(echo "$TTYS" | awk '{print $1}')
|
||||||
|
local PRIMARY_NET=$(echo "$NETS" | awk '{print $1}')
|
||||||
|
|
||||||
|
local DRIVER="option"
|
||||||
|
for drv in $(find "$dev" -name "driver" 2>/dev/null); do
|
||||||
|
local drv_target=$(basename "$(readlink -f "$drv" 2>/dev/null)")
|
||||||
|
case "$drv_target" in
|
||||||
|
cdc_mbim|cdc_ncm|qmi_wwan|cdc_ether|rndis_host)
|
||||||
|
DRIVER="$drv_target"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[ $FIRST -eq 1 ] || echo ","
|
||||||
|
FIRST=0
|
||||||
|
|
||||||
|
local TTY_JSON=""
|
||||||
|
for t in $TTYS; do
|
||||||
|
[ -n "$TTY_JSON" ] && TTY_JSON="$TTY_JSON, "
|
||||||
|
TTY_JSON="$TTY_JSON\"$t\""
|
||||||
|
done
|
||||||
|
|
||||||
|
local NET_JSON=""
|
||||||
|
for n in $NETS; do
|
||||||
|
[ -n "$NET_JSON" ] && NET_JSON="$NET_JSON, "
|
||||||
|
NET_JSON="$NET_JSON\"$n\""
|
||||||
|
done
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"id": "$DEVNAME",
|
||||||
|
"vid": "$VID",
|
||||||
|
"pid": "$PID",
|
||||||
|
"vendor": "${MFG:-Generic}",
|
||||||
|
"model": "${PROD:-Modem}",
|
||||||
|
"serial": "${SER}",
|
||||||
|
"driver": "$DRIVER",
|
||||||
|
"primary_port": "$PRIMARY_TTY",
|
||||||
|
"primary_net": "$PRIMARY_NET",
|
||||||
|
"ports": [ $TTY_JSON ],
|
||||||
|
"nets": [ $NET_JSON ]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
done
|
||||||
|
|
||||||
|
echo " ]"
|
||||||
|
echo "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir -p "$RUN_DIR"
|
||||||
|
OUTPUT=$(detect_all)
|
||||||
|
|
||||||
|
# Atomic update cache
|
||||||
|
echo "$OUTPUT" > "${CACHE_FILE}.tmp" && mv -f "${CACHE_FILE}.tmp" "$CACHE_FILE"
|
||||||
|
|
||||||
|
# Output to stdout if not purely --cache mode
|
||||||
|
if [ "$1" != "--cache" ]; then
|
||||||
|
echo "$OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"admin/network/modem": {
|
||||||
|
"title": "Modem Manager",
|
||||||
|
"order": 65,
|
||||||
|
"action": {
|
||||||
|
"type": "firstchild"
|
||||||
|
},
|
||||||
|
"depends": {
|
||||||
|
"acl": [ "luci-app-modem-manager" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/network/modem/overview": {
|
||||||
|
"title": "Overview",
|
||||||
|
"order": 10,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "modem/overview"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/network/modem/settings": {
|
||||||
|
"title": "Settings",
|
||||||
|
"order": 20,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "modem/settings"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/network/modem/sms": {
|
||||||
|
"title": "SMS",
|
||||||
|
"order": 30,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "modem/sms"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/network/modem/templates": {
|
||||||
|
"title": "Templates",
|
||||||
|
"order": 35,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "modem/templates"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/network/modem/terminal": {
|
||||||
|
"title": "AT Terminal",
|
||||||
|
"order": 40,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "modem/terminal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/network/modem/log": {
|
||||||
|
"title": "Log",
|
||||||
|
"order": 50,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "modem/log"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"id": "fibocom-5g",
|
||||||
|
"name": "广和通 4G/5G 全系模组",
|
||||||
|
"vendor": "Fibocom",
|
||||||
|
"vid": "2cb7",
|
||||||
|
"default_baud": 115200,
|
||||||
|
"commands": {
|
||||||
|
"info": "ATI",
|
||||||
|
"cpin": "AT+CPIN?",
|
||||||
|
"csq": "AT+CSQ",
|
||||||
|
"operator": "AT+COPS?",
|
||||||
|
"network_reg": "AT+CEREG?;+C5GREG?",
|
||||||
|
"serving_cell": "AT+GTCCINFO?",
|
||||||
|
"ca_info": "AT+GTCAINFO?",
|
||||||
|
"reboot": "AT+CFUN=1,1",
|
||||||
|
"airplane_on": "AT+CFUN=4",
|
||||||
|
"airplane_off": "AT+CFUN=1"
|
||||||
|
},
|
||||||
|
"mode_switch": {
|
||||||
|
"cmd": "AT+GTUSBMODE={mode}",
|
||||||
|
"modes": {
|
||||||
|
"mbim": "1",
|
||||||
|
"ncm": "2",
|
||||||
|
"ecm": "3",
|
||||||
|
"rndis": "4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rat_preference": {
|
||||||
|
"cmd": "AT+GTRAT={rat}",
|
||||||
|
"options": {
|
||||||
|
"AUTO": "0",
|
||||||
|
"LTE_ONLY": "2",
|
||||||
|
"NR5G_ONLY": "3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supported_bands": {
|
||||||
|
"lte": [ "B1", "B3", "B5", "B8", "B34", "B38", "B39", "B40", "B41" ],
|
||||||
|
"nr5g": [ "N1", "N28", "N41", "N78", "N79" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"id": "generic-3gpp",
|
||||||
|
"name": "标准 3GPP 通用模组",
|
||||||
|
"vendor": "Generic",
|
||||||
|
"vid": "*",
|
||||||
|
"default_baud": 115200,
|
||||||
|
"commands": {
|
||||||
|
"info": "ATI",
|
||||||
|
"cpin": "AT+CPIN?",
|
||||||
|
"csq": "AT+CSQ",
|
||||||
|
"operator": "AT+COPS?",
|
||||||
|
"network_reg": "AT+CREG?;+CEREG?",
|
||||||
|
"reboot": "AT+CFUN=1,1",
|
||||||
|
"airplane_on": "AT+CFUN=4",
|
||||||
|
"airplane_off": "AT+CFUN=1"
|
||||||
|
},
|
||||||
|
"mode_switch": {
|
||||||
|
"cmd": "",
|
||||||
|
"modes": {}
|
||||||
|
},
|
||||||
|
"rat_preference": {
|
||||||
|
"cmd": "",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"supported_bands": {
|
||||||
|
"lte": [ "B1", "B3", "B5", "B8", "B38", "B39", "B40", "B41" ],
|
||||||
|
"nr5g": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"id": "quectel-5g",
|
||||||
|
"name": "移远通信 4G/5G 全系模组",
|
||||||
|
"vendor": "Quectel",
|
||||||
|
"vid": "2c7c",
|
||||||
|
"default_baud": 115200,
|
||||||
|
"commands": {
|
||||||
|
"info": "ATI",
|
||||||
|
"cpin": "AT+CPIN?",
|
||||||
|
"csq": "AT+CSQ",
|
||||||
|
"operator": "AT+COPS?",
|
||||||
|
"network_reg": "AT+CEREG=2;+CEREG?;+C5GREG=2;+C5GREG?",
|
||||||
|
"serving_cell": "AT+QENG=\"servingcell\"",
|
||||||
|
"ca_info": "AT+QCAINFO",
|
||||||
|
"temp": "AT+QTEMP",
|
||||||
|
"reboot": "AT+CFUN=1,1",
|
||||||
|
"airplane_on": "AT+CFUN=4",
|
||||||
|
"airplane_off": "AT+CFUN=1"
|
||||||
|
},
|
||||||
|
"mode_switch": {
|
||||||
|
"cmd": "AT+QCFG=\"usbnet\",{mode}",
|
||||||
|
"modes": {
|
||||||
|
"qmi": "0",
|
||||||
|
"ecm": "1",
|
||||||
|
"mbim": "2",
|
||||||
|
"rndis": "3",
|
||||||
|
"ncm": "5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rat_preference": {
|
||||||
|
"cmd": "AT+QNWPREFCFG=\"mode_pref\",{rat}",
|
||||||
|
"options": {
|
||||||
|
"AUTO": "AUTO",
|
||||||
|
"NR5G_ONLY": "NR5G",
|
||||||
|
"LTE_ONLY": "LTE",
|
||||||
|
"NR5G_LTE": "NR5G:LTE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supported_bands": {
|
||||||
|
"lte": [ "B1", "B3", "B5", "B8", "B34", "B38", "B39", "B40", "B41" ],
|
||||||
|
"nr5g": [ "N1", "N28", "N41", "N77", "N78", "N79" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"id": "zte-generic",
|
||||||
|
"name": "中兴微 4G/5G 模组",
|
||||||
|
"vendor": "ZTE",
|
||||||
|
"vid": "19d2",
|
||||||
|
"default_baud": 115200,
|
||||||
|
"commands": {
|
||||||
|
"info": "ATI",
|
||||||
|
"cpin": "AT+CPIN?",
|
||||||
|
"csq": "AT+CSQ",
|
||||||
|
"operator": "AT+COPS?",
|
||||||
|
"network_reg": "AT+CEREG?",
|
||||||
|
"reboot": "AT+CFUN=1,1",
|
||||||
|
"airplane_on": "AT+CFUN=4",
|
||||||
|
"airplane_off": "AT+CFUN=1"
|
||||||
|
},
|
||||||
|
"mode_switch": {
|
||||||
|
"cmd": "AT+ZMODE={mode}",
|
||||||
|
"modes": {
|
||||||
|
"ncm": "1",
|
||||||
|
"rndis": "2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rat_preference": {
|
||||||
|
"cmd": "AT+ZSNT={rat}",
|
||||||
|
"options": {
|
||||||
|
"AUTO": "0,0,0",
|
||||||
|
"LTE_ONLY": "6,0,0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supported_bands": {
|
||||||
|
"lte": [ "B1", "B3", "B5", "B8", "B34", "B38", "B39", "B40", "B41" ],
|
||||||
|
"nr5g": [ "N1", "N28", "N41", "N78" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"luci-app-modem-manager": {
|
||||||
|
"description": "Grant access to modem manager RPC methods",
|
||||||
|
"read": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.modem": [
|
||||||
|
"get_devices",
|
||||||
|
"get_status",
|
||||||
|
"get_templates",
|
||||||
|
"get_config",
|
||||||
|
"get_log",
|
||||||
|
"get_sms_status",
|
||||||
|
"get_sms_list",
|
||||||
|
"get_cell_lock",
|
||||||
|
"get_apn"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uci": [
|
||||||
|
"modem-manager",
|
||||||
|
"network"
|
||||||
|
],
|
||||||
|
"file": {
|
||||||
|
"/tmp/run/modem-manager/devices.json": [ "read" ],
|
||||||
|
"/tmp/run/modem-manager/status.json": [ "read" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"write": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.modem": [
|
||||||
|
"send_at",
|
||||||
|
"set_mode",
|
||||||
|
"set_preferences",
|
||||||
|
"set_apn",
|
||||||
|
"set_cell_lock",
|
||||||
|
"device_action",
|
||||||
|
"refresh_status",
|
||||||
|
"save_template",
|
||||||
|
"save_config",
|
||||||
|
"clear_log",
|
||||||
|
"set_sms_enable",
|
||||||
|
"send_sms",
|
||||||
|
"delete_sms"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uci": [
|
||||||
|
"modem-manager",
|
||||||
|
"network"
|
||||||
|
],
|
||||||
|
"file": {
|
||||||
|
"/etc/config/modem-manager": [ "read", "write" ],
|
||||||
|
"/var/log/modem-manager.log": [ "read", "write" ],
|
||||||
|
"/tmp/run/modem-manager/*": [ "read", "write" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-passwall
|
PKG_NAME:=luci-app-passwall
|
||||||
PKG_VERSION:=26.10.4
|
PKG_VERSION:=26.10.4
|
||||||
PKG_RELEASE:=330
|
PKG_RELEASE:=331
|
||||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||||
|
|
||||||
PKG_CONFIG_DEPENDS:= \
|
PKG_CONFIG_DEPENDS:= \
|
||||||
|
|||||||
@@ -1,18 +1,61 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# dhcp.leases to hosts
|
# DHCP leases and static hosts to hosts
|
||||||
|
|
||||||
|
. /lib/functions.sh
|
||||||
. /usr/share/passwall/utils.sh
|
. /usr/share/passwall/utils.sh
|
||||||
LOCK_FILE=${LOCK_PATH}/${CONFIG}_lease2hosts.lock
|
LOCK_FILE=${LOCK_PATH}/${CONFIG}_lease2hosts.lock
|
||||||
LEASE_FILE=$(uci -q get dhcp.@dnsmasq[0].leasefile || echo "/tmp/dhcp.leases")
|
DNSMASQ_LEASE_FILE=$(uci -q get 'dhcp.@dnsmasq[0].leasefile')
|
||||||
|
DNSMASQ_LEASE_FILE=${DNSMASQ_LEASE_FILE:-/tmp/dhcp.leases}
|
||||||
|
ODHCPD_LEASE_FILE=$(uci -q get dhcp.odhcpd.leasefile)
|
||||||
|
ODHCPD_LEASE_FILE=${ODHCPD_LEASE_FILE:-/tmp/odhcpd.leases}
|
||||||
HOSTS_FILE="$TMP_PATH2/dhcp-hosts"
|
HOSTS_FILE="$TMP_PATH2/dhcp-hosts"
|
||||||
TMP_FILE="/tmp/dhcp-hosts.tmp"
|
TMP_FILE="/tmp/dhcp-hosts.tmp"
|
||||||
|
|
||||||
exec 99>"$LOCK_FILE"
|
exec 9>"$LOCK_FILE"
|
||||||
flock -n 99
|
flock -n 9 || exit 0
|
||||||
if [ "$?" != 0 ]; then
|
|
||||||
exit 0
|
read_static_host() {
|
||||||
fi
|
local name ip
|
||||||
|
config_get name "$1" name
|
||||||
|
config_get ip "$1" ip
|
||||||
|
[ -n "$name" ] && [ "$name" != "*" ] && [ -n "$ip" ] && [ "$ip" != "ignore" ] || return
|
||||||
|
printf '%s %s\n' "$ip" "$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Static DHCP configuration only needs to be read once at startup.
|
||||||
|
STATIC_HOSTS=$(
|
||||||
|
config_load dhcp
|
||||||
|
config_foreach read_static_host host
|
||||||
|
)
|
||||||
|
|
||||||
|
read_odhcpd_leases() {
|
||||||
|
[ -f "$ODHCPD_LEASE_FILE" ] || return
|
||||||
|
awk -v now="$(date +%s)" '
|
||||||
|
function valid_name(name) {
|
||||||
|
return name ~ /^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/
|
||||||
|
}
|
||||||
|
# State records: # interface DUID IAID hostname expiry assigned length address/prefix ...
|
||||||
|
$1 == "#" {
|
||||||
|
if (NF < 9 || !valid_name($5) || ($6 != -1 && $6 <= now))
|
||||||
|
next
|
||||||
|
# Only host addresses, never delegated IPv6 prefixes.
|
||||||
|
if (!(($4 == "ipv4" && $8 == 32) || ($4 != "ipv4" && $8 == 128)))
|
||||||
|
next
|
||||||
|
for (i = 9; i <= NF; i++) {
|
||||||
|
split($i, addr, "/")
|
||||||
|
print addr[1] " " $5
|
||||||
|
}
|
||||||
|
next
|
||||||
|
}
|
||||||
|
# Older odhcpd versions also include hosts-format lines, with optional aliases.
|
||||||
|
NF >= 2 && $1 ~ /^[0-9a-fA-F:.]+$/ {
|
||||||
|
for (i = 2; i <= NF && $i !~ /^#/; i++)
|
||||||
|
if (valid_name($i))
|
||||||
|
print $1 " " $i
|
||||||
|
}
|
||||||
|
' "$ODHCPD_LEASE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
reload_dnsmasq_pids() {
|
reload_dnsmasq_pids() {
|
||||||
local pidfile pid
|
local pidfile pid
|
||||||
@@ -28,22 +71,26 @@ reload_dnsmasq_pids() {
|
|||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
|
|
||||||
if [ -f "$LEASE_FILE" ]; then
|
{
|
||||||
awk 'NF >= 4 && $4 != "*" {print $3" "$4}' "$LEASE_FILE" | sort > "$TMP_FILE"
|
[ -n "$STATIC_HOSTS" ] && printf '%s\n' "$STATIC_HOSTS"
|
||||||
if [ -s "$TMP_FILE" ]; then
|
if [ -f "$DNSMASQ_LEASE_FILE" ]; then
|
||||||
if [ ! -f "$HOSTS_FILE" ] || ! cmp -s "$TMP_FILE" "$HOSTS_FILE"; then
|
awk 'NF >= 4 && $4 != "*" {print $3" "$4}' "$DNSMASQ_LEASE_FILE"
|
||||||
mv "$TMP_FILE" "$HOSTS_FILE"
|
fi
|
||||||
reload_dnsmasq_pids
|
read_odhcpd_leases
|
||||||
else
|
} | sort -u > "$TMP_FILE"
|
||||||
rm -f "$TMP_FILE"
|
if [ -s "$TMP_FILE" ]; then
|
||||||
fi
|
if [ ! -f "$HOSTS_FILE" ] || ! cmp -s "$TMP_FILE" "$HOSTS_FILE"; then
|
||||||
|
mv "$TMP_FILE" "$HOSTS_FILE"
|
||||||
|
reload_dnsmasq_pids
|
||||||
else
|
else
|
||||||
if [ -s "$HOSTS_FILE" ]; then
|
|
||||||
: > "$HOSTS_FILE"
|
|
||||||
reload_dnsmasq_pids
|
|
||||||
fi
|
|
||||||
rm -f "$TMP_FILE"
|
rm -f "$TMP_FILE"
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
if [ -s "$HOSTS_FILE" ]; then
|
||||||
|
: > "$HOSTS_FILE"
|
||||||
|
reload_dnsmasq_pids
|
||||||
|
fi
|
||||||
|
rm -f "$TMP_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sleep 60
|
sleep 60
|
||||||
|
|||||||
Reference in new issue
Block a user