mirror of
https://github.com/rightup/pyMC_Repeater.git
synced 2026-03-28 17:43:06 +01:00
This commit sets up the initial project structure for the PyMC Repeater Daemon. It includes base configuration files, dependency definitions, and scaffolding for the main daemon service responsible for handling PyMC repeating operations.
935 lines
56 KiB
HTML
935 lines
56 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>pyMC Repeater - Help</title>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<link rel="stylesheet" href="/static/style.css">
|
||
<style>
|
||
.help-container {
|
||
display: flex;
|
||
gap: 2rem;
|
||
max-width: 1400px;
|
||
<div class="table-column">
|
||
<h4>RSSI</h4>
|
||
<div class="column-desc">Received Signal Strength Indicator (dBm)</div>
|
||
<div class="column-detail">
|
||
Measures signal power. <strong>More negative = weaker signal</strong><br>
|
||
<strong>Excellent:</strong> -80 to -100 dBm (strong)<br>
|
||
<strong>Good:</strong> -100 to -120 dBm (acceptable)<br>
|
||
<strong>Poor:</strong> -120 to -140 dBm (weak, may be unreliable)<br>
|
||
<strong>Note:</strong> RSSI is displayed for monitoring but does NOT directly affect packet score calculation. Score is based purely on SNR and packet length, matching the C++ MeshCore algorithm. However, RSSI typically correlates with SNR - better RSSI usually means better SNR.
|
||
</div>
|
||
</div>: 0 auto;
|
||
}
|
||
|
||
.help-sidebar {
|
||
width: 280px;
|
||
position: sticky;
|
||
top: 20px;
|
||
height: fit-content;
|
||
}
|
||
|
||
.help-toc {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 12px;
|
||
padding: 1.5rem;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.help-toc h3 {
|
||
margin: 0 0 1rem 0;
|
||
font-size: 0.9rem;
|
||
text-transform: uppercase;
|
||
color: #60a5fa;
|
||
font-weight: 600;
|
||
letter-spacing: 0.05em;
|
||
}
|
||
|
||
.help-toc ul {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
.help-toc li {
|
||
margin: 0.5rem 0;
|
||
}
|
||
|
||
.help-toc a {
|
||
color: #a8b1c3;
|
||
text-decoration: none;
|
||
display: block;
|
||
padding: 0.4rem 0.8rem;
|
||
border-radius: 6px;
|
||
transition: all 250ms ease;
|
||
}
|
||
|
||
.help-toc a:hover {
|
||
color: #60a5fa;
|
||
background: rgba(96, 165, 250, 0.1);
|
||
padding-left: 1.1rem;
|
||
}
|
||
|
||
.help-toc a.active {
|
||
color: #3b82f6;
|
||
background: rgba(59, 130, 246, 0.15);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.help-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.help-section {
|
||
margin-bottom: 4rem;
|
||
scroll-margin-top: 100px;
|
||
}
|
||
|
||
.help-section h2 {
|
||
font-size: 1.75rem;
|
||
margin: 0 0 1.5rem 0;
|
||
color: #f1f3f5;
|
||
border-bottom: 2px solid #3b82f6;
|
||
padding-bottom: 0.75rem;
|
||
}
|
||
|
||
.help-section h3 {
|
||
font-size: 1.2rem;
|
||
margin: 2rem 0 1rem 0;
|
||
color: #60a5fa;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.help-section p {
|
||
color: #a8b1c3;
|
||
line-height: 1.7;
|
||
margin: 0 0 1rem 0;
|
||
}
|
||
|
||
.help-section ul {
|
||
color: #a8b1c3;
|
||
margin: 1rem 0 1rem 2rem;
|
||
line-height: 1.8;
|
||
}
|
||
|
||
.help-section li {
|
||
margin: 0.5rem 0;
|
||
}
|
||
|
||
.table-explanation {
|
||
background: rgba(59, 130, 246, 0.08);
|
||
border-left: 3px solid #3b82f6;
|
||
padding: 1.5rem;
|
||
border-radius: 8px;
|
||
margin: 1.5rem 0;
|
||
}
|
||
|
||
.table-column {
|
||
margin: 1.5rem 0;
|
||
background: rgba(255, 255, 255, 0.03);
|
||
padding: 1.2rem;
|
||
border-radius: 8px;
|
||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.table-column h4 {
|
||
margin: 0 0 0.5rem 0;
|
||
color: #60a5fa;
|
||
font-size: 1rem;
|
||
font-weight: 600;
|
||
font-family: monospace;
|
||
}
|
||
|
||
.table-column .column-desc {
|
||
color: #7d8599;
|
||
font-size: 0.9rem;
|
||
margin: 0.5rem 0;
|
||
}
|
||
|
||
.table-column .column-detail {
|
||
color: #a8b1c3;
|
||
font-size: 0.95rem;
|
||
line-height: 1.6;
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
.score-formula {
|
||
background: rgba(16, 185, 129, 0.08);
|
||
border-left: 3px solid #10b981;
|
||
padding: 1.5rem;
|
||
border-radius: 8px;
|
||
margin: 1.5rem 0;
|
||
font-family: monospace;
|
||
font-size: 0.95rem;
|
||
color: #10b981;
|
||
line-height: 1.8;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.warning-box {
|
||
background: rgba(245, 158, 11, 0.08);
|
||
border-left: 3px solid #f59e0b;
|
||
padding: 1.5rem;
|
||
border-radius: 8px;
|
||
margin: 1.5rem 0;
|
||
}
|
||
|
||
.warning-box strong {
|
||
color: #f59e0b;
|
||
}
|
||
|
||
.warning-box p {
|
||
color: #a8b1c3;
|
||
margin: 0.5rem 0;
|
||
}
|
||
|
||
.info-box {
|
||
background: rgba(59, 130, 246, 0.08);
|
||
border-left: 3px solid #3b82f6;
|
||
padding: 1.5rem;
|
||
border-radius: 8px;
|
||
margin: 1.5rem 0;
|
||
}
|
||
|
||
.info-box p {
|
||
color: #a8b1c3;
|
||
margin: 0.5rem 0;
|
||
}
|
||
|
||
.config-impact {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1.5rem;
|
||
margin: 1.5rem 0;
|
||
}
|
||
|
||
.config-item {
|
||
background: rgba(255, 255, 255, 0.03);
|
||
padding: 1.2rem;
|
||
border-radius: 8px;
|
||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.config-item h5 {
|
||
margin: 0 0 0.5rem 0;
|
||
color: #60a5fa;
|
||
font-size: 0.95rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.config-item p {
|
||
color: #a8b1c3;
|
||
font-size: 0.9rem;
|
||
margin: 0;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
code {
|
||
background: rgba(0, 0, 0, 0.3);
|
||
padding: 0.2rem 0.5rem;
|
||
border-radius: 4px;
|
||
font-family: monospace;
|
||
color: #60a5fa;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.back-to-top {
|
||
display: inline-block;
|
||
margin-top: 2rem;
|
||
padding: 0.75rem 1.5rem;
|
||
background: #3b82f6;
|
||
color: #fff;
|
||
text-decoration: none;
|
||
border-radius: 8px;
|
||
transition: all 250ms ease;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.back-to-top:hover {
|
||
background: #60a5fa;
|
||
}
|
||
|
||
@media (max-width: 1024px) {
|
||
.help-container {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.help-sidebar {
|
||
position: relative;
|
||
width: 100%;
|
||
top: auto;
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.config-impact {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="layout">
|
||
<!-- Navigation Component -->
|
||
<!-- NAVIGATION_PLACEHOLDER -->
|
||
|
||
<!-- Main Content -->
|
||
<main class="content">
|
||
<header>
|
||
<h1>Help & Documentation</h1>
|
||
<p>Learn how to interpret packet data, understand scoring, and optimize your configuration</p>
|
||
</header>
|
||
|
||
<div class="help-container">
|
||
<!-- Table of Contents Sidebar -->
|
||
<aside class="help-sidebar">
|
||
<div class="help-toc">
|
||
<h3>Contents</h3>
|
||
<ul>
|
||
<li><a href="#packet-table" class="toc-link">Packet Table</a></li>
|
||
<li><a href="#column-details" class="toc-link">Column Details</a></li>
|
||
<li><a href="#scoring-system" class="toc-link">Scoring System</a></li>
|
||
<li><a href="#score-factors" class="toc-link">Score Factors</a></li>
|
||
<li><a href="#reactive-scoring" class="toc-link">Reactive Scoring</a></li>
|
||
<li><a href="#configuration" class="toc-link">Configuration Effects</a></li>
|
||
<li><a href="#config-settings" class="toc-link">Config Settings</a></li>
|
||
</ul>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- Main Content -->
|
||
<div class="help-content">
|
||
<!-- Packet Table Section -->
|
||
<section id="packet-table" class="help-section">
|
||
<h2>Packet Table Overview</h2>
|
||
|
||
<p>The packet table displays real-time information about every packet your repeater receives and processes. Each row represents a single packet event, showing transmission details, signal quality metrics, and repeater processing information.</p>
|
||
|
||
<div class="info-box">
|
||
<p><strong>Purpose:</strong> The packet table helps you monitor network traffic, diagnose signal issues, and understand how your repeater is handling different types of packets.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Column Details Section -->
|
||
<section id="column-details" class="help-section">
|
||
<h2>Column Details</h2>
|
||
|
||
<div class="table-column">
|
||
<h4>Time</h4>
|
||
<div class="column-desc">Format: HH:MM:SS</div>
|
||
<div class="column-detail">
|
||
The exact time the packet was received by the radio module. Displayed in 24-hour format. Useful for correlating events with logs and identifying traffic patterns throughout the day.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-column">
|
||
<h4>Type</h4>
|
||
<div class="column-desc">Packet payload type identifier</div>
|
||
<div class="column-detail">
|
||
<strong>ADVERT:</strong> Node advertisement/discovery packets (usually broadcasts)<br>
|
||
<strong>ACK:</strong> Acknowledgment responses<br>
|
||
<strong>TXT:</strong> Text messages<br>
|
||
<strong>GRP:</strong> Group messages<br>
|
||
<strong>PATH:</strong> Path information packets<br>
|
||
<strong>RESP:</strong> Response packets<br>
|
||
<strong>TRACE:</strong> Trace/debug packets<br>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-column">
|
||
<h4>Route</h4>
|
||
<div class="column-desc">Routing mode indicator</div>
|
||
<div class="column-detail">
|
||
<strong>DIRECT:</strong> Packet explicitly routed to this repeater (contains its address in the path)<br>
|
||
<strong>FLOOD:</strong> Broadcast packet intended for all nodes in range<br>
|
||
DIRECT packets have higher priority since they're specifically addressed to your repeater. FLOOD packets are retransmitted if bandwidth allows.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-column">
|
||
<h4>Length</h4>
|
||
<div class="column-desc">Payload size in bytes</div>
|
||
<div class="column-detail">
|
||
The actual payload data size (not including LoRa overhead). Affects airtime consumption and score calculation. Larger packets take longer to transmit, consuming more airtime budget. Typical range: 20-250 bytes.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-column">
|
||
<h4>RSSI</h4>
|
||
<div class="column-desc">Received Signal Strength Indicator (dBm)</div>
|
||
<div class="column-detail">
|
||
Measures signal power. <strong>More negative = weaker signal</strong><br>
|
||
<strong>Excellent:</strong> -80 to -100 dBm (strong)<br>
|
||
<strong>Good:</strong> -100 to -120 dBm (acceptable)<br>
|
||
<strong>Poor:</strong> -120 to -140 dBm (weak, may be unreliable)<br>
|
||
Affects score calculation - better RSSI yields higher scores. Distance and obstacles reduce RSSI.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-column">
|
||
<h4>SNR</h4>
|
||
<div class="column-desc">Signal-to-Noise Ratio (dB)</div>
|
||
<div class="column-detail">
|
||
Measures signal clarity vs. background noise. <strong>Higher = cleaner signal</strong><br>
|
||
<strong>Excellent:</strong> SNR > 10 dB (very clean)<br>
|
||
<strong>Good:</strong> SNR 5-10 dB (normal operation)<br>
|
||
<strong>Poor:</strong> SNR < 5 dB (noisy environment)<br>
|
||
Even with weak RSSI, high SNR indicates reliable reception. Critical for score calculation.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-column">
|
||
<h4>Score</h4>
|
||
<div class="column-desc">Composite quality metric (0.0 - 1.0)</div>
|
||
<div class="column-detail">
|
||
A single number representing overall packet quality based on SNR and packet length. This matches the C++ MeshCore algorithm exactly. Higher scores (closer to 1.0) indicate better quality packets with good SNR relative to the spreading factor threshold. Used internally for optional reactive delay optimization (when use_score_for_tx is enabled). See Scoring System section for detailed calculation method.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-column">
|
||
<h4>TX Delay</h4>
|
||
<div class="column-desc">Time in milliseconds</div>
|
||
<div class="column-detail">
|
||
How long the repeater waited before retransmitting. Delay factors include:<br>
|
||
• Airtime budget checking<br>
|
||
• Random collision avoidance (0-5ms factor)<br>
|
||
• Current channel utilization<br>
|
||
• Optional quality-based prioritization (when enabled)<br>
|
||
Longer delays may indicate congestion or airtime throttling to comply with duty cycle limits.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-column">
|
||
<h4>Status</h4>
|
||
<div class="column-desc">Packet processing outcome</div>
|
||
<div class="column-detail">
|
||
<strong>FORWARDED:</strong> Packet has been successfully retransmitted to other nodes. The repeater forwarded this packet over the air.<br>
|
||
<strong>DROPPED:</strong> Packet was rejected and not forwarded.<br>
|
||
<br>
|
||
<strong>Drop Reasons:</strong>
|
||
<ul style="margin: 8px 0; padding-left: 20px; font-size: 0.9em; line-height: 1.6;">
|
||
<li><strong>Duplicate:</strong> Packet hash already in cache. Prevents redundant retransmission.</li>
|
||
<li><strong>Empty payload:</strong> Packet has no payload data. Cannot be processed.</li>
|
||
<li><strong>Path at max size:</strong> Path field has reached maximum length. Cannot add repeater identifier.</li>
|
||
<li><strong>Duty cycle limit:</strong> Airtime budget exhausted. Cannot transmit (EU 1% duty cycle or configured limit).</li>
|
||
<li><strong>Direct: no path:</strong> Direct-mode packet lacks routing path.</li>
|
||
<li><strong>Direct: not our hop:</strong> Direct-mode packet is not addressed to this repeater node.</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Scoring System Section -->
|
||
<section id="scoring-system" class="help-section">
|
||
<h2>Scoring System</h2>
|
||
|
||
<p>The packet score is calculated using the exact same algorithm as the C++ MeshCore implementation. It combines SNR (relative to spreading factor threshold) and packet length to produce a single quality indicator (0.0 to 1.0). This score can optionally be used for reactive delay optimization when use_score_for_tx is enabled.</p>
|
||
|
||
<h3>The Scoring Formula</h3>
|
||
|
||
<div class="score-formula">
|
||
<div style="background: var(--color-bg-secondary); border: 1px solid var(--color-border); padding: 20px; border-radius: 8px; margin-bottom: 20px;">
|
||
<div style="font-size: 1.2em; font-weight: bold; margin-bottom: 15px; text-align: center; color: var(--color-text-primary);">
|
||
Score = SNR Factor × Length Factor
|
||
</div>
|
||
|
||
<table style="width: 100%; border-collapse: collapse; background: var(--color-bg-tertiary); border-radius: 6px; overflow: hidden;">
|
||
<tr>
|
||
<td style="padding: 12px; border-right: 1px solid var(--color-border); width: 50%;">
|
||
<div style="font-size: 0.9em; color: var(--color-text-secondary); margin-bottom: 6px;">SNR Factor</div>
|
||
<div style="font-size: 1.1em; font-family: monospace; color: var(--color-accent-primary);">
|
||
(SNR - SF<sub>threshold</sub>) / 10
|
||
</div>
|
||
</td>
|
||
<td style="padding: 12px; width: 50%;">
|
||
<div style="font-size: 0.9em; color: var(--color-text-secondary); margin-bottom: 6px;">Length Factor</div>
|
||
<div style="font-size: 1.1em; font-family: monospace; color: var(--color-accent-primary);">
|
||
(1 - length / 256)
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
|
||
<h4 style="margin-top: 20px; margin-bottom: 15px; color: var(--color-text-primary);">Spreading Factor Thresholds</h4>
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 20px;">
|
||
<div style="background: var(--color-bg-tertiary); padding: 12px; border-radius: 6px; border-left: 4px solid var(--color-accent-primary); color: var(--color-text-secondary);">
|
||
<strong style="color: var(--color-text-primary);">SF7</strong> → -7.5 dB
|
||
</div>
|
||
<div style="background: var(--color-bg-tertiary); padding: 12px; border-radius: 6px; border-left: 4px solid var(--color-accent-primary); color: var(--color-text-secondary);">
|
||
<strong style="color: var(--color-text-primary);">SF8</strong> → -10.0 dB
|
||
</div>
|
||
<div style="background: var(--color-bg-tertiary); padding: 12px; border-radius: 6px; border-left: 4px solid var(--color-accent-primary); color: var(--color-text-secondary);">
|
||
<strong style="color: var(--color-text-primary);">SF9</strong> → -12.5 dB
|
||
</div>
|
||
<div style="background: var(--color-bg-tertiary); padding: 12px; border-radius: 6px; border-left: 4px solid var(--color-accent-primary); color: var(--color-text-secondary);">
|
||
<strong style="color: var(--color-text-primary);">SF10</strong> → -15.0 dB
|
||
</div>
|
||
<div style="background: var(--color-bg-tertiary); padding: 12px; border-radius: 6px; border-left: 4px solid var(--color-accent-primary); color: var(--color-text-secondary);">
|
||
<strong style="color: var(--color-text-primary);">SF11</strong> → -17.5 dB
|
||
</div>
|
||
<div style="background: var(--color-bg-tertiary); padding: 12px; border-radius: 6px; border-left: 4px solid var(--color-accent-primary); color: var(--color-text-secondary);">
|
||
<strong style="color: var(--color-text-primary);">SF12</strong> → -20.0 dB
|
||
</div>
|
||
</div>
|
||
|
||
<h4 style="margin-bottom: 15px; color: var(--color-text-primary);">Real-World Example</h4>
|
||
<div style="background: var(--color-bg-tertiary); border-left: 4px solid var(--color-info); padding: 15px; border-radius: 6px;">
|
||
<p style="color: var(--color-text-primary);"><strong>Packet Details:</strong></p>
|
||
<ul style="margin: 8px 0; padding-left: 20px; color: var(--color-text-secondary);">
|
||
<li>SNR: <code style="background: var(--color-bg-secondary); padding: 2px 6px; border-radius: 3px; color: var(--color-accent-primary);">12 dB</code></li>
|
||
<li>Spreading Factor: <code style="background: var(--color-bg-secondary); padding: 2px 6px; border-radius: 3px; color: var(--color-accent-primary);">SF8</code></li>
|
||
<li>Payload Length: <code style="background: var(--color-bg-secondary); padding: 2px 6px; border-radius: 3px; color: var(--color-accent-primary);">100 bytes</code></li>
|
||
</ul>
|
||
<hr style="border: none; border-top: 1px solid var(--color-border); margin: 12px 0;">
|
||
<p style="margin: 8px 0; color: var(--color-text-primary);"><strong>Calculation:</strong></p>
|
||
<div style="background: var(--color-bg-secondary); padding: 12px; border-radius: 4px; font-family: monospace; font-size: 0.95em; color: var(--color-text-secondary);">
|
||
SNR Factor = (12 - (-10)) / 10 = 22 / 10 = <strong style="color: var(--color-accent-primary);">2.2</strong> (clamped to 1.0)<br>
|
||
Length Factor = (1 - 100/256) = 0.609<br>
|
||
<strong style="color: var(--color-accent-primary);">Score = 1.0 × 0.609 = 0.61</strong> (FAIR quality)
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<p>This formula ensures that:</p>
|
||
<ul>
|
||
<li><strong>Signal quality matters:</strong> Higher SNR produces higher scores, with SF-specific thresholds</li>
|
||
<li><strong>Smaller packets score higher:</strong> They consume less airtime due to shorter transmission time</li>
|
||
<li><strong>Poor SNR packets may score zero:</strong> If SNR falls below SF threshold, score = 0.0</li>
|
||
</ul>
|
||
|
||
<h3>Score Interpretation</h3>
|
||
|
||
<div style="margin-top: 20px;">
|
||
<!-- Visual score scale -->
|
||
<div style="margin-bottom: 20px;">
|
||
<div style="font-weight: bold; margin-bottom: 8px; color: var(--color-text-primary);">Quality Scale</div>
|
||
<div style="background: linear-gradient(90deg, #ef4444 0%, #f97316 25%, #eab308 50%, #84cc16 75%, #22c55e 100%); height: 30px; border-radius: 6px; margin-bottom: 8px; border: 2px solid var(--color-border);"></div>
|
||
<div style="display: flex; justify-content: space-between; font-size: 0.85em; color: var(--color-text-secondary);">
|
||
<span>0.0</span>
|
||
<span>0.25</span>
|
||
<span>0.5</span>
|
||
<span>0.75</span>
|
||
<span>1.0</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Score ratings -->
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;">
|
||
<div style="background: linear-gradient(135deg, rgba(34, 197, 94, 0.1), rgba(34, 197, 94, 0.05)); border-left: 4px solid #22c55e; padding: 15px; border-radius: 6px;">
|
||
<div style="font-weight: bold; color: #22c55e; font-size: 1.1em; margin-bottom: 6px;">0.9 - 1.0 Excellent</div>
|
||
<div style="color: #555; font-size: 0.9em;">Perfect conditions, high SNR, small payload</div>
|
||
</div>
|
||
|
||
<div style="background: linear-gradient(135deg, rgba(132, 204, 22, 0.1), rgba(132, 204, 22, 0.05)); border-left: 4px solid #84cc16; padding: 15px; border-radius: 6px;">
|
||
<div style="font-weight: bold; color: #84cc16; font-size: 1.1em; margin-bottom: 6px;">0.7 - 0.9 Good</div>
|
||
<div style="color: #555; font-size: 0.9em;">Normal operation, acceptable signal</div>
|
||
</div>
|
||
|
||
<div style="background: linear-gradient(135deg, rgba(234, 179, 8, 0.1), rgba(234, 179, 8, 0.05)); border-left: 4px solid #eab308; padding: 15px; border-radius: 6px;">
|
||
<div style="font-weight: bold; color: #ca8a04; font-size: 1.1em; margin-bottom: 6px;">0.5 - 0.7 Fair</div>
|
||
<div style="color: #555; font-size: 0.9em;">Degraded conditions, lower SNR</div>
|
||
</div>
|
||
|
||
<div style="background: linear-gradient(135deg, rgba(249, 115, 22, 0.1), rgba(249, 115, 22, 0.05)); border-left: 4px solid #f97316; padding: 15px; border-radius: 6px;">
|
||
<div style="font-weight: bold; color: #ea580c; font-size: 1.1em; margin-bottom: 6px;">0.3 - 0.5 Poor</div>
|
||
<div style="color: #555; font-size: 0.9em;">Marginal conditions, weak signal</div>
|
||
</div>
|
||
|
||
<div style="background: linear-gradient(135deg, rgba(239, 68, 68, 0.1), rgba(239, 68, 68, 0.05)); border-left: 4px solid #ef4444; padding: 15px; border-radius: 6px;">
|
||
<div style="font-weight: bold; color: #dc2626; font-size: 1.1em; margin-bottom: 6px;">< 0.3 Very Poor</div>
|
||
<div style="color: #555; font-size: 0.9em;">Barely usable, may be dropped</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Score Factors Section -->
|
||
<section id="score-factors" class="help-section">
|
||
<h2>What Affects Your Score?</h2>
|
||
|
||
<h3>Primary Factors</h3>
|
||
|
||
<div class="table-column">
|
||
<h4>Signal-to-Noise Ratio (SNR)</h4>
|
||
<div class="column-detail">
|
||
<strong>Impact: HIGHEST</strong><br>
|
||
Each 1 dB improvement in SNR can increase score by ~0.05. High interference environments significantly reduce scores. The repeater benefits from placement with clear LoS (line of sight) to minimize multipath and fading.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-column">
|
||
<h4>Packet Payload Length</h4>
|
||
<div class="column-detail">
|
||
<strong>Impact: HIGH</strong><br>
|
||
Larger packets consume more airtime due to longer transmission times. A 100-byte packet scores lower than a 50-byte packet with identical SNR.
|
||
</div>
|
||
|
||
<div class="table-column">
|
||
<h4>RSSI (Signal Strength)</h4>
|
||
<div class="column-detail">
|
||
<strong>Impact: NOT USED IN SCORING</strong><br>
|
||
RSSI is displayed for monitoring purposes but does NOT affect the score calculation. The C++ MeshCore algorithm uses only SNR and packet length. However, RSSI correlates with SNR - better RSSI typically means better SNR, which indirectly results in higher scores.
|
||
</div>
|
||
</div>
|
||
|
||
<h3>Environmental Factors</h3>
|
||
|
||
<ul>
|
||
<li><strong>Weather:</strong> Rain and fog reduce signal strength and increase noise</li>
|
||
<li><strong>Time of Day:</strong> Atmospheric conditions change, especially during dawn/dusk</li>
|
||
<li><strong>Frequency Congestion:</strong> More devices on 869 MHz = higher noise floor</li>
|
||
<li><strong>Physical Obstructions:</strong> Buildings and trees block signals, increase fading</li>
|
||
<li><strong>Antenna Orientation:</strong> Poor antenna alignment reduces SNR significantly</li>
|
||
</ul>
|
||
|
||
<div class="warning-box">
|
||
<p><strong>Environmental Issues:</strong> If you see consistently low scores across many packets, check your antenna placement, orientation, and surroundings. Poor environmental conditions are often the limiting factor, not the repeater itself.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Reactive Scoring Section -->
|
||
<section id="reactive-scoring" class="help-section">
|
||
<h2>Reactive Score-Based Delay Optimization</h2>
|
||
|
||
<p>The repeater includes an optional reactive scoring system that dynamically prioritizes packets based on signal quality during network congestion. This feature matches the C++ MeshCore behavior for intelligent packet prioritization.</p>
|
||
|
||
<h3>How It Works</h3>
|
||
|
||
<div class="info-box">
|
||
<p><strong>Key Principle:</strong> When the repeater detects congestion (calculated TX delay ≥ 50ms), it automatically applies a quality-based delay multiplier to high-quality packets, giving them priority while gracefully backing off low-quality packets.</p>
|
||
<p><strong>Default Behavior:</strong> This feature is <strong>disabled by default</strong> (use_score_for_tx: false). When disabled, all packets follow standard C++ MeshCore delay calculation with pure randomization.</p>
|
||
</div>
|
||
|
||
<h3>Delay Multiplier Formula</h3>
|
||
|
||
<div class="score-formula">
|
||
<div style="background: var(--color-bg-secondary); border: 1px solid var(--color-border); padding: 20px; border-radius: 8px;">
|
||
<div style="font-size: 1.1em; font-weight: bold; margin-bottom: 15px; text-align: center; color: var(--color-text-primary);">
|
||
Applied Only When: delay ≥ 50ms AND use_score_for_tx = true
|
||
</div>
|
||
|
||
<div style="background: var(--color-bg-tertiary); padding: 15px; border-radius: 6px; font-family: monospace; color: var(--color-accent-primary); line-height: 1.8;">
|
||
<strong style="color: var(--color-text-primary);">Delay Multiplier = max(0.2, 1.0 - score)</strong>
|
||
</div>
|
||
|
||
<div style="margin-top: 15px; color: var(--color-text-secondary); font-size: 0.9em;">
|
||
<p><strong>What this means:</strong></p>
|
||
<ul style="margin: 10px 0; padding-left: 20px;">
|
||
<li><strong>Perfect packet (score 1.0):</strong> Multiplier = max(0.2, 0.0) = 0.2 → Gets 20% of base delay (fast priority)</li>
|
||
<li><strong>Good packet (score 0.7):</strong> Multiplier = max(0.2, 0.3) = 0.3 → Gets 30% of base delay</li>
|
||
<li><strong>Fair packet (score 0.5):</strong> Multiplier = max(0.2, 0.5) = 0.5 → Gets 50% of base delay</li>
|
||
<li><strong>Poor packet (score 0.2):</strong> Multiplier = max(0.2, 0.8) = 0.8 → Gets 80% of base delay (slower, backoff)</li>
|
||
<li><strong>Minimum floor:</strong> No packet gets less than 20% multiplier (prevents starvation)</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<h3>Example: Reactive Scoring in Action</h3>
|
||
|
||
<div class="info-box">
|
||
<p><strong>Scenario:</strong> Two packets arrive during congestion (base delay 100ms), tx_delay_factor=1.0</p>
|
||
<ul>
|
||
<li><strong>Packet X:</strong> Excellent signal, score = 0.9</li>
|
||
<li><strong>Packet Y:</strong> Weak signal, score = 0.4</li>
|
||
</ul>
|
||
<p><strong>Without Reactive Scoring (disabled):</strong></p>
|
||
<ul>
|
||
<li>Packet X: TX Delay = 0-500ms (pure random collision avoidance)</li>
|
||
<li>Packet Y: TX Delay = 0-500ms (pure random collision avoidance)</li>
|
||
<li>Result: Both may transmit at same time, causing collision</li>
|
||
</ul>
|
||
<p><strong>With Reactive Scoring (enabled, congestion detected):</strong></p>
|
||
<ul>
|
||
<li>Packet X: Multiplier = 0.1 → TX Delay = 0-50ms (high priority, transmits first)</li>
|
||
<li>Packet Y: Multiplier = 0.6 → TX Delay = 0-300ms (lower priority, waits longer)</li>
|
||
<li>Result: High-quality packets forward with minimal delay; marginal packets gracefully back off</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<h3>Configuration</h3>
|
||
|
||
<div class="table-column">
|
||
<h4>use_score_for_tx</h4>
|
||
<div class="column-desc">Enable/disable reactive score-based delay optimization</div>
|
||
<div class="column-detail">
|
||
<strong>Default:</strong> false (disabled)<br>
|
||
<strong>Options:</strong> true or false<br>
|
||
<strong>When true:</strong> Activates quality-based delay multiplier when congestion detected (delay ≥ 50ms)<br>
|
||
<strong>When false:</strong> Standard C++ MeshCore behavior, pure random delays, no score influence on timing<br>
|
||
<strong>Location in config.yaml:</strong>
|
||
<div style="background: var(--color-bg-secondary); padding: 12px; border-radius: 4px; margin-top: 8px; font-family: monospace; font-size: 0.9em;">
|
||
repeater:<br>
|
||
use_score_for_tx: false
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-column">
|
||
<h4>score_threshold</h4>
|
||
<div class="column-desc">Reserved for future enhancement / statistics monitoring</div>
|
||
<div class="column-detail">
|
||
<strong>Default:</strong> 0.3<br>
|
||
<strong>Range:</strong> 0.0 - 1.0<br>
|
||
<strong>Current Status:</strong> This value is read from config but <strong>not currently used</strong> in packet processing. It is reserved for future features.<br>
|
||
<strong>Future Potential Uses:</strong>
|
||
<ul style="margin: 8px 0; padding-left: 20px;">
|
||
<li>Dashboard quality alerts when average packet score drops below threshold</li>
|
||
<li>Proactive packet filtering - dropping very poor quality packets upfront (below threshold)</li>
|
||
<li>Quality monitoring and trend statistics in web UI</li>
|
||
<li>Logging alerts for poor signal conditions</li>
|
||
</ul>
|
||
<strong>Recommendation:</strong> Leave at default (0.3). Changing it currently has <strong>no effect on packet processing</strong>. This setting will become active once future quality monitoring features are implemented.
|
||
</div>
|
||
</div>
|
||
|
||
<h3>When to Enable Reactive Scoring</h3>
|
||
|
||
<div class="config-impact">
|
||
<div class="config-item">
|
||
<h5>Enable (use_score_for_tx: true)</h5>
|
||
<p>
|
||
• High-traffic networks where collisions are frequent<br>
|
||
• Noisy environments with poor average signal quality<br>
|
||
• You want to prioritize high-quality packets during congestion<br>
|
||
• Testing adaptive network behavior<br>
|
||
• Duty-cycle constrained regions (EU) with limited bandwidth
|
||
</p>
|
||
</div>
|
||
|
||
<div class="config-item">
|
||
<h5>Disable (use_score_for_tx: false)</h5>
|
||
<p>
|
||
• Low-traffic networks where congestion is rare<br>
|
||
• You want pure C++ MeshCore compatibility<br>
|
||
• Consistent delay behavior is more important than efficiency<br>
|
||
• New deployments - start simple and tune later<br>
|
||
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="warning-box">
|
||
<p><strong>Important:</strong> Reactive scoring only affects TX delay timing, not packet forwarding decisions. All packets still get forwarded (unless dropped for other reasons like duplicates or duty cycle). The system gracefully prioritizes quality during congestion without dropping packets, matching MeshCore's intelligent backpressure strategy.</p>
|
||
</div>
|
||
</section>
|
||
<h2>Configuration Impact on Scoring</h2>
|
||
|
||
<p>Your repeater's configuration settings directly affect packet scoring and processing behavior.</p>
|
||
|
||
<h3>Radio Configuration Parameters</h3>
|
||
|
||
<div class="config-impact">
|
||
<div class="config-item">
|
||
<h5>Spreading Factor (SF)</h5>
|
||
<p><strong>Current setting:</strong> SF 8<br>
|
||
<strong>Higher SF (9-12):</strong> Better range and SNR, but slower transmission, more airtime consumed<br>
|
||
<strong>Lower SF (7):</strong> Faster transmission, less airtime, but worse sensitivity and range<br>
|
||
<strong>Score impact:</strong> Higher SF generally improves SNR = higher scores, but increases payload duration penalty</p>
|
||
</div>
|
||
|
||
<div class="config-item">
|
||
<h5>Bandwidth (BW)</h5>
|
||
<p><strong>Current setting:</strong> 62.5 kHz<br>
|
||
<strong>Wider BW (125 kHz):</strong> Faster data rate, less airtime per byte, but worse sensitivity<br>
|
||
<strong>Narrower BW (31.25 kHz):</strong> Better sensitivity, but slower transmission<br>
|
||
<strong>Score impact:</strong> BW affects SNR - narrower = potentially better SNR but longer TX times</p>
|
||
</div>
|
||
|
||
<div class="config-item">
|
||
<h5>TX Power</h5>
|
||
<p><strong>Current setting:</strong> 14 dBm<br>
|
||
<strong>Higher power:</strong> Better outbound range, but may increase noise at nearby receivers<br>
|
||
<strong>Lower power:</strong> Reduces interference, saves energy, but limits outbound range<br>
|
||
<strong>Score impact:</strong> TX power only affects outgoing transmissions, not received score</p>
|
||
</div>
|
||
|
||
<div class="config-item">
|
||
<h5>Coding Rate (CR)</h5>
|
||
<p><strong>Current setting:</strong> 4/8<br>
|
||
<strong>Higher CR (4/7):</strong> Less error correction, faster transmission, more airtime efficient<br>
|
||
<strong>Lower CR (4/8):</strong> More error correction, better resilience to interference<br>
|
||
<strong>Score impact:</strong> Higher CR can improve SNR in clean environments, reduce it in noisy ones</p>
|
||
</div>
|
||
</div>
|
||
|
||
<h3>Duty Cycle Configuration</h3>
|
||
|
||
<div class="table-explanation">
|
||
<p><strong>Current Duty Cycle Limit:</strong> 6% max airtime per hour</p>
|
||
<p>This means your repeater can spend at most 3.6 minutes (21.6 seconds per minute) transmitting per hour. How this affects packet handling:</p>
|
||
<ul>
|
||
<li><strong>When below limit:</strong> All packets retransmitted if they pass validation</li>
|
||
<li><strong>When approaching limit:</strong> Incoming packets may be dropped if airtime budget is exhausted</li>
|
||
<li><strong>When limit reached:</strong> All new transmissions are dropped until the duty cycle budget resets (each minute)</li>
|
||
</ul>
|
||
<p><strong>Important:</strong> The repeater does NOT queue packets for later transmission. When duty cycle limit is reached, packets are immediately dropped. This is by design - a repeater must forward immediately or drop the packet. Note: Packet score does not affect duty cycle enforcement - all packets are treated equally when duty cycle limit is reached.</p>
|
||
</div>
|
||
|
||
<h3>Airtime Consumption Example</h3>
|
||
|
||
<div class="info-box">
|
||
<p><strong>Scenario:</strong> 100-byte packet at SF8, BW 62.5 kHz, CR 4/8<br>
|
||
<strong>Airtime:</strong> ~512 ms<br>
|
||
<strong>At 6% duty cycle:</strong> Can transmit ~420 packets/hour maximum<br>
|
||
<strong>Effect on score:</strong> High volume of large packets will consume budget quickly, causing lower-scored packets to be dropped
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Configuration Settings Section -->
|
||
<section id="config-settings" class="help-section">
|
||
<h2>Configuration Settings Reference</h2>
|
||
|
||
<p>The repeater is configured via <code>config.yaml</code>. This section explains key settings and how they affect packet performance.</p>
|
||
|
||
<div class="info-box">
|
||
<p><strong>Important:</strong> Packet <strong>Score</strong> (signal quality) and <strong>TX Delay</strong> (collision avoidance timing) are independent systems. Score is calculated from SNR and packet length. Delays are configured via tx_delay_factor and direct_tx_delay_factor and are based on airtime, not signal quality.</p>
|
||
</div>
|
||
|
||
<h3>Delay Settings</h3>
|
||
|
||
<div class="table-column">
|
||
<h4>tx_delay_factor</h4>
|
||
<div class="column-desc">Flood mode transmission delay multiplier</div>
|
||
<div class="column-detail">
|
||
<strong>Default:</strong> 1.0<br>
|
||
<strong>Purpose:</strong> Scales the base collision-avoidance delay for flood packets.<br>
|
||
<strong>Formula:</strong> delay = random(0-5) × (airtime × 52/50 ÷ 2) × tx_delay_factor<br>
|
||
<strong>Effect:</strong> Higher values = longer delays between flood packet retransmissions, reducing collisions but increasing latency. Lower values speed up propagation in low-traffic areas.<br>
|
||
<strong>Typical range:</strong> 0.5 - 2.0 (0.5 = faster, 2.0 = collision-resistant)
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-column">
|
||
<h4>direct_tx_delay_factor</h4>
|
||
<div class="column-desc">Direct mode transmission delay (in seconds)</div>
|
||
<div class="column-detail">
|
||
<strong>Default:</strong> 0.5 seconds<br>
|
||
<strong>Purpose:</strong> Fixed delay for direct-routed packets (packets specifically addressed to this repeater).<br>
|
||
<strong>Effect:</strong> Direct packets wait this many seconds before retransmission. Direct packets bypass the collision-avoidance algorithm and use a fixed delay instead.<br>
|
||
<strong>Note:</strong> Typically lower than flood delays to prioritize DIRECT packets. 0 = immediate forwarding.<br>
|
||
<strong>Typical range:</strong> 0 - 2.0 seconds
|
||
</div>
|
||
</div>
|
||
|
||
<h3>How TX Delay is Calculated</h3>
|
||
|
||
<p>The TX Delay shown in the packet table follows the MeshCore C++ implementation for collision avoidance:</p>
|
||
|
||
<div class="info-box">
|
||
<p><strong>For FLOOD packets (broadcast):</strong><br>
|
||
TX Delay = random(0 to 5) × (airtime_ms × 52/50 ÷ 2) × tx_delay_factor ÷ 1000<br><br>
|
||
<strong>For DIRECT packets (addressed to this repeater):</strong><br>
|
||
TX Delay = direct_tx_delay_factor (fixed, in seconds)<br><br>
|
||
<strong>Optional Reactive Scoring:</strong><br>
|
||
If use_score_for_tx is enabled AND delay ≥ 50ms:<br>
|
||
TX Delay = base_delay × max(0.2, 1.0 - packet_score)<br>
|
||
This applies a quality-based multiplier during congestion: high-score packets get shorter delays (priority), low-score packets get longer delays (backoff).<br><br>
|
||
<strong>Example:</strong> FLOOD packet with 100ms airtime, tx_delay_factor=1.0, score=0.8:<br>
|
||
• Base delay = (100 × 52/50 ÷ 2) = 52 ms<br>
|
||
• With random(0-5) multiplier: 0-260 ms (before score adjustment)<br>
|
||
• If ≥50ms AND score adjustment active: 0-260ms × max(0.2, 1.0-0.8) = 0-260ms × 0.2 = <strong>0-52ms</strong> (prioritized)<br><br>
|
||
<strong>Tuning:</strong> Increase tx_delay_factor in high-traffic areas to reduce collisions. Decrease in low-traffic areas for faster propagation. Enable use_score_for_tx for intelligent priority during congestion. Direct packets bypass randomization and use fixed delays.
|
||
</p>
|
||
</div>
|
||
|
||
<h3>Duty Cycle Constraints</h3>
|
||
|
||
<div class="table-column">
|
||
<h4>max_airtime_per_minute</h4>
|
||
<div class="column-desc">Maximum transmission time per minute in milliseconds</div>
|
||
<div class="column-detail">
|
||
<strong>Common values:</strong><br>
|
||
• <code>3600 ms/min</code> = 100% duty cycle (US/AU FCC, no restriction)<br>
|
||
• <code>36 ms/min</code> = 1% duty cycle (EU ETSI standard)<br>
|
||
• <code>360 ms/min</code> = 10% duty cycle (compromise for EU testing)<br><br>
|
||
<strong>Effect on packet handling:</strong> Duty cycle enforcement is <strong>independent of packet score</strong>. When duty cycle limit is reached, ALL packets are dropped equally - regardless of signal quality. The system does not prioritize high-score packets; it simply refuses to transmit until the budget resets.<br>
|
||
<strong>TX Delay impact:</strong> TX Delay shown in the packet table is unaffected by duty cycle limits. However, packets may be completely blocked (dropped) when airtime budget is exhausted. There is no queuing or delay-until-later mechanism - dropped packets are lost immediately.<br>
|
||
<strong>Packet distribution during high traffic:</strong> When approaching or exceeding duty cycle limits (>80%), incoming packets are dropped indiscriminately based on airtime availability. The mean packet score will fluctuate based on random traffic mix, not because the system prefers high-score packets. All packets have equal probability of being dropped when budget is exhausted.
|
||
</div>
|
||
</div>
|
||
|
||
<h3>How These Work Together</h3>
|
||
|
||
<div class="info-box">
|
||
<p><strong>Example Scenario - Packet Forwarding with Delay:</strong></p>
|
||
<p>You receive 3 packets with different routes and sizes (tx_delay_factor=1.0, direct_tx_delay_factor=0.5s):</p>
|
||
<ul>
|
||
<li><strong>Packet A:</strong> Route DIRECT, 50 bytes → TX Delay = 0.5 seconds (fixed)</li>
|
||
<li><strong>Packet B:</strong> Route FLOOD, 100 bytes → TX Delay = random(0-5) × 52ms × 1.0 = 0-260 ms</li>
|
||
<li><strong>Packet C:</strong> Route FLOOD, 150 bytes → TX Delay = random(0-5) × 78ms × 1.0 = 0-390 ms</li>
|
||
</ul>
|
||
<p><strong>Processing order (without duty cycle limits):</strong></p>
|
||
<ul>
|
||
<li>Packet A: Waits 0.5s, then forwards (direct packets get fixed priority)</li>
|
||
<li>Packets B & C: Random delays prevent collision, lower packet transmitted first if random lucky</li>
|
||
</ul>
|
||
<p><strong>If duty cycle ~95% full:</strong> Still forwards all three, but with increased TX delays. If insufficient airtime remains for a packet, it is dropped immediately (not queued)</p>
|
||
</div>
|
||
|
||
<h3>Optimization Tips</h3>
|
||
|
||
<ul>
|
||
<li><strong>For high-traffic/interference:</strong> Increase <code>tx_delay_factor</code> to 1.5-2.0 to reduce collisions with more randomization</li>
|
||
<li><strong>For low-traffic areas:</strong> Decrease <code>tx_delay_factor</code> to 0.5 for faster propagation</li>
|
||
<li><strong>For priority direct packets:</strong> Lower <code>direct_tx_delay_factor</code> below 0.5s for faster handling</li>
|
||
<li><strong>For duty-cycle constrained regions (EU):</strong> Keep default settings; airtime budget enforces fairness</li>
|
||
<li><strong>Monitor TX Delay column:</strong> Increasing delays indicate network congestion or approaching duty cycle limits</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<a href="#packet-table" class="back-to-top">↑ Back to Top</a>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<script>
|
||
// Table of contents active link highlighting
|
||
const tocLinks = document.querySelectorAll('.toc-link');
|
||
const sections = document.querySelectorAll('.help-section');
|
||
|
||
function updateActiveTocLink() {
|
||
let current = '';
|
||
|
||
sections.forEach(section => {
|
||
const sectionTop = section.offsetTop;
|
||
const sectionHeight = section.clientHeight;
|
||
const scrollPosition = window.scrollY + 150;
|
||
|
||
if (scrollPosition >= sectionTop && scrollPosition < sectionTop + sectionHeight) {
|
||
current = section.getAttribute('id');
|
||
}
|
||
});
|
||
|
||
tocLinks.forEach(link => {
|
||
link.classList.remove('active');
|
||
if (link.getAttribute('href') === `#${current}`) {
|
||
link.classList.add('active');
|
||
}
|
||
});
|
||
}
|
||
|
||
window.addEventListener('scroll', updateActiveTocLink);
|
||
updateActiveTocLink();
|
||
|
||
// Smooth scroll for anchor links
|
||
tocLinks.forEach(link => {
|
||
link.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
const targetId = link.getAttribute('href');
|
||
const targetSection = document.querySelector(targetId);
|
||
if (targetSection) {
|
||
targetSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|