@alexgyver/serial 1.0.10 → 1.0.12

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.
@@ -0,0 +1,32 @@
1
+
2
+ name: Publish to PlatformIO Registry
3
+
4
+ on:
5
+ release:
6
+ types: [published]
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout release tag
14
+ uses: actions/checkout@v4
15
+ with:
16
+ ref: ${{ github.event.release.tag_name }}
17
+
18
+ - name: Set up Python
19
+ uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.x"
22
+
23
+ - name: Install PlatformIO
24
+ run: pip install -U platformio
25
+
26
+ - name: Check package
27
+ run: pio pkg pack
28
+
29
+ - name: Publish to PlatformIO Registry
30
+ env:
31
+ PLATFORMIO_AUTH_TOKEN: ${{ secrets.PLATFORMIO_AUTH_TOKEN }}
32
+ run: pio pkg publish --owner gyverlibs --non-interactive
@@ -0,0 +1,35 @@
1
+
2
+ name: Telegram Message
3
+ on:
4
+ release:
5
+ types: [published]
6
+ jobs:
7
+ build:
8
+ name: Send Message
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - name: Check if major or minor release
12
+ id: semver
13
+ shell: bash
14
+ run: |
15
+ TAG="${{ github.event.release.tag_name }}"
16
+ VERSION="${TAG#v}"
17
+ PATCH="$(echo "$VERSION" | cut -d. -f3)"
18
+
19
+ if [[ "$PATCH" == "0" ]]; then
20
+ echo "should_send=true" >> "$GITHUB_OUTPUT"
21
+ else
22
+ echo "should_send=false" >> "$GITHUB_OUTPUT"
23
+ fi
24
+
25
+ - name: send telegram message on release
26
+ if: steps.semver.outputs.should_send == 'true'
27
+ uses: appleboy/telegram-action@master
28
+ with:
29
+ to: ${{ secrets.TELEGRAM_TO }}
30
+ token: ${{ secrets.TELEGRAM_TOKEN }}
31
+ disable_web_page_preview: true
32
+ message: |
33
+ ${{ github.event.repository.name }} ${{ github.event.release.tag_name }}
34
+ ${{ github.event.release.body }}
35
+ https://github.com/${{ github.repository }}
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # Serial.js
2
2
  Обёртка на Web Serial API
3
3
  - Автоматическое переподключение
4
- - Сохранение последнего выбранного порта
4
+ - Восстановление ранее разрешённого порта
5
+ - Безопасная работа с несколькими разрешёнными портами
5
6
  - Буферизация отправки
6
7
  - Буферизация приёма, разделение текста по разделителю
7
8
 
@@ -19,6 +20,7 @@ config(params = {});
19
20
  // baud: 115200
20
21
  // auto_open: false
21
22
  // reconnect: 1000
23
+ // forgetOtherPorts: true
22
24
 
23
25
  onbin(b);
24
26
  ontext(t);
@@ -32,12 +34,24 @@ onerror(e);
32
34
  static supported();
33
35
  opened();
34
36
  selected();
37
+ getInfo();
35
38
  getName();
36
39
 
37
40
  select();
41
+ restore();
42
+ getPorts();
43
+ forget();
38
44
  open();
39
45
  close();
40
46
 
41
47
  sendBin(data);
42
48
  sendText(text);
