diff --git a/SiKRadioTools/README.md b/SiKRadioTools/README.md new file mode 100644 index 00000000..2a425f50 --- /dev/null +++ b/SiKRadioTools/README.md @@ -0,0 +1,18 @@ +# SiK Radio Tools (WebTools bundle) + +Static build of [SiK Radio Tools](https://github.com/JamesM9/SIK-Radio-Tools) for the ArduPilot WebTools hub. + +## Rebuild from upstream + +```bash +git clone https://github.com/JamesM9/SIK-Radio-Tools.git +cd SIK-Radio-Tools/sik-radio-tools +npm ci +npm run build +``` + +Copy **`index.html`** and the entire **`dist/`** directory into this folder (`WebTools/SiKRadioTools/`), replacing existing files. + +## License + +GPL-3.0 (same as [ArduPilot/WebTools](https://github.com/ArduPilot/WebTools)). diff --git a/SiKRadioTools/dist/app.css b/SiKRadioTools/dist/app.css new file mode 100644 index 00000000..533a8150 --- /dev/null +++ b/SiKRadioTools/dist/app.css @@ -0,0 +1,396 @@ +/* SiK Radio Tools - Modern dark-first UI */ + +:root { + --bg-primary: #0f1419; + --bg-secondary: #1a2332; + --bg-tertiary: #243044; + --text-primary: #e6edf3; + --text-secondary: #8b949e; + --accent: #58a6ff; + --accent-hover: #79b8ff; + --success: #3fb950; + --warning: #d29922; + --error: #f85149; + --border: #30363d; + --radius: 8px; + --font: 'Segoe UI', system-ui, -apple-system, sans-serif; +} + +[data-theme="light"] { + --bg-primary: #ffffff; + --bg-secondary: #f6f8fa; + --bg-tertiary: #eaeef2; + --text-primary: #1f2328; + --text-secondary: #656d76; + --accent: #0969da; + --accent-hover: #0550ae; + --success: #1a7f37; + --warning: #9a6700; + --error: #cf222e; + --border: #d0d7de; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: var(--font); + font-size: 14px; + line-height: 1.5; + color: var(--text-primary); + background: var(--bg-primary); + min-height: 100vh; +} + +#app { + max-width: 1200px; + margin: 0 auto; + padding: 24px; +} + +.browser-warning { + padding: 12px 16px; + margin-bottom: 20px; + border-radius: var(--radius); + background: color-mix(in srgb, var(--warning) 18%, var(--bg-secondary)); + border: 1px solid color-mix(in srgb, var(--warning) 45%, var(--border)); + color: var(--text-primary); + font-size: 13px; + line-height: 1.45; +} + +/* Header */ +.app-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 1px solid var(--border); +} + +.app-title { + font-size: 24px; + font-weight: 600; + margin: 0; +} + +/* Connection bar */ +.connection-bar { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + padding: 16px; + background: var(--bg-secondary); + border-radius: var(--radius); + margin-bottom: 24px; +} + +.connection-status { + display: flex; + align-items: center; + gap: 8px; +} + +.status-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--text-secondary); +} + +.status-dot.connected { + background: var(--success); + box-shadow: 0 0 8px var(--success); +} + +.status-dot.error { + background: var(--error); +} + +.status-dot.connecting { + background: var(--warning); + animation: pulse 1s infinite; +} + +@keyframes pulse { + 50% { opacity: 0.5; } +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 16px; + font-size: 14px; + font-weight: 500; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-tertiary); + color: var(--text-primary); + cursor: pointer; + transition: background 0.15s, border-color 0.15s; +} + +.btn:hover:not(:disabled) { + background: var(--bg-secondary); + border-color: var(--text-secondary); +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background: var(--accent); + border-color: var(--accent); + color: #fff; +} + +.btn-primary:hover:not(:disabled) { + background: var(--accent-hover); + border-color: var(--accent-hover); +} + +.btn-danger { + background: var(--error); + border-color: var(--error); + color: #fff; +} + +.btn-danger:hover:not(:disabled) { + opacity: 0.9; +} + +.btn-sm { + padding: 4px 10px; + font-size: 12px; +} + +/* Form controls */ +.form-group { + margin-bottom: 16px; +} + +.form-label { + display: block; + margin-bottom: 4px; + font-weight: 500; + color: var(--text-primary); +} + +.form-hint { + font-size: 12px; + color: var(--text-secondary); + margin-top: 4px; +} + +input[type="text"], +input[type="number"], +select, +textarea { + width: 100%; + padding: 8px 12px; + font-size: 14px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-primary); + color: var(--text-primary); +} + +input:focus, +select:focus, +textarea:focus { + outline: none; + border-color: var(--accent); +} + +/* Tabs */ +.tabs { + display: flex; + gap: 4px; + margin-bottom: 24px; + border-bottom: 1px solid var(--border); + overflow-x: auto; +} + +.tab { + padding: 10px 16px; + font-size: 14px; + font-weight: 500; + color: var(--text-secondary); + background: none; + border: none; + border-bottom: 2px solid transparent; + cursor: pointer; + white-space: nowrap; + transition: color 0.15s; +} + +.tab:hover { + color: var(--text-primary); +} + +.tab.active { + color: var(--accent); + border-bottom-color: var(--accent); +} + +/* Tab panels */ +.tab-panel { + display: none; +} + +.tab-panel.active { + display: block; +} + +/* Cards */ +.card { + background: var(--bg-secondary); + border-radius: var(--radius); + border: 1px solid var(--border); + padding: 20px; + margin-bottom: 20px; +} + +.card-title { + font-size: 16px; + font-weight: 600; + margin: 0 0 16px 0; +} + +/* Parameter grid */ +.param-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; +} + +.param-field { + display: flex; + flex-direction: column; + gap: 4px; +} + +.param-field.advanced { + opacity: 0.9; +} + +/* Terminal */ +.terminal-container { + background: #0d1117; + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px; + font-family: 'Consolas', 'Monaco', monospace; + font-size: 13px; + max-height: 400px; + overflow-y: auto; +} + +.terminal-line { + margin: 2px 0; + word-break: break-all; +} + +.terminal-line.tx { + color: #79c0ff; +} + +.terminal-line.rx { + color: #7ee787; +} + +.terminal-line.timestamp { + color: var(--text-secondary); + font-size: 11px; +} + +.terminal-input-row { + display: flex; + gap: 8px; + margin-top: 12px; +} + +.terminal-input-row input { + flex: 1; +} + +.fw-progress-row { + display: flex; + align-items: center; + gap: 12px; +} + +.fw-progress-row progress { + width: 240px; + height: 12px; +} + +/* Toast */ +.toast-container { + position: fixed; + bottom: 24px; + right: 24px; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 8px; +} + +.toast { + padding: 12px 20px; + border-radius: var(--radius); + font-size: 14px; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + animation: slideIn 0.2s ease; +} + +@keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +.toast.success { background: var(--success); color: #fff; } +.toast.error { background: var(--error); color: #fff; } +.toast.warning { background: var(--warning); color: #000; } +.toast.info { background: var(--accent); color: #fff; } + +/* Loading spinner */ +.spinner { + width: 18px; + height: 18px; + border: 2px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Diff view */ +.diff-add { + background: rgba(63, 185, 80, 0.2); + padding: 2px 4px; +} + +.diff-remove { + background: rgba(248, 81, 73, 0.2); + padding: 2px 4px; +} + +/* Responsive */ +@media (max-width: 768px) { + #app { padding: 16px; } + .connection-bar { flex-direction: column; align-items: stretch; } + .param-grid { grid-template-columns: 1fr; } +} diff --git a/SiKRadioTools/dist/app.js b/SiKRadioTools/dist/app.js new file mode 100644 index 00000000..680a1c27 --- /dev/null +++ b/SiKRadioTools/dist/app.js @@ -0,0 +1,11 @@ +/** + * SiK Radio Tools - Main application entry + */ +import { renderApp } from './ui/app.js'; +document.addEventListener('DOMContentLoaded', () => { + const root = document.getElementById('app'); + if (!root) + return; + renderApp(root); +}); +//# sourceMappingURL=app.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/app.js.map b/SiKRadioTools/dist/app.js.map new file mode 100644 index 00000000..fcf74168 --- /dev/null +++ b/SiKRadioTools/dist/app.js.map @@ -0,0 +1 @@ +{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,GAAG,EAAE;IACjD,MAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IAC5C,IAAI,CAAC,IAAI;QAAE,OAAO;IAClB,SAAS,CAAC,IAAI,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/SiKRadioTools/dist/assets/icons/icon128.png b/SiKRadioTools/dist/assets/icons/icon128.png new file mode 100644 index 00000000..5a453c0e Binary files /dev/null and b/SiKRadioTools/dist/assets/icons/icon128.png differ diff --git a/SiKRadioTools/dist/assets/icons/icon16.png b/SiKRadioTools/dist/assets/icons/icon16.png new file mode 100644 index 00000000..5a453c0e Binary files /dev/null and b/SiKRadioTools/dist/assets/icons/icon16.png differ diff --git a/SiKRadioTools/dist/assets/icons/icon48.png b/SiKRadioTools/dist/assets/icons/icon48.png new file mode 100644 index 00000000..5a453c0e Binary files /dev/null and b/SiKRadioTools/dist/assets/icons/icon48.png differ diff --git a/SiKRadioTools/dist/assets/webtools-hub-tile.png b/SiKRadioTools/dist/assets/webtools-hub-tile.png new file mode 100644 index 00000000..216d487c Binary files /dev/null and b/SiKRadioTools/dist/assets/webtools-hub-tile.png differ diff --git a/SiKRadioTools/dist/diagnostics/logger.js b/SiKRadioTools/dist/diagnostics/logger.js new file mode 100644 index 00000000..e910f6f1 --- /dev/null +++ b/SiKRadioTools/dist/diagnostics/logger.js @@ -0,0 +1,42 @@ +/** + * Device event and diagnostics logger + */ +const MAX_ENTRIES = 500; +const entries = []; +export function log(level, message, source) { + const entry = { + id: crypto.randomUUID(), + timestamp: Date.now(), + level, + message, + source, + }; + entries.push(entry); + if (entries.length > MAX_ENTRIES) { + entries.shift(); + } +} +export function logInfo(msg, source) { + log('info', msg, source); +} +export function logWarn(msg, source) { + log('warn', msg, source); +} +export function logError(msg, source) { + log('error', msg, source); +} +export function logDebug(msg, source) { + log('debug', msg, source); +} +export function getEntries() { + return [...entries]; +} +export function clearLog() { + entries.length = 0; +} +export function formatEntry(e) { + const time = new Date(e.timestamp).toISOString().slice(11, 23); + const src = e.source ? ` [${e.source}]` : ''; + return `[${time}] ${e.level.toUpperCase()}${src} ${e.message}`; +} +//# sourceMappingURL=logger.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/diagnostics/logger.js.map b/SiKRadioTools/dist/diagnostics/logger.js.map new file mode 100644 index 00000000..31df1753 --- /dev/null +++ b/SiKRadioTools/dist/diagnostics/logger.js.map @@ -0,0 +1 @@ +{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/diagnostics/logger.ts"],"names":[],"mappings":"AAAA;;GAEG;AAYH,MAAM,WAAW,GAAG,GAAG,CAAC;AACxB,MAAM,OAAO,GAAe,EAAE,CAAC;AAE/B,MAAM,UAAU,GAAG,CAAC,KAAe,EAAE,OAAe,EAAE,MAAe;IACnE,MAAM,KAAK,GAAa;QACtB,EAAE,EAAE,MAAM,CAAC,UAAU,EAAE;QACvB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;QACrB,KAAK;QACL,OAAO;QACP,MAAM;KACP,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpB,IAAI,OAAO,CAAC,MAAM,GAAG,WAAW,EAAE,CAAC;QACjC,OAAO,CAAC,KAAK,EAAE,CAAC;IAClB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,GAAW,EAAE,MAAe;IAClD,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,GAAW,EAAE,MAAe;IAClD,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAW,EAAE,MAAe;IACnD,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAW,EAAE,MAAe;IACnD,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,QAAQ;IACtB,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,CAAW;IACrC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IAC/D,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7C,OAAO,IAAI,IAAI,KAAK,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,GAAG,GAAG,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;AACjE,CAAC"} \ No newline at end of file diff --git a/SiKRadioTools/dist/params/mapper.js b/SiKRadioTools/dist/params/mapper.js new file mode 100644 index 00000000..afaacb35 --- /dev/null +++ b/SiKRadioTools/dist/params/mapper.js @@ -0,0 +1,95 @@ +/** + * Parameter mapping, validation, and config import/export + */ +import { SIK_PARAM_SCHEMA } from './schema.js'; +function coerceEnumValue(def, value) { + const opts = def.options ?? []; + if (typeof value === 'string' && /^-?\d+$/.test(value)) { + const asNum = parseInt(value, 10); + if (opts.some((o) => o.value === asNum)) { + return asNum; + } + } + return value; +} +/** Validate a value against param definition */ +export function validateParam(def, value) { + if (def.type === 'enum') { + const opts = def.options ?? []; + const normalized = coerceEnumValue(def, value); + return opts.some((o) => o.value === normalized); + } + if (def.type === 'number') { + const n = typeof value === 'number' ? value : parseFloat(String(value)); + if (isNaN(n)) + return false; + if (def.min !== undefined && n < def.min) + return false; + if (def.max !== undefined && n > def.max) + return false; + return true; + } + return true; +} +/** Coerce value to correct type for param */ +export function coerceParam(def, value) { + if (def.type === 'enum') { + return coerceEnumValue(def, value); + } + if (def.type === 'number') { + if (typeof value === 'number') + return value; + const n = parseFloat(String(value)); + return isNaN(n) ? def.default : n; + } + return String(value); +} +/** Get default values for all params */ +export function getDefaultParams() { + const out = {}; + for (const def of SIK_PARAM_SCHEMA) { + out[def.key] = def.default; + } + return out; +} +/** Filter params to only known schema keys, with validation */ +export function sanitizeParams(input) { + const out = {}; + for (const def of SIK_PARAM_SCHEMA) { + const v = input[def.key]; + if (v === undefined) + continue; + if (validateParam(def, v)) { + out[def.key] = coerceParam(def, v); + } + } + return out; +} +/** Convert ATI5 params (from radio) to our schema keys */ +export function ati5ToParams(ati5) { + const out = {}; + const ati5Upper = {}; + for (const [k, v] of Object.entries(ati5)) { + ati5Upper[k.toUpperCase()] = v; + } + for (const def of SIK_PARAM_SCHEMA) { + const v = ati5[def.key] ?? + ati5Upper[def.key] ?? + (def.register ? ati5[def.register] : undefined); + if (v !== undefined) { + out[def.key] = coerceParam(def, v); + } + } + return out; +} +/** Compute diff between current and new params */ +export function diffParams(current, next) { + const out = {}; + for (const key of Object.keys(next)) { + if (current[key] !== next[key]) { + out[key] = next[key]; + } + } + return out; +} +//# sourceMappingURL=mapper.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/params/mapper.js.map b/SiKRadioTools/dist/params/mapper.js.map new file mode 100644 index 00000000..b7cdcd2a --- /dev/null +++ b/SiKRadioTools/dist/params/mapper.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mapper.js","sourceRoot":"","sources":["../../src/params/mapper.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/SiKRadioTools/dist/params/schema.js b/SiKRadioTools/dist/params/schema.js new file mode 100644 index 00000000..f1a000a1 --- /dev/null +++ b/SiKRadioTools/dist/params/schema.js @@ -0,0 +1,167 @@ +/** + * Schema-driven parameter definitions for SiK radios + * Supports different firmware variants via schema + */ +/** Common SiK parameters - 900MHz defaults where applicable */ +export const SIK_PARAM_SCHEMA = [ + { + key: 'SERIAL_SPEED', + label: 'Serial Speed', + description: 'Baud rate for serial connection (57 = 57600)', + type: 'number', + min: 1, + max: 1152, + default: 57, + requiresReboot: true, + category: 'basic', + register: 'S1', + }, + { + key: 'AIR_SPEED', + label: 'Air Speed', + description: 'RF data rate in kbps. Lower = longer range, less bandwidth. Supported: 2,4,8,16,19,24,32,48,64,96,128,192,250', + type: 'number', + min: 2, + max: 250, + default: 64, + requiresReboot: true, + category: 'basic', + register: 'S2', + }, + { + key: 'NETID', + label: 'Network ID', + description: 'Must match on both radios (0-255)', + type: 'number', + min: 0, + max: 255, + default: 25, + requiresReboot: true, + category: 'basic', + register: 'S3', + }, + { + key: 'TXPOWER', + label: 'TX Power (dBm)', + description: 'Transmit power. Supported: 1,2,5,8,11,14,17,20', + type: 'number', + min: 1, + max: 20, + default: 20, + requiresReboot: false, + category: 'basic', + register: 'S4', + }, + { + key: 'ECC', + label: 'Error Correction', + description: 'Golay ECC. 0=off (recommended), 1=on. Some newer chips do not support ECC.', + type: 'enum', + options: [ + { value: 0, label: 'Off' }, + { value: 1, label: 'On' }, + ], + default: 0, + requiresReboot: true, + category: 'basic', + register: 'S5', + }, + { + key: 'MAVLINK', + label: 'MAVLink Mode', + description: '0=off, 1=frame+report, 2=low latency (RC_OVERRIDE priority)', + type: 'enum', + options: [ + { value: 0, label: 'Off' }, + { value: 1, label: 'MAVLINK' }, + { value: 2, label: 'Low Latency' }, + ], + default: 1, + requiresReboot: true, + category: 'basic', + register: 'S6', + }, + { + key: 'MIN_FREQ', + label: 'Min Frequency (kHz)', + description: '900MHz: 895000, 433MHz: 414000', + type: 'number', + min: 414000, + max: 935000, + default: 915000, + requiresReboot: true, + category: 'basic', + register: 'S8', + }, + { + key: 'MAX_FREQ', + label: 'Max Frequency (kHz)', + description: '900MHz: 928000 (US) or 935000, 433MHz: 454000', + type: 'number', + min: 414000, + max: 935000, + default: 928000, + requiresReboot: true, + category: 'basic', + register: 'S9', + }, + { + key: 'NUM_CHANNELS', + label: 'Number of Channels', + description: 'Frequency hopping channels (1-50)', + type: 'number', + min: 1, + max: 50, + default: 50, + requiresReboot: true, + category: 'basic', + register: 'S10', + }, + { + key: 'DUTY_CYCLE', + label: 'Duty Cycle (%)', + description: 'Max transmit time percentage. 100=normal, 0=receive only', + type: 'number', + min: 0, + max: 100, + default: 100, + requiresReboot: true, + category: 'advanced', + register: 'S11', + }, + { + key: 'LBT_RSSI', + label: 'LBT RSSI', + description: 'Listen Before Talk threshold. 0=disabled, 25+=enabled', + type: 'number', + min: 0, + max: 255, + default: 0, + requiresReboot: true, + category: 'advanced', + register: 'S12', + }, + { + key: 'MAX_WINDOW', + label: 'Max Window (ms)', + description: '33=low latency, 131=default (higher bandwidth)', + type: 'number', + min: 33, + max: 131, + default: 131, + requiresReboot: true, + category: 'advanced', + register: 'S15', + }, +]; +/** Map param key to S-register for AT commands */ +export function keyToRegister(key) { + return SIK_PARAM_SCHEMA.find((p) => p.key === key)?.register; +} +/** Map S-register to param key */ +export function registerToKey(reg) { + const r = reg.toUpperCase().replace(/^S/, ''); + const def = SIK_PARAM_SCHEMA.find((p) => p.register === `S${r}`); + return def?.key; +} +//# sourceMappingURL=schema.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/params/schema.js.map b/SiKRadioTools/dist/params/schema.js.map new file mode 100644 index 00000000..de6c5dba --- /dev/null +++ b/SiKRadioTools/dist/params/schema.js.map @@ -0,0 +1 @@ +{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/params/schema.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/SiKRadioTools/dist/persistence/profiles.js b/SiKRadioTools/dist/persistence/profiles.js new file mode 100644 index 00000000..1d28047d --- /dev/null +++ b/SiKRadioTools/dist/persistence/profiles.js @@ -0,0 +1,50 @@ +/** + * Profile management - save, load, compare + */ +import { getProfiles, saveProfiles } from './storage.js'; +import { sanitizeParams, diffParams } from '../params/mapper.js'; +export async function listProfiles() { + return getProfiles(); +} +export async function saveProfile(name, params) { + const profiles = await getProfiles(); + const profile = { + id: crypto.randomUUID(), + name, + createdAt: Date.now(), + params: sanitizeParams(params), + }; + profiles.push(profile); + await saveProfiles(profiles); + return profile; +} +export async function deleteProfile(id) { + const profiles = await getProfiles().then((p) => p.filter((x) => x.id !== id)); + await saveProfiles(profiles); +} +export async function loadProfile(id) { + const profiles = await getProfiles(); + const p = profiles.find((x) => x.id === id); + return p ? { ...p.params } : null; +} +export function compareProfile(profile, current) { + return diffParams(current, profile); +} +export function exportProfileToJSON(profile) { + return JSON.stringify({ + name: profile.name, + createdAt: new Date(profile.createdAt).toISOString(), + params: profile.params, + }, null, 2); +} +export function importProfileFromJSON(json) { + const data = JSON.parse(json); + if (!data.params || typeof data.params !== 'object') { + throw new Error('Invalid profile JSON: missing params'); + } + return { + name: data.name ?? 'Imported', + params: data.params, + }; +} +//# sourceMappingURL=profiles.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/persistence/profiles.js.map b/SiKRadioTools/dist/persistence/profiles.js.map new file mode 100644 index 00000000..7d6f9efc --- /dev/null +++ b/SiKRadioTools/dist/persistence/profiles.js.map @@ -0,0 +1 @@ +{"version":3,"file":"profiles.js","sourceRoot":"","sources":["../../src/persistence/profiles.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAqB,MAAM,cAAc,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,UAAU,EAAoB,MAAM,qBAAqB,CAAC;AAEnF,MAAM,CAAC,KAAK,UAAU,YAAY;IAChC,OAAO,WAAW,EAAE,CAAC;AACvB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAY,EAAE,MAAmB;IACjE,MAAM,QAAQ,GAAG,MAAM,WAAW,EAAE,CAAC;IACrC,MAAM,OAAO,GAAiB;QAC5B,EAAE,EAAE,MAAM,CAAC,UAAU,EAAE;QACvB,IAAI;QACJ,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;QACrB,MAAM,EAAE,cAAc,CAAC,MAAM,CAAC;KAC/B,CAAC;IACF,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACvB,MAAM,YAAY,CAAC,QAAQ,CAAC,CAAC;IAC7B,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,EAAU;IAC5C,MAAM,QAAQ,GAAG,MAAM,WAAW,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IAC/E,MAAM,YAAY,CAAC,QAAQ,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,EAAU;IAC1C,MAAM,QAAQ,GAAG,MAAM,WAAW,EAAE,CAAC;IACrC,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAC5C,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,OAAoB,EAAE,OAAoB;IACvE,OAAO,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,OAAqB;IACvD,OAAO,IAAI,CAAC,SAAS,CACnB;QACE,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,SAAS,EAAE,IAAI,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE;QACpD,MAAM,EAAE,OAAO,CAAC,MAAM;KACvB,EACD,IAAI,EACJ,CAAC,CACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,UAAU;QAC7B,MAAM,EAAE,IAAI,CAAC,MAAM;KACpB,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/SiKRadioTools/dist/persistence/storage.js b/SiKRadioTools/dist/persistence/storage.js new file mode 100644 index 00000000..61aa55cd --- /dev/null +++ b/SiKRadioTools/dist/persistence/storage.js @@ -0,0 +1,41 @@ +/** + * Settings and profiles persisted in localStorage + */ +const KEYS = { + SETTINGS: 'app_settings', + PROFILES: 'profiles', +}; +const DEFAULT_SETTINGS = { + baudRate: 57600, + darkMode: false, +}; +function readLocalStorageJson(key, fallback) { + try { + const raw = localStorage.getItem(key); + if (raw == null) + return fallback; + return JSON.parse(raw); + } + catch { + return fallback; + } +} +function writeLocalStorageJson(key, value) { + localStorage.setItem(key, JSON.stringify(value)); +} +export async function getSettings() { + const stored = readLocalStorageJson(KEYS.SETTINGS, null); + return { ...DEFAULT_SETTINGS, ...stored }; +} +export async function saveSettings(settings) { + const current = await getSettings(); + writeLocalStorageJson(KEYS.SETTINGS, { ...current, ...settings }); +} +export async function getProfiles() { + const list = readLocalStorageJson(KEYS.PROFILES, null); + return Array.isArray(list) ? list : []; +} +export async function saveProfiles(profiles) { + writeLocalStorageJson(KEYS.PROFILES, profiles); +} +//# sourceMappingURL=storage.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/persistence/storage.js.map b/SiKRadioTools/dist/persistence/storage.js.map new file mode 100644 index 00000000..d6446379 --- /dev/null +++ b/SiKRadioTools/dist/persistence/storage.js.map @@ -0,0 +1 @@ +{"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/persistence/storage.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,IAAI,GAAG;IACX,QAAQ,EAAE,cAAc;IACxB,QAAQ,EAAE,UAAU;CACZ,CAAC;AAEX,MAAM,gBAAgB,GAAgB;IACpC,QAAQ,EAAE,KAAK;IACf,QAAQ,EAAE,KAAK;CAChB,CAAC;AAEF,SAAS,oBAAoB,CAAI,GAAW,EAAE,QAAW;IACvD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,GAAG,IAAI,IAAI;YAAE,OAAO,QAAQ,CAAC;QACjC,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAM,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,CAAC;IAClB,CAAC;AACH,CAAC;AAED,SAAS,qBAAqB,CAAC,GAAW,EAAE,KAAc;IACxD,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;AACnD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,MAAM,MAAM,GAAG,oBAAoB,CAA8B,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACtF,OAAO,EAAE,GAAG,gBAAgB,EAAE,GAAG,MAAM,EAAE,CAAC;AAC5C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAA8B;IAC/D,MAAM,OAAO,GAAG,MAAM,WAAW,EAAE,CAAC;IACpC,qBAAqB,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,GAAG,OAAO,EAAE,GAAG,QAAQ,EAAE,CAAC,CAAC;AACpE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,MAAM,IAAI,GAAG,oBAAoB,CAAwB,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC9E,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;AACzC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAwB;IACzD,qBAAqB,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AACjD,CAAC"} \ No newline at end of file diff --git a/SiKRadioTools/dist/protocol/at-parser.js b/SiKRadioTools/dist/protocol/at-parser.js new file mode 100644 index 00000000..9c77baef --- /dev/null +++ b/SiKRadioTools/dist/protocol/at-parser.js @@ -0,0 +1,71 @@ +/** + * AT command response parser for SiK radios + */ +/** Parse ATI5-style parameter output: S0: FORMAT=22, S1: SERIAL_SPEED=57, S3:NETID=26, etc. */ +export function parseATI5Response(lines) { + const params = {}; + const paramRe = /^S(\d+):\s*(\w+)=(.+)$/; + const shortRe = /^S(\d+)=(.+)$/; + for (const line of lines) { + const t = line.trim(); + let m = t.match(paramRe); + if (m) { + const [, regNum, key, value] = m; + const num = parseInt(value, 10); + const val = isNaN(num) ? value.trim() : num; + params[key] = val; + params[`S${regNum}`] = val; + } + else { + m = t.match(shortRe); + if (m) { + const [, regNum, value] = m; + const num = parseInt(value, 10); + params[`S${regNum}`] = isNaN(num) ? value.trim() : num; + } + } + } + return params; +} +/** Check if response indicates OK */ +export function isOK(line) { + return /^OK\s*$/i.test(line.trim()); +} +/** Check if response indicates ERROR */ +export function isError(line) { + return /^ERROR\s*$/i.test(line.trim()); +} +/** Extract lines between command and OK/ERROR */ +export function parseATResponse(lines) { + const result = { ok: false, lines: [] }; + for (const line of lines) { + const t = line.trim(); + if (isOK(t)) { + result.ok = true; + break; + } + if (isError(t)) { + result.ok = false; + break; + } + if (t.length > 0) { + result.lines.push(t); + } + } + if (result.lines.length > 0 && result.lines.some((l) => /^S\d+:\s*\w+=/.test(l))) { + result.params = parseATI5Response(result.lines); + } + return result; +} +/** Parse a single ATSn? response: "57" or "value" */ +export function parseATSResponse(lines) { + for (const line of lines) { + const t = line.trim(); + if (isOK(t) || isError(t)) + continue; + const num = parseInt(t, 10); + return isNaN(num) ? t : num; + } + return null; +} +//# sourceMappingURL=at-parser.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/protocol/at-parser.js.map b/SiKRadioTools/dist/protocol/at-parser.js.map new file mode 100644 index 00000000..384ff3bc --- /dev/null +++ b/SiKRadioTools/dist/protocol/at-parser.js.map @@ -0,0 +1 @@ +{"version":3,"file":"at-parser.js","sourceRoot":"","sources":["../../src/protocol/at-parser.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/SiKRadioTools/dist/protocol/bootloader-client.js b/SiKRadioTools/dist/protocol/bootloader-client.js new file mode 100644 index 00000000..7d59cbde --- /dev/null +++ b/SiKRadioTools/dist/protocol/bootloader-client.js @@ -0,0 +1,166 @@ +/** + * SiK bootloader flashing client (Web Serial) + */ +const INSYNC = 0x12; +const OK = 0x10; +const EOC = 0x20; +const GET_SYNC = 0x21; +const GET_DEVICE = 0x22; +const CHIP_ERASE = 0x23; +const LOAD_ADDRESS = 0x24; +const PROG_MULTI = 0x27; +const READ_MULTI = 0x28; +const REBOOT = 0x30; +const PROG_MULTI_MAX = 32; +const READ_MULTI_MAX = 128; +export class BootloaderClient { + constructor(transport, onLog) { + this.byteQueue = []; + this.unsubData = null; + this.transport = transport; + this.onLog = onLog; + if (!transport.addDataListener) { + throw new Error('Transport does not support raw data listener'); + } + this.unsubData = transport.addDataListener((data) => { + for (const b of data) + this.byteQueue.push(b); + }); + } + dispose() { + this.unsubData?.(); + this.unsubData = null; + } + log(msg) { + this.onLog?.(msg); + } + clearRx() { + this.byteQueue = []; + } + async writeBytes(bytes) { + await this.transport.write(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)); + } + async readByte(timeoutMs = 3000) { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + if (this.byteQueue.length > 0) { + return this.byteQueue.shift(); + } + await new Promise((r) => setTimeout(r, 5)); + } + throw new Error('Timeout waiting for bootloader data'); + } + async getSync(timeoutMs = 3000) { + const a = await this.readByte(timeoutMs); + const b = await this.readByte(timeoutMs); + return a === INSYNC && b === OK; + } + async sync(retries = 3) { + for (let i = 0; i < retries; i++) { + this.clearRx(); + // Send NOP stream like official uploader and request sync. + await this.writeBytes(new Uint8Array(PROG_MULTI_MAX + 2)); + await this.writeBytes([GET_SYNC, EOC]); + try { + if (await this.getSync(1000)) { + this.log('Bootloader sync OK'); + return true; + } + } + catch { + // retry + } + await new Promise((r) => setTimeout(r, 100)); + } + return false; + } + async identify() { + await this.writeBytes([GET_DEVICE, EOC]); + const boardId = await this.readByte(); + const boardFreq = await this.readByte(); + const ok = await this.getSync(); + if (!ok) + throw new Error('Bootloader identify failed'); + this.log(`Board ID=0x${boardId.toString(16)} FREQ=0x${boardFreq.toString(16)}`); + return { boardId, boardFreq }; + } + async erase() { + await this.writeBytes([CHIP_ERASE, EOC]); + const ok = await this.getSync(10000); + if (!ok) + throw new Error('Erase failed'); + } + async loadAddress(address, useBanking) { + if (useBanking) { + await this.writeBytes([ + LOAD_ADDRESS, + address & 0xff, + (address >> 8) & 0xff, + (address >> 16) & 0xff, + EOC, + ]); + } + else { + await this.writeBytes([LOAD_ADDRESS, address & 0xff, (address >> 8) & 0xff, EOC]); + } + const ok = await this.getSync(); + if (!ok) + throw new Error(`LOAD_ADDRESS failed at 0x${address.toString(16)}`); + } + async programChunk(data) { + await this.writeBytes([PROG_MULTI, data.length]); + await this.writeBytes(data); + await this.writeBytes([EOC]); + const ok = await this.getSync(); + if (!ok) + throw new Error('PROG_MULTI failed'); + } + async verifyChunk(data) { + await this.writeBytes([READ_MULTI, data.length, EOC]); + for (let i = 0; i < data.length; i++) { + const b = await this.readByte(); + if (b !== data[i]) { + throw new Error(`Verify mismatch at chunk byte ${i}`); + } + } + const ok = await this.getSync(); + if (!ok) + throw new Error('READ_MULTI sync failed'); + } + *split(data, max) { + for (let i = 0; i < data.length; i += max) { + yield data.slice(i, i + max); + } + } + async flash(fw, opts) { + const verify = opts?.verify ?? true; + const onProgress = opts?.onProgress; + onProgress?.({ phase: 'erase', completed: 0, total: fw.totalBytes }); + await this.erase(); + let completed = 0; + onProgress?.({ phase: 'program', completed, total: fw.totalBytes }); + for (const seg of fw.segments) { + await this.loadAddress(seg.address, fw.usesBanking); + for (const chunk of this.split(seg.data, PROG_MULTI_MAX)) { + await this.programChunk(chunk); + completed += chunk.length; + onProgress?.({ phase: 'program', completed, total: fw.totalBytes }); + } + } + if (verify) { + completed = 0; + onProgress?.({ phase: 'verify', completed, total: fw.totalBytes }); + for (const seg of fw.segments) { + await this.loadAddress(seg.address, fw.usesBanking); + for (const chunk of this.split(seg.data, READ_MULTI_MAX)) { + await this.verifyChunk(chunk); + completed += chunk.length; + onProgress?.({ phase: 'verify', completed, total: fw.totalBytes }); + } + } + } + await this.writeBytes([REBOOT]); + onProgress?.({ phase: 'reboot', completed: fw.totalBytes, total: fw.totalBytes }); + } +} +//# sourceMappingURL=bootloader-client.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/protocol/bootloader-client.js.map b/SiKRadioTools/dist/protocol/bootloader-client.js.map new file mode 100644 index 00000000..2726c0c5 --- /dev/null +++ b/SiKRadioTools/dist/protocol/bootloader-client.js.map @@ -0,0 +1 @@ +{"version":3,"file":"bootloader-client.js","sourceRoot":"","sources":["../../src/protocol/bootloader-client.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/SiKRadioTools/dist/protocol/hex-parser.js b/SiKRadioTools/dist/protocol/hex-parser.js new file mode 100644 index 00000000..f9ab9039 --- /dev/null +++ b/SiKRadioTools/dist/protocol/hex-parser.js @@ -0,0 +1,75 @@ +/** + * Intel HEX parser for SiK firmware flashing + */ +function parseHexByte(hex) { + return parseInt(hex, 16); +} +export function parseIntelHex(content) { + const lines = content.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0); + const ranges = new Map(); + let upper = 0; + let usesBanking = false; + for (const line of lines) { + if (!line.startsWith(':')) + continue; + const raw = line.slice(1); + if (raw.length < 10 || raw.length % 2 !== 0) { + throw new Error(`Invalid HEX line: ${line}`); + } + const bytes = []; + for (let i = 0; i < raw.length; i += 2) { + bytes.push(parseHexByte(raw.slice(i, i + 2))); + } + const count = bytes[0]; + const addr = (bytes[1] << 8) | bytes[2]; + const type = bytes[3]; + const data = bytes.slice(4, 4 + count); + if (type === 0x00) { + const abs = (upper << 16) + addr; + if (upper !== 0) + usesBanking = true; + ranges.set(abs, data); + } + else if (type === 0x04) { + if (count !== 2) + throw new Error('Invalid type 04 record'); + upper = (data[0] << 8) | data[1]; + if (upper !== 0) + usesBanking = true; + } + else if (type === 0x01) { + break; + } + } + // Merge contiguous ranges + const merged = new Map(); + const addresses = [...ranges.keys()].sort((a, b) => a - b); + for (const address of addresses) { + const bytes = [...(ranges.get(address) ?? [])]; + const nextStart = address + bytes.length; + if (merged.has(nextStart)) { + bytes.push(...(merged.get(nextStart) ?? [])); + merged.delete(nextStart); + } + let mergedIntoExisting = false; + for (const [start, existing] of [...merged.entries()]) { + if (start + existing.length === address) { + existing.push(...bytes); + mergedIntoExisting = true; + break; + } + } + if (!mergedIntoExisting) { + merged.set(address, bytes); + } + } + const segments = [...merged.entries()] + .sort((a, b) => a[0] - b[0]) + .map(([address, data]) => ({ address, data: new Uint8Array(data) })); + const totalBytes = segments.reduce((sum, s) => sum + s.data.length, 0); + if (totalBytes === 0) { + throw new Error('HEX file contains no data records'); + } + return { segments, usesBanking, totalBytes }; +} +//# sourceMappingURL=hex-parser.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/protocol/hex-parser.js.map b/SiKRadioTools/dist/protocol/hex-parser.js.map new file mode 100644 index 00000000..48679125 --- /dev/null +++ b/SiKRadioTools/dist/protocol/hex-parser.js.map @@ -0,0 +1 @@ +{"version":3,"file":"hex-parser.js","sourceRoot":"","sources":["../../src/protocol/hex-parser.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/SiKRadioTools/dist/protocol/line-buffer.js b/SiKRadioTools/dist/protocol/line-buffer.js new file mode 100644 index 00000000..34c410cd --- /dev/null +++ b/SiKRadioTools/dist/protocol/line-buffer.js @@ -0,0 +1,38 @@ +/** + * Line buffer - accumulates incoming bytes and emits complete lines + */ +export class LineBuffer { + constructor(callbacks) { + this.buffer = ''; + this.lineEndings = /\r\n|\r|\n/; + this.callbacks = callbacks; + } + /** Push raw data; emits complete lines */ + push(data) { + this.buffer += data; + this.flushLines(); + } + /** Push raw bytes (UTF-8 decoded) */ + pushBytes(bytes) { + this.push(new TextDecoder().decode(bytes)); + } + flushLines() { + const parts = this.buffer.split(this.lineEndings); + this.buffer = parts.pop() ?? ''; + for (const line of parts) { + this.callbacks.onLine(line); + } + } + /** Flush any remaining buffered content as a final line */ + flush() { + if (this.buffer.trim().length > 0) { + this.callbacks.onLine(this.buffer); + this.buffer = ''; + } + } + /** Clear the buffer without emitting */ + clear() { + this.buffer = ''; + } +} +//# sourceMappingURL=line-buffer.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/protocol/line-buffer.js.map b/SiKRadioTools/dist/protocol/line-buffer.js.map new file mode 100644 index 00000000..6e562fef --- /dev/null +++ b/SiKRadioTools/dist/protocol/line-buffer.js.map @@ -0,0 +1 @@ +{"version":3,"file":"line-buffer.js","sourceRoot":"","sources":["../../src/protocol/line-buffer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,MAAM,OAAO,UAAU;IAKrB,YAAY,SAA8B;QAJlC,WAAM,GAAG,EAAE,CAAC;QAEZ,gBAAW,GAAW,YAAY,CAAC;QAGzC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;IAED,0CAA0C;IAC1C,IAAI,CAAC,IAAY;QACf,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC;QACpB,IAAI,CAAC,UAAU,EAAE,CAAC;IACpB,CAAC;IAED,qCAAqC;IACrC,SAAS,CAAC,KAAiB;QACzB,IAAI,CAAC,IAAI,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAC7C,CAAC;IAEO,UAAU;QAChB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAClD,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;QAChC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;IAED,2DAA2D;IAC3D,KAAK;QACH,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACnC,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QACnB,CAAC;IACH,CAAC;IAED,wCAAwC;IACxC,KAAK;QACH,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;IACnB,CAAC;CACF"} \ No newline at end of file diff --git a/SiKRadioTools/dist/protocol/sik-client.js b/SiKRadioTools/dist/protocol/sik-client.js new file mode 100644 index 00000000..5bcd67a2 --- /dev/null +++ b/SiKRadioTools/dist/protocol/sik-client.js @@ -0,0 +1,219 @@ +/** + * SiK radio AT command client + * Handles command mode, guard times, timeouts, and parameter read/write + */ +import { parseATResponse, parseATI5Response } from './at-parser.js'; +const GUARD_MS = 1500; +const GUARD_MAX_WAIT_MS = 15000; // stop waiting after this; user may need to stop data flow +const CMD_TIMEOUT_MS = 5000; +export class SiKRadioClient { + constructor(transport) { + this.inCommandMode = false; + this.lastActivity = 0; + this.lastReceiveTime = 0; + this.responseQueue = []; + this.pendingLines = []; + this.callbacks = {}; + this.transport = transport; + this.setupTransport(); + } + setCallbacks(cb) { + this.callbacks = cb; + } + setupTransport() { + this.transport.setCallbacks({ + onData: () => { + this.lastReceiveTime = Date.now(); + }, + onLine: (line) => this.handleLine(line), + }); + } + handleLine(line) { + const t = line.trim(); + if (t.length === 0) + return; + this.lastReceiveTime = Date.now(); + this.pendingLines.push(t); + if (/^OK\s*$/i.test(t) || /^ERROR\s*$/i.test(t)) { + const result = parseATResponse(this.pendingLines); + this.pendingLines = []; + const resolve = this.responseQueue.shift(); + if (resolve) + resolve(result); + } + } + async waitForResponse() { + return new Promise((resolve, reject) => { + const resolver = (result) => { + clearTimeout(timeout); + resolve(result); + }; + const timeout = setTimeout(() => { + const idx = this.responseQueue.indexOf(resolver); + if (idx !== -1) { + this.responseQueue.splice(idx, 1); + reject(new Error('AT command timeout — radio did not respond')); + } + }, CMD_TIMEOUT_MS); + this.responseQueue.push(resolver); + }); + } + /** Wait until 1.5s of no incoming data and 1.5s since our last send so the radio will recognize +++. */ + async ensureGuardTime() { + const deadline = Date.now() + GUARD_MAX_WAIT_MS; + while (Date.now() < deadline) { + const now = Date.now(); + const sinceReceive = now - this.lastReceiveTime; + const sinceSend = now - this.lastActivity; + if (sinceReceive >= GUARD_MS && sinceSend >= GUARD_MS) { + this.lastActivity = Date.now(); + return; + } + const needReceive = Math.max(0, GUARD_MS - sinceReceive); + const needSend = Math.max(0, GUARD_MS - sinceSend); + const waitMs = Math.min(Math.max(needReceive, needSend), deadline - Date.now()); + if (waitMs > 0) { + await new Promise((r) => setTimeout(r, waitMs)); + } + } + throw new Error('No line silence — stop data flow (disconnect other radio or autopilot) and try again'); + } + /** If radio is in command mode, exit to passthrough (ATO). Then +++ will be recognized. */ + async ensurePassthrough() { + this.pendingLines = []; + let resolveExit; + const exitPromise = new Promise((r) => { resolveExit = r; }); + const resolver = (result) => { resolveExit(result); }; + this.responseQueue.push(resolver); + await this.transport.write('ATO\r\n'); + this.lastActivity = Date.now(); + const EXIT_TIMEOUT_MS = 1500; + setTimeout(() => { + const idx = this.responseQueue.indexOf(resolver); + if (idx !== -1) { + this.responseQueue.splice(idx, 1); + resolveExit({ ok: false, lines: [] }); + } + }, EXIT_TIMEOUT_MS); + const exitResult = await exitPromise; + if (exitResult.ok) { + this.inCommandMode = false; + } + this.pendingLines = []; + } + async enterCommandMode() { + await this.ensurePassthrough(); + await this.ensureGuardTime(); + await this.transport.write('+++'); + this.lastActivity = Date.now(); + const result = await this.waitForResponse(); + this.inCommandMode = result.ok; + return result.ok; + } + async exitCommandMode() { + const result = await this.sendAT('O'); + this.inCommandMode = !result.ok; + return result.ok; + } + async sendAT(cmd) { + this.pendingLines = []; + const fullCmd = cmd.startsWith('AT') ? cmd : `AT${cmd}`; + this.callbacks.onLog?.(`TX: ${fullCmd}`); + await this.transport.write(fullCmd + '\r\n'); + return this.waitForResponse(); + } + async getVersion() { + const result = await this.sendAT('I'); + return result.lines[0] ?? 'Unknown'; + } + async readAllParameters() { + const lines = await this.readATI5Lines(); + const params = parseATI5Response(lines); + if (Object.keys(params).length === 0) { + throw new Error('Failed to parse ATI5 response'); + } + return params; + } + /** + * ATI5 on some SiK firmwares does not end with OK/ERROR, so we cannot rely on waitForResponse(). + * Capture raw lines until a short quiet period after receiving S-register lines. + */ + async readATI5Lines() { + if (!this.transport.addLineListener) { + // Fallback for transports without extra line listener support + const result = await this.sendAT('I5'); + return result.lines; + } + const lines = []; + let sawRegisterLine = false; + let lastLineAt = 0; + const QUIET_MS = 250; + const MAX_MS = 5000; + const start = Date.now(); + const unsubscribe = this.transport.addLineListener((line) => { + const t = line.trim(); + if (!t) + return; + lines.push(t); + lastLineAt = Date.now(); + if (/^S\d+[:=]/.test(t)) { + sawRegisterLine = true; + } + }); + try { + this.callbacks.onLog?.('TX: ATI5'); + await this.transport.write('ATI5\r\n'); + this.lastActivity = Date.now(); + while (Date.now() - start < MAX_MS) { + if (sawRegisterLine && Date.now() - lastLineAt >= QUIET_MS) { + break; + } + await new Promise((r) => setTimeout(r, 50)); + } + } + finally { + unsubscribe(); + } + return lines; + } + async readParameter(register) { + const result = await this.sendAT(`${register}?`); + if (!result.ok) + return null; + const line = result.lines.find((l) => !/^OK|^ERROR/i.test(l)); + if (!line) + return null; + const num = parseInt(line, 10); + return isNaN(num) ? line : num; + } + async writeParameter(register, value) { + const result = await this.sendAT(`${register}=${value}`); + return result.ok; + } + async saveParameters() { + const result = await this.sendAT('&W'); + return result.ok; + } + async reboot() { + // ATZ performs an immediate software reset on many SiK firmwares and may not return OK. + await this.transport.write('ATZ\r\n'); + this.lastActivity = Date.now(); + this.inCommandMode = false; + return true; + } + async resetDefaults() { + const result = await this.sendAT('&F'); + return result.ok; + } + /** Send RT command to remote radio if link is active */ + async getRemoteParametersIfAvailable() { + const result = await this.sendAT('RTI5'); + if (!result.ok || !result.params) + return null; + return result.params; + } + get inCommandModeState() { + return this.inCommandMode; + } +} +//# sourceMappingURL=sik-client.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/protocol/sik-client.js.map b/SiKRadioTools/dist/protocol/sik-client.js.map new file mode 100644 index 00000000..95b6c102 --- /dev/null +++ b/SiKRadioTools/dist/protocol/sik-client.js.map @@ -0,0 +1 @@ +{"version":3,"file":"sik-client.js","sourceRoot":"","sources":["../../src/protocol/sik-client.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/SiKRadioTools/dist/transport/index.js b/SiKRadioTools/dist/transport/index.js new file mode 100644 index 00000000..157c589f --- /dev/null +++ b/SiKRadioTools/dist/transport/index.js @@ -0,0 +1,3 @@ +export { SerialTransport } from './serial.js'; +export { MockTransport } from './mock.js'; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/transport/index.js.map b/SiKRadioTools/dist/transport/index.js.map new file mode 100644 index 00000000..fdec65ad --- /dev/null +++ b/SiKRadioTools/dist/transport/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/transport/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC"} \ No newline at end of file diff --git a/SiKRadioTools/dist/transport/mock.js b/SiKRadioTools/dist/transport/mock.js new file mode 100644 index 00000000..05a5403d --- /dev/null +++ b/SiKRadioTools/dist/transport/mock.js @@ -0,0 +1,116 @@ +/** + * Mock transport for UI testing without hardware + * Simulates SiK radio AT responses + */ +const MOCK_ATI5 = `S0: FORMAT=22 +S1: SERIAL_SPEED=57 +S2: AIR_SPEED=64 +S3: NETID=25 +S4: TXPOWER=20 +S5: ECC=0 +S6: MAVLINK=1 +S7: OPPRESEND=1 +S8: MIN_FREQ=915000 +S9: MAX_FREQ=928000 +S10: NUM_CHANNELS=50 +S11: DUTY_CYCLE=100 +S12: LBT_RSSI=0 +S13: MANCHESTER=0 +S14: RTSCTS=0 +S15: MAX_WINDOW=131 +OK`; +export class MockTransport { + constructor() { + this.callbacks = {}; + this.lineListeners = new Set(); + this.dataListeners = new Set(); + this._isConnected = false; + this.responseDelay = 50; + } + get isConnected() { + return this._isConnected; + } + get portInfo() { + return this._isConnected ? { name: 'Mock SiK Radio (Demo)' } : undefined; + } + setCallbacks(cb) { + this.callbacks = cb; + } + addLineListener(cb) { + this.lineListeners.add(cb); + return () => this.lineListeners.delete(cb); + } + addDataListener(cb) { + this.dataListeners.add(cb); + return () => this.dataListeners.delete(cb); + } + async requestPort() { + await this.simulateDelay(); + // Simulate success - no actual port + } + async reconnectKnownPort() { + await this.simulateDelay(); + return false; + } + async open() { + await this.simulateDelay(); + this._isConnected = true; + this.callbacks.onClose = this.callbacks.onClose; + } + async close() { + await this.simulateDelay(); + this._isConnected = false; + this.callbacks.onClose?.(); + } + async write(data) { + const str = typeof data === 'string' ? data : new TextDecoder().decode(data); + await this.simulateDelay(); + // Echo back for terminal + this.emitLines([str.trim()]); + // Simulate AT responses + const upper = str.trim().toUpperCase(); + if (upper === '+++') { + this.emitLines(['OK']); + } + else if (upper === 'ATO') { + this.emitLines(['OK']); + } + else if (upper === 'AT' || upper === 'ATI') { + this.emitLines(['SiK radio v1.0 on 3DR Radio', 'OK']); + } + else if (upper === 'ATI5') { + this.emitLines(MOCK_ATI5.split('\n')); + } + else if (/^ATS\d+\?$/.test(upper)) { + this.emitLines(['57', 'OK']); + } + else if (/^ATS\d+=.+$/.test(upper)) { + this.emitLines(['OK']); + } + else if (upper === 'AT&W') { + this.emitLines(['OK']); + } + else if (upper === 'AT&F') { + this.emitLines(['OK']); + } + else if (upper === 'ATZ') { + this.emitLines(['OK']); + } + else if (upper.startsWith('AT')) { + this.emitLines(['OK']); + } + } + emitLines(lines) { + for (const line of lines) { + this.callbacks.onLine?.(line); + this.lineListeners.forEach((cb) => cb(line)); + this.callbacks.onData?.(line + '\r\n'); + const bytes = new TextEncoder().encode(line + '\r\n'); + this.dataListeners.forEach((cb) => cb(bytes)); + } + } + simulateDelay() { + return new Promise((r) => setTimeout(r, this.responseDelay)); + } +} +//# sourceMappingURL=mock.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/transport/mock.js.map b/SiKRadioTools/dist/transport/mock.js.map new file mode 100644 index 00000000..78247660 --- /dev/null +++ b/SiKRadioTools/dist/transport/mock.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mock.js","sourceRoot":"","sources":["../../src/transport/mock.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/SiKRadioTools/dist/transport/serial.js b/SiKRadioTools/dist/transport/serial.js new file mode 100644 index 00000000..36d91788 --- /dev/null +++ b/SiKRadioTools/dist/transport/serial.js @@ -0,0 +1,158 @@ +/** + * Serial transport using Web Serial API + * Requires user gesture for requestPort() + */ +import { LineBuffer } from '../protocol/line-buffer.js'; +export class SerialTransport { + constructor() { + this.port = null; + this.reader = null; + this.readerLock = false; + this.callbacks = {}; + this.lineListeners = new Set(); + this.dataListeners = new Set(); + this._isConnected = false; + this.lineBuffer = new LineBuffer({ + onLine: (line) => { + this.callbacks.onLine?.(line); + this.lineListeners.forEach((cb) => cb(line)); + }, + }); + } + addLineListener(cb) { + this.lineListeners.add(cb); + return () => this.lineListeners.delete(cb); + } + addDataListener(cb) { + this.dataListeners.add(cb); + return () => this.dataListeners.delete(cb); + } + get isConnected() { + return this._isConnected; + } + get portInfo() { + if (!this.port) + return undefined; + try { + const info = this.port.getInfo?.(); + const vid = info?.usbVendorId; + const pid = info?.usbProductId; + const name = this.port.serialNumber ?? + (vid != null && pid != null ? `USB ${vid.toString(16).padStart(4, '0')}:${pid.toString(16).padStart(4, '0')}` : null) ?? + 'Serial Port'; + return { name, vendorId: vid, productId: pid }; + } + catch { + return { name: 'Serial Port' }; + } + } + setCallbacks(cb) { + this.callbacks = cb; + } + async requestPort(options) { + if (!navigator.serial) { + throw new Error('Web Serial API is not available. Use Chrome 89+ on desktop.'); + } + const filters = options?.filters ?? SerialTransport.SIK_FILTERS; + this.port = await navigator.serial.requestPort({ filters }); + } + async reconnectKnownPort() { + if (!navigator.serial) + return false; + const ports = await navigator.serial.getPorts(); + if (ports.length === 0) + return false; + this.port = ports[0]; + return true; + } + async open(options) { + if (!this.port) { + throw new Error('No port selected. Call requestPort() or reconnectKnownPort() first.'); + } + await this.port.open({ + baudRate: options.baudRate, + dataBits: 8, + stopBits: 1, + parity: 'none', + bufferSize: 65536, + }); + this._isConnected = true; + this.startReadLoop(); + } + async close() { + this._isConnected = false; + if (this.reader) { + try { + await this.reader.cancel(); + } + catch { + /* ignore */ + } + this.reader = null; + } + if (this.port) { + try { + await this.port.close(); + } + catch { + /* ignore */ + } + this.port = null; + } + this.callbacks.onClose?.(); + } + async write(data) { + if (!this.port?.writable) { + throw new Error('Port is not open for writing'); + } + const writer = this.port.writable.getWriter(); + try { + const bytes = typeof data === 'string' + ? new TextEncoder().encode(data) + : data; + await writer.write(bytes); + } + finally { + writer.releaseLock(); + } + } + async startReadLoop() { + if (!this.port?.readable || this.readerLock) + return; + this.readerLock = true; + this.reader = this.port.readable.getReader(); + try { + const port = this.port; + while (this._isConnected && port.readable) { + const { value, done } = await this.reader.read(); + if (done) + break; + if (value && value.length > 0) { + this.dataListeners.forEach((cb) => cb(value)); + const text = new TextDecoder().decode(value); + this.callbacks.onData?.(text); + this.lineBuffer.push(text); + } + } + } + catch (err) { + if (this._isConnected) { + this.callbacks.onError?.(err instanceof Error ? err : new Error(String(err))); + } + } + finally { + this.reader?.releaseLock(); + this.reader = null; + this.readerLock = false; + if (this._isConnected) { + this._isConnected = false; + this.callbacks.onClose?.(); + } + } + } +} +/** Known SiK radio USB IDs (FTDI-based: Holybro, 3DR, etc.) */ +SerialTransport.SIK_FILTERS = [ + { usbVendorId: 0x0403, usbProductId: 0x6015 }, +]; +//# sourceMappingURL=serial.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/transport/serial.js.map b/SiKRadioTools/dist/transport/serial.js.map new file mode 100644 index 00000000..0637f00a --- /dev/null +++ b/SiKRadioTools/dist/transport/serial.js.map @@ -0,0 +1 @@ +{"version":3,"file":"serial.js","sourceRoot":"","sources":["../../src/transport/serial.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/SiKRadioTools/dist/transport/types.js b/SiKRadioTools/dist/transport/types.js new file mode 100644 index 00000000..9734bd2e --- /dev/null +++ b/SiKRadioTools/dist/transport/types.js @@ -0,0 +1,5 @@ +/** + * Transport layer interface - abstracts serial/TCP/Bluetooth for future extensibility + */ +export {}; +//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/transport/types.js.map b/SiKRadioTools/dist/transport/types.js.map new file mode 100644 index 00000000..07160c0d --- /dev/null +++ b/SiKRadioTools/dist/transport/types.js.map @@ -0,0 +1 @@ +{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/transport/types.ts"],"names":[],"mappings":"AAAA;;GAEG"} \ No newline at end of file diff --git a/SiKRadioTools/dist/types.js b/SiKRadioTools/dist/types.js new file mode 100644 index 00000000..f6c0c28c --- /dev/null +++ b/SiKRadioTools/dist/types.js @@ -0,0 +1,5 @@ +/** + * Shared types for SiK Radio Tools + */ +export {}; +//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/types.js.map b/SiKRadioTools/dist/types.js.map new file mode 100644 index 00000000..095d11ea --- /dev/null +++ b/SiKRadioTools/dist/types.js.map @@ -0,0 +1 @@ +{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"} \ No newline at end of file diff --git a/SiKRadioTools/dist/ui/advanced.js b/SiKRadioTools/dist/ui/advanced.js new file mode 100644 index 00000000..d0c03530 --- /dev/null +++ b/SiKRadioTools/dist/ui/advanced.js @@ -0,0 +1,83 @@ +/** + * Advanced tab - manual register access, developer tools + */ +import { getSikClient } from './app.js'; +import { SIK_PARAM_SCHEMA } from '../params/schema.js'; +import { showToast } from './toast.js'; +export function renderAdvancedTab(container, _state) { + container.innerHTML = ` +
+

