-
-
Notifications
You must be signed in to change notification settings - Fork 11
feat(connection): host-link strength meter for BLE and WiFi/TCP #769
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| // @vitest-environment node | ||
| import net from 'net'; | ||
| import { afterEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import { probeHttpRttMs, probeTcpRttMs } from './host-link-rtt'; | ||
|
|
||
| describe('probeTcpRttMs', () => { | ||
| let server: net.Server | null = null; | ||
|
|
||
| afterEach(async () => { | ||
| await new Promise<void>((resolve) => { | ||
| if (!server) { | ||
| resolve(); | ||
| return; | ||
| } | ||
| server.close(() => { | ||
| resolve(); | ||
| }); | ||
| server = null; | ||
| }); | ||
| }); | ||
|
|
||
| it('returns a finite RTT when the host accepts a TCP connect', async () => { | ||
| server = net.createServer((socket) => { | ||
| socket.destroy(); | ||
| }); | ||
| await new Promise<void>((resolve) => { | ||
| server!.listen(0, '127.0.0.1', () => { | ||
| resolve(); | ||
| }); | ||
| }); | ||
| const addr = server.address(); | ||
| if (!addr || typeof addr === 'string') throw new Error('expected TCP port'); | ||
| const rtt = await probeTcpRttMs('127.0.0.1', addr.port); | ||
| expect(rtt).not.toBeNull(); | ||
| expect(rtt!).toBeGreaterThanOrEqual(0); | ||
| expect(rtt!).toBeLessThan(3000); | ||
| }); | ||
|
|
||
| it('returns null when the port is closed', async () => { | ||
| const rtt = await probeTcpRttMs('127.0.0.1', 1); | ||
| expect(rtt).toBeNull(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('probeHttpRttMs', () => { | ||
| it('returns null when fetch fails', async () => { | ||
| vi.stubGlobal( | ||
| 'fetch', | ||
| vi.fn(() => Promise.reject(new Error('network down'))), | ||
| ); | ||
| await expect(probeHttpRttMs('127.0.0.1', false)).resolves.toBeNull(); | ||
| vi.unstubAllGlobals(); | ||
| }); | ||
|
|
||
| it('returns RTT when the host answers', async () => { | ||
| vi.stubGlobal( | ||
| 'fetch', | ||
| vi.fn(() => Promise.resolve({ ok: true, status: 200 } as Response)), | ||
| ); | ||
| const rtt = await probeHttpRttMs('example.test', false); | ||
| expect(rtt).not.toBeNull(); | ||
| expect(rtt!).toBeGreaterThanOrEqual(0); | ||
| vi.unstubAllGlobals(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import net from 'net'; | ||
|
|
||
| import { formatHostForSocket } from '../shared/connectHost'; | ||
| import { MS_PER_SECOND } from '../shared/timeConstants'; | ||
| import { sanitizeLogMessage } from './log-service'; | ||
|
|
||
| /** Align with renderer `HOST_LINK_RTT_PROBE_TIMEOUT_MS`. */ | ||
| export const HOST_LINK_RTT_PROBE_TIMEOUT_MS = 3 * MS_PER_SECOND; | ||
|
|
||
| /** | ||
| * Time a Meshtastic HTTP `/json/report` GET. Returns RTT ms, or null on failure. | ||
| * Does not throw — callers treat null as "no bars". | ||
| */ | ||
| export async function probeHttpRttMs(host: string, tls: boolean): Promise<number | null> { | ||
| const protocol = tls ? 'https' : 'http'; | ||
| const url = `${protocol}://${host}/json/report`; | ||
| const started = Date.now(); | ||
| try { | ||
| const res = await fetch(url, { signal: AbortSignal.timeout(HOST_LINK_RTT_PROBE_TIMEOUT_MS) }); | ||
| const rtt = Date.now() - started; | ||
| if (!res.ok) { | ||
| console.debug( | ||
| `[hostLink] HTTP probe non-OK ${res.status} for ${sanitizeLogMessage(host)} rtt=${rtt}ms`, | ||
| ); | ||
| // Still usable as latency if the host answered quickly. | ||
| return rtt; | ||
| } | ||
| return rtt; | ||
| } catch (err) { | ||
| console.debug( | ||
| `[hostLink] HTTP probe failed for ${sanitizeLogMessage(host)}:`, | ||
| sanitizeLogMessage(err instanceof Error ? err.message : String(err)), | ||
| ); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Time a TCP connect to host:port, then destroy the socket immediately. | ||
| * Measures LAN reachability latency without attaching a protocol session. | ||
| */ | ||
| export function probeTcpRttMs(host: string, port: number): Promise<number | null> { | ||
| return new Promise((resolve) => { | ||
| let settled = false; | ||
| const socketHost = formatHostForSocket(host); | ||
| const socket = new net.Socket(); | ||
| const started = Date.now(); | ||
| const finish = (rtt: number | null) => { | ||
| if (settled) return; | ||
| settled = true; | ||
| clearTimeout(timer); | ||
| try { | ||
| socket.removeAllListeners(); | ||
| socket.destroy(); | ||
| } catch { | ||
| // catch-no-log-ok probe socket cleanup | ||
| } | ||
| resolve(rtt); | ||
| }; | ||
| const timer = setTimeout(() => { | ||
| finish(null); | ||
| }, HOST_LINK_RTT_PROBE_TIMEOUT_MS); | ||
| socket.once('connect', () => { | ||
| finish(Date.now() - started); | ||
| }); | ||
| socket.once('error', (err) => { | ||
| console.debug( | ||
| `[hostLink] TCP probe failed for ${sanitizeLogMessage(socketHost)}:${port}:`, | ||
| sanitizeLogMessage(err.message), | ||
| ); | ||
| finish(null); | ||
| }); | ||
| try { | ||
| socket.connect(port, socketHost); | ||
| } catch (err) { | ||
| console.debug( | ||
| `[hostLink] TCP probe connect threw for ${sanitizeLogMessage(socketHost)}:${port}:`, | ||
| sanitizeLogMessage(err instanceof Error ? err.message : String(err)), | ||
| ); | ||
| finish(null); | ||
| } | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: Colorado-Mesh/mesh-client
Length of output: 163
🏁 Script executed:
Repository: Colorado-Mesh/mesh-client
Length of output: 50381
Use
clampTcpPort()for the TCP probe port validation.clampTcpPort()is the shared TCP-port helper insrc/shared/tcpPort.ts; replace the inlineNumber.isInteger(port) || port < 1 || port > 65535check with it to satisfy the shared-validation guideline.🧰 Tools
🪛 ast-grep (0.45.0)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from 'child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
🤖 Prompt for AI Agents
Source: Coding guidelines