blob: 9e4bf536d64f1605845a91d68e928a67260f3d1e [file] [edit]
/*
* This file is part of the UWB stack for linux.
*
* Copyright (c) 2020-2022 Qorvo US, Inc.
*
* This software is provided under the GNU General Public License, version 2
* (GPLv2), as well as under a Qorvo commercial license.
*
* You may choose to use this software under the terms of the GPLv2 License,
* version 2 ("GPLv2"), as published by the Free Software Foundation.
* You should have received a copy of the GPLv2 along with this program. If
* not, see <http://www.gnu.org/licenses/>.
*
* This program is distributed under the GPLv2 in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GPLv2 for more
* details.
*
* If you cannot meet the requirements of the GPLv2, you may not use this
* software for any purpose without first obtaining a commercial license from
* Qorvo. Please contact Qorvo to inquire about licensing terms.
*/
#include <linux/slab.h>
#include <linux/netdevice.h>
#include <linux/errno.h>
#include <net/mcps802154_schedule.h>
#include <net/fira_region_nl.h>
#include "fira_region.h"
#include "fira_region_call.h"
#include "fira_access.h"
#include "fira_session.h"
#include "fira_crypto.h"
#include "warn_return.h"
static struct mcps802154_region_ops fira_region_ops;
static bool do_crypto_selftest_on_module_init;
static void fira_report_event(struct work_struct *work)
{
struct fira_local *local =
container_of(work, struct fira_local, report_work);
struct sk_buff *skb;
int r;
while (!skb_queue_empty(&local->report_queue)) {
skb = skb_dequeue(&local->report_queue);
r = mcps802154_region_event(local->llhw, skb);
if (r == -ECONNREFUSED)
/* TODO stop. */
;
}
}
static struct mcps802154_region *fira_open(struct mcps802154_llhw *llhw)
{
struct fira_local *local;
local = kzalloc(sizeof(*local), GFP_KERNEL);
if (!local)
return NULL;
local->llhw = llhw;
local->region.ops = &fira_region_ops;
INIT_LIST_HEAD(&local->inactive_sessions);
INIT_LIST_HEAD(&local->active_sessions);
skb_queue_head_init(&local->report_queue);
INIT_WORK(&local->report_work, fira_report_event);
/* FIXME: Hack to simplify unit test, which is borderline. */
local->block_duration_rx_margin_ppm = UWB_BLOCK_DURATION_MARGIN_PPM;
fira_crypto_init(NULL);
return &local->region;
}
static void fira_close(struct mcps802154_region *region)
{
struct fira_local *local = region_to_local(region);
struct fira_session *session, *s;
list_for_each_entry_safe (session, s, &local->inactive_sessions,
entry) {
fira_session_free(local, session);
}
cancel_work_sync(&local->report_work);
skb_queue_purge(&local->report_queue);
kfree_sensitive(local);
}
static void fira_notify_stop(struct mcps802154_region *region)
{
struct fira_local *local = region_to_local(region);
struct fira_session *session, *s;
list_for_each_entry_safe (session, s, &local->active_sessions, entry) {
fira_session_stop_controlees(session);
fira_session_fsm_stop(local, session);
}
}
static int fira_call(struct mcps802154_region *region, u32 call_id,
const struct nlattr *attrs, const struct genl_info *info)
{
struct fira_local *local = region_to_local(region);
switch (call_id) {
case FIRA_CALL_GET_CAPABILITIES:
return fira_get_capabilities(local, info);
default:
return fira_session_control(local, call_id, attrs, info);
}
}
/**
* fira_session_init_block_start_dtu() - Build the first block start dtu.
* @local: FiRa context.
* @session: Session context.
* @timestamp_dtu: First access opportunity.
*/
static void fira_session_init_block_start_dtu(struct fira_local *local,
struct fira_session *session,
u32 timestamp_dtu)
{
if (!session->block_start_valid) {
const struct fira_session_params *params = &session->params;
int dtu_freq_khz = local->llhw->dtu_freq_hz / 1000;
session->block_start_valid = true;
session->block_start_dtu =
timestamp_dtu +
params->initiation_time_ms * dtu_freq_khz;
session->next_access_timestamp_dtu = session->block_start_dtu;
}
}
/**
* fira_get_next_session() - Find the next session which should have the
* access.
* @local: FiRa context.
* @next_timestamp_dtu: Next access opportunity.
* @max_duration_dtu: Max duration of the next access opportunity.
* @adopted_demand: Output updated related to next session returned.
*
* Return: Pointer to the next session which should have the access.
*/
static struct fira_session *
fira_get_next_session(struct fira_local *local, u32 next_timestamp_dtu,
int max_duration_dtu,
struct fira_session_demand *adopted_demand)
{
struct fira_session *adopted_session = NULL;
struct fira_session *session;
bool is_candidate_unsync, is_adopted_unsync;
int max_unsync_duration_dtu = max_duration_dtu;
int r;
/*
* Little cheat to start all sessions as initiation_time_ms is a
* delay, and not a absolute time. This is the only function
* which change the session content.
*/
list_for_each_entry (session, &local->active_sessions, entry) {
fira_session_init_block_start_dtu(local, session,
next_timestamp_dtu);
}
/*
* Reminder: active_sessions list is sorted by session->priority
* from highest priority to lowest priority.
*/
list_for_each_entry (session, &local->active_sessions, entry) {
/*
* Welcome in sessions election!
*
* First, the candidate session will provide its wish in
* 'candidate_demand'.
* And then the candidate will be compared with the adopted
* session. The "best" will be become or stay the adopted
* session.
* So the session election will process candidate after
* candidate, to find the most appropriate session.
*/
struct fira_session_demand candidate_demand;
/*
* Sessions with lower priority are not allowed to overlap
* the adopted session. But a lower priority can start and
* stop before the session adopted.
*/
if (adopted_session && adopted_session->params.priority !=
session->params.priority) {
/* Is there some time left? */
if (is_before_dtu(next_timestamp_dtu,
adopted_demand->timestamp_dtu))
/*
* Limit max duration for session with lower
* priority to not overlap sessions which have
* an higher priority.
*/
max_duration_dtu =
adopted_demand->timestamp_dtu -
next_timestamp_dtu;
else
/* No more time left. */
break;
if (!max_unsync_duration_dtu ||
max_unsync_duration_dtu > max_duration_dtu)
max_unsync_duration_dtu = max_duration_dtu;
}
is_candidate_unsync = session->params.device_type ==
FIRA_DEVICE_TYPE_CONTROLEE &&
!session->controlee.synchronised;
/* Retrieve the wish of the session candidate. */
r = fira_session_fsm_get_demand(
local, session, next_timestamp_dtu,
is_candidate_unsync ? max_unsync_duration_dtu :
max_duration_dtu,
&candidate_demand);
/* When 'r' is one, the session have a demand. */
if (r != 1)
/* The session doesn't have a demand. */
continue;
/*
* If there is no adopted session, the candidate is the
* adopted session.
*/
if (!adopted_session)
goto candidate_adopted;
/*
* Is session finish before the adopted session ?
* adopted_demand | [-----]
* candidate | [------]
* --+-----------------------> Time
*/
if (is_before_dtu(candidate_demand.timestamp_dtu +
candidate_demand.max_duration_dtu,
adopted_demand->timestamp_dtu))
/*
* Candidate is adopted and replace the
* previous one.
*/
goto candidate_adopted;
/*
* Is session start after the adopted session ?
* adopted_demand | [------]
* candidate | [--------]
* --+----------------------> Time
*/
if (is_before_dtu(adopted_demand->timestamp_dtu +
adopted_demand->max_duration_dtu,
candidate_demand.timestamp_dtu))
/* Candidate is not adopted. */
continue;
/*
* The candidate session have an overlap with the adopted
* session. Try the negotiation first to find an agreement
* about the access usage.
*
* But take care, synchronized session have a better
* eloquence in case of negotiation failure with an
* unsynchronized session.
*/
is_adopted_unsync = adopted_session->params.device_type ==
FIRA_DEVICE_TYPE_CONTROLEE &&
!adopted_session->controlee.synchronised;
/*
* The candidate session have an overlap with the adopted
* session.
*
* adopted_demand | [------]
* candidate | [--------]
* --+----------------------> Time
*
* Request a duration reduction to the adopted session.
*/
if (is_adopted_unsync &&
!is_before_dtu(candidate_demand.timestamp_dtu,
adopted_demand->timestamp_dtu)) {
int limit_duration_dtu =
candidate_demand.timestamp_dtu -
adopted_demand->timestamp_dtu;
struct fira_session_demand tmp;
if (limit_duration_dtu)
/* Ask to reduce the duration. */
r = fira_session_fsm_get_demand(
local, adopted_session,
next_timestamp_dtu, limit_duration_dtu,
&tmp);
else
/* Both sessions start at same time. */
r = 0;
if (r == 1) {
/*
* The adopted session accept to
* reduction its max duration.
*/
*adopted_demand = tmp;
max_unsync_duration_dtu = limit_duration_dtu;
continue;
}
if (!is_candidate_unsync)
/*
* In this corrupted world, synchronized
* session have better relation.
*/
goto candidate_adopted;
}
/*
* The candidate session have an overlap with the adopted
* session.
*
* adopted_demand | [-----]
* candidate | [------]
* --+----------------------> Time
*
* Request a duration reduction to the candidate session.
*/
if (is_candidate_unsync &&
!is_before_dtu(adopted_demand->timestamp_dtu,
candidate_demand.timestamp_dtu)) {
int limit_duration_dtu = adopted_demand->timestamp_dtu -
candidate_demand.timestamp_dtu;
struct fira_session_demand tmp;
if (limit_duration_dtu)
/* Ask to reduce the duration. */
r = fira_session_fsm_get_demand(
local, session, next_timestamp_dtu,
limit_duration_dtu, &tmp);
else
/* Both sessions start at same time. */
r = 0;
if (r == 1) {
/*
* The candidate session accept to
* reduction its max duration.
*/
adopted_session = session;
*adopted_demand = tmp;
max_unsync_duration_dtu = limit_duration_dtu;
continue;
}
if (!is_adopted_unsync)
/*
* In this corrupted world, synchronized
* session have better relation.
*/
continue;
}
/*
* Finally, negotiation between adopted and candidate fails.
* One of the session will probably have ranging not done.
* Choose the session which have the oldest access.
*/
if (is_before_dtu(session->last_access_timestamp_dtu,
adopted_session->last_access_timestamp_dtu))
goto candidate_adopted;
/* Candidate is not adopted. */
continue;
candidate_adopted:
adopted_session = session;
*adopted_demand = candidate_demand;
}
return adopted_session;
}
static struct mcps802154_access *
fira_get_access(struct mcps802154_region *region, u32 next_timestamp_dtu,
int next_in_region_dtu, int region_duration_dtu)
{
struct fira_local *local = region_to_local(region);
/* 'fsd' acronyms is FiRa Session Demand. */
struct fira_session_demand fsd;
struct fira_session *session;
int max_duration_dtu =
region_duration_dtu ? region_duration_dtu - next_in_region_dtu :
0;
session = fira_get_next_session(local, next_timestamp_dtu,
max_duration_dtu, &fsd);
if (session)
return fira_session_fsm_get_access(local, session, &fsd);
return NULL;
}
static int fira_get_demand(struct mcps802154_region *region,
u32 next_timestamp_dtu,
struct mcps802154_region_demand *next_demand)
{
struct fira_local *local = region_to_local(region);
/* 'fsd' for FiRa Session Demand. */
struct fira_session_demand fsd;
struct fira_session *session;
session = fira_get_next_session(local, next_timestamp_dtu, 0, &fsd);
if (session) {
next_demand->timestamp_dtu = fsd.timestamp_dtu;
next_demand->max_duration_dtu = fsd.max_duration_dtu;
return 1;
}
return 0;
}
static struct mcps802154_region_ops fira_region_ops = {
.owner = THIS_MODULE,
.name = "fira",
.open = fira_open,
.close = fira_close,
.notify_stop = fira_notify_stop,
.call = fira_call,
.get_access = fira_get_access,
.get_demand = fira_get_demand,
};
struct fira_session *fira_get_session_by_session_id(struct fira_local *local,
u32 session_id)
{
struct fira_session *session;
list_for_each_entry (session, &local->active_sessions, entry) {
if (session->id == session_id)
return session;
}
list_for_each_entry (session, &local->inactive_sessions, entry) {
if (session->id == session_id)
return session;
}
return NULL;
}
void fira_check_all_missed_ranging(struct fira_local *local,
const struct fira_session *recent_session,
u32 timestamp_dtu)
{
struct fira_session *session, *s;
/*
* Process sessions with safe function, as the session FSM can leave
* the active list for many stop reasons.
*/
list_for_each_entry_safe (session, s, &local->active_sessions, entry) {
if (recent_session == session)
continue;
/*
* Does the session started during the access of
* recent_session?
*/
if (!session->block_start_valid)
continue;
fira_session_fsm_check_missed_ranging(local, session,
timestamp_dtu);
}
}
int __init fira_region_init(void)
{
if (do_crypto_selftest_on_module_init)
WARN_RETURN(fira_crypto_test());
return mcps802154_region_register(&fira_region_ops);
}
void __exit fira_region_exit(void)
{
mcps802154_region_unregister(&fira_region_ops);
}
module_param_named(crypto_selftest, do_crypto_selftest_on_module_init, bool, 0644);
module_init(fira_region_init);
module_exit(fira_region_exit);
MODULE_DESCRIPTION("FiRa Region for IEEE 802.15.4 MCPS");
MODULE_AUTHOR("Nicolas Schodet <[email protected]>");
MODULE_VERSION("1.0");
MODULE_LICENSE("GPL v2");