מדפסת התוויות לא מדפיסה, ארדואינו לא מגיב, חיישן תעשייתי שותק — כל זה בגלל היעדר חיבור בין הדפדפן ליציאת COM. Web Serial API פותר את זה: הדפדפן מקבל גישה ישירה ליציאה הטורית ללא התקנת דרייברים או יישומים מקומיים. אנו מספקים אינטגרציה מלאה של יציאת COM עם יישומי אינטרנט המיועדים לשליטה תעשייתית בציוד, סורקי ברקוד ופרויקטי אינטרנט של ארדואינו. אנו מיישמים פתרון זה כבר למעלה מ-5 שנים (מאז 2019) והשלמנו יותר מ-50 פרויקטים עבור חיישנים תעשייתיים, מכשור רפואי ומסופי POS. החיסכון ברישיונות מגיע ל-50%, עלויות התחזוקה מופחתות ב-40%. עלויות האינטגרציה מתחילות מ-800 דולר.
למה להשתמש ב-Web Serial API במקום ביישומים מקוריים?
זה מפשט את הפריסה פי 10: אין צורך להתקין דרייברים, אין עדכוני יישומים, אין צורך בהגדרת סביבה. המשתמש פשוט פותח דף בדפדפן Chromium ועובד. השוואה עם גישות מסורתיות:
| קריטריון | Web Serial API | Java Applet | יישום Electron |
|---|---|---|---|
| התקנה | לא נדרשת | נדרש JRE | נדרשת התקנה |
| עדכון | אוטומטי (אינטרנט) | ידני | ידני |
| אבטחה | מקור זהה, HTTPS | מוטלת בספק | תלוי בבנייה |
| תמיכה במכשירים | USB/Bluetooth/COM | COM בלבד | דרך Native Messaging |
ה-API הוא פתרון מודרני ומאובטח. אנו מיישמים אותו במפתח מלא תוך 2–4 ימים.
תמיכת דפדפנים ומגבלות
ה-API נתמך ב-Chrome 89+ (מאז מרץ 2021), Edge 89+, Opera 75+. Firefox ו-Safari לא תומכים בו. לכן, הדף חייב לדרוש דפדפן Chromium או לספק חלופה ל-Serial API.
בדיקת תמיכה:
if (!('serial' in navigator)) { throw new Error('Web Serial API не поддерживается. Используйте Chrome 89+') } הרשאת מקור: בסביבת ייצור, יש להוסיף את הכותרת if (!('serial' in navigator)) { throw new Error('Web Serial API не поддерживается. Используйте Chrome 89+') } .
הבטחת פעולה בדפדפנים לא נתמכים
אם הקהל שלך משתמש ב-Firefox או Safari, ספק חלופה. אנו מציעים שלוש גישות:
| שיטת חלופה | תיאור | מורכבות |
|---|---|---|
| קלט ידני | המשתמש מזין נתונים ידנית דרך טופס | מינימלית |
| העלאת קובץ | נתונים מיוצאים מהמכשיר לקובץ, ואז מועלים | נמוכה |
| סוכן WebSocket | סוכן מקומי על המכשיר מעביר נתונים דרך WebSocket | בינונית |
הבחירה תלויה בסוג הנתונים ובדרישות האוטומציה. לדוגמה, עבור סורק ברקוד, העלאת קובץ מספיקה; עבור שליטה אינטראקטיבית בארדואינו, יישום האינטרנט ידרוש סוכן WebSocket.
ארכיטקטורת השירות: בידוד עבודת היציאה
אנו מבודדים את כל עבודת היציאה במחלקת SerialService. רכיב ה-UI לא יודע על זרמים ומאגרים — הוא קורא למתודות השירות ומקבל נתונים דרך קריאות חוזרות או EventEmitter.
type SerialDataHandler = (data: Uint8Array) => void
type SerialErrorHandler = (error: Error) => void
interface SerialConfig {
baudRate: number
dataBits?: 7 | 8
stopBits?: 1 | 2
parity?: 'none' | 'even' | 'odd'
bufferSize?: number
flowControl?: 'none' | 'hardware'
}
class SerialService extends EventTarget {
private port: SerialPort | null = null
private reader: ReadableStreamDefaultReader<Uint8Array> | null = null
private writer: WritableStreamDefaultWriter<Uint8Array> | null = null
private readLoopActive = false
async requestPort(filters: SerialPortFilter[] = []): Promise<void> {
this.port = await navigator.serial.requestPort({ filters })
}
async connect(config: SerialConfig): Promise<void> {
if (!this.port) throw new Error('Порт не выбран')
await this.port.open({
baudRate: config.baudRate,
dataBits: config.dataBits ?? 8,
stopBits: config.stopBits ?? 1,
parity: config.parity ?? 'none',
bufferSize: config.bufferSize ?? 4096,
flowControl: config.flowControl ?? 'none',
})
this.writer = this.port.writable!.getWriter()
this.startReadLoop()
}
private async startReadLoop(): Promise<void> {
if (!this.port?.readable) return
this.readLoopActive = true
while (this.port.readable && this.readLoopActive) {
this.reader = this.port.readable.getReader()
try {
while (true) {
const { value, done } = await this.reader.read()
if (done) break
if (value) {
this.dispatchEvent(
Object.assign(new Event('data'), { detail: value })
)
}
}
} catch (error) {
if (this.readLoopActive) {
this.dispatchEvent(
Object.assign(new Event('error'), { detail: error })
)
}
} finally {
this.reader.releaseLock()
}
}
}
async write(data: Uint8Array | string): Promise<void> {
if (!this.writer) throw new Error('Порт не открыт')
const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data
await this.writer.write(bytes)
}
async disconnect(): Promise<void> {
this.readLoopActive = false
this.reader?.cancel()
this.writer?.releaseLock()
await this.port?.close()
this.port = null
this.reader = null
this.writer = null
}
get isConnected(): boolean {
return this.port !== null && this.port.readable !== null
}
}לפרטים נוספים על מתודות Serial API, עיין בתיעוד הרשמי ב-MDN.
איך לעבוד עם פרוטוקולים: מתאם לבקשה-תגובה
רוב המכשירים משתמשים בפרוטוקולים טקסטואליים או בינאריים בתקשורת UART בדפדפן. דוגמה למכשיר עם פרוטוקול בקשה-תגובה המשתמש במפרידים Permissions-Policy: serial=*:
class LineProtocolAdapter {
private buffer = ''
private pendingResolvers: Array<(line: string) => void> = []
constructor(private serial: SerialService) {
serial.addEventListener('data', (e: Event) => {
const event = e as Event & { detail: Uint8Array }
this.buffer += new TextDecoder().decode(event.detail)
this.flushLines()
})
}
private flushLines(): void {
const lines = this.buffer.split('\r\n')
this.buffer = lines.pop() ?? ''
for (const line of lines) {
if (line.trim()) {
const resolver = this.pendingResolvers.shift()
if (resolver) resolver(line.trim())
}
}
}
async sendCommand(command: string, timeoutMs = 2000): Promise<string> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.pendingResolvers = this.pendingResolvers.filter(r => r !== resolve)
reject(new Error(`Timeout: нет ответа на "${command}" за ${timeoutMs}ms`))
}, timeoutMs)
this.pendingResolvers.push((line) => {
clearTimeout(timer)
resolve(line)
})
this.serial.write(command + '\r\n').catch(reject)
})
}
}
// Использование:
const adapter = new LineProtocolAdapter(serialService)
const version = await adapter.sendCommand('VERSION')
const sensorData = await adapter.sendCommand('READ SENSOR 1')כדי לסנן מכשירים לפי USB Vendor/Product ID, השתמש ב-type SerialDataHandler = (data: Uint8Array) => void type SerialErrorHandler = (error: Error) => void interface SerialConfig { baudRate: number dataBits?: 7 | 8 stopBits?: 1 | 2 parity?: 'none' | 'even' | 'odd' bufferSize?: number flowControl?: 'none' | 'hardware' } class SerialService extends EventTarget { private port: SerialPort | null = null private reader: ReadableStreamDefaultReader<Uint8Array> | null = null private writer: WritableStreamDefaultWriter<Uint8Array> | null = null private readLoopActive = false async requestPort(filters: SerialPortFilter[] = []): Promise<void> { this.port = await navigator.serial.requestPort({ filters }) } async connect(config: SerialConfig): Promise<void> { if (!this.port) throw new Error('Порт не выбран') await this.port.open({ baudRate: config.baudRate, dataBits: config.dataBits ?? 8, stopBits: config.stopBits ?? 1, parity: config.parity ?? 'none', bufferSize: config.bufferSize ?? 4096, flowControl: config.flowControl ?? 'none', }) this.writer = this.port.writable!.getWriter() this.startReadLoop() } private async startReadLoop(): Promise<void> { if (!this.port?.readable) return this.readLoopActive = true while (this.port.readable && this.readLoopActive) { this.reader = this.port.readable.getReader() try { while (true) { const { value, done } = await this.reader.read() if (done) break if (value) { this.dispatchEvent( Object.assign(new Event('data'), { detail: value }) ) } } } catch (error) { if (this.readLoopActive) { this.dispatchEvent( Object.assign(new Event('error'), { detail: error }) ) } } finally { this.reader.releaseLock() } } } async write(data: Uint8Array | string): Promise<void> { if (!this.writer) throw new Error('Порт не открыт') const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data await this.writer.write(bytes) } async disconnect(): Promise<void> { this.readLoopActive = false this.reader?.cancel() this.writer?.releaseLock() await this.port?.close() this.port = null this.reader = null this.writer = null } get isConnected(): boolean { return this.port !== null && this.port.readable !== null } } עם אפשרות \r\n. לאחר הרשאה ראשונית, ניתן לשחזר את היציאה ללא דיאלוג דרך class LineProtocolAdapter { private buffer = '' private pendingResolvers: Array<(line: string) => void> = [] constructor(private serial: SerialService) { serial.addEventListener('data', (e: Event) => { const event = e as Event & { detail: Uint8Array } this.buffer += new TextDecoder().decode(event.detail) this.flushLines() }) } private flushLines(): void { const lines = this.buffer.split('\r\n') this.buffer = lines.pop() ?? '' for (const line of lines) { if (line.trim()) { const resolver = this.pendingResolvers.shift() if (resolver) resolver(line.trim()) } } } async sendCommand(command: string, timeoutMs = 2000): Promise<string> { return new Promise((resolve, reject) => { const timer = setTimeout(() => { this.pendingResolvers = this.pendingResolvers.filter(r => r !== resolve) reject(new Error(`Timeout: нет ответа на "${command}" за ${timeoutMs}ms`)) }, timeoutMs) this.pendingResolvers.push((line) => { clearTimeout(timer) resolve(line) }) this.serial.write(command + '\r\n').catch(reject) }) } } // Использование: const adapter = new LineProtocolAdapter(serialService) const version = await adapter.sendCommand('VERSION') const sensorData = await adapter.sendCommand('READ SENSOR 1') . זה מאפשר חיבור מחדש אוטומטי של USB. עבור פרוטוקולים בינאריים, עבוד ישירות עם navigator.serial.requestPort().
איך ליישם אינטגרציה: תוכנית שלב אחר שלב
- ניתוח פרוטוקול המכשיר. למד את התיעוד: הגדרת קצב באוד, זוגיות, פורמט פקודות. אסוף נתוני בדיקה.
- הגדר את היציאה. פתח את היציאה עם הפרמטרים הנדרשים דרך
filters. השתמש במחלקת SerialService מהדוגמה. - יישם את מתאם הפרוטוקול. עבור פרוטוקולים טקסטואליים, כתוב
navigator.serial.getPorts(); עבור פרוטוקולים בינאריים, עבוד ישירות עםUint8Array. - אינטגרציה עם UI. עטוף את השירות ב-React Serial Service (הוק
SerialPort.open()) או ב-Vue-composable. הוסף טיפול בשגיאות וחיבור מחדש. - בדיקות. בדוק על מכשיר אמיתי או אמולטור (socat/VSPE). ודא שחלופת Serial API עובדת.
טעויות אינטגרציה נפוצות
- שכחת לקצץ תווים מיותרים מהמאגר — מקבל ג'יבריש.
- לא טיפלת בניתוק המכשיר — היציאה נתקעת.
- שימוש בקצב באוד שגוי — הנתונים לא קריאים.
- לא יישמת timeout לפקודות — היישום נתקע לנצח.
מה כלול בעבודה
- ניתוח פרוטוקול של המכשיר היעד
- הגדרת פרמטרי יציאה (הגדרת קצב באוד, זוגיות, בקרת זרימה)
- יישום מחלקות SerialService ומתאם פרוטוקול
- React hook או Vue-composable
- טיפול בחיבור מחדש
- חלופה לדפדפנים לא נתמכים
- בדיקות על חומרה אמיתית או אמולטור
אם המכשיר משתמש בפרוטוקול בינארי קנייני, נדרש זמן נוסף להנדסה הפוכה או ללימוד התיעוד. אנו גם מבטיחים תמיכה בפתרון שפותח וייעוץ חינם לחודש אחד לאחר הפריסה.
לוח זמנים: 2–4 ימים בהתאם למורכבות פרוטוקול המכשיר. בצע הזמנה — המהנדסים שלנו יתאימו את הפתרון לציוד שלך. המחירים מתחילים ב-800 דולר לאינטגרציה סטנדרטית.