43
- ```
49
+ ```
50
+
51
+ `select()` использует порт, возвращённый системным диалогом выбора. По умолчанию `forgetOtherPorts: true` - разрешение остаётся только у выбранного порта, поэтому после перезагрузки страницы его можно восстановить автоматически.
52
+
53
+ Для приложений, работающих с несколькими портами, нужен `forgetOtherPorts: false`. В этом режиме библиотека не отзывает чужие разрешения, а `restore()` автоматически выбирает порт только тогда, когда разрешён ровно один. Если портов несколько, надо вызвать `select()`.
54
+
55
+ - `getPorts()` возвращает все ранее разрешённые порты
56
+ - `restore()` восстанавливает порт без системного диалога, если выбор однозначен
57
+ - `forget()` закрывает текущий порт и отзывает только его разрешение
package/README_EN.md ADDED
@@ -0,0 +1,45 @@
1
+ This is an automatic translation and may be incorrect in some places. See the source README and examples for authoritative information.
2
+
3
+ # Serial.js
4
+ Wrapper on Web Serial API
5
+ - Automatic reconnection
6
+ - Preservation of the last selected port
7
+ - Shipment buffering
8
+ - Reception buffering, separation of text by separator
9
+
10
+ [demo](https://gyverlibs.github.io/Serial.js/test/)
11
+
12
+ > **Browser**: https://gyverlibs.github.io/Serial.js/Serial.min.js
13
+
14
+ > **Node**: npm i @alexgyver/serial
15
+
16
+ ## Doc.
17
+ ```js
18
+ constructor(params = {});
19
+ config(params = {});
20
+ // eol: /\r?\n/
21
+ // baud: 115200
22
+ // auto_open: false
23
+ // reconnect: 1000
24
+
25
+ onbin(b);
26
+ ontext(t);
27
+
28
+ onopen():
29
+ onclose():
30
+ onchange(s):
31
+ onselect(name);
32
+ onerror(e);
33
+
34
+ static supported();
35
+ opened();
36
+ selected();
37
+ getName();
38
+
39
+ select();
40
+ open();
41
+ close();
42
+
43
+ sendBin(data);
44
+ sendText(text);
45
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexgyver/serial",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Web Serial API wrapper",
5
5
  "main": "./serial.js",
6
6
  "module": "./serial.js",
@@ -14,14 +14,14 @@
14
14
  "url": "git+https://github.com/GyverLibs/Serial.js.git"
15
15
  },
16
16
  "devDependencies": {
17
- "webpack": "^5.106.2",
18
- "webpack-cli": "^7.0.2",
19
- "webpack-dev-server": "^5.2.4",
17
+ "webpack": "^5.110.2",
18
+ "webpack-cli": "^7.2.3",
19
+ "webpack-dev-server": "^6.0.0",
20
20
  "style-loader": "^4.0.0",
21
- "css-loader": "^7.1.4"
21
+ "css-loader": "^7.1.5"
22
22
  },
23
23
  "dependencies": {
24
- "@alexgyver/utils": "^1.2.8"
24
+ "@alexgyver/utils": "^1.2.18"
25
25
  },
26
26
  "author": "AlexGyver <alex@alexgyver.ru>",
27
27
  "license": "MIT"
package/serial.js CHANGED
@@ -25,10 +25,11 @@ export default class SerialJS {
25
25
  baud: 115200,
26
26
  auto_open: false,
27
27
  reconnect: 1000,
28
+ forgetOtherPorts: true,
28
29
  };
29
30
  this.cfg = { ...def, ...params };
30
31
 
31
- this._setLastPort().then(() => this.onselect(this.getName()));
32
+ this.restore().then(() => this.onselect(this.getName()));
32
33
  this.splitter = new StreamSplitter(this.cfg.eol);
33
34
  this.splitter.ontext = (t) => {
34
35
  try {
@@ -57,10 +58,14 @@ export default class SerialJS {
57
58
  return !!this._port;
58
59
  }
59
60
 
61
+ getInfo() {
62
+ return this._port?.getInfo() ?? null;
63
+ }
64
+
60
65
  getName() {
61
- if (!this._port) return null;
66
+ if (!this._port) return 'None';
62
67
 
63
- switch (this._port.getInfo().usbProductId) {
68
+ switch (this.getInfo().usbProductId) {
64
69
  case 0x55d3: return 'CH343';
65
70
  case 0x7584: return 'CH340S';
66
71
  case 0x7522: case 0x7523: return 'CH340';
@@ -74,48 +79,87 @@ export default class SerialJS {
74
79
  async select() {
75
80
  try {
76
81
  await this.close();
77
- this._port = null;
78
- let ports = await navigator.serial.getPorts();
79
- for (let p of ports) await p.forget();
80
- await sleep(50);
81
- await navigator.serial.requestPort();
82
- await this._setLastPort();
82
+ const port = await navigator.serial.requestPort();
83
+
84
+ if (this.cfg.forgetOtherPorts) {
85
+ const ports = await navigator.serial.getPorts();
86
+ for (const p of ports) {
87
+ if (p !== port) await p.forget();
88
+ }
89
+ }
90
+
91
+ this._port = port;
83
92
  } catch (e) {
84
93
  this._error(e);
85
94
  }
86
95
  this.onselect(this.getName());
87
- if (this.cfg.auto_open) this.open();
96
+ if (this.cfg.auto_open) return this.open();
97
+ return this.selected();
98
+ }
99
+
100
+ async getPorts() {
101
+ return navigator.serial.getPorts();
102
+ }
103
+
104
+ async restore() {
105
+ const ports = await this.getPorts();
106
+ this._port = this.cfg.forgetOtherPorts
107
+ ? (ports[0] ?? null)
108
+ : (ports.length === 1 ? ports[0] : null);
88
109
  return this.selected();
89
110
  }
90
111
 
112
+ async forget() {
113
+ await this.close();
114
+ if (!this._port) return false;
115
+
116
+ const port = this._port;
117
+ this._port = null;
118
+
119
+ try {
120
+ await port.forget();
121
+ this.onselect(this.getName());
122
+ return true;
123
+ } catch (e) {
124
+ this._port = port;
125
+ this._error(e);
126
+ return false;
127
+ }
128
+ }
129
+
91
130
  async open() {
92
- const ports = await navigator.serial.getPorts();
93
- if (!ports.length) return false;
131
+ if (this.opened()) return true;
132
+ if (this._openPromise) return this._openPromise;
133
+ if (this._state !== SerialJS.State.Closed) return false;
94
134
 
95
135
  if (this.cfg.reconnect) this.retry = true;
96
136
 
97
- this._open();
98
- return true;
137
+ const promise = this._connect();
138
+ this._openPromise = promise;
139
+ const result = await promise;
140
+ if (this._openPromise === promise) this._openPromise = null;
141
+ return result;
99
142
  }
100
- async _open() {
101
- if (this._state !== SerialJS.State.Closed) return;
102
-
143
+ async _connect() {
103
144
  this._change(SerialJS.State.Opening);
104
145
 
105
146
  try {
106
- await this._setLastPort();
147
+ if (!this._port) await this.restore();
107
148
 
108
149
  if (!this._port) {
109
150
  throw new Error('No port');
110
151
  }
111
152
 
112
- if (this._state === SerialJS.State.Closing) return;
153
+ if (this._state === SerialJS.State.Closing) {
154
+ await this._cleanup();
155
+ return false;
156
+ }
113
157
 
114
158
  await this._port.open({ baudRate: this.cfg.baud });
115
159
 
116
160
  if (this._state === SerialJS.State.Closing) {
117
- await this._port.close();
118
- return;
161
+ await this._cleanup();
162
+ return false;
119
163
  }
120
164
 
121
165
  this.writer = this._port.writable.getWriter();
@@ -123,7 +167,18 @@ export default class SerialJS {
123
167
 
124
168
  this._sender.reset();
125
169
  this._change(SerialJS.State.Open);
170
+ void this._listen();
171
+ return true;
172
+ } catch (e) {
173
+ this._error(e);
174
+ }
175
+
176
+ await this._cleanup();
177
+ return false;
178
+ }
126
179
 
180
+ async _listen() {
181
+ try {
127
182
  while (this._state === SerialJS.State.Open) {
128
183
  const { value, done } = await this.reader.read();
129
184
 
@@ -147,6 +202,10 @@ export default class SerialJS {
147
202
  this._error(e);
148
203
  }
149
204
 
205
+ await this._cleanup();
206
+ }
207
+
208
+ async _cleanup() {
150
209
  this._sender.reset();
151
210
 
152
211
  try {
@@ -168,7 +227,7 @@ export default class SerialJS {
168
227
 
169
228
  if (this.retry) {
170
229
  setTimeout(() => {
171
- if (this.retry) this._open();
230
+ if (this.retry) this.open();
172
231
  }, this.cfg.reconnect);
173
232
  }
174
233
  }
@@ -219,11 +278,16 @@ export default class SerialJS {
219
278
  async sendBin(data) {
220
279
  if (!this.opened() || !this.writer) return false;
221
280
 
222
- return this._sender.runNothrow(async () => {
223
- if (!this.opened() || !this.writer) return false;
224
- await this.writer.write(data);
225
- return true;
226
- });
281
+ try {
282
+ return await this._sender.run(async () => {
283
+ if (!this.opened() || !this.writer) return false;
284
+ await this.writer.write(data);
285
+ return true;
286
+ });
287
+ } catch (e) {
288
+ if (this.opened()) this._error(e);
289
+ return false;
290
+ }
227
291
  }
228
292
 
229
293
  //#region private
@@ -231,11 +295,7 @@ export default class SerialJS {
231
295
  _decoder = new TextDecoder();
232
296
  _sender = new SerialExecutor();
233
297
  _state = SerialJS.State.Closed;
234
-
235
- async _setLastPort() {
236
- let ports = await navigator.serial.getPorts();
237
- this._port = ports.length ? ports[0] : null;
238
- }
298
+ _openPromise = null;
239
299
 
240
300
  _error(e) {
241
301
  this.onerror('[SerialJS] ' + e);
@@ -251,4 +311,4 @@ export default class SerialJS {
251
311
  case SerialJS.State.Closed: this.onclose(); break;
252
312
  }
253
313
  }
254
- }
314
+ }
package/serial.min.js CHANGED
@@ -1 +1 @@
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._session=0,this.reset()}run(t){const e=this._session,s=this._queue.then(()=>{if(e!==this._session)throw new Error("Queue empty");return t()});return this._queue=s.catch(()=>{}),s}runNothrow(t){return this.run(t).catch(()=>null)}reset(){this._session++,this._queue=Promise.resolve()}}class r{static State={Closed:"closed",Opening:"opening",Open:"open",Closing:"closing"};onbin=null;ontext=null;onopen(){}onclose(){}onchange(t){}onselect(t){}onerror(t){}constructor(t={}){this.cfg={eol:/\r?\n/,baud:115200,auto_open:!1,reconnect:1e3,...t},this._setLastPort().then(()=>this.onselect(this.getName())),this.splitter=new e(this.cfg.eol),this.splitter.ontext=t=>{try{this.ontext?.(t)}catch(t){this._error(t)}}}config(t={}){this.cfg={...this.cfg,...t},this.splitter.eol=this.cfg.eol}static supported(){return"serial"in navigator}opened(){return this._state==r.State.Open}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.cfg.auto_open&&this.open(),this.selected()}async open(){return!!(await navigator.serial.getPorts()).length&&(this.cfg.reconnect&&(this.retry=!0),this._open(),!0)}async _open(){if(this._state===r.State.Closed){this._change(r.State.Opening);try{if(await this._setLastPort(),!this._port)throw new Error("No port");if(this._state===r.State.Closing)return;if(await this._port.open({baudRate:this.cfg.baud}),this._state===r.State.Closing)return void await this._port.close();for(this.writer=this._port.writable.getWriter(),this.reader=this._port.readable.getReader(),this._sender.reset(),this._change(r.State.Open);this._state===r.State.Open;){const{value:t,done:e}=await this.reader.read();if(e)break;if(t){try{this.onbin?.(t)}catch(t){this._error(t)}this.ontext&&this.splitter.write(this._decoder.decode(t,{stream:!0}))}}}catch(t){this._error(t)}this._sender.reset();try{this.reader&&this.reader.releaseLock()}catch(t){}try{this.writer&&this.writer.releaseLock()}catch(t){}this.reader=null,this.writer=null;try{this._port&&await this._port.close()}catch(t){}this._change(r.State.Closed),this.retry&&setTimeout(()=>{this.retry&&this._open()},this.cfg.reconnect)}}async close(){return this.retry=!1,this._sender.reset(),await this._close(),!0}async _close(){switch(this._state){case r.State.Closed:return;case r.State.Opening:this._change(r.State.Closing);break;case r.State.Open:if(this._change(r.State.Closing),this.reader)try{await this.reader.cancel()}catch(t){}}let e=0;for(;this._state===r.State.Closing;)if(await t(10),++e>200){this._error("Close timeout"),this._change(r.State.Closed);break}}async sendText(t){return this.sendBin((new TextEncoder).encode(t))}async sendBin(t){return!(!this.opened()||!this.writer)&&this._sender.runNothrow(async()=>!(!this.opened()||!this.writer||(await this.writer.write(t),0)))}_port=null;_decoder=new TextDecoder;_sender=new s;_state=r.State.Closed;async _setLastPort(){let t=await navigator.serial.getPorts();this._port=t.length?t[0]:null}_error(t){this.onerror("[SerialJS] "+t)}_change(t){if(this._state!==t)switch(this._state=t,this.onchange(t),t){case r.State.Open:this.onopen();break;case r.State.Closed:this.onclose()}}}export{r 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._session=0,this.reset()}run(t){const e=this._session,s=this._queue.then(()=>{if(e!==this._session)throw new Error("Queue empty");return t()});return this._queue=s.catch(()=>{}),s}runNothrow(t){return this.run(t).catch(()=>null)}reset(){this._session++,this._queue=Promise.resolve()}}class r{static State={Closed:"closed",Opening:"opening",Open:"open",Closing:"closing"};onbin=null;ontext=null;onopen(){}onclose(){}onchange(t){}onselect(t){}onerror(t){}constructor(t={}){this.cfg={eol:/\r?\n/,baud:115200,auto_open:!1,reconnect:1e3,forgetOtherPorts:!0,...t},this.restore().then(()=>this.onselect(this.getName())),this.splitter=new e(this.cfg.eol),this.splitter.ontext=t=>{try{this.ontext?.(t)}catch(t){this._error(t)}}}config(t={}){this.cfg={...this.cfg,...t},this.splitter.eol=this.cfg.eol}static supported(){return"serial"in navigator}opened(){return this._state==r.State.Open}selected(){return!!this._port}getInfo(){return this._port?.getInfo()??null}getName(){if(!this._port)return"None";switch(this.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();const t=await navigator.serial.requestPort();if(this.cfg.forgetOtherPorts){const e=await navigator.serial.getPorts();for(const s of e)s!==t&&await s.forget()}this._port=t}catch(t){this._error(t)}return this.onselect(this.getName()),this.cfg.auto_open?this.open():this.selected()}async getPorts(){return navigator.serial.getPorts()}async restore(){const t=await this.getPorts();return this._port=this.cfg.forgetOtherPorts?t[0]??null:1===t.length?t[0]:null,this.selected()}async forget(){if(await this.close(),!this._port)return!1;const t=this._port;this._port=null;try{return await t.forget(),this.onselect(this.getName()),!0}catch(e){return this._port=t,this._error(e),!1}}async open(){if(this.opened())return!0;if(this._openPromise)return this._openPromise;if(this._state!==r.State.Closed)return!1;this.cfg.reconnect&&(this.retry=!0);const t=this._connect();this._openPromise=t;const e=await t;return this._openPromise===t&&(this._openPromise=null),e}async _connect(){this._change(r.State.Opening);try{if(this._port||await this.restore(),!this._port)throw new Error("No port");return this._state===r.State.Closing?(await this._cleanup(),!1):(await this._port.open({baudRate:this.cfg.baud}),this._state===r.State.Closing?(await this._cleanup(),!1):(this.writer=this._port.writable.getWriter(),this.reader=this._port.readable.getReader(),this._sender.reset(),this._change(r.State.Open),this._listen(),!0))}catch(t){this._error(t)}return await this._cleanup(),!1}async _listen(){try{for(;this._state===r.State.Open;){const{value:t,done:e}=await this.reader.read();if(e)break;if(t){try{this.onbin?.(t)}catch(t){this._error(t)}this.ontext&&this.splitter.write(this._decoder.decode(t,{stream:!0}))}}}catch(t){this._error(t)}await this._cleanup()}async _cleanup(){this._sender.reset();try{this.reader&&this.reader.releaseLock()}catch(t){}try{this.writer&&this.writer.releaseLock()}catch(t){}this.reader=null,this.writer=null;try{this._port&&await this._port.close()}catch(t){}this._change(r.State.Closed),this.retry&&setTimeout(()=>{this.retry&&this.open()},this.cfg.reconnect)}async close(){return this.retry=!1,this._sender.reset(),await this._close(),!0}async _close(){switch(this._state){case r.State.Closed:return;case r.State.Opening:this._change(r.State.Closing);break;case r.State.Open:if(this._change(r.State.Closing),this.reader)try{await this.reader.cancel()}catch(t){}}let e=0;for(;this._state===r.State.Closing;)if(await t(10),++e>200){this._error("Close timeout"),this._change(r.State.Closed);break}}async sendText(t){return this.sendBin((new TextEncoder).encode(t))}async sendBin(t){if(!this.opened()||!this.writer)return!1;try{return await this._sender.run(async()=>!(!this.opened()||!this.writer||(await this.writer.write(t),0)))}catch(t){return this.opened()&&this._error(t),!1}}_port=null;_decoder=new TextDecoder;_sender=new s;_state=r.State.Closed;_openPromise=null;_error(t){this.onerror("[SerialJS] "+t)}_change(t){if(this._state!==t)switch(this._state=t,this.onchange(t),t){case r.State.Open:this.onopen();break;case r.State.Closed:this.onclose()}}}export{r as default};