Manual Parameter Editor

+

Direct AT register access. Use when schema doesn't cover your firmware variant.

+
+ + = + + + +
+
+
+

Register Reference

+
+ ${SIK_PARAM_SCHEMA.map((p) => `${p.register ?? ''}: ${p.key} - ${p.description}`).join('\n')} +
+
+
+

Firmware Workflow

+

Firmware upgrade placeholder. Future versions may support bootloader mode and firmware upload.

+
+ `; + const regInput = document.getElementById('adv-register'); + const valInput = document.getElementById('adv-value'); + document.getElementById('adv-read')?.addEventListener('click', async () => { + const client = getSikClient(); + if (!client) { + showToast('error', 'Not connected'); + return; + } + const reg = regInput.value.trim().toUpperCase(); + if (!reg) { + showToast('warning', 'Enter register (e.g. S1)'); + return; + } + try { + const ok = await client.enterCommandMode(); + if (!ok) + throw new Error('Failed to enter command mode'); + const val = await client.readParameter(reg); + await client.exitCommandMode(); + valInput.value = String(val ?? ''); + showToast('success', `Read ${reg}=${val}`); + } + catch (err) { + showToast('error', err instanceof Error ? err.message : String(err)); + } + }); + document.getElementById('adv-write')?.addEventListener('click', async () => { + const client = getSikClient(); + if (!client) { + showToast('error', 'Not connected'); + return; + } + const reg = regInput.value.trim().toUpperCase(); + const val = valInput.value.trim(); + if (!reg || val === '') { + showToast('warning', 'Enter register and value'); + return; + } + try { + const ok = await client.enterCommandMode(); + if (!ok) + throw new Error('Failed to enter command mode'); + await client.writeParameter(reg, val); + await client.saveParameters(); + await client.exitCommandMode(); + showToast('success', `Wrote ${reg}=${val}. Use ATZ to reboot.`); + } + catch (err) { + showToast('error', err instanceof Error ? err.message : String(err)); + } + }); +} +//# sourceMappingURL=advanced.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/ui/advanced.js.map b/SiKRadioTools/dist/ui/advanced.js.map new file mode 100644 index 00000000..7acedfe7 --- /dev/null +++ b/SiKRadioTools/dist/ui/advanced.js.map @@ -0,0 +1 @@ +{"version":3,"file":"advanced.js","sourceRoot":"","sources":["../../src/ui/advanced.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,MAAM,UAAU,iBAAiB,CAAC,SAAsB,EAAE,MAAgB;IACxE,SAAS,CAAC,SAAS,GAAG;;;;;;;;;;;;;;;UAed,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,IAAI,EAAE,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;;;;;;;GAOjG,CAAC;IAEF,MAAM,QAAQ,GAAG,QAAQ,CAAC,cAAc,CAAC,cAAc,CAAqB,CAAC;IAC7E,MAAM,QAAQ,GAAG,QAAQ,CAAC,cAAc,CAAC,WAAW,CAAqB,CAAC;IAE1E,QAAQ,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;QACxE,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;QAC9B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,SAAS,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;YACpC,OAAO;QACT,CAAC;QACD,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAChD,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,SAAS,CAAC,SAAS,EAAE,0BAA0B,CAAC,CAAC;YACjD,OAAO;QACT,CAAC;QACD,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,gBAAgB,EAAE,CAAC;YAC3C,IAAI,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;YACzD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;YAC5C,MAAM,MAAM,CAAC,eAAe,EAAE,CAAC;YAC/B,QAAQ,CAAC,KAAK,GAAG,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;YACnC,SAAS,CAAC,SAAS,EAAE,QAAQ,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC;QAC7C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,SAAS,CAAC,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QACvE,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,cAAc,CAAC,WAAW,CAAC,EAAE,gBAAgB,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;QACzE,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;QAC9B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,SAAS,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;YACpC,OAAO;QACT,CAAC;QACD,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAChD,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAClC,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK,EAAE,EAAE,CAAC;YACvB,SAAS,CAAC,SAAS,EAAE,0BAA0B,CAAC,CAAC;YACjD,OAAO;QACT,CAAC;QACD,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,gBAAgB,EAAE,CAAC;YAC3C,IAAI,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;YACzD,MAAM,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YACtC,MAAM,MAAM,CAAC,cAAc,EAAE,CAAC;YAC9B,MAAM,MAAM,CAAC,eAAe,EAAE,CAAC;YAC/B,SAAS,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,GAAG,sBAAsB,CAAC,CAAC;QAClE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,SAAS,CAAC,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QACvE,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"} \ No newline at end of file diff --git a/SiKRadioTools/dist/ui/app.js b/SiKRadioTools/dist/ui/app.js new file mode 100644 index 00000000..e33d50ad --- /dev/null +++ b/SiKRadioTools/dist/ui/app.js @@ -0,0 +1,147 @@ +/** + * Main app shell - tabs, connection bar, routing + */ +import { getSettings, saveSettings } from '../persistence/storage.js'; +import { showToast } from './toast.js'; +import { renderConnectionBar } from './connection.js'; +import { renderSettingsTab } from './settings.js'; +import { renderTerminalTab } from './terminal.js'; +import { renderDiagnosticsTab } from './diagnostics.js'; +import { renderProfilesTab } from './profiles.js'; +import { renderAdvancedTab } from './advanced.js'; +import { renderFirmwareTab } from './firmware.js'; +let state = { + connectionState: 'disconnected', + transport: null, + sikClient: null, + baudRate: 57600, + darkMode: false, + demoMode: false, + activeTab: 'settings', + currentParams: {}, +}; +export function getState() { + return { ...state }; +} +export function setState(partial) { + state = { ...state, ...partial }; + render(); +} +export function getTransport() { + return state.transport; +} +export function getSikClient() { + return state.sikClient; +} +export function getCurrentParams() { + return { ...state.currentParams }; +} +export function setCurrentParams(params) { + state.currentParams = params; +} +const TABS = [ + { id: 'settings', label: 'Settings' }, + { id: 'terminal', label: 'Terminal' }, + { id: 'firmware', label: 'Firmware' }, + { id: 'diagnostics', label: 'Diagnostics' }, + { id: 'profiles', label: 'Profiles' }, + { id: 'advanced', label: 'Advanced' }, +]; +function webSerialSupported() { + return typeof navigator !== 'undefined' && !!navigator.serial; +} +function render() { + const root = getRoot(); + root.innerHTML = ` + ${webSerialSupported() + ? '' + : ``} +
+

