From 897ded34b4afdab1f5424f3eaf6ff24d8a2369a4 Mon Sep 17 00:00:00 2001 From: James McD <34198084+JamesM9@users.noreply.github.com> Date: Sun, 29 Mar 2026 11:10:39 -0400 Subject: [PATCH] WebTools: add SiK Radio Tools static bundle and hub row Made-with: Cursor --- SiKRadioTools/README.md | 18 + SiKRadioTools/dist/app.css | 396 ++++++++++++++++++ SiKRadioTools/dist/app.js | 11 + SiKRadioTools/dist/app.js.map | 1 + SiKRadioTools/dist/assets/icons/icon128.png | Bin 0 -> 25996 bytes SiKRadioTools/dist/assets/icons/icon16.png | Bin 0 -> 25996 bytes SiKRadioTools/dist/assets/icons/icon48.png | Bin 0 -> 25996 bytes .../dist/assets/webtools-hub-tile.png | Bin 0 -> 34164 bytes SiKRadioTools/dist/diagnostics/logger.js | 42 ++ SiKRadioTools/dist/diagnostics/logger.js.map | 1 + SiKRadioTools/dist/params/mapper.js | 95 +++++ SiKRadioTools/dist/params/mapper.js.map | 1 + SiKRadioTools/dist/params/schema.js | 167 ++++++++ SiKRadioTools/dist/params/schema.js.map | 1 + SiKRadioTools/dist/persistence/profiles.js | 50 +++ .../dist/persistence/profiles.js.map | 1 + SiKRadioTools/dist/persistence/storage.js | 41 ++ SiKRadioTools/dist/persistence/storage.js.map | 1 + SiKRadioTools/dist/protocol/at-parser.js | 71 ++++ SiKRadioTools/dist/protocol/at-parser.js.map | 1 + .../dist/protocol/bootloader-client.js | 166 ++++++++ .../dist/protocol/bootloader-client.js.map | 1 + SiKRadioTools/dist/protocol/hex-parser.js | 75 ++++ SiKRadioTools/dist/protocol/hex-parser.js.map | 1 + SiKRadioTools/dist/protocol/line-buffer.js | 38 ++ .../dist/protocol/line-buffer.js.map | 1 + SiKRadioTools/dist/protocol/sik-client.js | 219 ++++++++++ SiKRadioTools/dist/protocol/sik-client.js.map | 1 + SiKRadioTools/dist/transport/index.js | 3 + SiKRadioTools/dist/transport/index.js.map | 1 + SiKRadioTools/dist/transport/mock.js | 116 +++++ SiKRadioTools/dist/transport/mock.js.map | 1 + SiKRadioTools/dist/transport/serial.js | 158 +++++++ SiKRadioTools/dist/transport/serial.js.map | 1 + SiKRadioTools/dist/transport/types.js | 5 + SiKRadioTools/dist/transport/types.js.map | 1 + SiKRadioTools/dist/types.js | 5 + SiKRadioTools/dist/types.js.map | 1 + SiKRadioTools/dist/ui/advanced.js | 83 ++++ SiKRadioTools/dist/ui/advanced.js.map | 1 + SiKRadioTools/dist/ui/app.js | 147 +++++++ SiKRadioTools/dist/ui/app.js.map | 1 + SiKRadioTools/dist/ui/connection.js | 106 +++++ SiKRadioTools/dist/ui/connection.js.map | 1 + SiKRadioTools/dist/ui/diagnostics.js | 37 ++ SiKRadioTools/dist/ui/diagnostics.js.map | 1 + SiKRadioTools/dist/ui/firmware.js | 161 +++++++ SiKRadioTools/dist/ui/firmware.js.map | 1 + SiKRadioTools/dist/ui/profiles.js | 126 ++++++ SiKRadioTools/dist/ui/profiles.js.map | 1 + SiKRadioTools/dist/ui/settings.js | 229 ++++++++++ SiKRadioTools/dist/ui/settings.js.map | 1 + SiKRadioTools/dist/ui/terminal.js | 130 ++++++ SiKRadioTools/dist/ui/terminal.js.map | 1 + SiKRadioTools/dist/ui/toast.js | 33 ++ SiKRadioTools/dist/ui/toast.js.map | 1 + SiKRadioTools/index.html | 14 + images/SiKRadioTools_Icon.png | Bin 0 -> 25996 bytes index.html | 14 +- 59 files changed, 2779 insertions(+), 2 deletions(-) create mode 100644 SiKRadioTools/README.md create mode 100644 SiKRadioTools/dist/app.css create mode 100644 SiKRadioTools/dist/app.js create mode 100644 SiKRadioTools/dist/app.js.map create mode 100644 SiKRadioTools/dist/assets/icons/icon128.png create mode 100644 SiKRadioTools/dist/assets/icons/icon16.png create mode 100644 SiKRadioTools/dist/assets/icons/icon48.png create mode 100644 SiKRadioTools/dist/assets/webtools-hub-tile.png create mode 100644 SiKRadioTools/dist/diagnostics/logger.js create mode 100644 SiKRadioTools/dist/diagnostics/logger.js.map create mode 100644 SiKRadioTools/dist/params/mapper.js create mode 100644 SiKRadioTools/dist/params/mapper.js.map create mode 100644 SiKRadioTools/dist/params/schema.js create mode 100644 SiKRadioTools/dist/params/schema.js.map create mode 100644 SiKRadioTools/dist/persistence/profiles.js create mode 100644 SiKRadioTools/dist/persistence/profiles.js.map create mode 100644 SiKRadioTools/dist/persistence/storage.js create mode 100644 SiKRadioTools/dist/persistence/storage.js.map create mode 100644 SiKRadioTools/dist/protocol/at-parser.js create mode 100644 SiKRadioTools/dist/protocol/at-parser.js.map create mode 100644 SiKRadioTools/dist/protocol/bootloader-client.js create mode 100644 SiKRadioTools/dist/protocol/bootloader-client.js.map create mode 100644 SiKRadioTools/dist/protocol/hex-parser.js create mode 100644 SiKRadioTools/dist/protocol/hex-parser.js.map create mode 100644 SiKRadioTools/dist/protocol/line-buffer.js create mode 100644 SiKRadioTools/dist/protocol/line-buffer.js.map create mode 100644 SiKRadioTools/dist/protocol/sik-client.js create mode 100644 SiKRadioTools/dist/protocol/sik-client.js.map create mode 100644 SiKRadioTools/dist/transport/index.js create mode 100644 SiKRadioTools/dist/transport/index.js.map create mode 100644 SiKRadioTools/dist/transport/mock.js create mode 100644 SiKRadioTools/dist/transport/mock.js.map create mode 100644 SiKRadioTools/dist/transport/serial.js create mode 100644 SiKRadioTools/dist/transport/serial.js.map create mode 100644 SiKRadioTools/dist/transport/types.js create mode 100644 SiKRadioTools/dist/transport/types.js.map create mode 100644 SiKRadioTools/dist/types.js create mode 100644 SiKRadioTools/dist/types.js.map create mode 100644 SiKRadioTools/dist/ui/advanced.js create mode 100644 SiKRadioTools/dist/ui/advanced.js.map create mode 100644 SiKRadioTools/dist/ui/app.js create mode 100644 SiKRadioTools/dist/ui/app.js.map create mode 100644 SiKRadioTools/dist/ui/connection.js create mode 100644 SiKRadioTools/dist/ui/connection.js.map create mode 100644 SiKRadioTools/dist/ui/diagnostics.js create mode 100644 SiKRadioTools/dist/ui/diagnostics.js.map create mode 100644 SiKRadioTools/dist/ui/firmware.js create mode 100644 SiKRadioTools/dist/ui/firmware.js.map create mode 100644 SiKRadioTools/dist/ui/profiles.js create mode 100644 SiKRadioTools/dist/ui/profiles.js.map create mode 100644 SiKRadioTools/dist/ui/settings.js create mode 100644 SiKRadioTools/dist/ui/settings.js.map create mode 100644 SiKRadioTools/dist/ui/terminal.js create mode 100644 SiKRadioTools/dist/ui/terminal.js.map create mode 100644 SiKRadioTools/dist/ui/toast.js create mode 100644 SiKRadioTools/dist/ui/toast.js.map create mode 100644 SiKRadioTools/index.html create mode 100644 images/SiKRadioTools_Icon.png 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 0000000000000000000000000000000000000000..5a453c0e5c610a0888ddb87fcfff3843bda5bf71 GIT binary patch literal 25996 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

