Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
FROM node:22-alpine

ENV NODE_ENV=production

WORKDIR /app

COPY package*.json ./

RUN npm install
RUN npm install && npm cache clean --force

COPY . .

RUN chown -R node:node /app

USER node

CMD ["sh", "-c", "node index.mjs ${DEVICE:-/dev/ttyACM0}"]
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ docker-compose up
```
You will be able to inspect the logs. Once everything is working correctly, run the container with ``docker-compose up -d`` to run it in the background.

### Configuration in Docker
You can override default values with a .env file, such as:
```sh
# Set device to a wifi companion
DEVICE=192.168.10.74:5000
# Override timeout to 30 mins (if no adverts have been received within this time, the container will report unhealthy and restart automatically)
ADVERT_TIMEOUT_MS=1800000
```
## Running with Podman
To avoid running with priveledged mode, podman can be used to build and run this image.

Expand Down
15 changes: 13 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
services:
map-uploader:
build: .
# Optionally expose the port if you want to monitor the container's health externally
# ports:
# - "8080:8080"
environment:
# Set to the appropriate device for your setup, either /dev/ttyACM0 if connected by USB to a USB companion or IP:port if connected over network to a wifi companion
- DEVICE=/dev/ttyACM0
- DEVICE=${DEVICE:-/dev/ttyACM0}
# Maximum time (in milliseconds) to wait for an advert before health check fails. Default: 3600000 (1 hour)
- ADVERT_TIMEOUT_MS=${ADVERT_TIMEOUT_MS:-3600000}
restart: unless-stopped
# Only necessary for accessing USB devices, such as a USB companion
privileged: true
# Adjust according to your setup, but this is the default
devices:
- /dev/ttyACM0:/dev/ttyACM0
- /dev/ttyACM0:/dev/ttyACM0
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 1
start_period: 5s
72 changes: 62 additions & 10 deletions index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,45 @@ import {

import { KeyPair } from './supercop/index.mjs';
import crypto from 'crypto';
import http from 'http';

const device = process.argv[2] ?? '/dev/ttyACM0';
const apiURL = 'https://map.meshcore.dev/api/v1/uploader/node';
const seenAdverts = {};
let clientInfo = {};
let isHealthy = false;
const advertTimeoutMs = parseInt(process.env.ADVERT_TIMEOUT_MS ?? '300000', 10); // milliseconds
let lastAdvertTime = null;

// Wrapper for console methods that adds timestamp
const formatLog = (level, ...args) => {
const timestamp = new Date().toISOString();
return [`[${timestamp}] [${level}]`, ...args];
};

const log = (...args) => console.log(...formatLog('INFO', ...args));
const warn = (...args) => console.warn(...formatLog('WARN', ...args));
const error = (...args) => console.error(...formatLog('ERROR', ...args));

// Health check server
http.createServer((req, res) => {
if (req.url === '/health') {
if (!isHealthy) {
res.writeHead(503);
res.end('Not Ready');
} else if (lastAdvertTime && Date.now() - lastAdvertTime > advertTimeoutMs) {
const timeoutSec = Math.floor(advertTimeoutMs / 1000);
res.writeHead(503);
res.end(`No adverts received in ${timeoutSec} seconds`);
} else {
res.writeHead(200);
res.end('OK');
}
} else {
res.writeHead(404);
res.end();
}
}).listen(8080);

const signData = async (kp, data) => {
const json = JSON.stringify(data);
Expand All @@ -34,22 +68,24 @@ const processPacket = async (connection, rawPacket) => {
const node = { pubKey, name: advert.parsed.name, ts: advert.timestamp, type: advert.parsed.type.toLowerCase() };

if(!advert.isVerified()) {
console.warn('ignoring: signature verification failed', node);
warn('ignoring: signature verification failed', node);
return;
}

if(seenAdverts[pubKey]) {
if(seenAdverts[pubKey] >= advert.timestamp) {
console.warn('ignoring: possible replay attack', node);
warn('ignoring: possible replay attack', node);
return;
}
if(advert.timestamp < seenAdverts[pubKey] + 3600) {
console.warn('ignoring: timestamp too new to reupload', node)
warn('ignoring: timestamp too new to reupload', node)
return;
}
}

console.log(`uploading`, node);
lastAdvertTime = Date.now();

log(`uploading`, node);
const data = {
params: {
freq: clientInfo.radioFreq / 1000,
Expand All @@ -70,13 +106,13 @@ const processPacket = async (connection, rawPacket) => {

// console.debug('DEBUG: sent request', req);

console.log('sending', requestData)
console.log(await req.json());
log('sending', requestData)
log(await req.json());

seenAdverts[pubKey] = advert.timestamp;
}

console.log(`Connecting to ${device}...`);
log(`Connecting to ${device}...`);

let connection;
if(device.startsWith('/') || device.startsWith('COM')){
Expand All @@ -87,22 +123,38 @@ if(device.startsWith('/') || device.startsWith('COM')){
}

connection.on('connected', async () => {
console.log(`Connected.`);
log(`Connected.`);
isHealthy = true;

connection.setManualAddContacts();

clientInfo = await connection.getSelfInfo();
clientInfo.kp = KeyPair.from({ publicKey: clientInfo.publicKey, secretKey: (await connection.exportPrivateKey()).privateKey });

console.log('Map uploader waiting for adverts...');
log('Map uploader waiting for adverts...');
});

connection.on('disconnected', () => {
log('Disconnected. Exiting...');
process.exit(1);
});

connection.on('close', () => {
log('Connection closed. Exiting...');
process.exit(1);
});

connection.on('error', (err) => {
error('Connection error:', err);
process.exit(1);
});

connection.on(Constants.PushCodes.LogRxData, async (event) => {
try {
await processPacket(connection, event.raw);
}
catch(e) {
console.error(e);
error(e);
}
});

Expand Down