Skip to content

Commit d92626a

Browse files
authored
Improve local stats noise floor chart (#1926)
* Improve local stats noise floor tooling * Simplify local stats chart controls * Add local stats screenshot * Add local stats reading request to chart * Move local stats request to action bar * Compact local stats action labels * Simplify local stats chart presentation * Fix local stats chart axis clipping * Fix local stats chart time label clipping * Add copy actions to local stats fields
1 parent 367d0f9 commit d92626a

9 files changed

Lines changed: 536 additions & 137 deletions

File tree

Meshtastic/Persistence/PerformanceSeedData.swift

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@ import Foundation
1414
struct PerformanceSeedConfiguration {
1515
let nodeCount: Int
1616
let telemetryHistoryPerNode: Int
17+
let localStatsHistoryPerNode: Int
1718
let positionHistoryPerNode: Int
1819
let directMessageCount: Int
1920
let channelMessageCount: Int
2021
let resetStore: Bool
2122
let compactNodeList: Bool
2223
let disableDiscovery: Bool
2324
let initialTab: NavigationState.Tab
25+
let opensLocalStatsLog: Bool
26+
let localStatsSameHourSeed: Bool
2427
}
2528

2629
@MainActor
@@ -34,13 +37,16 @@ enum PerformanceSeedData {
3437
return PerformanceSeedConfiguration(
3538
nodeCount: integerValue("MESHTASTIC_PERF_SEED_NODES", environment: environment, defaultValue: 5_000),
3639
telemetryHistoryPerNode: integerValue("MESHTASTIC_PERF_TELEMETRY_HISTORY", environment: environment, defaultValue: 3),
40+
localStatsHistoryPerNode: integerValue("MESHTASTIC_PERF_LOCAL_STATS_HISTORY", environment: environment, defaultValue: integerValue("MESHTASTIC_PERF_TELEMETRY_HISTORY", environment: environment, defaultValue: 3)),
3741
positionHistoryPerNode: integerValue("MESHTASTIC_PERF_POSITION_HISTORY", environment: environment, defaultValue: 3),
3842
directMessageCount: integerValue("MESHTASTIC_PERF_DIRECT_MESSAGES", environment: environment, defaultValue: 0),
3943
channelMessageCount: integerValue("MESHTASTIC_PERF_CHANNEL_MESSAGES", environment: environment, defaultValue: 0),
4044
resetStore: boolValue("MESHTASTIC_PERF_RESET_STORE", environment: environment) || arguments.contains("--meshtastic-perf-reset"),
4145
compactNodeList: boolValue("MESHTASTIC_PERF_COMPACT_LIST", environment: environment) || arguments.contains("--meshtastic-perf-compact-list"),
4246
disableDiscovery: !boolValue("MESHTASTIC_PERF_ENABLE_DISCOVERY", environment: environment),
43-
initialTab: arguments.contains("--meshtastic-perf-start-map") ? .map : .nodes
47+
initialTab: arguments.contains("--meshtastic-perf-start-map") ? .map : .nodes,
48+
opensLocalStatsLog: arguments.contains("--meshtastic-perf-start-local-stats"),
49+
localStatsSameHourSeed: arguments.contains("--meshtastic-perf-local-stats-same-hour")
4450
)
4551
}
4652

@@ -69,11 +75,14 @@ enum PerformanceSeedData {
6975
try? context.save()
7076
}
7177
router.selectedTab = configuration.initialTab
78+
if configuration.opensLocalStatsLog {
79+
router.selectedNodeNum = 0x0A00_0000
80+
}
7281
Logger.data.info("📈 [PerfSeed] Existing large mesh seed found; skipping reseed")
7382
return
7483
}
7584

76-
Logger.data.info("📈 [PerfSeed] Seeding \(configuration.nodeCount, privacy: .public) nodes, \(configuration.telemetryHistoryPerNode, privacy: .public) telemetry samples/type, \(configuration.positionHistoryPerNode, privacy: .public) positions/node")
85+
Logger.data.info("📈 [PerfSeed] Seeding \(configuration.nodeCount, privacy: .public) nodes, \(configuration.telemetryHistoryPerNode, privacy: .public) telemetry samples/type, \(configuration.localStatsHistoryPerNode, privacy: .public) local stats samples/node, \(configuration.positionHistoryPerNode, privacy: .public) positions/node")
7786

