├── .gitignore ├── LICENSE ├── README.md ├── deno.json ├── deps.ts ├── examples ├── blink.ts ├── blink_sketch.ino └── print_ports.ts ├── mod.ts ├── src ├── common │ ├── port_info.ts │ ├── serial_port.ts │ ├── util.ts │ └── web_serial.ts ├── darwin │ ├── aio.ts │ ├── corefoundation.ts │ ├── enumerate.ts │ ├── iokit.ts │ ├── mod.ts │ ├── nix.ts │ ├── serial_port.ts │ └── termios.ts ├── linux │ ├── enumerate.ts │ ├── mod.ts │ └── serial_port.ts ├── serial_port.ts ├── unix │ └── mod.ts └── windows │ ├── deps.ts │ ├── enumerate.ts │ ├── mod.ts │ └── serial_port.ts └── test.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .DS_Store 3 | deno.lock 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2022-2023 DjDeveloperr 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deno SerialPort 2 | 3 | [![Tags](https://img.shields.io/github/release/DjDeveloperr/deno_serial)](https://github.com/DjDeveloperr/deno_serial/releases) 4 | [![License](https://img.shields.io/github/license/DjDeveloperr/deno_serial)](https://github.com/DjDeveloperr/deno_serial/blob/master/LICENSE) 5 | [![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/DjDeveloperr) 6 | 7 | Serial Port API for Deno with zero third-party native dependencies. 8 | 9 | | Platform | `getPorts` | `open` | 10 | | -------- | ---------- | ------ | 11 | | Windows | ✅ | ✅ | 12 | | macOS | ❌ | ❌ | 13 | | Linux | ❌ | ❌ | 14 | 15 | ## Try out 16 | 17 | Run the following to list all available ports: 18 | 19 | ```sh 20 | deno run --unstable --allow-ffi -r https://raw.githubusercontent.com/DjDeveloperr/deno_serial/main/examples/print_ports.ts 21 | ``` 22 | 23 | NOTE: Not yet published to deno.land/x as not all platforms are supported yet. 24 | 25 | ## Usage 26 | 27 | ```ts 28 | import { getPorts, open } from "https://deno.land/x/serialport@0.1.0/mod.ts"; 29 | 30 | const ports = getPorts(); 31 | console.log("Ports:", ports); 32 | 33 | const port = open({ name: ports[0].name, baudRate: 9600 }); 34 | 35 | // ... 36 | 37 | port.close(); 38 | ``` 39 | 40 | ## License 41 | 42 | Apache-2.0. Check [LICENSE](./LICENSE) for more information. 43 | 44 | Copyright © 2022-2023 DjDeveloperr 45 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "print-ports": "deno run --allow-ffi --unstable examples/print_ports.ts" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export * from "https://raw.githubusercontent.com/DjDeveloperr/struct/55ad2ee3ecd8b47e61e2857f94f2cebc644a5ecc/mod.ts"; 2 | -------------------------------------------------------------------------------- /examples/blink.ts: -------------------------------------------------------------------------------- 1 | import { getPorts, open, USBPortInfo } from "../mod.ts"; 2 | 3 | const portInfo = getPorts().find((port) => port.type === "USB") as USBPortInfo; 4 | if (!portInfo) { 5 | throw new Error("No serial ports found."); 6 | } 7 | 8 | const port = open({ name: portInfo.name, baudRate: 9600 }); 9 | console.log("Opened port:", portInfo.friendlyName); 10 | 11 | let on = false; 12 | const size = new Uint8Array(1); 13 | const buffer = new Uint8Array(64); 14 | 15 | (async () => { 16 | while (true) { 17 | await port.read(size); 18 | const n = await port.read(buffer.subarray(0, size[0])); 19 | console.log( 20 | "read:", 21 | new TextDecoder().decode(buffer.subarray(0, n!)), 22 | ); 23 | } 24 | })(); 25 | 26 | setInterval(async () => { 27 | on = !on; 28 | await port.write(new Uint8Array([on ? 0x01 : 0x02])); 29 | }, 1000); 30 | -------------------------------------------------------------------------------- /examples/blink_sketch.ino: -------------------------------------------------------------------------------- 1 | void setup() { 2 | pinMode(LED_BUILTIN, OUTPUT); 3 | Serial.begin(9600); 4 | } 5 | 6 | void loop() { 7 | while (Serial.available() > 0) { 8 | int msg = Serial.read(); 9 | if (msg == 1) { 10 | digitalWrite(LED_BUILTIN, HIGH); 11 | Serial.println("LED ON"); 12 | } else if (msg == 2) { 13 | digitalWrite(LED_BUILTIN, LOW); 14 | Serial.println("LED OFF"); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/print_ports.ts: -------------------------------------------------------------------------------- 1 | import { getPorts } from "../mod.ts"; 2 | 3 | const ports = getPorts(); 4 | 5 | if (ports.length === 0) { 6 | console.log("No serial ports found."); 7 | } else { 8 | console.log("Available ports:"); 9 | for (const port of ports) { 10 | switch (port.type) { 11 | case "USB": 12 | console.log( 13 | "- USB:", 14 | `${port.name} (${port.friendlyName}, ${port.manufacturer}, SN: ${port.serialNumber}, VID: ${port.vendorId}, PID: ${port.productId})`, 15 | ); 16 | break; 17 | case "Bluetooth": 18 | console.log("- Bluetooth:", port.name); 19 | break; 20 | case "PCI": 21 | console.log("- PCI:", port.name); 22 | break; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./src/serial_port.ts"; 2 | export * from "./src/common/serial_port.ts"; 3 | export * from "./src/common/port_info.ts"; 4 | -------------------------------------------------------------------------------- /src/common/port_info.ts: -------------------------------------------------------------------------------- 1 | export type PortType = "USB" | "Bluetooth" | "PCI"; 2 | 3 | export interface BasePortInfo { 4 | type: PortType; 5 | name: string; 6 | } 7 | 8 | export interface USBPortInfo extends BasePortInfo { 9 | type: "USB"; 10 | vendorId: number; 11 | productId: number; 12 | manufacturer: string; 13 | friendlyName: string; 14 | serialNumber: string; 15 | } 16 | 17 | export interface BluetoothPortInfo extends BasePortInfo { 18 | type: "Bluetooth"; 19 | } 20 | 21 | export interface PCIPortInfo extends BasePortInfo { 22 | type: "PCI"; 23 | } 24 | 25 | export type PortInfo = USBPortInfo | BluetoothPortInfo | PCIPortInfo; 26 | -------------------------------------------------------------------------------- /src/common/serial_port.ts: -------------------------------------------------------------------------------- 1 | export enum DataBits { 2 | FIVE = 5, 3 | SIX = 6, 4 | SEVEN = 7, 5 | EIGHT = 8, 6 | } 7 | 8 | export enum Parity { 9 | NONE = 0, 10 | ODD = 1, 11 | EVEN = 2, 12 | } 13 | 14 | export enum StopBits { 15 | ONE = 0, 16 | TWO = 2, 17 | } 18 | 19 | export enum FlowControl { 20 | NONE, 21 | SOFTWARE, 22 | HARDWARE, 23 | } 24 | 25 | export interface SerialOpenOptions { 26 | name: string; 27 | baudRate: number; 28 | dataBits?: DataBits; 29 | stopBits?: StopBits; 30 | parity?: Parity; 31 | flowControl?: FlowControl; 32 | timeout?: number; 33 | } 34 | 35 | export enum ClearBuffer { 36 | INPUT, 37 | OUTPUT, 38 | ALL, 39 | } 40 | 41 | export interface SerialPort { 42 | readonly name?: string; 43 | 44 | baudRate: number; 45 | dataBits: DataBits; 46 | stopBits: StopBits; 47 | parity: Parity; 48 | flowControl: FlowControl; 49 | timeout: number; 50 | 51 | read(p: Uint8Array): Promise; 52 | write(p: Uint8Array): Promise; 53 | 54 | writeRequestToSend(level: boolean): void; 55 | writeDataTerminalReady(level: boolean): void; 56 | 57 | readClearToSend(): boolean; 58 | readDataSetReady(): boolean; 59 | readRingIndicator(): boolean; 60 | readCarrierDetect(): boolean; 61 | 62 | bytesToRead(): number; 63 | bytesToWrite(): number; 64 | 65 | clear(buffer: ClearBuffer): void; 66 | 67 | setBreak(): void; 68 | clearBreak(): void; 69 | 70 | flush(): void; 71 | 72 | close(): void; 73 | } 74 | -------------------------------------------------------------------------------- /src/common/util.ts: -------------------------------------------------------------------------------- 1 | export function cString(str: string): Uint8Array { 2 | const encoder = new TextEncoder(); 3 | return encoder.encode(str + "\0"); 4 | } 5 | 6 | export function refptr() { 7 | return new Uint8Array(8); 8 | } 9 | 10 | export function deref(ptr: Uint8Array): Deno.PointerValue { 11 | return Deno.UnsafePointer.create(new BigUint64Array(ptr.buffer)[0]); 12 | } 13 | 14 | export class Deferred { 15 | promise: Promise; 16 | resolve!: () => void; 17 | reject!: () => void; 18 | 19 | constructor() { 20 | this.promise = new Promise((resolve, reject) => { 21 | this.resolve = resolve; 22 | this.reject = reject; 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/common/web_serial.ts: -------------------------------------------------------------------------------- 1 | export interface Serial { 2 | getPorts(): Promise; 3 | } 4 | 5 | export interface SerialPortInfo { 6 | name: string; 7 | friendlyName?: string; 8 | manufacturer?: string; 9 | usbVendorId?: number; 10 | usbProductId?: number; 11 | serialNumber?: string; 12 | } 13 | 14 | export type ParityType = "none" | "even" | "odd"; 15 | export type FlowControlType = "none" | "software" | "hardware"; 16 | 17 | export interface SerialOptions { 18 | baudRate: number; 19 | dataBits?: number; 20 | stopBits?: number; 21 | parity?: ParityType; 22 | bufferSize?: number; 23 | flowControl?: FlowControlType; 24 | } 25 | 26 | export interface SerialOutputSignals { 27 | dataTerminalReady?: boolean; 28 | requestToSend?: boolean; 29 | break?: boolean; 30 | } 31 | 32 | export interface SerialInputSignals { 33 | dataCarrierDetect: boolean; 34 | ringIndicator: boolean; 35 | dataSetReady: boolean; 36 | clearToSend: boolean; 37 | } 38 | 39 | export interface SerialPort extends EventTarget { 40 | getInfo(): Promise; 41 | 42 | open(options: SerialOptions): Promise; 43 | 44 | readonly readable: ReadableStream | null; 45 | readonly writable: WritableStream | null; 46 | 47 | setSignals(signals: SerialOutputSignals): Promise; 48 | getSignals(): Promise; 49 | 50 | close(): Promise; 51 | } 52 | -------------------------------------------------------------------------------- /src/darwin/aio.ts: -------------------------------------------------------------------------------- 1 | import nix, { unwrap } from "./nix.ts"; 2 | 3 | export class AIOCB { 4 | data: Uint8Array; 5 | 6 | constructor(fd: number, data: Uint8Array) { 7 | this.data = new Uint8Array(80); 8 | const view = new DataView(this.data.buffer); 9 | view.setUint32(0, fd, true); 10 | const ptr = Deno.UnsafePointer.of(data); 11 | const ptrVal = Deno.UnsafePointer.value(ptr); 12 | view.setBigUint64(16, BigInt(ptrVal), true); 13 | view.setBigUint64(24, BigInt(data.byteLength), true); 14 | } 15 | 16 | read() { 17 | const result = nix.aio_read(this.data); 18 | unwrap(result); 19 | } 20 | 21 | write() { 22 | const result = nix.aio_write(this.data); 23 | unwrap(result); 24 | } 25 | 26 | async suspend(timeout1?: number, timeout2?: number) { 27 | const result = await nix.aio_suspend( 28 | this.data ?? new Uint8Array( 29 | new BigUint64Array([ 30 | BigInt(Deno.UnsafePointer.value(Deno.UnsafePointer.of(this.data))), 31 | ]).buffer, 32 | ), 33 | 1, 34 | timeout1 !== undefined && timeout2 !== undefined 35 | ? new Uint8Array( 36 | new BigUint64Array([BigInt(timeout1), BigInt(timeout2)]).buffer, 37 | ) 38 | : null, 39 | ); 40 | unwrap(result, this.error()); 41 | } 42 | 43 | cancel() { 44 | const result = nix.aio_cancel(0, this.data); 45 | unwrap(result); 46 | } 47 | 48 | error() { 49 | return nix.aio_error(this.data); 50 | } 51 | 52 | return() { 53 | const result = Number(nix.aio_return(this.data)); 54 | return unwrap(result); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/darwin/corefoundation.ts: -------------------------------------------------------------------------------- 1 | const corefoundation = Deno.dlopen( 2 | "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", 3 | { 4 | CFStringCreateWithBytes: { 5 | parameters: ["pointer", "buffer", "i32", "u32", "bool"], 6 | result: "pointer", 7 | }, 8 | 9 | CFRelease: { 10 | parameters: ["pointer"], 11 | result: "void", 12 | }, 13 | 14 | CFDictionarySetValue: { 15 | parameters: ["pointer", "pointer", "pointer"], 16 | result: "void", 17 | }, 18 | 19 | CFRetain: { 20 | parameters: ["pointer"], 21 | result: "pointer", 22 | }, 23 | 24 | CFDictionaryGetValue: { 25 | parameters: ["pointer", "pointer"], 26 | result: "pointer", 27 | }, 28 | 29 | CFGetTypeID: { 30 | parameters: ["pointer"], 31 | result: "u32", 32 | }, 33 | 34 | CFStringGetTypeID: { 35 | parameters: [], 36 | result: "u32", 37 | }, 38 | 39 | CFStringGetCString: { 40 | parameters: ["pointer", "buffer", "i32", "u32"], 41 | result: "bool", 42 | }, 43 | 44 | CFNumberGetValue: { 45 | parameters: ["pointer", "i32", "buffer"], 46 | result: "bool", 47 | }, 48 | }, 49 | ).symbols; 50 | 51 | export default corefoundation; 52 | 53 | export function createCFString(str: string) { 54 | const buffer = new TextEncoder().encode(str); 55 | return corefoundation.CFStringCreateWithBytes( 56 | null, 57 | buffer, 58 | buffer.byteLength, 59 | 0x08000100, 60 | false, 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/darwin/enumerate.ts: -------------------------------------------------------------------------------- 1 | import iokit, { ioreturn, kIOSerialBSDServiceValue } from "./iokit.ts"; 2 | import corefoundation, { createCFString } from "./corefoundation.ts"; 3 | import { cString, deref, refptr } from "../common/util.ts"; 4 | import { SerialPort, SerialPortInfo } from "../common/web_serial.ts"; 5 | import { SerialPortDarwin } from "./serial_port.ts"; 6 | 7 | const stringBuffer = new Uint8Array(256); 8 | 9 | function readStringBuffer() { 10 | const len = stringBuffer.indexOf(0); 11 | return new TextDecoder().decode(stringBuffer.subarray(0, len)); 12 | } 13 | 14 | const kIOServiceClass = cString("IOService"); 15 | const kIOUSBDeviceClassName = "IOUSBDevice"; 16 | const usbDeviceClassName = "IOUSBHostDevice"; 17 | const kCFNumberSInt8Type = 0; 18 | const kCFNumberSInt16Type = 1; 19 | const kCFNumberSInt32Type = 2; 20 | 21 | const i8 = new Int8Array(1); 22 | const i8u8 = new Uint8Array(i8.buffer); 23 | const i16 = new Int16Array(1); 24 | const i16u8 = new Uint8Array(i16.buffer); 25 | const i32 = new Int32Array(1); 26 | const i32u8 = new Uint8Array(i32.buffer); 27 | 28 | function getParentDeviceByType(device: Deno.PointerValue, parentType: string) { 29 | while (true) { 30 | iokit.IOObjectGetClass(device, stringBuffer); 31 | const className = readStringBuffer(); 32 | 33 | if (className === parentType) { 34 | return device; 35 | } 36 | 37 | const parentRef = refptr(); 38 | const result = iokit.IORegistryEntryGetParentEntry( 39 | device, 40 | kIOServiceClass, 41 | parentRef, 42 | ); 43 | try { 44 | ioreturn(result); 45 | } catch (_) { 46 | return null; 47 | } 48 | device = deref(parentRef); 49 | 50 | if (!device) { 51 | return null; 52 | } 53 | } 54 | } 55 | 56 | function getIntProperty( 57 | deviceType: Deno.PointerValue, 58 | name: string, 59 | cfNumberType: number, 60 | ) { 61 | const key = createCFString(name); 62 | const value = iokit.IORegistryEntryCreateCFProperty(deviceType, key, null, 0); 63 | if (!value) { 64 | return null; 65 | } 66 | 67 | switch (cfNumberType) { 68 | case kCFNumberSInt8Type: { 69 | corefoundation.CFNumberGetValue(value, cfNumberType, i8u8); 70 | return i8[0]; 71 | } 72 | 73 | case kCFNumberSInt16Type: { 74 | corefoundation.CFNumberGetValue(value, cfNumberType, i16u8); 75 | return i16[0]; 76 | } 77 | 78 | case kCFNumberSInt32Type: { 79 | corefoundation.CFNumberGetValue(value, cfNumberType, i32u8); 80 | return i32[0]; 81 | } 82 | } 83 | } 84 | 85 | function getStringProperty(deviceType: Deno.PointerValue, name: string) { 86 | const key = createCFString(name); 87 | const value = iokit.IORegistryEntryCreateCFProperty(deviceType, key, null, 0); 88 | if (!value) { 89 | return null; 90 | } 91 | 92 | const result = corefoundation.CFStringGetCString( 93 | value, 94 | stringBuffer, 95 | stringBuffer.byteLength, 96 | 0, 97 | ); 98 | if (!result) { 99 | return null; 100 | } 101 | 102 | return readStringBuffer(); 103 | } 104 | 105 | function getPortInfo(service: Deno.PointerValue, name: string): SerialPortInfo { 106 | const usbDevice = getParentDeviceByType(service, usbDeviceClassName) ?? 107 | getParentDeviceByType(service, kIOUSBDeviceClassName); 108 | 109 | if (usbDevice) { 110 | return { 111 | name, 112 | usbVendorId: getIntProperty(usbDevice, "idVendor", kCFNumberSInt16Type) ?? 113 | 0, 114 | usbProductId: 115 | getIntProperty(usbDevice, "idProduct", kCFNumberSInt16Type) ?? 116 | 0, 117 | manufacturer: getStringProperty(usbDevice, "USB Vendor Name") ?? "", 118 | friendlyName: getStringProperty(usbDevice, "USB Product Name") ?? "", 119 | serialNumber: getStringProperty(usbDevice, "USB Serial Number") ?? "", 120 | }; 121 | } else { 122 | return { 123 | name, 124 | }; 125 | } 126 | } 127 | 128 | export function getPortsDarwin(): SerialPort[] { 129 | const ports: SerialPortInfo[] = []; 130 | 131 | const matchingServices = iokit.IOServiceMatching(kIOSerialBSDServiceValue); 132 | 133 | const key = createCFString("IOSerialBSDClientType"); 134 | const value = createCFString("IOSerialStream"); 135 | 136 | corefoundation.CFDictionarySetValue(matchingServices, key, value); 137 | 138 | const classesToMatchRef = refptr(); 139 | let result = iokit.IOServiceGetMatchingServices( 140 | null, 141 | corefoundation.CFRetain(matchingServices), 142 | classesToMatchRef, 143 | ); 144 | ioreturn(result); 145 | const classesToMatch = deref(classesToMatchRef); 146 | 147 | let service = iokit.IOIteratorNext(classesToMatch); 148 | 149 | while (service) { 150 | const propsRef = refptr(); 151 | result = iokit.IORegistryEntryCreateCFProperties( 152 | service, 153 | propsRef, 154 | null, 155 | 0, 156 | ); 157 | ioreturn(result); 158 | const props = deref(propsRef); 159 | 160 | const stringType = corefoundation.CFStringGetTypeID(); 161 | 162 | for (const key of ["IOCalloutDevice", "IODialinDevice"]) { 163 | const cfkey = createCFString(key); 164 | const value = corefoundation.CFDictionaryGetValue(props, cfkey); 165 | 166 | const valueType = corefoundation.CFGetTypeID(value); 167 | 168 | if (valueType === stringType) { 169 | const result = corefoundation.CFStringGetCString( 170 | value, 171 | stringBuffer, 172 | stringBuffer.byteLength, 173 | 0x08000100, 174 | ); 175 | 176 | if (result) { 177 | const name = readStringBuffer(); 178 | const portInfo = getPortInfo(service, name); 179 | if (portInfo) { 180 | ports.push(portInfo); 181 | } 182 | } 183 | } 184 | } 185 | 186 | service = iokit.IOIteratorNext(classesToMatch); 187 | } 188 | 189 | return ports.map((info) => new SerialPortDarwin(info)); 190 | } 191 | 192 | const port = getPortsDarwin()[2]; 193 | 194 | await port.open({ baudRate: 9600 }); 195 | 196 | async function pipeToConsole(signal: AbortSignal) { 197 | for await (const chunk of port.readable!) { 198 | if (signal.aborted) { 199 | break; 200 | } 201 | await Deno.stdout.write(chunk); 202 | } 203 | } 204 | 205 | const abortController = new AbortController(); 206 | const pipe = pipeToConsole(abortController.signal); 207 | 208 | let on = false; 209 | 210 | const loop = setInterval(async () => { 211 | const writer = port.writable!.getWriter(); 212 | on = !on; 213 | await writer.write(new Uint8Array([on ? 1 : 2])).catch(console.error); 214 | writer.releaseLock(); 215 | }, 500); 216 | 217 | setTimeout(async () => { 218 | clearInterval(loop); 219 | console.log("closing"); 220 | abortController.abort(); 221 | // await pipe; 222 | await port.close(); 223 | console.log("closed"); 224 | }, 2000); 225 | -------------------------------------------------------------------------------- /src/darwin/iokit.ts: -------------------------------------------------------------------------------- 1 | import { cString } from "../common/util.ts"; 2 | 3 | export const kIOSerialBSDServiceValue = cString("IOSerialBSDClient"); 4 | 5 | export default Deno.dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", { 6 | IOServiceMatching: { 7 | parameters: ["buffer"], 8 | result: "pointer", 9 | }, 10 | 11 | IOServiceGetMatchingServices: { 12 | parameters: ["pointer", "pointer", "buffer"], 13 | result: "i32", 14 | }, 15 | 16 | IOIteratorNext: { 17 | parameters: ["pointer"], 18 | result: "pointer", 19 | }, 20 | 21 | IORegistryEntryCreateCFProperties: { 22 | parameters: ["pointer", "buffer", "pointer", "i32"], 23 | result: "i32", 24 | }, 25 | 26 | IOObjectGetClass: { 27 | parameters: ["pointer", "buffer"], 28 | result: "i32", 29 | }, 30 | 31 | IORegistryEntryGetParentEntry: { 32 | parameters: ["pointer", "buffer", "buffer"], 33 | result: "i32", 34 | }, 35 | 36 | IORegistryEntryCreateCFProperty: { 37 | parameters: ["pointer", "pointer", "pointer", "i32"], 38 | result: "pointer", 39 | }, 40 | }).symbols; 41 | 42 | export function ioreturn(result: number) { 43 | if (result !== 0) { 44 | throw new Error(`IOKit error: ${result}`); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/darwin/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./enumerate.ts"; 2 | export * from "./serial_port.ts"; 3 | -------------------------------------------------------------------------------- /src/darwin/nix.ts: -------------------------------------------------------------------------------- 1 | const nix = Deno.dlopen("libSystem.dylib", { 2 | open: { 3 | parameters: ["buffer", "i32", "i32"], 4 | result: "i32", 5 | nonblocking: true, 6 | }, 7 | 8 | ioctl: { 9 | parameters: ["i32", "i64"], 10 | result: "i32", 11 | }, 12 | 13 | ioctl1: { 14 | parameters: ["i32", "i64", "buffer"], 15 | result: "i32", 16 | name: "ioctl", 17 | }, 18 | 19 | tcgetattr: { 20 | parameters: ["i32", "buffer"], 21 | result: "i32", 22 | }, 23 | 24 | tcsetattr: { 25 | parameters: ["i32", "i32", "buffer"], 26 | result: "i32", 27 | }, 28 | 29 | cfmakeraw: { 30 | parameters: ["buffer"], 31 | result: "void", 32 | }, 33 | 34 | fcntl: { 35 | parameters: ["i32", "i32", "i32"], 36 | result: "i32", 37 | }, 38 | 39 | strerror: { 40 | parameters: ["i32"], 41 | result: "pointer", 42 | }, 43 | 44 | aio_read: { 45 | parameters: ["buffer"], 46 | result: "i32", 47 | }, 48 | 49 | aio_write: { 50 | parameters: ["buffer"], 51 | result: "i32", 52 | }, 53 | 54 | aio_suspend: { 55 | parameters: ["buffer", "i32", "buffer"], 56 | result: "i32", 57 | nonblocking: true, 58 | }, 59 | 60 | aio_cancel: { 61 | parameters: ["i32", "buffer"], 62 | result: "i32", 63 | }, 64 | 65 | aio_error: { 66 | parameters: ["buffer"], 67 | result: "i32", 68 | }, 69 | 70 | aio_return: { 71 | parameters: ["buffer"], 72 | result: "i64", 73 | }, 74 | 75 | cfsetospeed: { 76 | parameters: ["buffer", "i32"], 77 | result: "i32", 78 | }, 79 | 80 | cfsetispeed: { 81 | parameters: ["buffer", "i32"], 82 | result: "i32", 83 | }, 84 | 85 | tcflush: { 86 | parameters: ["i32", "i32"], 87 | result: "i32", 88 | }, 89 | 90 | close: { 91 | parameters: ["i32"], 92 | result: "i32", 93 | }, 94 | 95 | read: { 96 | parameters: ["i32", "buffer", "i32"], 97 | result: "i32", 98 | nonblocking: true, 99 | }, 100 | 101 | write: { 102 | parameters: ["i32", "buffer", "i32"], 103 | result: "i32", 104 | nonblocking: true, 105 | }, 106 | }).symbols; 107 | 108 | export default nix; 109 | 110 | export class UnixError extends Error { 111 | errno: number; 112 | constructor(errno: number) { 113 | const str = nix.strerror(errno); 114 | const jstr = Deno.UnsafePointerView.getCString(str!); 115 | super(`UnixError: ${errno}: ${jstr}`); 116 | this.errno = errno; 117 | } 118 | } 119 | 120 | export function unwrap(result: number, error?: number) { 121 | if (result < 0) { 122 | let errno; 123 | if (error !== undefined) { 124 | errno = error; 125 | } else { 126 | const lib = Deno.dlopen("libSystem.dylib", { 127 | errno: { 128 | type: "i32", 129 | }, 130 | }); 131 | errno = lib.symbols.errno; 132 | lib.close(); 133 | } 134 | throw new UnixError(errno); 135 | } 136 | return result; 137 | } 138 | -------------------------------------------------------------------------------- /src/darwin/serial_port.ts: -------------------------------------------------------------------------------- 1 | import { cString, Deferred } from "../common/util.ts"; 2 | import { AIOCB } from "./aio.ts"; 3 | import nix, { UnixError, unwrap } from "./nix.ts"; 4 | import { Termios } from "./termios.ts"; 5 | import { 6 | SerialOptions, 7 | SerialOutputSignals, 8 | SerialPort, 9 | SerialPortInfo, 10 | } from "../common/web_serial.ts"; 11 | 12 | export class SerialPortDarwin extends EventTarget implements SerialPort { 13 | #info: SerialPortInfo; 14 | #fd?: number; 15 | 16 | #state = "closed"; 17 | #bufferSize?: number; 18 | #readable?: ReadableStream; 19 | #readFatal = false; 20 | #writable?: WritableStream; 21 | #writeFatal = false; 22 | #pendingClosePromise?: Deferred; 23 | 24 | constructor(info: SerialPortInfo) { 25 | super(); 26 | this.#info = info; 27 | } 28 | 29 | getInfo(): Promise { 30 | return Promise.resolve(this.#info); 31 | } 32 | 33 | async open(options: SerialOptions) { 34 | if (this.#state !== "closed") { 35 | throw new DOMException("Port is already open", "InvalidStateError"); 36 | } 37 | 38 | if (options.dataBits && options.dataBits !== 7 && options.dataBits !== 8) { 39 | throw new TypeError("Invalid dataBits, must be one of: 7, 8"); 40 | } 41 | 42 | if (options.stopBits && options.stopBits !== 1 && options.stopBits !== 2) { 43 | throw new TypeError("Invalid stopBits, must be one of: 1, 2"); 44 | } 45 | 46 | if (options.bufferSize === 0) { 47 | throw new TypeError("Invalid bufferSize, must be greater than 0"); 48 | } 49 | 50 | if ( 51 | options.flowControl && options.flowControl !== "none" && 52 | options.flowControl !== "software" && options.flowControl !== "hardware" 53 | ) { 54 | throw new TypeError( 55 | "Invalid flowControl, must be one of: none, software, hardware", 56 | ); 57 | } 58 | 59 | if ( 60 | options.parity && options.parity !== "none" && 61 | options.parity !== "even" && options.parity !== "odd" 62 | ) { 63 | throw new TypeError( 64 | "Invalid parity, must be one of: none, even, odd", 65 | ); 66 | } 67 | 68 | const fd = await nix.open( 69 | cString(this.#info.name), 70 | 0x802, 71 | 0, 72 | ); 73 | unwrap(fd); 74 | this.#fd = fd; 75 | 76 | unwrap(nix.ioctl(fd, 536900621)); 77 | 78 | let termios = new Termios(); 79 | unwrap(nix.tcgetattr(fd, termios.data)); 80 | 81 | termios.cflag |= 34816; 82 | 83 | nix.cfmakeraw(termios.data); 84 | 85 | unwrap(nix.tcsetattr(fd, 0, termios.data)); 86 | 87 | const actualTermios = new Termios(); 88 | unwrap(nix.tcgetattr(fd, actualTermios.data)); 89 | 90 | if ( 91 | actualTermios.iflag !== termios.iflag || 92 | actualTermios.oflag !== termios.oflag || 93 | actualTermios.cflag !== termios.cflag || 94 | actualTermios.lflag !== termios.lflag 95 | ) { 96 | throw new Error("Failed to apply termios settings"); 97 | } 98 | 99 | unwrap(nix.fcntl(fd, 4, 0)); // F_SETFL 100 | 101 | termios = Termios.get(fd); 102 | termios.setParity(options.parity ?? "none"); 103 | termios.setFlowControl(options.flowControl ?? "none"); 104 | termios.setDataBits(options.dataBits ?? 8); 105 | termios.setStopBits(options.stopBits ?? 1); 106 | termios.set(fd, options.baudRate); 107 | 108 | this.#state = "opened"; 109 | this.#bufferSize = options.bufferSize ?? 255; 110 | } 111 | 112 | #aiocbs = new Set(); 113 | 114 | get readable() { 115 | if (this.#readable) { 116 | return this.#readable; 117 | } 118 | 119 | if (this.#state !== "opened" || this.#readFatal) { 120 | return null; 121 | } 122 | 123 | const highWaterMark = this.#bufferSize!; 124 | 125 | const stream = new ReadableStream({ 126 | type: "bytes", 127 | pull: async (controller) => { 128 | const read = async (buffer: Uint8Array) => { 129 | const aio = new AIOCB(this.#fd!, buffer); 130 | aio.read(); 131 | this.#aiocbs.add(aio); 132 | while (true) { 133 | try { 134 | await aio.suspend(0, 50); 135 | } catch (e) { 136 | if (e instanceof UnixError) { 137 | if (e.errno === 36) { 138 | continue; 139 | } else { 140 | break; 141 | } 142 | } 143 | } 144 | break; 145 | } 146 | this.#aiocbs.delete(aio); 147 | return aio.return(); 148 | }; 149 | 150 | if (controller.byobRequest) { 151 | if (controller.byobRequest.view) { 152 | const buffer = new Uint8Array( 153 | controller.byobRequest.view.buffer, 154 | controller.byobRequest.view.byteOffset, 155 | controller.byobRequest.view.byteLength, 156 | ); 157 | const nread = await read(buffer); 158 | if (nread === 0) { 159 | controller.close(); 160 | return; 161 | } 162 | controller.byobRequest.respond(nread); 163 | } else { 164 | const buffer = new Uint8Array( 165 | controller.desiredSize ?? highWaterMark, 166 | ); 167 | const nread = await read(buffer); 168 | if (nread === 0) { 169 | controller.close(); 170 | return; 171 | } 172 | controller.byobRequest.respondWithNewView( 173 | buffer.subarray(0, nread), 174 | ); 175 | } 176 | } else { 177 | const buffer = new Uint8Array( 178 | controller.desiredSize ?? highWaterMark, 179 | ); 180 | const nread = await read(buffer); 181 | if (nread === 0) { 182 | controller.close(); 183 | return; 184 | } 185 | controller.enqueue(buffer.subarray(0, nread)); 186 | } 187 | }, 188 | 189 | cancel: () => { 190 | nix.tcflush(this.#fd!, 0); 191 | this.#readable = undefined; 192 | if (this.#writable === null && this.#pendingClosePromise) { 193 | this.#pendingClosePromise.resolve(); 194 | } 195 | return Promise.resolve(); 196 | }, 197 | }, { highWaterMark }); 198 | 199 | this.#readable = stream; 200 | 201 | return stream; 202 | } 203 | 204 | get writable() { 205 | if (this.#writable) { 206 | return this.#writable; 207 | } 208 | 209 | if (this.#state !== "opened" || this.#writeFatal) { 210 | return null; 211 | } 212 | 213 | const highWaterMark = this.#bufferSize!; 214 | 215 | const stream = new WritableStream({ 216 | write: async (chunk) => { 217 | await nix.write(this.#fd!, chunk, chunk.byteLength); 218 | }, 219 | 220 | close: () => { 221 | nix.tcflush(this.#fd!, 1); 222 | this.#writable = undefined; 223 | if (this.#readable === null && this.#pendingClosePromise) { 224 | this.#pendingClosePromise.resolve(); 225 | } 226 | return Promise.resolve(); 227 | }, 228 | 229 | abort: () => { 230 | nix.tcflush(this.#fd!, 1); 231 | this.#writable = undefined; 232 | if (this.#readable === null && this.#pendingClosePromise) { 233 | this.#pendingClosePromise.resolve(); 234 | } 235 | return Promise.resolve(); 236 | }, 237 | }, { highWaterMark, size: () => highWaterMark }); 238 | 239 | this.#writable = stream; 240 | 241 | return stream; 242 | } 243 | 244 | async setSignals(_signals: SerialOutputSignals) { 245 | // TODO 246 | } 247 | 248 | // deno-lint-ignore require-await 249 | async getSignals() { 250 | // TODO 251 | return { 252 | dataCarrierDetect: false, 253 | ringIndicator: false, 254 | dataSetReady: false, 255 | clearToSend: false, 256 | }; 257 | } 258 | 259 | close() { 260 | if (this.#state !== "opened") { 261 | throw new DOMException("Port is not open", "InvalidStateError"); 262 | } 263 | 264 | console.log("nonexclusive"); 265 | unwrap( 266 | nix.ioctl( 267 | this.#fd!, 268 | 536900622, 269 | ), 270 | ); 271 | // set nonblock 272 | unwrap(nix.fcntl(this.#fd!, 4, 2048)); 273 | console.log("closing??"); 274 | unwrap(nix.close(this.#fd!)); 275 | console.log("closed??"); 276 | 277 | return Promise.resolve(); 278 | 279 | // for (const aio of this.#aiocbs) { 280 | // console.log(nix.aio_error(aio.data)); 281 | // console.log(nix.aio_return(aio.data)); 282 | // console.log(nix.aio_cancel(this.#fd!, aio.data)); 283 | // } 284 | 285 | // console.log(unwrap(nix.aio_cancel(this.#fd!, null))); 286 | 287 | // const pendingClosePromise = new Deferred(); 288 | 289 | // if (this.#readable === null && this.#writable === null) { 290 | // pendingClosePromise.resolve(); 291 | // } else { 292 | // this.#pendingClosePromise = pendingClosePromise; 293 | // } 294 | 295 | // const cancelPromise = this.#readable 296 | // ? this.#readable.cancel() 297 | // : Promise.resolve(); 298 | // const abortPromise = this.#writable 299 | // ? this.#writable.abort() 300 | // : Promise.resolve(); 301 | 302 | // this.#state = "closing"; 303 | 304 | // return Promise.all([cancelPromise, abortPromise, pendingClosePromise]) 305 | // .then( 306 | // () => { 307 | // this.#state = "closed"; 308 | // this.#readFatal = this.#writeFatal = false; 309 | // this.#pendingClosePromise = undefined; 310 | // }, 311 | // (r) => { 312 | // this.#pendingClosePromise = undefined; 313 | // throw r; 314 | // }, 315 | // ); 316 | } 317 | 318 | [Symbol.for("Deno.customInspect")]( 319 | inspect: typeof Deno.inspect, 320 | options: Deno.InspectOptions, 321 | ) { 322 | return `SerialPort ${ 323 | inspect({ name: this.#info.name, state: this.#state }, options) 324 | }`; 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/darwin/termios.ts: -------------------------------------------------------------------------------- 1 | import { FlowControlType, ParityType } from "../common/web_serial.ts"; 2 | import nix, { unwrap } from "./nix.ts"; 3 | 4 | const PARENB = 0x00001000; 5 | const PARODD = 0x00002000; 6 | const INPCK = 0x00000010; 7 | const IGNPAR = 0x00000004; 8 | 9 | const IXON = 0x00000200; 10 | const IXOFF = 0x00000400; 11 | const CRTSCTS = 0x00010000 | 0x00020000; 12 | 13 | const CSTOPB = 0x00000400; 14 | const CS7 = 0x00000200; 15 | const CS8 = 0x00000300; 16 | const CSIZE = 0x00000300; 17 | 18 | /** 19 | * struct termios { 20 | * tcflag_t c_iflag; // input flags 21 | * tcflag_t c_oflag; // output flags 22 | * tcflag_t c_cflag; // control flags 23 | * tcflag_t c_lflag; // local flags 24 | * cc_t c_line; // line discipline 25 | * cc_t c_cc[NCCS]; // control characters 26 | * speed_t c_ispeed; // input speed 27 | * speed_t c_ospeed; // output speed 28 | * }; 29 | * 30 | * typedef unsigned int tcflag_t; 31 | * typedef unsigned char cc_t; 32 | * typedef unsigned int speed_t; 33 | * 34 | * #define NCCS 32 35 | */ 36 | export class Termios { 37 | data: Uint8Array; 38 | view: DataView; 39 | 40 | constructor() { 41 | this.data = new Uint8Array(72); 42 | this.view = new DataView(this.data.buffer); 43 | } 44 | 45 | static get(fd: number) { 46 | const termios = new Termios(); 47 | unwrap(nix.tcgetattr(fd, termios.data)); 48 | nix.cfsetospeed(termios.data, 9600); 49 | nix.cfsetispeed(termios.data, 9600); 50 | return termios; 51 | } 52 | 53 | set(fd: number, baudRate: number) { 54 | nix.cfsetospeed(this.data, baudRate); 55 | nix.cfsetispeed(this.data, baudRate); 56 | 57 | unwrap(nix.tcsetattr(fd, 0, this.data)); 58 | 59 | // if (baudRate > 0) { 60 | // unwrap(nix.ioctl1( 61 | // fd, 62 | // 0x80045402, 63 | // new Uint8Array( 64 | // new BigUint64Array([BigInt(baudRate)]).buffer, 65 | // ), 66 | // )); 67 | // } 68 | } 69 | 70 | setParity(parity: ParityType) { 71 | switch (parity) { 72 | case "none": 73 | this.cflag &= ~(PARENB | PARODD); 74 | this.iflag &= ~INPCK; 75 | this.iflag |= IGNPAR; 76 | break; 77 | case "odd": 78 | this.cflag |= PARENB | PARODD; 79 | this.iflag |= INPCK; 80 | this.iflag &= ~IGNPAR; 81 | break; 82 | case "even": 83 | this.cflag |= PARENB; 84 | this.cflag &= ~PARODD; 85 | this.iflag |= INPCK; 86 | this.iflag &= ~IGNPAR; 87 | break; 88 | } 89 | } 90 | 91 | setFlowControl(flowControl: FlowControlType) { 92 | switch (flowControl) { 93 | case "none": 94 | this.cflag &= ~CRTSCTS; 95 | this.iflag &= ~(IXON | IXOFF); 96 | break; 97 | case "software": 98 | this.cflag &= ~CRTSCTS; 99 | this.iflag |= IXON | IXOFF; 100 | break; 101 | case "hardware": 102 | this.cflag |= CRTSCTS; 103 | this.iflag &= ~(IXON | IXOFF); 104 | break; 105 | } 106 | } 107 | 108 | setDataBits(dataBits: number) { 109 | let size = 0; 110 | 111 | switch (dataBits) { 112 | case 7: 113 | size = CS7; 114 | break; 115 | case 8: 116 | size = CS8; 117 | break; 118 | } 119 | 120 | this.cflag &= ~CSIZE; 121 | this.cflag |= size; 122 | } 123 | 124 | setStopBits(stopBits: number) { 125 | switch (stopBits) { 126 | case 1: 127 | this.cflag &= ~CSTOPB; 128 | break; 129 | case 2: 130 | this.cflag |= CSTOPB; 131 | break; 132 | } 133 | } 134 | 135 | get iflag() { 136 | return this.view.getUint32(0, true); 137 | } 138 | 139 | set iflag(value: number) { 140 | this.view.setUint32(0, value, true); 141 | } 142 | 143 | get oflag() { 144 | return this.view.getUint32(4, true); 145 | } 146 | 147 | set oflag(value: number) { 148 | this.view.setUint32(4, value, true); 149 | } 150 | 151 | get cflag() { 152 | return this.view.getUint32(8, true); 153 | } 154 | 155 | set cflag(value: number) { 156 | this.view.setUint32(8, value, true); 157 | } 158 | 159 | get lflag() { 160 | return this.view.getUint32(12, true); 161 | } 162 | 163 | set lflag(value: number) { 164 | this.view.setUint32(12, value, true); 165 | } 166 | 167 | get line() { 168 | return this.view.getUint8(16); 169 | } 170 | 171 | set line(value: number) { 172 | this.view.setUint8(16, value); 173 | } 174 | 175 | get cc() { 176 | return this.data.subarray(17, 17 + 32); 177 | } 178 | 179 | set cc(value: Uint8Array) { 180 | this.data.set(value, 17); 181 | } 182 | 183 | get ispeed() { 184 | return this.view.getUint32(52, true); 185 | } 186 | 187 | set ispeed(value: number) { 188 | this.view.setUint32(52, value, true); 189 | } 190 | 191 | get ospeed() { 192 | return this.view.getUint32(56, true); 193 | } 194 | 195 | set ospeed(value: number) { 196 | this.view.setUint32(56, value, true); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/linux/enumerate.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DjDeveloperr/deno_serial/13674f49da27696b7e8cb3ba2dc603c791a2c837/src/linux/enumerate.ts -------------------------------------------------------------------------------- /src/linux/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./enumerate.ts"; 2 | export * from "./serial_port.ts"; 3 | -------------------------------------------------------------------------------- /src/linux/serial_port.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DjDeveloperr/deno_serial/13674f49da27696b7e8cb3ba2dc603c791a2c837/src/linux/serial_port.ts -------------------------------------------------------------------------------- /src/serial_port.ts: -------------------------------------------------------------------------------- 1 | import { SerialOpenOptions } from "./common/serial_port.ts"; 2 | // import { getPortsDarwin } from "./darwin/enumerate.ts"; 3 | import { getPortsWin, SerialPortWin } from "./windows/mod.ts"; 4 | 5 | export function getPorts() { 6 | if (Deno.build.os === "windows") { 7 | return getPortsWin(); 8 | } else { 9 | throw new Error(`Unsupported OS: ${Deno.build.os}`); 10 | } 11 | } 12 | 13 | export function open(options: SerialOpenOptions) { 14 | if (Deno.build.os === "windows") { 15 | return new SerialPortWin(options); 16 | } else { 17 | throw new Error(`Unsupported OS: ${Deno.build.os}`); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/unix/mod.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DjDeveloperr/deno_serial/13674f49da27696b7e8cb3ba2dc603c791a2c837/src/unix/mod.ts -------------------------------------------------------------------------------- /src/windows/deps.ts: -------------------------------------------------------------------------------- 1 | export * as Fs from "https://win32.deno.dev/5cd87d0/Storage.FileSystem"; 2 | export * as Di from "https://win32.deno.dev/5cd87d0/Devices.DeviceAndDriverInstallation"; 3 | export * as Comm from "https://win32.deno.dev/5cd87d0/Devices.Communication"; 4 | export * as Reg from "https://win32.deno.dev/5cd87d0/System.Registry"; 5 | export * as Foundation from "https://win32.deno.dev/5cd87d0/Foundation"; 6 | export { OverlappedPromise } from "https://win32.deno.dev/5cd87d0/overlapped"; 7 | export { unwrap } from "https://raw.githubusercontent.com/DjDeveloperr/deno_win32/5cd87d0/error.ts"; 8 | -------------------------------------------------------------------------------- /src/windows/enumerate.ts: -------------------------------------------------------------------------------- 1 | import { Di, Reg, unwrap } from "./deps.ts"; 2 | import { PortInfo, PortType } from "../common/port_info.ts"; 3 | 4 | function getPortName( 5 | devInfoList: Deno.PointerValue, 6 | devInfoData: Uint8Array, 7 | ) { 8 | const hkey = Di.SetupDiOpenDevRegKey( 9 | devInfoList, 10 | devInfoData, 11 | Di.DICS_FLAG_GLOBAL, 12 | 0, 13 | Di.DIREG_DEV, 14 | Reg.KEY_READ, 15 | ); 16 | const nameBuf = new Uint8Array(256); 17 | const len = new Uint32Array([nameBuf.byteLength]); 18 | const result = Reg.RegQueryValueExA( 19 | hkey, 20 | "PortName", 21 | null, 22 | null, 23 | nameBuf, 24 | new Uint8Array(len.buffer), 25 | ); 26 | unwrap(result); 27 | const nameLen = len[0]; 28 | Reg.RegCloseKey(hkey); 29 | return new TextDecoder().decode(nameBuf.slice(0, nameLen - 1)); 30 | } 31 | 32 | function getPortInstanceId( 33 | devInfoList: Deno.PointerValue, 34 | devInfoData: Uint8Array, 35 | ) { 36 | const instanceIdBuf = new Uint8Array(260); 37 | const instanceIdLen = new Uint32Array(1); 38 | unwrap(Di.SetupDiGetDeviceInstanceIdA( 39 | devInfoList, 40 | devInfoData, 41 | instanceIdBuf, 42 | instanceIdBuf.byteLength, 43 | new Uint8Array(instanceIdLen.buffer), 44 | )); 45 | return new TextDecoder().decode( 46 | instanceIdBuf.slice(0, instanceIdLen[0] - 1), 47 | ); 48 | } 49 | 50 | function getPortProperty( 51 | devInfoList: Deno.PointerValue, 52 | devInfoData: Uint8Array, 53 | id: number, 54 | ) { 55 | const resultBuf = new Uint8Array(260); 56 | const requiredSize = new Uint32Array(1); 57 | Di.SetupDiGetDeviceRegistryPropertyA( 58 | devInfoList, 59 | devInfoData, 60 | id, 61 | null, 62 | resultBuf, 63 | resultBuf.byteLength, 64 | new Uint8Array(requiredSize.buffer), 65 | ); 66 | return new TextDecoder().decode(resultBuf.slice(0, requiredSize[0] - 1)); 67 | } 68 | 69 | export function SetupDiClassGuidsFromNameA(className: string) { 70 | let guidList = new Uint8Array(16); 71 | let guidListSize = guidList.byteLength / 16; 72 | const actualSizePtr = new Uint8Array(4); 73 | 74 | const result = Di.SetupDiClassGuidsFromNameA( 75 | className, 76 | guidList, 77 | guidListSize, 78 | actualSizePtr, 79 | ); 80 | unwrap(result); 81 | 82 | const actualSize = new Uint32Array(actualSizePtr.buffer)[0]; 83 | 84 | if (actualSize === 0) { 85 | return []; 86 | } 87 | 88 | if (actualSize !== guidListSize) { 89 | guidList = new Uint8Array(actualSize * 16); 90 | guidListSize = actualSize; 91 | 92 | const result = Di.SetupDiClassGuidsFromNameA( 93 | className, 94 | guidList, 95 | guidListSize, 96 | actualSizePtr, 97 | ); 98 | 99 | unwrap(result); 100 | } 101 | 102 | const guids = new Array(actualSize); 103 | for (let i = 0; i < actualSize; i++) { 104 | const guidPtr = new Uint8Array(guidList.buffer, i * 16, 16); 105 | guids[i] = guidPtr.slice(); 106 | } 107 | 108 | return guids; 109 | } 110 | 111 | export function getPortsWin() { 112 | const devInfoData = Di.allocSP_DEVINFO_DATA({ 113 | cbSize: Di.sizeofSP_DEVINFO_DATA, 114 | }); 115 | 116 | const ports: PortInfo[] = []; 117 | for ( 118 | const guid of SetupDiClassGuidsFromNameA("Ports") 119 | ) { 120 | const devInfoList = Di.SetupDiGetClassDevsA( 121 | guid, 122 | null, 123 | null, 124 | Di.DIGCF_PRESENT, 125 | )!; 126 | unwrap(Number(Deno.UnsafePointer.value(devInfoList))); 127 | 128 | for (let i = 0; true; i++) { 129 | const result = Di.SetupDiEnumDeviceInfo(devInfoList, i, devInfoData); 130 | if (!result) { 131 | break; 132 | } else { 133 | const name = getPortName(devInfoList, devInfoData); 134 | const instanceId = getPortInstanceId(devInfoList, devInfoData); 135 | 136 | const instance = instanceId.match( 137 | /(\w+)\\VID_([A-Fa-f0-9]+)&PID_([A-Fa-f0-9]+)\\([A-Fa-f0-9]+)/, 138 | ); 139 | if (instance === null) continue; 140 | 141 | const type = instance[1] as PortType; 142 | const vendorId = parseInt(instance[2], 16); 143 | const productId = parseInt(instance[3], 16); 144 | const serialNumber = instance[4]; 145 | 146 | const manufacturer = getPortProperty( 147 | devInfoList, 148 | devInfoData, 149 | 0x0000000B, 150 | ); 151 | const friendlyName = getPortProperty( 152 | devInfoList, 153 | devInfoData, 154 | 0x0000000C, 155 | ); 156 | 157 | ports.push({ 158 | name, 159 | type, 160 | vendorId, 161 | productId, 162 | serialNumber, 163 | manufacturer, 164 | friendlyName, 165 | }); 166 | } 167 | } 168 | 169 | Di.SetupDiDestroyDeviceInfoList(devInfoList); 170 | } 171 | return ports; 172 | } 173 | -------------------------------------------------------------------------------- /src/windows/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./enumerate.ts"; 2 | export * from "./serial_port.ts"; 3 | -------------------------------------------------------------------------------- /src/windows/serial_port.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClearBuffer, 3 | DataBits, 4 | FlowControl, 5 | Parity, 6 | SerialOpenOptions, 7 | SerialPort, 8 | StopBits, 9 | } from "../common/serial_port.ts"; 10 | import { Comm, Foundation, Fs, OverlappedPromise, unwrap } from "./deps.ts"; 11 | 12 | // deno-fmt-ignore 13 | const fBinary = 0b0000_0000_0000_0001; 14 | // deno-fmt-ignore 15 | // const fParity = 0b0000_0000_0000_0010; 16 | // deno-fmt-ignore 17 | const fOutxCtsFlow = 0b0000_0000_0000_0100; 18 | // deno-fmt-ignore 19 | const fOutxDsrFlow = 0b0000_0000_0000_1000; 20 | // deno-fmt-ignore 21 | const fDtrControl0 = 0b0000_0000_0001_0000; 22 | // deno-fmt-ignore 23 | const fDtrControl1 = 0b0000_0000_0010_0000; 24 | // deno-fmt-ignore 25 | const fDsrSensitivity = 0b0000_0000_0100_0000; 26 | // deno-fmt-ignore 27 | const fOutX = 0b0000_0001_0000_0000; 28 | // deno-fmt-ignore 29 | const fInX = 0b0000_0010_0000_0000; 30 | // deno-fmt-ignore 31 | const fErrorChar = 0b0000_0100_0000_0000; 32 | // deno-fmt-ignore 33 | const fNull = 0b0000_1000_0000_0000; 34 | // deno-fmt-ignore 35 | const fRtsControl0 = 0b0001_0000_0000_0000; 36 | // deno-fmt-ignore 37 | const fRtsControl1 = 0b0010_0000_0000_0000; 38 | // deno-fmt-ignore 39 | // const fRtsControl = 0b0011_0000_0000_0000; 40 | // deno-fmt-ignore 41 | const fAbortOnError = 0b0100_0000_0000_0000; 42 | 43 | function getbit(bits: number, bit: number) { 44 | return (bits & bit) === bit; 45 | } 46 | 47 | function setbit(bits: number, bit: number, value: boolean) { 48 | if (value) { 49 | return bits | bit; 50 | } else { 51 | return bits & ~bit; 52 | } 53 | } 54 | 55 | function flowControlToDcb(flowControl: FlowControl, dcb: Comm.DCBView) { 56 | let bits = dcb._bitfield; 57 | switch (flowControl) { 58 | case FlowControl.NONE: 59 | bits = setbit(bits, fOutxCtsFlow, false); 60 | bits = setbit(bits, fRtsControl0, false); 61 | bits = setbit(bits, fRtsControl1, false); 62 | bits = setbit(bits, fOutX, false); 63 | bits = setbit(bits, fInX, false); 64 | break; 65 | case FlowControl.SOFTWARE: 66 | bits = setbit(bits, fOutxCtsFlow, true); 67 | bits = setbit(bits, fRtsControl0, false); 68 | bits = setbit(bits, fRtsControl1, false); 69 | bits = setbit(bits, fOutX, true); 70 | bits = setbit(bits, fInX, true); 71 | break; 72 | case FlowControl.HARDWARE: 73 | bits = setbit(bits, fOutxCtsFlow, false); 74 | bits = setbit(bits, fRtsControl0, true); 75 | bits = setbit(bits, fRtsControl1, false); 76 | bits = setbit(bits, fOutX, false); 77 | bits = setbit(bits, fInX, false); 78 | break; 79 | } 80 | dcb._bitfield = bits; 81 | } 82 | 83 | function dcbToFlowControl(dcb: Comm.DCBView) { 84 | const bits = dcb._bitfield; 85 | if (getbit(bits, fOutxCtsFlow)) { 86 | return FlowControl.SOFTWARE; 87 | } else if (getbit(bits, fRtsControl0)) { 88 | return FlowControl.HARDWARE; 89 | } else { 90 | return FlowControl.NONE; 91 | } 92 | } 93 | 94 | const comstat = Comm.allocCOMSTAT(); 95 | 96 | export class SerialPortWin implements SerialPort { 97 | #handle: Deno.PointerValue; 98 | #timeout = 100; 99 | 100 | readonly name?: string; 101 | 102 | #_dcb: Comm.DCBView; 103 | 104 | constructor(options: SerialOpenOptions) { 105 | this.#handle = Fs.CreateFileA( 106 | "\\\\.\\" + options.name, 107 | Fs.FILE_GENERIC_READ | Fs.FILE_GENERIC_WRITE, 108 | 0, 109 | null, 110 | Fs.OPEN_EXISTING, 111 | Fs.FILE_ATTRIBUTE_NORMAL | Fs.FILE_FLAG_OVERLAPPED, 112 | null, 113 | )!; 114 | 115 | this.name = options.name; 116 | const dcb = Comm.allocDCB(); 117 | unwrap(Comm.GetCommState(this.#handle, dcb)); 118 | const dv = new Comm.DCBView(dcb); 119 | dv.XonChar = 17; 120 | dv.XoffChar = 19; 121 | dv.ErrorChar = 0; 122 | dv.EofChar = 26; 123 | let bitfield = dv._bitfield; 124 | bitfield = setbit(bitfield, fBinary, true); 125 | bitfield = setbit(bitfield, fOutxDsrFlow, false); 126 | bitfield = setbit(bitfield, fDtrControl0, false); 127 | bitfield = setbit(bitfield, fDtrControl1, false); 128 | bitfield = setbit(bitfield, fDsrSensitivity, false); 129 | bitfield = setbit(bitfield, fErrorChar, false); 130 | bitfield = setbit(bitfield, fNull, false); 131 | bitfield = setbit(bitfield, fAbortOnError, false); 132 | 133 | dv.BaudRate = options.baudRate; 134 | dv.ByteSize = options.dataBits || DataBits.EIGHT; 135 | dv.StopBits = options.stopBits || StopBits.ONE; 136 | dv.Parity = options.parity || Parity.NONE; 137 | flowControlToDcb(options.flowControl || FlowControl.NONE, dv); 138 | Comm.SetCommState(this.#handle, dcb); 139 | 140 | this.#_dcb = dv; 141 | 142 | this.timeout = options.timeout ?? 0; 143 | } 144 | 145 | get #dcb() { 146 | unwrap(Comm.GetCommState(this.#handle, this.#_dcb.buffer)); 147 | return this.#_dcb; 148 | } 149 | 150 | set #dcb(dcb: Comm.DCBView) { 151 | unwrap(Comm.SetCommState(this.#handle, dcb.buffer)); 152 | } 153 | 154 | get baudRate(): number { 155 | return this.#dcb.BaudRate; 156 | } 157 | 158 | set baudRate(value: number) { 159 | const dcb = this.#dcb; 160 | dcb.BaudRate = value; 161 | this.#dcb = dcb; 162 | } 163 | 164 | get dataBits(): DataBits { 165 | return this.#dcb.ByteSize; 166 | } 167 | 168 | set dataBits(value: DataBits) { 169 | const dcb = this.#dcb; 170 | dcb.ByteSize = value; 171 | this.#dcb = dcb; 172 | } 173 | 174 | set stopBits(value: StopBits) { 175 | const dcb = this.#dcb; 176 | dcb.StopBits = value; 177 | this.#dcb = dcb; 178 | } 179 | 180 | get stopBits(): StopBits { 181 | return this.#dcb.StopBits; 182 | } 183 | 184 | get parity(): Parity { 185 | return this.#dcb.Parity; 186 | } 187 | 188 | set parity(value: Parity) { 189 | const dcb = this.#dcb; 190 | dcb.Parity = value; 191 | this.#dcb = dcb; 192 | } 193 | 194 | get flowControl(): FlowControl { 195 | const dcb = this.#dcb; 196 | return dcbToFlowControl(dcb); 197 | } 198 | 199 | set flowControl(value: FlowControl) { 200 | const dcb = this.#dcb; 201 | flowControlToDcb(value, dcb); 202 | this.#dcb = dcb; 203 | } 204 | 205 | get timeout() { 206 | return this.#timeout; 207 | } 208 | 209 | set timeout(ms: number) { 210 | const timeouts = Comm.allocCOMMTIMEOUTS({ 211 | ReadTotalTimeoutConstant: ms, 212 | }); 213 | Comm.SetCommTimeouts(this.#handle, timeouts); 214 | this.#timeout = ms; 215 | } 216 | 217 | writeRequestToSend(level: boolean): void { 218 | Comm.EscapeCommFunction(this.#handle, level ? 3 : 4); 219 | } 220 | 221 | writeDataTerminalReady(level: boolean): void { 222 | Comm.EscapeCommFunction(this.#handle, level ? 5 : 6); 223 | } 224 | 225 | get #modemStatus() { 226 | const status = new Uint32Array(1); 227 | unwrap( 228 | Comm.GetCommModemStatus(this.#handle, new Uint8Array(status.buffer)), 229 | ); 230 | return status[0]; 231 | } 232 | 233 | readClearToSend(): boolean { 234 | return (this.#modemStatus & 0x0010) !== 0; 235 | } 236 | 237 | readDataSetReady(): boolean { 238 | return (this.#modemStatus & 0x0020) !== 0; 239 | } 240 | 241 | readRingIndicator(): boolean { 242 | return (this.#modemStatus & 0x0040) !== 0; 243 | } 244 | 245 | readCarrierDetect(): boolean { 246 | return (this.#modemStatus & 0x0080) !== 0; 247 | } 248 | 249 | bytesToRead(): number { 250 | const errors = new Uint32Array(1); 251 | Comm.ClearCommError(this.#handle, new Uint8Array(errors.buffer), comstat); 252 | return new Comm.COMSTATView(comstat).cbInQue; 253 | } 254 | 255 | bytesToWrite(): number { 256 | const errors = new Uint32Array(1); 257 | Comm.ClearCommError(this.#handle, new Uint8Array(errors.buffer), comstat); 258 | return new Comm.COMSTATView(comstat).cbOutQue; 259 | } 260 | 261 | clear(buffer: ClearBuffer): void { 262 | Comm.PurgeComm( 263 | this.#handle, 264 | ({ 265 | [ClearBuffer.INPUT]: 0x0001 | 0x0004, 266 | [ClearBuffer.OUTPUT]: 0x0002 | 0x0008, 267 | [ClearBuffer.ALL]: 0x0001 | 0x0002 | 0x0004 | 0x0008, 268 | })[buffer], 269 | ); 270 | } 271 | 272 | setBreak(): void { 273 | Comm.SetCommBreak(this.#handle); 274 | } 275 | 276 | clearBreak(): void { 277 | Comm.ClearCommBreak(this.#handle); 278 | } 279 | 280 | flush(): void { 281 | Fs.FlushFileBuffers(this.#handle); 282 | } 283 | 284 | #pending = new Set(); 285 | 286 | async read(p: Uint8Array): Promise { 287 | try { 288 | const controller = new AbortController(); 289 | const overlapped = new OverlappedPromise(this.#handle, controller.signal); 290 | // const bytes = new Uint32Array(1); 291 | Fs.ReadFile( 292 | this.#handle, 293 | p, 294 | p.byteLength, 295 | null, // new Uint8Array(bytes.buffer), 296 | overlapped.buffer, 297 | ); 298 | this.#pending.add(controller); 299 | await overlapped; 300 | this.#pending.delete(controller); 301 | return Number(overlapped.internalHigh); 302 | } catch (_) { 303 | return null; 304 | } 305 | } 306 | 307 | async write(p: Uint8Array): Promise { 308 | const controller = new AbortController(); 309 | const overlapped = new OverlappedPromise(this.#handle, controller.signal); 310 | // const bytes = new Uint32Array(1); 311 | Fs.WriteFile( 312 | this.#handle, 313 | p, 314 | p.byteLength, 315 | null, // new Uint8Array(bytes.buffer), 316 | overlapped.buffer, 317 | ); 318 | this.#pending.add(controller); 319 | await overlapped; 320 | this.#pending.delete(controller); 321 | return Number(overlapped.internalHigh); 322 | } 323 | 324 | close(): void { 325 | for (const overlapped of this.#pending) { 326 | overlapped.abort(); 327 | } 328 | Foundation.CloseHandle(this.#handle); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | import { getPorts, open } from "./mod.ts"; 2 | import { assertEquals } from "https://deno.land/std@0.119.0/testing/asserts.ts"; 3 | import { ClearBuffer } from "./src/common/serial_port.ts"; 4 | 5 | const OK = new Uint8Array([0x01, 0x00]); 6 | const ERR = 0x00; 7 | const INVALID_COMMAND = 0x01; 8 | const CMD_OUTPUT_MODE = 0x01; 9 | const CMD_DIGITAL_WRITE = 0x02; 10 | const HIGH = 1; 11 | const LOW = 0; 12 | 13 | Deno.test("arduino blink", async (t) => { 14 | const portInfo = getPorts()[0]; 15 | if (!portInfo) { 16 | throw new Error("No serial ports found."); 17 | } 18 | 19 | const port = open({ name: portInfo.name, baudRate: 9600 }); 20 | port.clear(ClearBuffer.ALL); 21 | 22 | async function read(len: number) { 23 | const buf = new Uint8Array(len); 24 | const bytesRead = await port.read(buf); 25 | if (bytesRead === null) return "closed"; 26 | return buf.subarray(0, bytesRead); 27 | } 28 | 29 | await t.step("set output mode", async () => { 30 | await port.write(new Uint8Array([CMD_OUTPUT_MODE, HIGH])); 31 | assertEquals(await read(2), OK); 32 | }); 33 | 34 | await t.step("write to pin (HIGH)", async () => { 35 | await port.write(new Uint8Array([CMD_DIGITAL_WRITE, HIGH])); 36 | assertEquals(await read(2), OK); 37 | }); 38 | 39 | await t.step("invalid command", async () => { 40 | await port.write(new Uint8Array([0x00])); 41 | assertEquals(await read(2), new Uint8Array([ERR, INVALID_COMMAND])); 42 | }); 43 | 44 | await t.step("wait 1s", async () => { 45 | await new Promise((resolve) => setTimeout(resolve, 300)); 46 | }); 47 | 48 | await t.step("write to pin (LOW)", async () => { 49 | await port.write(new Uint8Array([CMD_DIGITAL_WRITE, LOW])); 50 | assertEquals(await read(2), OK); 51 | }); 52 | 53 | await t.step("close port", () => { 54 | port.close(); 55 | }); 56 | }); 57 | --------------------------------------------------------------------------------