38 KiB
Hivemapper odc-api Deep Technical Dive
Repository: https://github.com/Hivemapper/odc-api
Branch: bee
Audit Date: 2026-03-23
Purpose: This is the open-source firmware/API running on Hivemapper Bee dashcam devices
Table of Contents
- Architecture Overview
- Configuration & Constants
- API Endpoints Reference
- Security Vulnerabilities
- Services
- SQLite Schema
- Redis Usage
- Data Flow Pipeline
- Landmark Proxy Service (Cloud Upload)
1. Architecture Overview
Application Structure
The odc-api is a Node.js/Express application that runs on Hivemapper Bee dashcam devices. It serves as the central API for:
- Device configuration and management
- GPS/GNSS data handling
- Frame kilometer (framekm) processing and upload
- ML/AI landmark detection integration
- Plugin management
- LTE/WiFi connectivity
Entry Point: src/index.ts
// Main application initialization
const app: Application = express();
const server = new HttpServer(app);
// Port configuration
PORT = 5000; // HTTP API
WS_PORT = 5001; // WebSocket
// Static file serving
app.use('/public', express.static(PUBLIC_FOLDER)); // /mnt/data
app.use('/tmp', express.static(TMP_PUBLIC_FOLDER)); // /tmp/recording
// Service advertisement via Bonjour/mDNS
bonjour.publish({ name: 'DashcamService', type: 'dashcam', port: PORT });
Service Runner Pattern
The serviceRunner is a simple interval-based task scheduler defined in src/services/index.ts:
class ServiceRunner {
services: IService[] = [];
add(service: IService) {
this.services.push(service);
}
run() {
this.services.map((service: IService) => {
if (service.interval || service.delay) {
const interval = setInterval(() => {
service.execute();
if (service.delay) clearInterval(interval);
}, service.interval || service.delay);
if (!service.delay) service.execute(); // Run immediately if not delayed
} else {
service.execute(); // One-shot execution
}
});
}
}
IService Interface:
interface IService {
execute: () => Promise<void>;
interval?: number; // Recurring interval in ms
delay?: number; // One-time delay in ms
}
Registered Services (from src/index.ts)
| Service | Purpose |
|---|---|
HeartBeatService |
Device health monitoring |
HeartBeatTelemetryService |
Telemetry reporting |
DeviceInfoService |
Device identification |
IntegrityCheckService |
Data integrity validation |
InitIMUCalibrationService |
IMU sensor calibration |
InitCronService |
Scheduled task initialization |
LoadPrivacyService |
Privacy zone loading |
GetFrameKmsService |
Frame kilometer retrieval/upload |
PluginService |
Plugin management |
LandmarkProxyService |
Uploads landmarks to Hivemapper cloud |
NetworkLogService |
Network activity logging |
LTEFirmwareUpdateService |
LTE modem firmware updates |
CleanupFramekmService |
Old framekm cleanup |
PluginUpdateService |
Plugin auto-updates |
WifiClientService |
WiFi client management |
2. Configuration & Constants
Main Configuration: src/config/hdc.ts
// Network
export const PORT = 5000;
export const WS_PORT = 5001;
// File System Paths
export const PUBLIC_FOLDER = '/mnt/data';
export const TMP_PUBLIC_FOLDER = '/tmp/recording';
export const FRAMES_ROOT_FOLDER = '/tmp/recording/pic';
export const FRAMEKM_ROOT_FOLDER = '/mnt/data/framekm';
export const FRAMEKM_SHORT_ROOT_FOLDER = '/mnt/data/framekm_short';
export const FRAMEKM_CUSTOM_ROOT_FOLDER = '/mnt/data/framekm_custom';
export const UNPROCESSED_FRAMEKM_ROOT_FOLDER = '/mnt/data/unprocessed_framekm';
export const CACHED_OBSERVATIONS_FOLDER = '/mnt/data/cached_observations';
export const RAW_DATA_ROOT_FOLDER = '/mnt/data/raw';
export const DB_PATH = '/mnt/data/data-logger.v1.4.5.db';
export const GPS_ROOT_FOLDER = '/mnt/data/gps';
export const CONFIG_PATH = '/opt/dashcam/bin/config.json';
export const METADATA_ROOT_FOLDER = '/mnt/data/metadata';
export const LANDMARKS_METADATA_ROOT_FOLDER = '/mnt/data/landmarks';
export const ML_METADATA_ROOT_FOLDER = '/mnt/data/ml_metadata';
export const ML_ROOT_FOLDER = '/mnt/data/models';
export const PRIVACY_ZONES_CONFIG = '/mnt/data/ppz.json';
// External Commands
export const CMD = {
RESTART_CAMERA: 'systemctl restart camera-bridge',
START_CAMERA: 'systemctl start camera-bridge',
STOP_CAMERA: 'systemctl stop camera-bridge',
READ_DEVICE_INFO: '/opt/dashcam/bin/eeprom_access.py -r -f /tmp/dump.bin -o 0 -ba 0 -l 30',
DEVICE_SSID: 'cat /sys/firmware/devicetree/base/serial-number',
DELETE_FIRMWARE_FILES: 'rm -rf /data/lte-firmware/*.raucb && rm -rf /data/*.raucb',
};
API Utilities: src/util/api.ts
// Hivemapper API Base URL (via LTE proxy)
export const BASE_URL = 'https://hivemapper.com';
export const PROXY_URL = 'https://gateway.beemaps.com/';
// API call with auth cookie injection
async function callHmApi(url: string, options?: RequestInit): Promise<Response>;
async function callHmApiWithGateway(url: string, options?: RequestInit): Promise<Response>;
3. API Endpoints Reference
Route Mounting: src/routes/index.ts
router.use('/api/1', router);
router.use('/recordings', recordingsRouter);
router.use('/gps', gpsRouter);
router.use('/lora', loraRouter);
router.use('/upload', uploadRouter);
router.use('/ota', otaRouter);
router.use('/acl', aclRouter);
router.use('/config', configRouter);
router.use('/kpi', kpiRouter);
router.use('/framekm', framekmRouter);
router.use('/ml', mlRouter);
router.use('/metadata', metadataRouter);
router.use('/util', utilRouter);
router.use('/privacy', privacyRouter);
router.use('/led', ledRouter);
router.use('/logging', loggingRouter);
router.use('/db', dbRouter);
router.use('/instrumentation', instrumentationRouter);
router.use('/network', networkRouter);
router.use('/preview', previewRouter);
router.use('/firmware', firmwareRouter);
router.use('/plugin', pluginRouter);
router.use('/lane_counts', laneCountsRouter);
router.use('/embeddings', embeddingsRouter);
router.use('/aiEvents', aiEventsRouter);
router.use('/gnssConcise', gnssConciseRouter);
router.use('/zoo', modelZooRouter);
router.use('/zoo/2', modelZooV2Router);
router.use('/landmarks', landmarksRouter);
router.use('/position', positionRouter);
router.use('/time', timeRouter);
router.use('/beekeeper', beekeeperRouter);
router.use('/bursts', burstsRouter);
router.use('/file', fileRouter);
router.use('/lte', lteRouter);
router.use('/cache', cacheRouter);
router.use('/wifiClient', wifiClientRouter);
router.use('/gateway', gatewayRouter);
Core Endpoints (from src/routes/index.ts)
| Method | Path | Auth | Description | Security Notes |
|---|---|---|---|---|
GET |
/init |
None | Initial device configuration | - |
GET |
/info |
None | Device info (serial, firmware, etc.) | Leaks device identifiers |
GET |
/ping |
None | Health check, returns device state | - |
GET |
/locktime |
None | GNSS lock time info | - |
POST |
/cron |
None | Schedule cron jobs | ⚠️ Can schedule arbitrary tasks |
GET |
/full_reset |
None | DELETES ALL DATA | 🚨 CRITICAL: No auth, mass deletion |
GET |
/cleanup_cache |
None | Deletes cache directories | ⚠️ No auth, data deletion |
GET |
/log |
None | Read webserver log (last 2MB) | Potential info disclosure |
DELETE |
/log |
None | Clear webserver log | ⚠️ No auth |
POST |
/cmd |
None | EXECUTE ARBITRARY SHELL COMMANDS | 🚨 CRITICAL: Unauthenticated RCE |
POST |
/cmd/sync |
None | Execute shell commands synchronously | 🚨 CRITICAL: Unauthenticated RCE |
GET |
/health |
None | Health check | - |
GET |
/sensorquery |
None | Query GNSS/IMU/magnetometer data | - |
GET |
/auth |
Cookie | Authenticate with Hivemapper via cookie | - |
GET |
/plugins |
None | List installed plugins | - |
POST |
/saveUserConfig |
None | Save user configuration | - |
POST |
/lte-apn-config |
None | Configure LTE APN settings | ⚠️ Network config without auth |
Landmarks Endpoints (src/routes/landmarks.ts)
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/landmarks/ |
None | List landmarks with optional time filtering |
GET |
/landmarks/count |
None | Count landmarks (internal) |
GET |
/landmarks/last/:n |
None | Get last N landmarks |
GET |
/landmarks/id/:id |
None | Get landmarks starting from ID |
GET |
/landmarks/latest |
None | Get most recent landmark |
GET |
/landmarks/images/:id |
None | Get image file paths for landmark |
GET |
/landmarks/boundingBox/:id |
None | Get bounding box for landmark |
PUT |
/landmarks/upload |
None | Upload landmark image to external URL |
GET |
/landmarks/positionContext/:id |
None | Get GPS context around landmark |
DELETE |
/landmarks/deleteAll |
DEV_MODE | Delete all landmarks (requires DEV_MODE=1) |
GET |
/landmarks/:id/chips |
None | Get observation chips for landmark |
GET |
/landmarks/:id/chips/:chip_id |
None | Get cropped chip image |
Plugin Endpoints (src/routes/plugin.ts)
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/plugin/list |
None | List available plugins |
POST |
/plugin/enable/:name |
None | Enable a plugin |
POST |
/plugin/disable/:name |
None | Disable a plugin |
GET |
/plugin/status/:name |
None | Get plugin status |
Network Endpoints (src/routes/network.ts)
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/network/status |
None | Network connectivity status |
GET |
/network/wifi/scan |
None | Scan for WiFi networks |
POST |
/network/wifi/connect |
None | Connect to WiFi network |
Gateway/Proxy Endpoints (src/routes/gateway.ts)
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/gateway/pending |
None | Get pending proxy requests |
POST |
/gateway/proxyResponse |
None | Submit proxy response |
POST |
/gateway/forward |
None | Forward HTTP request through proxy |
File Endpoints (src/routes/file.ts)
| Method | Path | Auth | Description | Security Notes |
|---|---|---|---|---|
GET |
/file/* |
None | Read arbitrary file | ⚠️ Path traversal risk |
POST |
/file/* |
None | Write arbitrary file | 🚨 Arbitrary file write |
DELETE |
/file/* |
None | Delete arbitrary file | 🚨 Arbitrary file delete |
Privacy Endpoints (src/routes/privacy.ts)
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/privacy/zones |
None | Get privacy zones |
POST |
/privacy/zones |
None | Set privacy zones |
DELETE |
/privacy/zones |
None | Clear privacy zones |
OTA/Firmware Endpoints (src/routes/ota.ts, src/routes/firmware.ts)
| Method | Path | Auth | Description | Security Notes |
|---|---|---|---|---|
POST |
/ota/upload |
None | Upload firmware file | ⚠️ No auth on firmware upload |
POST |
/firmware/update |
None | Install firmware | 🚨 Unauthenticated firmware install |
4. Security Vulnerabilities
🚨 CRITICAL: Unauthenticated Remote Code Execution
Endpoint: POST /cmd and POST /cmd/sync
router.post('/cmd', async (req, res) => {
try {
exec(req.body.cmd, { encoding: 'utf-8' }, (error, stdout, stderr) => {
if (error) {
res.json({ error: stdout || stderr });
} else {
res.json({ output: stdout });
}
});
} catch (error: unknown) {
res.json({ error });
}
});
Impact: Any network-adjacent attacker can execute arbitrary shell commands on the device with no authentication.
Exploit:
curl -X POST http://dashcam:5000/api/1/cmd \
-H "Content-Type: application/json" \
-d '{"cmd": "cat /etc/shadow"}'
🚨 CRITICAL: Unauthenticated Mass Data Deletion
Endpoint: GET /full_reset
router.get('/full_reset', async (req: Request, res: Response) => {
const cmds = [
'rm -f /data/recording/framekm/*',
'rm -f /data/recording/framekm_short/*',
'rm -f /data/recording/framekm_custom/*',
'rm -f /data/recording/metadata/*',
'rm -f /data/recording/landmarks/*',
'rm -f /data/recording/csv/*',
'rm -f /data/recording/data-logger*',
'rm -f /data/recording/odc-api*',
'rm -rf /data/redis_handler/',
'rm -rf /data/recording/redis_handler/',
'rm -f /data/recording/*.log*',
];
// ... executes all commands
});
Impact: Any network-adjacent attacker can wipe all recorded mapping data.
🚨 CRITICAL: Arbitrary File Operations
Endpoint: /file/* (GET/POST/DELETE)
The file endpoint allows reading, writing, and deleting arbitrary files on the device filesystem with no authentication.
⚠️ HIGH: Unauthenticated Firmware Installation
Endpoint: POST /firmware/update (in src/config/hdc.ts)
export const updateFirmware = async (req: Request, res: Response) => {
try {
const output = execSync('rauc install /tmp/' + req.query.filename, {
encoding: 'utf-8',
});
res.json({ output });
} catch (error: any) {
res.json({ error: error.stdout || error.stderr });
}
};
Impact: Attacker can install malicious firmware with no authentication. Combined with file upload, this allows complete device takeover.
⚠️ HIGH: Network Configuration Without Auth
Endpoints: /network/wifi/connect, /lte-apn-config
Attackers can reconfigure network settings to redirect traffic or establish persistence.
⚠️ MEDIUM: Information Disclosure
/info- Leaks device serial number, firmware version/log- Exposes application logs which may contain sensitive data/sensorquery- Exposes precise GPS coordinates
Summary: No Authentication Model
The entire API has no authentication whatsoever. All endpoints are accessible to anyone on the same network (typically the device's AP or connected WiFi).
5. Services
LandmarkProxyService (src/services/landmarkProxyService.ts)
Purpose: Uploads detected landmarks (signs, roadwork, etc.) to Hivemapper cloud.
Interval: Every 5 minutes
Cloud Endpoint: PUT /plugins/auditMultiplePlugins
export const LandmarkProxyService: IService = {
execute: async () => {
const pluginsToPush = await getAllEnabledNonBeekeeperPlugins();
if (pluginsToPush.length === 0) return;
const landmarks = await fetchLandmarksFromId(state.last_scanned_id + 1, 1000);
const filtered = filterLandmarks(landmarks);
const payload = {
pluginNames: pluginsToPush,
dataType: 'landmark',
unit: await getAnonymousID(),
firmware: dashcamInfo.api_version || 'unknown',
session: getSessionId(),
payloads: filtered, // Landmark data with GPS coords
};
await callHmApiWithGateway('/plugins/auditMultiplePlugins', {
method: 'PUT',
body: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' },
});
},
interval: 5 * 60 * 1000, // 5 minutes
};
Data Uploaded:
- Device anonymous ID
- Firmware version
- Session ID
- All detected landmarks with:
- GPS coordinates (lat/lon/alt)
- Classification (speed-sign, stop-sign, roadwork, etc.)
- Confidence scores
- Timestamps
- Camera position at detection time
GetFrameKmsService
Handles frame kilometer packaging and upload to Hivemapper for HONEY rewards.
HeartBeatService
Periodic health checks and telemetry reporting.
PluginService
Manages third-party plugins (external ML models, custom processing).
6. SQLite Schema
Database Location
/mnt/data/data-logger.v1.4.5.db
Key Tables
landmarks Table
@Entity('landmarks')
export class Landmarks {
@PrimaryGeneratedColumn({ type: 'integer', name: 'id' })
id: number | null;
@Column('integer', { name: 'ts' })
ts: number; // Timestamp
@Column('integer', { name: 'map_feature_id', nullable: true })
mapFeatureId: number | null;
@Column('integer', { name: 'framekm_id', nullable: true })
framekmId: number | null;
@Column('text', { name: 'image_name' })
imageName: string;
@Column('integer', { name: 'image_id', nullable: true })
imageId: number | null;
@Column('integer', { name: 'class_id', nullable: true })
classId: number | null;
@Column('text', { name: 'class_label', nullable: true })
classLabel: string | null;
@Column('integer', { name: 'speed_label', nullable: true })
speedLabel: number | null;
@Column('real', { name: 'speed_label_conf', nullable: true })
speedLabelConf: number | null;
@Column('real', { name: 'distance', nullable: true })
distance: number | null;
@Column('integer', { name: 'x_center', nullable: true })
xCenter: number | null;
@Column('integer', { name: 'y_center', nullable: true })
yCenter: number | null;
@Column('real', { name: 'lat', nullable: true })
lat: number | null;
@Column('real', { name: 'lon', nullable: true })
lon: number | null;
@Column('real', { name: 'alt', nullable: true })
alt: number | null;
@Column('real', { name: 'azimuth', nullable: true })
azimuth: number | null;
@Column('real', { name: 'width', nullable: true })
width: number | null;
@Column('real', { name: 'height', nullable: true })
height: number | null;
@Column('real', { name: 'pitch', nullable: true })
pitch: number | null;
@Column('real', { name: 'roll', nullable: true })
roll: number | null;
@Column('real', { name: 'yaw', nullable: true })
yaw: number | null;
@Column('real', { name: 'confidence', nullable: true })
confidence: number | null;
@Column('integer', { name: 'x1', nullable: true })
x1: number | null;
@Column('integer', { name: 'y1', nullable: true })
y1: number | null;
@Column('integer', { name: 'x2', nullable: true })
x2: number | null;
@Column('integer', { name: 'y2', nullable: true })
y2: number | null;
@Column('real', { name: 'cam_lat', nullable: true })
camLat: number | null;
@Column('real', { name: 'cam_lon', nullable: true })
camLon: number | null;
@Column('real', { name: 'cam_heading', nullable: true })
camHeading: number | null;
@Column('integer', { name: 'track_id', nullable: true })
trackId: number | null;
@Column('text', { name: 'attributes', nullable: true })
attributes: string | null; // JSON string
@Column('text', { name: 'model_id', nullable: true })
modelId: string | null;
@Column('text', { name: 'model_hash', nullable: true })
modelHash: string | null;
}
map_features Table
@Entity('map_features')
export class MapFeatures {
@PrimaryGeneratedColumn({ type: 'integer', name: 'id' })
id: number | null;
@Column('real', { name: 'lat', nullable: true })
lat: number | null;
@Column('real', { name: 'lon', nullable: true })
lon: number | null;
@Column('real', { name: 'alt', nullable: true })
alt: number | null;
@Column('real', { name: 'azimuth', nullable: true })
azimuth: number | null;
@Column('integer', { name: 'class_id', nullable: true })
classId: number | null;
@Column('text', { name: 'class_label', nullable: true })
classLabel: string | null;
@Column('real', { name: 'width', nullable: true })
width: number | null;
@Column('real', { name: 'height', nullable: true })
height: number | null;
@Column('integer', { name: 'observation_count', nullable: true })
observationCount: number | null;
@Column('real', { name: 'cam_lat', nullable: true })
camLat: number | null;
@Column('real', { name: 'cam_lon', nullable: true })
camLon: number | null;
@Column('real', { name: 'pos_confidence', nullable: true })
posConfidence: number | null;
@Column('real', { name: 'depth_confidence', nullable: true })
depthConfidence: number | null;
@Column('real', { name: 'overall_confidence', nullable: true })
overallConfidence: number | null;
@Column('text', { name: 'attributes', nullable: true })
attributes: string | null;
}
config Table
Key-value store for device configuration.
CREATE TABLE config (
key TEXT PRIMARY KEY,
value TEXT
);
plugins Table
CREATE TABLE plugins (
plugin TEXT PRIMARY KEY,
state TEXT -- 'enabled' or 'disabled'
);
landmark_proxy_state Table
Tracks cloud upload progress.
@Entity('landmark_proxy_state')
export class LandmarkProxyState {
@PrimaryGeneratedColumn({ type: 'integer', name: 'id' })
id: number;
@Column('integer', { name: 'last_scanned_id', default: 0 })
last_scanned_id: number;
}
7. Redis Usage
Connection
// src/util/redis.ts
const client = createClient(); // Defaults to localhost:6379
Key Namespaces
// LTE Status Keys
export enum RedisLTEKeys {
OPERATOR = 'LTE_INIT_OPERATOR',
APN = 'LTE_INIT_APN',
PROTOCOL = 'LTE_INIT_PROTOCOL',
PIN_STATUS = 'LTE_INIT_PIN_STATUS',
ICCID = 'LTE_INIT_ICCID',
IMEISV = 'LTE_INIT_IMEISV',
IMEI = 'LTE_INIT_IMEI',
LTE_HEALTHY = 'LTE_INIT_LTE_HEALTHY',
}
// External ML Model Integration
export enum RedisExternalModelKeys {
REDIS_KEY_READY = 'EXTERNAL_MODEL_CLASSIFIER_READY',
REDIS_KEY_INPUT_QUEUE = 'EXTERNAL_MODEL_INPUT_QUEUE',
REDIS_KEY_OUTPUT_QUEUE = 'EXTERNAL_MODEL_OUTPUT_QUEUE',
}
// Map AI Service Status
export enum RedisMapAIKeys {
READY = 'MAP_AI_READY',
}
// Frame Counters
export enum RedisFrameCountKeys {
RGB = 'FRAME_COUNT_RGB',
LEFT = 'FRAME_COUNT_LEFT',
RIGHT = 'FRAME_COUNT_RIGHT',
}
IPC Pattern
Redis is used for inter-process communication between:
odc-api(this service)depthai_gate(camera frame acquisition)map-ai(landmark detection ML)- External classifier models
The ML pipeline uses Redis queues:
- Frames pushed to
EXTERNAL_MODEL_INPUT_QUEUE - Classification results read from
EXTERNAL_MODEL_OUTPUT_QUEUE
8. Data Flow Pipeline
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Camera HW │────▶│ depthai_gate │────▶│ map-ai │
│ (RGB + Stereo) │ │ (Frame capture) │ │ (ML detection) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
│ Redis
▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ SQLite DB │◀────│ odc-api │◀────│ Detection │
│ (landmarks, │ │ (This service) │ │ Results │
│ map_features) │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
│ │ LTE / WiFi
│ ▼
│ ┌─────────────────┐
│ │ Hivemapper │
│ │ Cloud API │
│ │ │
│ │ - framekm upload│
│ │ - landmark data │
│ │ - telemetry │
│ └─────────────────┘
│
▼
┌─────────────────┐
│ Local Storage │
│ /mnt/data/ │
│ - framekm/ │
│ - landmarks/ │
│ - metadata/ │
└─────────────────┘
File System Layout
/mnt/data/
├── data-logger.v1.4.5.db # Main SQLite database
├── framekm/ # Processed frame kilometers
├── framekm_short/ # Short framekilometers
├── framekm_custom/ # Custom resolution framekilometers
├── unprocessed_framekm/ # Pending processing
├── cached_observations/ # Landmark detection image chips
├── metadata/ # Frame metadata
├── landmarks/ # Landmark metadata
├── gps/ # GPS/GNSS logs
├── imu/ # IMU sensor data
├── models/ # ML models
├── ppz.json # Privacy zones config
├── wifi.cfg # WiFi configuration
├── camera.conf # Camera configuration
└── config/ # eMMC configuration
/tmp/recording/
├── pic/ # Live camera frames
└── ...
/opt/dashcam/bin/
├── config.json # Device configuration
├── camera_config.json # Camera settings
├── ml/ # ML scripts and models
├── eeprom_access.py # EEPROM reader
├── imucalibrator # IMU calibration tool
├── acl # Access control tool
├── cleanup_framekm.sh # Cleanup script
└── bridge.sh # Camera bridge script
9. Landmark Proxy Service (Cloud Upload)
What Gets Uploaded
The LandmarkProxyService uploads detected map features (landmarks) to Hivemapper's cloud infrastructure.
Endpoint: PUT https://hivemapper.com/api/plugins/auditMultiplePlugins
Payload Structure:
{
pluginNames: string[], // Active non-beekeeper plugins
dataType: 'landmark',
unit: string, // Anonymous device ID
firmware: string, // Firmware version
session: string, // Session UUID
payloads: [ // Array of landmarks
{
id: number,
class_label: string, // 'speed-sign', 'stop-sign', etc.
class_label_confidence: number,
overall_confidence: number,
ts: number, // Unix timestamp (ms)
lat: number, // Landmark latitude
lon: number, // Landmark longitude
alt: number, // Altitude
width: number, // Estimated physical width
height: number, // Estimated physical height
pos_confidence: number,
azimuth: number,
attributes: { // Class-specific attributes
speed_label?: number,
speed_label_conf?: number,
turn_rule?: string,
// etc.
}
}
]
}
Filtered Landmark Classes
Only these classes are uploaded to cloud:
Uploaded as-is:
speed-sign(EU)turn-restriction(EU)turn-rules-eu(EU)regulatory-speed-sign(US)turn-rules-sign(US)turn-restriction-sign(US)roadwork-coneroadwork-postroadwork-panelroadwork-barrelroadwork-barricadestop-sign
Transformed to generic-sign:
blue-polygon-sign(EU)blue-circle-sign(EU)general-yellow-sign(EU)warning-sign(EU)highway-sign(US)one-way-sign(US)vehicle-flow-sign(US)advisory-speed-sign(US)yield-signdo-not-enter-signother-prohibitory-sign
Confidence Threshold
export const OVERALL_CONFIDENCE_THRESHOLD = 0.6;
Only landmarks with overall_confidence >= 0.6 are uploaded.
Upload Interval
Every 5 minutes, up to 1000 landmarks per batch.
Appendix: Key Files Reference
| File | Purpose |
|---|---|
src/index.ts |
Application entry point |
src/routes/index.ts |
Route mounting and core endpoints |
src/routes/landmarks.ts |
Landmark API endpoints |
src/routes/plugin.ts |
Plugin management endpoints |
src/routes/gateway.ts |
HTTP proxy endpoints |
src/services/index.ts |
ServiceRunner definition |
src/services/landmarkProxyService.ts |
Cloud upload service |
src/services/getFrameKmsService.ts |
Frame kilometer upload |
src/services/pluginService.ts |
Plugin lifecycle |
src/sqlite/index.ts |
Database initialization |
src/sqlite/landmarks.ts |
Landmark queries |
src/sqlite/config.ts |
Config storage |
src/sqlite/plugins.ts |
Plugin state storage |
src/entities/entities/Landmarks.ts |
Landmark entity/schema |
src/entities/entities/MapFeatures.ts |
Map feature entity/schema |
src/config/hdc.ts |
Device constants |
src/util/redis.ts |
Redis client |
src/util/api.ts |
Hivemapper API calls |
10. Improvement Analysis (ADAMaps Integration)
10a. odc-api Bloat Audit
We run a liberated Bee cut off from Hivemapper's cloud. The following services are dead weight:
| Service | Kill? | Reason |
|---|---|---|
LandmarkProxyService |
✅ Kill | Uploads to hivemapper.com — blocked at network level but consumes CPU every 5min |
HeartBeatTelemetryService |
✅ Kill | Telemetry to Hivemapper cloud |
GetFrameKmsService |
✅ Kill | FrameKm upload to Hivemapper for HONEY rewards — we don't want this |
LTEFirmwareUpdateService |
✅ Kill | LTE modem firmware — we don't use LTE |
LTEInstrumentationUploadService |
✅ Kill | LTE telemetry |
LogLteUsageService |
✅ Kill | LTE tracking |
LteGnssAuthService |
✅ Kill | Hivemapper GNSS auth via LTE |
BkGnssAuthService |
✅ Kill | Beekeeper GNSS auth |
NetworkLogService |
✅ Kill | Network logging to Hivemapper |
PluginService |
⚠️ Careful | Manages plugins — kill if no plugins enabled |
PluginUpdateService |
✅ Kill | Auto-updates from Hivemapper |
PluginStatsUploadService |
✅ Kill | Stats upload to cloud |
CheckForLogRequestsService |
✅ Kill | Remote log requests from Hivemapper |
CleanupGatewayService |
✅ Kill | Gateway proxy cleanup |
HeartBeatService |
⚠️ Keep | Local health state used by other services |
InitialiseConfigService |
✅ Keep | Config loading |
DeviceInfoService |
✅ Keep | Device ID needed for our forwarder |
InitIMUCalibrationService |
✅ Keep | IMU calibration |
LoadPrivacyService |
✅ Keep | Privacy zones — relevant if we add fuzzing |
GetUserConfigService |
✅ Keep | User config |
UsbStateCheckService |
✅ Keep | USB state management |
LandmarkProxyService |
✅ REPLACE | Run our own adacam-forwarder instead |
WifiClientService |
✅ Keep | WiFi connectivity |
Estimated savings from killing cloud services: ~30-40% CPU reduction, eliminates all outbound Hivemapper traffic.
10b. Database Architecture: Two Tables We Should Know
There are two separate tables that our forwarder should understand:
landmarks — individual detection observations (one row per detection event)
- Has:
id, ts, image_name, class_label, confidence, lat, lon, x1/y1/x2/y2, cam_lat, cam_lon, cam_heading, attributes (JSON) - This is what our forwarder currently reads from
map_features — Hivemapper's fused/deduped sign positions (one row per physical sign)
- Has:
lat, lon, class_label, observation_count, overall_confidence, pos_confidence - This is their clustered output — equivalent to our
signstable in ADAMaps - We could read from here instead for already-deduplicated signs
Recommendation: Keep reading from landmarks for raw ingestion. Use map_features as a secondary source to bootstrap ADAMaps signs table with pre-clustered positions.
10c. Columns We're Missing in Our Forwarder
Comparing what's in the landmarks schema vs what our forwarder currently captures:
| Column | Captured? | Notes |
|---|---|---|
id |
✅ | cursor key |
ts |
✅ | timestamp |
class_label |
✅ | sign type |
confidence |
✅ | detection confidence |
lat |
✅ | sign GPS lat |
lon |
✅ | sign GPS lon |
image_name |
✅ | for image upload |
x1/y1/x2/y2 |
✅ | bbox |
cam_lat |
❌ MISSING | camera position at detection — useful for triangulation |
cam_lon |
❌ MISSING | camera position at detection |
cam_heading |
❌ MISSING | vehicle heading — key for sign facing direction |
attributes |
❌ MISSING | JSON with speed_label, turn_rule etc — critical for sign text |
azimuth |
❌ MISSING | estimated sign azimuth |
width/height |
❌ MISSING | estimated physical size |
overall_confidence |
❌ MISSING | fused confidence (different from confidence) |
map_feature_id |
❌ MISSING | FK to map_features table |
model_id |
❌ MISSING | which ML model detected it |
Immediate action: Update adacam-forwarder.py to capture cam_lat, cam_lon, cam_heading, and attributes. The attributes JSON is especially important — it contains speed_label (the actual speed number from a speed sign), speed_label_conf, and turn_rule for turn restriction signs. This is the sign text we've been trying to capture manually.
10d. The attributes JSON — Key Contents
For speed signs:
{"speed_label": 25, "speed_label_conf": 0.999}
For turn restrictions:
{"turn_rule": "noLeftTurn"}
For roadwork:
{"roadwork_type": "construction"}
This means Hivemapper's model already reads speed values from signs — we just haven't been forwarding this data. Our whole Phase 2 "read the sign text" agent task might be partially automated by just consuming attributes.
10e. Image Strategy: Chip Endpoint vs Full Frame
The /landmarks/:id/chips/:chip_id endpoint uses Jimp to crop the full frame to just the bounding box.
Current approach: We upload full frames, our API stores them, we serve crops via Pillow.
Better approach: Let odc-api do the crop on-device, upload only the chip. Smaller payload, same result. Could hit GET /api/1/landmarks/:id/chips/:chip_id directly from the forwarder instead of uploading the full cached_observations file.
Tradeoff: REST call per detection vs direct SQLite + file read. At 500 detections per cycle, the REST overhead is acceptable. The bandwidth savings (~95% smaller images) are significant over cellular.
10f. Cursor Strategy
Our current forwarder uses last_detection_id and queries WHERE id > :last_id. This matches exactly what odc-api's fetchLandmarksFromId(id, limit) does internally.
Should we use the REST endpoint instead of direct SQLite?
- Pro: Cleaner, survives odc-api schema changes, already filters landmarks by class
- Pro: Gets the
filterLandmarks()transform for free (normalizes generic-sign classes) - Con: Adds HTTP overhead, potential for missed detections if odc-api crashes
- Con:
filterLandmarks()strips some classes we might want (warning-sign, yield-sign)
Recommendation: Stick with direct SQLite. Add cam_lat, cam_lon, cam_heading, attributes to the SELECT. Don't rely on their class filtering — we want all classes.
10g. Architectural Recommendation: Replace odc-api?
odc-api is 35+ route files, TypeScript, Node.js, TypeORM. It's doing a lot we don't need.
Option A: Keep odc-api, kill dead services
- Edit
/opt/odc-api/odc-api-bee.js(compiled bundle) — painful - OR compile a stripped version from source
- Saves ~14% CPU (just killing Node.js bloat)
Option B: Replace odc-api with adacam-api
- Write a lean Python or Go service that does only what we need:
- Serve
/api/1/info,/api/1/ping(Hivemapper app compatibility) - Proxy to Redis for ML status
- Serve
/landmarksendpoints - Run our forwarder logic inline
- Serve
- Eliminates Node.js entirely (~30MB RAM savings)
- Full control over what runs
Option C: Run both (current approach)
- Keep odc-api for compatibility and ML pipeline
- Our forwarder reads SQLite directly, separate process
- Cleanest for now, most stable
Recommendation: Option C short-term. Option B long-term if we want to run on multiple Bees without the dead weight. The ML pipeline (depthai_gate → map-ai → Redis → SQLite) is independent of odc-api's cloud upload logic, so odc-api is only needed for the SQLite writes and local API serving.
10h. Security: What Else to Block on Our Bee
We've already blocked /api/1/cmd at network level. Additional endpoints to firewall:
| Endpoint | Risk | Action |
|---|---|---|
POST /cmd/sync |
RCE | Block |
GET /full_reset |
Mass data deletion | Block |
GET /cleanup_cache |
Data deletion | Block |
POST /firmware/update |
Firmware install | Block |
POST /ota/upload |
Firmware upload | Block |
GET /file/* |
Arbitrary file read | Block |
POST /file/* |
Arbitrary file write | Block |
DELETE /file/* |
Arbitrary file delete | Block |
POST /network/wifi/connect |
Network reconfig | Block |
POST /lte-apn-config |
Network reconfig | Block |
GET /auth |
Cookie auth to Hivemapper | Block (stops cookie passing) |
Our current iptables hardening blocks inbound from non-AP interfaces. These need to also be blocked on the AP interface itself, since anyone connected to the Bee AP has full access to port 5000.
10i. LandmarkProxyService — Does It Still Call Home?
Yes. Even with beekeeper disabled, LandmarkProxyService checks getAllEnabledNonBeekeeperPlugins(). If any plugin is enabled, it uploads. If all plugins are disabled, it skips the upload.
Our hardening (blocking hivemapper.com in /etc/hosts) prevents the actual upload. But the service still runs every 5 minutes, fetches landmarks from SQLite, formats the payload, and makes a TCP connection attempt that fails silently.
Fix: Disable all plugins via POST /api/1/plugin/disable/:name for each active plugin. Confirm with GET /api/1/plugins — all should show enabled: false. This stops LandmarkProxyService from doing anything at all.
Document generated: 2026-03-23 | Last updated: 2026-03-23 (improvement analysis added)