SiK Radio Tools

+ + +
+
+ +
+
+ `; + document.documentElement.dataset.theme = state.darkMode ? 'dark' : 'light'; + // Event bindings + document.getElementById('demo-mode')?.addEventListener('change', (e) => { + const checked = e.target.checked; + if (state.connectionState === 'connected') { + showToast('warning', 'Disconnect before switching mode'); + e.target.checked = !checked; + return; + } + setState({ demoMode: checked }); + }); + document.getElementById('dark-mode')?.addEventListener('change', (e) => { + const darkMode = e.target.checked; + setState({ darkMode }); + saveSettings({ darkMode }); + }); + document.querySelectorAll('.tab').forEach((btn) => { + btn.addEventListener('click', () => { + setState({ activeTab: btn.dataset.tab ?? 'settings' }); + }); + }); + // Render connection bar and active tab + const connBar = document.getElementById('connection-bar'); + if (connBar) { + renderConnectionBar(connBar, state, setState); + } + const tabContent = document.getElementById('tab-content'); + if (tabContent) { + tabContent.innerHTML = ''; + const panel = document.createElement('div'); + panel.className = 'tab-panel active'; + panel.id = `panel-${state.activeTab}`; + tabContent.appendChild(panel); + switch (state.activeTab) { + case 'settings': + renderSettingsTab(panel, state); + break; + case 'terminal': + renderTerminalTab(panel, state); + break; + case 'firmware': + renderFirmwareTab(panel, state); + break; + case 'diagnostics': + renderDiagnosticsTab(panel, state); + break; + case 'profiles': + renderProfilesTab(panel, state); + break; + case 'advanced': + renderAdvancedTab(panel, state); + break; + default: + renderSettingsTab(panel, state); + } + } +} +let appRoot = null; +export function renderApp(root) { + appRoot = root; + getSettings().then((s) => { + setState({ baudRate: s.baudRate, darkMode: s.darkMode }); + render(); + }); +} +function getRoot() { + return appRoot ?? document.getElementById('app') ?? document.body; +} +//# sourceMappingURL=app.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/ui/app.js.map b/SiKRadioTools/dist/ui/app.js.map new file mode 100644 index 00000000..71600d30 --- /dev/null +++ b/SiKRadioTools/dist/ui/app.js.map @@ -0,0 +1 @@ +{"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/ui/app.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/SiKRadioTools/dist/ui/connection.js b/SiKRadioTools/dist/ui/connection.js new file mode 100644 index 00000000..b43360f1 --- /dev/null +++ b/SiKRadioTools/dist/ui/connection.js @@ -0,0 +1,106 @@ +/** + * Connection bar - Connect, Disconnect, baud rate, status + */ +import { SerialTransport, MockTransport } from '../transport/index.js'; +import { SiKRadioClient } from '../protocol/sik-client.js'; +import { saveSettings } from '../persistence/storage.js'; +import { logInfo, logError } from '../diagnostics/logger.js'; +import { showToast } from './toast.js'; +const BAUD_OPTIONS = [9600, 19200, 38400, 57600, 115200]; +export function renderConnectionBar(container, state, setState) { + const statusClass = state.connectionState === 'connected' ? 'connected' + : state.connectionState === 'error' ? 'error' + : state.connectionState === 'connecting' ? 'connecting' + : ''; + container.innerHTML = ` +
+
+ + ${getStatusText(state.connectionState)} + ${state.transport?.portInfo?.name ? `(${state.transport.portInfo.name})` : ''} +
+ + ${state.connectionState === 'connected' + ? `` + : ``} +
+ `; + const btnConnect = document.getElementById('btn-connect'); + const btnDisconnect = document.getElementById('btn-disconnect'); + const baudSelect = document.getElementById('baud-rate'); + baudSelect?.addEventListener('change', () => { + const baud = parseInt(baudSelect.value, 10); + setState({ baudRate: baud }); + saveSettings({ baudRate: baud }); + }); + btnConnect?.addEventListener('click', async () => { + await handleConnect(state, setState); + }); + btnDisconnect?.addEventListener('click', async () => { + await handleDisconnect(state, setState); + }); +} +function getStatusText(s) { + switch (s) { + case 'connected': return 'Connected'; + case 'connecting': return 'Connecting...'; + case 'error': return 'Error'; + default: return 'Disconnected'; + } +} +async function handleConnect(state, setState) { + setState({ connectionState: 'connecting' }); + logInfo('Connecting...', 'connection'); + try { + const TransportClass = state.demoMode ? MockTransport : SerialTransport; + const transport = new TransportClass(); + if (state.demoMode) { + await transport.open({ baudRate: state.baudRate }); + } + else { + const hadPort = await transport.reconnectKnownPort(); + if (!hadPort) { + await transport.requestPort(); + } + await transport.open({ baudRate: state.baudRate }); + } + const sikClient = new SiKRadioClient(transport); + sikClient.setCallbacks({ + onLog: (msg) => logInfo(msg, 'sik'), + }); + setState({ + connectionState: 'connected', + transport, + sikClient, + }); + saveSettings({ lastConnectedPort: transport.portInfo?.name }); + showToast('success', state.demoMode ? 'Demo mode connected' : 'Radio connected'); + logInfo('Connected', 'connection'); + } + catch (err) { + const msg = err instanceof Error ? err.message : String(err); + setState({ connectionState: 'error' }); + showToast('error', msg); + logError(msg, 'connection'); + } +} +async function handleDisconnect(state, setState) { + if (state.transport) { + try { + await state.transport.close(); + } + catch { + /* ignore */ + } + } + setState({ + connectionState: 'disconnected', + transport: null, + sikClient: null, + }); + showToast('info', 'Disconnected'); + logInfo('Disconnected', 'connection'); +} +//# sourceMappingURL=connection.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/ui/connection.js.map b/SiKRadioTools/dist/ui/connection.js.map new file mode 100644 index 00000000..074fd435 --- /dev/null +++ b/SiKRadioTools/dist/ui/connection.js.map @@ -0,0 +1 @@ +{"version":3,"file":"connection.js","sourceRoot":"","sources":["../../src/ui/connection.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/SiKRadioTools/dist/ui/diagnostics.js b/SiKRadioTools/dist/ui/diagnostics.js new file mode 100644 index 00000000..20bb36e6 --- /dev/null +++ b/SiKRadioTools/dist/ui/diagnostics.js @@ -0,0 +1,37 @@ +/** + * Diagnostics tab - event log, link metrics placeholder + */ +import { getEntries, formatEntry, clearLog } from '../diagnostics/logger.js'; +export function renderDiagnosticsTab(container, _state) { + container.innerHTML = ` +
+

