ble-mcp-test
Version:
Complete BLE testing stack: WebSocket bridge server, MCP observability layer, and Web Bluetooth API mock. Test real BLE devices in Playwright/E2E tests without browser support.
148 lines (147 loc) • 5.37 kB
JavaScript
import { formatHex } from './utils.js';
import { Logger } from './logger.js';
export class LogBuffer {
buffer = [];
maxSize;
logger;
sequenceCounter = 0;
clientPositions = new Map(); // client_id -> last_seen_id
subscribers = [];
constructor(maxSize) {
// Default 10k, configurable via env var or constructor
this.maxSize = maxSize || parseInt(process.env.BLE_MCP_LOG_BUFFER_SIZE || '10000', 10);
this.logger = new Logger('LogBuffer');
// Validate reasonable bounds (100 to 1M entries)
if (this.maxSize < 100)
this.maxSize = 100;
if (this.maxSize > 1000000)
this.maxSize = 1000000;
this.logger.debug(`Initialized with max size: ${this.maxSize} entries`);
}
push(direction, data) {
const entry = {
id: this.sequenceCounter++,
timestamp: new Date().toISOString(),
direction,
hex: formatHex(data),
size: data.length
};
this.buffer.push(entry);
// Maintain circular buffer size
while (this.buffer.length > this.maxSize) {
this.buffer.shift();
}
// Notify subscribers
this.subscribers.forEach(callback => callback(entry));
}
// Alias for compatibility
logPacket(direction, data) {
this.push(direction, data);
}
pushSystemLog(level, message) {
const entry = {
id: this.sequenceCounter++,
timestamp: new Date().toISOString(),
direction: level, // Reuse direction field for log level
hex: message, // Store message in hex field
size: 0
};
this.buffer.push(entry);
// Maintain circular buffer size
while (this.buffer.length > this.maxSize) {
this.buffer.shift();
}
}
getLogsSince(since, limit, clientId) {
const startIdx = this.parseSince(since, clientId);
// Filter from start index
const filtered = this.buffer.slice(startIdx);
// Apply limit
const result = filtered.slice(0, limit);
// Update client position if clientId provided
if (clientId && result.length > 0) {
const lastId = result[result.length - 1].id;
this.updateClientPosition(clientId, lastId);
}
return result;
}
searchPackets(hexPattern, limit) {
// Convert hex pattern to regex-safe string (remove spaces)
const cleanPattern = hexPattern.replace(/\s+/g, '').toUpperCase();
// Create regex that matches the pattern in the hex string
const regex = new RegExp(cleanPattern, 'i');
const matches = [];
// Search from newest to oldest
for (let i = this.buffer.length - 1; i >= 0 && matches.length < limit; i--) {
const entry = this.buffer[i];
const cleanHex = entry.hex.replace(/\s+/g, '');
if (regex.test(cleanHex)) {
matches.push(entry);
}
}
// Return in chronological order
return matches.reverse();
}
getClientPosition(clientId) {
return this.clientPositions.get(clientId) || 0;
}
updateClientPosition(clientId, lastSeenId) {
this.clientPositions.set(clientId, lastSeenId);
}
parseSince(since, clientId) {
// Handle 'last' - return client's last position
if (since === 'last' && clientId) {
const lastId = this.clientPositions.get(clientId) || 0;
return this.buffer.findIndex(e => e.id > lastId);
}
// Handle duration strings: '30s', '5m', '1h'
const durationMatch = since.match(/^(\d+)([smh])$/);
if (durationMatch) {
const [, num, unit] = durationMatch;
const multipliers = { s: 1000, m: 60000, h: 3600000 };
const cutoffTime = Date.now() - (parseInt(num) * multipliers[unit]);
// Find first entry after cutoff
const idx = this.buffer.findIndex(e => new Date(e.timestamp).getTime() > cutoffTime);
return idx === -1 ? 0 : idx;
}
// Handle ISO timestamp
try {
const cutoffTime = new Date(since).getTime();
const idx = this.buffer.findIndex(e => new Date(e.timestamp).getTime() > cutoffTime);
return idx === -1 ? 0 : idx;
}
catch {
// Default to beginning if parsing fails
return 0;
}
}
// Helper method for testing and debugging
getBufferSize() {
return this.buffer.length;
}
// Helper method to get current connection state
getConnectionStats() {
let packetsTransmitted = 0;
let packetsReceived = 0;
for (const entry of this.buffer) {
if (entry.direction === 'TX') {
packetsTransmitted++;
}
else {
packetsReceived++;
}
}
return { packetsTransmitted, packetsReceived };
}
// Subscribe to new entries
subscribe(callback) {
this.subscribers.push(callback);
// Return unsubscribe function
return () => {
const index = this.subscribers.indexOf(callback);
if (index > -1) {
this.subscribers.splice(index, 1);
}
};
}
}