일반적으로 아두이노를 사용할 때 소스코드외에 만들어지는 파일에 대해서는 관심이 없다. 사실 관심을 가질 필요가 거의 없다. 다만, 강의자료를 만드는 입장에서 아두이노라는 임베디드 시스템에 프로그램이 들어가는 과정, 그 안에 있는 프로그램을 밖으로 꺼내는 과정등을 설명할 때 아주 잠간이지만 hex 파일이라는 것을 소개하게 된다.
이 글은 hex 파일에 대해 간략한 소개와 함께 아두이노 사용시 만들어지는 hex 파일에 대한 겉핧기식의 정보를 제공한다.
아두이노 HEX 파일 위치
아두이노 IDE 를 실행시킨 후 다음과 같은 간단한 프로그램을 만들어서 컴파일 한 후 만들어지는 hex 파일을 찾도록 한다.
윈도우10 운영체제를 사용하고 있다면 [윈도우]키를 누른다음 %temp% 를 입력하면 폴더가 검색된다. 내 컴퓨터의 경우C:\Users\event\AppData\Local\Temp폴더가 나온다.
폴더를 찾아 간 다음 수정한 날짜를 최신으로 설정하면 방금 만든 파일과 폴더가 나온다. 폴더를 찾아서 내부에 보면 아두이노로 만든 파일들이 나온다.
파일 중 확장자가 hex 인 것이 헥사파일이다. 즉 아두이노에 들어갈 이진파일의 내용이다.
HEX 파일 내용 (Format)
hex 파일은 소스파일을 제대로 컴파일 했을때만 만들어진다. 즉, 오류가 발생하거나 아직 컴파일하지 않은 소스파일에 대해서는 hex 파일이 만들어지지 않는다.
Byte count, two hex digits (one hex digit pair), indicating the number of bytes (hex digit pairs) in the data field. The maximum byte count is 255 (0xFF). 16 (0x10) and 32 (0x20) are commonly used byte counts.
Address, four hex digits, representing the 16-bit beginning memory address offset of the data. The physical address of the data is computed by adding this offset to a previously established base address, thus allowing memory addressing beyond the 64 kilobyte limit of 16-bit addresses. The base address, which defaults to zero, can be changed by various types of records. Base addresses and address offsets are always expressed as big endian values.
Record type (see record types below), two hex digits, 00 to 05, defining the meaning of the data field.
Data, a sequence of n bytes of data, represented by 2n hex digits. Some records omit this field (n equals zero). The meaning and interpretation of data bytes depends on the application.
Checksum, two hex digits, a computed value that can be used to verify the record has no errors.
OLD NEW OLD NEW ============================== 't' 'u' 0x74 0x75 222 223 0xDE 0xDF 1046 1047 0x416 0x417
‘t’ 의 아스키코드는 0x74 고, ‘u’ 의 아스크코드는 0x75 다. 즉 4와 5가 다르다. (1) 에서 이 부분을 확인할 수 있다. 223 을 16진수로 고치면 0xDE 이고, 224를 16진수로 고치면 0xDF 이다. E와 F가 다르가 나오는 부분이 (2)이다. 마찬가지로 1046 을 16진수로 고치면 0x416 이고 1을 더한 값이 0x417 이다. (3)에서 6과 7이 다르게 나온다.
여기까지 아두이노 코드를 PC 에서 아두이노로 넘길 때 avr-gcc 를 사용해서 만들어지는 hex 파일에 대해 간략히 살펴보았다. 이 내용은 추후에 아두이노안에 들어있는 이진 파일을 받아온 후 어셈블리코드로 재구성할 때 도움이 된다.
사실 최근들어 어셈블리코드는 일반적으로 거의 사용되지 않는다. 사용되지 않는 이유는 첫째 C 컴파일러의 성능이 좋아졌기 때문이다. 불과 몇년전만 하더라도 C로 만들어진 8051 이나 AVR 코드를 신뢰하지 못하는 개발자들이 있었다. 그래서 시간이 걸려도 직접 어셈블러로 코드를 만들곤 했다. 하지만 컴파일러의 성능이 좋아지면서 이제는 C로 만들거나 어셈블러로 개발자가 직접 최적화를 한 것이 큰 차이를 가지지 않게 되었다. 또한 MCU 들의 성능이 과거에 비해 좋아지고 있다. 한 비트의 의미가 예전같지 않게 된 것이다.
아두이노가 대표적이다. 아두이노 우노는 Atmega328 AVR 칩을 사용하고 플래시메모리가 32kB 나 된다. SRAM 2kB, EEPROM 1kB 를 가지고 있다. 2000년대 초반에 8051을 사용할 때 저정도의 메모리는 상상하기 힘든 것이었다. 한참 8051 에 빠져있을때 AT89C51 을 사용해서 기존과 다른 8051 보드를 직접 만들어 사용한 적이 있다. AT89C51 은 4kB 플래시메모리를 가지고 있었고, 그 당시 이정도면 매우 훌륭한 MCU 였다. 아두이노 메가는 Atmega2560 칩을 사용하고, 메모리는 플래시 256kB 를 가지고 있다. Atmega2560 칩은 3D 프린터의 제어기로 일반적으로 사용된다.
JavaScript typed arraysare array-like objects that provide a mechanism for reading and writing raw binary data in memory buffers.As you may already know,Arrayobjects grow and shrink dynamically and can have any JavaScript value. JavaScript engines perform optimizations so that these arrays are fast.
However, as web applications become more and more powerful, adding features such as audio and video manipulation, access to raw data using WebSockets, and so forth, it has become clear that there are times when it would be helpful for JavaScript code to be able to quickly and easily manipulate raw binary data. This is where typed arrays come in. Each entry in a JavaScript typed array is a raw binary value in one of a number of supported formats, from 8-bit integers to 64-bit floating-point numbers.
However, typed arrays arenotto be confused with normal arrays, as callingArray.isArray()on a typed array returnsfalse. Moreover, not all methods available for normal arrays are supported by typed arrays (e.g. push and pop).
Buffers and views: typed array architecture
To achieve maximum flexibility and efficiency, JavaScript typed arrays split the implementation intobuffersandviews. A buffer (implemented by theArrayBufferobject) is an object representing a chunk of data; it has no format to speak of and offers no mechanism for accessing its contents. In order to access the memory contained in a buffer, you need to use a view. A view provides a context — that is, a data type, starting offset, and the number of elements — that turns the data into a typed array.
ArrayBuffer
TheArrayBufferis a data type that is used to represent a generic, fixed-length binary data buffer. You can't directly manipulate the contents of anArrayBuffer; instead, you create a typed array view or aDataViewwhich represents the buffer in a specific format, and use that to read and write the contents of the buffer.
Typed array views
Typed array views have self-descriptive names and provide views for all the usual numeric types likeInt8,Uint32,Float64and so forth. There is one special typed array view, theUint8ClampedArray. It clamps the values between 0 and 255. This is useful forCanvas data processing, for example.
TypeValue RangeSize in bytesDescriptionWeb IDL typeEquivalent C type
TheDataViewis a low-level interface that provides a getter/setter API to read and write arbitrary data to the buffer. This is useful when dealing with different types of data, for example. Typed array views are in the native byte-order (seeEndianness) of your platform. With aDataViewyou are able to control the byte-order. It is big-endian by default and can be set to little-endian in the getter/setter methods.
Web APIs using typed arrays
These are some examples of APIs that make use of typed arrays; there are others, and more are being added all the time.
First of all, we will need to create a buffer, here with a fixed length of 16-bytes:
let buffer =newArrayBuffer(16);
At this point, we have a chunk of memory whose bytes are all pre-initialized to 0. There's not a lot we can do with it, though. We can confirm that it is indeed 16 bytes long, and that's about it:
if(buffer.byteLength ===16){ console.log("Yes, it's 16 bytes.");}else{ console.log("Oh no, it's the wrong size!");}
Before we can really work with this buffer, we need to create a view. Let's create a view that treats the data in the buffer as an array of 32-bit signed integers:
let int32View =newInt32Array(buffer);
Now we can access the fields in the array just like a normal array:
for(let i =0; i < int32View.length; i++){ int32View[i]= i *2;}
This fills out the 4 entries in the array (4 entries at 4 bytes each makes 16 total bytes) with the values0,2,4, and6.
Multiple views on the same data
Things start to get really interesting when you consider that you can create multiple views onto the same data. For example, given the code above, we can continue like this:
let int16View =newInt16Array(buffer);for(let i =0; i < int16View.length; i++){ console.log('Entry '+ i +': '+ int16View[i]);}
Here we create a 16-bit integer view that shares the same buffer as the existing 32-bit view and we output all the values in the buffer as 16-bit integers. Now we get the output0,0,2,0,4,0,6,0.
You can go a step farther, though. Consider this:
int16View[0]=32; console.log('Entry 0 in the 32-bit array is now '+ int32View[0]);
The output from this is"Entry 0 in the 32-bit array is now 32".
In other words, the two arrays are indeed simply viewed on the same data buffer, treating it as different formats. You can do this with anyview types.
Working with complex data structures
By combining a single buffer with multiple views of different types, starting at different offsets into the buffer, you can interact with data objects containing multiple data types. This lets you, for example, interact with complex data structures fromWebGL, data files, or C structures you need to use while usingjs-ctypes.
Consider this C structure:
struct someStruct { unsigned long id; char username[16]; float amountDue;};
You can access a buffer containing data in this format like this:
let buffer =newArrayBuffer(24);// ... read the data into the buffer ...let idView =newUint32Array(buffer,0,1);let usernameView =newUint8Array(buffer,4,16);let amountDueView =newFloat32Array(buffer,20,1);
Then you can access, for example, the amount due withamountDueView[0].
Note:Thedata structure alignmentin a C structure is platform-dependent. Take precautions and considerations for these padding differences.
Conversion to normal arrays
After processing a typed array, it is sometimes useful to convert it back to a normal array in order to benefit from theArrayprototype. This can be done usingArray.from(), or using the following code whereArray.from()is unsupported.
let typedArray =newUint8Array([1,2,3,4]), normalArray =Array.prototype.slice.call(typedArray); normalArray.length ===4; normalArray.constructor === Array;
DesktopMobileServerChromeEdgeFirefoxInternet ExplorerOperaSafariAndroid webviewChrome for AndroidFirefox for AndroidOpera for AndroidSafari on iOSSamsung InternetNode.jsInt8ArrayInt8Array()constructor
DataViewview는buffer에 저장된 데이터로 부터 값을읽고,쓰기위한low-level 인터페이스를 제공한다.(getter/setter API 제공)
// 12 bytes byffervarbuffer=newArrayBuffer(12);// view 를 생성한다.varview=newDataView(buffer,2,2);// 해당 view 가 시작하는 위치를 반환한다.console.log(view.byteOffset);// 2
데이터를 다루기위한 DataView 의 특성들은 아래와 같다.
Typed Array Views
DataView를 상속한 아래클래스들을 통해buffer에 저장된 데이터를 다룰 수 있게된다.
// unsigned int 8(1 bytes)varbuffer=newArrayBuffer(20);varuint8View=newUint8Array(buffer);// 0 ~ 255(unsigned int 8(1 bytes) 로 표현 가능한 수)uint8View[0]=0;uint8View[1]=255;console.log(uint8View);// [0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]// signed int 8(1 bytes)varbuffer=newArrayBuffer(20);varint8View=newInt8Array(buffer);// -127 ~ 128(signed int 8(1 bytes) 로 표현 가능한 수)// signed 의 경우 부호(양수/음수)를 나타내기 위해 총 8bit 중 1 비트(0: 양수, 1: 음수) 사용하기 때문에, 나머지 7bit(-127 ~ 128(표현 가능한 수))를 통해 숫자를 표현하게 된다.int8View[0]=-128;int8View[1]=127;console.log(int8View);// [-128, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]// unsigned int 16(2 bytes)varbuffer=newArrayBuffer(20);varuint16View=newUint16Array(buffer);// 0 ~ 65535(unsigned int 16(2 bytes) 로 표현 가능한 수)uint16View[0]=65535;console.log(uint16View);// [65535, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Little Endian방식의 장점: 산술연산유닛(ALU)에서 메모리를 읽는 방식이 메모리 주소가 낮은 쪽에서부터 높은 쪽으로 읽기 때문에 산술 연산의 수행이 더 쉽다.(연산 처리 과정에서 이런 장점이 있는 정도로만 알고 넘어가자…)
Big Endian:상위 바이트부터 데이터가 저장되는 방식.
적용 이유:
각**CPU**(Intel / Spac) **타입**에 따라차이를 보이는byte order(**데이터 저장 순서**)는, 동일한 시스템 안에서만 데이터를 주고 받는다면,Endian에 대해 전혀 신경쓸 필요가 없지만,이기종간에 데이터를 주고 받을 경우, 서로간의데이터 저장 방식 차이로 인해 전혀 엉뚱한 결과를 반환하게 된다.
서로 다른 Endian 간의 데이터 통신 해결책:
공통되는 Endian(약속된Endian규칙)으로변환후, 데이타를 주고/받는 방법.
즉 서로간에 사용할Endian(Little EndianorBig Endian) 을 하나로 통일시켜 데이터를 주고 받는 것이다.
또 하나의 방법은byte order(바이트 저장 순서) 를 신경쓸 필요가 없는,데이터 타입을 사용하는 것이다.char타입은1byte의 크기를 가지기때문에,byte order에 대해 전혀 신경쓸 필요가 없다. 예를 들면 12345678 을 int 형으로 보내는 대신문자열“12345678” 로 변환시켜전송하면 된다.
예제 소스에서는NodeJS및Socket.IO와 관련된 내용은 최대한 배제 하였습니다.(특별히 포스트 내용과 관련 없다고 판단한 내용)
파일 업로드
File및FileReader API를 지원하는 브라우저를 통해 파일 업로드 기능을 만들 수 있다.
하지만IE(10/11 포함) 브라우저는 지원하지 않는다고 보면된다.
Source Example
Cliend Side(use JS)
먼저 저장소로 부터 내려받은 파일을 include 한다.
<scriptsrc="siofu_client.js"></script>
// socket 서버에 연결varsocket=io.connect('http://localhost:9090');// socket 객체를 SocketIOFileUpload 클래스로 전달한다.varuploader=newSocketIOFileUpload(socket);// listenOnSubmit 메서드에 input[type="button"] 및 input[type="file"] Element 를 전달한다.uploader.listenOnSubmit($('#btn_upload').get(0),$('#siofu_input').get(0));// KiB === byte 단위// KB === KByte 단위// 한번에 로드될 chunks 파일 사이즈// chunkSize 를 0으로 할당하면, chunk 를 사용하지 않게 된다.uploader.chunkSize=1024*100;// 102400 byte 로 chunk 단위를 나눈다.uploader.addEventListener("start",function(event){console.log('started upload of file');});// progress 이벤트를 통해 현재 진행 상황을 볼 수 있다.uploader.addEventListener("progress",function(event){varpercent=event.bytesLoaded/event.file.size*100;console.log("File is",percent.toFixed(2),"percent loaded");});// 파일 업로드가 끝날을때 이벤트가 발생한다.uploader.addEventListener("complete",function(event){console.log('completed file upload');});
Server Side(use nodeJS)
varuploader=newsiofu();uploader.dir="uploads";uploader.listen(socket);// Do something when a file is saved:uploader.on("saved",function(event){console.log(event.file);});// Error handler:uploader.on("error",function(event){console.log("Error from uploader",event);});
서버에서 내려받은ArrayBuffer(이미지데이터) 로view(uInt8Array) 를 생성 후, 버퍼에 저장된 데이터를 조작한다.
Source Example
Cliend Side(use JS)
varcw=327;varch=125;// canvas Element 를 가져온다.varcanvas=document.querySelector('canvas');// context 를 생성한다.varctx=canvas.getContext('2d');// view(부호 없는 1byte 정수 배열)를 생성한다.varuInt8Array=newUint8Array(payload.buffer);// view를 통해 Blob Object 를 생성한다.varblob=newBlob([uInt8Array],{type:'image/jpeg'});varoriginalImgData=null;// Blob Object를 참조하는 URL를 생성한다.varurl=URL.createObjectURL(blob);varimg=newImage;// 이미지 로드 이벤트$(img).bind('load',function(){canvas.width=img.width;canvas.height=img.height;// 캔버스에 해당 이미지를 그린다.ctx.drawImage(img,0,0,img.width,img.height);// 각 px 에 대한 정보(r,g,b,a)가 담긴 이미지 데이터를 가져온다.originalImgData=ctx.getImageData(0,0,canvas.width,canvas.height);// 반전 효과를 준다.// invert();// 흑백 효과를 준다.empty();});// Blob 객체를 참조하는 URL을 img.src 에 할당 후 로드한다.img.src=url;// px 단위의 이미지 데이터를 조작하여, 반전 효과를 준다.functioninvert(){originalImgData=ctx.getImageData(0,(canvas.height/2),canvas.width,canvas.height);vardata=originalImgData.data;for(vari=0;i<data.length;i+=4){data[i]=255-data[i];// reddata[i+1]=255-data[i+1];// greendata[i+2]=255-data[i+2];// blue}ctx.putImageData(originalImgData,0,(canvas.height/2));};// px 단위의 이미지 데이터를 조작하여, 흑백 효과를 준다.functionempty(){originalImgData=ctx.getImageData(0,0,canvas.width,canvas.height);vardata=originalImgData.data;for(vari=0;i<data.length;i+=4){// 각 픽셀의 밝기만 조사하여 R, G, B 색상 요소를 균일하게 만들면 회색이 된다.(색상 정보를 아래 공식(각 요소(R, G, B)가 밝기에 미치는 영향은 29:58:11로 전문가에 의해 계산되어 있다)으로 R,G,B 요소에서 제거한다)// 128 이상은 흰색으로, 128 이하는 검정색으로 만들어 버림으로써, 흰색과 검정색 두 가지만 남긴다. 경계값인 128을 조정하면 밝기가 달라진다.vargray=data[i]*0.299+data[i+1]*0.587+data[i+2]*0.114;if(gray>128){gray=255;}else{gray=0;}data[i]=gray;// reddata[i+1]=gray;// greendata[i+2]=gray;// blue}ctx.putImageData(originalImgData,0,0);};
Server Side(use nodeJS)
varfs=require('fs');// 파일을 읽은 후 클라이언트로 버퍼를 전달한다.fs.readFile('./lib/img/nmms_20823487.jpg',function(err,buf){// it's possible to embed binary data// within arbitrarily-complex objectssocket.emit('onSocketMsg',{type:'resultImageData',payload:{buffer:buf}});});
서버에서 내려받은ArrayBuffer(영상데이터) 로view(in Typed Array Views) 를 생성 후, 버퍼에 저장된 데이터를 조작한다.
영상및 오디오 데이터의 경우,브라우저 지원 여부및지원 포맷에 대해 반드시 확인해봐야한다.
아래 소스는Chrome브라우저에서*.mp4및*.webm포맷으로만 테스트되었습니다.
Source Example
Cliend Side(use JS)
varvw=327;varvh=125;// video Element 를 가져온다.varvideo=document.querySelector('video');video.width=vw;video.height=vh;// view(부호 없는 1byte 정수 배열)를 생성한다.varuInt8Array=newUint8Array(payload.buffer);// view를 통해 Blob Object 를 생성한다.varblob=newBlob([uInt8Array],{type:'video/webm'});//var blob = new Blob([uInt8Array], {type: 'video/mp4'});// Blob Object를 참조하는 URL 를 생성한다.varurl=URL.createObjectURL(blob);// Blob 객체를 참조하는 URL을 video.src 에 할당 후 로드한다.video.src=url;
서버에서 Chunk 방식으로 내려받은ArrayBuffer(영상데이터) 로view(in Typed Array Views) 를 생성 후, 버퍼에 저장된 데이터를 조작한다.
MediaSource API를 통해 내려받은 영상 데이터를 조작할 수 있다.
영상및 오디오 데이터의 경우,브라우저 지원 여부및지원 포맷에 대해 반드시 확인해봐야한다.
아래 소스는Chrome브라우저에서*.webm(vorbis 및 vp8 코덱) 포맷으로만 테스트되었습니다.
Source Example
Cliend Side(use JS)
저장소로 부터 내려받은 파일을 include 한다.
<scriptsrc="socket.io-stream.js"></script>
// stream 메서드에 socket 객체를 전달 후 해당 이벤트를 바인딩한다.ss(socket).on('onSocketMsg',function(data){data=data||{};vartype=data.type;varpayload=data.payload;if(type==='resultChunkVideoData'){varvw=1024;varvh=768;// video Element 를 가져온다.varvideo=document.querySelector('video');video.width=vw;video.height=vh;console.log(payload.stream);// 내려받은 stream 데이터// MediaSource 객체를 생성한다.varms=newMediaSource();// MediaSource 객체를 참조하는 URL 를 생성한다.varurl=URL.createObjectURL(ms);// MediaSource 객체를 참조하는 URL을 video.src 에 할당 후 로드한다.video.src=url;// MediaSource 객체에 각 이벤트를 바인딩 시킨다.ms.addEventListener('sourceopen',callback,false);// ms.addEventListener('webkitsourceopen', callback, false);ms.addEventListener('sourceended',function(e){console.log('mediaSource readyState: '+this.readyState);},false);functioncallback(){// 재생하려는 영상 소스를 추가한다.varsourceBuffer=ms.addSourceBuffer('video/webm; codecs="vp8"');// var sourceBuffer = ms.addSourceBuffer('video/webm; codecs="vp8,vorbis"');sourceBuffer.addEventListener('updatestart',function(e){// console.log('updatestart: ' + ms.readyState);});sourceBuffer.addEventListener('update',function(){// console.log('update: ' + ms.readyState);},false);sourceBuffer.addEventListener('updateend',function(e){console.log('updateend: '+ms.readyState);});sourceBuffer.addEventListener('error',function(e){console.log('error: '+ms.readyState);});sourceBuffer.addEventListener('abort',function(e){console.log('abort: '+ms.readyState);});payload.stream.on('data',function(data){// chunk dataconsole.log(data);// 버퍼에 내려받은 스트림 데이터를 할당한다.sourceBuffer.appendBuffer(data);});// 데이터 전송이 완료되었을 경우 발생한다.payload.stream.on('end',function(){console.log('endOfStream call');// 스트림을 종료한다.ms.endOfStream();});}}});
Server Side(use nodeJS)
varss=require('socket.io-stream');ss(socket).on('onSocketMsg',function(data){data=data||{};vartype=data.type;varpayload=data.payload;varstream=ss.createStream();if(type==='downloadChunkVideo'){// webm 포맷의 영상을 가져온다.varfilename=path.basename('feelings_vp9-20130806-244.webm');// 파일 스트림을 생성한다.fs.createReadStream(filename).pipe(stream);ss(socket).emit('onSocketMsg',{type:'resultChunkVideoData',payload:{stream:stream}});}});