Device Event Log

+
+ +
+
+
+
+

Link Metrics

+

RSSI and link quality appear here when MAVLink RADIO packets are received over an active link. Connect a radio and establish a link to see live data.

+ +
+ `; + const logEl = document.getElementById('diagnostics-log'); + const refreshLog = () => { + const entries = getEntries(); + logEl.innerHTML = entries + .slice(-200) + .map((e) => `
${formatEntry(e)}
`) + .join(''); + logEl.scrollTop = logEl.scrollHeight; + }; + refreshLog(); + document.getElementById('btn-clear-log')?.addEventListener('click', () => { + clearLog(); + refreshLog(); + }); +} +//# sourceMappingURL=diagnostics.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/ui/diagnostics.js.map b/SiKRadioTools/dist/ui/diagnostics.js.map new file mode 100644 index 00000000..17b254b8 --- /dev/null +++ b/SiKRadioTools/dist/ui/diagnostics.js.map @@ -0,0 +1 @@ +{"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../../src/ui/diagnostics.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAE7E,MAAM,UAAU,oBAAoB,CAAC,SAAsB,EAAE,MAAgB;IAC3E,SAAS,CAAC,SAAS,GAAG;;;;;;;;;;;;;;;GAerB,CAAC;IAEF,MAAM,KAAK,GAAG,QAAQ,CAAC,cAAc,CAAC,iBAAiB,CAAE,CAAC;IAE1D,MAAM,UAAU,GAAG,GAAS,EAAE;QAC5B,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;QAC7B,KAAK,CAAC,SAAS,GAAG,OAAO;aACtB,KAAK,CAAC,CAAC,GAAG,CAAC;aACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,8BAA8B,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC;aAChE,IAAI,CAAC,EAAE,CAAC,CAAC;QACZ,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,YAAY,CAAC;IACvC,CAAC,CAAC;IAEF,UAAU,EAAE,CAAC;IAEb,QAAQ,CAAC,cAAc,CAAC,eAAe,CAAC,EAAE,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;QACvE,QAAQ,EAAE,CAAC;QACX,UAAU,EAAE,CAAC;IACf,CAAC,CAAC,CAAC;AACL,CAAC"} \ No newline at end of file diff --git a/SiKRadioTools/dist/ui/firmware.js b/SiKRadioTools/dist/ui/firmware.js new file mode 100644 index 00000000..bd14067a --- /dev/null +++ b/SiKRadioTools/dist/ui/firmware.js @@ -0,0 +1,161 @@ +/** + * Firmware flashing tab + */ +import { getSikClient, getTransport } from './app.js'; +import { showToast } from './toast.js'; +import { parseIntelHex } from '../protocol/hex-parser.js'; +import { BootloaderClient } from '../protocol/bootloader-client.js'; +let selectedFirmware = null; +export function renderFirmwareTab(container, state) { + container.innerHTML = ` +
+

