Integrasi JavaScript (NodeJS) dengan UHF Reader
Keterbatasan komunikasi pada web browser pada tutorial Integrasi JavaScript (NodeJS) dengan UHF Reader seringkali menjadi kendala, terutama jika kita ingin menghubungkan perangkat melalui jaringan lokal. Di sinilah Node.js hadir sebagai solusi yang jauh lebih fleksibel. Berbeda dengan JavaScript di browser yang sangat dibatasi, Node.js memiliki akses langsung ke sistem operasi.
Dengan Node.js, kita bisa membangun aplikasi yang mampu berkomunikasi dengan perangkat reader melalui dua jalur sekaligus: menggunakan jalur Serial (RS-232) untuk perangkat yang terhubung langsung, maupun melalui TCP/IP Socket (LAN/Ethernet) untuk perangkat yang berada di jaringan. Artinya, mau pakai kabel USB to RS232 Cable biasa ataupun kabel jaringan (LAN), kita bisa mendeteksi dan mengirim data bytes tanpa perlu perantara aplikasi lain.
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.
Install Dependencies (Packages)
Sebelum mulai ngoding, pastikan sudah menginisialisasi proyek (npm init -y). Setelah itu, buka terminal di folder proyek kamu dan jalankan perintah berikut:
npm install serialport express socket.ioFolder tree
.
├── node_modules/
├── package.json
├── package-lock.json
├── public/
│ └── index.html
└── src/
├── reader/
│ ├── command.js
│ ├── main.js
│ ├── reader.js
│ ├── response.js
│ ├── transport.js
│ └── utils.js
└── server.js
public/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HW-VX Series (Node.js Backend)</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>
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<div id="app" class="container mt-4" style="max-width: 800px">
<h2 class="text-center mb-4">(Node.js) HW-VX Series - Answer Mode</h2>
<div class="text-center mb-4">
<p class="mb-1">
Server Connection:
<span
:class="state.serverConnected ? 'text-success font-weight-bold' : 'text-danger font-weight-bold'"
>
{{ state.serverConnected ? 'Online' : 'Offline' }}
</span>
</p>
<p class="mb-3">
Hardware Status:
<span
:class="state.isOpen ? 'text-success font-weight-bold' : 'text-danger font-weight-bold'"
>
{{ state.isOpen ? 'Port Open' : 'Port Closed' }}
</span>
</p>
</div>
<div class="card mb-4 shadow-sm" v-if="!state.isOpen">
<div class="card-body">
<h5 class="card-title mb-3">Connection Settings</h5>
<div class="mb-3">
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
v-model="state.config.type"
value="serial"
id="radioSerial"
/>
<label class="form-check-label" for="radioSerial"
>Serial (USB)</label
>
</div>
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
v-model="state.config.type"
value="tcp"
id="radioTCP"
/>
<label class="form-check-label" for="radioTCP">TCP/IP</label>
</div>
</div>
<div v-if="state.config.type === 'serial'" class="row">
<div class="col-md-8 mb-2">
<label class="font-weight-bold">Serial Port</label>
<div class="input-group">
<select class="form-control" v-model="state.config.serialPort">
<option
v-for="port in state.availablePorts"
:key="port"
:value="port"
>
{{ port }}
</option>
<option
v-if="state.availablePorts.length === 0"
value=""
disabled
>
No ports found...
</option>
</select>
<div class="input-group-append">
<button
class="btn btn-outline-secondary"
type="button"
@click="scanPorts"
>
Scan
</button>
</div>
</div>
</div>
<div class="col-md-4 mb-2">
<label class="font-weight-bold">Baud Rate</label>
<select class="form-control" v-model="state.config.baudRate">
<option :value="9600">9600 bps</option>
<option :value="19200">19200 bps</option>
<option :value="38400">38400 bps</option>
<option :value="57600">57600 bps</option>
<option :value="115200">115200 bps</option>
</select>
</div>
</div>
<div v-if="state.config.type === 'tcp'" class="row">
<div class="col-md-8 mb-2">
<label class="font-weight-bold">IP Address</label>
<input
type="text"
class="form-control"
v-model="state.config.ipAddress"
placeholder="192.168.1.192"
/>
</div>
<div class="col-md-4 mb-2">
<label class="font-weight-bold">Port</label>
<input
type="number"
class="form-control"
v-model="state.config.tcpPort"
/>
</div>
</div>
</div>
</div>
<div class="row justify-content-center mb-4">
<div class="col-auto">
<button
class="btn px-4"
:class="state.isOpen ? 'btn-danger' : 'btn-primary'"
@click="togglePort"
:disabled="!state.serverConnected || state.scanning"
>
{{ state.isOpen ? 'Close Port' : 'Open Port' }}
</button>
</div>
<div class="col-auto">
<button
class="btn px-4"
:class="state.scanning ? 'btn-warning' : 'btn-success'"
@click="toggleScan"
:disabled="!state.isOpen"
>
{{ state.scanning ? 'Stop Scan' : 'Start Scan' }}
</button>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body p-0">
<table class="table table-striped mb-0">
<thead class="thead-dark">
<tr>
<th class="text-center" style="width: 80px">No.</th>
<th>Tag (Hex)</th>
<th class="text-center" style="width: 120px">Count</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, idx) in tagsList" :key="row.hex">
<td class="text-center">{{ idx + 1 }}</td>
<td class="font-monospace text-primary">{{ row.hex }}</td>
<td class="text-center font-weight-bold">{{ row.count }}</td>
</tr>
<tr v-if="tagsList.length === 0">
<td colspan="3" class="text-center text-muted py-4">
No tags detected.
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const { createApp, reactive, computed, onMounted } = Vue;
createApp({
setup() {
const socket = io();
const state = reactive({
serverConnected: false,
isOpen: false,
scanning: false,
tagMap: new Map(),
availablePorts: [],
config: {
type: "serial",
serialPort: "",
baudRate: 57600,
ipAddress: "192.168.1.192",
tcpPort: 6000,
},
});
const tagsList = computed(() => {
return Array.from(state.tagMap.values()).sort(
(a, b) => b.count - a.count,
);
});
function scanPorts() {
socket.emit("get_ports");
}
onMounted(() => {
socket.on("connect", () => {
state.serverConnected = true;
scanPorts();
});
socket.on("disconnect", () => {
state.serverConnected = false;
state.isOpen = false;
state.scanning = false;
});
socket.on("ports_list", (ports) => {
state.availablePorts = ports;
if (
ports.length > 0 &&
!ports.includes(state.config.serialPort)
) {
state.config.serialPort = ports[0];
}
});
socket.on("port_status", (data) => {
state.isOpen = data.isOpen;
if (!data.isOpen) {
state.scanning = false;
}
});
socket.on("scan_status", (data) => {
state.scanning = data.scanning;
});
socket.on("tag_data", (data) => {
const hex = data.hex;
if (state.tagMap.has(hex)) {
state.tagMap.get(hex).count += 1;
} else {
state.tagMap.set(hex, { hex: hex, count: 1 });
}
});
socket.on("error", (msg) => {
alert("Error: " + msg);
});
});
function togglePort() {
if (state.isOpen) {
socket.emit("close_port");
} else {
socket.emit(
"open_port",
JSON.parse(JSON.stringify(state.config)),
);
}
}
function toggleScan() {
if (state.scanning) {
socket.emit("stop_scan");
} else {
state.tagMap.clear();
socket.emit("start_scan");
}
}
return {
state,
tagsList,
togglePort,
toggleScan,
scanPorts,
};
},
}).mount("#app");
</script>
</body>
</html>src/reader/transport.js
const net = require("net");
const { SerialPort } = require("serialport");
class Transport {
async readBytes(length) {
throw new Error("Method 'readBytes()' must be implemented.");
}
async writeBytes(buffer) {
throw new Error("Method 'writeBytes()' must be implemented.");
}
async close() {
throw new Error("Method 'close()' must be implemented.");
}
async readFrame() {
try {
const lengthBytes = await this.readBytes(1);
if (!lengthBytes || lengthBytes.length === 0) {
return null;
}
const frameLength = lengthBytes[0];
const data = await this.readBytes(frameLength);
return Buffer.concat([lengthBytes, data]);
} catch (error) {
console.error("Failed read frame:", error);
return null;
}
}
}
class TcpTransport extends Transport {
constructor(ipAddress, port) {
super();
this.socket = new net.Socket();
this.socket.on("error", (err) => {
console.error("TCP Socket Error:", err.message);
});
this.socket.connect(port, ipAddress);
}
readBytes(length) {
return new Promise((resolve, reject) => {
if (this.socket.destroyed) {
return reject(new Error("Socket is closed"));
}
const chunk = this.socket.read(length);
if (chunk !== null) return resolve(chunk);
const onReadable = () => {
const chunk = this.socket.read(length);
if (chunk !== null) {
cleanup();
resolve(chunk);
}
};
const onError = (err) => {
cleanup();
reject(err);
};
const cleanup = () => {
this.socket.removeListener("readable", onReadable);
this.socket.removeListener("error", onError);
};
this.socket.on("readable", onReadable);
this.socket.on("error", onError);
});
}
writeBytes(buffer) {
return new Promise((resolve, reject) => {
if (this.socket.destroyed) {
return reject(new Error("Socket is closed"));
}
this.socket.write(buffer, (err) => {
if (err) reject(err);
else resolve();
});
});
}
async close() {
this.socket.destroy();
}
}
class SerialTransport extends Transport {
constructor(serialPortPath, baudRate) {
super();
this.port = new SerialPort({
path: serialPortPath,
baudRate: baudRate,
});
this.port.on("error", (err) => {
console.error("Serial Port Error:", err.message);
});
}
readBytes(length) {
return new Promise((resolve, reject) => {
const chunk = this.port.read(length);
if (chunk !== null) return resolve(chunk);
const onReadable = () => {
const chunk = this.port.read(length);
if (chunk !== null) {
cleanup();
resolve(chunk);
}
};
const onError = (err) => {
cleanup();
reject(err);
};
const cleanup = () => {
this.port.removeListener("readable", onReadable);
this.port.removeListener("error", onError);
};
this.port.on("readable", onReadable);
this.port.on("error", onError);
});
}
writeBytes(buffer) {
return new Promise((resolve, reject) => {
this.port.write(buffer, (err) => {
if (err) reject(err);
else resolve();
});
});
}
async close() {
return new Promise((resolve, reject) => {
this.port.close((err) => {
if (err) reject(err);
else resolve();
});
});
}
}
module.exports = { Transport, TcpTransport, SerialTransport };src/reader/command.js
const { calculateChecksum } = require("./utils");
const CMD_INVENTORY = 0x01;
const CMD_READ_MEMORY = 0x02;
const CMD_WRITE_MEMORY = 0x03;
const CMD_SET_LOCK = 0x06;
const CMD_SET_READER_POWER = 0x2f;
const CMD_GET_WORK_MODE = 0x36;
const CMD_SET_WORK_MODE = 0x35;
class Command {
constructor(command, readerAddress = 0xff, data = null) {
this.command = command;
this.readerAddress = readerAddress;
if (typeof data === "number") {
this.data = Buffer.from([data]);
} else if (data === null || data === undefined) {
this.data = Buffer.alloc(0);
} else if (Buffer.isBuffer(data) || Array.isArray(data)) {
this.data = Buffer.from(data);
} else {
this.data = Buffer.alloc(0);
}
this.frameLength = 4 + this.data.length;
this.baseData = Buffer.concat([
Buffer.from([this.frameLength, this.readerAddress, this.command]),
this.data,
]);
}
serialize() {
const checksum = calculateChecksum(this.baseData);
return Buffer.concat([this.baseData, Buffer.from(checksum)]);
}
}
module.exports = {
CMD_INVENTORY,
CMD_READ_MEMORY,
CMD_WRITE_MEMORY,
CMD_SET_LOCK,
CMD_SET_READER_POWER,
CMD_GET_WORK_MODE,
CMD_SET_WORK_MODE,
Command,
};
src/reader/response.js
const { calculateChecksum, hexReadable } = require("./utils");
const InventoryWorkMode = Object.freeze({
ANSWER_MODE: 0,
ACTIVE_MODE: 1,
TRIGGER_MODE_LOW: 2,
TRIGGER_MODE_HIGH: 3,
});
const OutputInterface = Object.freeze({
WIEGAND: 0,
RS232_485: 1,
SYRIS485: 2,
});
const Protocol = Object.freeze({
PROTOCOL_18000_6C: 0,
PROTOCOL_18000_6B: 1,
});
const AddressType = Object.freeze({
WORD: 0,
BYTE: 1,
});
const WiegandOutputAddressing = Object.freeze({
WORD: 0,
BYTE: 1,
});
const WiegandFormat = Object.freeze({
WIEGAND_26BITS: 0,
WIEGAND_34BITS: 1,
});
const WiegandBitOrder = Object.freeze({
HIGH_BIT_FIRST: 0,
LOW_BIT_FIRST: 1,
});
const InventoryMemoryBank = Object.freeze({
PASSWORD: 0,
EPC: 1,
TID: 2,
USER: 3,
INVENTORY_MULTIPLE: 4,
INVENTORY_SINGLE: 5,
EAS_ALARM: 6,
});
function getEnumName(enumObj, value) {
return (
Object.keys(enumObj).find((key) => enumObj[key] === value) || "UNKNOWN"
);
}
class Response {
constructor(responseBytes) {
if (!Buffer.isBuffer(responseBytes) || responseBytes.length < 6) {
throw new Error("Response data is too short to be valid.");
}
this.responseBytes = responseBytes;
this.length = responseBytes[0];
if (responseBytes.length < this.length) {
throw new Error("Response length mismatch.");
}
this.readerAddress = responseBytes[1];
this.command = responseBytes[2];
this.status = responseBytes[3];
this.data = responseBytes.subarray(4, this.length - 1);
this.checksum = responseBytes.subarray(this.length - 1, this.length + 1);
// Verify checksum
const dataToCheck = Buffer.concat([
this.responseBytes.subarray(0, 4),
this.data,
]);
const [crcMsb, crcLsb] = calculateChecksum(dataToCheck);
if (this.checksum[0] !== crcMsb || this.checksum[1] !== crcLsb) {
throw new Error("Checksum assertion failed.");
}
}
toString() {
const lines = [
">>> START RESPONSE ================================",
`RESPONSE >> ${hexReadable(this.responseBytes)}`,
`READER ADDRESS >> ${hexReadable(Buffer.from([this.readerAddress]))}`,
`COMMAND >> ${hexReadable(Buffer.from([this.command]))}`,
`STATUS >> ${hexReadable(Buffer.from([this.status]))}`,
];
if (this.data && this.data.length > 0) {
lines.push(`DATA >> ${hexReadable(this.data)}`);
}
lines.push(`CHECKSUM >> ${hexReadable(this.checksum)}`);
lines.push(">>> END RESPONSE ================================");
return lines.join("\n");
}
}
class WiegandMode {
constructor(value) {
this.value = value;
this._wiegandFormat = value & 0b1;
this._bitOrder = (value & 0b10) >> 1;
}
toString() {
const formatName = getEnumName(WiegandFormat, this._wiegandFormat).replace(
/_/g,
" ",
);
const orderName = getEnumName(WiegandBitOrder, this._bitOrder).replace(
/_/g,
" ",
);
return `Wiegand Mode: ${formatName}, Bit Order: ${orderName}`;
}
get wiegandFormat() {
return this._wiegandFormat;
}
set wiegandFormat(fmt) {
this._wiegandFormat = fmt;
this.updateValue();
}
get bitOrder() {
return this._bitOrder;
}
set bitOrder(order) {
this._bitOrder = order;
this.updateValue();
}
updateValue() {
this.value = (this._wiegandFormat & 0b1) | ((this._bitOrder & 0b1) << 1);
}
}
class WorkModeState {
constructor(value) {
this.value = value;
this.protocol = value & 0b1;
this.outputInterface = (value & 0b10) >> 1;
this.beep = !(value & 0b100);
this.addressType = (value & 0b1000) >> 3;
this.rs485Enable = !!(value & 0b10000);
}
toString() {
const protocolName = getEnumName(Protocol, this.protocol).replace(
/_/g,
"-",
);
const outputName = getEnumName(
OutputInterface,
this.outputInterface,
).replace(/_/g, "/");
return (
`Protocol: ${protocolName} ` +
`| Output Mode: ${outputName} ` +
`| Address Type: ${this.addressType} | RS485: ${this.rs485Enable ? "Enabled" : "Disabled"} ` +
`| Beep/Buzzer: ${this.beep ? "Enabled" : "Disabled"}`
);
}
toInt() {
let val = 0;
val |= this.protocol;
val |= this.outputInterface << 1;
val |= this.beep ? 0 : 0b100;
val |= this.addressType << 3;
val |= (this.rs485Enable ? 1 : 0) << 4;
return val;
}
}
class WorkMode {
constructor(responseBytes) {
this.wiegandMode = new WiegandMode(responseBytes[0]);
this.wiegandInterval = responseBytes[1];
this.wiegandPulseWidth = responseBytes[2];
this.wiegandPulseInterval = responseBytes[3];
this.inventoryWorkMode = responseBytes[4];
this.workModeState = new WorkModeState(responseBytes[5]);
this.memoryBank = responseBytes[6];
this.firstAddress = responseBytes[7];
this.wordNumber = responseBytes[8];
this.singleTagTime = responseBytes[9];
this.accuracy = responseBytes[10];
this.offsetTime = responseBytes[11];
}
toString() {
return [
`Wiegand Mode: ${this.wiegandMode}`,
`Wiegand Interval: ${this.wiegandInterval}`,
`Wiegand Pulse Width: ${this.wiegandPulseWidth}`,
`Wiegand Pulse Interval: ${this.wiegandPulseInterval}`,
`Inventory Work Mode: ${getEnumName(InventoryWorkMode, this.inventoryWorkMode).replace(/_/g, " ")}`,
`Work Mode State: ${this.workModeState}`,
`Memory Bank: ${getEnumName(InventoryMemoryBank, this.memoryBank).replace(/_/g, " ")}`,
`First Address: ${this.firstAddress}`,
`Word Number: ${this.wordNumber}`,
`Single Tag Time: ${this.singleTagTime}`,
`Accuracy: ${this.accuracy}`,
`Offset Time: ${this.offsetTime}`,
].join("\n");
}
toBytes() {
return Buffer.from([
this.inventoryWorkMode,
this.workModeState.toInt(),
this.memoryBank,
this.firstAddress,
this.wordNumber,
this.singleTagTime,
]);
}
}
module.exports = {
Response,
InventoryWorkMode,
OutputInterface,
Protocol,
AddressType,
WiegandOutputAddressing,
WiegandFormat,
WiegandBitOrder,
WiegandMode,
WorkModeState,
InventoryMemoryBank,
WorkMode,
};
src/reader/utils.js
function calculateChecksum(data) {
let value = 0xffff;
for (let i = 0; i < data.length; i++) {
value ^= data[i];
for (let j = 0; j < 8; j++) {
if (value & 0x0001) {
value = (value >>> 1) ^ 0x8408;
} else {
value = value >>> 1;
}
}
}
const crcMsb = (value >>> 8) & 0xff;
const crcLsb = value & 0xff;
return Buffer.from([crcLsb, crcMsb]);
}
function hexReadable(data, bytesSeparator = " ") {
if (typeof data === "number") {
return data.toString(16).padStart(2, "0").toUpperCase();
}
return Array.from(data)
.map((byte) => byte.toString(16).padStart(2, "0").toUpperCase())
.join(bytesSeparator);
}
module.exports = {
calculateChecksum,
hexReadable,
};
src/reader/reader.js
const { Transport } = require("./transport");
const { Response, WorkMode } = require("./response");
const {
Command,
CMD_INVENTORY,
CMD_READ_MEMORY,
CMD_WRITE_MEMORY,
CMD_SET_LOCK,
CMD_SET_READER_POWER,
CMD_GET_WORK_MODE,
CMD_SET_WORK_MODE,
} = require("./command");
class Reader {
/**
* @param {Transport} transport
*/
constructor(transport) {
this.transport = transport;
}
async close() {
await this.transport.close();
}
async _sendRequest(command) {
await this.transport.writeBytes(command.serialize());
}
async _getResponse() {
return await this.transport.readFrame();
}
/**
* 8.2.1 Inventory (Answer Mode)
*/
async *inventoryAnswerMode(startAddressTid = null, lenTid = null) {
let command;
if (startAddressTid !== null && lenTid !== null) {
command = new Command(
CMD_INVENTORY,
0xff,
Buffer.from([startAddressTid, lenTid]),
);
} else {
command = new Command(CMD_INVENTORY);
}
await this._sendRequest(command);
const rawResponse = await this._getResponse();
if (!rawResponse) return;
const response = new Response(rawResponse);
const data = response.data;
if (!data || data.length === 0) {
return;
}
const tagCount = data[0];
let n = 0;
let pointer = 1;
while (n < tagCount) {
const tagLen = data[pointer];
const tagDataStart = pointer + 1;
const nextTagStart = tagDataStart + tagLen;
const tag = data.subarray(tagDataStart, nextTagStart);
yield tag;
pointer = nextTagStart;
n += 1;
}
}
/**
* Inventory Active Mode
*/
async *inventoryActiveMode() {
while (true) {
let rawResponse;
try {
rawResponse = await this._getResponse();
} catch (error) {
continue;
}
if (rawResponse === null) {
continue;
}
const response = new Response(rawResponse);
yield response;
}
}
/**
* 8.2.2 Read Data
*/
async readMemory(
epc,
memoryBank,
startAddress,
length,
accessPassword = Buffer.alloc(4),
) {
const epcLengthWord = Math.floor(epc.length / 2);
const requestData = Buffer.concat([
Buffer.from([epcLengthWord]), // EPC Length in word
epc,
Buffer.from([memoryBank, startAddress, length]),
accessPassword,
]);
const command = new Command(CMD_READ_MEMORY, 0xff, requestData);
await this._sendRequest(command);
return new Response(await this._getResponse());
}
/**
* 8.2.4 Write Data
*/
async writeMemory(
epc,
memoryBank,
startAddress,
dataToWrite,
accessPassword = Buffer.alloc(4),
) {
const dataLengthWord = Math.floor(dataToWrite.length / 2);
const epcLengthWord = Math.floor(epc.length / 2);
const requestData = Buffer.concat([
Buffer.from([dataLengthWord]), // Data length in word
Buffer.from([epcLengthWord]), // EPC Length in word
epc,
Buffer.from([memoryBank, startAddress]),
dataToWrite,
accessPassword,
]);
const command = new Command(CMD_WRITE_MEMORY, 0xff, requestData);
await this._sendRequest(command);
return new Response(await this._getResponse());
}
/**
* 8.2.6 Lock
*/
async lock(epc, select, setProtect, accessPassword) {
const epcLengthWord = Math.floor(epc.length / 2);
const parameter = Buffer.concat([
Buffer.from([epcLengthWord]),
epc,
Buffer.from([select, setProtect]),
accessPassword,
]);
const command = new Command(CMD_SET_LOCK, 0xff, parameter);
await this._sendRequest(command);
return new Response(await this._getResponse());
}
/**
* 8.4.6 Set Power
*/
async setPower(power) {
if (power < 0 || power > 30) {
throw new Error("Power must be between 0 and 30");
}
const command = new Command(
CMD_SET_READER_POWER,
0xff,
Buffer.from([power]),
);
await this._sendRequest(command);
return new Response(await this._getResponse());
}
/**
* 8.4.10 Get WorkMode
*/
async getWorkMode() {
const command = new Command(CMD_GET_WORK_MODE);
await this._sendRequest(command);
const response = new Response(await this._getResponse());
return new WorkMode(response.data);
}
/**
* 8.4.9 Set WorkMode
*/
async setWorkMode(workMode) {
const command = new Command(CMD_SET_WORK_MODE, 0xff, workMode.toBytes());
await this._sendRequest(command);
return new Response(await this._getResponse());
}
}
module.exports = { Reader };
src/reader/main.js
const { SerialTransport, TcpTransport } = require("./transport");
const { Reader } = require("./reader");
const { hexReadable } = require("./utils");
const { InventoryWorkMode } = require("./response");
async function main() {
const transport = new SerialTransport("/dev/ttyUSB0", 57600);
// const transport = new TcpTransport('192.168.1.192', 6000);
const reader = new Reader(transport);
let isRunning = true;
process.on("SIGINT", async () => {
console.log("\nCtrl + C. Exiting...");
isRunning = false;
await reader.close();
process.exit(0);
});
try {
// =========================================================
// 1. Inventory - Answer Mode
// =========================================================
const tags = reader.inventoryAnswerMode();
for await (const tag of tags) {
console.log(`Tag: ${hexReadable(tag)}`);
}
// =========================================================
// 2. Inventory - Active Mode
// =========================================================
// const responses = reader.inventoryActiveMode();
// for await (const response of responses) {
// if (!isRunning) break; // Berhenti jika Ctrl+C ditekan
// // console.log(response.toString());
// const tag = response.data;
// console.log(`Tag: ${hexReadable(tag)}`);
// }
// =========================================================
// 3. Read specific memory, read memory bank TID (0x02)
// =========================================================
// const epc = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0xAD, 0xEF, 0x01, 0x23]);
// const responseRead = await reader.readMemory(epc, 0x02, 0x00, 0x07);
// console.log(responseRead.toString());
// =========================================================
// 4. Write specific memory, write memory bank EPC (0x01)
// =========================================================
// const epc = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0xAD, 0xEF, 0x01, 0x23]);
// const dataToWrite = Buffer.from([0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x99, 0x88]);
// const responseWrite = await reader.writeMemory(epc, 0x01, 0x02, dataToWrite);
// console.log(responseWrite.toString());
// =========================================================
// 5. Lock memory bank
// =========================================================
// const epc = Buffer.from([0xE2, 0x00, 0xA8, 0x32, 0xFE, 0x55, 0x83, 0x91, 0xCE, 0x26, 0x89, 0xAB]);
// const accessPassword = Buffer.alloc(4); // Sama dengan bytes(4) atau 00 00 00 00
// const responseLock = await reader.lock(epc, 0x02, 0x02, accessPassword);
// console.log(responseLock.toString());
// =========================================================
// 6. Set power
// =========================================================
// const power = 28;
// const responseSetPower = await reader.setPower(power);
// console.log(responseSetPower.toString());
// =========================================================
// 7. Get & set work mode
// =========================================================
// const workMode = await reader.getWorkMode();
// console.log(workMode.toString());
//
// // Set buzzer On/Off only works when inventory work mode is set to ACTIVE_MODE
// workMode.workModeState.beep = true;
// workMode.inventoryWorkMode = InventoryWorkMode.ACTIVE_MODE;
// const responseSetWorkMode = await reader.setWorkMode(workMode);
// console.log(responseSetWorkMode.toString());
//
// // If you want to set back to ANSWER_MODE, you can use the following code
// workMode.inventoryWorkMode = InventoryWorkMode.ANSWER_MODE;
// const responseSetAnswerMode = await reader.setWorkMode(workMode);
// console.log(responseSetAnswerMode.toString());
} catch (error) {
console.error("Error:", error);
} finally {
if (isRunning) {
await reader.close();
}
}
}
main();
src/server.js
const express = require("express");
const http = require("http");
const { Server } = require("socket.io");
const path = require("path");
const { SerialPort } = require("serialport");
const { SerialTransport, TcpTransport } = require("./reader/transport");
const { Reader } = require("./reader/reader");
const { hexReadable } = require("./reader/utils");
const app = express();
const server = http.createServer(app);
const io = new Server(server);
app.use(express.static(path.join(__dirname, "../public")));
let transport = null;
let reader = null;
let isReading = false;
io.on("connection", (socket) => {
console.log("Browser connected");
socket.on("get_ports", async () => {
try {
const ports = await SerialPort.list();
const portPaths = ports.map((port) => port.path);
socket.emit("ports_list", portPaths);
} catch (error) {
console.error("Failed to list ports:", error);
}
});
socket.on("open_port", async (config) => {
try {
if (transport) {
await transport.close();
transport = null;
}
if (config.type === "serial") {
transport = new SerialTransport(
config.serialPort,
parseInt(config.baudRate),
);
} else if (config.type === "tcp") {
transport = new TcpTransport(
config.ipAddress,
parseInt(config.tcpPort),
);
}
reader = new Reader(transport);
socket.emit("port_status", { isOpen: true, message: "Port Opened" });
} catch (error) {
socket.emit("error", "Failed to open port: " + error.message);
}
});
socket.on("close_port", async () => {
try {
isReading = false;
if (transport) {
await transport.close();
transport = null;
reader = null;
}
socket.emit("port_status", { isOpen: false, message: "Port Closed" });
} catch (error) {
console.error(error);
}
});
socket.on("start_scan", async () => {
if (!reader || isReading) return;
isReading = true;
socket.emit("scan_status", { scanning: true });
const scanLoop = async () => {
while (isReading) {
try {
const tags = reader.inventoryAnswerMode();
for await (const tag of tags) {
if (!isReading) break;
const tagHex = hexReadable(tag, "");
socket.emit("tag_data", { hex: tagHex });
}
await new Promise((resolve) => setTimeout(resolve, 50));
} catch (error) {
console.error("Scan error:", error.message);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
socket.emit("scan_status", { scanning: false });
};
scanLoop();
});
socket.on("stop_scan", () => {
isReading = false;
});
socket.on("disconnect", () => {
isReading = false;
console.log("Browser disconnected");
});
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});
Running
1. Debugging (Via Terminal/CLI)
Buat yang pengen sat-set ngecek apakah reader RFID sudah terhubung dan bisa baca tag dengan benar tanpa perlu buka browser.
node src/reader/main.jsOutput:
$ node src/reader/main.js
Tag: FF 00 00 00 00 00 00 00 00 00 00 01
Tag: FF 00 00 00 00 00 00 00 00 00 00 02
...2. Web UI
Kalau pengen lihat hasil scan-nya tampil rapi dan interaktif di halaman web, kamu harus menyalakan web server-nya.
node src/server.js