🎉 Sync 2026-06-28 20:31:01

This commit is contained in:
github-actions[bot] 2026-06-28 20:31:01 +08:00
parent 31880da43f
commit 15bc162efb
24 changed files with 279 additions and 227 deletions

View File

@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.6.2 PKG_VERSION:=26.6.2
PKG_RELEASE:=167 PKG_RELEASE:=168
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \

View File

@ -12,6 +12,9 @@ s = m:section(TypedSection, "global_app", translate("App Update"),
s.anonymous = true s.anonymous = true
s:append(Template(appname .. "/app_update/app_version")) s:append(Template(appname .. "/app_update/app_version"))
o = s:option(Flag, "github_proxy", translate("GitHub Proxy"), translate("Use gh-proxy instead of proxy nodes for component updates."))
o.default = 0
local k, v local k, v
local com = require "luci.passwall.com" local com = require "luci.passwall.com"
for _, k in ipairs(com.order) do for _, k in ipairs(com.order) do

View File

@ -996,7 +996,14 @@ local default_file_tree = {
local function get_api_json(url) local function get_api_json(url)
local jsonc = require "luci.jsonc" local jsonc = require "luci.jsonc"
local return_code, content = curl_auto(url, nil, curl_args) local gh_proxy = uci_get_type("global_app", "github_proxy", "0")
local return_code, content
if gh_proxy == "1" then
url = "https://gh-proxy.org/" .. url
return_code, content = curl_base(url, nil, curl_args)
else
return_code, content = curl_auto(url, nil, curl_args)
end
if return_code ~= 0 or content == "" then return {} end if return_code ~= 0 or content == "" then return {} end
return jsonc.parse(content) or {} return jsonc.parse(content) or {}
end end
@ -1113,7 +1120,14 @@ function to_download(app_name, url, size)
local _curl_args = clone(curl_args) local _curl_args = clone(curl_args)
table.insert(_curl_args, "--speed-limit 51200 --speed-time 15 --max-time 300") table.insert(_curl_args, "--speed-limit 51200 --speed-time 15 --max-time 300")
local return_code, result = curl_auto(url, tmp_file, _curl_args) local gh_proxy = uci_get_type("global_app", "github_proxy", "0")
local return_code, result
if gh_proxy == "1" then
url = "https://gh-proxy.org/" .. url
return_code, result = curl_base(url, tmp_file, _curl_args)
else
return_code, result = curl_auto(url, tmp_file, _curl_args)
end
result = return_code == 0 result = return_code == 0
if not result then if not result then

View File

@ -1198,6 +1198,12 @@ msgstr "%s 客户端程序路径"
msgid "alternate API URL for version checking" msgid "alternate API URL for version checking"
msgstr "用于版本检查的 API URL" msgstr "用于版本检查的 API URL"
msgid "GitHub Proxy"
msgstr "GitHub 反代"
msgid "Use gh-proxy instead of proxy nodes for component updates."
msgstr "开启后将使用 gh-proxy 反向代理,不通过节点更新组件。"
msgid "Node Subscribe" msgid "Node Subscribe"
msgstr "节点订阅" msgstr "节点订阅"

View File

@ -24,3 +24,7 @@ doh.pub
dot.pub dot.pub
doh.360.cn doh.360.cn
dot.360.cn dot.360.cn
#GitHub Proxy
gh-proxy.com
gh-proxy.org

View File

@ -3,8 +3,8 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=luci-app-ssr-plus LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=195 PKG_VERSION:=196
PKG_RELEASE:=33 PKG_RELEASE:=34
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \

View File

@ -11,7 +11,7 @@ LUCI_DEPENDS:= +luci-app-modeminfo +ssw
PKG_LICENSE:=GPLv3 PKG_LICENSE:=GPLv3
PKG_VERSION:=0.0.3 PKG_VERSION:=0.0.3
PKG_RELEASE:=4 PKG_RELEASE:=5
include $(TOPDIR)/feeds/luci/luci.mk include $(TOPDIR)/feeds/luci/luci.mk

View File

@ -135,7 +135,7 @@ return view.extend({
// Every-N-days field (shown only when period=interval) // Every-N-days field (shown only when period=interval)
o = s.option(form.Value, 'period_days', _('Repeat every (days)'), o = s.option(form.Value, 'period_days', _('Repeat every (days)'),
_('Switch to the reserve SIM every this many days. Minimum 2.')); _('Switch to your backup SIM card after a specified number of days per month. Minimum 2.'));
o.placeholder = '3'; o.placeholder = '3';
o.datatype = 'uinteger'; o.datatype = 'uinteger';
o.default = '3'; o.default = '3';

View File

@ -195,8 +195,8 @@ msgstr "Еженедельно (определённый день)"
msgid "Repeat every (days)" msgid "Repeat every (days)"
msgstr "Повторять каждые (дней)" msgstr "Повторять каждые (дней)"
msgid "Switch to the reserve SIM every this many days. Minimum 2." msgid "Switch to your backup SIM card after a specified number of days per month. Minimum 2."
msgstr "Переключаться на резервную SIM раз в указанное количество дней. Минимум 2." msgstr "Переключаться на резервную SIM через указанное количество дней в месяце. Минимум 2."
msgid "Must be 2 or more" msgid "Must be 2 or more"
msgstr "Должно быть 2 или более" msgstr "Должно быть 2 или более"

View File

@ -45,6 +45,18 @@
@apply dark:hue-rotate-150 dark:invert; @apply dark:hue-rotate-150 dark:invert;
} }
/* luci-app-modemdata */
[data-page="admin-modem-modemdata-modempreview"] {
& .ifacebox-body {
& .ifacebadge {
@apply w-auto!;
}
}
}
/* luci-mod-dashboard */ /* luci-mod-dashboard */
.Dashboard { .Dashboard {
& > .section-content { & > .section-content {

View File

@ -19,7 +19,7 @@
} }
.zonebadge { .zonebadge {
@apply border-hairline bg-surface-sunken text-text inline-flex items-center gap-1.5 overflow-visible rounded-lg border px-2 py-1 align-middle text-sm font-semibold leading-normal max-md:gap-1 max-md:px-1.5 max-md:text-xs; @apply border-hairline bg-surface-sunken text-text inline-flex items-center gap-1.5 overflow-visible rounded-lg border px-2 py-1 align-middle text-sm leading-normal font-semibold max-md:gap-1 max-md:px-1.5 max-md:text-xs;
&[style*="--zone-color-rgb"] { &[style*="--zone-color-rgb"] {
@apply border-transparent bg-[rgb(var(--zone-color-rgb),.7)]!; @apply border-transparent bg-[rgb(var(--zone-color-rgb),.7)]!;
@ -30,7 +30,7 @@
} }
&.zonebadge-empty { &.zonebadge-empty {
@apply border-dashed bg-transparent text-text-muted font-normal italic; @apply text-text-muted border-dashed bg-transparent font-normal italic;
} }
.cbi-dropdown & { .cbi-dropdown & {
@ -39,21 +39,17 @@
} }
.ifacebadge { .ifacebadge {
@apply border-hairline bg-surface-sunken text-text hover:border-hairline hover:bg-surface-sunken inline-flex cursor-default flex-wrap items-center gap-2 rounded-2xl border px-3 py-2 text-sm font-normal shadow-sm transition-[box-shadow,border-color,background-color] duration-150 hover:shadow-md max-md:gap-1.5 max-md:px-2.5 max-md:py-1.5 max-md:text-xs; @apply border-hairline bg-surface-sunken text-text hover:border-hairline hover:bg-surface-sunken inline-flex cursor-default flex-nowrap items-center gap-2 rounded-2xl border px-3 py-2 text-sm font-normal shadow-sm transition-[box-shadow,border-color,background-color] duration-150 hover:shadow-md max-md:gap-1.5 max-md:px-2.5 max-md:py-1.5 max-md:text-xs;
.cbi-tooltip & {
@apply flex-nowrap;
}
.zonebadge > & { .zonebadge > & {
@apply flex-nowrap rounded-2xl px-1.5 py-0; @apply rounded-2xl px-1.5 py-0;
& img { & img {
@apply mr-1 w-4; @apply mr-1 w-4;
} }
} }
.cbi-dropdown & { .cbi-dropdown & {
@apply flex-nowrap gap-1 rounded-lg px-1.5 py-0.5 shadow-none hover:shadow-none; @apply gap-1 rounded-lg px-1.5 py-0.5 shadow-none hover:shadow-none;
& img { & img {
@apply mr-1 w-4; @apply mr-1 w-4;
} }

View File

@ -93,12 +93,20 @@
} }
& .ifacebox-body { & .ifacebox-body {
@apply text-text flex w-full flex-1 flex-col items-center justify-around gap-2 rounded-b-2xl p-4 text-center max-md:rounded-b-3xl; /* Flex column base: `items-stretch` lets label/value rows span full width
(e.g. luci-app-modemdata) while `justify-center` keeps rows tight together
and balanced instead of spread apart. Icon-only cards (switch ports, the
interfaces list) centre horizontally via `text-center`/`mx-auto`. The
interfaces overview layers its richer layout under `.network-status-table`. */
@apply text-text flex w-full flex-1 flex-col items-stretch justify-center gap-1 rounded-b-2xl p-4 text-center max-md:rounded-b-3xl;
td & { td & {
@apply max-md:flex-row max-md:rounded-r-3xl max-md:rounded-bl-none max-md:py-2 max-md:pr-2 max-md:pl-4; @apply max-md:flex-row max-md:items-center max-md:rounded-r-3xl max-md:rounded-bl-none max-md:py-2 max-md:pr-2 max-md:pl-4;
} }
& > img {
@apply mx-auto;
}
& > span { & > span {
@apply text-text space-y-1.5 max-md:space-y-1 max-md:text-sm max-md:leading-5; @apply text-text space-y-1.5 max-md:space-y-1 max-md:text-sm max-md:leading-5;
@ -128,13 +136,10 @@
} }
& > div { & > div {
@apply flex w-full flex-wrap space-y-2.5 max-md:space-y-1.5; @apply block w-full space-y-0;
td & { td & {
@apply max-md:w-auto max-md:flex-1 max-md:space-y-1; @apply max-md:w-auto max-md:flex-1 max-md:space-y-1;
} }
& .ifacebadge {
@apply flex-1;
}
} }
& .cbi-tooltip-container { & .cbi-tooltip-container {
@ -157,3 +162,21 @@
} }
} }
} }
/*
Network interfaces overview (admin/network) the page `.ifacebox` was designed
for. The rich equal-height card layout is scoped here (not on the base) so
third-party pages keep the plain, tolerant defaults above. Mirrors the
base-vs-`.network-status-table` split in luci-theme-bootstrap.
*/
.network-status-table .ifacebox-body {
@apply flex h-full flex-1 flex-col items-center justify-around gap-2;
& > div {
@apply flex flex-wrap;
}
& .ifacebadge {
@apply flex-1;
}
}

View File

@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS) LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS)
LUCI_DEPENDS:=+luci-base LUCI_DEPENDS:=+luci-base
PKG_VERSION:=1.0.2 PKG_VERSION:=1.0.3
PKG_RELEASE:=35 PKG_RELEASE:=36
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=Apache-2.0
LUCI_MINIFY_CSS:= LUCI_MINIFY_CSS:=

View File

@ -12,6 +12,7 @@
<a href="https://github.com/eamonxg/luci-theme-aurora/releases/latest"><img alt="GitHub release" src="https://img.shields.io/github/v/release/eamonxg/luci-theme-aurora"></a> <a href="https://github.com/eamonxg/luci-theme-aurora/releases/latest"><img alt="GitHub release" src="https://img.shields.io/github/v/release/eamonxg/luci-theme-aurora"></a>
<a href="https://github.com/eamonxg/luci-theme-aurora/releases"><img alt="Downloads" src="https://img.shields.io/github/downloads/eamonxg/luci-theme-aurora/total"></a> <a href="https://github.com/eamonxg/luci-theme-aurora/releases"><img alt="Downloads" src="https://img.shields.io/github/downloads/eamonxg/luci-theme-aurora/total"></a>
<a href="https://discord.gg/EBncRrzfTw"><img alt="Discord" src="https://img.shields.io/badge/Discord-5865F2?logo=discord&logoColor=white"></a> <a href="https://discord.gg/EBncRrzfTw"><img alt="Discord" src="https://img.shields.io/badge/Discord-5865F2?logo=discord&logoColor=white"></a>
<a href="#contributing"><img alt="Contributors" src="https://img.shields.io/github/contributors/eamonxg/luci-theme-aurora?color=orange"></a>
</div> </div>
<div align="center"> <div align="center">
@ -114,6 +115,20 @@ Aurora uses **Vite** and a modern front-end toolchain, and is experimenting with
[discord.gg/EBncRrzfTw](https://discord.gg/EBncRrzfTw) [discord.gg/EBncRrzfTw](https://discord.gg/EBncRrzfTw)
Thanks goes to these wonderful people:
<!-- contributors:start -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.29%"><a href="https://github.com/eamonxg"><img src="https://avatars.githubusercontent.com/u/114069097?v=4&s=160" width="80px;" alt="eamonxg"/><br /><sub><b>eamonxg</b></sub></a></td>
<td align="center" valign="top" width="14.29%"><a href="https://github.com/cjayacopra"><img src="https://avatars.githubusercontent.com/u/83209495?v=4&s=160" width="80px;" alt="cjayacopra"/><br /><sub><b>cjayacopra</b></sub></a></td>
<td align="center" valign="top" width="14.29%"><a href="https://github.com/chillykidd"><img src="https://avatars.githubusercontent.com/u/197483577?v=4&s=160" width="80px;" alt="chillykidd"/><br /><sub><b>chillykidd</b></sub></a></td>
</tr>
</tbody>
</table>
<!-- contributors:end -->
## License & Credits ## License & Credits
[Apache 2.0](LICENSE). Thanks to: [Apache 2.0](LICENSE). Thanks to:

View File

@ -12,6 +12,7 @@
<a href="https://github.com/eamonxg/luci-theme-aurora/releases/latest"><img alt="GitHub release" src="https://img.shields.io/github/v/release/eamonxg/luci-theme-aurora"></a> <a href="https://github.com/eamonxg/luci-theme-aurora/releases/latest"><img alt="GitHub release" src="https://img.shields.io/github/v/release/eamonxg/luci-theme-aurora"></a>
<a href="https://github.com/eamonxg/luci-theme-aurora/releases"><img alt="Downloads" src="https://img.shields.io/github/downloads/eamonxg/luci-theme-aurora/total"></a> <a href="https://github.com/eamonxg/luci-theme-aurora/releases"><img alt="Downloads" src="https://img.shields.io/github/downloads/eamonxg/luci-theme-aurora/total"></a>
<a href="https://discord.gg/EBncRrzfTw"><img alt="Discord" src="https://img.shields.io/badge/Discord-5865F2?logo=discord&logoColor=white"></a> <a href="https://discord.gg/EBncRrzfTw"><img alt="Discord" src="https://img.shields.io/badge/Discord-5865F2?logo=discord&logoColor=white"></a>
<a href="#加入贡献"><img alt="Contributors" src="https://img.shields.io/github/contributors/eamonxg/luci-theme-aurora?color=orange"></a>
</div> </div>
<div align="center"> <div align="center">
@ -115,6 +116,20 @@ Aurora 使用 **Vite** 与现代前端工具链构建,并尝试将 AI 融入
[discord.gg/EBncRrzfTw](https://discord.gg/EBncRrzfTw) [discord.gg/EBncRrzfTw](https://discord.gg/EBncRrzfTw)
感谢这些出色的贡献者:
<!-- contributors:start -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.29%"><a href="https://github.com/eamonxg"><img src="https://avatars.githubusercontent.com/u/114069097?v=4&s=160" width="80px;" alt="eamonxg"/><br /><sub><b>eamonxg</b></sub></a></td>
<td align="center" valign="top" width="14.29%"><a href="https://github.com/cjayacopra"><img src="https://avatars.githubusercontent.com/u/83209495?v=4&s=160" width="80px;" alt="cjayacopra"/><br /><sub><b>cjayacopra</b></sub></a></td>
<td align="center" valign="top" width="14.29%"><a href="https://github.com/chillykidd"><img src="https://avatars.githubusercontent.com/u/197483577?v=4&s=160" width="80px;" alt="chillykidd"/><br /><sub><b>chillykidd</b></sub></a></td>
</tr>
</tbody>
</table>
<!-- contributors:end -->
## 许可与致谢 ## 许可与致谢
[Apache 2.0](LICENSE)。致谢: [Apache 2.0](LICENSE)。致谢:

File diff suppressed because one or more lines are too long

View File

@ -5,10 +5,10 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=einat-ebpf PKG_NAME:=einat-ebpf
PKG_UPSTREAM_VERSION:=0.1.10 PKG_UPSTREAM_VERSION:=0.1.11
PKG_UPSTREAM_GITHASH:= PKG_UPSTREAM_GITHASH:=
PKG_VERSION:=$(PKG_UPSTREAM_VERSION)$(if $(PKG_UPSTREAM_GITHASH),~$(call version_abbrev,$(PKG_UPSTREAM_GITHASH))) PKG_VERSION:=$(PKG_UPSTREAM_VERSION)$(if $(PKG_UPSTREAM_GITHASH),~$(call version_abbrev,$(PKG_UPSTREAM_GITHASH)))
PKG_RELEASE:=1 PKG_RELEASE:=2
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_UPSTREAM_VERSION) PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_UPSTREAM_VERSION)
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_SOURCE_SUBDIR) PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_SOURCE_SUBDIR)

View File

@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=fastfetch PKG_NAME:=fastfetch
PKG_VERSION:=2.65.1 PKG_VERSION:=2.65.2
PKG_RELEASE:=10 PKG_RELEASE:=11
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/fastfetch-cli/fastfetch/tar.gz/$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/fastfetch-cli/fastfetch/tar.gz/$(PKG_VERSION)?

View File

@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=shadowsocksr-libev PKG_NAME:=shadowsocksr-libev
PKG_VERSION:=2.5.6 PKG_VERSION:=2.5.6
PKG_RELEASE:=2 PKG_RELEASE:=3
PKG_SOURCE_PROTO:=git PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/shadowsocksrr/shadowsocksr-libev PKG_SOURCE_URL:=https://github.com/shadowsocksrr/shadowsocksr-libev

View File

@ -320,8 +320,8 @@ static void
merge(uint8_t *left, int llength, uint8_t *right, merge(uint8_t *left, int llength, uint8_t *right,
int rlength, uint32_t salt, uint64_t key) int rlength, uint32_t salt, uint64_t key)
{ {
uint8_t *ltmp = (uint8_t *)malloc(llength * sizeof(uint8_t)); uint8_t *ltmp = (uint8_t *)malloc(llength);
uint8_t *rtmp = (uint8_t *)malloc(rlength * sizeof(uint8_t)); uint8_t *rtmp = (uint8_t *)malloc(rlength);
uint8_t *ll = ltmp; uint8_t *ll = ltmp;
uint8_t *rr = rtmp; uint8_t *rr = rtmp;
@ -783,7 +783,7 @@ cipher_context_set_iv(cipher_ctx_t *ctx, uint8_t *iv, size_t iv_len,
} }
if (!enc) { if (!enc) {
memcpy(ctx->iv, iv, iv_len); memcpy(ctx->iv, iv, min(iv_len, MAX_IV_LENGTH));
} }
if (enc_method >= SALSA20) { if (enc_method >= SALSA20) {
@ -803,8 +803,8 @@ cipher_context_set_iv(cipher_ctx_t *ctx, uint8_t *iv, size_t iv_len,
#ifdef USE_CRYPTO_APPLECC #ifdef USE_CRYPTO_APPLECC
cipher_cc_t *cc = &ctx->cc; cipher_cc_t *cc = &ctx->cc;
if (cc->valid == kCCContextValid) { if (cc->valid == kCCContextValid) {
memcpy(cc->iv, iv, iv_len); memcpy(cc->iv, iv, min(iv_len, MAX_IV_LENGTH));
memcpy(cc->key, true_key, enc_key_len); memcpy(cc->key, true_key, min(enc_key_len, MAX_KEY_LENGTH));
cc->iv_len = iv_len; cc->iv_len = iv_len;
cc->key_len = enc_key_len; cc->key_len = enc_key_len;
cc->encrypt = enc ? kCCEncrypt : kCCDecrypt; cc->encrypt = enc ? kCCEncrypt : kCCDecrypt;

View File

@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=ssw PKG_NAME:=ssw
PKG_VERSION:=0.0.3 PKG_VERSION:=0.0.4
PKG_RELEASE:=4 PKG_RELEASE:=5
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru> PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/package.mk

View File

@ -8,6 +8,9 @@ START=99
STOP=10 STOP=10
USE_PROCD=1 USE_PROCD=1
CRON_FILE="/etc/crontabs/root"
CRON_TAG="# ssw-schedule"
load_config_gpio(){ load_config_gpio(){
local gpio value local gpio value
for c in gpio value; do for c in gpio value; do
@ -39,6 +42,82 @@ _apply_modem_state(){
sleep 30 && reload_iface & sleep 30 && reload_iface &
} }
# Parse "HH:MM" -> cron fields "MM HH"
time_to_cron(){
local t="$1" h m
h="${t%%:*}"
m="${t##*:}"
h=$(echo "$h" | sed 's/^0*//')
m=$(echo "$m" | sed 's/^0*//')
[ -z "$h" ] && h=0
[ -z "$m" ] && m=0
printf "%s %s" "$m" "$h"
}
# Add duration minutes to "HH:MM", output as cron "MM HH"
time_add_min(){
local t="$1" dur="$2" h m total_min
h="${t%%:*}"
m="${t##*:}"
h=$(echo "$h" | sed 's/^0*//')
m=$(echo "$m" | sed 's/^0*//')
[ -z "$h" ] && h=0
[ -z "$m" ] && m=0
total_min=$(( h * 60 + m + dur ))
h=$(( total_min / 60 % 24 ))
m=$(( total_min % 60 ))
printf "%s %s" "$m" "$h"
}
# Build cron day/month/weekday fields from period settings
# Outputs: "dom month dow"
cron_period_fields(){
local period="$1"
local period_days="$2"
local weekday="$3"
case "$period" in
daily) echo "* * *" ;;
weekly) echo "* * ${weekday:-1}" ;;
monthly) echo "1 * *" ;;
interval) echo "*/${period_days:-1} * *" ;;
*) echo "* * *" ;;
esac
}
update_cron(){
local s_enable s_period s_time_on s_duration s_period_days s_weekday
config_get s_enable schedule enable "0"
config_get s_period schedule period "daily"
config_get s_time_on schedule time_on "00:00"
config_get s_duration schedule duration "0"
config_get s_period_days schedule period_days "1"
config_get s_weekday schedule weekday "1"
# Remove old ssw entries
sed -i "/$CRON_TAG/d" "$CRON_FILE" 2>/dev/null
[ "$s_enable" != "1" ] && {
/etc/init.d/cron reload 2>/dev/null
return
}
local fields cron_on cron_off
fields=$(cron_period_fields "$s_period" "$s_period_days" "$s_weekday")
cron_on=$(time_to_cron "$s_time_on")
cron_off=$(time_add_min "$s_time_on" "$s_duration")
printf "%s %s /usr/share/ssw_schedule.sh %s\n" \
"$cron_on" "$fields" "$CRON_TAG" >> "$CRON_FILE"
if [ "$s_duration" -gt 0 ] 2>/dev/null; then
printf "%s %s /usr/share/ssw_schedule.sh revert %s\n" \
"$cron_off" "$fields" "$CRON_TAG" >> "$CRON_FILE"
fi
/etc/init.d/cron reload 2>/dev/null
logger -t "SW SIM" "Schedule updated: on=${s_time_on} duration=${s_duration}min period=${s_period}"
}
start_service(){ start_service(){
config_load ssw config_load ssw
config_foreach load_config_gpio modem config_foreach load_config_gpio modem
@ -62,13 +141,21 @@ start_service(){
_apply_modem_state _apply_modem_state
fi fi
update_cron
procd_open_instance procd_open_instance
procd_set_param command /usr/share/ssw_track.sh procd_set_param command /usr/share/ssw_track.sh
procd_close_instance procd_close_instance
} }
stop_service(){
sed -i "/$CRON_TAG/d" "$CRON_FILE" 2>/dev/null
/etc/init.d/cron reload 2>/dev/null
}
reload_service(){ reload_service(){
config_load ssw
update_cron
start_service start_service
} }

View File

@ -0,0 +1,63 @@
#!/bin/sh
# SIM slot switcher - schedule handler
# Called by cron, generated by /etc/init.d/ssw
# by Konstantine Shevlakov at <shevlakov@132lan.ru> 2025
NODE="SW SIM SCHED"
get_vars() {
for d in modem sim; do
for s in gpio value; do
eval "${d}_${s}=$(uci -q get ssw.${d}.${s} 2>/dev/null)"
done
done
}
reload_iface() {
local iface
iface=$(uci show network | awk -F'[.]' '/devices/{gsub("'\''",""); print $2}' | tail -1)
[ -n "$iface" ] && ifup "$iface"
}
sw_sim() {
local cur_sim next_sim
cur_sim=$(cat /sys/class/gpio/"$sim_gpio"/value)
case "$cur_sim" in
0) next_sim=1 ;;
1) next_sim=0 ;;
*) logger -t "$NODE" "ERROR: unexpected sim gpio value: $cur_sim"; exit 1 ;;
esac
logger -t "$NODE" "Switching SIM: slot $cur_sim -> slot $next_sim"
echo "0" > /sys/class/gpio/"$modem_gpio"/value
echo "$next_sim" > /sys/class/gpio/"$sim_gpio"/value
if [ "$modem_value" = "1" ]; then
sleep 2
echo "1" > /sys/class/gpio/"$modem_gpio"/value
if [ -x /etc/init.d/smstools3 ]; then
/etc/init.d/smstools3 restart
fi
fi
sleep 5 && reload_iface &
}
sw_revert() {
local cur_sim
cur_sim=$(cat /sys/class/gpio/"$sim_gpio"/value)
if [ "$cur_sim" != "$sim_value" ]; then
logger -t "$NODE" "Reverting SIM to default slot $sim_value"
sw_sim
else
logger -t "$NODE" "SIM already on default slot $sim_value, nothing to do"
fi
}
# --- main ---
get_vars
case "$1" in
revert) sw_revert ;;
*) sw_sim ;;
esac

View File

@ -58,18 +58,6 @@ get_vars(){
[ -n "$interval" ] || interval=60 [ -n "$interval" ] || interval=60
[ -n "$times_rsrp" ] || times_rsrp=5 [ -n "$times_rsrp" ] || times_rsrp=5
# Schedule variables
sched_enable=$(uci -q get ssw.schedule.enable 2>/dev/null)
sched_time_on=$(uci -q get ssw.schedule.time_on 2>/dev/null)
sched_duration=$(uci -q get ssw.schedule.duration 2>/dev/null)
sched_apn=$(uci -q get ssw.schedule.apn 2>/dev/null)
sched_period=$(uci -q get ssw.schedule.period 2>/dev/null)
sched_period_days=$(uci -q get ssw.schedule.period_days 2>/dev/null)
sched_weekday=$(uci -q get ssw.schedule.weekday 2>/dev/null)
[ -n "$sched_duration" ] || sched_duration=0
[ -n "$sched_period" ] || sched_period=daily
[ -n "$sched_period_days" ] || sched_period_days=1
[ -n "$sched_weekday" ] || sched_weekday=1
} }
# SIM Switch # SIM Switch
@ -92,163 +80,6 @@ sw_sim(){
set > /tmp/apn.ssw set > /tmp/apn.ssw
} }
# Revert rule switch (failover)
sw_rule(){
if [ -f /tmp/ssw.vars ]; then
. /tmp/ssw.vars
fi
if [ "$cur_sim" -ne "$sim_value" ]; then
if [ "$(date +%s)" -gt "$SWDATE" ]; then
logger -t "$NODE" "Revert to default SIM slot with $apn"
sw_sim
fi
fi
}
# Schedule: revert to default SIM after duration
sched_revert(){
[ -f /tmp/ssw_sched.vars ] && . /tmp/ssw_sched.vars
if [ -n "$SCHED_REVERT_AT" ] && [ "$(date +%s)" -ge "$SCHED_REVERT_AT" ]; then
cur_sim=$(cat /sys/class/gpio/$sim_gpio/value)
if [ "$cur_sim" -ne "$sim_value" ]; then
logger -t "$NODE" "Schedule: reverting to default SIM slot"
iface=$(uci show network | awk -F [.] '/devices/{gsub("'\''","");print $2}' | tail -1)
uci set network.$iface.apn="${apn1:-internet}"
uci commit network
reload_config network
sw_sim && sleep 20 && reload_iface &
fi
rm -f /tmp/ssw_sched.vars
logger -t "$NODE" "Schedule: revert done, schedule state cleared"
fi
}
# Check if today matches the configured schedule period.
# Returns 0 (true) if the switch should fire today, 1 otherwise.
sched_day_match(){
today=$(date +%Y%m%d)
now_dow=$(date +%w) # 0=Sun .. 6=Sat
case "$sched_period" in
daily)
return 0
;;
weekly)
[ "$now_dow" = "$sched_weekday" ] && return 0
return 1
;;
interval)
# Read the date of the last successful switch from persistent state.
# We keep it in /tmp/ssw_sched_last.date so it survives across
# the daemon's iterations but resets on reboot (intentional —
# after reboot the interval counter restarts from today).
last_date=""
[ -f /tmp/ssw_sched_last.date ] && last_date=$(cat /tmp/ssw_sched_last.date)
if [ -z "$last_date" ]; then
# First run ever (or after reboot): trigger today.
return 0
fi
# Calculate elapsed days between last_date and today (YYYYMMDD arithmetic).
# Use date to convert both to unix timestamps then divide.
ts_last=$(date -d "$last_date" +%s 2>/dev/null)
[ -z "$ts_last" ] && ts_last=$(date -jf "%Y%m%d" "$last_date" +%s 2>/dev/null)
ts_now=$(date +%s)
elapsed_days=$(( (ts_now - ts_last) / 86400 ))
[ "$elapsed_days" -ge "$sched_period_days" ] && return 0
return 1
;;
*)
# Unknown period — treat as daily
return 0
;;
esac
}
# Schedule: switch to reserve SIM at configured time
check_schedule(){
[ "$sched_enable" = "1" ] || return
[ -n "$sched_time_on" ] || return
# Handle pending revert first — takes priority over new switch logic.
if [ -f /tmp/ssw_sched.vars ]; then
sched_revert
return
fi
# Parse HH:MM (strip leading zeros to avoid octal)
sched_h=$(echo "$sched_time_on" | cut -d: -f1 | sed 's/^0*//')
sched_m=$(echo "$sched_time_on" | cut -d: -f2 | sed 's/^0*//')
[ -z "$sched_h" ] && sched_h=0
[ -z "$sched_m" ] && sched_m=0
now_h=$(date +%H | sed 's/^0*//')
now_m=$(date +%M | sed 's/^0*//')
[ -z "$now_h" ] && now_h=0
[ -z "$now_m" ] && now_m=0
# Check time window: [sched_time .. sched_time + ceil(interval/60)) minutes
now_total=$(( now_h * 60 + now_m ))
sched_total=$(( sched_h * 60 + sched_m ))
interval_min=$(( interval / 60 + 1 ))
diff=$(( now_total - sched_total ))
# Not in the time window yet
if [ "$diff" -lt 0 ] || [ "$diff" -ge "$interval_min" ]; then
return
fi
# Check if the period condition is met (daily / weekly / every N days)
sched_day_match || return
# Check if we already fired today (prevents re-triggering within the same window)
today=$(date +%Y%m%d)
if [ -f /tmp/ssw_sched_fired.date ]; then
fired=$(cat /tmp/ssw_sched_fired.date)
[ "$fired" = "$today" ] && return
fi
# All conditions met — switch to reserve SIM
cur_sim=$(cat /sys/class/gpio/$sim_gpio/value)
if [ "$cur_sim" -ne "$sim_value" ]; then
# Already on reserve SIM (e.g. failover active), skip but record the day.
logger -t "$NODE" "Schedule: already on reserve SIM, skipping switch"
echo "$today" > /tmp/ssw_sched_fired.date
return
fi
logger -t "$NODE" "Schedule: switching to reserve SIM (period=$sched_period, time=$sched_time_on)"
iface=$(uci show network | awk -F [.] '/devices/{gsub("'\''","");print $2}' | tail -1)
apn_use="${sched_apn:-${apn2:-internet}}"
uci set network.$iface.apn="$apn_use"
uci commit network
reload_config network
sw_sim && sleep 20 && reload_iface &
# Record fire date (suppress re-trigger within same day/window)
echo "$today" > /tmp/ssw_sched_fired.date
# For interval mode: record as the last successful switch date
if [ "$sched_period" = "interval" ]; then
echo "$today" > /tmp/ssw_sched_last.date
fi
# Arm the revert timer if duration > 0
if [ "$sched_duration" -gt 0 ]; then
revert_at=$(( $(date +%s) + sched_duration * 60 ))
echo "SCHED_REVERT_AT=$revert_at" > /tmp/ssw_sched.vars
logger -t "$NODE" "Schedule: will revert to default SIM in ${sched_duration} min"
else
logger -t "$NODE" "Schedule: no auto-revert configured"
fi
}
# Check interface state via mwan3 # Check interface state via mwan3
monitor_mwan3(){ monitor_mwan3(){
iface=$(uci show network | awk -F [.] '/devices/{print $2}') iface=$(uci show network | awk -F [.] '/devices/{print $2}')
@ -268,10 +99,10 @@ monitor_rsrp(){
CRSRP=$(echo "$SIGNAL" | jsonfilter -e '@["modem"][*]["5g"]["rsrp"]' | awk '{printf "%.0f\n", $1}') CRSRP=$(echo "$SIGNAL" | jsonfilter -e '@["modem"][*]["5g"]["rsrp"]' | awk '{printf "%.0f\n", $1}')
fi fi
if [ $CRSRP -ne 0 ]; then if [ -n "$CRSRP" ] && [ "$CRSRP" -ne "0" ]; then
echo $CRSRP >> /tmp/ssw_rsrp.var echo $CRSRP >> /tmp/ssw_rsrp.var
fi fi
if [ $cnt -eq $times_rsrp ]; then if [ "$cnt" -eq "$times_rsrp" ]; then
RSRP=$(awk '{sum+=$1} END { printf "%.0f\n", sum/NR }' /tmp/ssw_rsrp.var) RSRP=$(awk '{sum+=$1} END { printf "%.0f\n", sum/NR }' /tmp/ssw_rsrp.var)
cat /dev/null > /tmp/ssw_rsrp.var cat /dev/null > /tmp/ssw_rsrp.var
if [ $RSRP -lt $rsrp ]; then if [ $RSRP -lt $rsrp ]; then
@ -297,9 +128,6 @@ while true; do
get_vars get_vars
sleep $interval sleep $interval
# Schedule check runs every iteration, independently of failover
check_schedule
if [ "$enable" = "1" ]; then if [ "$enable" = "1" ]; then
cur_sim=$(cat /sys/class/gpio/$sim_gpio/value) cur_sim=$(cat /sys/class/gpio/$sim_gpio/value)
monitor_rsrp monitor_rsrp
@ -333,22 +161,8 @@ while true; do
uci commit network uci commit network
reload_config network reload_config network
if [ "$revert" = "1" ]; then
if [ "$cur_sim" -eq "$sim_value" ]; then
FBT=${FBT:=$((($interval+$times_rsrp)*2))}
FBT=$(($FBT*2))
SWDATE=$((`date +%s`+$FBT))
echo "FBT=$FBT" > /tmp/ssw.vars
echo SWDATE=$SWDATE >> /tmp/ssw.vars
logger -t "$NODE" "Back to default SIM-slot after $(date -d @${SWDATE})"
fi
fi
sw_sim && sleep 20 && reload_iface & sw_sim && sleep 20 && reload_iface &
fi fi
if [ "$revert" = "1" ]; then
sw_rule && sleep 20 && reload_iface &
fi
cnt=0 cnt=0
else else
cnt=$(($cnt+1)) cnt=$(($cnt+1))