Files
wlan-ap/feeds/ucentral/ucentral-state/files/ucentral-state
Paul White 34e4a01e25 ucentral-state: Respect LED config before enabling
Ensure that LEDs are configured to be ON before attempting to change their state.

Previously, if the LED was configured to be OFF, it would still enter a double-blink
state when the cloud connection was lost, and then switch to solid ON upon
reconnection—ignoring the configured OFF state.

This update changes that behavior:
    - If LEDs are configured OFF, they will remain OFF even during cloud
      disconnection (no double-blink).
    - After temporary state changes (e.g., during factory reset), the LED will
      return to its configured state (either OFF or ON).

Signed-off-by: Paul White <paul@shasta.cloud>
2025-08-20 08:16:08 +02:00

301 lines
7.2 KiB
Plaintext
Executable File

#!/usr/bin/ucode
'use strict';
import * as libubus from 'ubus';
import * as libuci from 'uci';
import * as uloop from 'uloop';
import * as nl80211 from 'nl80211';
import * as rtnl from 'rtnl';
import * as fs from 'fs';
import { ulog_open, ulog, ULOG_SYSLOG, ULOG_STDIO, LOG_DAEMON, LOG_INFO } from 'log';
ulog_open(ULOG_SYSLOG, LOG_DAEMON, "state");
uloop.init();
let ubus = libubus.connect();
let uci = libuci.cursor();
let config;
let offline_timer;
let current_state;
let online = false;
let leds_off = false;
function self_healing() {
let heal_wifi = false;
let health_stat = json(fs.readfile('/tmp/ucentral.health'));
let last_nw_restart_ts = int(fs.readfile('/tmp/ucentral.nw_restart_ts')) || 0;
let time_passed_since_nw_restart = time() - last_nw_restart_ts;
if (health_stat) {
if (health_stat.data.rrm_chanutil == false) {
// RRM with Channel utilization abnormal, restart rrmd
ulog(LOG_INFO, 'RRM with Channel utilization abnormal, restarting rrmd\n');
system('/etc/init.d/rrmd restart');
}
if (health_stat.sanity != 100) {
for (let iface in health_stat.data.interfaces) {
let iface_data = health_stat.data.interfaces[iface];
if (iface_data.ssids) {
// one of the VAPs have an issue: flag up!
heal_wifi = true;
ulog(LOG_INFO, 'Time passed since last network restart = %d seconds\n', time_passed_since_nw_restart);
break;
}
}
} else {
// all VAPs are healthy, no need to heal anything
return;
}
}
if (fs.stat('/tmp/rrm_timestamp')) {
let rrm_chan_switch_flag = int(fs.readfile('/tmp/rrm_chan_switch')) || 0;
let last_rrm_timestamp = int(fs.readfile('/tmp/rrm_timestamp'));
let time_passed_since_rrm = time() - last_rrm_timestamp;
if (rrm_chan_switch_flag == 1) {
// RRM chan switch in progress, do not restart network!
ulog(LOG_INFO, 'RRM channel switch in progress, cannot restart network \n');
heal_wifi = false;
}
if (time_passed_since_rrm < 180) {
// RRM in progress, do not restart network!
ulog(LOG_INFO, 'RRM with Channel utilization may still be in progress, cannot restart network \n');
heal_wifi = false;
}
}
// keep a gap of at least 5 minutes between network restarts
if (heal_wifi && time_passed_since_nw_restart > 300) {
ulog(LOG_INFO, 'Restarting network \n');
// update network restart timestamp
let f = fs.open("/tmp/ucentral.nw_restart_ts", "w");
if (f) {
f.write(time());
f.close();
}
// restart network
system('/etc/init.d/network restart');
}
}
let healthcheck;
healthcheck = {
run: function(delay) {
if (healthcheck.pid)
healthcheck.pid.delete();
ulog(LOG_INFO, 'start healthcheck in ' + delay / 1000 + ' seconds\n');
if (healthcheck.interval)
healthcheck.interval.set(delay);
else
healthcheck.interval = uloop.interval(delay, healthcheck.spawn);
},
complete: function() {
self_healing();
},
spawn: function() {
ulog(LOG_INFO, 'healthcheck execute\n');
healthcheck.pid = uloop.process('/usr/share/ucentral/health.uc', [], {}, healthcheck.complete);
},
};
let state;
state = {
run: function(delay) {
if (state.pid)
state.pid.delete();
ulog(LOG_INFO, 'start state in ' + delay / 1000 + ' seconds\n');
if (state.interval)
state.interval.set(delay);
else
state.interval = uloop.interval(delay, state.spawn);
},
complete: function() {
},
spawn: function() {
if (!online) {
ulog(LOG_INFO, 'offline - skipping state execution\n');
return;
}
ulog(LOG_INFO, 'state execute\n');
state.pid = uloop.process('/usr/share/ucentral/state.uc', [], {}, state.complete);
},
};
function offline_handler() {
let status = ubus.call('ucentral', 'status');
if (status?.connected)
return;
ulog(LOG_INFO, 'going offline\n');
ubus.call('network.interface.admin_ui', 'up');
}
function online_handler() {
ulog(LOG_INFO, 'going online\n');
ubus.call('network.interface.admin_ui', 'down');
if (offline_timer) {
offline_timer.cancel();
}
}
function config_load() {
ulog(LOG_INFO, 'loading config\n');
uci.load('system');
let led_off_cfg = uci.get("system", "@system[0]", "leds_off");
if (led_off_cfg == 1) {
leds_off = true;
}
uci.load('state');
config = uci.get_all('state');
if (healthcheck?.interval)
healthcheck.interval.cancel();
if (config?.health?.interval)
healthcheck.run(config?.health?.interval * 1000);
if (state?.interval)
state.interval.cancel();
if (config?.stats?.interval)
state.run(config?.stats?.interval * 1000);
let status = ubus.call('ucentral', 'status');
if (status?.connected) {
online_handler();
online = true;
} else if (config.ui.offline_trigger)
offline_timer = uloop.timer(config.ui.offline_trigger * 1000, offline_handler);
}
function led_write(led, property, value) {
let path = '/sys/class/leds/' + led + '/' + property;
let current = trim(fs.readfile(path));
if (current == value)
return;
let file = fs.open(path, 'w');
if (!file)
return;
file.write(value);
file.close();
}
function led_find(alias) {
let path = fs.readfile('/proc/device-tree/aliases/' + alias);
if (!path)
return;
return trim(fs.readfile('/proc/device-tree/' + trim(path) + '/label'));
}
function factory_reset_timeout() {
let led = led_find('led-running');
if (led)
led_write(led, 'trigger', leds-off ? 'none' : 'default-on');
}
let blink_timer;
function blink_timeout() {
if (current_state == 'blink') {
current_state = 'online';
}
system('/etc/init.d/led turnon');
if (!blink_timer)
return;
blink_timer.cancel();
blink_timer = null;
}
let state_handler = {
offline: function() {
online = false;
let led = led_find('led-running');
if (!leds_off && led)
led_write(led, 'trigger', 'heartbeat');
if (config.ui.offline_trigger) {
if (offline_timer)
offline_timer.cancel();
offline_timer = uloop.timer(config.ui.offline_trigger * 1000, offline_handler);
}
return 0;
},
online: function() {
online = true;
let led = led_find('led-running');
if (!leds_off && led)
led_write(led, 'trigger', 'default-on');
online_handler();
return 0;
},
'factory-reset': function() {
let led = led_find('led-running');
if (!led)
return ubus.STATUS_INVALID_ARGUMENT;
led_write(led, 'trigger', 'timer');
led_write(led, 'delay_on', '100');
led_write(led, 'delay_off', '100');
uloop.timer(6000, factory_reset_timeout);
return 0;
},
blink: function(args) {
if (!args.duration) {
blink_timeout();
return 0;
}
system('/etc/init.d/led blink');
if (args.duration != 0xffff)
blink_timer = uloop.timer((args.duration || 10) * 1000, blink_timeout);
return 0;
},
};
let ubus_methods = {
set: {
call: function(req) {
ulog(LOG_INFO, 'state %s -> %s\n', current_state, req.args.state);
if (current_state == req.args.state && req.args.state != 'blink')
return;
if (!state_handler[req.args.state])
return ubus.STATUS_INVALID_ARGUMENT;
if (current_state != req.args.state)
ulog(LOG_INFO, 'state %s -> %s\n', current_state, req.args.state);
current_state = req.args.state;
blink_timeout();
ulog(LOG_INFO, 'set state -> ' + req.args.state + '\n');
return state_handler[req.args.state](req.args);
},
args: {
state:'',
duration: 0
}
},
reload: {
call: function(req) {
config_load();
return 0;
},
args: {
}
},
};
ubus.publish('state', ubus_methods);
config_load();
uloop.run();
uloop.done();