2017-08-28 12:27:45 -04:00
/ * *
* @ description Mesh Agent Transport Module - using websocket relay
* @ author Ylian Saint - Hilaire
* @ version v0 . 0.1 f
* /
// Construct a MeshServer agent direction object
2019-10-15 18:50:11 -04:00
var CreateAgentRedirect = function ( meshserver , module , serverPublicNamePort , authCookie , rauthCookie , domainUrl ) {
2017-08-28 12:27:45 -04:00
var obj = { } ;
obj . m = module ; // This is the inner module (Terminal or Desktop)
module . parent = obj ;
obj . meshserver = meshserver ;
2019-01-28 18:47:54 -05:00
obj . authCookie = authCookie ;
2019-10-15 18:50:11 -04:00
obj . rauthCookie = rauthCookie ;
2020-05-07 02:23:27 -04:00
obj . State = 0 ; // 0 = Disconnected, 1 = Connected, 2 = Connected to server, 3 = End-to-end connection.
2018-02-11 20:13:26 -05:00
obj . nodeid = null ;
2019-12-12 20:45:42 -05:00
obj . options = null ;
2017-08-28 12:27:45 -04:00
obj . socket = null ;
obj . connectstate = - 1 ;
obj . tunnelid = Math . random ( ) . toString ( 36 ) . substring ( 2 ) ; // Generate a random client tunnel id
obj . protocol = module . protocol ; // 1 = SOL, 2 = KVM, 3 = IDER, 4 = Files, 5 = FileTransfer
2018-02-11 20:13:26 -05:00
obj . onStateChanged = null ;
obj . ctrlMsgAllowed = true ;
2017-10-15 02:22:19 -04:00
obj . attemptWebRTC = false ;
2018-01-16 20:30:34 -05:00
obj . webRtcActive = false ;
2018-02-05 14:56:29 -05:00
obj . webSwitchOk = false ;
2017-10-15 02:22:19 -04:00
obj . webchannel = null ;
2018-02-11 20:13:26 -05:00
obj . webrtc = null ;
2018-01-09 23:13:41 -05:00
obj . debugmode = 0 ;
2019-08-13 14:49:05 -04:00
obj . serverIsRecording = false ;
2021-05-08 21:09:49 -04:00
obj . urlname = 'meshrelay.ashx' ;
2020-04-14 05:53:40 -04:00
obj . latency = { lastSend : null , current : - 1 , callback : null } ;
2019-05-21 17:19:32 -04:00
if ( domainUrl == null ) { domainUrl = '/' ; }
2017-08-28 12:27:45 -04:00
2019-05-20 19:00:33 -04:00
// Console Message
obj . consoleMessage = null ;
obj . onConsoleMessageChange = null ;
2020-05-07 02:23:27 -04:00
// Session Metadata
obj . metadata = null ;
obj . onMetadataChange = null ;
2017-08-28 12:27:45 -04:00
// Private method
//obj.debug = function (msg) { console.log(msg); }
2021-11-18 15:00:45 -05:00
// Display websocket or webrtc data to the console
function logData ( e , name ) {
if ( typeof e . data == 'object' ) {
var view = new Uint8Array ( e . data ) , cmd = ( view [ 0 ] << 8 ) + view [ 1 ] , cmdsize = ( view [ 2 ] << 8 ) + view [ 3 ] ;
console . log ( name + ' binary data' , cmd , cmdsize , e . data . byteLength , buf2hex ( e . data ) . substring ( 0 , 24 ) ) ;
} else if ( typeof e . data == 'string' ) {
console . log ( name + ' string data' , e . data . length , e . data ) ;
} else {
console . log ( name + ' unknown data' , e . data ) ;
}
}
2017-08-28 12:27:45 -04:00
obj . Start = function ( nodeid ) {
2021-05-08 21:09:49 -04:00
var url2 , url = window . location . protocol . replace ( 'http' , 'ws' ) + '//' + window . location . host + window . location . pathname . substring ( 0 , window . location . pathname . lastIndexOf ( '/' ) ) + '/' + obj . urlname + '?browser=1&p=' + obj . protocol + ( nodeid ? ( '&nodeid=' + nodeid ) : '' ) + '&id=' + obj . tunnelid ;
2019-12-18 15:00:08 -05:00
//if (serverPublicNamePort) { url2 = window.location.protocol.replace('http', 'ws') + '//' + serverPublicNamePort + '/meshrelay.ashx?id=' + obj.tunnelid; } else { url2 = url; }
2019-01-28 18:47:54 -05:00
if ( ( authCookie != null ) && ( authCookie != '' ) ) { url += '&auth=' + authCookie ; }
2020-04-28 16:22:49 -04:00
if ( ( urlargs != null ) && ( urlargs . slowrelay != null ) ) { url += '&slowrelay=' + urlargs . slowrelay ; }
2017-08-28 12:27:45 -04:00
obj . nodeid = nodeid ;
obj . connectstate = 0 ;
obj . socket = new WebSocket ( url ) ;
2020-05-16 17:07:53 -04:00
obj . socket . binaryType = 'arraybuffer' ;
2017-08-28 12:27:45 -04:00
obj . socket . onopen = obj . xxOnSocketConnected ;
obj . socket . onmessage = obj . xxOnMessage ;
2021-11-18 15:00:45 -05:00
//obj.socket.onmessage = function (e) { logData(e, 'WebSocket'); obj.xxOnMessage(e); }
2019-02-20 18:26:27 -05:00
obj . socket . onerror = function ( e ) { /* console.error(e); */ }
2017-08-28 12:27:45 -04:00
obj . socket . onclose = obj . xxOnSocketClosed ;
obj . xxStateChange ( 1 ) ;
2020-08-06 19:05:48 -04:00
if ( obj . meshserver != null ) {
var rurl = '*' + domainUrl + 'meshrelay.ashx?p=' + obj . protocol + '&nodeid=' + nodeid + '&id=' + obj . tunnelid ;
if ( ( rauthCookie != null ) && ( rauthCookie != '' ) ) { rurl += ( '&rauth=' + rauthCookie ) ; }
obj . meshserver . send ( { action : 'msg' , type : 'tunnel' , nodeid : obj . nodeid , value : rurl , usage : obj . protocol } ) ;
//obj.debug('Agent Redir Start: ' + url);
}
2017-08-28 12:27:45 -04:00
}
obj . xxOnSocketConnected = function ( ) {
2018-01-09 23:13:41 -05:00
if ( obj . debugmode == 1 ) { console . log ( 'onSocketConnected' ) ; }
2019-12-18 15:00:08 -05:00
//obj.debug('Agent Redir Socket Connected');
2017-08-28 12:27:45 -04:00
obj . xxStateChange ( 2 ) ;
}
2017-10-15 02:22:19 -04:00
// Called to pass websocket control messages
obj . xxOnControlCommand = function ( msg ) {
2018-01-18 18:43:43 -05:00
var controlMsg ;
try { controlMsg = JSON . parse ( msg ) ; } catch ( e ) { return ; }
2021-11-10 16:21:30 -05:00
if ( controlMsg . ctrlChannel != '102938' ) { if ( obj . m . ProcessData ) { obj . m . ProcessData ( msg ) ; } else { console . log ( msg ) ; } return ; }
2019-12-16 20:20:39 -05:00
if ( ( typeof args != 'undefined' ) && args . redirtrace ) { console . log ( 'RedirRecv' , controlMsg ) ; }
2019-05-20 19:00:33 -04:00
if ( controlMsg . type == 'console' ) {
2020-05-03 17:04:40 -04:00
obj . setConsoleMessage ( controlMsg . msg , controlMsg . msgid , controlMsg . msgargs , controlMsg . timeout ) ;
2020-05-07 02:23:27 -04:00
} else if ( controlMsg . type == 'metadata' ) {
obj . metadata = controlMsg ;
if ( obj . onMetadataChange ) obj . onMetadataChange ( obj . metadata ) ;
2020-04-15 19:20:55 -04:00
} else if ( ( controlMsg . type == 'rtt' ) && ( typeof controlMsg . time == 'number' ) ) {
2020-04-14 01:48:20 -04:00
obj . latency . current = ( new Date ( ) . getTime ( ) ) - controlMsg . time ;
2020-04-14 05:53:40 -04:00
if ( obj . latency . callbacks != null ) { obj . latency . callback ( obj . latency . current ) ; }
2019-05-20 19:00:33 -04:00
} else if ( obj . webrtc != null ) {
2018-01-18 18:43:43 -05:00
if ( controlMsg . type == 'answer' ) {
obj . webrtc . setRemoteDescription ( new RTCSessionDescription ( controlMsg ) , function ( ) { /*console.log('WebRTC remote ok');*/ } , obj . xxCloseWebRTC ) ;
2018-02-05 14:56:29 -05:00
} else if ( controlMsg . type == 'webrtc0' ) {
obj . webSwitchOk = true ; // Other side is ready for switch over
performWebRtcSwitch ( ) ;
2018-01-18 18:43:43 -05:00
} else if ( controlMsg . type == 'webrtc1' ) {
2019-12-18 15:00:08 -05:00
obj . sendCtrlMsg ( '{"ctrlChannel":"102938","type":"webrtc2"}' ) ; // Confirm we got end of data marker, indicates data will no longer be received on websocket.
2018-01-18 18:43:43 -05:00
} else if ( controlMsg . type == 'webrtc2' ) {
// TODO: Resume/Start sending data over WebRTC
}
2022-05-05 19:16:58 -04:00
} else if ( controlMsg . type == 'ping' ) { // if we get a ping, respond with a pong.
obj . sendCtrlMsg ( '{"ctrlChannel":"102938","type":"pong"}' ) ;
2017-10-15 02:22:19 -04:00
}
}
2020-04-18 22:44:07 -04:00
// Set the console message
2020-05-03 17:04:40 -04:00
obj . setConsoleMessage = function ( str , id , args , timeout ) {
2020-04-18 22:44:07 -04:00
if ( obj . consoleMessage == str ) return ;
obj . consoleMessage = str ;
2020-05-03 15:54:51 -04:00
obj . consoleMessageId = id ;
obj . consoleMessageArgs = args ;
2020-05-03 17:04:40 -04:00
obj . consoleMessageTimeout = timeout ;
2020-05-03 15:54:51 -04:00
if ( obj . onConsoleMessageChange ) { obj . onConsoleMessageChange ( obj , obj . consoleMessage , obj . consoleMessageId ) ; }
2020-04-18 22:44:07 -04:00
}
2018-07-30 17:21:03 -04:00
obj . sendCtrlMsg = function ( x ) { if ( obj . ctrlMsgAllowed == true ) { if ( ( typeof args != 'undefined' ) && args . redirtrace ) { console . log ( 'RedirSend' , typeof x , x ) ; } try { obj . socket . send ( x ) ; } catch ( ex ) { } } }
2018-02-11 20:13:26 -05:00
2018-02-05 14:56:29 -05:00
function performWebRtcSwitch ( ) {
if ( ( obj . webSwitchOk == true ) && ( obj . webRtcActive == true ) ) {
2020-04-14 06:08:53 -04:00
obj . latency . current = - 1 ; // RTT will no longer be calculated when WebRTC is enabled
2019-12-18 15:00:08 -05:00
obj . sendCtrlMsg ( '{"ctrlChannel":"102938","type":"webrtc0"}' ) ; // Indicate to the meshagent that it can start traffic switchover
obj . sendCtrlMsg ( '{"ctrlChannel":"102938","type":"webrtc1"}' ) ; // Indicate to the meshagent that data traffic will no longer be sent over websocket.
2018-02-05 14:56:29 -05:00
// TODO: Hold/Stop sending data over websocket
if ( obj . onStateChanged != null ) { obj . onStateChanged ( obj , obj . State ) ; }
}
}
2020-04-14 05:53:40 -04:00
2017-08-28 12:27:45 -04:00
obj . xxOnMessage = function ( e ) {
2019-02-07 18:00:10 -05:00
//console.log('Recv', e.data, e.data.byteLength, obj.State);
2017-10-15 02:22:19 -04:00
if ( obj . State < 3 ) {
2019-08-13 14:49:05 -04:00
if ( ( e . data == 'c' ) || ( e . data == 'cr' ) ) {
if ( e . data == 'cr' ) { obj . serverIsRecording = true ; }
2019-12-16 20:20:39 -05:00
if ( obj . options != null ) { delete obj . options . action ; obj . options . type = 'options' ; try { obj . sendCtrlMsg ( JSON . stringify ( obj . options ) ) ; } catch ( ex ) { } }
2018-03-30 18:26:36 -04:00
try { obj . socket . send ( obj . protocol ) ; } catch ( ex ) { }
2017-10-15 02:22:19 -04:00
obj . xxStateChange ( 3 ) ;
if ( obj . attemptWebRTC == true ) {
// Try to get WebRTC setup
var configuration = null ; //{ "iceServers": [ { 'urls': 'stun:stun.services.mozilla.com' }, { 'urls': 'stun:stun.l.google.com:19302' } ] };
if ( typeof RTCPeerConnection !== 'undefined' ) { obj . webrtc = new RTCPeerConnection ( configuration ) ; }
else if ( typeof webkitRTCPeerConnection !== 'undefined' ) { obj . webrtc = new webkitRTCPeerConnection ( configuration ) ; }
2019-12-18 15:00:08 -05:00
if ( ( obj . webrtc != null ) && ( obj . webrtc . createDataChannel ) ) {
obj . webchannel = obj . webrtc . createDataChannel ( 'DataChannel' , { } ) ; // { ordered: false, maxRetransmits: 2 }
2020-05-16 17:07:53 -04:00
obj . webchannel . binaryType = 'arraybuffer' ;
2019-02-07 18:00:10 -05:00
obj . webchannel . onmessage = obj . xxOnMessage ;
2021-11-18 15:00:45 -05:00
//obj.webchannel.onmessage = function (e) { logData(e, 'WebRTC'); obj.xxOnMessage(e); }
2018-02-07 21:45:14 -05:00
obj . webchannel . onopen = function ( ) { obj . webRtcActive = true ; performWebRtcSwitch ( ) ; } ;
2019-02-04 21:06:01 -05:00
obj . webchannel . onclose = function ( event ) { if ( obj . webRtcActive ) { obj . Stop ( ) ; } }
2017-10-15 02:22:19 -04:00
obj . webrtc . onicecandidate = function ( e ) {
if ( e . candidate == null ) {
2019-12-16 20:20:39 -05:00
try { obj . sendCtrlMsg ( JSON . stringify ( obj . webrtcoffer ) ) ; } catch ( ex ) { } // End of candidates, send the offer
2017-10-15 02:22:19 -04:00
} else {
2019-12-18 15:00:08 -05:00
obj . webrtcoffer . sdp += ( 'a=' + e . candidate . candidate + '\r\n' ) ; // New candidate, add it to the SDP
2017-10-15 02:22:19 -04:00
}
}
2018-02-02 15:46:09 -05:00
obj . webrtc . oniceconnectionstatechange = function ( ) {
if ( obj . webrtc != null ) {
2018-12-20 15:12:24 -05:00
if ( obj . webrtc . iceConnectionState == 'disconnected' ) { if ( obj . webRtcActive == true ) { obj . Stop ( ) ; } else { obj . xxCloseWebRTC ( ) ; } }
2018-04-19 21:19:15 -04:00
else if ( obj . webrtc . iceConnectionState == 'failed' ) { obj . xxCloseWebRTC ( ) ; }
2018-02-02 15:46:09 -05:00
}
}
2017-10-15 02:22:19 -04:00
obj . webrtc . createOffer ( function ( offer ) {
// Got the offer
obj . webrtcoffer = offer ;
2018-01-16 20:30:34 -05:00
obj . webrtc . setLocalDescription ( offer , function ( ) { /*console.log('WebRTC local ok');*/ } , obj . xxCloseWebRTC ) ;
2017-10-15 02:22:19 -04:00
} , obj . xxCloseWebRTC , { mandatory : { OfferToReceiveAudio : false , OfferToReceiveVideo : false } } ) ;
}
}
2020-04-14 05:53:40 -04:00
2017-10-15 02:22:19 -04:00
return ;
}
}
2018-01-16 20:30:34 -05:00
2020-05-16 17:07:53 -04:00
// Control messages, most likely WebRTC setup
2020-05-18 20:57:11 -04:00
//console.log('New data', e.data.byteLength);
2017-10-15 02:22:19 -04:00
if ( typeof e . data == 'string' ) {
2021-05-08 21:09:49 -04:00
if ( e . data [ 0 ] == '~' ) { obj . m . ProcessData ( e . data ) ; } else { obj . xxOnControlCommand ( e . data ) ; }
2020-05-16 17:07:53 -04:00
} else {
// Send the data to the module
if ( obj . m . ProcessBinaryCommand ) {
2021-03-12 16:44:42 -05:00
// If only 1 byte
if ( ( cmdAccLen == 0 ) && ( e . data . byteLength < 4 ) ) return ; // Ignore any commands less than 4 bytes.
2020-05-16 17:07:53 -04:00
// Send as Binary Command
2020-05-18 20:57:11 -04:00
if ( cmdAccLen != 0 ) {
// Accumulator is active
var view = new Uint8Array ( e . data ) ;
cmdAcc . push ( view ) ;
cmdAccLen += view . byteLength ;
//console.log('Accumulating', cmdAccLen);
if ( cmdAccCmdSize <= cmdAccLen ) {
var tmp = new Uint8Array ( cmdAccLen ) , tmpPtr = 0 ;
for ( var i in cmdAcc ) { tmp . set ( cmdAcc [ i ] , tmpPtr ) ; tmpPtr += cmdAcc [ i ] . byteLength ; }
//console.log('AccumulatorCompleted');
obj . m . ProcessBinaryCommand ( cmdAccCmd , cmdAccCmdSize , tmp ) ;
cmdAccCmd = 0 , cmdAccCmdSize = 0 , cmdAccLen = 0 , cmdAcc = [ ] ;
}
} else {
// Accumulator is not active
var view = new Uint8Array ( e . data ) , cmd = ( view [ 0 ] << 8 ) + view [ 1 ] , cmdsize = ( view [ 2 ] << 8 ) + view [ 3 ] ;
if ( ( cmd == 27 ) && ( cmdsize == 8 ) ) { cmd = ( view [ 8 ] << 8 ) + view [ 9 ] ; cmdsize = ( view [ 5 ] << 16 ) + ( view [ 6 ] << 8 ) + view [ 7 ] ; view = view . slice ( 8 ) ; }
//console.log(cmdsize, view.byteLength);
if ( cmdsize != view . byteLength ) {
//console.log('AccumulatorRequired', cmd, cmdsize, view.byteLength);
cmdAccCmd = cmd ; cmdAccCmdSize = cmdsize ; cmdAccLen = view . byteLength , cmdAcc = [ view ] ;
} else {
obj . m . ProcessBinaryCommand ( cmd , cmdsize , view ) ;
}
}
2020-05-16 17:07:53 -04:00
} else if ( obj . m . ProcessBinaryData ) {
// Send as Binary
obj . m . ProcessBinaryData ( new Uint8Array ( e . data ) ) ;
2019-02-07 18:00:10 -05:00
} else {
2020-05-16 17:07:53 -04:00
// Send as Text
2020-08-25 13:28:56 -04:00
if ( e . data . byteLength < 16000 ) { // Process small data block
obj . m . ProcessData ( String . fromCharCode . apply ( null , new Uint8Array ( e . data ) ) ) ; // This will stack overflow on Chrome with 100k+ blocks.
} else { // Process large data block
var bb = new Blob ( [ new Uint8Array ( e . data ) ] ) , f = new FileReader ( ) ;
f . onload = function ( e ) { obj . m . ProcessData ( e . target . result ) ; } ;
2021-05-24 21:13:53 -04:00
f . readAsBinaryString ( bb ) ;
2020-08-25 13:28:56 -04:00
}
2019-02-07 18:00:10 -05:00
}
2020-04-14 05:53:40 -04:00
}
2017-08-28 12:27:45 -04:00
} ;
2019-02-07 18:00:10 -05:00
2020-05-18 20:57:11 -04:00
// Command accumulator, this is used for WebRTC fragmentation
var cmdAccCmd = 0 , cmdAccCmdSize = 0 , cmdAccLen = 0 , cmdAcc = [ ] ;
2018-07-06 13:13:19 -04:00
obj . sendText = function ( x ) {
if ( typeof x != 'string' ) { x = JSON . stringify ( x ) ; } // Turn into a string if needed
obj . send ( encode _utf8 ( x ) ) ; // Encode UTF8 correctly
}
2018-02-11 20:13:26 -05:00
obj . send = function ( x ) {
2019-12-18 15:00:08 -05:00
//obj.debug('Agent Redir Send(' + obj.webRtcActive + ', ' + x.length + '): ' + rstr2hex(x));
//console.log('Agent Redir Send(' + obj.webRtcActive + ', ' + x.length + '): ' + ((typeof x == 'string')?x:rstr2hex(x)));
2019-12-16 20:20:39 -05:00
if ( ( typeof args != 'undefined' ) && args . redirtrace ) { console . log ( 'RedirSend' , typeof x , x . length , ( x [ 0 ] == '{' ) ? x : rstr2hex ( x ) . substring ( 0 , 64 ) ) ; }
2018-03-30 18:26:36 -04:00
try {
if ( obj . socket != null && obj . socket . readyState == WebSocket . OPEN ) {
if ( typeof x == 'string' ) {
if ( obj . debugmode == 1 ) {
var b = new Uint8Array ( x . length ) , c = [ ] ;
for ( var i = 0 ; i < x . length ; ++ i ) { b [ i ] = x . charCodeAt ( i ) ; c . push ( x . charCodeAt ( i ) ) ; }
if ( obj . webRtcActive == true ) { obj . webchannel . send ( b . buffer ) ; } else { obj . socket . send ( b . buffer ) ; }
//console.log('Send', c);
} else {
var b = new Uint8Array ( x . length ) ;
for ( var i = 0 ; i < x . length ; ++ i ) { b [ i ] = x . charCodeAt ( i ) ; }
if ( obj . webRtcActive == true ) { obj . webchannel . send ( b . buffer ) ; } else { obj . socket . send ( b . buffer ) ; }
}
2018-01-09 23:13:41 -05:00
} else {
2018-03-30 18:26:36 -04:00
//if (obj.debugmode == 1) { console.log('Send', x); }
if ( obj . webRtcActive == true ) { obj . webchannel . send ( x ) ; } else { obj . socket . send ( x ) ; }
2018-01-09 23:13:41 -05:00
}
2017-08-28 12:27:45 -04:00
}
2018-03-30 18:26:36 -04:00
} catch ( ex ) { }
2017-08-28 12:27:45 -04:00
}
obj . xxOnSocketClosed = function ( ) {
2019-12-18 15:00:08 -05:00
//obj.debug('Agent Redir Socket Closed');
2018-01-16 20:30:34 -05:00
//if (obj.debugmode == 1) { console.log('onSocketClosed'); }
2018-01-09 23:13:41 -05:00
obj . Stop ( 1 ) ;
2017-08-28 12:27:45 -04:00
}
obj . xxStateChange = function ( newstate ) {
if ( obj . State == newstate ) return ;
obj . State = newstate ;
obj . m . xxStateChange ( obj . State ) ;
if ( obj . onStateChanged != null ) obj . onStateChanged ( obj , obj . State ) ;
}
2018-04-19 21:19:15 -04:00
// Close the WebRTC connection, should be called if a problem occurs during WebRTC setup.
obj . xxCloseWebRTC = function ( ) {
2018-04-17 22:00:31 -04:00
if ( obj . webchannel != null ) { try { obj . webchannel . close ( ) ; } catch ( e ) { } obj . webchannel = null ; }
if ( obj . webrtc != null ) { try { obj . webrtc . close ( ) ; } catch ( e ) { } obj . webrtc = null ; }
obj . webRtcActive = false ;
2018-04-19 21:19:15 -04:00
}
obj . Stop = function ( x ) {
if ( obj . debugmode == 1 ) { console . log ( 'stop' , x ) ; }
2020-04-14 05:53:40 -04:00
2018-04-19 21:19:15 -04:00
// Clean up WebRTC
obj . xxCloseWebRTC ( ) ;
2018-04-17 22:00:31 -04:00
2019-12-18 15:00:08 -05:00
//obj.debug('Agent Redir Socket Stopped');
2017-08-28 12:27:45 -04:00
obj . connectstate = - 1 ;
2018-01-18 18:43:43 -05:00
if ( obj . socket != null ) {
2022-08-05 18:39:11 -04:00
try { if ( obj . socket . readyState == 1 ) { obj . sendCtrlMsg ( '{"ctrlChannel":"102938","type":"close"}' ) ; } } catch ( ex ) { } // If connected, send the close command
try { if ( obj . socket . readyState <= 1 ) { obj . socket . close ( ) ; } } catch ( ex ) { } // If connecting or connected, close the websocket
2018-01-18 18:43:43 -05:00
obj . socket = null ;
}
obj . xxStateChange ( 0 ) ;
2017-08-28 12:27:45 -04:00
}
2021-11-18 15:00:45 -05:00
// Buffer is an ArrayBuffer
function buf2hex ( buffer ) { return [ ... new Uint8Array ( buffer ) ] . map ( x => x . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ; }
2017-08-28 12:27:45 -04:00
return obj ;
}