(function () {
'use strict';
var term = document.getElementById('term');
var connStatus = document.getElementById('conn-status');
var form = document.getElementById('input-bar');
var input = document.getElementById('cmd');
var menuBtn = document.getElementById('menu-btn');
var menuPanel = document.getElementById('menu-panel');
var deviceSelect = document.getElementById('device-select');
var hdrDevice = document.getElementById('hdr-device');
var hdrCallerid = document.getElementById('hdr-callerid');
var hdrCallid = document.getElementById('hdr-callid');
var hdrAx25 = document.getElementById('hdr-ax25');
var hdrConnected = document.getElementById('hdr-connected');
var wsUrl = window.MAX25_WS_URL;
var defaults = window.MAX25_SESSION_DEFAULTS || {};
var ws = null;
var reconnectTimer = null;
var reconnectDelayMs = 2000;
var reconnectAttempt = 0;
var lineBuf = '';
var rxLines = [];
var RX_MAX = 200;
var waitingReply = false;
var pendingCmd = null;
var devices = [];
var sessionReady = false;
var bootstrapQueue = [];
var status = {
device: '',
callerid: '',
callid: '',
ax25_ui: true,
connected: false,
monitor: false,
stack: ''
};
function classifyLine(line) {
if (!line) {
return 'ignore';
}
if (line === 'OK') {
return 'ok';
}
if (line.indexOf('ERR ') === 0) {
return 'err';
}
if (line.indexOf('STATUS ') === 0) {
return 'status';
}
if (line.indexOf('RX ') === 0) {
return 'rx';
}
if (line.indexOf('EVENT ') === 0) {
return 'event';
}
if (line.indexOf('DEVICE ') === 0) {
return 'device';
}
return 'other';
}
function parseKvLine(payload) {
var out = {};
var parts = payload.split(' ');
var i;
for (i = 0; i < parts.length; i++) {
var eq = parts[i].indexOf('=');
if (eq > 0) {
out[parts[i].slice(0, eq)] = parts[i].slice(eq + 1);
}
}
return out;
}
function parseStatus(line) {
var kv = parseKvLine(line.slice(7));
if (kv.device !== undefined) {
status.device = kv.device;
}
if (kv.callerid !== undefined) {
status.callerid = kv.callerid;
}
if (kv.callid !== undefined) {
status.callid = kv.callid;
}
if (kv.ax25_ui !== undefined) {
status.ax25_ui = kv.ax25_ui === 'on';
}
if (kv.connected !== undefined) {
status.connected = kv.connected === 'yes';
}
if (kv.stack !== undefined) {
status.stack = kv.stack;
}
updateHeader();
}
function parseDeviceLine(line) {
var kv = parseKvLine(line.slice(7));
if (!kv.id) {
return null;
}
return {
id: kv.id,
hardware: kv.hardware || '',
serial: kv.serial || '',
stack: kv.stack || '',
enabled: kv.enabled || 'yes',
error: kv.error || '',
voice: kv.voice || ''
};
}
function validCallsign(value) {
if (!value) {
return false;
}
var m = /^([A-Z0-9]{1,6})(?:-([0-9]{1,2}))?$/i.exec(value.trim());
if (!m) {
return false;
}
if (m[2] !== undefined && parseInt(m[2], 10) > 15) {
return false;
}
return true;
}
function updateHeader() {
hdrDevice.textContent = status.device || '-';
hdrCallerid.textContent = status.callerid || '-';
hdrCallid.textContent = status.callid || '-';
hdrAx25.textContent = status.ax25_ui ? 'on' : 'off';
hdrConnected.textContent = status.connected ? 'yes' : 'no';
}
function refreshDeviceSelect() {
var html = '';
var i;
if (devices.length === 0) {
deviceSelect.innerHTML = '';
deviceSelect.disabled = true;
return;
}
for (i = 0; i < devices.length; i++) {
var d = devices[i];
var sel = d.id === status.device ? ' selected' : '';
html += '';
}
deviceSelect.innerHTML = html;
deviceSelect.disabled = !sessionReady;
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&')
.replace(//g, '>');
}
function escapeAttr(s) {
return escapeHtml(s).replace(/"/g, '"');
}
function appendRx(text) {
var lines = String(text).split('\n');
var i;
for (i = 0; i < lines.length; i++) {
if (lines[i] === '' && i === lines.length - 1) {
continue;
}
rxLines.push(lines[i]);
if (rxLines.length > RX_MAX) {
rxLines.shift();
}
}
term.textContent = rxLines.join('\n') + (rxLines.length ? '\n' : '');
term.scrollTop = term.scrollHeight;
}
function setConnStatus(s) {
connStatus.textContent = s;
}
function setInputEnabled(enabled) {
input.disabled = !enabled;
if (enabled) {
input.focus();
}
}
function sendRaw(cmd) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
return false;
}
ws.send(cmd + '\n');
return true;
}
function sendCmd(cmd, label) {
pendingCmd = label || cmd;
waitingReply = true;
return sendRaw(cmd);
}
function applyEvent(line) {
if (line === 'EVENT connected') {
status.connected = true;
updateHeader();
return true;
}
if (line === 'EVENT disconnected') {
status.connected = false;
updateHeader();
return true;
}
return false;
}
var bootstrapped = false;
function handleLine(line) {
var kind = classifyLine(line);
if (kind === 'rx') {
appendRx(line.slice(3));
return;
}
if (kind === 'event') {
if (applyEvent(line)) {
appendRx(line);
}
return;
}
if (waitingReply) {
if (kind === 'status') {
parseStatus(line);
appendRx('STATUS device=' + (status.device || '-') +
' stack=' + (status.stack || '-') +
' connected=' + (status.connected ? 'yes' : 'no') +
' callerid=' + (status.callerid || '-') +
' callid=' + (status.callid || '-'));
return;
}
if (kind === 'device') {
var dev = parseDeviceLine(line);
if (dev) {
devices.push(dev);
}
if (pendingCmd === 'GET DEVICES') {
appendRx(line);
}
return;
}
if (kind === 'ok') {
waitingReply = false;
if (pendingCmd === 'GET DEVICES') {
refreshDeviceSelect();
}
pendingCmd = null;
if (bootstrapQueue.length > 0) {
window.setTimeout(runBootstrapStep, 0);
}
return;
}
if (kind === 'err') {
waitingReply = false;
appendRx(line);
pendingCmd = null;
if (bootstrapQueue.length > 0) {
bootstrapQueue = [];
}
return;
}
return;
}
if (kind === 'status') {
parseStatus(line);
if (!bootstrapped) {
bootstrapped = true;
startBootstrap();
}
return;
}
if (kind === 'err') {
appendRx(line);
}
}
function feedText(chunk) {
lineBuf += chunk;
var idx;
while ((idx = lineBuf.indexOf('\n')) >= 0) {
var line = lineBuf.slice(0, idx);
lineBuf = lineBuf.slice(idx + 1);
if (line.endsWith('\r')) {
line = line.slice(0, -1);
}
handleLine(line);
}
}
function queueBootstrap(cmd, label) {
bootstrapQueue.push({ cmd: cmd, label: label || cmd });
}
function runBootstrapStep() {
if (!sessionReady || waitingReply || bootstrapQueue.length === 0) {
return;
}
var step = bootstrapQueue.shift();
sendCmd(step.cmd, step.label);
}
function startBootstrap() {
bootstrapQueue = [];
devices = [];
refreshDeviceSelect();
sessionReady = true;
if (defaults.callerid && defaults.callerid !== status.callerid) {
queueBootstrap('SET CALLERID ' + defaults.callerid.toUpperCase());
}
if (defaults.callid && defaults.callid !== status.callid) {
queueBootstrap('SET CALLID ' + defaults.callid.toUpperCase());
}
queueBootstrap('SET AX25_UI ' + (defaults.ax25_ui !== false ? 'on' : 'off'));
if (defaults.device) {
queueBootstrap('SET DEVICE ' + defaults.device);
}
queueBootstrap('GET DEVICES', 'GET DEVICES');
if (defaults.connect_on_start !== false) {
queueBootstrap('CONNECT');
}
queueBootstrap('GET STATUS');
sessionReady = true;
runBootstrapStep();
}
function fetchDevices() {
devices = [];
sendCmd('GET DEVICES', 'GET DEVICES');
}
function setDevice(id) {
if (!id) {
return;
}
var known = false;
var i;
for (i = 0; i < devices.length; i++) {
if (devices[i].id === id) {
known = true;
break;
}
}
if (devices.length === 0) {
appendRx('ERR device list empty — run /devices first');
return;
}
if (!known) {
appendRx('ERR unknown device (not in max25d): ' + id);
return;
}
sendCmd('SET DEVICE ' + id);
window.setTimeout(function () {
sendCmd('GET STATUS');
}, 50);
}
function slashHelp() {
appendRx(
'-- MAX25 browser commands (F10 menu equivalent) --\n' +
'/callerid [id] /callid [id] /status\n' +
'/send /monitor [on|off|toggle]\n' +
'/connect /disconnect\n' +
'/device /devices\n' +
'/ax25_ui on|off /help\n' +
'Plain text (no /) is sent as SEND (like max25-terminal Enter).'
);
}
function handleSlash(line) {
var parts = line.slice(1).trim().split(/\s+/);
var cmd = (parts[0] || '').toLowerCase();
var arg = parts.slice(1).join(' ');
menuPanel.classList.remove('open');
menuBtn.setAttribute('aria-expanded', 'false');
if (cmd === 'help' || cmd === 'menu') {
slashHelp();
return;
}
if (cmd === 'callerid' || cmd === 'caller') {
if (arg) {
if (!validCallsign(arg)) {
appendRx('ERR invalid CALLERID');
return;
}
sendCmd('SET CALLERID ' + arg.toUpperCase());
} else {
var v = window.prompt('CALLERID', status.callerid || defaults.callerid || '');
if (v && validCallsign(v)) {
sendCmd('SET CALLERID ' + v.toUpperCase());
}
}
return;
}
if (cmd === 'callid' || cmd === 'call') {
if (arg) {
if (!validCallsign(arg)) {
appendRx('ERR invalid CALLID');
return;
}
sendCmd('SET CALLID ' + arg.toUpperCase());
} else {
var w = window.prompt('CALLID', status.callid || defaults.callid || '');
if (w && validCallsign(w)) {
sendCmd('SET CALLID ' + w.toUpperCase());
}
}
return;
}
if (cmd === 'status') {
sendCmd('GET STATUS');
return;
}
if (cmd === 'send') {
if (!arg) {
arg = window.prompt('Send line', '');
}
if (arg) {
sendCmd('SEND ' + arg);
}
return;
}
if (cmd === 'monitor') {
var on;
if (arg === 'on' || arg === 'yes' || arg === '1') {
on = true;
} else if (arg === 'off' || arg === 'no' || arg === '0') {
on = false;
} else {
on = !status.monitor;
}
status.monitor = on;
sendCmd('MONITOR ' + (on ? 'on' : 'off'));
appendRx(on ? 'MONITOR on' : 'MONITOR off');
return;
}
if (cmd === 'connect') {
sendCmd(status.connected ? 'DISCONNECT' : 'CONNECT');
return;
}
if (cmd === 'disconnect') {
sendCmd('DISCONNECT');
return;
}
if (cmd === 'devices') {
fetchDevices();
return;
}
if (cmd === 'device') {
if (!arg) {
fetchDevices();
appendRx('Pick device from dropdown or: /device ');
return;
}
setDevice(arg);
return;
}
if (cmd === 'ax25_ui' || cmd === 'ax25-ui') {
var flag = (arg || '').toLowerCase();
if (flag !== 'on' && flag !== 'off') {
flag = status.ax25_ui ? 'off' : 'on';
}
sendCmd('SET AX25_UI ' + flag);
return;
}
if (cmd === 'quit' || cmd === 'exit') {
if (ws) {
ws.close();
}
return;
}
appendRx('ERR unknown /command — try /help');
}
function scheduleReconnect() {
if (reconnectTimer) {
return;
}
reconnectAttempt += 1;
setConnStatus('reconnecting in ' + (reconnectDelayMs / 1000) + 's');
reconnectTimer = window.setTimeout(function () {
reconnectTimer = null;
connect();
}, reconnectDelayMs);
}
function connect() {
setConnStatus('connecting');
setInputEnabled(false);
sessionReady = false;
lineBuf = '';
ws = new WebSocket(wsUrl);
ws.onopen = function () {
reconnectAttempt = 0;
setConnStatus('connected');
setInputEnabled(true);
};
ws.onmessage = function (ev) {
feedText(ev.data);
};
ws.onclose = function (ev) {
var reason = ev.reason ? ' (' + ev.reason + ')' : '';
appendRx('\n[websocket disconnected: code ' + ev.code + reason + ']\n');
setConnStatus('disconnected: ' + ev.code);
ws = null;
sessionReady = false;
setInputEnabled(false);
deviceSelect.disabled = true;
scheduleReconnect();
};
ws.onerror = function () {
setConnStatus('error');
setInputEnabled(false);
};
}
form.addEventListener('submit', function (ev) {
ev.preventDefault();
if (!ws || ws.readyState !== WebSocket.OPEN || waitingReply) {
return;
}
var text = input.value;
input.value = '';
if (!text) {
return;
}
if (text.charAt(0) === '/') {
handleSlash(text);
return;
}
sendCmd('SEND ' + text);
});
menuBtn.addEventListener('click', function (ev) {
ev.stopPropagation();
var open = menuPanel.classList.toggle('open');
menuBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
});
document.addEventListener('click', function () {
menuPanel.classList.remove('open');
menuBtn.setAttribute('aria-expanded', 'false');
});
menuPanel.addEventListener('click', function (ev) {
ev.stopPropagation();
var btn = ev.target.closest('button[data-action]');
if (!btn) {
return;
}
var action = btn.getAttribute('data-action');
if (action === 'callerid') {
handleSlash('/callerid');
} else if (action === 'callid') {
handleSlash('/callid');
} else if (action === 'status') {
handleSlash('/status');
} else if (action === 'send') {
handleSlash('/send');
} else if (action === 'monitor') {
handleSlash('/monitor toggle');
} else if (action === 'connect') {
handleSlash('/connect');
} else if (action === 'devices') {
handleSlash('/devices');
} else if (action === 'ax25_ui') {
handleSlash('/ax25_ui');
} else if (action === 'help') {
handleSlash('/help');
}
});
deviceSelect.addEventListener('change', function () {
if (deviceSelect.value) {
setDevice(deviceSelect.value);
}
});
updateHeader();
connect();
})();