PHP Classes

File: docs/proposals/CWNMeshMapping.md

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   docs/proposals/CWNMeshMapping.md   Download  
File: docs/proposals/CWNMeshMapping.md
Role: Auxiliary data
Content type: text/markdown
Description: Auxiliary data
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 10,543 bytes
 

Contents

Class file image Download

CWN MeshCore Node Mapping

> Draft ? This proposal was generated by AI and may not have been reviewed for accuracy.

Overview

Extend the Community Wireless Node List (CWN) WebDoor to automatically ingest node advertisements heard by the binkterm-php MeshCore bridge. When the bridge hears a MeshCore advert (either a live push or a startup contact-list dump), it POSTs the node data to a new BBS API endpoint, which UPSERTs the record into cwn_networks. Nodes auto-expire if not heard for 2 days.

The bridge lives in a separate repository (binktermphp-meshcorebridge). Changes are needed in both repos.

Design Decisions (already settled)

| # | Decision | |---|----------| | 1 | Skip nodes with invalid/missing coordinates. Only ingest if has_location is true and lat/lon pass range validation. | | 2 | 2-day rolling expiry. MeshCore-sourced nodes are hidden from map/list queries when last_seen_at < NOW() - INTERVAL '2 days'. They are not hard-deleted. | | 3 | Contact-list dump also pushed. The startup contacts_start / contact / contacts_end sequence should be forwarded too, not only live new_advert pushes. | | 4 | Repeaters only. Only nodes with adv_type_name === 'repeater' (type 2) are ingested. Chat/companion nodes (type 1), room servers (type 3), and sensors (type 4) are silently skipped. Filter is applied in the bridge before the HTTP call is made. |

Data Flow

MeshCore radio
    ?
    ?  new_advert (0x8A push)  OR  contact record (0x03, from startup dump)
    ?
MeshCoreBridge::handleFrame()           [binktermphp-meshcorebridge]
    ?
    ?  if has_location && coords valid
    ?
BbsApiClient::reportAdvert(array $data)
    ?  POST /api/meshcore/advert
    ?  Authorization: Bearer <api_key>
    ?
BBS route handler                        [binkterm-php]
    ?  authenticates via requirePacketBbsAuth($bridgeNodeId)
    ?  (same auth helper already used by /api/packetbbs/command)
    ?
UPSERT cwn_networks WHERE public_key = $pubKeyHex
    action = 'created' | 'updated'

Database Migration

Check the current highest migration version with:

ls database/migrations/ | sort -V | tail -5

Name the new file v1.11.0.87_cwn_meshcore_nodes.sql (one increment above the current max).

-- Add MeshCore / machine-ingest support to cwn_networks

ALTER TABLE cwn_networks
    ALTER COLUMN submitted_by       DROP NOT NULL,
    ALTER COLUMN submitted_by_username DROP NOT NULL,
    ALTER COLUMN description        DROP NOT NULL;

ALTER TABLE cwn_networks
    ADD COLUMN IF NOT EXISTS public_key   VARCHAR(64),   -- full 32-byte pub key as hex (64 chars)
    ADD COLUMN IF NOT EXISTS source_type  VARCHAR(20) NOT NULL DEFAULT 'manual',
    ADD COLUMN IF NOT EXISTS last_seen_at TIMESTAMP,
    ADD COLUMN IF NOT EXISTS hop_count    SMALLINT;

-- Partial unique index: one CWN row per MeshCore node (allows NULLs for manual entries)
CREATE UNIQUE INDEX IF NOT EXISTS idx_cwn_networks_public_key
    ON cwn_networks(public_key)
    WHERE public_key IS NOT NULL;

Why submitted_by and description become nullable: machine-ingested entries have no human submitter and no human-written description. All existing manual rows keep their NOT NULL values; only new auto-ingest rows will carry NULLs here.

New BBS API Endpoint

File: routes/packetbbs-routes.php (reuses the existing requirePacketBbsAuth() helper)

POST /api/meshcore/advert
Authorization: Bearer <api_key>
Content-Type: application/json

Request body

{
    "bridge_node_id": "aabbccddeeff",
    "pub_key_hex":    "aabbccddeeff001122334455667788990011223344556677889900112233445566",
    "name":           "NodeName",
    "adv_type":       "chat",
    "latitude":       37.123456,
    "longitude":      -122.123456,
    "hop_count":      0,
    "timestamp_iso":  "2026-05-01T12:34:56Z"
}

Fields name and adv_type may be empty strings if the radio didn't include them. hop_count is the out_path_len from the decoded packet (0 = direct neighbour).

Validation

  • Reject if `latitude` outside [-90, 90] or `longitude` outside [-180, 180].
  • Reject if `pub_key_hex` is not exactly 64 lowercase hex characters.

UPSERT logic

INSERT INTO cwn_networks
    (public_key, ssid, latitude, longitude, source_type, hop_count, last_seen_at,
     network_type, bbs_name)
VALUES
    (:pub_key_hex, :name, :lat, :lon, 'meshcore', :hops, NOW(), :adv_type, :bbs_name)
ON CONFLICT (public_key) WHERE public_key IS NOT NULL DO UPDATE SET
    ssid         = EXCLUDED.ssid,
    latitude     = EXCLUDED.latitude,
    longitude    = EXCLUDED.longitude,
    hop_count    = EXCLUDED.hop_count,
    network_type = EXCLUDED.network_type,
    last_seen_at = NOW(),
    date_updated = NOW()
RETURNING id, (xmax = 0) AS was_inserted

was_inserted is true on INSERT, false on UPDATE ? use this to build the action field in the JSON response.

