Integrasi JavaScript (Web Browser) dengan UHF Reader §
Kini, dengan perkembangan teknologi web, JavaScript di browser pun dapat mengakses perangkat serial melalui API modern yang disebut Web Serial API . Salah satu metode utama dalam API ini adalah navigator.serial
, yang memungkinkan aplikasi web untuk berkomunikasi dengan perangkat serial secara langsung tanpa perlu aplikasi tambahan.
navigator.serial
adalah bagian dari Web Serial API yang memungkinkan aplikasi web untuk mengakses dan berkomunikasi dengan reader Electron yang terhubung melalui port serial, dengan API ini kita dapat mendeteksi perangkat, read & write data bytes.
Kode §
Kode JavaScript yang dibuat dapat disesuai kebutuhan, pada tutorial ini kami menggunakan Reader HW-VX6336 (HW-VX63 Series ), sehingga kode JavaScript disesuaikan dengan dokumentasi protokol Reader HW-VX63 Series .
Jika menggunakan reader jenis lain, seperti EL-UHF-RC4 Series dapat disesuaikan class untuk pengiriman dan penerimaan data bytes berdasarkan format protokol reader tersebut.
Folder tree §
.
├── index.html
└── reader.js
index.html
§
<! DOCTYPE html >
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title >Serial Communication</ title >
< link rel = "stylesheet" href = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" >
< script src = "https://unpkg.com/vue@3/dist/vue.global.js" ></ script >
</ head >
< body >
< div id = "app" class = "container mt-5" >
< h1 class = "text-center" >Serial Port Communication</ h1 >
< div class = "text-center mb-4" >
< button class = "btn btn-primary mr-1" @click = "openPort" >Open</ button >
< button class = "btn btn-danger" @click = "closePort" >Close</ button >
</ div >
< div class = "text-center mb-4" >
< button class = "btn btn-success mr-1" @click = "start" :disabled = "insInventory" >Start</ button >
< button class = "btn btn-warning" @click = "stop" :disabled = "!insInventory" >Stop</ button >
</ div >
< div >
< h4 >Received Tags:</ h4 >
< table class = "table table-striped" >
< thead >
< tr >
< th >No.</ th >
< th >Tag (Hex)</ th >
< th >Count</ th >
</ tr >
</ thead >
< tbody >
< tr v-for = "(tag, index) in tags" :key = "index" >
< td >{{ index+1 }}</ td >
< td >{{ tagHex(tag.data) }}</ td >
< td >{{ tag.count }}</ td >
</ tr >
</ tbody >
</ table >
</ div >
</ div >
< script type = "module" >
import { SerialTransport, Reader } from './reader.js' ;
const app = Vue. createApp ({
data () {
return {
transport: new SerialTransport (),
reader: null ,
tags: [], // Store tags with counts
insInventory: false
};
},
methods: {
async openPort () {
await this .transport. open ();
this .reader = new Reader ( this .transport);
},
async closePort () {
await this .transport. close ();
this .insInventory = false ;
},
async start () {
// Clear tags
this .tags = [];
this .insInventory = true ;
while ( this .insInventory) {
const newTags = await this .reader. inventoryAnswerMode ();
this . updateTags (newTags); // Update tag counts
await new Promise ( r => setTimeout (r, 100 ));
}
},
stop () {
this .insInventory = false ;
},
tagHex ( tag ) {
return Array. from (tag)
. map ( byte => byte. toString ( 16 ). padStart ( 2 , '0' ). toUpperCase ())
. join ( ' ' ); // Convert tag to hex string
},
updateTags ( newTags ) {
if (newTags == null || newTags == undefined ) {
return ;
}
if ( ! newTags || newTags. length === 0 ) {
return ;
}
newTags. forEach ( tag => {
const existingTag = this .tags. find ( t => this . tagHex (t.data) === this . tagHex (tag));
if (existingTag) {
existingTag.count ++ ; // Increment count if tag already exists
} else {
this .tags. push ({ data: tag, count: 1 }); // Add new tag with count 1
}
});
}
}
});
app. mount ( '#app' );
</ script >
</ body >
</ html >
reader.js
§
export class SerialTransport {
constructor () {
this .port = null ;
this .reader = null ;
}
async open ( baudRate = 57600 , bufferSize = 1024 ) {
try {
this .port = await navigator.serial. requestPort ();
await this .port. open ({ baudRate, bufferSize });
this .reader = this .port.readable?. getReader ({ mode: "byob" });
console. log ( "Port opened successfully." );
} catch (error) {
console. error ( "Error opening the port:" , error);
}
}
async close () {
if ( ! this .port) {
return ;
}
if ( this .reader) {
await this .reader. releaseLock ();
}
await this .port?. close ();
console. log ( "Port closed successfully." );
}
async clear () {
if ( ! this .reader) {
console. error ( "Reader is not initialized." );
return ;
}
try {
const controller = new AbortController ();
const signal = controller.signal;
// Set a timeout to abort the read operation after 100ms (adjust as needed)
const timeoutId = setTimeout (() => {
controller. abort (); // Abort the read operation
console. log ( "Clear operation timed out, moving on..." );
}, 100 );
// Attempt to read from the port
const { value : bytes } = await this .reader. read ( new Uint8Array ( 1024 ), { signal });
// Clear the timeout if read is successful or times out
clearTimeout (timeoutId);
if ( ! bytes || bytes. length === 0 ) {
console. log ( "No data in buffer, clearing completed." );
return ;
}
// Log the cleared data if any
console. log ( "Buffer cleared:" , bytes);
} catch (error) {
if (error.name === 'AbortError' ) {
// This means the read operation was aborted due to timeout
console. log ( "Clear operation aborted due to timeout, moving on..." );
} else {
// Log other errors
console. error ( "Error during buffer clear:" , error);
}
}
}
async read ( size ) {
if ( ! this .reader) {
console. error ( "Reader is not initialized." );
return null ;
}
try {
const controller = new AbortController ();
const signal = controller.signal;
// Set a timeout to abort the read operation
const timeoutId = setTimeout (() => {
controller. abort ();
console. error ( "Read operation timed out." );
}, 1000 );
const { value : bytes } = await this .reader. read ( new Uint8Array (size), { signal });
clearTimeout (timeoutId); // Clear timeout if read is successful
return bytes;
} catch (error) {
if (error.name === 'AbortError' ) {
console. error ( "Read operation was aborted due to timeout." );
} else {
console. error ( "Error during read:" , error);
}
return null ;
}
}
async write ( data ) {
try {
const writer = this .port?.writable?. getWriter ();
if ( ! writer) {
console. error ( "Unable to write: Port not writable." );
return ;
}
await writer. write (data);
await writer. close ();
} catch (error) {
console. error ( "Error during write:" , error);
}
}
}
export class Reader {
constructor ( transport ) {
this .transport = transport;
}
// Please refer to protocol documentation, how data bytes are read and parsed.
// This is using the HW-VX series reader.
async inventoryAnswerMode () {
try {
// Send Inventory, please check the documentation
const inventoryCommand = new Uint8Array ([ 0x04 , 0xFF , 0x01 , 0x1B , 0xB4 ]);
await this .transport. write (inventoryCommand);
// Read first byte to get the length of next bytes
const firstByte = await this .transport. read ( 1 );
const byteLength = firstByte?.[ 0 ];
if ( ! byteLength) {
console. error ( "Invalid first byte length." );
return null ;
}
if (byteLength == 0 ) {
return null ;
}
// Read additional bytes as indicated by the first byte
const dataBytes = await this .transport. read (byteLength);
if ( ! dataBytes) {
console. error ( "Failed to read additional bytes." );
return null ;
}
// Combine first byte and the additional bytes
const frame = new Uint8Array (firstByte.byteLength + dataBytes.byteLength);
frame. set (firstByte);
frame. set (dataBytes, firstByte.byteLength);
// Parse to Response class
const response = new Response (frame);
if ( ! response. validateChecksum ()) {
console. log ( "Checksum is not valid!" );
console. log ( "Clearing buffer..." );
await this .transport. clear ();
console. log ( "Buffer cleared. Proceeding..." );
return [];
}
const data = response.data;
if (data. length === 0 ) {
return [];
}
const tags = [];
const tagCount = data[ 0 ]; // The first byte indicates the number of tags
console. log ( "Tag count: " + tagCount);
let n = 0 ;
let pointer = 1 ; // Start reading after the tagCount byte
while (n < tagCount) {
const tagLen = data[pointer]; // Length of the tag
const tagDataStart = pointer + 1 ; // Start of the tag data
const tagMainStart = tagDataStart; // Start of the main tag
const tagMainEnd = tagMainStart + tagLen; // End of the main tag
const nextTagStart = tagMainEnd; // Start of the next tag
// Create a new Uint8Array for the tag
const tag = new Uint8Array ([
... data. subarray (tagDataStart, tagMainStart),
... data. subarray (tagMainStart, tagMainEnd),
... data. subarray (nextTagStart, nextTagStart)
]);
if (tag. length == 0 ) {
break ;
}
tags. push (tag); // Add the tag to the list
console. log ( "Tag ke-" + (n + 1 ) + ": " + Array. from (tag)
. map ( byte => byte. toString ( 16 ). padStart ( 2 , '0' ). toUpperCase ())
. join ( ' ' ));
pointer = nextTagStart; // Move the pointer to the next tag
n += 1 ; // Increment the tag count
}
return tags;
} catch (error) {
console. error ( "Error in inventoryAnswerMode:" , error);
return null ;
}
}
}
class Response {
constructor ( responseBytes ) {
if ( ! (responseBytes instanceof Uint8Array )) {
throw new TypeError ( "Expected responseBytes to be an instance of Uint8Array." );
}
this .responseBytes = responseBytes;
this . length = responseBytes[ 0 ];
this .readerAddress = responseBytes[ 1 ];
this .command = responseBytes[ 2 ];
this .status = responseBytes[ 3 ];
this .data = responseBytes. slice ( 4 , - 2 );
this .checksum = responseBytes. slice ( - 2 );
}
getChecksumValue () {
return ( this .checksum[ 1 ] << 8 ) | this .checksum[ 0 ];
}
validateChecksum () {
const calculatedChecksum = this . calculateChecksum ( this .responseBytes. slice ( 0 , - 2 ));
return this . getChecksumValue () === calculatedChecksum;
}
calculateChecksum ( data ) {
let value = 0xFFFF ;
for ( let byte of data) {
value ^= byte;
for ( let i = 0 ; i < 8 ; i ++ ) {
value = (value & 0x0001 ) !== 0 ? (value >> 1 ) ^ 0x8408 : value >> 1 ;
}
}
return value;
}
}
Demo §
Telah di uji pada browser:
Chrome: Versi 129.0.6668.100 (Official Build) (64-bit)
Brave: Versi 1.70.126 Chromium: 129.0.6668.100 (Official Build) unknown (64-bit)