Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
sverben committed Apr 13, 2024
0 parents commit a2ba870
Show file tree
Hide file tree
Showing 12 changed files with 1,715 additions and 0 deletions.
37 changes: 37 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Build avrdude with Emscripten

on:
push:
tags:
- v*

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Install dependencies
run: sudo apt update && sudo apt install gcc-avr avr-libc freeglut3-dev arduino-core-avr

- name: Setup NodeJS
uses: actions/setup-node@v4
with:
node-version: 20.x
registry-url: 'https://registry.npmjs.org'

- name: Build SimAVR
run: yarn build:simavr

- name: Install Dependencies
run: yarn install --frozen-lockfile

- name: Build NPM Module
run: yarn build

- name: Publish to NPM
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
run: npm publish --access public
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.idea
build
build_assets
node_modules
dist
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Playwright Arduino

Mocks the WebSerial API to test Arduino Uploaders Playwright

## Usage

Install the package with `yarn add -D @leaphy-robotics/playwright-arduino` or using NPM `npm i --save-dev @leaphy-robotics/playwright-arduino`.

``js
import { test, expect } from '@playwright/test';
import setup from '@leaphy-robotics/playwright-arduino';

test('test', async ({ page }) => {
await setup(page);

// Your test code
...
});
``

## Development

### Building simulator
This step is required to be performed at least once `yarn build:simavr`

### Watching package
You can watch for changes and automatically recompile the NPM Module using `yarn watch`

### Using local package
Link the module using `yarn link`, now use it in your (test) project using `yarn link @leaphy-robotics/playwright-arduino`
14 changes: 14 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
set -e
rm -rf build_assets build

BUILD_PATH=$(realpath build)
mkdir build_assets build && cd build_assets

git clone https://github.com/buserror/simavr
cd simavr

make -j8
cd examples/board_simduino

EXECUTABLE=$(find . | grep simduino.elf)
cp ATmegaBOOT_168_atmega328.ihex "${EXECUTABLE}" "${BUILD_PATH}"
29 changes: 29 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@leaphy-robotics/playwright-arduino",
"version": "1.0.0",
"license": "LGPL-3.0-only",
"main": "./dist/index.js",
"type": "module",
"types": "./dist/index.d.ts",
"files": [
"dist",
"build"
],
"scripts": {
"build": "tsup src",
"watch": "tsup --watch src",
"build:simavr": "./build.sh"
},
"devDependencies": {
"@types/node": "^20.12.7",
"@types/serialport": "^8.0.5",
"@types/w3c-web-serial": "^1.0.6",
"playwright": "^1.43.0",
"tsup": "^8.0.2",
"tsx": "^4.7.2",
"typescript": "^5.4.5"
},
"dependencies": {
"serialport": "^12.0.0"
}
}
19 changes: 19 additions & 0 deletions src/board.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { spawn } from 'child_process'
import { ChildProcess } from "node:child_process";

class Board {
private process: ChildProcess
public port = '/tmp/simavr-uart0'

constructor() {
this.process = spawn('./simduino.elf', {
cwd: `${import.meta.dirname}/../build`
})
}

public stop() {
this.process.kill()
}
}

export default Board
169 changes: 169 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import {SerialPort} from "serialport";
import {Page} from "playwright";
import Board from "./board.ts";
import {randomUUID} from "node:crypto";
import {clearTimeout} from "node:timers";
import * as fs from "node:fs";

type CallbackEvent = {
resolve: (value: unknown) => void,
type: string,
args: any[]
}

declare module globalThis {
let requestSerial: (options: SerialPortRequestOptions) => Promise<string>,
openSerial: (id: string, options: SerialOptions) => Promise<void|Error>,
readPort: (id: string) => Promise<number[]|Error>,
readCallback: (data: number[]) => void,
writePort: (id: string, data: number[]) => Promise<void|Error>,
closePort: (id: string) => Promise<void|Error>,
setSignals: (id: string, signals: SerialOutputSignals) => Promise<void|Error>,
getPorts: () => Promise<string[]>,
onDone: Record<string, (value: any) => void>,
reader: ReadableStreamDefaultReader<CallbackEvent>
}

