diff --git a/embedded8266/common/commonservices.c b/embedded8266/common/commonservices.c index 14ac1bf..a9948d9 100644 --- a/embedded8266/common/commonservices.c +++ b/embedded8266/common/commonservices.c @@ -61,17 +61,25 @@ static void ICACHE_FLASH_ATTR scandone(void *arg, STATUS status) -void ICACHE_FLASH_ATTR issue_command(void *arg, char *pusrdata, unsigned short len) +int ICACHE_FLASH_ATTR issue_command(char * buffer, int retsize, char *pusrdata, unsigned short len) { - pespconn = (struct espconn *)arg; - //We accept commands on this. + char * buffend = buffer; switch( pusrdata[0] ) { + case 'e': case 'E': //Echo + if( retsize > len ) + { + ets_memcpy( buffend, pusrdata, len ); + return len; + } + else + { + return -1; + } case 'f': case 'F': //Flashing commands (F_) { flashchip->chip_size = 0x01000000; -// ets_printf( "PC%p\n", &pusrdata[2] ); const char * colon = (const char *) ets_strstr( (char*)&pusrdata[2], ":" ); int nr = my_atoi( &pusrdata[2] ); @@ -79,51 +87,33 @@ void ICACHE_FLASH_ATTR issue_command(void *arg, char *pusrdata, unsigned short l { case 'e': case 'E': //(FE#\n) <- # = sector. { - char __attribute__ ((aligned (16))) buffer[50]; - char * buffend; - buffend = buffer; - EnterCritical(); spi_flash_erase_sector( nr ); //SPI_FLASH_SEC_SIZE 4096 ExitCritical(); buffend += ets_sprintf(buffend, "FE%d\r\n", nr ); - espconn_sent( pespconn, buffer, ets_strlen( buffer ) ); break; } case 'b': case 'B': //(FE#\n) <- # = sector. { - char __attribute__ ((aligned (16))) buffer[50]; - char * buffend; - buffend = buffer; - //spi_flash_erase_sector( nr ); //SPI_FLASH_SEC_SIZE 4096 - EnterCritical(); SPIEraseBlock( nr ); ExitCritical(); buffend += ets_sprintf(buffend, "FB%d\r\n", nr ); - espconn_sent( pespconn, buffer, ets_strlen( buffer ) ); break; } case 'm': case 'M': //Execute the flash re-writer { - char ret[128]; - int n = ets_sprintf( ret, "FM" ); - espconn_sent( pespconn, ret, n ); int r = (*GlobalRewriteFlash)( &pusrdata[2], len-2 ); - n = ets_sprintf( ret, "!FM%d", r ); - espconn_sent( pespconn, ret, n ); + buffend += ets_sprintf( buffend, "!FM%d\r\n", r ); + break; } case 'w': case 'W': //Flash Write (FW#\n) <- # = byte pos. if( colon ) { - char __attribute__ ((aligned (32))) buffer[1300]; - char * buffend; - buffend = buffer; - buffer[0] = 0; colon++; const char * colon2 = (const char *) ets_strstr( (char*)colon, ":" ); if( colon2 ) @@ -136,50 +126,37 @@ void ICACHE_FLASH_ATTR issue_command(void *arg, char *pusrdata, unsigned short l spi_flash_write( nr, (uint32*)buffer, (datlen/4)*4 ); ExitCritical(); - //SPIWrite( nr, (uint32_t*)buffer, (datlen/4)*4 ); - - buffend = buffer; buffend += ets_sprintf(buffend, "FW%d\r\n", nr ); - if( ets_strlen( buffer ) ) - espconn_sent( pespconn, buffer, ets_strlen( buffer ) ); + break; } } + buffend += ets_sprintf(buffend, "!FW\r\n" ); break; case 'r': case 'R': //Flash Read (FR#\n) <- # = sector. if( colon ) { - char __attribute__ ((aligned (16))) buffer[1300]; - char * buffend; - buffend = buffer; - buffer[0] = 0; colon++; int datlen = my_atoi( colon ); + datlen = (datlen/4)*4; //Must be multiple of 4 bytes if( datlen <= 1280 ) { - buffend += ets_sprintf(buffend, "FR%08d:%04d:", nr, datlen ); - int frontlen = buffend - buffer; - spi_flash_read( nr, (uint32*)buffend, (datlen/4)*4 ); - espconn_sent( pespconn, buffer, frontlen + datlen ); - buffer[0] = 0; - if( ets_strlen( buffer ) ) - espconn_sent( pespconn, buffer, ets_strlen( buffer ) ); + buffend += ets_sprintf(buffend, "FR%08d:%04d:", nr, datlen ); //Caution: This string must be a multiple of 4 bytes. + spi_flash_read( nr, (uint32*)buffend, datlen ); + break; } } + buffend += ets_sprintf(buffend, "!FR\r\n" ); break; } flashchip->chip_size = 0x00080000; - break; + return buffend - buffer; } case 'w': case 'W': // (W1:SSID:PASSWORD) (To connect) or (W2) to be own base station. ...or WI, to get info... or WS to scan. { char * colon = (char *) ets_strstr( (char*)&pusrdata[2], ":" ); char * colon2 = (colon)?((char *)ets_strstr( (char*)(colon+1), ":" )):0; char * extra = colon2; - char __attribute__ ((aligned (16))) buffer[1300]; - char * buffend; - buffend = buffer; - buffer[0] = 0; if( extra ) { @@ -196,59 +173,65 @@ void ICACHE_FLASH_ATTR issue_command(void *arg, char *pusrdata, unsigned short l switch (pusrdata[1]) { case '1': //Station mode + case '2': //AP Mode if( colon && colon2 ) { int c1l = 0; int c2l = 0; - struct station_config stationConf; *colon = 0; colon++; *colon2 = 0; colon2++; c1l = ets_strlen( colon ); c2l = ets_strlen( colon2 ); if( c1l > 32 ) c1l = 32; if( c2l > 32 ) c2l = 32; - os_bzero( &stationConf, sizeof( stationConf ) ); printf( "Switching to: \"%s\"/\"%s\".\n", colon, colon2 ); - os_memcpy(&stationConf.ssid, colon, c1l); - os_memcpy(&stationConf.password, colon2, c2l); + if( pusrdata[1] == '1' ) + { + struct station_config stationConf; + os_bzero( &stationConf, sizeof( stationConf ) ); - wifi_set_opmode( 1 ); - wifi_station_set_config(&stationConf); - wifi_station_connect(); - espconn_sent( pespconn, "W1\r\n", 4 ); + os_memcpy(&stationConf.ssid, colon, c1l); + os_memcpy(&stationConf.password, colon2, c2l); - printf( "Switching.\n" ); - } - break; - case '2': - { - wifi_set_opmode_current( 1 ); - wifi_set_opmode( 2 ); - espconn_sent( pespconn, "W2\r\n", 4 ); + wifi_set_opmode( 1 ); + wifi_station_set_config(&stationConf); + wifi_station_connect(); + + buffend += ets_sprintf( buffend, "W1\r\n" ); + printf( "Switching.\n" ); + } + else + { + wifi_set_opmode_current( 1 ); + wifi_set_opmode( 2 ); + buffend += ets_sprintf( buffend, "W2\r\n" ); + } } break; case 'I': { - struct station_config sc; int mode = wifi_get_opmode(); - char buffer[200]; - char * buffend = &buffer[0]; buffend += ets_sprintf( buffend, "WI%d", mode ); - wifi_station_get_config( &sc ); - - buffend += ets_sprintf( buffend, ":%s:%s", sc.ssid, sc.password ); - - espconn_sent( pespconn, buffer, buffend - buffer ); + if( mode == 2 ) + { + struct softap_config ap; + wifi_softap_get_config( &ap ); + buffend += ets_sprintf( buffend, "\t%s\t%s\t%s\t%d", ap.ssid, ap.password, enctypes[ap.authmode], ap.channel ); + } + else + { + struct station_config sc; + wifi_station_get_config( &sc ); + buffend += ets_sprintf( buffend, "\t%s\t%s\tna\t%d", sc.ssid, sc.password, wifi_get_channel() ); + } } break; case 'S': case 's': { - char buffer[1024]; - char * buffend = &buffer[0]; int i, r; scanplace = 0; @@ -267,34 +250,41 @@ void ICACHE_FLASH_ATTR issue_command(void *arg, char *pusrdata, unsigned short l buffend += ets_sprintf( buffend, "WS%d\n", r ); uart0_sendStr(buffer); - espconn_sent( pespconn, buffer, buffend - buffer ); - + break; } break; case 'R': case 'r': { - char buffer[1024]; - char * buffend = &buffer[0]; int i, r; buffend += ets_sprintf( buffend, "WR%d\n", scanplace ); - for( i = 0; i < scanplace; i++ ) + for( i = 0; i < scanplace && buffend - buffer < retsize - 64; i++ ) { buffend += ets_sprintf( buffend, "#%s\t%s\t%d\t%d\t%s\n", totalscan[i].name, totalscan[i].mac, totalscan[i].rssi, totalscan[i].channel, enctypes[totalscan[i].encryption] ); } - espconn_sent( pespconn, buffer, buffend - buffer ); - + break; } break; } - break; + return buffend - buffer; } default: break; } + return -1; +} + +void ICACHE_FLASH_ATTR issue_command_udp(void *arg, char *pusrdata, unsigned short len) +{ + char __attribute__ ((aligned (32))) retbuf[1300]; + int r = issue_command( retbuf, 1300, pusrdata, len ); + if( r > 0 ) + { + espconn_sent( (struct espconn *)arg, retbuf, r ); + } } void ICACHE_FLASH_ATTR CSInit() @@ -304,7 +294,7 @@ void ICACHE_FLASH_ATTR CSInit() pUdpServer->type = ESPCONN_UDP; pUdpServer->proto.udp = (esp_udp *)os_zalloc(sizeof(esp_udp)); pUdpServer->proto.udp->local_port = 7878; - espconn_regist_recvcb(pUdpServer, issue_command); + espconn_regist_recvcb(pUdpServer, issue_command_udp); espconn_create( pUdpServer ); pHTTPServer = (struct espconn *)os_zalloc(sizeof(struct espconn)); diff --git a/embedded8266/common/commonservices.h b/embedded8266/common/commonservices.h index 2d6afbd..4986072 100644 --- a/embedded8266/common/commonservices.h +++ b/embedded8266/common/commonservices.h @@ -6,6 +6,12 @@ #include "c_types.h" +//Returns nr bytes to return. You must allocate retdata. +//It MUST be at least 1,300 bytes large and it MUST be 32-bit aligned. +//NOTE: It is SAFE to use pusrdata and retdata as the same buffer. +int ICACHE_FLASH_ATTR issue_command(char * retdata, int retsize, char *pusrdata, unsigned short len); + + //Includes UDP Control + HTTP Interfaces void ICACHE_FLASH_ATTR CSInit(); void ICACHE_FLASH_ATTR CSTick( int slowtick ); diff --git a/embedded8266/common/http.c b/embedded8266/common/http.c index 2283b31..76151b3 100644 --- a/embedded8266/common/http.c +++ b/embedded8266/common/http.c @@ -11,6 +11,9 @@ struct HTTPConnection HTTPConnections[HTTP_CONNECTIONS]; struct HTTPConnection * curhttp; uint8 * curdata; uint16 curlen; +uint8 wsmask[4]; +uint8 wsmaskplace; + ICACHE_FLASH_ATTR void InternalStartHTTP( ); ICACHE_FLASH_ATTR void HTTPHandleInternalCallback( ); @@ -29,7 +32,7 @@ void ICACHE_FLASH_ATTR HTTPGotData( ) while( curlen-- ) { - c = POP; + c = HTTPPOP; // sendhex2( h->state ); sendchr( ' ' ); switch( curhttp->state ) @@ -97,8 +100,15 @@ void ICACHE_FLASH_ATTR HTTPGotData( ) WebSocketGotData( c ); break; case HTTP_WAIT_CLOSE: - //printf( "__HTTPCLose1\n" ); - HTTPClose( ); + if( curhttp->keep_alive ) + { + curhttp->state = HTTP_STATE_WAIT_METHOD; + } + else + { + //printf( "__HTTPCLose1\n" ); + HTTPClose( ); + } break; default: break; @@ -130,8 +140,15 @@ static void DoHTTP( uint8_t timed ) case HTTP_WAIT_CLOSE: if( TCPDoneSend( curhttp->socket ) ) { - //printf( "HTTPCLose2\n"); - HTTPClose( ); + if( curhttp->keep_alive ) + { + curhttp->state = HTTP_STATE_WAIT_METHOD; + } + else + { + //printf( "HTTPCLose2\n"); + HTTPClose( ); + } } break; case HTTP_STATE_DATA_WEBSOCKET: @@ -187,14 +204,21 @@ void ICACHE_FLASH_ATTR HTTPHandleInternalCallback( ) START_PACK; //TODO: Content Length? MIME-Type? - PushString("HTTP/1.1 200 Ok\r\nConnection: close"); + PushString("HTTP/1.1 200 Ok\r\n"); if( curhttp->bytesleft < 0xfffffffe ) { - PushString("\r\nContent-Length: "); + PushString("Connection: keep-alive\r\nContent-Length: "); Uint32To10Str( stto, curhttp->bytesleft ); PushBlob( stto, os_strlen( stto ) ); + curhttp->keep_alive = 1; } + else + { + PushString("Connection: close\r\n"); + curhttp->keep_alive = 0; + } + PushString( "\r\nContent-Type: " ); //Content-Type? while( slen && ( curhttp->pathbuffer[--slen] != '.' ) ); @@ -203,6 +227,10 @@ void ICACHE_FLASH_ATTR HTTPHandleInternalCallback( ) { PushString( "audio/mpeg3" ); } + else if( strcmp( k, "gz" ) == 0 ) + { + PushString( "text/plain\r\nContent-Encoding: gzip\r\nCache-Control: public, max-age=3600" ); + } else if( curhttp->bytesleft == 0xfffffffe ) { PushString( "text/plain" ); @@ -410,6 +438,7 @@ void ICACHE_FLASH_ATTR WebSocketGotData( uint8_t c ) #define WS_KEY_LEN 36 #define WS_KEY "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" +#define WS_RETKEY_SIZEM1 32 while( curlen > 1 ) { @@ -449,14 +478,16 @@ void ICACHE_FLASH_ATTR WebSocketGotData( uint8_t c ) SHA1_Update( &c, inkey, i ); SHA1_Final( hash, &c ); - if( SHA1_HASH_LEN * 2 > MAX_PATHLEN ) - { - HTDEBUG( "Pathlen too short.\n" ); - curhttp->state = HTTP_WAIT_CLOSE; - return; - } +#if (WS_RETKEY_SIZE > MAX_PATHLEN - 10 ) +#error MAX_PATHLEN too short. +#endif - my_base64_encode( hash, SHA1_HASH_LEN, curhttp->pathbuffer ); + my_base64_encode( hash, SHA1_HASH_LEN, curhttp->pathbuffer + (MAX_PATHLEN-WS_RETKEY_SIZEM1) ); + + curhttp->bytessofar = 0; + curhttp->bytesleft = 0; + + NewWebSocket(); //Respond... curhttp->state_deets = 1; @@ -507,7 +538,16 @@ void ICACHE_FLASH_ATTR WebSocketGotData( uint8_t c ) { payloadlen &= 0x7f; } - WebSocketData( curdata, payloadlen ); + + wsmask[0] = curdata[0]; + wsmask[1] = curdata[1]; + wsmask[2] = curdata[2]; + wsmask[3] = curdata[3]; + curdata += 4; + curlen -= 4; + wsmaskplace = 0; + + WebSocketData( payloadlen ); curlen -= payloadlen; curdata += payloadlen; @@ -525,13 +565,11 @@ void ICACHE_FLASH_ATTR WebSocketTickInternal() case 4: //Has key full HTTP header, etc. wants response. START_PACK; PushString( "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " ); - PushString( curhttp->pathbuffer ); + PushString( curhttp->pathbuffer + (MAX_PATHLEN-WS_RETKEY_SIZEM1) ); PushString( "\r\n\r\n" ); EndTCPWrite( curhttp->socket ); curhttp->state_deets = 5; - curhttp->bytessofar = 0; - curhttp->bytesleft = 0; - NewWebSocket(); + curhttp->keep_alive = 0; break; case 5: WebSocketTick(); @@ -557,4 +595,11 @@ void ICACHE_FLASH_ATTR WebSocketSend( uint8_t * data, int size ) EndTCPWrite( curhttp->socket ); } +uint8_t WSPOPMASK() +{ + uint8_t mask = wsmask[wsmaskplace]; + wsmaskplace = (wsmaskplace+1)&3; + return (*curdata++)^(mask); +} + diff --git a/embedded8266/common/http.h b/embedded8266/common/http.h index 4504344..af06892 100644 --- a/embedded8266/common/http.h +++ b/embedded8266/common/http.h @@ -22,15 +22,18 @@ void ICACHE_FLASH_ATTR HTTPCustomStart( ); void ICACHE_FLASH_ATTR HTTPCustomCallback( ); //called when we can send more data -void ICACHE_FLASH_ATTR WebSocketData( uint8_t * data, int len ); +void ICACHE_FLASH_ATTR WebSocketData( int len ); void ICACHE_FLASH_ATTR WebSocketTick( ); void ICACHE_FLASH_ATTR WebSocketNew(); extern struct HTTPConnection * curhttp; extern uint8 * curdata; extern uint16 curlen; +extern uint8 wsmask[4]; +extern uint8 wsmaskplace; -#define POP (*curdata++) +uint8_t WSPOPMASK(); +#define HTTPPOP (*curdata++) #define HTTP_STATE_NONE 0 #define HTTP_STATE_WAIT_METHOD 1 @@ -49,7 +52,10 @@ struct HTTPConnection { uint8_t state:4; uint8_t state_deets; - uint8_t pathbuffer[MAX_PATHLEN]; //Also used for temporary and handshake information when using websockets. + + //Provides path, i.e. "/index.html" but, for websockets, the last + //32 bytes of the buffer are used for the websockets key. + uint8_t pathbuffer[MAX_PATHLEN]; uint8_t is_dynamic:1; uint16_t timeout; @@ -60,6 +66,7 @@ struct HTTPConnection } data; void * rcb; + void * rcbDat; //For websockets primarily. uint32_t bytesleft; uint32_t bytessofar; @@ -67,6 +74,7 @@ struct HTTPConnection uint8_t is404:1; uint8_t isdone:1; uint8_t isfirst:1; + uint8_t keep_alive:1; uint8_t need_resend:1; struct espconn * socket; diff --git a/embedded8266/common/http_custom.c b/embedded8266/common/http_custom.c index 9399fcb..ff69ac9 100644 --- a/embedded8266/common/http_custom.c +++ b/embedded8266/common/http_custom.c @@ -34,11 +34,16 @@ static ICACHE_FLASH_ATTR void echo() static ICACHE_FLASH_ATTR void issue() { - char mydat[512]; - int len = URLDecode( mydat, 512, curhttp->pathbuffer+9 ); - - issue_command(curhttp->socket, mydat, len ); + uint8_t __attribute__ ((aligned (32))) buf[1300]; + int len = URLDecode( buf, 1300, curhttp->pathbuffer+9 ); + int r = issue_command(buf, 1300, buf, len ); + if( r > 0 ) + { + START_PACK; + PushBlob( buf, r ); + EndTCPWrite( curhttp->socket ); + } curhttp->state = HTTP_WAIT_CLOSE; } @@ -72,6 +77,7 @@ void ICACHE_FLASH_ATTR HTTPCustomStart( ) } + void ICACHE_FLASH_ATTR HTTPCustomCallback( ) { if( curhttp->rcb ) @@ -80,18 +86,91 @@ void ICACHE_FLASH_ATTR HTTPCustomCallback( ) curhttp->isdone = 1; } -void ICACHE_FLASH_ATTR WebSocketTick() + + + +static void ICACHE_FLASH_ATTR WSEchoData( int len ) { + char cbo[len]; + int i; + for( i = 0; i < len; i++ ) + { + cbo[i] = WSPOPMASK(); + } + WebSocketSend( cbo, len ); } -void ICACHE_FLASH_ATTR WebSocketData( uint8_t * data, int len ) + +static void ICACHE_FLASH_ATTR WSEvalData( int len ) { - char stout[128]; - int stl = ets_sprintf( stout, "output.innerHTML = %d; doSend('x' );\n", curhttp->bytessofar++ ); - WebSocketSend( stout, stl ); + char cbo[128]; + int l = ets_sprintf( cbo, "output.innerHTML = %d; doSend('x' );", curhttp->bytessofar++ ); + + WebSocketSend( cbo, l ); } + +static void ICACHE_FLASH_ATTR WSCommandData( int len ) +{ + uint8_t __attribute__ ((aligned (32))) buf[1300]; + int i; + + for( i = 0; i < len; i++ ) + { + buf[i] = WSPOPMASK(); + } + + i = issue_command(buf, 1300, buf, len ); + + if( i < 0 ) i = 0; + + WebSocketSend( buf, i ); +} + + +// output.innerHTML = msg++ + " " + lasthz; +// doSend('x' ); + + + void ICACHE_FLASH_ATTR NewWebSocket() { + if( strcmp( (const char*)curhttp->pathbuffer, "/d/ws/echo" ) == 0 ) + { + curhttp->rcb = 0; + curhttp->rcbDat = (void*)&WSEchoData; + } + else if( strcmp( (const char*)curhttp->pathbuffer, "/d/ws/evaltest" ) == 0 ) + { + curhttp->rcb = 0; + curhttp->rcbDat = (void*)&WSEvalData; + } + else if( strcmp( (const char*)curhttp->pathbuffer, "/d/ws/issue" ) == 0 ) + { + curhttp->rcb = 0; + curhttp->rcbDat = (void*)&WSCommandData; + } + else + { + curhttp->is404 = 1; + } } + + + +void ICACHE_FLASH_ATTR WebSocketTick() +{ + if( curhttp->rcb ) + { + ((void(*)())curhttp->rcb)(); + } +} + +void ICACHE_FLASH_ATTR WebSocketData( int len ) +{ + if( curhttp->rcbDat ) + { + ((void(*)( int ))curhttp->rcbDat)( len ); + } +} diff --git a/embedded8266/image.elf b/embedded8266/image.elf index 7b0599d..2ae291d 100755 Binary files a/embedded8266/image.elf and b/embedded8266/image.elf differ diff --git a/embedded8266/web/page/colorchord.html b/embedded8266/web/page/colorchord.html new file mode 100644 index 0000000..e58758d --- /dev/null +++ b/embedded8266/web/page/colorchord.html @@ -0,0 +1,68 @@ + +
++ |
+
+
| |
+ |
+
+
| |
+ |
+
+
| |
+ |
+
+
|
Messages: " + msg + "
Hz:" + lasthz + "
"; + if( commsup != 1 ) + { + commsup = 1; + $('#SystemStatusClicker').css("color", "green" ); + } + + if( lastitem && lastitem.callback ) + { + lastitem.callback( lastitem, evt.data ); + lastitem = null; + } + + if( workqueue.length ) + { + var elem = workqueue.shift(); + if( elem.request ) + { + doSend( elem.request ); + lastitem = elem; + return; + } + } + + + doSend('e'); +} + +function onError(evt) +{ + $('#SystemStatusClicker').css("color", "red" ); + commsup = 0; +} + +function doSend(message) +{ + websocket.send(message); +} + + + +function ShowHideEvent( objname ) +{ + var obj = $( "#" + objname ); + obj.slideToggle( 'fast' ).toggleClass( 'opened' ); + var opened = obj.is( '.opened' ); + localStorage["sh" + objname] = opened?1:0; +} + + +function IssueCustomCommand() +{ + QueueOperation( $("#custom_command").val(), function( req,data) { $("#custom_command_response").val( data ); } ); +} + + + +window.addEventListener("load", init, false); + + + + + + + +///////// Various functions that are not core appear down here. + + +did_wifi_get_config = false; + +function WifiDataTicker() +{ + if( !did_wifi_get_config ) + { + QueueOperation( "WI", function(req,data) + { + var params = data.split( "\t" ); + + document.wifisection.wifitype.value = Number( params[0].substr(2) ); + document.wifisection.wificurname.value = params[1]; + document.wifisection.wificurpassword.value = params[2]; + document.wifisection.wificurenctype.value = params[3]; + document.wifisection.wificurchannel.value = Number( params[4] ); + + did_wifi_get_config = true; + } ); + } + + + QueueOperation( "WR", function(req,data) { + var lines = data.split( "\n" ); + var innerhtml; + if( lines.length < 3 ) + { + innerhtml = "No APs found. Did you scan?"; + } + else + { + innerhtml = "SSID | MAC | RS | Ch | Enc |
---|---|---|---|---|
" + dats[0] + " | " + dats[1] + " | " + dats[2] + " | " + dats[3] + " | " + dats[4] + " |