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' ) ;
0 commit comments