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 ini berjalan di mode Answer Mode, silahkan buka program demo, ubah terlebih dulu ke mode Answer Mode, tutorial ada di Jenis Work Mode UHF Reader .
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-3" >
< h1 class = "text-center" >Serial Port Communication</ h1 >
< div class = "text-center" >
< p >
Status: < span :class = "deviceStatus === 'Open' ? 'text-success' : 'text-danger'" >{{ deviceStatus
}}</ span >
</ p >
< p v-if = "connectedDeviceName" >
Connected Device: < span class = "text-primary" >{{ connectedDeviceName }}</ span >
</ p >
</ div >
< div class = "row justify-content-center mb-4" >
< div class = "col-auto" >
< button class = "btn" :class = "deviceStatus === 'Open' ? 'btn-danger' : 'btn-primary'" @click = "togglePort" >
{{ deviceStatus === 'Open' ? 'Close' : 'Open' }}
</ button >
</ div >
< div class = "col-auto" >
< button class = "btn" :class = "insInventory ? 'btn-warning' : 'btn-success'" @click = "toggleInventory"
:disabled = "deviceStatus != 'Open'" >
{{ insInventory ? 'Stop' : 'Start' }}
</ button >
</ div >
</ 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 ,
deviceStatus: "Closed" ,
connectedDeviceName: null // Name of the connected device
};
},
methods: {
async togglePort () {
if ( this .deviceStatus === 'Open' ) {
// Close the port
try {
await this .transport. close ();
this .deviceStatus = 'Closed' ;
this .connectedDeviceName = null ;
this .insInventory = false ; // Ensure inventory is stopped
} catch (error) {
console. error ( "Failed to close port:" , error);
alert ( "Failed to close port." );
}
} else {
// Open the port
try {
await this .transport. open ();
this .reader = new Reader ( this .transport);
this .deviceStatus = 'Open' ;
this .connectedDeviceName = this .transport. getDeviceInfo ();
} catch (error) {
console. error ( "Failed to open port:" , error);
alert ( "Failed to open port. Please check the connection." );
}
}
},
async toggleInventory () {
if ( this .insInventory) {
// Stop the inventory process
this .insInventory = false ;
} else {
// Start the inventory process
this .tags = []; // Clear tags
this .insInventory = true ;
while ( this .insInventory) {
try {
const newTags = await this .reader. inventoryAnswerMode ();
this . updateTags (newTags); // Update tag counts
await new Promise ( r => setTimeout (r, 100 ));
} catch (error) {
console. error ( "Error during inventory:" , error);
alert ( "Error during inventory process." );
this .insInventory = false ; // Stop on error
}
}
}
},
async openPort () {
try {
await this .transport. open ();
this .reader = new Reader ( this .transport);
// Update device status and name
this .deviceStatus = "Open" ;
this .connectedDeviceName = this .transport. getDeviceInfo ();
} catch (error) {
console. error ( "Failed to open port:" , error);
alert ( "Failed to open port. Please check the connection." );
}
},
async closePort () {
try {
await this .transport. close ();
// Update device status and name
this .deviceStatus = "Closed" ;
this .connectedDeviceName = null ;
this .insInventory = false ;
} catch (error) {
console. error ( "Failed to close port:" , error);
alert ( "Failed to close port." );
}
},
async start () {
// Clear tags
this .tags = [];
this .insInventory = true ;
while ( this .insInventory) {
try {
const newTags = await this .reader. inventoryAnswerMode ();
this . updateTags (newTags); // Update tag counts
await new Promise ( r => setTimeout (r, 100 ));
} catch (error) {
console. error ( "Error during inventory:" , error);
alert ( "Error during inventory process." );
}
}
},
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 || 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 ;
}
getDeviceInfo () {
if ( ! this .port) {
console. error ( "Port is not initialized." );
return null ;
}
const { usbVendorId , usbProductId } = this .port. getInfo ();
const deviceName = `Vendor ID: ${ usbVendorId }, Product ID: ${ usbProductId }` ;
return deviceName;
}
async open ( baudRate = 57600 , bufferSize = 1024 ) {
// You can set filter if need it
// { usbVendorId: 4292, usbProductId: 60000 } just for HW-VX6336 reader
// this.port = await navigator.serial.requestPort({
// filters: [{ usbVendorId: 4292, usbProductId: 60000 }]
// });
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." );
}
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: Version 131.0.6778.85 (Official Build) (64-bit)
Brave: Version 1.73.91 Chromium: 131.0.6778.85 (Official Build) (64-bit)