Skip to content

Commit 1f531d9

Browse files
committed
Add Messenger to Composer
1 parent 353253b commit 1f531d9

File tree

2 files changed

+158
-1
lines changed

2 files changed

+158
-1
lines changed

composer.html

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,26 @@
7070
</details>
7171
</div>
7272

73+
<div id="composer-messenger" class="commands-section page-section">
74+
<details class="section section-card">
75+
<summary class="section-card-title"><span class="icon messages" aria-hidden="true"></span>Messenger</summary>
76+
<div class="section-card-body messenger-card">
77+
<div class="messenger-meta">
78+
<span id="composer-messenger-port" class="messenger-pill">Port: -</span>
79+
<span class="messenger-pill">Payload: length + ASCII bytes</span>
80+
</div>
81+
<div class="messenger-input">
82+
<textarea id="composer-messenger-text" rows="2" placeholder="Type a message (ASCII, max 46)" oninput="updateComposerMessengerPayload()"></textarea>
83+
<span id="composer-messenger-count" class="messenger-limit messenger-limit-centered">0 / 46</span>
84+
<div class="messenger-send-row">
85+
<input type="text" id="composer-messenger-hex" class="messenger-payload-output" readonly value="(no message)" />
86+
</div>
87+
<div id="composer-messenger-error" class="input-helper"></div>
88+
</div>
89+
</div>
90+
</details>
91+
</div>
92+
7393
<div id="floating-bar">
7494
<select id="payload-type" onchange="updateEncodedMessage()">
7595
<option value="none">Payload type</option>
@@ -142,11 +162,14 @@ <h3 id="import-preview-title">Import preview</h3>
142162
await loadSettings(selectedFile);
143163
displaySettings();
144164
displayCommands();
165+
updateComposerMessengerMeta();
166+
updateComposerMessengerPayload();
145167
updateEncodedMessage(); // Update once shown
146168
}
147169

148170
const credentialSettingIds = new Set([0x10, 0x11, 0x12, 0x21, 0x2a, 0x44, 0x45, 0x46]);
149171
let lastPayloadType = 'none';
172+
const COMPOSER_MESSAGING_MAX_LEN = 46;
150173

