@@ -123,6 +123,7 @@ const MATCH_ATTRS = [
123123 'vid' , 'pid' , 'serial_number' , 'manufacturer' , 'product' , 'location'
124124] ;
125125const PROTOCOLS = [ 'TCP' , 'TELNET' , 'SSL' , 'SOCKET' ] ;
126+ const CONTROL_SIGNALS = [ 'rts' , 'dtr' , 'cts' , 'dsr' , 'ri' , 'cd' ] ;
126127const BAUDRATES = [ 300 , 1200 , 2400 , 4800 , 9600 , 19200 , 38400 , 57600 , 115200 ,
127128 230400 , 460800 , 921600 ] ;
128129const BYTESIZES = { 8 : 'EIGHTBITS' , 7 : 'SEVENBITS' , 6 : 'SIXBITS' , 5 : 'FIVEBITS' } ;
@@ -220,6 +221,30 @@ function renderPortCard(port, index) {
220221 if ( ser . baudrate ) info += ser . baudrate + ' \u2014 ' ;
221222 info += connected ? 'connected' : 'disconnected' ;
222223 div . appendChild ( el ( 'p' , info ) ) ;
224+ // Signal indicators (clickable for RTS/DTR)
225+ if ( port . signals ) {
226+ const sigDiv = el ( 'div' , null , 'signal-indicators' ) ;
227+ CONTROL_SIGNALS . forEach ( sig => {
228+ const on = port . signals [ sig ] ;
229+ const clickable = sig === 'rts' || sig === 'dtr' ;
230+ const badge = el ( 'span' , sig . toUpperCase ( ) ,
231+ 'signal-badge ' + ( on ? 'signal-on' : 'signal-off' )
232+ + ( clickable ? ' signal-click' : '' ) ) ;
233+ if ( clickable ) {
234+ badge . title = sig . toUpperCase ( ) + ': click to toggle' ;
235+ badge . onclick = ( ) => {
236+ badge . classList . add ( 'signal-busy' ) ;
237+ api ( 'PUT' , '/api/ports/' + index + '/signals' ,
238+ { [ sig ] : ! on } ) . then ( ( ) => loadPorts ( ) ) . catch ( e => {
239+ badge . classList . remove ( 'signal-busy' ) ;
240+ if ( e !== 'unauthorized' ) alert ( e ) ;
241+ } ) ;
242+ } ;
243+ }
244+ sigDiv . appendChild ( badge ) ;
245+ } ) ;
246+ div . appendChild ( sigDiv ) ;
247+ }
223248 // Show configured port or match
224249 if ( ser . match ) {
225250 const matchStr = Object . entries ( ser . match )
@@ -232,7 +257,18 @@ function renderPortCard(port, index) {
232257 ( port . servers || [ ] ) . forEach ( ( s , si ) => {
233258 const proto = ( s . protocol || 'tcp' ) . toUpperCase ( ) ;
234259 const addr = proto === 'SOCKET' ? s . address : s . address + ':' + s . port ;
235- const li = el ( 'li' , proto + ' \u2014 ' + addr ) ;
260+ const li = el ( 'li' ) ;
261+ li . appendChild ( document . createTextNode ( proto + ' \u2014 ' + addr ) ) ;
262+ if ( s . control ) {
263+ const parts = [ ] ;
264+ if ( s . control . rts ) parts . push ( 'RTS' ) ;
265+ if ( s . control . dtr ) parts . push ( 'DTR' ) ;
266+ if ( s . control . signals && s . control . signals . length )
267+ parts . push ( 'report: ' + s . control . signals . map (
268+ s => s . toUpperCase ( ) ) . join ( ', ' ) ) ;
269+ const label = parts . length ? parts . join ( ' | ' ) : 'escape only' ;
270+ li . appendChild ( el ( 'div' , 'ctrl: ' + label , 'control-signals' ) ) ;
271+ }
236272 const clients = s . connections || [ ] ;
237273 if ( clients . length ) {
238274 const cul = el ( 'ul' ) ;
@@ -252,7 +288,7 @@ function renderPortCard(port, index) {
252288 } ) ;
253289 li . appendChild ( cul ) ;
254290 } else {
255- li . appendChild ( el ( 'em' , ' no connections' ) ) ;
291+ li . appendChild ( el ( 'em' , ' no connections' , 'empty' ) ) ;
256292 }
257293 ul . appendChild ( li ) ;
258294 } ) ;
@@ -374,6 +410,7 @@ function buildConfigFromStatus(port) {
374410 } ;
375411 if ( s . port !== undefined ) srv . port = s . port ;
376412 if ( s . ssl ) srv . ssl = s . ssl ;
413+ if ( s . control ) srv . control = s . control ;
377414 return srv ;
378415 } ) ;
379416 if ( ! config . servers . length ) {
@@ -698,15 +735,108 @@ function renderServerBox(srv, index, total) {
698735 } ) ;
699736 box . appendChild ( sslDiv ) ;
700737
738+ // Control section
739+ const ctlDiv = el ( 'div' ) ;
740+ ctlDiv . className = 'srv-control-fields' ;
741+ const ctl = srv . control || null ;
742+ // Enable checkbox
743+ const ctlEnableRow = el ( 'div' , null , 'field-row' ) ;
744+ const ctlEnableLbl = document . createElement ( 'label' ) ;
745+ ctlEnableLbl . className = 'ctl-signal-label' ;
746+ const ctlEnableCb = document . createElement ( 'input' ) ;
747+ ctlEnableCb . type = 'checkbox' ;
748+ ctlEnableCb . className = 'ctl-enable' ;
749+ ctlEnableCb . checked = ! ! ctl ;
750+ ctlEnableLbl . appendChild ( ctlEnableCb ) ;
751+ ctlEnableLbl . appendChild ( document . createTextNode ( ' Control protocol' ) ) ;
752+ ctlEnableRow . appendChild ( ctlEnableLbl ) ;
753+ ctlDiv . appendChild ( ctlEnableRow ) ;
754+ // Control details (shown when enabled)
755+ const ctlDetails = el ( 'div' ) ;
756+ ctlDetails . className = 'ctl-details' ;
757+ // Protocol description
758+ const ctlDesc = el ( 'p' ,
759+ 'Binary escape protocol using 0xFF prefix. '
760+ + 'All 0xFF bytes in data are escaped (FF FF). ' ,
761+ 'ctl-desc' ) ;
762+ const ctlMoreBtn = el ( 'a' , 'Protocol reference' ) ;
763+ ctlMoreBtn . href = '#' ;
764+ ctlMoreBtn . className = 'detect-link' ;
765+ ctlMoreBtn . onclick = e => {
766+ e . preventDefault ( ) ;
767+ const dlg = $ ( 'ctl-protocol-dlg' ) ;
768+ dlg . classList . toggle ( 'hidden' ) ;
769+ } ;
770+ ctlDesc . appendChild ( ctlMoreBtn ) ;
771+ ctlDetails . appendChild ( ctlDesc ) ;
772+ // RTS/DTR write enable
773+ const ctlWriteRow = el ( 'div' , null , 'field-row' ) ;
774+ ctlWriteRow . appendChild ( el ( 'label' , 'Allow set:' ) ) ;
775+ [ 'rts' , 'dtr' ] . forEach ( sig => {
776+ const lbl = document . createElement ( 'label' ) ;
777+ lbl . className = 'ctl-signal-label' ;
778+ const cb = document . createElement ( 'input' ) ;
779+ cb . type = 'checkbox' ;
780+ cb . className = 'ctl-write' ;
781+ cb . dataset . signal = sig ;
782+ cb . checked = ctl ? ! ! ctl [ sig ] : false ;
783+ lbl . appendChild ( cb ) ;
784+ lbl . appendChild ( document . createTextNode ( ' ' + sig . toUpperCase ( ) ) ) ;
785+ ctlWriteRow . appendChild ( lbl ) ;
786+ } ) ;
787+ ctlDetails . appendChild ( ctlWriteRow ) ;
788+ // Report signals
789+ const ctlSigRow = el ( 'div' , null , 'field-row' ) ;
790+ ctlSigRow . appendChild ( el ( 'label' , 'Report:' ) ) ;
791+ const ctlSignals = ctl ? ( ctl . signals || [ ] ) : [ ] ;
792+ CONTROL_SIGNALS . forEach ( sig => {
793+ const lbl = document . createElement ( 'label' ) ;
794+ lbl . className = 'ctl-signal-label' ;
795+ const cb = document . createElement ( 'input' ) ;
796+ cb . type = 'checkbox' ;
797+ cb . className = 'ctl-signal' ;
798+ cb . dataset . signal = sig ;
799+ cb . checked = ctlSignals . includes ( sig ) ;
800+ lbl . appendChild ( cb ) ;
801+ lbl . appendChild ( document . createTextNode ( ' ' + sig . toUpperCase ( ) ) ) ;
802+ ctlSigRow . appendChild ( lbl ) ;
803+ } ) ;
804+ ctlDetails . appendChild ( ctlSigRow ) ;
805+ // Poll interval
806+ const pollRow = el ( 'div' , null , 'field-row' ) ;
807+ pollRow . appendChild ( el ( 'label' , 'Poll interval:' ) ) ;
808+ const pollSel = document . createElement ( 'select' ) ;
809+ pollSel . className = 'ctl-poll-interval' ;
810+ const pollOptions = [ 1 , 2 , 5 , 10 , 20 , 50 , 100 , 200 , 500 , 1000 ] ;
811+ const curPoll = ctl ? Math . round ( ( ctl . poll_interval || 0.1 ) * 1000 ) : 100 ;
812+ pollOptions . forEach ( ms => {
813+ const opt = document . createElement ( 'option' ) ;
814+ opt . value = ms ;
815+ opt . textContent = ms < 1000 ? ms + ' ms' : ( ms / 1000 ) + ' s' ;
816+ if ( ms === curPoll ) opt . selected = true ;
817+ pollSel . appendChild ( opt ) ;
818+ } ) ;
819+ pollRow . appendChild ( pollSel ) ;
820+ ctlDetails . appendChild ( pollRow ) ;
821+ ctlDiv . appendChild ( ctlDetails ) ;
822+ const updateCtlVisibility = ( ) => {
823+ ctlDetails . classList . toggle ( 'hidden' , ! ctlEnableCb . checked ) ;
824+ } ;
825+ ctlEnableCb . onchange = updateCtlVisibility ;
826+ updateCtlVisibility ( ) ;
827+ box . appendChild ( ctlDiv ) ;
828+
701829 // Update visibility based on protocol
702830 const updateProtoFields = ( ) => {
703831 const proto = protoSel . value ;
704832 const isSocket = proto === 'SOCKET' ;
705833 const isSsl = proto === 'SSL' ;
834+ const isTelnet = proto === 'TELNET' ;
706835 addrLabel . textContent = isSocket ? 'Path:' : 'Address:' ;
707836 portLabel . classList . toggle ( 'hidden' , isSocket ) ;
708837 portInput . classList . toggle ( 'hidden' , isSocket ) ;
709838 sslDiv . classList . toggle ( 'hidden' , ! isSsl ) ;
839+ ctlDiv . classList . toggle ( 'hidden' , isTelnet ) ;
710840 if ( isSocket ) {
711841 addrInput . value = addrInput . value === '0.0.0.0' ? '' : addrInput . value ;
712842 }
@@ -815,6 +945,18 @@ function collectConfig() {
815945 if ( cacerts ) ssl . ca_certs = cacerts ;
816946 if ( Object . keys ( ssl ) . length ) srv . ssl = ssl ;
817947 }
948+ if ( proto !== 'telnet' && box . querySelector ( '.ctl-enable' ) . checked ) {
949+ const ctl = { } ;
950+ box . querySelectorAll ( '.ctl-write:checked' ) . forEach (
951+ cb => { ctl [ cb . dataset . signal ] = true ; } ) ;
952+ const signals = [ ] ;
953+ box . querySelectorAll ( '.ctl-signal:checked' ) . forEach (
954+ cb => signals . push ( cb . dataset . signal ) ) ;
955+ if ( signals . length ) ctl . signals = signals ;
956+ const pollMs = parseInt ( box . querySelector ( '.ctl-poll-interval' ) . value ) ;
957+ if ( pollMs ) ctl . poll_interval = pollMs / 1000 ;
958+ srv . control = ctl ;
959+ }
818960 config . servers . push ( srv ) ;
819961 } ) ;
820962
0 commit comments