Firmware Update

+
+
+ How to prepare a SiK firmware .hex file +
+ 1) Download a valid SiK firmware .hex for your exact radio hardware/frequency (for example from ArduPilot SiK releases or your radio vendor).
+ 2) Make sure the target board/frequency matches your radio (e.g. RFD900x vs HM-TRP, 900MHz vs 433MHz).
+ 3) If the download is a ZIP, extract it and select the firmware .hex file only (not bootloader files unless you intend to replace bootloader).
+ 4) Connect radio by USB at 115200 baud in this tool before flashing.
+ 5) In case sync fails, put radio into bootloader/update mode (this tab also tries AT&UPDATE automatically).
+ 6) Keep USB connected and do not power-cycle during erase/program/verify. +
+
+
+
+ + + No file selected +
+ +
+ +
+ +
+ +
+ +
+
+ + Idle +
+
+ +
+ +
+
+
+ `; + bindFirmwareActions(container, state); +} +function bindFirmwareActions(container, state) { + const fileEl = container.querySelector('#fw-file'); + const metaEl = container.querySelector('#fw-meta'); + const flashBtn = container.querySelector('#fw-flash'); + const verifyEl = container.querySelector('#fw-verify'); + const progressEl = container.querySelector('#fw-progress'); + const progressText = container.querySelector('#fw-progress-text'); + const logEl = container.querySelector('#fw-log'); + const appendLog = (msg) => { + const line = document.createElement('div'); + line.className = 'terminal-line rx'; + line.textContent = `[${new Date().toISOString().slice(11, 23)}] ${msg}`; + logEl.appendChild(line); + logEl.scrollTop = logEl.scrollHeight; + }; + const setProgress = (p) => { + const percent = p.total > 0 ? Math.floor((p.completed / p.total) * 100) : 0; + progressEl.value = percent; + progressText.textContent = `${p.phase}: ${p.completed}/${p.total} (${percent}%)`; + }; + fileEl.addEventListener('change', async () => { + const file = fileEl.files?.[0]; + selectedFirmware = null; + if (!file) { + metaEl.textContent = 'No file selected'; + return; + } + try { + const text = await file.text(); + const parsed = parseIntelHex(text); + selectedFirmware = parsed; + metaEl.textContent = `${file.name} • ${parsed.totalBytes} bytes • ${parsed.segments.length} segments${parsed.usesBanking ? ' • 24-bit' : ''}`; + appendLog(`Loaded firmware: ${file.name}`); + } + catch (err) { + metaEl.textContent = 'Failed to parse HEX file'; + showToast('error', err instanceof Error ? err.message : String(err)); + } + }); + flashBtn.addEventListener('click', async () => { + if (!selectedFirmware) { + showToast('error', 'Select a .hex firmware file first'); + return; + } + if (state.demoMode) { + showToast('error', 'Firmware flashing is not available in demo mode'); + return; + } + if (state.baudRate !== 115200) { + showToast('warning', 'For flashing, reconnect at 115200 baud'); + return; + } + const transport = getTransport(); + if (!transport?.isConnected) { + showToast('error', 'Not connected'); + return; + } + const sik = getSikClient(); + if (!sik) { + showToast('error', 'SiK client not available'); + return; + } + flashBtn.disabled = true; + flashBtn.textContent = 'Flashing...'; + progressEl.value = 0; + progressText.textContent = 'Starting...'; + let boot = null; + try { + boot = new BootloaderClient(transport, appendLog); + setProgress({ phase: 'sync', completed: 0, total: selectedFirmware.totalBytes }); + let synced = await boot.sync(); + if (!synced) { + appendLog('No bootloader sync; trying AT&UPDATE...'); + const inCmd = await sik.enterCommandMode().catch(() => false); + if (!inCmd) { + throw new Error('Could not enter command mode for AT&UPDATE'); + } + await transport.write('AT&UPDATE\r\n'); + await new Promise((r) => setTimeout(r, 900)); + synced = await boot.sync(); + } + if (!synced) { + throw new Error('Failed to sync bootloader. Put radio in bootloader mode and retry.'); + } + await boot.identify(); + await boot.flash(selectedFirmware, { + verify: verifyEl.checked, + onProgress: setProgress, + }); + showToast('success', 'Firmware flash complete'); + appendLog('Flash completed successfully'); + } + catch (err) { + showToast('error', err instanceof Error ? err.message : String(err)); + appendLog(`Flash failed: ${err instanceof Error ? err.message : String(err)}`); + } + finally { + boot?.dispose(); + flashBtn.disabled = false; + flashBtn.textContent = 'Flash Firmware'; + } + }); +} +//# sourceMappingURL=firmware.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/ui/firmware.js.map b/SiKRadioTools/dist/ui/firmware.js.map new file mode 100644 index 00000000..508fa876 --- /dev/null +++ b/SiKRadioTools/dist/ui/firmware.js.map @@ -0,0 +1 @@ +{"version":3,"file":"firmware.js","sourceRoot":"","sources":["../../src/ui/firmware.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/SiKRadioTools/dist/ui/profiles.js b/SiKRadioTools/dist/ui/profiles.js new file mode 100644 index 00000000..5cbae75f --- /dev/null +++ b/SiKRadioTools/dist/ui/profiles.js @@ -0,0 +1,126 @@ +/** + * Profiles tab - save/load/compare config profiles + */ +import { listProfiles, saveProfile, deleteProfile, loadProfile, exportProfileToJSON, importProfileFromJSON, } from '../persistence/profiles.js'; +import { getDefaultParams } from '../params/mapper.js'; +import { getCurrentParams } from './app.js'; +import { showToast } from './toast.js'; +export function renderProfilesTab(container, _state) { + container.innerHTML = ` +
+