151174
function isCredentialSetting(setting) {
152175
if (!setting || !setting.id) {
@@ -244,6 +267,99 @@ <h3 id="import-preview-title">Import preview</h3>
244267
downloadSettingsJson(settings, safeFilename);
245268
}
246269

270+
function updateComposerMessengerMeta() {
271+
const portEl = document.getElementById('composer-messenger-port');
272+
if (!portEl) {
273+
return;
274+
}
275+
const port = settingsData && settingsData.ports ? settingsData.ports.port_lr_messaging : null;
276+
portEl.textContent = `Port: ${Number.isFinite(port) ? port : '-'}`;
277+
}
278+
279+
function setComposerMessengerError(message) {
280+
const errorEl = document.getElementById('composer-messenger-error');
281+
const outputEl = document.getElementById('composer-messenger-hex');
282+
if (errorEl) {
283+
errorEl.textContent = message || '';
284+
}
285+
if (!outputEl) {
286+
return;
287+
}
288+
if (message) {
289+
outputEl.classList.add('invalid');
290+
} else {
291+
outputEl.classList.remove('invalid');
292+
}
293+
}
294+
295+
function getComposerMessengerPayload() {
296+
const input = document.getElementById('composer-messenger-text');
297+
if (!input) {
298+
return { empty: true, error: null, payload: null };
299+
}
300+
const text = (input.value || '').trim();
301+
if (!text) {
302+
return { empty: true, error: null, payload: null };
303+
}
304+
const encoder = new TextEncoder();
305+
const bytes = encoder.encode(text);
306+
if (bytes.some((b) => b > 0x7F)) {
307+
return { empty: false, error: 'Only ASCII characters are supported.', payload: null };
308+
}
309+
if (bytes.length > COMPOSER_MESSAGING_MAX_LEN) {
310+
return { empty: false, error: `Message too long (max ${COMPOSER_MESSAGING_MAX_LEN}).`, payload: null };
311+
}
312+
const payload = new Uint8Array(1 + bytes.length);
313+
payload[0] = bytes.length;
314+
payload.set(bytes, 1);
315+
return { empty: false, error: null, payload };
316+
}
317+
318+
function updateComposerMessengerPayload() {
319+
const input = document.getElementById('composer-messenger-text');
320+
const output = document.getElementById('composer-messenger-hex');
321+
const countEl = document.getElementById('composer-messenger-count');
322+
323+
if (!input || !output || !countEl) {
324+
return;
325+
}
326+
327+
const text = input.value || '';
328+
const encoder = new TextEncoder();
329+
const bytes = encoder.encode(text);
330+
331+
countEl.textContent = `${bytes.length} / ${COMPOSER_MESSAGING_MAX_LEN}`;
332+
countEl.style.color = bytes.length > COMPOSER_MESSAGING_MAX_LEN ? '#b42318' : '#7c8797';
333+
334+
if (!text.trim()) {
335+
output.value = '(no message)';
336+
setComposerMessengerError('');
337+
updateEncodedMessage();
338+
return;
339+
}
340+
341+
if (bytes.some((b) => b > 0x7F)) {
342+
output.value = '(invalid payload)';
343+
setComposerMessengerError('Only ASCII characters are supported.');
344+
updateEncodedMessage();
345+
return;
346+
}
347+
348+
if (bytes.length > COMPOSER_MESSAGING_MAX_LEN) {
349+
output.value = '(invalid payload)';
350+
setComposerMessengerError(`Message too long (max ${COMPOSER_MESSAGING_MAX_LEN}).`);
351+
updateEncodedMessage();
352+
return;
353+
}
354+
355+
const payload = new Uint8Array(1 + bytes.length);
356+
payload[0] = bytes.length;
357+
payload.set(bytes, 1);
358+
output.value = bytesToHex(payload);
359+
setComposerMessengerError('');
360+
updateEncodedMessage();
361+
}
362+
247363
function showImportPreviewModal({ title, subtitle, rows, note }) {
248364
return new Promise((resolve) => {
249365
const overlay = document.getElementById('import-preview-overlay');
@@ -629,6 +745,7 @@ <h3 id="import-preview-title">Import preview</h3>
629745

630746
const encodedBytes = [];
631747
hexOutput.classList.remove('invalid');
748+
const messengerPayload = getComposerMessengerPayload();
632749

633750
const types = new Set();
634751
// Iterate each known setting
@@ -727,7 +844,21 @@ <h3 id="import-preview-title">Import preview</h3>
727844

728845
// If nothing is selected
729846
if (encodedBytes.length === 0) {
730-
hexOutput.value = '(no settings selected)';
847+
if (!messengerPayload.empty) {
848+
if (messengerPayload.error) {
849+
hexOutput.value = `Error (messenger): ${messengerPayload.error}`;
850+
hexOutput.classList.add('invalid');
851+
return;
852+
}
853+
encodedBytes.push(...messengerPayload.payload);
854+
types.add('messenger');
855+
} else {
856+
hexOutput.value = '(no settings selected)';
857+
return;
858+
}
859+
} else if (!messengerPayload.empty) {
860+
hexOutput.value = 'Error: Messenger payload cannot be combined with settings or commands';
861+
hexOutput.classList.add('invalid');
731862
return;
732863
}
733864

@@ -736,6 +867,13 @@ <h3 id="import-preview-title">Import preview</h3>
736867
port = 3;
737868
} else if (types.has("command")) {
738869
port = 32;
870+
} else if (types.has("messenger")) {
871+
port = settingsData && settingsData.ports ? settingsData.ports.port_lr_messaging : null;
872+
if (!Number.isFinite(port)) {
873+
hexOutput.value = 'Error: No LoRaWAN messaging port found';
874+
hexOutput.classList.add('invalid');
875+
return;
876+
}
739877
} else {
740878
hexOutput.value = 'Error: Unknown type ' + types;
741879
hexOutput.classList.add('invalid');

style.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ body.connected #header-section {
9595
}
9696

9797
#composer-commands {
98+
padding-bottom: 0;
99+
}
100+
101+
#composer-messenger {
98102
padding-bottom: 60px;
99103
}
100104

@@ -984,6 +988,21 @@ body.connected #commands-section {
984988
gap: 12px;
985989
}
986990

991+
.messenger-payload-output {
992+
width: 100%;
993+
height: 40px;
994+
padding: 0 10px;
995+
border-radius: var(--radius-sm);
996+
border: 1px solid rgba(31, 42, 35, 0.2);
997+
background: #fff;
998+
color: #000;
999+
font-family: monospace;
1000+
}
1001+
1002+
.messenger-payload-output.invalid {
1003+
border-color: #b42318;
1004+
}
1005+
9871006
.messenger-limit {
9881007
font-size: 12px;
9891008
color: #7c8797;

0 commit comments

Comments
 (0)