320 lines
11 KiB
C++
320 lines
11 KiB
C++
#include "CompanionDelayedReplies.h"
|
|
#include <string.h>
|
|
|
|
namespace mesh {
|
|
|
|
// Keep complete member initialization shared, not expanded into a large owner.
|
|
#if defined(__GNUC__) && !defined(__clang__)
|
|
__attribute__((noinline, noclone))
|
|
#elif defined(__clang__)
|
|
__attribute__((noinline))
|
|
#endif
|
|
CompanionDelayedReplies::CompanionDelayedReplies() noexcept {}
|
|
|
|
bool CompanionDelayedReplies::due(uint32_t now, uint32_t deadline) {
|
|
return (int32_t)(now - deadline) >= 0;
|
|
}
|
|
|
|
uint32_t CompanionDelayedReplies::retirementDeadline(
|
|
uint32_t now, uint32_t radio_deadline, uint32_t floor) {
|
|
uint32_t remaining = due(now, radio_deadline) ? 0 : radio_deadline - now;
|
|
if (remaining + DELIVERY_GRACE_MS > floor) floor = remaining + DELIVERY_GRACE_MS;
|
|
return now + floor;
|
|
}
|
|
|
|
void CompanionDelayedReplies::reset(Reply& reply) {
|
|
// The frame bytes need not be erased: Empty is the sole ownership authority.
|
|
reply.route = nullptr;
|
|
reply.kind = None;
|
|
reply.phase = Empty;
|
|
reply.history = NO_HISTORY;
|
|
reply.frame_len = 0;
|
|
reply.sent_pending = false;
|
|
}
|
|
|
|
uint32_t CompanionDelayedReplies::radioBudget(const Reply& reply) {
|
|
// Leave room for retirement grace within the signed half of millis range.
|
|
const uint32_t maximum = 0x7FFFFFFFU - DELIVERY_GRACE_MS;
|
|
const uint32_t extra = reply.timeout / 5;
|
|
uint32_t budget = reply.timeout > maximum - extra
|
|
? maximum : reply.timeout + extra;
|
|
if (!reply.terminal) {
|
|
// Official apps start an estimate + 5s timer when they receive SENT.
|
|
const uint32_t host_budget = reply.timeout > maximum - HOST_EXTRA_TIMEOUT_MS
|
|
? maximum : reply.timeout + HOST_EXTRA_TIMEOUT_MS;
|
|
if (host_budget > budget) budget = host_budget;
|
|
}
|
|
return budget;
|
|
}
|
|
|
|
void CompanionDelayedReplies::arm(Reply& reply, uint32_t timeout,
|
|
bool flood, uint32_t now) {
|
|
reply.timeout = timeout;
|
|
reply.flood = flood;
|
|
reply.sent_pending = !reply.terminal;
|
|
reply.sent_deadline = now + DELIVERY_GRACE_MS;
|
|
// Before SENT admission only the bounded pre-SENT grace applies. Terminal
|
|
// output has no SENT frame and retains its printed arm-time radio budget.
|
|
reply.radio_deadline = reply.terminal
|
|
? now + radioBudget(reply) : reply.sent_deadline;
|
|
reply.frame_len = 0;
|
|
reply.phase = AwaitRadio;
|
|
}
|
|
|
|
bool CompanionDelayedReplies::store(Reply& reply, const uint8_t* frame,
|
|
size_t len, uint32_t now) {
|
|
if (reply.phase != AwaitRadio || frame == nullptr || len == 0
|
|
|| len > sizeof(reply.frame) || due(now, reply.radio_deadline)
|
|
|| (reply.sent_pending && due(now, reply.sent_deadline))) return false;
|
|
memcpy(reply.frame, frame, len);
|
|
reply.frame_len = len;
|
|
reply.delivery_deadline = now + DELIVERY_GRACE_MS;
|
|
if (reply.sent_pending
|
|
&& (int32_t)(reply.sent_deadline - reply.delivery_deadline) < 0) {
|
|
reply.delivery_deadline = reply.sent_deadline;
|
|
}
|
|
reply.phase = AwaitAdmission;
|
|
return true;
|
|
}
|
|
|
|
bool CompanionDelayedReplies::service(Reply& reply,
|
|
BaseSerialInterface* serial, uint32_t now) {
|
|
if (reply.phase == Empty || reply.phase == Reserved) return false;
|
|
if (reply.terminal) return due(now, reply.radio_deadline);
|
|
if (serial == nullptr || !serial->isReplyRouteAvailable(reply.route)
|
|
|| due(now, reply.phase == AwaitAdmission
|
|
? reply.delivery_deadline : reply.radio_deadline)
|
|
|| (reply.sent_pending && due(now, reply.sent_deadline))) return true;
|
|
if (reply.sent_pending) {
|
|
uint8_t sent[10];
|
|
sent[0] = 6; // RESP_CODE_SENT, unchanged Companion wire format.
|
|
sent[1] = reply.flood ? 1 : 0;
|
|
memcpy(sent + 2, &reply.tag, 4);
|
|
memcpy(sent + 6, &reply.timeout, 4);
|
|
if (serial->writeFrameToRoute(reply.route, sent, sizeof(sent)) != sizeof(sent)) {
|
|
return false;
|
|
}
|
|
reply.sent_pending = false;
|
|
reply.radio_deadline = now + radioBudget(reply);
|
|
}
|
|
return reply.phase == AwaitAdmission
|
|
&& serial->writeFrameToRoute(reply.route, reply.frame, reply.frame_len)
|
|
== reply.frame_len;
|
|
}
|
|
|
|
void CompanionDelayedReplies::prune(uint32_t now) {
|
|
for (unsigned i = 0; i < HISTORY_SIZE; ++i) {
|
|
if (requests[i].phase == AwaitAdmission && due(now, requests[i].until)) {
|
|
requests[i].phase = Empty;
|
|
}
|
|
if (traces[i].phase == AwaitAdmission && due(now, traces[i].until)) {
|
|
traces[i].phase = Empty;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool CompanionDelayedReplies::reserveRequest(Kind kind, const uint8_t peer[32],
|
|
BaseSerialInterface* route, bool terminal, uint32_t now) {
|
|
prune(now);
|
|
if (hasRequest() || kind == None || kind == Trace || peer == nullptr
|
|
|| (!terminal && route == nullptr)) return false;
|
|
uint8_t free = NO_HISTORY;
|
|
for (unsigned i = 0; i < HISTORY_SIZE; ++i) {
|
|
const RequestGuard& held = requests[i];
|
|
if (held.phase == Empty) {
|
|
free = i;
|
|
} else if (kind == Login && held.login && memcmp(held.peer, peer, 32) == 0) {
|
|
return false;
|
|
}
|
|
}
|
|
if (free == NO_HISTORY) return false;
|
|
RequestGuard& held = requests[free];
|
|
memcpy(held.peer, peer, 32);
|
|
held.tag = 0;
|
|
held.login = kind == Login;
|
|
held.phase = Reserved;
|
|
request.route = route;
|
|
request.kind = kind;
|
|
request.history = free;
|
|
request.terminal = terminal;
|
|
request.radio_deadline = now;
|
|
request.phase = Reserved;
|
|
return true;
|
|
}
|
|
|
|
bool CompanionDelayedReplies::allowRequestTag(uint32_t tag) {
|
|
if (request.phase != Reserved || request.history >= HISTORY_SIZE) return false;
|
|
RequestGuard& held = requests[request.history];
|
|
for (unsigned i = 0; i < HISTORY_SIZE; ++i) {
|
|
if (i != request.history && requests[i].phase != Empty
|
|
&& requests[i].tag == tag && memcmp(requests[i].peer, held.peer, 32) == 0) {
|
|
return false;
|
|
}
|
|
}
|
|
held.tag = tag;
|
|
return true;
|
|
}
|
|
|
|
void CompanionDelayedReplies::rememberLoginTag(uint32_t tag) {
|
|
if (request.kind == Login && request.history < HISTORY_SIZE) {
|
|
requests[request.history].tag = tag;
|
|
}
|
|
}
|
|
|
|
void CompanionDelayedReplies::armRequest(uint32_t tag, uint32_t timeout,
|
|
bool flood, uint32_t now) {
|
|
request.tag = tag;
|
|
if (request.kind != Login && request.history < HISTORY_SIZE) {
|
|
requests[request.history].tag = tag;
|
|
}
|
|
arm(request, timeout, flood, now);
|
|
}
|
|
|
|
void CompanionDelayedReplies::abandonRequest() {
|
|
if (request.history < HISTORY_SIZE) requests[request.history].phase = Empty;
|
|
reset(request);
|
|
}
|
|
|
|
void CompanionDelayedReplies::retireRequest(uint32_t now) {
|
|
if (request.phase == Reserved) {
|
|
abandonRequest();
|
|
return;
|
|
}
|
|
if (request.history < HISTORY_SIZE && requests[request.history].phase == Reserved) {
|
|
RequestGuard& held = requests[request.history];
|
|
held.until = retirementDeadline(now, request.radio_deadline,
|
|
held.login ? LOGIN_QUARANTINE_MS : DELIVERY_GRACE_MS);
|
|
held.phase = AwaitAdmission;
|
|
}
|
|
reset(request);
|
|
}
|
|
|
|
const uint8_t* CompanionDelayedReplies::requestPeer() const {
|
|
return request.history < HISTORY_SIZE ? requests[request.history].peer : nullptr;
|
|
}
|
|
|
|
bool CompanionDelayedReplies::requestMatches(const uint8_t peer[32], uint32_t tag) const {
|
|
const uint8_t* expected = requestPeer();
|
|
return request.phase >= AwaitRadio && expected != nullptr
|
|
&& memcmp(expected, peer, 32) == 0
|
|
&& (request.kind == Login || request.tag == tag);
|
|
}
|
|
|
|
bool CompanionDelayedReplies::blocksResponse(const uint8_t peer[32],
|
|
uint32_t tag, uint32_t now) const {
|
|
// A tag-only match from another identity must not become a generic host reply.
|
|
if (request.phase >= AwaitRadio && request.kind != Login && request.tag == tag
|
|
&& (!requestMatches(peer, tag) || request.phase == AwaitAdmission)) return true;
|
|
for (unsigned i = 0; i < HISTORY_SIZE; ++i) {
|
|
const RequestGuard& held = requests[i];
|
|
if (held.phase == Empty || memcmp(held.peer, peer, 32) != 0) continue;
|
|
if (held.phase == AwaitAdmission && due(now, held.until)) continue;
|
|
if (i == request.history && request.phase == AwaitRadio) continue;
|
|
if (held.login || held.tag == tag) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool CompanionDelayedReplies::storeRequest(const uint8_t* frame, size_t len, uint32_t now) {
|
|
return store(request, frame, len, now);
|
|
}
|
|
|
|
void CompanionDelayedReplies::serviceRequest(BaseSerialInterface* serial, uint32_t now) {
|
|
prune(now);
|
|
if (service(request, serial, now)) retireRequest(now);
|
|
}
|
|
|
|
uint8_t CompanionDelayedReplies::reserveTrace(uint32_t tag, uint32_t auth, uint32_t now) {
|
|
prune(now);
|
|
uint8_t free = NO_HISTORY;
|
|
for (unsigned i = 0; i < HISTORY_SIZE; ++i) {
|
|
const TraceGuard& held = traces[i];
|
|
if (held.phase == Empty) {
|
|
free = i;
|
|
} else if (held.tag == tag && held.auth == auth) {
|
|
return NO_HISTORY;
|
|
}
|
|
}
|
|
if (free != NO_HISTORY) {
|
|
traces[free].tag = tag;
|
|
traces[free].auth = auth;
|
|
traces[free].phase = Reserved;
|
|
}
|
|
return free;
|
|
}
|
|
|
|
void CompanionDelayedReplies::abandonTrace(uint8_t history) {
|
|
if (history < HISTORY_SIZE) traces[history].phase = Empty;
|
|
}
|
|
|
|
void CompanionDelayedReplies::retireTrace(uint8_t history, uint32_t now,
|
|
uint32_t radio_deadline) {
|
|
if (history < HISTORY_SIZE && traces[history].phase == Reserved) {
|
|
traces[history].until = retirementDeadline(now, radio_deadline, DELIVERY_GRACE_MS);
|
|
traces[history].phase = AwaitAdmission;
|
|
}
|
|
}
|
|
|
|
bool CompanionDelayedReplies::reserveBinaryTrace(uint32_t tag, uint32_t auth,
|
|
BaseSerialInterface* route, uint32_t now) {
|
|
if (hasBinaryTrace() || route == nullptr) return false;
|
|
uint8_t history = reserveTrace(tag, auth, now);
|
|
if (history == NO_HISTORY) return false;
|
|
trace.route = route;
|
|
trace.tag = tag;
|
|
trace.auth = auth;
|
|
trace.kind = Trace;
|
|
trace.history = history;
|
|
trace.terminal = false;
|
|
trace.phase = Reserved;
|
|
return true;
|
|
}
|
|
|
|
void CompanionDelayedReplies::armBinaryTrace(uint32_t timeout, uint32_t now) {
|
|
arm(trace, timeout, false, now);
|
|
}
|
|
|
|
void CompanionDelayedReplies::abandonBinaryTrace() {
|
|
abandonTrace(trace.history);
|
|
reset(trace);
|
|
}
|
|
|
|
void CompanionDelayedReplies::retireBinaryTrace(uint32_t now) {
|
|
if (trace.phase == Reserved) {
|
|
abandonBinaryTrace();
|
|
return;
|
|
}
|
|
retireTrace(trace.history, now, trace.radio_deadline);
|
|
reset(trace);
|
|
}
|
|
|
|
bool CompanionDelayedReplies::blocksTrace(uint32_t tag, uint32_t auth, uint32_t now) const {
|
|
for (unsigned i = 0; i < HISTORY_SIZE; ++i) {
|
|
if (traces[i].phase == AwaitAdmission && !due(now, traces[i].until)
|
|
&& traces[i].tag == tag && traces[i].auth == auth) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool CompanionDelayedReplies::storeBinaryTrace(const uint8_t* frame, size_t len, uint32_t now) {
|
|
return store(trace, frame, len, now);
|
|
}
|
|
|
|
void CompanionDelayedReplies::serviceBinaryTrace(BaseSerialInterface* serial, uint32_t now) {
|
|
prune(now);
|
|
if (service(trace, serial, now)) retireBinaryTrace(now);
|
|
}
|
|
|
|
bool CompanionDelayedReplies::hasReplyForRoute(BaseSerialInterface* route, uint32_t now) const {
|
|
const Reply* slots[] = { &request, &trace };
|
|
for (const Reply* reply : slots) {
|
|
if (reply->phase >= AwaitRadio && reply->route == route
|
|
&& !due(now, reply->phase == AwaitAdmission
|
|
? reply->delivery_deadline : reply->radio_deadline)
|
|
&& !(reply->sent_pending && due(now, reply->sent_deadline))) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
} // namespace mesh
|