7887
let now = Date()
7988
let baseNodeNum: Int64 = 0x0A00_0000
@@ -90,6 +99,9 @@ enum PerformanceSeedData {
9099
do {
91100
try context.save()
92101
router.selectedTab = configuration.initialTab
102+
if configuration.opensLocalStatsLog {
103+
router.selectedNodeNum = baseNodeNum
104+
}
93105
let duration = Date().timeIntervalSince(start)
94106
Logger.data.info("📈 [PerfSeed] Finished seeding \(configuration.nodeCount, privacy: .public) nodes in \(duration, privacy: .public) seconds")
95107
} catch {
@@ -199,6 +211,48 @@ enum PerformanceSeedData {
199211
environmentMetrics.nodeTelemetry = node
200212
context.insert(environmentMetrics)
201213
}
214+
215+
for sample in 0..<configuration.localStatsHistoryPerNode {
216+
let timestamp = if configuration.localStatsSameHourSeed {
217+
localStatsSameHourTimestamp(now: now, sample: sample)
218+
} else {
219+
now.addingTimeInterval(TimeInterval(-(sample * 900 + index % 600)))
220+
}
221+
let localStats = TelemetryEntity()
222+
localStats.metricsType = 4
223+
localStats.time = timestamp
224+
localStats.noiseFloor = syntheticNoiseFloor(nodeIndex: index, sample: sample)
225+
localStats.channelUtilization = Float((index * 3 + sample * 5) % 100) / 2
226+
localStats.airUtilTx = Float((index + sample * 2) % 80) / 10
227+
localStats.numPacketsTx = Int32(120 + index % 70 + sample * 3)
228+
localStats.numPacketsRx = Int32(300 + index % 140 + sample * 5)
229+
localStats.numPacketsRxBad = Int32((index + sample) % 11)
230+
localStats.numRxDupe = Int32((index + sample * 2) % 9)
231+
localStats.numTxRelay = Int32((index + sample * 3) % 24)
232+
localStats.numTxRelayCanceled = Int32((index + sample) % 4)
233+
localStats.numOnlineNodes = Int32(max(1, min(250, configuration.nodeCount - (sample % 12))))
234+
localStats.numTotalNodes = Int32(configuration.nodeCount)
235+
localStats.uptimeSeconds = Int32(86_400 + index * 60 + sample * 900)
236+
localStats.nodeTelemetry = node
237+
context.insert(localStats)
238+
}
239+
}
240+
241+
private static func localStatsSameHourTimestamp(now: Date, sample: Int) -> Date {
242+
let calendar = Calendar.current
243+
let currentHourStart = calendar.dateInterval(of: .hour, for: now)?.start ?? now
244+
let minute = calendar.component(.minute, from: now)
245+
let hourStart = minute < 25 ? currentHourStart.addingTimeInterval(-3_600) : currentHourStart
246+
return hourStart.addingTimeInterval(TimeInterval(sample * 300))
247+
}
248+
249+
private static func syntheticNoiseFloor(nodeIndex: Int, sample: Int) -> Int32 {
250+
let dailyWave = sin(Double(sample) / 8.0) * 5.0
251+
let nodeBias = Double((nodeIndex % 13) - 6)
252+
let interferenceSpike = sample.isMultiple(of: 37) ? 14.0 : 0.0
253+
let deterministicJitter = (deterministicUnitValue(nodeIndex * 4_096 + sample, salt: 0x8EBC_6AF0_9C88_C6E3) - 0.5) * 6.0
254+
let value = -102.0 + dailyWave + nodeBias + interferenceSpike + deterministicJitter
255+
return Int32(value.rounded())
202256
}
203257

204258
private static func insertPositions(

Meshtastic/Resources/docs/developer/swiftdata.html

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,11 @@ <h3>Environment variables</h3>
224224
<td>Device + environment metric samples per node.</td>
225225
</tr>
226226
<tr>
227+
<td><code>MESHTASTIC_PERF_LOCAL_STATS_HISTORY</code></td>
228+
<td><code>MESHTASTIC_PERF_TELEMETRY_HISTORY</code></td>
229+
<td>Local stats samples per node, including synthetic noise floor, packet counters, utilization, and node counts.</td>
230+
</tr>
231+
<tr>
227232
<td><code>MESHTASTIC_PERF_POSITION_HISTORY</code></td>
228233
<td><code>3</code></td>
229234
<td>Position history entries per node.</td>
@@ -265,9 +270,23 @@ <h3>Example: seed 5 000 nodes with a clean store</h3>
265270
SIMCTL_CHILD_MESHTASTIC_PERF_COMPACT_LIST=true \
266271
xcrun simctl launch &lt;UDID&gt; gvh.MeshtasticClient
267272
</code></pre>
273+
<h3>Example: seed local stats for noise-floor chart work</h3>
274+
<p>Use a smaller node count and a larger local stats history when tuning the Local Stats Log UI. This keeps the simulator responsive while giving the chart enough variation to show quiet periods, busy periods, and occasional interference spikes.</p>
275+
<pre><code class="language-bash">SIMCTL_CHILD_MESHTASTIC_PERF_SEED_NODES=20 \
276+
SIMCTL_CHILD_MESHTASTIC_PERF_LOCAL_STATS_HISTORY=168 \
277+
SIMCTL_CHILD_MESHTASTIC_PERF_TELEMETRY_HISTORY=3 \
278+
SIMCTL_CHILD_MESHTASTIC_PERF_POSITION_HISTORY=3 \
279+
SIMCTL_CHILD_MESHTASTIC_PERF_RESET_STORE=true \
280+
SIMCTL_CHILD_MESHTASTIC_PERF_ENABLE_DISCOVERY=0 \
281+
xcrun simctl launch &lt;UDID&gt; gvh.MeshtasticClient \
282+
--meshtastic-perf-seed \
283+
--meshtastic-perf-start-local-stats
284+
</code></pre>
285+
<p><code>--meshtastic-perf-start-local-stats</code> selects seeded node <code>0x0A000000</code> and opens its Local Stats Log directly in DEBUG simulator builds.</p>
286+
<p>Add <code>--meshtastic-perf-local-stats-same-hour</code> when checking short-range noise-floor chart layout. It keeps local stats samples in the same hour at 5-minute intervals, which makes <code>1h</code> axis label clipping easy to reproduce.</p>
268287
<p>On subsequent launches <strong>without</strong> <code>MESHTASTIC_PERF_RESET_STORE</code>, the harness detects the existing node count and skips re-seeding, so the app starts at full speed against the already-seeded store.</p>
269288
<h3>What to expect</h3>
270-
<p>5 000 nodes (3 telemetry samples/type, 3 positions/node) seed in approximately <strong>12 seconds</strong> on an Apple Silicon Mac. The app navigates automatically to the Nodes tab. Typical idle CPU after seeding is under 2%.</p>
289+
<p>5 000 nodes (3 device/environment telemetry samples, 3 local stats samples, 3 positions/node) seed in approximately <strong>12 seconds</strong> on an Apple Silicon Mac. The app navigates automatically to the Nodes tab. Typical idle CPU after seeding is under 2%.</p>
271290
<blockquote>
272291
<p><strong>Tip — Checking seed progress</strong>
273292
Seed log lines are emitted at <code>Info</code> level under the <code>🗄️ Data</code> OSLog category. To stream them:</p>

Meshtastic/Resources/docs/index.json

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -944,35 +944,35 @@
944944
"meshtastic",
945945
"perf",
946946
"seed",
947+
"node",
948+
"simctl",
949+
"stats",
947950
"nodes",
948-
"context",
951+
"local",
949952
"store",
950-
"node",
953+
"context",
954+
"packet",
951955
"model",
956+
"history",
957+
"child",
952958
"app",
953-
"packet",
954959
"data",
955960
"var",
956-
"type",
961+
"true",
962+
"telemetry",
957963
"swift",
958964
"save",
959-
"messages",
960-
"true",
961-
"simctl",
962-
"set",
963-
"schema",
964965
"position",
965966
"per",
966-
"nodeinfoentity",
967+
"messages",
967968
"launch",
968-
"channel",
969-
"writes",
970969
"use",
971-
"telemetry",
972-
"self",
973-
"seeding"
970+
"type",
971+
"set",
972+
"schema",
973+
"nodeinfoentity"
974974
],
975-
"charCount": 8479
975+
"charCount": 9767
976976
},
977977
{
978978
"id": "tak-protocol",

Meshtastic/Resources/docs/markdown/developer/swiftdata.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ Pass variables to the simulator using the `SIMCTL_CHILD_` prefix (the prefix is
189189
|----------|---------|-------------|
190190
| `MESHTASTIC_PERF_SEED_NODES` || **Required to activate.** Number of nodes to seed (e.g. `5000`). |
191191
| `MESHTASTIC_PERF_TELEMETRY_HISTORY` | `3` | Device + environment metric samples per node. |
192+
| `MESHTASTIC_PERF_LOCAL_STATS_HISTORY` | `MESHTASTIC_PERF_TELEMETRY_HISTORY` | Local stats samples per node, including synthetic noise floor, packet counters, utilization, and node counts. |
192193
| `MESHTASTIC_PERF_POSITION_HISTORY` | `3` | Position history entries per node. |
193194
| `MESHTASTIC_PERF_DIRECT_MESSAGES` | `0` | Direct messages to seed between node 0 and node 1. |
194195
| `MESHTASTIC_PERF_CHANNEL_MESSAGES` | `0` | Channel messages to seed on channel 0. |
@@ -213,11 +214,31 @@ SIMCTL_CHILD_MESHTASTIC_PERF_COMPACT_LIST=true \
213214
xcrun simctl launch <UDID> gvh.MeshtasticClient
214215
```
215216

217+
### Example: seed local stats for noise-floor chart work
218+
219+
Use a smaller node count and a larger local stats history when tuning the Local Stats Log UI. This keeps the simulator responsive while giving the chart enough variation to show quiet periods, busy periods, and occasional interference spikes.
220+
221+
```bash
222+
SIMCTL_CHILD_MESHTASTIC_PERF_SEED_NODES=20 \
223+
SIMCTL_CHILD_MESHTASTIC_PERF_LOCAL_STATS_HISTORY=168 \
224+
SIMCTL_CHILD_MESHTASTIC_PERF_TELEMETRY_HISTORY=3 \
225+
SIMCTL_CHILD_MESHTASTIC_PERF_POSITION_HISTORY=3 \
226+
SIMCTL_CHILD_MESHTASTIC_PERF_RESET_STORE=true \
227+
SIMCTL_CHILD_MESHTASTIC_PERF_ENABLE_DISCOVERY=0 \
228+
xcrun simctl launch <UDID> gvh.MeshtasticClient \
229+
--meshtastic-perf-seed \
230+
--meshtastic-perf-start-local-stats
231+
```
232+
233+
`--meshtastic-perf-start-local-stats` selects seeded node `0x0A000000` and opens its Local Stats Log directly in DEBUG simulator builds.
234+
235+
Add `--meshtastic-perf-local-stats-same-hour` when checking short-range noise-floor chart layout. It keeps local stats samples in the same hour at 5-minute intervals, which makes `1h` axis label clipping easy to reproduce.
236+
216237
On subsequent launches **without** `MESHTASTIC_PERF_RESET_STORE`, the harness detects the existing node count and skips re-seeding, so the app starts at full speed against the already-seeded store.
217238

218239
### What to expect
219240

220-
5 000 nodes (3 telemetry samples/type, 3 positions/node) seed in approximately **12 seconds** on an Apple Silicon Mac. The app navigates automatically to the Nodes tab. Typical idle CPU after seeding is under 2%.
241+
5 000 nodes (3 device/environment telemetry samples, 3 local stats samples, 3 positions/node) seed in approximately **12 seconds** on an Apple Silicon Mac. The app navigates automatically to the Nodes tab. Typical idle CPU after seeding is under 2%.
221242

222243
> **Tip — Checking seed progress**
223244
> Seed log lines are emitted at `Info` level under the `🗄️ Data` OSLog category. To stream them:

Meshtastic/Views/Nodes/Helpers/Actions/RequestLocalStatsButton.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ struct RequestLocalStatsButton: View {
55
@EnvironmentObject var accessoryManager: AccessoryManager
66

77
var node: NodeInfoEntity
8+
var title = "Request Local Stats"
9+
var cooldownTitle = "Local Stats"
10+
var systemImage = "chart.bar"
811

912
@State
1013
private var isPresentingLocalStatsSentAlert: Bool = false
@@ -27,17 +30,19 @@ struct RequestLocalStatsButton: View {
2730
} label: { completion in
2831
if let completion, completion.percentComplete > 0.0 {
2932
Label {
30-
Text("Local Stats (in \(Int(completion.secondsRemaining))s)")
33+
Text("\(cooldownTitle) \(Int(completion.secondsRemaining))s")
3134
.foregroundStyle(.secondary)
35+
.lineLimit(1)
3236
} icon: {
3337
Image("progress.ring.dashed", variableValue: completion.percentComplete)
3438
.foregroundStyle(.secondary)
3539
}.disabled(true)
3640
} else {
3741
Label {
38-
Text("Request Local Stats")
42+
Text(title)
43+
.lineLimit(1)
3944
} icon: {
40-
Image(systemName: "chart.bar")
45+
Image(systemName: systemImage)
4146
.symbolRenderingMode(.hierarchical)
4247
}
4348
}

0 commit comments

Comments
 (0)