ZtSCFpi6lM`rmmEHA4c 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 Y6>PF-+OTHe-#s9h-fi7#sMC}G0VT-LZU6uP literal 0 HcmV?d00001 diff --git a/SiKRadioTools/dist/assets/icons/icon16.png b/SiKRadioTools/dist/assets/icons/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..5a453c0e5c610a0888ddb87fcfff3843bda5bf71 GIT binary patch literal 25996 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

ZtSCFpi6lM`rmmEHA4c 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 Y6>PF-+OTHe-#s9h-fi7#sMC}G0VT-LZU6uP literal 0 HcmV?d00001 diff --git a/SiKRadioTools/dist/assets/icons/icon48.png b/SiKRadioTools/dist/assets/icons/icon48.png new file mode 100644 index 0000000000000000000000000000000000000000..5a453c0e5c610a0888ddb87fcfff3843bda5bf71 GIT binary patch literal 25996 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

ZtSCFpi6lM`rmmEHA4c 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 Y6>PF-+OTHe-#s9h-fi7#sMC}G0VT-LZU6uP literal 0 HcmV?d00001 diff --git a/SiKRadioTools/dist/assets/webtools-hub-tile.png b/SiKRadioTools/dist/assets/webtools-hub-tile.png new file mode 100644 index 0000000000000000000000000000000000000000..216d487c503a694423dcff8cbf3c316fe9b2854c GIT binary patch literal 34164 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 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":"AAAA;;;GAGG;AAyBH,+DAA+D;AAC/D,MAAM,CAAC,MAAM,gBAAgB,GAAe;IAC1C;QACE,GAAG,EAAE,cAAc;QACnB,KAAK,EAAE,cAAc;QACrB,WAAW,EAAE,8CAA8C;QAC3D,IAAI,EAAE,QAAQ;QACd,GAAG,EAAE,CAAC;QACN,GAAG,EAAE,IAAI;QACT,OAAO,EAAE,EAAE;QACX,cAAc,EAAE,IAAI;QACpB,QAAQ,EAAE,OAAO;QACjB,QAAQ,EAAE,IAAI;KACf;IACD;QACE,GAAG,EAAE,WAAW;QAChB,KAAK,EAAE,WAAW;QAClB,WAAW,EAAE,+GAA+G;QAC5H,IAAI,EAAE,QAAQ;QACd,GAAG,EAAE,CAAC;QACN,GAAG,EAAE,GAAG;QACR,OAAO,EAAE,EAAE;QACX,cAAc,EAAE,IAAI;QACpB,QAAQ,EAAE,OAAO;QACjB,QAAQ,EAAE,IAAI;KACf;IACD;QACE,GAAG,EAAE,OAAO;QACZ,KAAK,EAAE,YAAY;QACnB,WAAW,EAAE,mCAAmC;QAChD,IAAI,EAAE,QAAQ;QACd,GAAG,EAAE,CAAC;QACN,GAAG,EAAE,GAAG;QACR,OAAO,EAAE,EAAE;QACX,cAAc,EAAE,IAAI;QACpB,QAAQ,EAAE,OAAO;QACjB,QAAQ,EAAE,IAAI;KACf;IACD;QACE,GAAG,EAAE,SAAS;QACd,KAAK,EAAE,gBAAgB;QACvB,WAAW,EAAE,gDAAgD;QAC7D,IAAI,EAAE,QAAQ;QACd,GAAG,EAAE,CAAC;QACN,GAAG,EAAE,EAAE;QACP,OAAO,EAAE,EAAE;QACX,cAAc,EAAE,KAAK;QACrB,QAAQ,EAAE,OAAO;QACjB,QAAQ,EAAE,IAAI;KACf;IACD;QACE,GAAG,EAAE,KAAK;QACV,KAAK,EAAE,kBAAkB;QACzB,WAAW,EAAE,4EAA4E;QACzF,IAAI,EAAE,MAAM;QACZ,OAAO,EAAE;YACP,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE;YAC1B,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE;SAC1B;QACD,OAAO,EAAE,CAAC;QACV,cAAc,EAAE,IAAI;QACpB,QAAQ,EAAE,OAAO;QACjB,QAAQ,EAAE,IAAI;KACf;IACD;QACE,GAAG,EAAE,SAAS;QACd,KAAK,EAAE,cAAc;QACrB,WAAW,EAAE,6DAA6D;QAC1E,IAAI,EAAE,MAAM;QACZ,OAAO,EAAE;YACP,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE;YAC1B,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE;YAC9B,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE;SACnC;QACD,OAAO,EAAE,CAAC;QACV,cAAc,EAAE,IAAI;QACpB,QAAQ,EAAE,OAAO;QACjB,QAAQ,EAAE,IAAI;KACf;IACD;QACE,GAAG,EAAE,UAAU;QACf,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EAAE,gCAAgC;QAC7C,IAAI,EAAE,QAAQ;QACd,GAAG,EAAE,MAAM;QACX,GAAG,EAAE,MAAM;QACX,OAAO,EAAE,MAAM;QACf,cAAc,EAAE,IAAI;QACpB,QAAQ,EAAE,OAAO;QACjB,QAAQ,EAAE,IAAI;KACf;IACD;QACE,GAAG,EAAE,UAAU;QACf,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EAAE,+CAA+C;QAC5D,IAAI,EAAE,QAAQ;QACd,GAAG,EAAE,MAAM;QACX,GAAG,EAAE,MAAM;QACX,OAAO,EAAE,MAAM;QACf,cAAc,EAAE,IAAI;QACpB,QAAQ,EAAE,OAAO;QACjB,QAAQ,EAAE,IAAI;KACf;IACD;QACE,GAAG,EAAE,cAAc;QACnB,KAAK,EAAE,oBAAoB;QAC3B,WAAW,EAAE,mCAAmC;QAChD,IAAI,EAAE,QAAQ;QACd,GAAG,EAAE,CAAC;QACN,GAAG,EAAE,EAAE;QACP,OAAO,EAAE,EAAE;QACX,cAAc,EAAE,IAAI;QACpB,QAAQ,EAAE,OAAO;QACjB,QAAQ,EAAE,KAAK;KAChB;IACD;QACE,GAAG,EAAE,YAAY;QACjB,KAAK,EAAE,gBAAgB;QACvB,WAAW,EAAE,0DAA0D;QACvE,IAAI,EAAE,QAAQ;QACd,GAAG,EAAE,CAAC;QACN,GAAG,EAAE,GAAG;QACR,OAAO,EAAE,GAAG;QACZ,cAAc,EAAE,IAAI;QACpB,QAAQ,EAAE,UAAU;QACpB,QAAQ,EAAE,KAAK;KAChB;IACD;QACE,GAAG,EAAE,UAAU;QACf,KAAK,EAAE,UAAU;QACjB,WAAW,EAAE,uDAAuD;QACpE,IAAI,EAAE,QAAQ;QACd,GAAG,EAAE,CAAC;QACN,GAAG,EAAE,GAAG;QACR,OAAO,EAAE,CAAC;QACV,cAAc,EAAE,IAAI;QACpB,QAAQ,EAAE,UAAU;QACpB,QAAQ,EAAE,KAAK;KAChB;IACD;QACE,GAAG,EAAE,YAAY;QACjB,KAAK,EAAE,iBAAiB;QACxB,WAAW,EAAE,gDAAgD;QAC7D,IAAI,EAAE,QAAQ;QACd,GAAG,EAAE,EAAE;QACP,GAAG,EAAE,GAAG;QACR,OAAO,EAAE,GAAG;QACZ,cAAc,EAAE,IAAI;QACpB,QAAQ,EAAE,UAAU;QACpB,QAAQ,EAAE,KAAK;KAChB;CACF,CAAC;AAEF,kDAAkD;AAClD,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,OAAO,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,EAAE,QAAQ,CAAC;AAC/D,CAAC;AAED,kCAAkC;AAClC,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,MAAM,CAAC,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAC9C,MAAM,GAAG,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC,EAAE,CAAC,CAAC;IACjE,OAAO,GAAG,EAAE,GAAG,CAAC;AAClB,CAAC"} \ 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":"AAAA;;;GAGG;AAIH,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;GAgBf,CAAC;AAEJ,MAAM,OAAO,aAAa;IAA1B;QACU,cAAS,GAAuB,EAAE,CAAC;QACnC,kBAAa,GAAgC,IAAI,GAAG,EAAE,CAAC;QACvD,kBAAa,GAAoC,IAAI,GAAG,EAAE,CAAC;QAC3D,iBAAY,GAAG,KAAK,CAAC;QACrB,kBAAa,GAAG,EAAE,CAAC;IA2F7B,CAAC;IAzFC,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3E,CAAC;IAED,YAAY,CAAC,EAAsB;QACjC,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;IACtB,CAAC;IAED,eAAe,CAAC,EAA0B;QACxC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC3B,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,eAAe,CAAC,EAA8B;QAC5C,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC3B,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,WAAW;QACf,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;QAC3B,oCAAoC;IACtC,CAAC;IAED,KAAK,CAAC,kBAAkB;QACtB,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK,CAAC,IAAI;QACR,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;QAC3B,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QACzB,IAAI,CAAC,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;IAClD,CAAC;IAED,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;QAC3B,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;QAC1B,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,IAAyB;QACnC,MAAM,GAAG,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC7E,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;QAE3B,yBAAyB;QACzB,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAE7B,wBAAwB;QACxB,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACvC,IAAI,KAAK,KAAK,KAAK,EAAE,CAAC;YACpB,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACzB,CAAC;aAAM,IAAI,KAAK,KAAK,KAAK,EAAE,CAAC;YAC3B,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACzB,CAAC;aAAM,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK,EAAE,CAAC;YAC7C,IAAI,CAAC,SAAS,CAAC,CAAC,6BAA6B,EAAE,IAAI,CAAC,CAAC,CAAC;QACxD,CAAC;aAAM,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;YAC5B,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QACxC,CAAC;aAAM,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACpC,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QAC/B,CAAC;aAAM,IAAI,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACrC,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACzB,CAAC;aAAM,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;YAC5B,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACzB,CAAC;aAAM,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;YAC5B,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACzB,CAAC;aAAM,IAAI,KAAK,KAAK,KAAK,EAAE,CAAC;YAC3B,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACzB,CAAC;aAAM,IAAI,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAClC,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;IAEO,SAAS,CAAC,KAAe;QAC/B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC;YAC9B,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;YAC7C,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;YACvC,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;YACtD,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;QAChD,CAAC;IACH,CAAC;IAEO,aAAa;QACnB,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC;IAC/D,CAAC;CACF"} \ 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":"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"} \ 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":"AAAA;;GAEG;AAKH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,IAAI,KAAK,GAAa;IACpB,eAAe,EAAE,cAAc;IAC/B,SAAS,EAAE,IAAI;IACf,SAAS,EAAE,IAAI;IACf,QAAQ,EAAE,KAAK;IACf,QAAQ,EAAE,KAAK;IACf,QAAQ,EAAE,KAAK;IACf,SAAS,EAAE,UAAU;IACrB,aAAa,EAAE,EAAE;CAClB,CAAC;AAEF,MAAM,UAAU,QAAQ;IACtB,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,OAA0B;IACjD,KAAK,GAAG,EAAE,GAAG,KAAK,EAAE,GAAG,OAAO,EAAE,CAAC;IACjC,MAAM,EAAE,CAAC;AACX,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,OAAO,KAAK,CAAC,SAA6B,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,OAAO,KAAK,CAAC,SAAkC,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,EAAE,GAAG,KAAK,CAAC,aAAa,EAAE,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,MAAuC;IACtE,KAAK,CAAC,aAAa,GAAG,MAAM,CAAC;AAC/B,CAAC;AAED,MAAM,IAAI,GAAG;IACX,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE;IACrC,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE;IACrC,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE;IACrC,EAAE,EAAE,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,EAAE;IAC3C,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE;IACrC,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE;CACtC,CAAC;AAEF,SAAS,kBAAkB;IACzB,OAAO,OAAO,SAAS,KAAK,WAAW,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC;AAChE,CAAC;AAED,SAAS,MAAM;IACb,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IAEvB,IAAI,CAAC,SAAS,GAAG;MAEb,kBAAkB,EAAE;QAClB,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC;;WAGN;;;;gDAI4C,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;;;;gDAI/B,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;;;;;;QAMvE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,sBAAsB,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,KAAK,WAAW,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;;;;GAIvI,CAAC;IAEF,QAAQ,CAAC,eAAe,CAAC,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;IAE3E,iBAAiB;IACjB,QAAQ,CAAC,cAAc,CAAC,WAAW,CAAC,EAAE,gBAAgB,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;QACrE,MAAM,OAAO,GAAI,CAAC,CAAC,MAA2B,CAAC,OAAO,CAAC;QACvD,IAAI,KAAK,CAAC,eAAe,KAAK,WAAW,EAAE,CAAC;YAC1C,SAAS,CAAC,SAAS,EAAE,kCAAkC,CAAC,CAAC;YACxD,CAAC,CAAC,MAA2B,CAAC,OAAO,GAAG,CAAC,OAAO,CAAC;YAClD,OAAO;QACT,CAAC;QACD,QAAQ,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,cAAc,CAAC,WAAW,CAAC,EAAE,gBAAgB,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;QACrE,MAAM,QAAQ,GAAI,CAAC,CAAC,MAA2B,CAAC,OAAO,CAAC;QACxD,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;QACvB,YAAY,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;QAChD,GAAG,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;YACjC,QAAQ,CAAC,EAAE,SAAS,EAAG,GAAmB,CAAC,OAAO,CAAC,GAAG,IAAI,UAAU,EAAE,CAAC,CAAC;QAC1E,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,uCAAuC;IACvC,MAAM,OAAO,GAAG,QAAQ,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC;IAC1D,IAAI,OAAO,EAAE,CAAC;QACZ,mBAAmB,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;IAChD,CAAC;IAED,MAAM,UAAU,GAAG,QAAQ,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;IAC1D,IAAI,UAAU,EAAE,CAAC;QACf,UAAU,CAAC,SAAS,GAAG,EAAE,CAAC;QAC1B,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC5C,KAAK,CAAC,SAAS,GAAG,kBAAkB,CAAC;QACrC,KAAK,CAAC,EAAE,GAAG,SAAS,KAAK,CAAC,SAAS,EAAE,CAAC;QACtC,UAAU,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAE9B,QAAQ,KAAK,CAAC,SAAS,EAAE,CAAC;YACxB,KAAK,UAAU;gBACb,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;gBAChC,MAAM;YACR,KAAK,UAAU;gBACb,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;gBAChC,MAAM;YACR,KAAK,UAAU;gBACb,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;gBAChC,MAAM;YACR,KAAK,aAAa;gBAChB,oBAAoB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;gBACnC,MAAM;YACR,KAAK,UAAU;gBACb,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;gBAChC,MAAM;YACR,KAAK,UAAU;gBACb,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;gBAChC,MAAM;YACR;gBACE,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;AACH,CAAC;AAED,IAAI,OAAO,GAAuB,IAAI,CAAC;AAEvC,MAAM,UAAU,SAAS,CAAC,IAAiB;IACzC,OAAO,GAAG,IAAI,CAAC;IACf,WAAW,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;QACvB,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;QACzD,MAAM,EAAE,CAAC;IACX,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,OAAO;IACd,OAAO,OAAO,IAAI,QAAQ,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC;AACpE,CAAC"} \ 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":"AAAA;;GAEG;AAGH,OAAO,EACL,YAAY,EACZ,WAAW,EACX,aAAa,EACb,WAAW,EACX,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,MAAM,UAAU,iBAAiB,CAAC,SAAsB,EAAE,MAAgB;IACxE,SAAS,CAAC,SAAS,GAAG;;;;;;;;;;;;;;GAcrB,CAAC;IAEF,MAAM,eAAe,GAAG;QACtB,EAAE,IAAI,EAAE,mBAAmB,EAAE,MAAM,EAAE,EAAE,GAAG,gBAAgB,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE;QACpG,EAAE,IAAI,EAAE,mBAAmB,EAAE,MAAM,EAAE,EAAE,GAAG,gBAAgB,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE;QAChI,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,GAAG,gBAAgB,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE;KAC7F,CAAC;IAEF,MAAM,SAAS,GAAG,QAAQ,CAAC,cAAc,CAAC,kBAAkB,CAAE,CAAC;IAC/D,SAAS,CAAC,SAAS,GAAG,eAAe;SAClC,GAAG,CACF,CAAC,CAAC,EAAE,EAAE,CAAC;;gBAEG,CAAC,CAAC,IAAI;sDACgC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;;GAE3E,CACE;SACA,IAAI,CAAC,EAAE,CAAC,CAAC;IAEZ,SAAS,CAAC,gBAAgB,CAAC,qBAAqB,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;QAChE,GAAG,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;YACjC,SAAS,CAAC,MAAM,EAAE,4DAA4D,CAAC,CAAC;QAClF,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,MAAM,WAAW,GAAG,KAAK,IAAmB,EAAE;QAC5C,MAAM,QAAQ,GAAG,MAAM,YAAY,EAAE,CAAC;QACtC,MAAM,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,cAAc,CAAE,CAAC;QACxD,MAAM,CAAC,SAAS;YACd,QAAQ,CAAC,MAAM,KAAK,CAAC;gBACnB,CAAC,CAAC,iDAAiD;gBACnD,CAAC,CAAC,QAAQ;qBACL,GAAG,CACF,CAAC,CAAC,EAAE,EAAE,CAAC;;kBAEH,CAAC,CAAC,IAAI;kDAC0B,CAAC,CAAC,EAAE;oDACF,CAAC,CAAC,EAAE;+DACO,CAAC,CAAC,EAAE;;OAE5D,CACM;qBACA,IAAI,CAAC,EAAE,CAAC,CAAC;QAElB,MAAM,CAAC,gBAAgB,CAAC,aAAa,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;YACrD,GAAG,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;gBACvC,MAAM,MAAM,GAAG,MAAM,WAAW,CAAE,GAAmB,CAAC,OAAO,CAAC,IAAK,CAAC,CAAC;gBACrE,IAAI,MAAM,EAAE,CAAC;oBACX,SAAS,CAAC,SAAS,EAAE,wCAAwC,CAAC,CAAC;oBAC/D,sDAAsD;gBACxD,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,MAAM,CAAC,gBAAgB,CAAC,eAAe,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;YACvD,GAAG,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;gBACvC,MAAM,EAAE,GAAI,GAAmB,CAAC,OAAO,CAAC,MAAO,CAAC;gBAChD,MAAM,QAAQ,GAAG,MAAM,YAAY,EAAE,CAAC;gBACtC,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;gBAC5C,IAAI,CAAC,EAAE,CAAC;oBACN,MAAM,IAAI,GAAG,mBAAmB,CAAC,CAAC,CAAC,CAAC;oBACpC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,kBAAkB,EAAE,CAAC,CAAC;oBAC5D,MAAM,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;oBACtC,CAAC,CAAC,IAAI,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;oBACnC,CAAC,CAAC,QAAQ,GAAG,eAAe,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC;oBAC9D,CAAC,CAAC,KAAK,EAAE,CAAC;oBACV,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;gBAC9B,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,MAAM,CAAC,gBAAgB,CAAC,eAAe,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;YACvD,GAAG,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;gBACvC,IAAI,CAAC,OAAO,CAAC,sBAAsB,CAAC;oBAAE,OAAO;gBAC7C,MAAM,aAAa,CAAE,GAAmB,CAAC,OAAO,CAAC,MAAO,CAAC,CAAC;gBAC1D,WAAW,EAAE,CAAC;gBACd,SAAS,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;YACvC,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,WAAW,EAAE,CAAC;IAEd,QAAQ,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,gBAAgB,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;QAC5E,MAAM,IAAI,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC;QACrC,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,gBAAgB,EAAE,CAAC;QACpG,MAAM,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAChC,WAAW,EAAE,CAAC;QACd,SAAS,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,cAAc,CAAC,oBAAoB,CAAC,EAAE,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;QAC5E,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAC9C,KAAK,CAAC,IAAI,GAAG,MAAM,CAAC;QACpB,KAAK,CAAC,MAAM,GAAG,OAAO,CAAC;QACvB,KAAK,CAAC,QAAQ,GAAG,KAAK,IAAI,EAAE;YAC1B,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;YAC9B,IAAI,CAAC,IAAI;gBAAE,OAAO;YAClB,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;gBAC/B,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;gBACrD,MAAM,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;gBAChC,WAAW,EAAE,CAAC;gBACd,SAAS,CAAC,SAAS,EAAE,aAAa,IAAI,EAAE,CAAC,CAAC;YAC5C,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,SAAS,CAAC,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YACvE,CAAC;QACH,CAAC,CAAC;QACF,KAAK,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC"} \ 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":"AAAA;;GAEG;AAGH,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,MAAM,SAAS,GAAG,GAAG,CAAC;AACtB,MAAM,aAAa,GAA6D,EAAE,CAAC;AACnF,IAAI,uBAAuB,GAAwB,IAAI,CAAC;AAExD,2FAA2F;AAC3F,SAAS,WAAW;IAClB,OAAO,QAAQ,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC;AACpD,CAAC;AAED,SAAS,UAAU,CAAC,IAAiB,EAAE,IAAY;IACjD,MAAM,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IACpD,aAAa,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,IAAI,aAAa,CAAC,MAAM,GAAG,SAAS;QAAE,aAAa,CAAC,KAAK,EAAE,CAAC;IAC5D,MAAM,MAAM,GAAG,WAAW,EAAE,CAAC;IAC7B,IAAI,CAAC,MAAM;QAAE,OAAO;IACpB,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IAC7C,MAAM,CAAC,SAAS,GAAG,iBAAiB,IAAI,EAAE,CAAC;IAC3C,MAAM,CAAC,WAAW,GAAG,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;IACzC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3B,MAAM,CAAC,SAAS,GAAG,MAAM,CAAC,YAAY,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,SAAsB,EAAE,MAAgB;IACxE,sDAAsD;IACtD,uBAAuB,EAAE,EAAE,CAAC;IAC5B,uBAAuB,GAAG,IAAI,CAAC;IAE/B,SAAS,CAAC,SAAS,GAAG;;;;;;;;;;;;;;;;;;;GAmBrB,CAAC;IAEF,kEAAkE;IAClE,MAAM,MAAM,GAAG,WAAW,EAAG,CAAC;IAC9B,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,CAAC;QAClC,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC7C,MAAM,CAAC,SAAS,GAAG,iBAAiB,KAAK,CAAC,IAAI,EAAE,CAAC;QACjD,MAAM,CAAC,WAAW,GAAG,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,EAAE,CAAC;QACrD,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7B,CAAC;IACD,MAAM,CAAC,SAAS,GAAG,MAAM,CAAC,YAAY,CAAC;IAEvC,MAAM,KAAK,GAAG,QAAQ,CAAC,cAAc,CAAC,gBAAgB,CAAqB,CAAC;IAC5E,MAAM,OAAO,GAAG,QAAQ,CAAC,cAAc,CAAC,eAAe,CAAE,CAAC;IAE1D,MAAM,WAAW,GAAG,KAAK,EAAE,GAAW,EAAiB,EAAE;QACvD,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;QACjC,IAAI,CAAC,SAAS,EAAE,WAAW,EAAE,CAAC;YAC5B,SAAS,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;YACpC,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QAC1B,IAAI,CAAC,MAAM;YAAE,OAAO;QAEpB,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YACrB,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;YAC9B,IAAI,MAAM,EAAE,CAAC;gBACX,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;gBACxB,IAAI,CAAC;oBACH,SAAS,CAAC,MAAM,EAAE,oCAAoC,CAAC,CAAC;oBACxD,uDAAuD;oBACvD,+DAA+D;oBAC/D,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,gBAAgB,EAAE,CAAC;oBAC3C,IAAI,CAAC,EAAE,EAAE,CAAC;wBACR,UAAU,CAAC,IAAI,EAAE,qCAAqC,CAAC,CAAC;oBAC1D,CAAC;oBACD,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,8BAA8B,CAAC,CAAC;gBACpG,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;oBAC/E,SAAS,CAAC,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;gBACvE,CAAC;gBACD,OAAO;YACT,CAAC;QACH,CAAC;QAED,6FAA6F;QAC7F,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACzB,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,MAAM,CAAC;QAChE,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC9B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACjF,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;QACrC,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACzB,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC;IACnB,CAAC,CAAC,CAAC;IAEH,KAAK,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;QACtC,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,EAAE,CAAC;YACtB,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACzB,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC;QACnB,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;QACtD,GAAG,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;YACjC,WAAW,CAAE,GAAmB,CAAC,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;QACtD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,cAAc,CAAC,eAAe,CAAC,EAAE,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;QACvE,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnG,SAAS,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,yGAAyG;IACzG,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;IACjC,IAAI,SAAS,EAAE,WAAW,IAAI,SAAS,CAAC,eAAe,EAAE,CAAC;QACxD,uBAAuB,GAAG,SAAS,CAAC,eAAe,CAAC,CAAC,IAAI,EAAE,EAAE;YAC3D,IAAI,IAAI,CAAC,IAAI,EAAE;gBAAE,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC"} \ 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 0000000000000000000000000000000000000000..5a453c0e5c610a0888ddb87fcfff3843bda5bf71 GIT binary patch literal 25996 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

ZtSCFpi6lM`rmmEHA4c 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 Y6>PF-+OTHe-#s9h-fi7#sMC}G0VT-LZU6uP literal 0 HcmV?d00001 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. + +