somewhat guest login
This commit is contained in:
2902
zefie_wtvp_minisrv/tools/client_sim.js
Normal file
2902
zefie_wtvp_minisrv/tools/client_sim.js
Normal file
File diff suppressed because it is too large
Load Diff
94
zefie_wtvp_minisrv/tools/diskmap_gen.js
Normal file
94
zefie_wtvp_minisrv/tools/diskmap_gen.js
Normal file
@@ -0,0 +1,94 @@
|
||||
const process = require('process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const classPath = path.resolve(__dirname + path.sep + "includes" + path.sep + "classes" + path.sep) + path.sep;
|
||||
const { WTVShared, clientShowAlert } = require(classPath + "/WTVShared.js");
|
||||
|
||||
const wtvshared = new WTVShared(); // creates minisrv_config
|
||||
const minisrv_config = wtvshared.getMiniSrvConfig(); // snatches minisrv_config
|
||||
|
||||
// primitive recursive diskmap generator, usage:
|
||||
// node diskmap_gen.js path_in_servicevault diskmap_name wtvdest [service_name]
|
||||
// service_name defaults to wtv-disk
|
||||
// will create a primitive diskmap you can then edit it as you need
|
||||
// example: node diskmap_gen.js content/Demo/ Demo.json DealerDemo file://Disk/Demo/
|
||||
|
||||
if (process.argv.length < 6) {
|
||||
console.error("Usage:", process.argv[0], process.argv[1], "path_in_service_vault", "diskmap_name", "wtv_file_dest", "groupname", "[service_name]");
|
||||
console.error("Example:", process.argv[0], process.argv[1], "content/Demo/ Demo.json DealerDemo file://Disk/Demo/");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const service_vault_subdir = process.argv[2];
|
||||
const out_file = process.argv[3];
|
||||
const group_name = process.argv[4];
|
||||
let client_dest = process.argv[5];
|
||||
let service_name;
|
||||
if (process.argv.length >= 7) service_name = process.argv[6];
|
||||
else service_name = "wtv-disk";
|
||||
|
||||
// find which service_vault the files are in
|
||||
// nothing fancy, won't support generating a list across multiple vaults
|
||||
// so be sure to choose ONE vault to keep all the files in before scanning
|
||||
// Can be any vault, and after the scan you could technically move files across vaults
|
||||
// so long as they are on the same service still
|
||||
|
||||
let service_vault = null;
|
||||
let service_vault_dir = null;
|
||||
if (minisrv_config.config.ServiceVaults) {
|
||||
Object.keys(minisrv_config.config.ServiceVaults).forEach(function (k) {
|
||||
if (service_vault_dir) return;
|
||||
const test = wtvshared.makeSafePath(wtvshared.returnAbsolutePath(minisrv_config.config.ServiceVaults[k]), service_name + path.sep + service_vault_subdir);
|
||||
console.log(" * Looking for", test);
|
||||
if (fs.existsSync(test)) {
|
||||
console.log(" * Found", test);
|
||||
service_vault = wtvshared.makeSafePath(wtvshared.returnAbsolutePath(minisrv_config.config.ServiceVaults[k]), service_name);
|
||||
service_vault_dir = test;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!service_vault) {
|
||||
console.error("Could not find", service_vault_subdir, "in any configured Service Vaults!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const recursiveDirList = function (dirPath, arrayOfFiles = null) {
|
||||
const files = fs.readdirSync(dirPath)
|
||||
|
||||
arrayOfFiles = arrayOfFiles || []
|
||||
|
||||
files.forEach(function (file) {
|
||||
if (fs.statSync(dirPath + "/" + file).isDirectory()) {
|
||||
arrayOfFiles = recursiveDirList(dirPath + "/" + file, arrayOfFiles)
|
||||
} else {
|
||||
arrayOfFiles.push(path.join(dirPath, "/", file))
|
||||
}
|
||||
})
|
||||
return arrayOfFiles
|
||||
}
|
||||
|
||||
const fileList = recursiveDirList(service_vault_dir);
|
||||
|
||||
if (fileList.length > 0) {
|
||||
const diskmap = {};
|
||||
diskmap[group_name] = {};
|
||||
if (client_dest.slice(client_dest.length - 1, 1) !== '/') client_dest += '/';
|
||||
diskmap[group_name].base = client_dest;
|
||||
diskmap[group_name].location = service_vault_subdir;
|
||||
diskmap[group_name].files = [];
|
||||
|
||||
|
||||
fileList.forEach(function (v) {
|
||||
diskmap[group_name].files.push({ "file": v.replace(service_vault_dir, client_dest).replace(new RegExp('\\' + path.sep, 'g'), '/') });
|
||||
});
|
||||
|
||||
// diskmap[group_name].files = diskmap_files;
|
||||
fs.writeFileSync(out_file,JSON.stringify(diskmap, null, "\t"));
|
||||
} else {
|
||||
throw ("No files found in", service_vault);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
172
zefie_wtvp_minisrv/tools/generate_msn_san_cert.js
Normal file
172
zefie_wtvp_minisrv/tools/generate_msn_san_cert.js
Normal file
@@ -0,0 +1,172 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const forge = require('node-forge');
|
||||
|
||||
const workspaceRoot = __dirname;
|
||||
const httpsDir = path.join(workspaceRoot, '..', 'includes', 'ServiceDeps', 'https');
|
||||
const msnDir = path.join(workspaceRoot, '..', 'includes', 'ServiceDeps', 'msntv2');
|
||||
const domainsFile = path.join(msnDir, 'msn_domains.txt');
|
||||
|
||||
const defaultCaCertPath = path.join(msnDir, 'msntv2.crt');
|
||||
const defaultCaKeyPath = path.join(msnDir, 'msntv2.key');
|
||||
const defaultOutCertPath = path.join(msnDir, 'msn_domains.crt');
|
||||
const defaultOutKeyPath = path.join(msnDir, 'msn_domains.key');
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = {};
|
||||
for (let i = 2; i < argv.length; i++) {
|
||||
const part = argv[i];
|
||||
if (!part.startsWith('--')) continue;
|
||||
const key = part.slice(2);
|
||||
const next = argv[i + 1];
|
||||
if (!next || next.startsWith('--')) {
|
||||
out[key] = true;
|
||||
continue;
|
||||
}
|
||||
out[key] = next;
|
||||
i += 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractDomainsFromRedirectMap(text) {
|
||||
const found = [];
|
||||
const seen = new Set();
|
||||
const re = /"([A-Za-z0-9.-]+\.)"\s*:\s*self\.redirect_ip/g;
|
||||
let match;
|
||||
while ((match = re.exec(text))) {
|
||||
const clean = match[1].replace(/\.$/, '').toLowerCase();
|
||||
if (!seen.has(clean)) {
|
||||
seen.add(clean);
|
||||
found.push(clean);
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function loadDomains(args) {
|
||||
if (args['from-map-file']) {
|
||||
const mapText = fs.readFileSync(path.resolve(workspaceRoot, args['from-map-file']), 'utf8');
|
||||
const domains = extractDomainsFromRedirectMap(mapText);
|
||||
if (!domains.length) {
|
||||
throw new Error('No domains were extracted from --from-map-file.');
|
||||
}
|
||||
return domains;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(domainsFile)) {
|
||||
throw new Error('Domain file not found: ' + domainsFile);
|
||||
}
|
||||
|
||||
const domains = fs.readFileSync(domainsFile, 'utf8')
|
||||
.split(/\r?\n/)
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter((s) => s && !s.startsWith('#'));
|
||||
|
||||
return Array.from(new Set(domains));
|
||||
}
|
||||
|
||||
function loadPemOrThrow(filePath, label) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(label + ' file missing: ' + filePath);
|
||||
}
|
||||
return fs.readFileSync(filePath, 'utf8');
|
||||
}
|
||||
|
||||
function ensureDirFor(filePath) {
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function signAlgorithm(sigName) {
|
||||
const lower = String(sigName || 'sha1').toLowerCase();
|
||||
if (lower === 'sha256') return forge.md.sha256.create();
|
||||
if (lower === 'sha384') return forge.md.sha384.create();
|
||||
if (lower === 'sha512') return forge.md.sha512.create();
|
||||
return forge.md.sha1.create();
|
||||
}
|
||||
|
||||
function generateCert({ domains, caCertPem, caKeyPem, outCertPath, outKeyPath, years, sig }) {
|
||||
const caCert = forge.pki.certificateFromPem(caCertPem);
|
||||
const caKey = forge.pki.privateKeyFromPem(caKeyPem);
|
||||
|
||||
const keys = forge.pki.rsa.generateKeyPair(2048);
|
||||
const cert = forge.pki.createCertificate();
|
||||
cert.publicKey = keys.publicKey;
|
||||
cert.serialNumber = forge.util.bytesToHex(forge.random.getBytesSync(16));
|
||||
|
||||
const now = new Date();
|
||||
cert.validity.notBefore = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
cert.validity.notAfter = new Date(now.getTime() + years * 365 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const cn = domains[0] || 'headwaiter.trusted.msntv.msn.com';
|
||||
cert.setSubject([
|
||||
{ name: 'commonName', value: cn },
|
||||
{ name: 'organizationName', value: 'Zefie Networks' },
|
||||
{ name: 'countryName', value: 'US' }
|
||||
]);
|
||||
cert.setIssuer(caCert.subject.attributes);
|
||||
|
||||
cert.setExtensions([
|
||||
{ name: 'basicConstraints', cA: false },
|
||||
{ name: 'keyUsage', digitalSignature: true, keyEncipherment: true },
|
||||
{ name: 'extKeyUsage', serverAuth: true },
|
||||
{
|
||||
name: 'subjectAltName',
|
||||
altNames: domains.map((d) => ({ type: 2, value: d }))
|
||||
}
|
||||
]);
|
||||
|
||||
cert.sign(caKey, signAlgorithm(sig));
|
||||
|
||||
ensureDirFor(outCertPath);
|
||||
ensureDirFor(outKeyPath);
|
||||
fs.writeFileSync(outKeyPath, forge.pki.privateKeyToPem(keys.privateKey), 'utf8');
|
||||
fs.writeFileSync(outCertPath, forge.pki.certificateToPem(cert), 'utf8');
|
||||
|
||||
return { cn, count: domains.length };
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
|
||||
const caCertPath = path.resolve(workspaceRoot, args['ca-cert'] || defaultCaCertPath);
|
||||
const caKeyPath = path.resolve(workspaceRoot, args['ca-key'] || defaultCaKeyPath);
|
||||
const outCertPath = path.resolve(workspaceRoot, args['out-cert'] || defaultOutCertPath);
|
||||
const outKeyPath = path.resolve(workspaceRoot, args['out-key'] || defaultOutKeyPath);
|
||||
const years = Number(args.years || 15);
|
||||
const sig = String(args.sig || 'sha1');
|
||||
|
||||
const domains = loadDomains(args);
|
||||
const caCertPem = loadPemOrThrow(caCertPath, 'CA cert');
|
||||
const caKeyPem = loadPemOrThrow(caKeyPath, 'CA key');
|
||||
|
||||
const result = generateCert({
|
||||
domains,
|
||||
caCertPem,
|
||||
caKeyPem,
|
||||
outCertPath,
|
||||
outKeyPath,
|
||||
years,
|
||||
sig
|
||||
});
|
||||
|
||||
console.log('[msn-san-cert] generated cert:', outCertPath);
|
||||
console.log('[msn-san-cert] generated key :', outKeyPath);
|
||||
console.log('[msn-san-cert] domains :', result.count);
|
||||
console.log('[msn-san-cert] common name :', result.cn);
|
||||
console.log('[msn-san-cert] signature alg :', sig);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
main();
|
||||
} catch (err) {
|
||||
console.error('[msn-san-cert] error:', err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
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
|
||||
};
|
||||
2555
zefie_wtvp_minisrv/tools/unroll_pcap.js
Normal file
2555
zefie_wtvp_minisrv/tools/unroll_pcap.js
Normal file
File diff suppressed because it is too large
Load Diff
212
zefie_wtvp_minisrv/tools/wtv_img_converter.js
Normal file
212
zefie_wtvp_minisrv/tools/wtv_img_converter.js
Normal file
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* wtV_img_converter.js - WebTV PNG/GIF conversion CLI
|
||||
*
|
||||
* Usage:
|
||||
* node wtV_img_converter.js <command> [options] <input> [output]
|
||||
*
|
||||
* Commands:
|
||||
* convert Convert a PNG to the best WebTV format (auto: JPEG or Artemis GIF)
|
||||
* encode Convert a PNG with alpha to an Artemis ALP/ALF GIF
|
||||
* decode Convert a WebTV Artemis ALP/ALF GIF back to a PNG
|
||||
* detect Report whether a GIF contains an Artemis ALP/ALF block
|
||||
*
|
||||
* Options:
|
||||
* --type <ALP|ALF> Artemis variant to use for encoding (default: ALP)
|
||||
* --colors <n> Palette size for full-color quantization (default: 256)
|
||||
* --quality <n> JPEG quality when output is JPEG (default: 85)
|
||||
* --output, -o <file> Output file path (alternative to positional argument)
|
||||
* --help, -h Show this help
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const WTVImage = require('../includes/classes/WTVImage');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Argument parser
|
||||
// ---------------------------------------------------------------------------
|
||||
function parseArgs(argv) {
|
||||
const args = { options: {}, positional: [] };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === '--help' || a === '-h') {
|
||||
args.options.help = true;
|
||||
} else if (a === '--type') {
|
||||
args.options.type = argv[++i];
|
||||
} else if (a === '--colors') {
|
||||
args.options.colors = parseInt(argv[++i], 10);
|
||||
} else if (a === '--quality') {
|
||||
args.options.quality = parseInt(argv[++i], 10);
|
||||
} else if (a === '--max-width') {
|
||||
args.options.maxWidth = parseInt(argv[++i], 10);
|
||||
} else if (a === '--max-height') {
|
||||
args.options.maxHeight = parseInt(argv[++i], 10);
|
||||
} else if (a === '--output' || a === '-o') {
|
||||
args.options.output = argv[++i];
|
||||
} else if (a.startsWith('--')) {
|
||||
console.error(`Unknown option: ${a}`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
args.positional.push(a);
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
WebTV Image Converter
|
||||
=======================
|
||||
Usage: node wtv_img_converter.js <command> [options] <input> [output]
|
||||
|
||||
Commands:
|
||||
convert Convert an image to the best WebTV format
|
||||
- No alpha → JPEG
|
||||
- Palette PNG → Artemis GIF (palette 1:1, no requantization)
|
||||
- Full-color RGBA → Artemis GIF (quantized)
|
||||
|
||||
encode Convert a PNG with alpha to an Artemis ALP or ALF GIF
|
||||
(throws if the PNG has no alpha channel)
|
||||
|
||||
decode Convert a WebTV Artemis ALP/ALF GIF back to a standard RGBA PNG
|
||||
|
||||
detect Report whether a file is an Artemis ALP/ALF GIF (no output file needed)
|
||||
|
||||
Options:
|
||||
--type <ALP|ALF> Artemis variant for encoding/convert (default: ALF)
|
||||
--colors <n> Palette size for full-color quantization (default: 256)
|
||||
--quality <n> JPEG quality when output is JPEG (default: 85)
|
||||
--max-width <n> Maximum width to scale input before encoding
|
||||
--max-height <n> Maximum height to scale input before encoding
|
||||
--output, -o <file> Output file path
|
||||
--help, -h Show this help
|
||||
|
||||
Examples:
|
||||
node wtV_img_converter.js convert logo.png
|
||||
node wtV_img_converter.js convert logo.png logo_wtv.gif --type ALF --colors 128
|
||||
node wtV_img_converter.js encode icon.png icon.gif --type ALP
|
||||
node wtV_img_converter.js decode artemis.gif result.png
|
||||
node wtV_img_converter.js detect artemis.gif
|
||||
`.trim());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Output path helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
function resolveOutput(inputFile, suggestedExt, override) {
|
||||
if (override) return override;
|
||||
const base = path.join(
|
||||
path.dirname(inputFile),
|
||||
path.basename(inputFile, path.extname(inputFile))
|
||||
);
|
||||
return base + suggestedExt;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Commands
|
||||
// ---------------------------------------------------------------------------
|
||||
async function cmdConvert(inputFile, outputFile, opts) {
|
||||
const ImageBuf = fs.readFileSync(inputFile);
|
||||
const { data, mime } = await WTVImage.ImageToWebTV(ImageBuf, {
|
||||
type: opts.type || 'ALP',
|
||||
colors: opts.colors || 256,
|
||||
jpegQuality: opts.quality || 85,
|
||||
maxWidth: opts.maxWidth,
|
||||
maxHeight: opts.maxHeight
|
||||
}, {
|
||||
"quality": 80,
|
||||
"compressionLevel": 9,
|
||||
"palette": true,
|
||||
"effort": 10
|
||||
});
|
||||
|
||||
const ext = mime === 'image/gif' ? '.gif' : '.jpg';
|
||||
const dest = resolveOutput(inputFile, ext, outputFile);
|
||||
fs.writeFileSync(dest, data);
|
||||
console.log(`[convert] ${inputFile} → ${dest} (${mime}, ${data.length} bytes)`);
|
||||
}
|
||||
|
||||
async function cmdEncode(inputFile, outputFile, opts) {
|
||||
const ImageBuf = fs.readFileSync(inputFile);
|
||||
const gifBuf = await WTVImage.ImageToGIF(ImageBuf, {
|
||||
type: opts.type || 'ALF',
|
||||
colors: opts.colors || 256
|
||||
});
|
||||
|
||||
const dest = resolveOutput(inputFile, '.gif', outputFile);
|
||||
fs.writeFileSync(dest, gifBuf);
|
||||
const type = WTVImage.detect(gifBuf);
|
||||
console.log(`[encode] ${inputFile} → ${dest} (Artemis ${type}, ${gifBuf.length} bytes)`);
|
||||
}
|
||||
|
||||
async function cmdDecode(inputFile, outputFile, opts) {
|
||||
const gifBuf = fs.readFileSync(inputFile);
|
||||
const type = WTVImage.detect(gifBuf);
|
||||
if (!type) {
|
||||
console.error(`[decode] ${inputFile} does not contain an Artemis ALP/ALF block.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const ImageBuf = await WTVImage.gifToPNG(gifBuf);
|
||||
const dest = resolveOutput(inputFile, '.png', outputFile);
|
||||
fs.writeFileSync(dest, ImageBuf);
|
||||
console.log(`[decode] ${inputFile} (Artemis ${type}) → ${dest} (${ImageBuf.length} bytes)`);
|
||||
}
|
||||
|
||||
function cmdDetect(inputFile) {
|
||||
const buf = fs.readFileSync(inputFile);
|
||||
const type = WTVImage.detect(buf);
|
||||
if (type) {
|
||||
console.log(`[detect] ${inputFile} → Artemis ${type}`);
|
||||
} else {
|
||||
console.log(`[detect] ${inputFile} → Not an Artemis ALP/ALF GIF`);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
(async () => {
|
||||
const raw = process.argv.slice(2);
|
||||
const args = parseArgs(raw);
|
||||
|
||||
if (args.options.help || args.positional.length === 0) {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const command = args.positional[0];
|
||||
const inputFile = args.positional[1];
|
||||
const outputFile = args.options.output || args.positional[2] || null;
|
||||
|
||||
if (!inputFile) {
|
||||
console.error('Error: no input file specified.');
|
||||
printHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(inputFile)) {
|
||||
console.error(`Error: input file not found: ${inputFile}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
switch (command) {
|
||||
case 'convert': await cmdConvert(inputFile, outputFile, args.options); break;
|
||||
case 'encode': await cmdEncode(inputFile, outputFile, args.options); break;
|
||||
case 'decode': await cmdDecode(inputFile, outputFile, args.options); break;
|
||||
case 'detect': cmdDetect(inputFile); break;
|
||||
default:
|
||||
console.error(`Unknown command: ${command}`);
|
||||
printHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user