@alexgyver/serial 1.0.0 → 1.0.2

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/README.md CHANGED
@@ -1,4 +1,38 @@
1
1
  # Serial.js
2
- Web Serial API wrapper, [demo](https://gyverlibs.github.io/Serial.js/test/).
2
+ Обёртка на Web Serial API
3
+ - Автоматическое переподключение
4
+ - Сохранение последнего выбранного порта
5
+ - Буферизация отправки
6
+ - Буферизация приёма, разделение текста по разделителю
3
7
 
4
- > npm i @alexgyver/serial
8
+ > npm i @alexgyver/serial
9
+
10
+ ## Дока
11
+ ```js
12
+ constructor(params = {});
13
+ config(params = {});
14
+ // eol: /\r?\n/
15
+ // baud: 115200
16
+ // reconnect: 1000
17
+
18
+ onbin(b);
19
+ ontext(t);
20
+
21
+ onopen():
22
+ onclose():
23
+ onchange(s):
24
+ onselect(name);
25
+ onerror(e);
26
+
27
+ static supported();
28
+ opened();
29
+ selected();
30
+ getName();
31
+
32
+ select();
33
+ open();
34
+ close();
35
+
36
+ sendBin(data);
37
+ sendText(text);
38
+ ```
package/package.json CHANGED
@@ -1,20 +1,26 @@
1
1
  {
2
2
  "name": "@alexgyver/serial",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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.98.0",
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.3"
18
24
  },
19
25
  "author": "AlexGyver <alex@alexgyver.ru>",
20
26
  "license": "MIT"
package/serial.js CHANGED
@@ -1,28 +1,56 @@
1
+ import { sleep, ShiftBuffer, StreamSplitter } from "@alexgyver/utils";
2
+
3
+ const States = {
4
+ Open: 1,
5
+ Closing: 2,
6
+ Closed: 3,
7
+ };
8
+
1
9
  export default class SerialJS {
2
10
  //#region handlers
3
11
  onbin = null;
4
12
  ontext = null;
5
- async onopen() { }
6
- async onclose() { }
7
- async onerror(e) { }
8
- async onportchange(selected) { }
13
+
14
+ onopen() { }
15
+ onclose() { }
16
+ onchange(s) { }
17
+ onselect(name) { }
18
+ onerror(e) { }
9
19
 
10
20
  //#region constructor
11
- constructor() {
12
- this._ok = 'serial' in navigator;
13
- if (this._ok) this._update().then(() => this.onportchange(this.selected()));
14
- else this._error('Browser is not supported');
21
+ constructor(params = {}) {
22
+ const def = {
23
+ eol: /\r?\n/,
24
+ baud: 115200,
25
+ reconnect: 1000,
26
+ };
27
+ this.cfg = { ...def, ...params };
28
+
29
+ this._setLastPort().then(() => this.onselect(this.getName()));
30
+ this.splitter = new StreamSplitter(this.cfg.eol);
31
+ this.splitter.ontext = (t) => this.ontext(t);
32
+ }
33
+
34
+ config(params = {}) {
35
+ this.cfg = { ...this.cfg, ...params };
36
+ this.splitter.eol = this.cfg.eol;
15
37
  }
16
38
 
17
39
  //#region methods
40
+ static supported() {
41
+ return 'serial' in navigator;
42
+ }
43
+
18
44
  opened() {
19
- return this._open;
45
+ return this._state == States.Open;
20
46
  }
47
+
21
48
  selected() {
22
49
  return !!this._port;
23
50
  }
51
+
24
52
  getName() {
25
- if (!this._port) return 'None';
53
+ if (!this._port) return null;
26
54
 
27
55
  switch (this._port.getInfo().usbProductId) {
28
56
  case 0x55d3: return 'CH343';
@@ -36,129 +64,124 @@ export default class SerialJS {
36
64
  }
37
65
 
38
66
  async select() {
39
- if (!this._ok) return;
40
-
41
67
  try {
42
68
  await this.close();
69
+ this._port = null;
43
70
  let ports = await navigator.serial.getPorts();
44
71
  for (let p of ports) await p.forget();
72
+ await sleep(50);
45
73
  await navigator.serial.requestPort();
46
- await this._update();
74
+ await this._setLastPort();
47
75
  } catch (e) {
48
- this._port = null;
49
76
  this._error(e);
50
77
  }
51
- this.onportchange(this.selected());
78
+ this.onselect(this.getName());
79
+ return this.selected();
80
+ }
81
+
82
+ async open() {
83
+ if (this.cfg.reconnect) this.retry = true;
84
+ await this._open();
52
85
  }
53
- async open(baud = 115200) {
54
- if (!this._ok) return;
86
+ async _open() {
87
+ if (this.opened()) return;
55
88
 
56
89
  try {
57
- await this.close();
58
- await this._update();
59
- if (!this.selected()) throw "No port";
60
- try {
61
- await this._port.open({ baudRate: baud });
62
- this._open = true;
63
- this.onopen();
64
- this.reader.reset();
65
- await this._readLoop();
66
- } finally {
67
- await this._port.close();
68
- this._open = false;
69
- this.onclose();
90
+ await this._close();
91
+ await this._setLastPort();
92
+ await this._port.open({ baudRate: this.cfg.baud });
93
+ this.writer = this._port.writable.getWriter();
94
+ this.reader = this._port.readable.getReader();
95
+ this._buffer.clear();
96
+ this._state = States.Open;
97
+ this._change(true);
98
+
99
+ while (this._state == States.Open) {
100
+ const { value, done } = await this.reader.read();
101
+ if (done) break;
102
+ if (value) {
103
+ if (this.onbin) this.onbin(value);
104
+ if (this.ontext) this.splitter.write(this._decoder.decode(value, { stream: true }));
105
+ }
70
106
  }
71
107
  } catch (e) {
72
108
  this._error(e);
109
+ if (this.retry) setTimeout(() => this._open(), this.cfg.reconnect);
73
110
  }
74
- }
75
- async close() {
76
- if (!this._ok) return;
77
- if (!this._open) return;
78
111
 
79
- this._close = true;
80
- if (this._reader) await this._reader.cancel();
112
+ if (this.reader) this.reader.releaseLock();
113
+ if (this.writer) this.writer.releaseLock();
114
+ this.reader = null;
115
+ this.writer = null;
116
+ this._state = States.Closed;
81
117
 
82
- while (this._open) await new Promise(r => setTimeout(r, 10));
118
+ try {
119
+ await this._port.close();
120
+ this._change(false);
121
+ } catch (e) { }
83
122
  }
84
123
 
85
- async sendBin(data) {
86
- if (!this._ok) return;
87
- if (!this.opened()) return;
124
+ async close() {
125
+ this.retry = false;
126
+ await this._close();
127
+ }
128
+ async _close() {
129
+ switch (this._state) {
130
+ case States.Closed: return;
131
+ case States.Open:
132
+ if (this.reader) await this.reader.cancel();
133
+ this._state = States.Closing;
134
+ break;
135
+ }
88
136
 
89
- try {
90
- let writer = this._port.writable.getWriter();
91
- await writer.write(data);
92
- writer.releaseLock();
93
- } catch (e) {
94
- this._error(e);
137
+ let i = 0;
138
+ while (this._state == States.Closing) {
139
+ await sleep(10);
140
+ if (++i > 200) {
141
+ this._error('Close timeout');
142
+ this._state = States.Closed;
143
+ break;
144
+ }
95
145
  }
146
+
96
147
  }
148
+
97
149
  async sendText(text) {
98
150
  await this.sendBin((new TextEncoder()).encode(text));
99
151
  }
100
152
 
101
- reader = new SplitReader();
153
+ async sendBin(data) {
154
+ this._buffer.push(data);
155
+ this._send();
156
+ }
102
157
 
103
158
  //#region private
104
159
  _port = null;
105
- _open = false;
106
- _close = false;
107
- _reader = null;
108
-
109
- _error(e) {
110
- this.onerror('[SerialJS] ' + e);
111
- }
112
- async _update() {
113
- let ports = await navigator.serial.getPorts();
114
- this._port = ports.length ? ports[0] : null;
115
- }
116
- async _readLoop() {
117
- this._close = false;
118
- if (this._port.readable) this._reader = this._port.readable.getReader();
119
- while (this._port.readable && !this._close) {
160
+ _state = States.Closed;
161
+ _buffer = new ShiftBuffer();
162
+ _decoder = new TextDecoder();
163
+
164
+ async _send() {
165
+ if (this._busy) return;
166
+ this._busy = true;
167
+ while (this._buffer.length) {
168
+ let d = this._buffer.shiftAll();
120
169
  try {
121
- while (true) {
122
- let read = await this._reader.read();
123
- if (read.done) break;
124
- if (this.onbin) this.onbin(read.value);
125
- if (this.ontext) this.ontext(new TextDecoder().decode(read.value));
126
- if (this.reader.ontext) this.reader._write(new TextDecoder().decode(read.value));
127
- }
128
- } finally {
129
- this._reader.releaseLock();
130
- this._reader = null;
131
- }
170
+ if (this.writer) await this.writer.write(d);
171
+ } catch (e) { }
132
172
  }
173
+ this._busy = false;
133
174
  }
134
- }
135
-
136
- class SplitReader {
137
- ontext = null;
138
-
139
- setEOL(eol) {
140
- this._eol = eol;
141
- this.reset();
175
+ async _setLastPort() {
176
+ let ports = await navigator.serial.getPorts();
177
+ this._port = ports.length ? ports[0] : null;
142
178
  }
143
179
 
144
- reset() {
145
- this._buf = "";
146
- this._skip = true;
180
+ _error(e) {
181
+ this.onerror('[SerialJS] ' + e);
147
182
  }
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);
183
+ _change(s) {
184
+ this.onchange(s);
185
+ s ? this.onopen() : this.onclose();
159
186
  }
160
-
161
- _buf = "";
162
- _skip = true;
163
- _eol = /\r?\n/;
164
187
  }
package/serial.min.js CHANGED
@@ -1 +1 @@
1
- var e={d:(t,s)=>{for(var r in s)e.o(s,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:s[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};e.d(t,{A:()=>s});class s{onbin=null;ontext=null;async onopen(){}async onclose(){}async onerror(e){}async onportchange(e){}constructor(){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 e=await navigator.serial.getPorts();for(let t of e)await t.forget();await navigator.serial.requestPort(),await this._update()}catch(e){this._port=null,this._error(e)}this.onportchange(this.selected())}}async open(e=115200){if(this._ok)try{if(await this.close(),await this._update(),!this.selected())throw"No port";try{await this._port.open({baudRate:e}),this._open=!0,this.onopen(),this.reader.reset(),await this._readLoop()}finally{await this._port.close(),this._open=!1,this.onclose()}}catch(e){this._error(e)}}async close(){if(this._ok&&this._open)for(this._close=!0,this._reader&&await this._reader.cancel();this._open;)await new Promise((e=>setTimeout(e,10)))}async sendBin(e){if(this._ok&&this.opened())try{let t=this._port.writable.getWriter();await t.write(e),t.releaseLock()}catch(e){this._error(e)}}async sendText(e){await this.sendBin((new TextEncoder).encode(e))}reader=new r;_port=null;_open=!1;_close=!1;_reader=null;_error(e){this.onerror("[SerialJS] "+e)}async _update(){let e=await navigator.serial.getPorts();this._port=e.length?e[0]:null}async _readLoop(){for(this._close=!1,this._port.readable&&(this._reader=this._port.readable.getReader());this._port.readable&&!this._close;)try{for(;;){let e=await this._reader.read();if(e.done)break;this.onbin&&this.onbin(e.value),this.ontext&&this.ontext((new TextDecoder).decode(e.value)),this.reader.ontext&&this.reader._write((new TextDecoder).decode(e.value))}}finally{this._reader.releaseLock(),this._reader=null}}}class r{ontext=null;setEOL(e){this._eol=e,this.reset()}reset(){this._buf="",this._skip=!0}_write(e){this._buf+=e;let t=this._buf.split(this._eol);1!=t.length&&(t[t.length-1].length?this._buf=t.pop():(this._buf="",t.pop()),this._skip&&(t.shift(),this._skip=!1),t.map(this.ontext))}_buf="";_skip=!0;_eol=/\r?\n/}var a=t.A;export{a as default};
1
+ const t=t=>new Promise(e=>setTimeout(e,t));class e{ontext=null;constructor(t=/\r?\n/,e=!1){this.eol=t,this.skip=e}reset(){this._buf=""}write(t){if(!this.ontext)return;if(!this.eol)return void this.ontext(t);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 s{constructor(){this.chunks=[],this.length=0}clear(){this.chunks=[]}push(t){return this.chunks.push(t),this.length+=t.length,!0}shift(t){t>this.length&&(t=this.length);const e=new Uint8Array(t);let s=0;for(;t>0;){const i=this.chunks[0];t>=i.length?(e.set(i,s),s+=i.length,t-=i.length,this.chunks.shift(),this.length-=i.length):(e.set(i.subarray(0,t),s),this.chunks[0]=i.subarray(t),this.length-=t,t=0)}return e}shiftAll(){return this.shift(this.length)}}class i{onbin=null;ontext=null;onopen(){}onclose(){}onchange(t){}onselect(t){}onerror(t){}constructor(t={}){this.cfg={eol:/\r?\n/,baud:115200,reconnect:1e3,...t},this._setLastPort().then(()=>this.onselect(this.getName())),this.splitter=new e(this.cfg.eol),this.splitter.ontext=t=>this.ontext(t)}config(t={}){this.cfg={...this.cfg,...t},this.splitter.eol=this.cfg.eol}static supported(){return"serial"in navigator}opened(){return 1==this._state}selected(){return!!this._port}getName(){if(!this._port)return null;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(){try{await this.close(),this._port=null;let e=await navigator.serial.getPorts();for(let t of e)await t.forget();await t(50),await navigator.serial.requestPort(),await this._setLastPort()}catch(t){this._error(t)}return this.onselect(this.getName()),this.selected()}async open(){this.cfg.reconnect&&(this.retry=!0),await this._open()}async _open(){if(!this.opened()){try{for(await this._close(),await this._setLastPort(),await this._port.open({baudRate:this.cfg.baud}),this.writer=this._port.writable.getWriter(),this.reader=this._port.readable.getReader(),this._buffer.clear(),this._state=1,this._change(!0);1==this._state;){const{value:t,done:e}=await this.reader.read();if(e)break;t&&(this.onbin&&this.onbin(t),this.ontext&&this.splitter.write(this._decoder.decode(t,{stream:!0})))}}catch(t){this._error(t),this.retry&&setTimeout(()=>this._open(),this.cfg.reconnect)}this.reader&&this.reader.releaseLock(),this.writer&&this.writer.releaseLock(),this.reader=null,this.writer=null,this._state=3;try{await this._port.close(),this._change(!1)}catch(t){}}}async close(){this.retry=!1,await this._close()}async _close(){switch(this._state){case 3:return;case 1:this.reader&&await this.reader.cancel(),this._state=2}let e=0;for(;2==this._state;)if(await t(10),++e>200){this._error("Close timeout"),this._state=3;break}}async sendText(t){await this.sendBin((new TextEncoder).encode(t))}async sendBin(t){this._buffer.push(t),this._send()}_port=null;_state=3;_buffer=new s;_decoder=new TextDecoder;async _send(){if(!this._busy){for(this._busy=!0;this._buffer.length;){let t=this._buffer.shiftAll();try{this.writer&&await this.writer.write(t)}catch(t){}}this._busy=!1}}async _setLastPort(){let t=await navigator.serial.getPorts();this._port=t.length?t[0]:null}_error(t){this.onerror("[SerialJS] "+t)}_change(t){this.onchange(t),t?this.onopen():this.onclose()}}export{i as default};
package/test/index.html CHANGED
@@ -5,6 +5,7 @@
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
7
7
  <script src="script.js" type="module" defer="defer"></script>
8
+ <title>Serial test</title>
8
9
  </head>
9
10
 
10
11
  <body>
package/test/script.js CHANGED
@@ -3,18 +3,16 @@ import SerialJS from "https://gyverlibs.github.io/Serial.js/serial.min.js";
3
3
  let i = 0;
4
4
  let ser = new SerialJS();
5
5
 
6
+ // control
6
7
  select_b.onclick = () => ser.select();
7
8
  open_b.onclick = () => ser.open();
8
9
  close_b.onclick = () => ser.close();
9
10
  send_b.onclick = () => ser.sendText('Hello ' + i++);
10
11
 
11
- // split reader
12
- // ser.reader.setEOL(/\r?\n/); // default
13
- ser.reader.ontext = t => console.log(t);
14
-
15
- // read raw
12
+ // read
16
13
  // ser.onbin = b => console.log(b);
17
- // ser.ontext = t => console.log(t);
14
+ ser.ontext = t => console.log(t);
15
+ // ser.online = t => console.log(t);
18
16
 
19
17
  // state
20
18
  ser.onopen = () => {
@@ -26,6 +24,6 @@ ser.onclose = () => {
26
24
  ser.onerror = e => {
27
25
  console.log(e);
28
26
  }
29
- ser.onportchange = () => {
30
- console.log('port change', ser.selected(), ser.getName());
27
+ ser.onselect = (port) => {
28
+ console.log('port change', port);
31
29
  }
@@ -0,0 +1,12 @@
1
+ #include <Arduino.h>
2
+
3
+ void setup() {
4
+ Serial.begin(115200);
5
+ Serial.setTimeout(10);
6
+ }
7
+
8
+ void loop() {
9
+ if (Serial.available()) {
10
+ Serial.println(Serial.readString());
11
+ }
12
+ }
@@ -0,0 +1,18 @@
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
+ <title>Serial test</title>
9
+ </head>
10
+
11
+ <body>
12
+ <button id="select_b">select</button>
13
+ <button id="open_b">open</button>
14
+ <button id="close_b">close</button>
15
+ <button id="send_b">send</button>
16
+ </body>
17
+
18
+ </html>
@@ -0,0 +1,29 @@
1
+ import SerialJS from "../serial";
2
+
3
+ let i = 0;
4
+ let ser = new SerialJS();
5
+
6
+ // control
7
+ select_b.onclick = () => ser.select();
8
+ open_b.onclick = () => ser.open();
9
+ close_b.onclick = () => ser.close();
10
+ send_b.onclick = () => ser.sendText('Hello ' + i++);
11
+
12
+ // read
13
+ // ser.onbin = b => console.log(b);
14
+ ser.ontext = t => console.log(t);
15
+ // ser.online = t => console.log(t);
16
+
17
+ // state
18
+ ser.onopen = () => {
19
+ console.log('Opened', ser.getName());
20
+ }
21
+ ser.onclose = () => {
22
+ console.log('Closed');
23
+ }
24
+ ser.onerror = e => {
25
+ console.log(e);
26
+ }
27
+ ser.onselect = (port) => {
28
+ console.log('port change', port);
29
+ }
@@ -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
+ };