The bbs_name is fetched server-side from BinkpConfig::getInstance()->getSystemName().

Response

{ "success": true, "action": "created", "network_id": 42 }

Bridge Changes (binktermphp-meshcorebridge)

src/BbsApiClient.php ? new method

/
 * Report a MeshCore node advertisement to the BBS CWN mapper.
 *
 * @param array{
 *     pub_key_hex: string,
 *     name: string,
 *     adv_type: string,
 *     latitude: float,
 *     longitude: float,
 *     hop_count: int,
 *     timestamp_iso: string
 * } $data
 */
public function reportAdvert(array $data): bool
{
    $data['bridge_node_id'] = $this->bridgeNodeId ?? '';
    $response = $this->post('/api/meshcore/advert', json_encode($data));
    return $response !== null;
}

src/MeshCoreBridge.php ? handleFrame() additions

Add two new cases to the switch on $packet['type']:

case 'new_advert':
    $this->maybeReportAdvert($packet);
    break;

case 'contact':
    $this->maybeReportAdvert($packet);
    break;

Add a private helper:

private function maybeReportAdvert(array $packet): void
{
    // Only ingest repeater nodes
    if (($packet['adv_type_name'] ?? '') !== 'repeater') {
        return;
    }

    $lat = (float)($packet['adv_lat_deg'] ?? 0.0);
    $lon = (float)($packet['adv_lon_deg'] ?? 0.0);

    // Reject zero coords and out-of-range values
    if ($lat === 0.0 && $lon === 0.0) {
        return;
    }
    if ($lat < -90.0 || $lat > 90.0 || $lon < -180.0 || $lon > 180.0) {
        return;
    }

    $pubKey = (string)($packet['pub_key_hex'] ?? '');
    if (strlen($pubKey) !== 64) {
        return;
    }

    $ok = $this->api->reportAdvert([
        'pub_key_hex'   => $pubKey,
        'name'          => (string)($packet['adv_name'] ?? ''),
        'adv_type'      => (string)($packet['adv_type_name'] ?? ''),
        'latitude'      => $lat,
        'longitude'     => $lon,
        'hop_count'     => (int)($packet['out_path_len'] ?? 0),
        'timestamp_iso' => (string)($packet['lastmod_iso'] ?? $packet['last_advert_iso'] ?? gmdate('c')),
    ]);

    if (!$ok && !empty($this->config['debug'])) {
        $this->log(sprintf(
            'advert report failed for %s: %s',
            substr($pubKey, 0, 12),
            $this->api->getLastError() ?? 'unknown error'
        ));
    }
}

Note on the zero-coordinate check: lat === 0.0 && lon === 0.0 catches the common case of a node that has never acquired GPS (MeshCore stores 0,0 when no fix). Per the design decision, these are silently skipped.

CWN WebDoor Changes

public_html/webdoors/cwn/api.php

handleListNetworks() ? add expiry filter for meshcore entries:

AND (
    source_type != 'meshcore'
    OR last_seen_at > NOW() - INTERVAL '2 days'
)

handleSearch() / searchByRadius() / searchByKeyword() ? same expiry clause.

handleSubmitNetwork() ? no change; manual submissions continue to require all fields including description and will continue to have submitted_by set.

public_html/webdoors/cwn/js/cwn.js

Map markers: use a different icon for meshcore entries vs manual entries. A simple approach is a coloured divIcon based on source_type:

function markerIconFor(network) {
    const isMesh = network.source_type === 'meshcore';
    return L.divIcon({
        className: '',
        html: `<i class="fas ${isMesh ? 'fa-broadcast-tower' : 'fa-wifi'}"
                  style="font-size:20px;color:${isMesh ? '#0dcaf0' : '#0d6efd'}"></i>`,
        iconSize: [24, 24],
        iconAnchor: [12, 12]
    });
}

Detail view (displayNetworkDetails()): add: - source_type badge (meshcore = cyan badge-info, manual = blue badge-primary) - public_key row: display first 12 chars with full key on title tooltip and a copy button - hop_count row (only shown for meshcore entries) - last_seen_at row replacing "Last Verified" for meshcore entries

List items: append a small [mesh] badge next to the node name when source_type === 'meshcore'.

public_html/webdoors/cwn/index.html

No structural changes required beyond what cwn.js injects dynamically. Verify the details modal <dl> is flexible enough to accommodate extra rows (it is ? rows are built in JS).

Files to Touch (summary)

binkterm-php repo

| File | Change | |------|--------| | database/migrations/v1.11.0.87_cwn_meshcore_nodes.sql | New migration | | routes/packetbbs-routes.php | New POST /api/meshcore/advert route | | public_html/webdoors/cwn/api.php | Add 2-day expiry clause to list/search queries | | public_html/webdoors/cwn/js/cwn.js | Mesh-aware icons, public key in detail view |

binktermphp-meshcorebridge repo

| File | Change | |------|--------| | src/BbsApiClient.php | Add reportAdvert() method | | src/MeshCoreBridge.php | Handle new_advert and contact packets; add maybeReportAdvert() |

Open Items / Not In Scope

  • Admin view of meshcore nodes: currently sysadmins can soft-delete any row via the existing delete endpoint; no dedicated admin UI planned for this phase.
  • Hard expiry cleanup: rows are hidden by the 2-day query filter but not deleted. A future maintenance script or cron job could prune them.
  • Signal quality (SNR): V3 packets carry SNR. Not captured in this phase; could be added as a `snr` column later.
  • Federation: the `bbs_name` column already supports future inter-BBS federation; no federation work in this phase.