Saved Profiles

+
+
+ + +
+
+
+

Example Profiles

+

Starter configurations for common use cases.

+
+
+ `; + const exampleProfiles = [ + { name: '900MHz US Default', params: { ...getDefaultParams(), MIN_FREQ: 915000, MAX_FREQ: 928000 } }, + { name: '900MHz Long Range', params: { ...getDefaultParams(), AIR_SPEED: 32, TXPOWER: 20, MIN_FREQ: 915000, MAX_FREQ: 928000 } }, + { name: '433MHz EU', params: { ...getDefaultParams(), MIN_FREQ: 414000, MAX_FREQ: 454000 } }, + ]; + const exampleEl = document.getElementById('example-profiles'); + exampleEl.innerHTML = exampleProfiles + .map((p) => ` +
+ ${p.name} + +
+ `) + .join(''); + exampleEl.querySelectorAll('[data-load-example]').forEach((btn) => { + btn.addEventListener('click', () => { + showToast('info', 'Import this profile in Profiles tab, then load in Settings'); + }); + }); + const refreshList = async () => { + const profiles = await listProfiles(); + const listEl = document.getElementById('profile-list'); + listEl.innerHTML = + profiles.length === 0 + ? '

No saved profiles yet.

' + : profiles + .map((p) => ` +
+ ${p.name} + + + +
+ `) + .join(''); + listEl.querySelectorAll('[data-load]').forEach((btn) => { + btn.addEventListener('click', async () => { + const params = await loadProfile(btn.dataset.load); + if (params) { + showToast('success', 'Profile loaded. Apply in Settings tab.'); + // Could use a custom event to pass params to settings + } + }); + }); + listEl.querySelectorAll('[data-export]').forEach((btn) => { + btn.addEventListener('click', async () => { + const id = btn.dataset.export; + const profiles = await listProfiles(); + const p = profiles.find((x) => x.id === id); + if (p) { + const json = exportProfileToJSON(p); + const blob = new Blob([json], { type: 'application/json' }); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = `sik-profile-${p.name.replace(/\s/g, '-')}.json`; + a.click(); + URL.revokeObjectURL(a.href); + } + }); + }); + listEl.querySelectorAll('[data-delete]').forEach((btn) => { + btn.addEventListener('click', async () => { + if (!confirm('Delete this profile?')) + return; + await deleteProfile(btn.dataset.delete); + refreshList(); + showToast('info', 'Profile deleted'); + }); + }); + }; + refreshList(); + document.getElementById('btn-save-new')?.addEventListener('click', async () => { + const name = prompt('Profile name:'); + if (!name) + return; + const params = Object.keys(getCurrentParams()).length > 0 ? getCurrentParams() : getDefaultParams(); + await saveProfile(name, params); + refreshList(); + showToast('success', 'Profile saved'); + }); + document.getElementById('btn-import-profile')?.addEventListener('click', () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.onchange = async () => { + const file = input.files?.[0]; + if (!file) + return; + try { + const text = await file.text(); + const { name, params } = importProfileFromJSON(text); + await saveProfile(name, params); + refreshList(); + showToast('success', `Imported: ${name}`); + } + catch (err) { + showToast('error', err instanceof Error ? err.message : String(err)); + } + }; + input.click(); + }); +} +//# sourceMappingURL=profiles.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/ui/profiles.js.map b/SiKRadioTools/dist/ui/profiles.js.map new file mode 100644 index 00000000..bfe37bc1 --- /dev/null +++ b/SiKRadioTools/dist/ui/profiles.js.map @@ -0,0 +1 @@ +{"version":3,"file":"profiles.js","sourceRoot":"","sources":["../../src/ui/profiles.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/SiKRadioTools/dist/ui/settings.js b/SiKRadioTools/dist/ui/settings.js new file mode 100644 index 00000000..a1383c38 --- /dev/null +++ b/SiKRadioTools/dist/ui/settings.js @@ -0,0 +1,229 @@ +/** + * Settings tab - schema-driven parameter editor + */ +import { SIK_PARAM_SCHEMA, keyToRegister } from '../params/schema.js'; +import { ati5ToParams, getDefaultParams, sanitizeParams, } from '../params/mapper.js'; +import { getSikClient, setCurrentParams } from './app.js'; +import { showToast } from './toast.js'; +let localParams = {}; +export function renderSettingsTab(container, state) { + const hasLoadedParams = state.currentParams && Object.keys(state.currentParams).length > 0; + localParams = hasLoadedParams ? { ...state.currentParams } : { ...getDefaultParams() }; + setCurrentParams(localParams); + container.innerHTML = ` +
+

