Node database: collect nodes during sync, expose GET /api/nodes (#7)
CI / build + unit (push) Successful in 39s
CI / build + unit (push) Successful in 39s
This commit was merged in pull request #7.
This commit is contained in:
+103
-3
@@ -3,9 +3,16 @@ import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
ApiOperation,
|
||||
ApiProperty,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { BROADCAST_ADDR } from './codec.js';
|
||||
import { MeshService } from './mesh.service.js';
|
||||
|
||||
@@ -21,18 +28,45 @@ export class SendTextDto {
|
||||
to?: string | number;
|
||||
}
|
||||
|
||||
// tag description is set via DocumentBuilder.addTag in main.ts (v12 ApiTagOptions has no description)
|
||||
@ApiTags('Mesh')
|
||||
@Controller()
|
||||
export class MeshController {
|
||||
constructor(private readonly mesh: MeshService) {}
|
||||
|
||||
@Get('status')
|
||||
@ApiOperation({ summary: 'Bridge connection status' })
|
||||
@ApiOperation({
|
||||
summary: 'Bridge connection status',
|
||||
description:
|
||||
'Whether the bot is connected to the meshtastic-serial-bridge and which node number this device has.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Connection status',
|
||||
example: {
|
||||
connected: true,
|
||||
myNodeNum: 123456789,
|
||||
host: '192.168.1.4',
|
||||
port: 4403,
|
||||
},
|
||||
})
|
||||
status() {
|
||||
return this.mesh.status();
|
||||
}
|
||||
|
||||
@Post('send')
|
||||
@ApiOperation({ summary: 'Send a text message over the mesh' })
|
||||
@ApiOperation({
|
||||
summary: 'Send a text message over the mesh',
|
||||
description:
|
||||
'Queues a TEXT_MESSAGE_APP packet on the pinned test channel (index 2, "testowy" — never the primary channel). Returns the Meshtastic packet id.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Packet queued',
|
||||
example: { id: 123456789 },
|
||||
})
|
||||
@ApiResponse({ status: 400, description: 'Invalid text or target' })
|
||||
@ApiResponse({ status: 503, description: 'Bridge not connected' })
|
||||
async send(@Body() body: SendTextDto) {
|
||||
if (typeof body?.text !== 'string' || body.text.length === 0) {
|
||||
throw new BadRequestException('text must be a non-empty string');
|
||||
@@ -53,8 +87,74 @@ export class MeshController {
|
||||
@Get('channels')
|
||||
@ApiOperation({
|
||||
summary: "The device's Meshtastic channels (keys redacted)",
|
||||
description:
|
||||
'Channel table streamed during the initial sync; empty until it completes. psk/keys are never exposed.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Channels sorted by index',
|
||||
example: [
|
||||
{ index: 0, name: 'Default', uplinkEnabled: true, downlinkEnabled: true },
|
||||
{
|
||||
index: 2,
|
||||
name: 'testowy',
|
||||
uplinkEnabled: false,
|
||||
downlinkEnabled: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
channels() {
|
||||
return this.mesh.channels();
|
||||
}
|
||||
|
||||
@Get('nodes')
|
||||
@ApiOperation({
|
||||
summary: 'Known mesh nodes (lean summaries)',
|
||||
description:
|
||||
'Node database collected during the initial sync plus live POSITION_APP updates; reset on every bridge reconnect. Names, hwModel and batteryLevel appear only when the device reported them.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Nodes sorted by node number',
|
||||
example: [
|
||||
{
|
||||
num: 123456789,
|
||||
longName: 'Kamil SP8MK/P',
|
||||
shortName: 'KK',
|
||||
hwModel: 'TBEAM',
|
||||
batteryLevel: 87,
|
||||
lastHeard: 1758000000,
|
||||
position: { lat: 52.1049637, lon: 20.9833152, alt: 123 },
|
||||
},
|
||||
],
|
||||
})
|
||||
async nodes() {
|
||||
return this.mesh.nodes();
|
||||
}
|
||||
|
||||
@Get('nodes/:id')
|
||||
@ApiOperation({
|
||||
summary: 'Look up one node by number or name',
|
||||
description:
|
||||
'Exact node number first (numbers and numeric strings), then case-insensitive exact long/short name, else the unique case-insensitive substring match across names. 404 when nothing or more than one node matches.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The matching node',
|
||||
example: {
|
||||
num: 123456789,
|
||||
longName: 'Kamil SP8MK/P',
|
||||
shortName: 'KK',
|
||||
lastHeard: null,
|
||||
},
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: 'No node (or more than one) matching the given id or name',
|
||||
})
|
||||
async node(@Param('id') id: string) {
|
||||
const n = await this.mesh.node(id);
|
||||
if (!n) throw new NotFoundException(`no node matching '${id}'`);
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user