@alexgyver/serial 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -4
- package/serial.js +31 -40
- package/serial.min.js +1 -1
- package/test_dev/index.html +17 -0
- package/test_dev/script.js +32 -0
- package/webpack.dev.config.js +39 -0
package/package.json
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexgyver/serial",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Web Serial API wrapper",
|
|
5
5
|
"main": "./serial.js",
|
|
6
6
|
"module": "./serial.js",
|
|
7
7
|
"types": "./serial.js",
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build": "webpack --config ./webpack.config.js"
|
|
9
|
+
"build": "webpack --config ./webpack.config.js",
|
|
10
|
+
"dev": "webpack serve --config ./webpack.dev.config.js & webpack --config ./webpack.dev.config.js"
|
|
10
11
|
},
|
|
11
12
|
"repository": {
|
|
12
13
|
"type": "git",
|
|
13
14
|
"url": "git+https://github.com/GyverLibs/Serial.js.git"
|
|
14
15
|
},
|
|
15
16
|
"devDependencies": {
|
|
16
|
-
"webpack": "^5.
|
|
17
|
-
"webpack-cli": "^6.0.1"
|
|
17
|
+
"webpack": "^5.103.0",
|
|
18
|
+
"webpack-cli": "^6.0.1",
|
|
19
|
+
"webpack-dev-server": "^5.2.2",
|
|
20
|
+
"html-webpack-plugin": "^5.6.5"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@alexgyver/utils": "^1.2.2"
|
|
18
24
|
},
|
|
19
25
|
"author": "AlexGyver <alex@alexgyver.ru>",
|
|
20
26
|
"license": "MIT"
|
package/serial.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { StreamSplitter } from "@alexgyver/utils";
|
|
2
|
+
|
|
1
3
|
export default class SerialJS {
|
|
2
4
|
//#region handlers
|
|
3
5
|
onbin = null;
|
|
4
6
|
ontext = null;
|
|
7
|
+
online = null;
|
|
8
|
+
|
|
5
9
|
async onopen() { }
|
|
6
10
|
async onclose() { }
|
|
7
11
|
async onerror(e) { }
|
|
@@ -9,6 +13,9 @@ export default class SerialJS {
|
|
|
9
13
|
|
|
10
14
|
//#region constructor
|
|
11
15
|
constructor() {
|
|
16
|
+
this.splitter = new StreamSplitter();
|
|
17
|
+
this.splitter.ontext = (t) => this.online(t);
|
|
18
|
+
|
|
12
19
|
this._ok = 'serial' in navigator;
|
|
13
20
|
if (this._ok) this._update().then(() => this.onportchange(this.selected()));
|
|
14
21
|
else this._error('Browser is not supported');
|
|
@@ -18,9 +25,11 @@ export default class SerialJS {
|
|
|
18
25
|
opened() {
|
|
19
26
|
return this._open;
|
|
20
27
|
}
|
|
28
|
+
|
|
21
29
|
selected() {
|
|
22
30
|
return !!this._port;
|
|
23
31
|
}
|
|
32
|
+
|
|
24
33
|
getName() {
|
|
25
34
|
if (!this._port) return 'None';
|
|
26
35
|
|
|
@@ -42,6 +51,7 @@ export default class SerialJS {
|
|
|
42
51
|
await this.close();
|
|
43
52
|
let ports = await navigator.serial.getPorts();
|
|
44
53
|
for (let p of ports) await p.forget();
|
|
54
|
+
await new Promise(r => setTimeout(r, 50));
|
|
45
55
|
await navigator.serial.requestPort();
|
|
46
56
|
await this._update();
|
|
47
57
|
} catch (e) {
|
|
@@ -50,6 +60,7 @@ export default class SerialJS {
|
|
|
50
60
|
}
|
|
51
61
|
this.onportchange(this.selected());
|
|
52
62
|
}
|
|
63
|
+
|
|
53
64
|
async open(baud = 115200) {
|
|
54
65
|
if (!this._ok) return;
|
|
55
66
|
|
|
@@ -61,7 +72,7 @@ export default class SerialJS {
|
|
|
61
72
|
await this._port.open({ baudRate: baud });
|
|
62
73
|
this._open = true;
|
|
63
74
|
this.onopen();
|
|
64
|
-
this.reader.reset();
|
|
75
|
+
// this.reader.reset();
|
|
65
76
|
await this._readLoop();
|
|
66
77
|
} finally {
|
|
67
78
|
await this._port.close();
|
|
@@ -72,6 +83,7 @@ export default class SerialJS {
|
|
|
72
83
|
this._error(e);
|
|
73
84
|
}
|
|
74
85
|
}
|
|
86
|
+
|
|
75
87
|
async close() {
|
|
76
88
|
if (!this._ok) return;
|
|
77
89
|
if (!this._open) return;
|
|
@@ -79,7 +91,11 @@ export default class SerialJS {
|
|
|
79
91
|
this._close = true;
|
|
80
92
|
if (this._reader) await this._reader.cancel();
|
|
81
93
|
|
|
82
|
-
|
|
94
|
+
const t0 = performance.now();
|
|
95
|
+
while (this._open) {
|
|
96
|
+
if (performance.now() - t0 > 2000) this._error("Close timeout");
|
|
97
|
+
await new Promise(r => setTimeout(r, 10));
|
|
98
|
+
}
|
|
83
99
|
}
|
|
84
100
|
|
|
85
101
|
async sendBin(data) {
|
|
@@ -94,12 +110,11 @@ export default class SerialJS {
|
|
|
94
110
|
this._error(e);
|
|
95
111
|
}
|
|
96
112
|
}
|
|
113
|
+
|
|
97
114
|
async sendText(text) {
|
|
98
115
|
await this.sendBin((new TextEncoder()).encode(text));
|
|
99
116
|
}
|
|
100
117
|
|
|
101
|
-
reader = new SplitReader();
|
|
102
|
-
|
|
103
118
|
//#region private
|
|
104
119
|
_port = null;
|
|
105
120
|
_open = false;
|
|
@@ -115,15 +130,21 @@ export default class SerialJS {
|
|
|
115
130
|
}
|
|
116
131
|
async _readLoop() {
|
|
117
132
|
this._close = false;
|
|
118
|
-
|
|
133
|
+
const decoder = new TextDecoder();
|
|
134
|
+
|
|
119
135
|
while (this._port.readable && !this._close) {
|
|
136
|
+
this._reader = this._port.readable.getReader();
|
|
120
137
|
try {
|
|
121
138
|
while (true) {
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
if (this.
|
|
126
|
-
if (this.
|
|
139
|
+
const { done, value } = await this._reader.read();
|
|
140
|
+
if (done) return;
|
|
141
|
+
|
|
142
|
+
if (this.onbin) this.onbin(value);
|
|
143
|
+
if (this.ontext || this.online) {
|
|
144
|
+
const text = decoder.decode(value);
|
|
145
|
+
if (this.ontext) this.ontext(text);
|
|
146
|
+
if (this.online) this.splitter.write(text);
|
|
147
|
+
}
|
|
127
148
|
}
|
|
128
149
|
} finally {
|
|
129
150
|
this._reader.releaseLock();
|
|
@@ -131,34 +152,4 @@ export default class SerialJS {
|
|
|
131
152
|
}
|
|
132
153
|
}
|
|
133
154
|
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
class SplitReader {
|
|
137
|
-
ontext = null;
|
|
138
|
-
|
|
139
|
-
setEOL(eol) {
|
|
140
|
-
this._eol = eol;
|
|
141
|
-
this.reset();
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
reset() {
|
|
145
|
-
this._buf = "";
|
|
146
|
-
this._skip = true;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
_write(str) {
|
|
150
|
-
this._buf += str;
|
|
151
|
-
let t = this._buf.split(this._eol);
|
|
152
|
-
if (t.length == 1) return;
|
|
153
|
-
|
|
154
|
-
if (t[t.length - 1].length) this._buf = t.pop();
|
|
155
|
-
else this._buf = "", t.pop();
|
|
156
|
-
|
|
157
|
-
if (this._skip) t.shift(), this._skip = false;
|
|
158
|
-
t.map(this.ontext);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
_buf = "";
|
|
162
|
-
_skip = true;
|
|
163
|
-
_eol = /\r?\n/;
|
|
164
155
|
}
|
package/serial.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
class t{ontext=null;constructor(t=/\r?\n/,e=!1){this._eol=t,this._skip=e}reset(){this._buf=""}write(t){if(!this.ontext)return;this._buf+=t;let e=this._buf.split(this._eol);1!=e.length&&(e[e.length-1].length?this._buf=e.pop():(this._buf="",e.pop()),this._skip&&(e.shift(),this._skip=!1),e.forEach(t=>this.ontext(t)))}_buf=""}class e{onbin=null;ontext=null;online=null;async onopen(){}async onclose(){}async onerror(t){}async onportchange(t){}constructor(){this.splitter=new t,this.splitter.ontext=t=>this.online(t),this._ok="serial"in navigator,this._ok?this._update().then(()=>this.onportchange(this.selected())):this._error("Browser is not supported")}opened(){return this._open}selected(){return!!this._port}getName(){if(!this._port)return"None";switch(this._port.getInfo().usbProductId){case 21971:return"CH343";case 30084:return"CH340S";case 29986:case 29987:return"CH340";case 21778:case 21795:case 21892:return"CH341";case 1026:case 1027:case 1028:case 1029:case 24577:case 1538:case 24592:return"FT232";case 38144:case 258:case 1281:case 32937:case 6e4:case 60001:case 60003:return"CP210x"}return"Unknown"}async select(){if(this._ok){try{await this.close();let t=await navigator.serial.getPorts();for(let e of t)await e.forget();await new Promise(t=>setTimeout(t,50)),await navigator.serial.requestPort(),await this._update()}catch(t){this._port=null,this._error(t)}this.onportchange(this.selected())}}async open(t=115200){if(this._ok)try{if(await this.close(),await this._update(),!this.selected())throw"No port";try{await this._port.open({baudRate:t}),this._open=!0,this.onopen(),await this._readLoop()}finally{await this._port.close(),this._open=!1,this.onclose()}}catch(t){this._error(t)}}async close(){if(!this._ok)return;if(!this._open)return;this._close=!0,this._reader&&await this._reader.cancel();const t=performance.now();for(;this._open;)performance.now()-t>2e3&&this._error("Close timeout"),await new Promise(t=>setTimeout(t,10))}async sendBin(t){if(this._ok&&this.opened())try{let e=this._port.writable.getWriter();await e.write(t),e.releaseLock()}catch(t){this._error(t)}}async sendText(t){await this.sendBin((new TextEncoder).encode(t))}_port=null;_open=!1;_close=!1;_reader=null;_error(t){this.onerror("[SerialJS] "+t)}async _update(){let t=await navigator.serial.getPorts();this._port=t.length?t[0]:null}async _readLoop(){this._close=!1;const t=new TextDecoder;for(;this._port.readable&&!this._close;){this._reader=this._port.readable.getReader();try{for(;;){const{done:e,value:s}=await this._reader.read();if(e)return;if(this.onbin&&this.onbin(s),this.ontext||this.online){const e=t.decode(s);this.ontext&&this.ontext(e),this.online&&this.splitter.write(e)}}}finally{this._reader.releaseLock(),this._reader=null}}}}export{e as default};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
7
|
+
<script src="script.js" type="module" defer="defer"></script>
|
|
8
|
+
</head>
|
|
9
|
+
|
|
10
|
+
<body>
|
|
11
|
+
<button id="select_b">select</button>
|
|
12
|
+
<button id="open_b">open</button>
|
|
13
|
+
<button id="close_b">close</button>
|
|
14
|
+
<button id="send_b">send</button>
|
|
15
|
+
</body>
|
|
16
|
+
|
|
17
|
+
</html>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import SerialJS from "../serial";
|
|
2
|
+
|
|
3
|
+
let i = 0;
|
|
4
|
+
let ser = new SerialJS();
|
|
5
|
+
|
|
6
|
+
select_b.onclick = () => ser.select();
|
|
7
|
+
open_b.onclick = () => ser.open();
|
|
8
|
+
close_b.onclick = () => ser.close();
|
|
9
|
+
send_b.onclick = () => ser.sendText('Hello ' + i++);
|
|
10
|
+
|
|
11
|
+
// split reader
|
|
12
|
+
// ser.reader.setEOL(/\r?\n/); // default
|
|
13
|
+
// ser.reader.ontext = t => console.log(t);
|
|
14
|
+
|
|
15
|
+
// read raw
|
|
16
|
+
// ser.onbin = b => console.log(b);
|
|
17
|
+
// ser.ontext = t => console.log(t);
|
|
18
|
+
ser.online = t => console.log(t);
|
|
19
|
+
|
|
20
|
+
// state
|
|
21
|
+
ser.onopen = () => {
|
|
22
|
+
console.log('Opened', ser.getName());
|
|
23
|
+
}
|
|
24
|
+
ser.onclose = () => {
|
|
25
|
+
console.log('Closed');
|
|
26
|
+
}
|
|
27
|
+
ser.onerror = e => {
|
|
28
|
+
console.log(e);
|
|
29
|
+
}
|
|
30
|
+
ser.onportchange = () => {
|
|
31
|
+
console.log('port change', ser.selected(), ser.getName());
|
|
32
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const PACKAGE = require('./package.json');
|
|
3
|
+
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
entry: {
|
|
7
|
+
index: './test_dev/script.js',
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
output: {
|
|
11
|
+
filename: 'script.js',
|
|
12
|
+
path: path.resolve(__dirname, 'dev'),
|
|
13
|
+
clean: true,
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
plugins: [
|
|
17
|
+
new HtmlWebpackPlugin({
|
|
18
|
+
template: `./test_dev/index.html`,
|
|
19
|
+
filename: `index.html`,
|
|
20
|
+
inject: true,
|
|
21
|
+
minify: false,
|
|
22
|
+
version: PACKAGE.version,
|
|
23
|
+
title: PACKAGE.title,
|
|
24
|
+
}),
|
|
25
|
+
],
|
|
26
|
+
|
|
27
|
+
devServer: {
|
|
28
|
+
watchFiles: ['test_dev/*.html'],
|
|
29
|
+
static: path.resolve(__dirname, './dev'),
|
|
30
|
+
hot: true,
|
|
31
|
+
open: true,
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
watchOptions: {
|
|
35
|
+
poll: 1000,
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
mode: 'development',
|
|
39
|
+
};
|