Radio Parameters

+
+ + + + + + +
+
+ +
+ `; + renderParamGrid(container.querySelector('#param-grid')); + bindSettingsActions(container, state); +} +function renderParamGrid(gridEl, params) { + const p = params ?? localParams; + gridEl.innerHTML = SIK_PARAM_SCHEMA.map((def) => { + const val = p[def.key] ?? def.default; + const inputHtml = def.type === 'enum' + ? `` + : ``; + return ` +
+ + ${inputHtml} + ${def.description} +
+ `; + }).join(''); + gridEl.querySelectorAll('input, select').forEach((el) => { + el.addEventListener('change', () => { + const key = el.dataset.param; + const val = el instanceof HTMLSelectElement + ? (el.value.match(/^\d+$/) ? parseInt(el.value, 10) : el.value) + : el.value; + localParams[key] = typeof val === 'string' && /^\d+$/.test(val) ? parseInt(val, 10) : val; + setCurrentParams(localParams); + document.getElementById('btn-save').disabled = false; + }); + }); +} +/** Update each visible input/select in-place without rebuilding grid HTML */ +function applyParamsToGrid(params) { + for (const def of SIK_PARAM_SCHEMA) { + const val = params[def.key] ?? def.default; + const el = document.querySelector(`input[data-param="${def.key}"], select[data-param="${def.key}"]`); + if (el) { + el.value = String(val); + // For localParams keep in sync + localParams[def.key] = typeof val === 'number' ? val : (typeof val === 'string' && /^\d+$/.test(val) ? parseInt(val, 10) : val); + } + } +} +function bindSettingsActions(container, _state) { + container.querySelector('#btn-load')?.addEventListener('click', async () => { + const client = getSikClient(); + if (!client) { + showToast('error', 'Not connected'); + return; + } + const loadBtn = container.querySelector('#btn-load'); + const saveBtn = container.querySelector('#btn-save'); + loadBtn.disabled = true; + loadBtn.textContent = 'Loading...'; + try { + let params; + try { + showToast('info', 'Sending ATI5…'); + params = await client.readAllParameters(); + } + catch { + showToast('info', 'Entering command mode, then ATI5…'); + const cmdOk = await client.enterCommandMode(); + if (!cmdOk) + throw new Error('Failed to enter command mode'); + params = await client.readAllParameters(); + await client.exitCommandMode().catch(() => { }); + } + const loaded = ati5ToParams(params); + const rawCount = Object.keys(params).length; + if (rawCount === 0) + throw new Error('Radio returned no parameters'); + localParams = { ...getDefaultParams(), ...loaded }; + setCurrentParams(localParams); + const gridEl = container.querySelector('#param-grid'); + if (gridEl) + applyParamsToGrid(localParams); + if (saveBtn) + saveBtn.disabled = true; + const count = Object.keys(loaded).length; + showToast('success', `Loaded ${count} params from radio (NETID=${localParams.NETID})`); + } + catch (err) { + const msg = err instanceof Error ? err.message : String(err); + if (/timeout|did not respond/i.test(msg)) { + showToast('error', 'Radio did not respond. Enter command mode first: Terminal → "Enter Cmd Mode", then Load from Radio again.'); + } + else { + showToast('error', msg); + } + } + finally { + loadBtn.disabled = false; + loadBtn.textContent = 'Load from Radio'; + } + }); + container.querySelector('#btn-save')?.addEventListener('click', async () => { + const client = getSikClient(); + if (!client) { + showToast('error', 'Not connected'); + return; + } + const saveBtn = container.querySelector('#btn-save'); + saveBtn.disabled = true; + saveBtn.textContent = 'Saving...'; + try { + const ok = await client.enterCommandMode(); + if (!ok) + throw new Error('Failed to enter command mode'); + for (const def of SIK_PARAM_SCHEMA) { + const reg = keyToRegister(def.key); + const val = localParams[def.key]; + if (reg && val !== undefined) { + await client.writeParameter(reg, val); + } + } + await client.saveParameters(); + await client.reboot(); + saveBtn.textContent = 'Save to Radio'; + showToast('success', 'Saved to radio. Rebooting...'); + } + catch (err) { + saveBtn.disabled = false; + saveBtn.textContent = 'Save to Radio'; + showToast('error', err instanceof Error ? err.message : String(err)); + } + }); + container.querySelector('#btn-reset')?.addEventListener('click', () => { + if (!confirm('Reset all parameters to defaults?')) + return; + localParams = { ...getDefaultParams() }; + setCurrentParams(localParams); + renderParamGrid(container.querySelector('#param-grid')); + container.querySelector('#btn-save').disabled = false; + showToast('info', 'Reset to defaults'); + }); + container.querySelector('#btn-export')?.addEventListener('click', () => { + const json = JSON.stringify({ params: localParams, exportedAt: new Date().toISOString() }, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = `sik-config-${Date.now()}.json`; + a.click(); + URL.revokeObjectURL(a.href); + showToast('success', 'Config exported'); + }); + container.querySelector('#btn-import')?.addEventListener('click', () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.onchange = async () => { + const file = input.files?.[0]; + if (!file) + return; + try { + const text = await file.text(); + const data = JSON.parse(text); + if (data.params) { + localParams = sanitizeParams(data.params); + setCurrentParams(localParams); + renderParamGrid(container.querySelector('#param-grid')); + container.querySelector('#btn-save').disabled = false; + showToast('success', 'Config imported'); + } + else { + throw new Error('Invalid config file'); + } + } + catch (err) { + showToast('error', err instanceof Error ? err.message : String(err)); + } + }; + input.click(); + }); + container.querySelector('#btn-clone-remote')?.addEventListener('click', async () => { + const client = getSikClient(); + if (!client) { + showToast('error', 'Not connected'); + return; + } + try { + const ok = await client.enterCommandMode(); + if (!ok) + throw new Error('Failed to enter command mode'); + for (const def of SIK_PARAM_SCHEMA) { + const reg = keyToRegister(def.key); + const val = localParams[def.key]; + if (reg && val !== undefined) { + await client.sendAT(`RT${reg}=${val}`); + } + } + await client.sendAT('RT&W'); + await client.exitCommandMode(); + showToast('success', 'Cloned to remote radio'); + } + catch (err) { + showToast('error', err instanceof Error ? err.message : String(err)); + } + }); +} +//# sourceMappingURL=settings.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/ui/settings.js.map b/SiKRadioTools/dist/ui/settings.js.map new file mode 100644 index 00000000..e3f4f251 --- /dev/null +++ b/SiKRadioTools/dist/ui/settings.js.map @@ -0,0 +1 @@ +{"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/ui/settings.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/SiKRadioTools/dist/ui/terminal.js b/SiKRadioTools/dist/ui/terminal.js new file mode 100644 index 00000000..425e6ee2 --- /dev/null +++ b/SiKRadioTools/dist/ui/terminal.js @@ -0,0 +1,130 @@ +/** + * Terminal tab - AT command console + */ +import { getTransport, getSikClient } from './app.js'; +import { showToast } from './toast.js'; +const MAX_LINES = 500; +const terminalLines = []; +let lineListenerUnsubscribe = null; +/** Always look up the output element live — avoids stale DOM reference after re-renders */ +function getOutputEl() { + return document.getElementById('terminal-output'); +} +function appendLine(type, text) { + const time = new Date().toISOString().slice(11, 23); + terminalLines.push({ type, text, time }); + if (terminalLines.length > MAX_LINES) + terminalLines.shift(); + const output = getOutputEl(); + if (!output) + return; + const lineEl = document.createElement('div'); + lineEl.className = `terminal-line ${type}`; + lineEl.textContent = `[${time}] ${text}`; + output.appendChild(lineEl); + output.scrollTop = output.scrollHeight; +} +export function renderTerminalTab(container, _state) { + // Unsubscribe existing listener before rebuilding DOM + lineListenerUnsubscribe?.(); + lineListenerUnsubscribe = null; + container.innerHTML = ` +
+

