Files
minisrv/zefie_wtvp_minisrv/tools/modem_proxy.js
2026-05-02 10:15:24 -04:00

362 lines
11 KiB
JavaScript

// Rockwell to USRobotics Modem Proxy for MAME Bitbanger
const net = require('net');
const { SerialPort } = require('serialport');
const { ReadlineParser } = require('@serialport/parser-readline');
// Configuration
const TCP_IP = '127.0.0.1';
const TCP_PORT = 57388;
const SERIAL_PORT = 'COM3';
const SERIAL_BAUDRATE = 115200;
let NEXT_RECV_IS_LAST_ASCII = false;
let DATA_MODE = false;
const THINGS_TO_STRIP = ["S95=36", "&Q5", "S51=31", "S220=0", "&Q5", "&K3", "&D2"];
const THINGS_TO_REPLACE = [
["M0", "M1"], // M1 = Speaker on
["S11=110", "S11=50"], // S11 = Dial speed
["S11=200", "S11=50"], // S11 = Dial speed
["18004653537", "5736666"],
["18006138199", "5736666"]
];
// Global variables
let serialPort = null;
let server = null;
let currentClient = null;
// Initialize serial port
function initSerial() {
return new Promise((resolve, reject) => {
try {
serialPort = new SerialPort({
path: SERIAL_PORT,
baudRate: SERIAL_BAUDRATE,
autoOpen: false,
// Disable buffering for immediate data flow
highWaterMark: 1,
// Set minimal timeouts
dataBits: 8,
stopBits: 1,
parity: 'none',
rtscts: false,
xon: false,
xoff: false,
xany: false
});
serialPort.open((err) => {
if (err) {
console.error('Error opening serial port:', err.message);
reject(err);
return;
}
console.log(`Serial port ${SERIAL_PORT} opened at ${SERIAL_BAUDRATE} baud`);
// Disable any internal buffering
serialPort.set({
brk: false,
cts: false,
dtr: true,
rts: true
});
// Add a small delay to ensure port is ready
setTimeout(() => {
resolve(serialPort);
}, 100);
});
} catch (error) {
console.error('Error initializing serial port:', error);
reject(error);
}
});
}
// Reset modem to command mode and hang up
function resetModemToCommandMode() {
try {
// Send escape sequence to exit data mode
serialPort.write(Buffer.from('+++', 'ascii'));
setTimeout(() => {
// Send hang up command
serialPort.write(Buffer.from('ATH\r', 'ascii'));
console.log("Sent modem reset commands: +++ and ATH");
}, 500);
} catch (error) {
console.error('Error resetting modem:', error);
}
}
// Handle data from socket to serial
function handleSocketToSerial(socket) {
let buffer = Buffer.alloc(0);
socket.on('data', (data) => {
if (!DATA_MODE) {
buffer = Buffer.concat([buffer, data]);
// Process commands only after receiving a complete command ending in '\r'
// Use Buffer.indexOf to find CR byte (0x0D) for binary safety
const crIndex = buffer.indexOf(0x0D); // '\r' = 0x0D
if (crIndex === -1) {
return; // Wait for complete command
}
// Extract command as buffer first, then convert to string only for processing
const commandBuffer = buffer.slice(0, crIndex);
let command = commandBuffer.toString('ascii').trim();
buffer = buffer.slice(crIndex + 1);
// Apply string stripping and replacement
THINGS_TO_STRIP.forEach(s => {
command = command.replace(s, "");
});
THINGS_TO_REPLACE.forEach(([find, replace]) => {
command = command.replace(find, replace);
});
const commandBytes = Buffer.from(command + '\r', 'ascii');
console.log("WEBTV COMMAND:", commandBytes.toString('ascii').trim());
if (command + '\r' === 'ATD\r') {
NEXT_RECV_IS_LAST_ASCII = true;
console.log("ATD detected - next serial response will trigger data mode");
}
try {
serialPort.write(commandBytes, (err) => {
if (err) {
console.error('Error writing command to serial port:', err);
} else {
// Force immediate transmission of commands too
serialPort.drain();
}
});
} catch (error) {
console.error('Error writing to serial port:', error);
}
} else {
// In data mode, pass through binary data unchanged with immediate write
try {
serialPort.write(data, (err) => {
if (err) {
console.error('Error writing to serial port:', err);
} else {
// Force immediate transmission
serialPort.drain((drainErr) => {
if (drainErr) {
console.error('Error draining serial port:', drainErr);
}
});
}
});
} catch (error) {
console.error('Error writing to serial port:', error);
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
if (DATA_MODE) {
console.log("Exiting data mode due to socket error");
DATA_MODE = false;
NEXT_RECV_IS_LAST_ASCII = false;
resetModemToCommandMode();
}
});
socket.on('close', () => {
console.log("Socket closed by remote");
if (DATA_MODE) {
console.log("Exiting data mode due to socket disconnect");
DATA_MODE = false;
NEXT_RECV_IS_LAST_ASCII = false;
resetModemToCommandMode();
}
currentClient = null;
});
}
// Handle data from serial to socket
function handleSerialToSocket(socket) {
const dataHandler = (data) => {
// Check if we're switching to data mode
if (!DATA_MODE && NEXT_RECV_IS_LAST_ASCII) {
DATA_MODE = true;
NEXT_RECV_IS_LAST_ASCII = false;
// Provide connection result and enable data mode
data = Buffer.from('79\r\n67\r\n19\r\n', 'ascii');
console.log("MODEM CONNECT RESULT:", data.toString('ascii'));
console.log("Data mode enabled");
} else if (!DATA_MODE) {
console.log("MODEM COMMAND RESPONSE:", data.toString('ascii'));
}
// Check for disabling data mode if unsupported or exit flags are received
// Use Buffer.equals for binary-safe comparison
const escapeSeq = Buffer.from("+++\r", 'ascii');
const exitCode = Buffer.from("3\r", 'ascii');
if (data.equals(escapeSeq) || data.equals(exitCode)) {
console.log("Data mode disabled by serial response");
DATA_MODE = false;
}
try {
if (socket && !socket.destroyed) {
socket.write(data);
} else {
console.log("[SERIAL->SOCKET] Cannot send - socket destroyed or null");
}
} catch (error) {
console.error('Send error:', error);
if (DATA_MODE) {
console.log("Exiting data mode due to send error");
DATA_MODE = false;
NEXT_RECV_IS_LAST_ASCII = false;
resetModemToCommandMode();
}
}
};
const errorHandler = (err) => {
console.error('Serial port error:', err);
if (DATA_MODE) {
console.log("Exiting data mode due to serial error");
DATA_MODE = false;
NEXT_RECV_IS_LAST_ASCII = false;
resetModemToCommandMode();
}
};
console.log("[SERIAL] Setting up data and error handlers");
serialPort.on('data', dataHandler);
serialPort.on('error', errorHandler);
// Return cleanup function
return () => {
console.log("[SERIAL] Cleaning up event handlers");
serialPort.removeListener('data', dataHandler);
serialPort.removeListener('error', errorHandler);
};
}
// Handle new client connection
function handleClient(socket) {
// Reset state for new connection
DATA_MODE = false;
NEXT_RECV_IS_LAST_ASCII = false;
console.log("Reset modem state for new connection");
// Disable TCP buffering for immediate data flow
socket.setNoDelay(true);
socket.setTimeout(0);
currentClient = socket;
handleSocketToSerial(socket);
const cleanupSerial = handleSerialToSocket(socket);
socket.on('close', () => {
// Ensure data mode is reset when client disconnects
if (DATA_MODE) {
console.log("Client disconnected, exiting data mode");
DATA_MODE = false;
NEXT_RECV_IS_LAST_ASCII = false;
resetModemToCommandMode();
}
// Clean up serial event listeners
cleanupSerial();
currentClient = null;
});
socket.on('error', (err) => {
console.error('Client socket error:', err);
cleanupSerial();
currentClient = null;
});
}
// Clean shutdown procedure
function cleanup() {
console.log("Cleaning up...");
// Reset modem if we're in data mode
if (DATA_MODE) {
console.log("Resetting modem before shutdown");
DATA_MODE = false;
NEXT_RECV_IS_LAST_ASCII = false;
resetModemToCommandMode();
}
if (currentClient) {
currentClient.destroy();
}
if (server) {
server.close();
}
if (serialPort && serialPort.isOpen) {
serialPort.close();
}
}
// Signal handlers
process.on('SIGINT', () => {
console.log('\nReceived interrupt signal, shutting down...');
cleanup();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\nReceived terminate signal, shutting down...');
cleanup();
process.exit(0);
});
// Main execution
async function main() {
try {
// Initialize serial port and wait for it to be ready
await initSerial();
// Start TCP server
server = net.createServer((socket) => {
console.log(`Connection from ${socket.remoteAddress}:${socket.remotePort}`);
handleClient(socket);
});
server.listen(TCP_PORT, TCP_IP, () => {
console.log(`Listening on ${TCP_IP}:${TCP_PORT}...`);
});
server.on('error', (err) => {
console.error('Server error:', err);
cleanup();
process.exit(1);
});
} catch (error) {
console.error('Failed to initialize:', error);
process.exit(1);
}
}
// Start the proxy
if (require.main === module) {
main();
}
module.exports = {
main,
cleanup
};