somewhat guest login
This commit is contained in:
361
zefie_wtvp_minisrv/tools/modem_proxy.js
Normal file
361
zefie_wtvp_minisrv/tools/modem_proxy.js
Normal file
@@ -0,0 +1,361 @@
|
||||
// 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
|
||||
};
|
||||
Reference in New Issue
Block a user