const ports: Record<string, SerialPort|null> = {}
export default async function setup(page: Page) {
const arduino = new Board()

const methods: Record<string, (...args: any[]) => Promise<any>> = {
async requestSerial(_page: Page, _options: SerialPortRequestOptions) {
const id = randomUUID()
ports[id] = null

return String(id)
},
async openSerial(_page: Page, id: string, options: SerialOptions) {
if (ports[id]) throw new DOMException('Port already open!')

return new Promise<void|Error>(resolve => {
ports[id] = new SerialPort({
baudRate: options.baudRate,
path: arduino.port,
dataBits: 8,
parity: 'none',
stopBits: 1,
}, err => {
if (err) return resolve(err)
resolve()
})
})
},
async readPort(page: Page, id: string) {
if (!ports[id]) throw new Error(`User read request to undefined port: ${id}`)

const port = ports[id] as SerialPort
try {
let buffer: number[]|null = null
let timeout: NodeJS.Timeout|null
port.on('data', async (data: Buffer) => {
if (!buffer) buffer = []
buffer.push(...Array.from(data.values()))

if (timeout) clearTimeout(timeout)
timeout = setTimeout(async () => {
const copy = buffer
buffer = null

await page.evaluate((result) => {
if (!result) return
globalThis.readCallback(result)
}, copy)
}, 25)
})
} catch (e) {
return e
}
},
async writePort(_page: Page, id: string, data: number[]) {
if (!ports[id]) throw new Error(`User write request to undefined port: ${id}`)

const port = ports[id] as SerialPort
return new Promise<void|Error>(resolve => {
port.write(Buffer.from(data), err => {
if (err) return resolve(err)
resolve()
})
})
},
async closePort(_page: Page, id: string) {
if (!ports[id]) throw new Error(`User close request to undefined port: ${id}`)

const port = ports[id] as SerialPort
ports[id] = null
return new Promise<void|Error>(resolve => port.close(err => {
if (err) return resolve(err)
resolve()
}))
},
setSignals(_page: Page, id: string, signals: SerialOutputSignals) {
if (!ports[id]) throw new Error(`User setSignals request to undefined port: ${id}`)

const port = ports[id] as SerialPort
return new Promise<void|Error>(resolve => {
port.set({
dtr: signals.dataTerminalReady,
rts: signals.requestToSend,
brk: signals.break
}, () => {
resolve()
})
})
},
async getPorts(_page: Page) {
return Array.from(Object.keys(ports))
}
}

await Promise.all(Object.entries(methods).map(async ([type, implementation]) => {
await page.exposeFunction(type, (...args: any[]) => implementation(page, ...args))
}))

await page.route('**/avrdude-worker.js', async route => {
const response = await route.fetch();
const script = await response.text();
await route.fulfill({ response, body: `${fs.readFileSync(`${import.meta.dirname}/page.js`)}\n\n${script}` });
});

await page.addInitScript({
path: `${import.meta.dirname}/page.js`
})

page.on('worker', async worker => {
let open = true
worker.on('close', () => open = false)
while (open) {
try {
const action = await worker.evaluate(async () => {
if (!globalThis.reader) return

const {value, done} = await globalThis.reader.read()
if (done || !value) return

const execution = crypto.randomUUID()
globalThis.onDone[execution] = value.resolve
return {
execution,
type: value.type,
args: value.args
}
})

if (!action) continue
if (!methods[action.type]) continue

methods[action.type](worker, ...action.args).then(async result => {
await worker.evaluate(async ({ execution, result }) => {
globalThis.onDone[execution](result)
}, {
execution: action.execution,
result
})
})
} catch { /* Once the worker has closed it will throw */ }
}
})

}
Loading

0 comments on commit a2ba870

Please sign in to comment.