| /* |
| * Copyright (c) 2019 Peter Bigot Consulting, LLC |
| * Copyright (c) 2020 Nordic Semiconductor ASA |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include <kernel.h> |
| #include <sys/onoff.h> |
| #include <stdio.h> |
| |
| #define SERVICE_REFS_MAX UINT16_MAX |
| |
| /* Confirm consistency of public flags with private flags */ |
| BUILD_ASSERT((ONOFF_FLAG_ERROR | ONOFF_FLAG_ONOFF | ONOFF_FLAG_TRANSITION) |
| < BIT(3)); |
| |
| #define ONOFF_FLAG_PROCESSING BIT(3) |
| #define ONOFF_FLAG_COMPLETE BIT(4) |
| #define ONOFF_FLAG_RECHECK BIT(5) |
| |
| /* These symbols in the ONOFF_FLAGS namespace identify bits in |
| * onoff_manager::flags that indicate the state of the machine. The |
| * bits are manipulated by process_event() under lock, and actions |
| * cued by bit values are executed outside of lock within |
| * process_event(). |
| * |
| * * ERROR indicates that the machine is in an error state. When |
| * this bit is set ONOFF will be cleared. |
| * * ONOFF indicates whether the target/current state is off (clear) |
| * or on (set). |
| * * TRANSITION indicates whether a service transition function is in |
| * progress. It combines with ONOFF to identify start and stop |
| * transitions, and with ERROR to identify a reset transition. |
| * * PROCESSING indicates that the process_event() loop is active. It |
| * is used to defer initiation of transitions and other complex |
| * state changes while invoking notifications associated with a |
| * state transition. This bounds the depth by limiting |
| * active process_event() call stacks to two instances. State changes |
| * initiated by a nested call will be executed when control returns |
| * to the parent call. |
| * * COMPLETE indicates that a transition completion notification has |
| * been received. This flag is set in the notification, and cleared |
| * by process_events() which is invoked from the notification. In |
| * the case of nested process_events() the processing is deferred to |
| * the top invocation. |
| * * RECHECK indicates that a state transition has completed but |
| * process_events() must re-check the overall state to confirm no |
| * additional transitions are required. This is used to simplify the |
| * logic when, for example, a request is received during a |
| * transition to off, which means that when the transition completes |
| * a transition to on must be initiated if the request is still |
| * present. Transition to ON with no remaining requests similarly |
| * triggers a recheck. |
| */ |
| |
| /* Identify the events that can trigger state changes, as well as an |
| * internal state used when processing deferred actions. |
| */ |
| enum event_type { |
| /* No-op event: used to process deferred changes. |
| * |
| * This event is local to the process loop. |
| */ |
| EVT_NOP, |
| |
| /* Completion of a service transition. |
| * |
| * This event is triggered by the transition notify callback. |
| * It can be received only when the machine is in a transition |
| * state (TO-ON, TO-OFF, or RESETTING). |
| */ |
| EVT_COMPLETE, |
| |
| /* Reassess whether a transition from a stable state is needed. |
| * |
| * This event causes: |
| * * a start from OFF when there are clients; |
| * * a stop from ON when there are no clients; |
| * * a reset from ERROR when there are clients. |
| * |
| * The client list can change while the manager lock is |
| * released (e.g. during client and monitor notifications and |
| * transition initiations), so this event records the |
| * potential for these state changes, and process_event() ... |
| * |
| */ |
| EVT_RECHECK, |
| |
| /* Transition to on. |
| * |
| * This is synthesized from EVT_RECHECK in a non-nested |
| * process_event() when state OFF is confirmed with a |
| * non-empty client (request) list. |
| */ |
| EVT_START, |
| |
| /* Transition to off. |
| * |
| * This is synthesized from EVT_RECHECK in a non-nested |
| * process_event() when state ON is confirmed with a |
| * zero reference count. |
| */ |
| EVT_STOP, |
| |
| /* Transition to resetting. |
| * |
| * This is synthesized from EVT_RECHECK in a non-nested |
| * process_event() when state ERROR is confirmed with a |
| * non-empty client (reset) list. |
| */ |
| EVT_RESET, |
| }; |
| |
| static void set_state(struct onoff_manager *mgr, |
| uint32_t state) |
| { |
| mgr->flags = (state & ONOFF_STATE_MASK) |
| | (mgr->flags & ~ONOFF_STATE_MASK); |
| } |
| |
| static int validate_args(const struct onoff_manager *mgr, |
| struct onoff_client *cli) |
| { |
| if ((mgr == NULL) || (cli == NULL)) { |
| return -EINVAL; |
| } |
| |
| int rv = sys_notify_validate(&cli->notify); |
| |
| if ((rv == 0) |
| && ((cli->notify.flags |
| & ~BIT_MASK(ONOFF_CLIENT_EXTENSION_POS)) != 0)) { |
| rv = -EINVAL; |
| } |
| |
| return rv; |
| } |
| |
| int onoff_manager_init(struct onoff_manager *mgr, |
| const struct onoff_transitions *transitions) |
| { |
| if ((mgr == NULL) |
| || (transitions == NULL) |
| || (transitions->start == NULL) |
| || (transitions->stop == NULL)) { |
| return -EINVAL; |
| } |
| |
| *mgr = (struct onoff_manager)ONOFF_MANAGER_INITIALIZER(transitions); |
| |
| return 0; |
| } |
| |
| static void notify_monitors(struct onoff_manager *mgr, |
| uint32_t state, |
| int res) |
| { |
| sys_slist_t *mlist = &mgr->monitors; |
| struct onoff_monitor *mon; |
| struct onoff_monitor *tmp; |
| |
| SYS_SLIST_FOR_EACH_CONTAINER_SAFE(mlist, mon, tmp, node) { |
| mon->callback(mgr, mon, state, res); |
| } |
| } |
| |
| static void notify_one(struct onoff_manager *mgr, |
| struct onoff_client *cli, |
| uint32_t state, |
| int res) |
| { |
| onoff_client_callback cb = |
| (onoff_client_callback)sys_notify_finalize(&cli->notify, res); |
| |
| if (cb) { |
| cb(mgr, cli, state, res); |
| } |
| } |
| |
| static void notify_all(struct onoff_manager *mgr, |
| sys_slist_t *list, |
| uint32_t state, |
| int res) |
| { |
| while (!sys_slist_is_empty(list)) { |
| sys_snode_t *node = sys_slist_get_not_empty(list); |
| struct onoff_client *cli = |
| CONTAINER_OF(node, |
| struct onoff_client, |
| node); |
| |
| notify_one(mgr, cli, state, res); |
| } |
| } |
| |
| static void process_event(struct onoff_manager *mgr, |
| int evt, |
| k_spinlock_key_t key); |
| |
| static void transition_complete(struct onoff_manager *mgr, |
| int res) |
| { |
| k_spinlock_key_t key = k_spin_lock(&mgr->lock); |
| |
| mgr->last_res = res; |
| process_event(mgr, EVT_COMPLETE, key); |
| } |
| |
| /* Detect whether static state requires a transition. */ |
| static int process_recheck(struct onoff_manager *mgr) |
| { |
| int evt = EVT_NOP; |
| uint32_t state = mgr->flags & ONOFF_STATE_MASK; |
| |
| if ((state == ONOFF_STATE_OFF) |
| && !sys_slist_is_empty(&mgr->clients)) { |
| evt = EVT_START; |
| } else if ((state == ONOFF_STATE_ON) |
| && (mgr->refs == 0U)) { |
| evt = EVT_STOP; |
| } else if ((state == ONOFF_STATE_ERROR) |
| && !sys_slist_is_empty(&mgr->clients)) { |
| evt = EVT_RESET; |
| } else { |
| ; |
| } |
| |
| return evt; |
| } |
| |
| /* Process a transition completion. |
| * |
| * If the completion requires notifying clients, the clients are moved |
| * from the manager to the output list for notification. |
| */ |
| static void process_complete(struct onoff_manager *mgr, |
| sys_slist_t *clients, |
| int res) |
| { |
| uint32_t state = mgr->flags & ONOFF_STATE_MASK; |
| |
| if (res < 0) { |
| /* Enter ERROR state and notify all clients. */ |
| *clients = mgr->clients; |
| sys_slist_init(&mgr->clients); |
| set_state(mgr, ONOFF_STATE_ERROR); |
| } else if ((state == ONOFF_STATE_TO_ON) |
| || (state == ONOFF_STATE_RESETTING)) { |
| *clients = mgr->clients; |
| sys_slist_init(&mgr->clients); |
| |
| if (state == ONOFF_STATE_TO_ON) { |
| struct onoff_client *cp; |
| |
| /* Increment reference count for all remaining |
| * clients and enter ON state. |
| */ |
| SYS_SLIST_FOR_EACH_CONTAINER(clients, cp, node) { |
| mgr->refs += 1U; |
| } |
| |
| set_state(mgr, ONOFF_STATE_ON); |
| } else { |
| __ASSERT_NO_MSG(state == ONOFF_STATE_RESETTING); |
| |
| set_state(mgr, ONOFF_STATE_OFF); |
| } |
| if (process_recheck(mgr) != EVT_NOP) { |
| mgr->flags |= ONOFF_FLAG_RECHECK; |
| } |
| } else if (state == ONOFF_STATE_TO_OFF) { |
| /* Any active clients are requests waiting for this |
| * transition to complete. Queue a RECHECK event to |
| * ensure we don't miss them if we don't unlock to |
| * tell anybody about the completion. |
| */ |
| set_state(mgr, ONOFF_STATE_OFF); |
| if (process_recheck(mgr) != EVT_NOP) { |
| mgr->flags |= ONOFF_FLAG_RECHECK; |
| } |
| } else { |
| __ASSERT_NO_MSG(false); |
| } |
| } |
| |
| /* There are two points in the state machine where the machine is |
| * unlocked to perform some external action: |
| * * Initiation of an transition due to some event; |
| * * Invocation of the user-specified callback when a stable state is |
| * reached or an error detected. |
| * |
| * Events received during these unlocked periods are recorded in the |
| * state, but processing is deferred to the top-level invocation which |
| * will loop to handle any events that occurred during the unlocked |
| * regions. |
| */ |
| static void process_event(struct onoff_manager *mgr, |
| int evt, |
| k_spinlock_key_t key) |
| { |
| sys_slist_t clients; |
| uint32_t state = mgr->flags & ONOFF_STATE_MASK; |
| int res = 0; |
| bool processing = ((mgr->flags & ONOFF_FLAG_PROCESSING) != 0); |
| |
| __ASSERT_NO_MSG(evt != EVT_NOP); |
| |
| /* If this is a nested call record the event for processing in |
| * the top invocation. |
| */ |
| if (processing) { |
| if (evt == EVT_COMPLETE) { |
| mgr->flags |= ONOFF_FLAG_COMPLETE; |
| } else { |
| __ASSERT_NO_MSG(evt == EVT_RECHECK); |
| |
| mgr->flags |= ONOFF_FLAG_RECHECK; |
| } |
| |
| goto out; |
| } |
| |
| sys_slist_init(&clients); |
| do { |
| onoff_transition_fn transit = NULL; |
| |
| if (evt == EVT_RECHECK) { |
| evt = process_recheck(mgr); |
| } |
| |
| if (evt == EVT_NOP) { |
| break; |
| } |
| |
| res = 0; |
| if (evt == EVT_COMPLETE) { |
| res = mgr->last_res; |
| process_complete(mgr, &clients, res); |
| /* NB: This can trigger a RECHECK */ |
| } else if (evt == EVT_START) { |
| __ASSERT_NO_MSG(state == ONOFF_STATE_OFF); |
| __ASSERT_NO_MSG(!sys_slist_is_empty(&mgr->clients)); |
| |
| transit = mgr->transitions->start; |
| __ASSERT_NO_MSG(transit != NULL); |
| set_state(mgr, ONOFF_STATE_TO_ON); |
| } else if (evt == EVT_STOP) { |
| __ASSERT_NO_MSG(state == ONOFF_STATE_ON); |
| __ASSERT_NO_MSG(mgr->refs == 0); |
| |
| transit = mgr->transitions->stop; |
| __ASSERT_NO_MSG(transit != NULL); |
| set_state(mgr, ONOFF_STATE_TO_OFF); |
| } else if (evt == EVT_RESET) { |
| __ASSERT_NO_MSG(state == ONOFF_STATE_ERROR); |
| __ASSERT_NO_MSG(!sys_slist_is_empty(&mgr->clients)); |
| |
| transit = mgr->transitions->reset; |
| __ASSERT_NO_MSG(transit != NULL); |
| set_state(mgr, ONOFF_STATE_RESETTING); |
| } else { |
| __ASSERT_NO_MSG(false); |
| } |
| |
| /* Have to unlock and do something if any of: |
| * * We changed state and there are monitors; |
| * * We completed a transition and there are clients to notify; |
| * * We need to initiate a transition. |
| */ |
| bool do_monitors = (state != (mgr->flags & ONOFF_STATE_MASK)) |
| && !sys_slist_is_empty(&mgr->monitors); |
| |
| evt = EVT_NOP; |
| if (do_monitors |
| || !sys_slist_is_empty(&clients) |
| || (transit != NULL)) { |
| uint32_t flags = mgr->flags | ONOFF_FLAG_PROCESSING; |
| |
| mgr->flags = flags; |
| state = flags & ONOFF_STATE_MASK; |
| |
| k_spin_unlock(&mgr->lock, key); |
| |
| if (do_monitors) { |
| notify_monitors(mgr, state, res); |
| } |
| |
| if (!sys_slist_is_empty(&clients)) { |
| notify_all(mgr, &clients, state, res); |
| } |
| |
| if (transit != NULL) { |
| transit(mgr, transition_complete); |
| } |
| |
| key = k_spin_lock(&mgr->lock); |
| mgr->flags &= ~ONOFF_FLAG_PROCESSING; |
| state = mgr->flags & ONOFF_STATE_MASK; |
| } |
| |
| /* Process deferred events. Completion takes priority |
| * over recheck. |
| */ |
| if ((mgr->flags & ONOFF_FLAG_COMPLETE) != 0) { |
| mgr->flags &= ~ONOFF_FLAG_COMPLETE; |
| evt = EVT_COMPLETE; |
| } else if ((mgr->flags & ONOFF_FLAG_RECHECK) != 0) { |
| mgr->flags &= ~ONOFF_FLAG_RECHECK; |
| evt = EVT_RECHECK; |
| } else { |
| ; |
| } |
| |
| state = mgr->flags & ONOFF_STATE_MASK; |
| } while (evt != EVT_NOP); |
| |
| out: |
| k_spin_unlock(&mgr->lock, key); |
| } |
| |
| int onoff_request(struct onoff_manager *mgr, |
| struct onoff_client *cli) |
| { |
| bool add_client = false; /* add client to pending list */ |
| bool start = false; /* trigger a start transition */ |
| bool notify = false; /* do client notification */ |
| int rv = validate_args(mgr, cli); |
| |
| if (rv < 0) { |
| return rv; |
| } |
| |
| k_spinlock_key_t key = k_spin_lock(&mgr->lock); |
| uint32_t state = mgr->flags & ONOFF_STATE_MASK; |
| |
| /* Reject if this would overflow the reference count. */ |
| if (mgr->refs == SERVICE_REFS_MAX) { |
| rv = -EAGAIN; |
| goto out; |
| } |
| |
| rv = state; |
| if (state == ONOFF_STATE_ON) { |
| /* Increment reference count, notify in exit */ |
| notify = true; |
| mgr->refs += 1U; |
| } else if ((state == ONOFF_STATE_OFF) |
| || (state == ONOFF_STATE_TO_OFF) |
| || (state == ONOFF_STATE_TO_ON)) { |
| /* Start if OFF, queue client */ |
| start = (state == ONOFF_STATE_OFF); |
| add_client = true; |
| } else if (state == ONOFF_STATE_RESETTING) { |
| rv = -ENOTSUP; |
| } else { |
| __ASSERT_NO_MSG(state == ONOFF_STATE_ERROR); |
| rv = -EIO; |
| } |
| |
| out: |
| if (add_client) { |
| sys_slist_append(&mgr->clients, &cli->node); |
| } |
| |
| if (start) { |
| process_event(mgr, EVT_RECHECK, key); |
| } else { |
| k_spin_unlock(&mgr->lock, key); |
| |
| if (notify) { |
| notify_one(mgr, cli, state, 0); |
| } |
| } |
| |
| return rv; |
| } |
| |
| int onoff_release(struct onoff_manager *mgr) |
| { |
| bool stop = false; /* trigger a stop transition */ |
| |
| k_spinlock_key_t key = k_spin_lock(&mgr->lock); |
| uint32_t state = mgr->flags & ONOFF_STATE_MASK; |
| int rv = state; |
| |
| if (state != ONOFF_STATE_ON) { |
| if (state == ONOFF_STATE_ERROR) { |
| rv = -EIO; |
| } else { |
| rv = -ENOTSUP; |
| } |
| goto out; |
| } |
| |
| __ASSERT_NO_MSG(mgr->refs > 0); |
| mgr->refs -= 1U; |
| stop = (mgr->refs == 0); |
| |
| out: |
| if (stop) { |
| process_event(mgr, EVT_RECHECK, key); |
| } else { |
| k_spin_unlock(&mgr->lock, key); |
| } |
| |
| return rv; |
| } |
| |
| int onoff_reset(struct onoff_manager *mgr, |
| struct onoff_client *cli) |
| { |
| bool reset = false; |
| int rv = validate_args(mgr, cli); |
| |
| if ((rv >= 0) |
| && (mgr->transitions->reset == NULL)) { |
| rv = -ENOTSUP; |
| } |
| |
| if (rv < 0) { |
| return rv; |
| } |
| |
| k_spinlock_key_t key = k_spin_lock(&mgr->lock); |
| uint32_t state = mgr->flags & ONOFF_STATE_MASK; |
| |
| rv = state; |
| |
| if ((state & ONOFF_FLAG_ERROR) == 0) { |
| rv = -EALREADY; |
| } else { |
| reset = (state != ONOFF_STATE_RESETTING); |
| sys_slist_append(&mgr->clients, &cli->node); |
| } |
| |
| if (reset) { |
| process_event(mgr, EVT_RECHECK, key); |
| } else { |
| k_spin_unlock(&mgr->lock, key); |
| } |
| |
| return rv; |
| } |
| |
| int onoff_cancel(struct onoff_manager *mgr, |
| struct onoff_client *cli) |
| { |
| if ((mgr == NULL) || (cli == NULL)) { |
| return -EINVAL; |
| } |
| |
| int rv = -EALREADY; |
| k_spinlock_key_t key = k_spin_lock(&mgr->lock); |
| uint32_t state = mgr->flags & ONOFF_STATE_MASK; |
| |
| if (sys_slist_find_and_remove(&mgr->clients, &cli->node)) { |
| __ASSERT_NO_MSG((state == ONOFF_STATE_TO_ON) |
| || (state == ONOFF_STATE_TO_OFF) |
| || (state == ONOFF_STATE_RESETTING)); |
| rv = state; |
| } |
| |
| k_spin_unlock(&mgr->lock, key); |
| |
| return rv; |
| } |
| |
| int onoff_monitor_register(struct onoff_manager *mgr, |
| struct onoff_monitor *mon) |
| { |
| if ((mgr == NULL) |
| || (mon == NULL) |
| || (mon->callback == NULL)) { |
| return -EINVAL; |
| } |
| |
| k_spinlock_key_t key = k_spin_lock(&mgr->lock); |
| |
| sys_slist_append(&mgr->monitors, &mon->node); |
| |
| k_spin_unlock(&mgr->lock, key); |
| |
| return 0; |
| } |
| |
| int onoff_monitor_unregister(struct onoff_manager *mgr, |
| struct onoff_monitor *mon) |
| { |
| int rv = -EINVAL; |
| |
| if ((mgr == NULL) |
| || (mon == NULL)) { |
| return rv; |
| } |
| |
| k_spinlock_key_t key = k_spin_lock(&mgr->lock); |
| |
| if (sys_slist_find_and_remove(&mgr->monitors, &mon->node)) { |
| rv = 0; |
| } |
| |
| k_spin_unlock(&mgr->lock, key); |
| |
| return rv; |
| } |
| |
| int onoff_sync_lock(struct onoff_sync_service *srv, |
| k_spinlock_key_t *keyp) |
| { |
| *keyp = k_spin_lock(&srv->lock); |
| return srv->count; |
| } |
| |
| int onoff_sync_finalize(struct onoff_sync_service *srv, |
| k_spinlock_key_t key, |
| struct onoff_client *cli, |
| int res, |
| bool on) |
| { |
| uint32_t state = ONOFF_STATE_ON; |
| |
| /* Clear errors visible when locked. If they are to be |
| * preserved the caller must finalize with the previous |
| * error code. |
| */ |
| if (srv->count < 0) { |
| srv->count = 0; |
| } |
| if (res < 0) { |
| srv->count = res; |
| state = ONOFF_STATE_ERROR; |
| } else if (on) { |
| srv->count += 1; |
| } else { |
| srv->count -= 1; |
| /* state would be either off or on, but since |
| * callbacks are used only when turning on don't |
| * bother changing it. |
| */ |
| } |
| |
| int rv = srv->count; |
| |
| k_spin_unlock(&srv->lock, key); |
| |
| if (cli) { |
| /* Detect service mis-use: onoff does not callback on transition |
| * to off, so no client should have been passed. |
| */ |
| __ASSERT_NO_MSG(on); |
| notify_one(NULL, cli, state, res); |
| } |
| |
| return rv; |
| } |