Checkpoint
Some checks failed
Test and build / Test and lint (push) Failing after 36s
Test and build / Build collector (push) Failing after 43s
Test and build / Build receiver (push) Failing after 42s

This commit is contained in:
2026-03-05 15:38:18 +01:00
parent 3106b2cf45
commit 13afa08e8a
108 changed files with 19509 additions and 729 deletions

331
server/handlers_meshcore.go Normal file
View File

@@ -0,0 +1,331 @@
package server
import (
"net/http"
"strconv"
"github.com/labstack/echo/v4"
"git.maze.io/go/ham/protocol/meshcore"
"git.maze.io/ham/hamview/schema"
)
// handleGetMeshCore returns aggregated statistics for the MeshCore network.
//
// Endpoint: GET /api/v1/meshcore
//
// Response: 200 OK
//
// MeshCoreStats - Network statistics including message counts and packet timeline
//
// Response: 500 Internal Server Error
//
// ErrorResponse - Error retrieving statistics
//
// Example Request:
//
// GET /api/v1/meshcore
//
// Example Response:
//
// {
// "messages": 150234,
// "nodes": 127,
// "receivers": 8,
// "packets": {
// "timestamps": [1709650800, 1709654400, 1709658000],
// "packets": [142, 203, 178]
// }
// }
func (s *Server) handleGetMeshCore(c echo.Context) error {
stats, err := schema.GetMeshCoreStats(c.Request().Context())
if err != nil {
return s.apiError(c, err)
}
return c.JSON(http.StatusOK, stats)
}
// handleGetMeshCoreGroups returns a list of public MeshCore groups/channels.
//
// Endpoint: GET /api/v1/meshcore/groups
//
// Response: 200 OK
//
// []MeshCoreGroup - Array of public group objects
//
// Response: 500 Internal Server Error
//
// ErrorResponse - Error retrieving groups
//
// Example Request:
//
// GET /api/v1/meshcore/groups
//
// Example Response:
//
// [
// {
// "id": 5,
// "name": "General Chat",
// "secret": "0123456789abcdef0123456789abcdef"
// },
// {
// "id": 7,
// "name": "Emergency",
// "secret": "fedcba9876543210fedcba9876543210"
// }
// ]
func (s *Server) handleGetMeshCoreGroups(c echo.Context) error {
groups, err := schema.GetMeshCoreGroups(c.Request().Context())
if err != nil {
return s.apiError(c, err)
}
return c.JSON(http.StatusOK, groups)
}
// handleGetMeshCoreNodes returns a list of MeshCore network nodes.
//
// Endpoint: GET /api/v1/meshcore/nodes
//
// Query Parameters:
// - type (optional): Filter by node type
// - "chat" - Chat nodes
// - "room" - Room nodes
// - "sensor" - Sensor nodes
// - "repeater" - Repeater nodes (default)
//
// Response: 200 OK
//
// []MeshCoreNode - Array of node objects, sorted by last_heard_at (descending)
//
// Response: 500 Internal Server Error
//
// ErrorResponse - Error retrieving nodes
//
// Example Request:
//
// GET /api/v1/meshcore/nodes
// GET /api/v1/meshcore/nodes?type=chat
//
// Example Response:
//
// [
// {
// "id": 42,
// "packet": [],
// "name": "NODE-CHARLIE",
// "type": 0,
// "prefix": "mc",
// "public_key": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
// "first_heard_at": "2026-01-15T08:00:00Z",
// "last_heard_at": "2026-03-05T14:25:00Z",
// "last_latitude": 52.3667,
// "last_longitude": 4.8945
// }
// ]
func (s *Server) handleGetMeshCoreNodes(c echo.Context) error {
var (
nodes []*schema.MeshCoreNode
err error
)
if kind := c.QueryParam("type"); kind != "" {
switch kind {
case "chat":
nodes, err = schema.GetMeshCoreNodesByType(c.Request().Context(), meshcore.Chat)
case "room":
nodes, err = schema.GetMeshCoreNodesByType(c.Request().Context(), meshcore.Room)
case "sensor":
nodes, err = schema.GetMeshCoreNodesByType(c.Request().Context(), meshcore.Sensor)
case "repeater":
fallthrough
default:
nodes, err = schema.GetMeshCoreNodesByType(c.Request().Context(), meshcore.Repeater)
}
} else {
nodes, err = schema.GetMeshCoreNodes(c.Request().Context())
}
if err != nil {
return s.apiError(c, err)
}
return c.JSON(http.StatusOK, nodes)
}
// handleGetMeshCoreNodesCloseTo returns nodes within a specified radius of a reference node.
//
// Endpoint: GET /api/v1/meshcore/nodes/close-to/:publickey
//
// Path Parameters:
// - publickey: Hex-encoded Ed25519 public key (64 characters) of the reference node
//
// Query Parameters:
// - radius (optional): Search radius in meters (default: 25000 = 25 km)
//
// Response: 200 OK
//
// NodesCloseToResponse - Object containing the reference node and nearby nodes
// {
// "node": MeshCoreNode, // The reference node
// "nodes": []MeshCoreNode // Nearby nodes, sorted by distance (ascending)
// // Each node has the "distance" field populated (in meters)
// }
//
// Response: 404 Not Found
//
// null - Node not found or has no location data
//
// Response: 400 Bad Request
//
// ErrorResponse - Invalid radius parameter
//
// Response: 500 Internal Server Error
//
// ErrorResponse - Error performing proximity query
//
// Example Request:
//
// GET /api/v1/meshcore/nodes/close-to/1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
// GET /api/v1/meshcore/nodes/close-to/1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef?radius=50000
//
// Example Response:
//
// {
// "node": {
// "id": 42,
// "name": "NODE-CHARLIE",
// "type": 0,
// "prefix": "mc",
// "public_key": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
// "first_heard_at": "2026-01-15T08:00:00Z",
// "last_heard_at": "2026-03-05T14:25:00Z",
// "last_latitude": 52.3667,
// "last_longitude": 4.8945,
// "distance": 0
// },
// "nodes": [
// {
// "id": 43,
// "name": "NODE-DELTA",
// "type": 0,
// "prefix": "mc",
// "public_key": "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
// "first_heard_at": "2026-02-01T10:00:00Z",
// "last_heard_at": "2026-03-05T14:20:00Z",
// "last_latitude": 52.3700,
// "last_longitude": 4.9000,
// "distance": 450.5
// }
// ]
// }
func (s *Server) handleGetMeshCoreNodesCloseTo(c echo.Context) error {
var radius float64
if value := c.QueryParam("radius"); value != "" {
var err error
if radius, err = strconv.ParseFloat(value, 64); err != nil {
return s.apiError(c, err)
}
}
if radius <= 0 {
radius = 25000.0 // 25 km
}
node, nodes, err := schema.GetMeshCoreNodesCloseTo(c.Request().Context(), c.Param("publickey"), radius)
if err != nil {
return s.apiError(c, err)
}
return c.JSON(http.StatusOK, map[string]any{
"node": node,
"nodes": nodes,
})
}
// handleGetMeshCorePackets returns MeshCore packets based on various filter criteria.
//
// Endpoint: GET /api/v1/meshcore/packets
//
// Query Parameters (mutually exclusive, evaluated in order):
// - hash: 16-character hex hash - Returns all packets with this hash
// - type: Integer payload type - Returns packets of this type
// - Can be combined with channel_hash for group messages
// - channel_hash: 2-character channel hash (requires type parameter)
// - (no parameters): Returns the 100 most recent packets
//
// Response: 200 OK
//
// []MeshCorePacket - Array of packet objects
//
// Response: 400 Bad Request
//
// ErrorResponse - Invalid hash format or payload type
//
// Response: 500 Internal Server Error
//
// ErrorResponse - Error retrieving packets
//
// Example Requests:
//
// GET /api/v1/meshcore/packets
// GET /api/v1/meshcore/packets?hash=a1b2c3d4e5f67890
// GET /api/v1/meshcore/packets?type=3
// GET /api/v1/meshcore/packets?type=3&channel_hash=ab
//
// Example Response:
//
// [
// {
// "id": 12345,
// "radio_id": 1,
// "radio": null,
// "snr": 8.5,
// "rssi": -95,
// "version": 1,
// "route_type": 0,
// "payload_type": 3,
// "hash": "a1b2c3d4e5f67890",
// "path": "AQIDBA==",
// "payload": "SGVsbG8gV29ybGQ=",
// "raw": "AQIDBAUGBwg=",
// "parsed": {"text": "Hello World"},
// "channel_hash": "ab",
// "received_at": "2026-03-05T14:30:00Z"
// }
// ]
//
// Payload Types:
// - 0: Ping
// - 1: Node announcement
// - 2: Direct text message
// - 3: Group text message
// - 4: Position update
// - (other types may be defined by the protocol)
func (s *Server) handleGetMeshCorePackets(c echo.Context) error {
var (
ctx = c.Request().Context()
packets []*schema.MeshCorePacket
err error
)
if hash := c.QueryParam("hash"); hash != "" {
packets, err = schema.GetMeshCorePacketsByHash(ctx, hash)
} else if kind := c.QueryParam("type"); kind != "" {
var payloadType int
if payloadType, err = strconv.Atoi(kind); err == nil {
if hash := c.QueryParam("channel_hash"); hash != "" {
packets, err = schema.GetMeshCorePacketsByChannelHash(ctx, hash)
} else {
packets, err = schema.GetMeshCorePacketsByPayloadType(ctx, meshcore.PayloadType(payloadType))
}
}
} else {
packets, err = schema.GetMeshCorePackets(ctx, 100)
}
if err != nil {
return s.apiError(c, err)
}
return c.JSON(http.StatusOK, packets)
}