AT Command Terminal

+
+
+ + +
+
+ + + + + + + + +
+
+ `; + // Replay terminal history into the freshly-created output element + const output = getOutputEl(); + for (const entry of terminalLines) { + const lineEl = document.createElement('div'); + lineEl.className = `terminal-line ${entry.type}`; + lineEl.textContent = `[${entry.time}] ${entry.text}`; + output.appendChild(lineEl); + } + output.scrollTop = output.scrollHeight; + const input = document.getElementById('terminal-input'); + const sendBtn = document.getElementById('terminal-send'); + const sendCommand = async (cmd) => { + const transport = getTransport(); + if (!transport?.isConnected) { + showToast('error', 'Not connected'); + return; + } + const toSend = cmd.trim(); + if (!toSend) + return; + if (toSend === '+++') { + const client = getSikClient(); + if (client) { + appendLine('tx', '+++'); + try { + showToast('info', 'Waiting for line silence (1.5s)...'); + // enterCommandMode waits the guard time then sends +++ + // The radio's OK will appear via addLineListener automatically + const ok = await client.enterCommandMode(); + if (!ok) { + appendLine('rx', 'ERROR: Failed to enter command mode'); + } + showToast(ok ? 'success' : 'error', ok ? 'Entered command mode' : 'Failed to enter command mode'); + } + catch (err) { + appendLine('rx', `Error: ${err instanceof Error ? err.message : String(err)}`); + showToast('error', err instanceof Error ? err.message : String(err)); + } + return; + } + } + // All other commands (ATI5, ATI, ATO, etc.) — send raw; responses appear via addLineListener + appendLine('tx', toSend); + const line = toSend.endsWith('\r\n') ? toSend : toSend + '\r\n'; + try { + await transport.write(line); + } + catch (err) { + appendLine('rx', `Error: ${err instanceof Error ? err.message : String(err)}`); + } + }; + sendBtn.addEventListener('click', () => { + sendCommand(input.value); + input.value = ''; + }); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + sendCommand(input.value); + input.value = ''; + } + }); + document.querySelectorAll('[data-cmd]').forEach((btn) => { + btn.addEventListener('click', () => { + sendCommand(btn.dataset.cmd ?? ''); + }); + }); + document.getElementById('terminal-copy')?.addEventListener('click', () => { + const text = terminalLines.map((l) => `[${l.time}] ${l.type.toUpperCase()}: ${l.text}`).join('\n'); + navigator.clipboard.writeText(text); + }); + // Subscribe to incoming serial data — listener uses appendLine which looks up output element dynamically + const transport = getTransport(); + if (transport?.isConnected && transport.addLineListener) { + lineListenerUnsubscribe = transport.addLineListener((line) => { + if (line.trim()) + appendLine('rx', line.trim()); + }); + } +} +//# sourceMappingURL=terminal.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/ui/terminal.js.map b/SiKRadioTools/dist/ui/terminal.js.map new file mode 100644 index 00000000..827fd558 --- /dev/null +++ b/SiKRadioTools/dist/ui/terminal.js.map @@ -0,0 +1 @@ +{"version":3,"file":"terminal.js","sourceRoot":"","sources":["../../src/ui/terminal.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/SiKRadioTools/dist/ui/toast.js b/SiKRadioTools/dist/ui/toast.js new file mode 100644 index 00000000..743605cf --- /dev/null +++ b/SiKRadioTools/dist/ui/toast.js @@ -0,0 +1,33 @@ +/** + * Toast notification component + */ +const CONTAINER_ID = 'toast-container'; +function getContainer() { + let el = document.getElementById(CONTAINER_ID); + if (!el) { + el = document.createElement('div'); + el.id = CONTAINER_ID; + el.className = 'toast-container'; + document.body.appendChild(el); + } + return el; +} +export function showToast(type, text, duration = 4000) { + const container = getContainer(); + const id = crypto.randomUUID(); + const el = document.createElement('div'); + el.className = `toast ${type}`; + el.textContent = text; + el.dataset.id = id; + container.appendChild(el); + const remove = () => { + el.style.animation = 'slideIn 0.2s ease reverse'; + setTimeout(() => el.remove(), 200); + }; + const t = setTimeout(remove, duration); + el.addEventListener('click', () => { + clearTimeout(t); + remove(); + }); +} +//# sourceMappingURL=toast.js.map \ No newline at end of file diff --git a/SiKRadioTools/dist/ui/toast.js.map b/SiKRadioTools/dist/ui/toast.js.map new file mode 100644 index 00000000..1a9ff5ea --- /dev/null +++ b/SiKRadioTools/dist/ui/toast.js.map @@ -0,0 +1 @@ +{"version":3,"file":"toast.js","sourceRoot":"","sources":["../../src/ui/toast.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,YAAY,GAAG,iBAAiB,CAAC;AAEvC,SAAS,YAAY;IACnB,IAAI,EAAE,GAAG,QAAQ,CAAC,cAAc,CAAC,YAAY,CAA0B,CAAC;IACxE,IAAI,CAAC,EAAE,EAAE,CAAC;QACR,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACnC,EAAE,CAAC,EAAE,GAAG,YAAY,CAAC;QACrB,EAAE,CAAC,SAAS,GAAG,iBAAiB,CAAC;QACjC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAA0B,EAAE,IAAY,EAAE,QAAQ,GAAG,IAAI;IACjF,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;IACjC,MAAM,EAAE,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;IAC/B,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IACzC,EAAE,CAAC,SAAS,GAAG,SAAS,IAAI,EAAE,CAAC;IAC/B,EAAE,CAAC,WAAW,GAAG,IAAI,CAAC;IACtB,EAAE,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC;IACnB,SAAS,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IAE1B,MAAM,MAAM,GAAG,GAAG,EAAE;QAClB,EAAE,CAAC,KAAK,CAAC,SAAS,GAAG,2BAA2B,CAAC;QACjD,UAAU,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,GAAG,CAAC,CAAC;IACrC,CAAC,CAAC;IAEF,MAAM,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACvC,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;QAChC,YAAY,CAAC,CAAC,CAAC,CAAC;QAChB,MAAM,EAAE,CAAC;IACX,CAAC,CAAC,CAAC;AACL,CAAC"} \ No newline at end of file diff --git a/SiKRadioTools/index.html b/SiKRadioTools/index.html new file mode 100644 index 00000000..11ae0e33 --- /dev/null +++ b/SiKRadioTools/index.html @@ -0,0 +1,14 @@ + + + + + + SiK Radio Tools + + + + +
+ + + diff --git a/images/SiKRadioTools_Icon.png b/images/SiKRadioTools_Icon.png new file mode 100644 index 00000000..5a453c0e Binary files /dev/null and b/images/SiKRadioTools_Icon.png differ diff --git a/index.html b/index.html index 5a676b5f..c8d57eaa 100644 --- a/index.html +++ b/index.html @@ -8,9 +8,9 @@
- +
- +

ArduPilot Web Tools

@@ -109,6 +109,16 @@

DFU Loader

DFU on USB + + + + + + +

SiK Radio Tools

+ Configure SiK telemetry radios over USB using the Web Serial API in your browser. All client-side; no data is uploaded to any server. Supports parameter load/save, terminal, firmware flash (.hex), profiles, and diagnostics. Use Chrome or Edge on desktop. + +