Compare commits

..

No commits in common. "client" and "main" have entirely different histories.
client ... main

96 changed files with 6105 additions and 2531 deletions

View File

@ -1,18 +0,0 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

137
.gitignore vendored
View File

@ -1,25 +1,132 @@
example-client/
build
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
.pnpm-debug.log*
node_modules
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
dist-ssr
*.local
yarn.lock
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Norman Andriansyah
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

773
README.md
View File

@ -1,13 +1,772 @@
# Lunar Vein: Arduino Client
# Lunar Vein: Arduino
Client version of the [lunar vein arduino](https://github.com/norman-andrians/lunar-vein-arduino) project, This is the source code of the serial communication API testing example, built with React, Typescript and Vite. The example web client code in the [main branch](https://github.com/norman-andrians/lunar-vein-arduino/tree/main/src/client) is only a **build version** of this code.
A **SIMPLE** REST API based serial communication. Assisted Firmata protocol and johnny-five API, enabling software communication with the Arduino board using the Server API. that's it. Idk about electronics and networking actually. But that piece of knowledge motivated me to make some IoT stuff. Enjoy (⁠づ⁠ ̄⁠ ⁠³⁠ ̄⁠)⁠づ
![ENA BBQ](https://media1.tenor.com/m/Nl2_tsGV-qkAAAAC/enadbbq-dreambbq.gif)
<small>*the animation is from ENA BBQ series by JoelG</small>
## Setup
Tools that are required
- Arduino Board
- [Arduino IDE](https://www.arduino.cc/en/software)
- [Node.js](https://nodejs.org/en) v16.0.0^ or other javascript runtime
### Board
Arduino assembly guide also available in [johnny-five](https://github.com/rwaldron/johnny-five?tab=readme-ov-file#setup-and-assemble-arduino) documentation
1. Plug in your Board via USB
2. Open Arduino IDE then select your Board
3. Go to `File > Examples > Firmata` Select `StandarFirmataPlus`
4. Upload the sketch
### Server
1. Clone project and Install
```bash
git clone https://github.com/norman-andrians/lunar-vein-arduino.git && cd lunar-vein-arduino
npm i
```
2. Add `.env` file in the project root directory with the `SERIAL_PORT` variable
```dotenv
SERIAL_PORT=/dev/ttyUSB0
```
3. Run the project
```bash
npm start
```
4. If the board is connected try to open the client page in [https://localhost:3000/](https://localhost:3000/)
![Client page](./docs/client.png)
#### Other scripts
- You can see what ports are connected to the device by running `npm run ports` script.
```bash
npm run ports
```
```
> lunar-vein-arduino@1.0.0 ports
> ts-node src/ports/print.ts
3 Ports available
1. /dev/ttyUSB0
2. /dev/ttyUSB1
3. /dev/ttyACM0
```
# REST API Documentation
## Table of contents
- [Common HTTP Responses](#common-http-responses)
- [JSON Response](#json-response)
- [Example Request](#example-request)
- [Digital](#digital)
- [Digital Read](#digital-read)
- [Digital Write](#digital-write)
- [Analog](#analog)
- [Analog Read](#analog-read)
- [Analog Write](#analog-write)
- [PIN](#pin)
- [Read PIN Mode](#read-pin-mode)
- [Set PIN Mode](#set-pin-mode)
- [LED](#led)
- [Read LED state](#read-led-state)
- [Set LED state](#set-led-state)
- [RGB LED](#rgb-led)
- [Read RGB state](#read-rgb-state)
- [Set RGB state](#set-rgb-state)
- [Piezo](#piezo)
- [Piezo Tone](#piezo-tone)
- [Piezo Note](#piezo-note)
- [Piezo Play Music](#piezo-play-music)
## Common HTTP Responses
Some HTTP responses are sent with JSON, otherwise an HTML body will be sent which is the default express.js response
| HTTP Status | Marks |
|-------------|-------|
| 200 | The request `act` was successful |
| 400 | You may be making an invalid request, try to check the payload or recheck the documentation. |
| 404 | The resource wass not found |
| 405 | Method not allowed, servers usually only accept `GET` and `PATCH` methods |
| 500 | An error in the app server, if it continues please raise an issue or ask to contribute. |
## JSON Response
```json
{
"status": 200,
"pin_state": {
"13": "HIGH"
},
"message": "Changed pin state 13 to HIGH"
}
```
| Property | Description |
|----------|-------------|
| status | HTTP Status code |
| pin_state | The state value of the pin that has been changed |
| message | Descriptive message of the changed state |
## Example Request
This is an example of sending a request to turn on the LED light, [See LED API](#set-led-state)
**Using curl**
```bash
curl -X PATCH http://localhost:3000/api-arduino/led/13/on
```
**Using fetch async/await javascript**
```javascript
async function turnOnLed() {
const res = await fetch("http://localhost:3000/api-arduino/led/13/on", { method: 'PATCH' }); // Set LED to HIGH
const data = await res.json();
console.log(data); // { status: 200, pin_state: { 13: high }, message: "Pin 13 Set to HIGH" }
}
```
## Digital
### Digital Read
- **URL Endpoint**
`/api-arduino/digital/:pin`
- **URL Params**
| Params | Mark | Type | Required | Description |
|--------|------|------|----------|-------------|
| pin | pin | `string` | true | Seleced pin |
- **Method**
`GET`
- **Example Request**
```javascript
fetch("http://localhost:3000/api-arduino/digital/13")
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error('error', err));
```
- **Example Response**
```json
{
"status": 200,
"pin_state": {
"13": "HIGH"
}
}
```
### Digital Write
- **URL Endpoint**
`/api-arduino/digital`
- **Body**
```typescript
{
pin: number,
state: 'HIGH' | 'LOW' | 1 | 0 | string
}
```
- **Method**
`PATCH`
- **Example Request**
```javascript
var data = {
pin: 13,
state: 'LOW'
}
var options = {
method: 'PATCH',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow'
}
fetch("http://localhost:3000/api-arduino/digital", options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error('error', err));
```
- **Example Response**
```json
{
"status": 200,
"pin_state": {
"13": "HIGH"
}
}
```
## Analog
### Analog Read
- **URL Endpoint**
`/api-arduino/analog/:p`
- **URL Params**
| Params | Mark | Type | Required | Description |
|--------|------|------|----------|-------------|
| pin | pin | `string` | true | Seleced pin |
- **Method**
`GET`
- **Example Request**
```javascript
fetch("http://localhost:3000/api-arduino/analog/A0")
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error('error', err));
```
- **Example Response**
```json
{
"status": 200,
"analog_state": {
"A0": 1023
}
}
```
### Analog Write
- **URL Endpoint**
`/api-arduino/analog`
- **Body**
```typescript
{
pin: string | number,
value: number
}
```
- **Method**
`PATCH`
- **Example Request**
```javascript
var data = {
pin: 'A0',
value: 1023
}
var options = {
method: 'PATCH',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow'
}
fetch("http://localhost:3000/api-arduino/analog", options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error('error', err));
```
- **Example Response**
```json
{
"status": 200,
"pin_state": {
"13": 1023
}
}
```
## React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
## PIN
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
### Read PIN Mode
- **URL Endpoint**
`/api-arduino/pin/:p/`
- **URL Params**
| Params | Mark | Type | Required | Description |
|--------|------|------|----------|-------------|
| p | pin | `string` | true | Seleced pin |
- **Method**
`GET`
- **Example Request**
```javascript
fetch("http://localhost:3000/api-arduino/pin/13")
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error('error', err));
```
- **Example Response**
```json
{
"status": 200,
"pins": [
{
"pin": 13,
"mode": "OUTPUT"
}
],
"message": "Pin 13 is OUTPUT"
}
```
### Set PIN Mode
- **URL Endpoint**
`/api-arduino/pin/:p/:m`
- **URL Params**
| Params | Mark | Type | Required | Description |
|--------|------|------|----------|-------------|
| p | pin | `string` | true | Seleced pin |
| m | mode | `'input'`, `'output'`, `'servo'` | true | Pin Mode |
- **Method**
`PATCH`
- **Example Request**
```javascript
fetch("http://localhost:3000/api-arduino/pin/13/output", { method: 'PATCH' })
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error('error', err));
```
- **Example Response**
```json
{
"status": 200,
"pins": [
{
"pin": 13,
"mode": "OUTPUT"
}
],
"message": "Pin 13 setted as OUTPUT"
}
```
## LED
### Read LED state
- **URL Endpoint**
`/api-arduino/led/:p/`
- **URL Params**
| Params | Mark | Type | Required | Description |
|--------|------|------|----------|-------------|
| p | pin | `string` | true | Seleced pin |
- **Method**
`GET`
- **Example Request**
```javascript
fetch("http://localhost:3000/api-arduino/led/13")
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error('error', err));
```
- **Example Response**
```json
{
"status": 200,
"pin_state": {
"13": "HIGH"
},
"message": "Led pin 13 is HIGH"
}
```
### Set LED state
- **URL Endpoint**
`/api-arduino/led/:p/:a`
- **URL Params**
| Params | Mark | Type | Required | Description |
|--------|------|------|----------|-------------|
| p | pin | `string` | true | Seleced pin |
| a | act | `'on'`, `'off'`, `'high'`, `'low'` | true | Action |
- **Method**
`PATCH`
- **Example Request**
```javascript
fetch("http://localhost:3000/api-arduino/led/13/output", { method: 'PATCH' })
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error('error', err));
```
- **Example Response**
```json
{
"status": 200,
"pin_state": {
"13": "HIGH"
},
"message": "Changed pin state 13 to HIGH"
}
```
## RGB LED
### Read RGB state
- **URL Endpoint**
`/api-arduino/rgb-led`
- **Body**
```typescript
{
r: number,
g: number,
b: number
}
```
- **Method**
`POST`
- **Example Request**
```javascript
var data = {
r: 7,
g: 6,
b: 5
}
var options = {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow'
}
fetch("http://localhost:3000/api-arduino/rgb-led", options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error('error', err));
```
- **Example Response**
```json
{
"status": 200,
"pin_state": {
"7": true,
"6": true,
"5": false
}
}
```
### Set RGB state
- **URL Endpoint**
`/api-arduino/rgb-led`
- **Body**
```typescript
{
r: {
pin: number,
value: boolean
},
g: {
pin: number,
value: boolean
},
b: {
pin: number,
value: boolean
}
}
```
- **Method**
`PATCH`
- **Example Request**
```javascript
var data = {
r: {
pin: 7,
value: true
},
g: {
pin: 6,
value: true
},
b: {
pin: 5,
value: false
}
}
var options = {
method: 'PATCH',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow'
}
fetch("http://localhost:3000/api-arduino/rgb-led", options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error('error', err));
```
- **Example Response**
```json
{
"status": 200,
"pin_state": {
"7": true,
"6": true,
"5": false
},
"message": "Success changed pins 7, 6, 5 to state HIGH, HIGH, LOW"
}
```
## Piezo
### Piezo Tone
- **URL Endpoint**
`/api-arduino/piezo/:p/:f`
- **URL Params**
| Params | Mark | Type | Required | Description |
|--------|------|------|----------|-------------|
| p | pin | `string` | true | Seleced pin |
| f | frequency | `number` | true | Frequency |
- **Method**
`PATCH`
- **Example Request**
```javascript
fetch("http://localhost:3000/api-arduino/piezo/6/300", { method: 'PATCH' })
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error('error', err));
```
- **Example Response**
```json
{
"status": 200,
"pin_tone": {
"6": 300
},
"message": "Piezo 6 tone 300"
}
```
### Piezo Note
- **URL Endpoint**
`/api-arduino/piezo/note`
- **Body**
```typescript
{
pin: number,
note: string
}
```
- **Method**
`PATCH`
- **Example Request**
```javascript
var data = {
pin: 6,
note: "B3"
}
var options = {
method: 'PATCH',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow'
}
fetch("http://localhost:3000/api-arduino/piezo/note", options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error('error', err));
```
- **Example Response**
```json
{
"status": 200,
"pin_tone": {
"6": 247
},
"pin_note": {
"6": "B3"
},
"message": "Piezo 6 tone note B3"
}
```
### Piezo Play Music
- **URL Endpoint**
`/api-arduino/piezo/music`
- **Body**
```typescript
{
pin: number,
notes: string[],
beats: number,
tempo: number
}
```
Example
```javascript
{
pin: 6,
notes: ["F4", "G4", "F4", "G4"],
beats: 1/4,
tempo: 100
}
```
- **Method**
`PATCH`
- **Example Request**
```javascript
var data = {
pin: 6,
notes: ["E4", "E4", "F4", "G4", "G4", "F4", "E4", "D4", "C4", "C4", "D4", "E4", "D4", "-", "C4", "C4"], // ode to joy notes
beats: 1/2,
tempo: 100
}
var options = {
method: 'PATCH',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow'
}
fetch("http://localhost:3000/api-arduino/piezo/music", options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error('error', err));
```
- **Example Response**
```json
{
"status": 200,
"pin_notes": {
"6": [ "E4", "E4", "F4", "G4", "G4", "F4", "E4", "D4", "C4", "C4", "D4", "E4", "D4", "-", "C4", "C4" ]
},
"message": "Piezo 6 play notes E4, E4, F4, G4..."
}
```

BIN
docs/client.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

4
nodemon.json Normal file
View File

@ -0,0 +1,4 @@
{
"watch": ["src"],
"ext": ".ts,.js"
}

3972
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,34 +1,41 @@
{
"name": "lunar-vein-arduino-client",
"name": "lunar-vein-arduino",
"version": "0.0.6",
"description": "A test to arduino",
"main": "index.ts",
"author": "Norman Andrians",
"license": "MIT",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
"start": "nodemon --exec ts-node ./src",
"dev": "nodemon --exec ts-node ./src/dev.ts",
"ports": "ts-node src/ports/print.ts",
"test-blink": "nodemon test/blink.js"
},
"dependencies": {
"axios": "^1.6.7",
"bootstrap-icons": "^1.11.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"socket.io-client": "^4.7.4"
"@serialport/bindings": "^9.2.9",
"@serialport/list": "^12.0.0",
"@types/ip": "^1.1.3",
"chalk": "^4.1.2",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.3",
"firmata": "^2.3.0",
"ip": "^2.0.1",
"johnny-five": "^2.1.0",
"nodemon": "^3.1.0",
"pkg": "^5.8.1",
"serialport": "^12.0.0",
"socket.io": "^4.7.4"
},
"devDependencies": {
"@types/react": "^18.2.56",
"@types/react-dom": "^18.2.19",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"@typescript-eslint/parser": "^7.0.2",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.18",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"typescript": "^5.2.2",
"vite": "^5.1.4"
"@types/express": "^4.17.21",
"@types/firmata": "^0.19.8",
"@types/johnny-five": "^2.1.9",
"@types/nodemon": "^1.19.6",
"@types/serialport": "^8.0.5",
"@types/socket.io": "^3.0.2",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
}

View File

@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1,13 +0,0 @@
import { BoardControllerProvider } from "./contexts/BoardController";
import MainPage from "./pages/MainPage";
import "bootstrap-icons/font/bootstrap-icons.min.css";
function App () {
return (
<BoardControllerProvider>
<MainPage />
</BoardControllerProvider>
)
}
export default App;

40
src/app.ts Normal file
View File

@ -0,0 +1,40 @@
import readline from "node:readline/promises";
import chalk from "chalk";
import { Board } from "johnny-five";
import { selectPort } from "./ports";
import { run } from "./server";
import { suBoard } from "./setup";
import { cfg } from "./setup/config";
import { getPublicIp } from "./utils/network";
async function questions () {
const line = readline.createInterface({
input: process.stdin,
output: process.stdout
})
const exposeHost = await line.question("Run in public network? (y/n) ");
if (exposeHost.toLowerCase() == "y") {
console.warn(chalk.yellow("Running a server on a public host may not be able to use the socket.io api"));
cfg.server.host = getPublicIp();
}
line.close();
}
export default async function main () {
const port = await selectPort();
await questions();
console.log(`Selected port ${port}`);
cfg.port = port;
suBoard.board = new Board({
port: port,
debug: false,
repl: false
});
run();
}

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 855 KiB

After

Width:  |  Height:  |  Size: 855 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 MiB

After

Width:  |  Height:  |  Size: 4.8 MiB

View File

Before

Width:  |  Height:  |  Size: 486 KiB

After

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 365 KiB

After

Width:  |  Height:  |  Size: 365 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -5,9 +5,10 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lunar Vein: Arduino</title>
<script type="module" crossorigin src="/assets/index-CfbleRJs.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-jsM6q3pf.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,35 +0,0 @@
import { ReactNode, Ref } from "react";
interface ControlSectionProps {
title: string;
description?: string | ReactNode;
id?: string;
refto?: Ref<HTMLDivElement>
stack: ReactNode;
colSpan?: number;
stackType?: 'grid' | 'flex';
}
export default function ControlSection ({
title,
description,
id,
refto,
stack
}: ControlSectionProps) {
return (
<div className="container py-16" id={id} ref={refto}>
<div className="container-grid items-center relative">
<div className={`col-span-6`}>
<h2 className="text-4xl font-poppins font-bold leading-normal mb-4">
{title}
</h2>
{description && <p className="mb-8">{description}</p>}
<div className={`flex flex-wrap gap-6`}>
{stack}
</div>
</div>
</div>
</div>
)
}

View File

@ -1,34 +0,0 @@
import MadeBy from "./info/MadeBy";
import Social from "./info/Social";
export default function Footer () {
return (
<footer className="bg-black bg-opacity-80 border-t border-border absolute bottom-0 left-0 w-full">
<div className="container py-12 text-center">
<MadeBy className="font-poppins" />
<div className="mt-8 flex flex-row justify-center gap-3">
<Social
name="X/Twitter"
link="https://twitter.com/NormanAndrians"
icon={<i className="bi bi-twitter-x text-3xl"></i>}
/>
<Social
name="Instagram"
link="https://www.instagram.com/normanandrians25/"
icon={<i className="bi bi-instagram text-3xl"></i>}
/>
<Social
name="Facebook"
link="https://www.facebook.com/profile.php?id=100076314820736"
icon={<i className="bi bi-facebook text-3xl"></i>}
/>
<Social
name="Github"
link="https://github.com/norman-andrians"
icon={<i className="bi bi-github text-3xl"></i>}
/>
</div>
</div>
</footer>
)
}

View File

@ -1,15 +0,0 @@
import { ReactNode } from "react";
import BackroundImg from "../assets/img/background.png";
export default function MainBody({ children }: { children?: ReactNode }) {
return (
<div
className="bg-background text-white bg-repeat-y bg-cover relative"
style={{
backgroundImage: `url(${BackroundImg})`
}}
>
{children}
</div>
)
}

View File

@ -1,75 +0,0 @@
import { CSSProperties, MouseEventHandler, ReactNode } from "react";
interface ButtonProps {
className?: string;
style?: CSSProperties,
onClick?: MouseEventHandler
children?: ReactNode
}
function Primary ({
className,
style,
onClick,
children
}: ButtonProps) {
return (
<button
className={`btn bg-primary bg-opacity-100 hover:bg-opacity-80 ${className}`}
style={style}
onClick={onClick}
>{children}</button>
)
}
function Secondary ({
className,
style,
onClick,
children
}: ButtonProps) {
return (
<button
className={`btn bg-finn border hover:bg-secondary border-border ${className}`}
style={style}
onClick={onClick}
>{children}</button>
)
}
function Danger ({
className,
style,
onClick,
children
}: ButtonProps) {
return (
<button
className={`btn bg-transparent border text-danger hover:bg-danger hover:text-white border-danger ${className}`}
style={style}
onClick={onClick}
>{children}</button>
)
}
function Button ({
className,
style,
onClick,
children
}: ButtonProps) {
return (
<button
className={`btn bg-finn border hover:bg-secondary border-border ${className}`}
style={style}
onClick={onClick}
>{children}</button>
)
}
Button.Primary = Primary;
Button.Secondary = Secondary;
Button.Danger = Danger;
export default Button;

View File

@ -1,83 +0,0 @@
import { FocusEvent, useState } from "react";
type EvoDropDownItem = {
name: string,
value: any
}
interface EvoDropDownProps {
className?: string;
name?: string;
items: EvoDropDownItem[];
initItem?: EvoDropDownItem;
onValueChange?: (item: EvoDropDownItem) => void;
onBlur?: (e: FocusEvent) => void
}
interface MenuProps extends EvoDropDownProps {
appear: boolean;
}
function Menu ({ className, items, initItem, appear, onValueChange, onBlur }: MenuProps) {
const [currentItem, setItem] = useState<EvoDropDownItem>(initItem || items[0]);
return (
<div className={`absolute p-2 flex flex-col w-full bg-black border mt-3 border-border rounded-lg ${appear ? "block" : "hidden"} ${className}`} onBlur={onBlur}>
{items.map((item) => (
<button
className="py-2 px-3 flex justify-between items-center bg-transparent bg-opacity-100 hover:bg-indigo-300 hover:bg-opacity-20 rounded-md"
onClick={() => {
setItem(item);
if (onValueChange) onValueChange(item)}
}
>
<div className="text-left text-sm">
{item.name}
</div>
{currentItem.value == item.value && <div className="rounded-full bg-white w-2 h-2"></div>}
</button>
))}
</div>
)
}
function EvoDropDown ({ className, name, items, initItem, onValueChange }: EvoDropDownProps) {
const [appear, setAppear] = useState<boolean>(false);
const [currentItem, setItem] = useState<EvoDropDownItem>(initItem || items[0]);
const handleClick = () => {
setAppear(!appear);
}
return (
<div className="relative">
<button className={`flex items-center justify-between cursor-pointer bg-finn border border-border rounded-lg ${className}`} onClick={handleClick}>
<div className="w-fit p-2 text-sm ps-4">{name}</div>
<div className="w-1/2 h-full p-2 text-right pe-4 text-sm font-roboto-mono bg-transparent">
{currentItem.name}
</div>
</button>
<div className={`absolute p-2 flex flex-col w-full bg-black border mt-3 border-border rounded-lg ${appear ? "block" : "hidden"}`}>
{items.map((item) => (
<button
className="py-2 px-3 flex justify-between items-center bg-transparent bg-opacity-100 hover:bg-indigo-300 hover:bg-opacity-20 rounded-md"
onClick={() => {
setAppear(false);
setItem(item);
if (onValueChange) onValueChange(item)}
}
>
<div className="text-left text-sm">
{item.name}
</div>
{currentItem.value == item.value && <div className="rounded-full bg-white w-2 h-2"></div>}
</button>
))}
</div>
</div>
)
}
EvoDropDown.Menu = Menu;
export default EvoDropDown;

View File

@ -1,38 +0,0 @@
import { ChangeEventHandler, HTMLInputTypeAttribute, useRef } from "react";
interface EvoInputProps {
className?: string;
name: string;
value: string;
type?: HTMLInputTypeAttribute
onChange?: ChangeEventHandler<HTMLInputElement>
}
export default function EvoInput ({
className,
name,
value,
type,
onChange
}: EvoInputProps) {
const labelRef = useRef<HTMLInputElement | null>(null);
const handleClick = () => {
labelRef.current?.focus();
}
return (
<label className={`flex items-center justify-between cursor-pointer bg-finn border border-border rounded-lg ${className}`} onClick={handleClick}>
<div className="w-fit p-2 text-sm ps-4">{name}</div>
<input
className="w-1/2 h-full p-2 text-right pe-4 text-sm font-roboto-mono bg-transparent"
type={type}
name={name}
id=""
value={value}
ref={labelRef}
onChange={onChange}
/>
</label>
)
}

View File

@ -1,31 +0,0 @@
import { ChangeEventHandler, HTMLInputTypeAttribute } from "react";
interface InputProps {
className?: string;
name?: string;
placeholder?: string;
value?: string;
type?: HTMLInputTypeAttribute
onChange?: ChangeEventHandler<HTMLInputElement>
}
export default function Input ({
className,
name,
placeholder,
value,
type,
onChange
}: InputProps) {
return (
<input
className={`px-4 py-2 text-left text-sm font-roboto-mono bg-finn border border-border rounded-lg ${className}`}
placeholder={placeholder}
type={type}
name={name}
id=""
value={value}
onChange={onChange}
/>
)
}

View File

@ -1,92 +0,0 @@
import { ChangeEventHandler, FormEventHandler, MouseEventHandler } from "react";
import Switch from "./Switch";
interface PinBoxProps {
className?: string;
value: number | string;
onValueChange?: ChangeEventHandler<HTMLInputElement | null>;
onDelete?: FormEventHandler<HTMLButtonElement | null>;
minusBtn?: boolean;
}
interface ToggleProps extends PinBoxProps {
state: boolean;
onStateChange?: FormEventHandler<HTMLButtonElement | null>;
}
function PinBox ({
className,
value,
onValueChange,
onDelete,
minusBtn
}: PinBoxProps) {
return (
<div className={`flex flex-col gap-3 ${className}`}>
{minusBtn && <button
className="border bg-transparent ms-auto hover:bg-secondary border-border rounded px-2"
onClick={onDelete}
>
<i className="bi bi-dash"></i>
</button>}
<div className="bg-secondary border border-border rounded-lg flex flex-col h-36 animate-size-in relative">
<div className="font-roboto-mono py-4 text-center">
Pin
</div>
<input
className="flex-grow bg-transparent pb-6 border-none text-center text-3xl font-roboto-mono"
type="number"
name="pinbox"
id=""
value={value}
onChange={onValueChange}
/>
</div>
</div>
)
}
function Toggle ({
className,
value,
minusBtn,
onDelete,
state,
onStateChange,
onValueChange
}: ToggleProps) {
return (
<div className={`flex flex-col gap-6 ${className}`}>
<PinBox
value={value}
minusBtn={minusBtn}
onDelete={onDelete}
onValueChange={onValueChange}
/>
<Switch
state={state}
onChange={onStateChange}
/>
</div>
)
}
function Add ({
className,
onClick
}: {
className?: string,
onClick?: MouseEventHandler<HTMLButtonElement>
}) {
return (
<button className={`bg-finn hover:bg-secondary transition border border-border rounded-lg flex items-center justify-center h-36 ${className}`} onClick={onClick}>
<i className="bi bi-plus text-border text-7xl"></i>
</button>
)
}
PinBox.Add = Add;
PinBox.Toggle = Toggle;
export default PinBox;

View File

@ -1,28 +0,0 @@
import { ChangeEventHandler } from "react";
interface SliderProps {
className?: string;
name?: string;
value?: number;
onChange?: ChangeEventHandler<HTMLInputElement | null>
}
export function Slider ({ className, name, value, onChange }: SliderProps) {
const percent = value ? value/10 : 0
return (
<input
className={`slider ${className}`}
style={{
background: `linear-gradient(to right, #3a3affcc ${percent}%, #3a3aff4d ${percent}%)`
}}
type="range"
min={0}
max={1000}
name={name}
value={value}
onChange={onChange}
id=""
/>
)
}

View File

@ -1,13 +0,0 @@
import { FormEventHandler } from "react";
export default function Switch ({ state, onChange }: { state: boolean, onChange?: FormEventHandler<HTMLButtonElement> }) {
return (
<button
className={`border switch ${state ? 'switch-on' : 'switch-off'}`}
onClick={onChange}
>
<div className="handler w-1/2 font-roboto-mono">OFF</div>
<input type="hidden" name="" className="hidden" />
</button>
)
}

View File

@ -1,13 +0,0 @@
import NolaImg from "../../assets/img/ocs/nola_3.png";
export default function MadeBy ({ className }: { className?: string }) {
return (
<div className={`group ${className}`}>
Made By
<a href="https://github.com/norman-andrians" target="_blank">
<img className="inline group-hover:animate-wewew" src={NolaImg} />
</a>
Norman Andrians
</div>
)
}

View File

@ -1,21 +0,0 @@
import { ReactNode } from "react";
interface SocialPops {
className?: string;
name: string;
link: string;
icon: ReactNode | string;
}
export default function Social({ className, name, link, icon }: SocialPops) {
return (
<a
href={link}
title={name}
target="_blank"
className={`opacity-80 transition bg-transparent hover:bg-black w-14 h-14 flex justify-center items-center rounded-full ${className}`}
>
{icon}
</a>
);
}

View File

@ -1,14 +0,0 @@
interface TwoRowTab {
className?: string;
prop?: string;
value?: string | number;
}
export default function TwoRowTab ({ className, prop, value }: TwoRowTab) {
return (
<div className={`flex justify-between font-roboto-mono ${className}`}>
<div>{prop}</div>
<div>{value}</div>
</div>
)
}

View File

@ -1,22 +0,0 @@
import Img from "../../assets/img/absurd-closing.png";
import Bg from "../../assets/img/bg-closing.png";
export default function Closing () {
return (
<div className="bg-gradient-to-t from-background via-background to-transparent">
<div className="pt-64 pb-96 bg-no-repeat bg-cover bg-bottom" style={{
backgroundImage: `url(${Bg})`,
}}>
<div className="container">
<header className="text-center font-poppins">
<h1 className="text-5xl font-bold leading-normal">That's All {":)"}</h1>
<div>Other features are still under development</div>
</header>
<picture className="">
<img className="mt-16 w-full h-auto" src={Img} alt="Goofy ahh closing" />
</picture>
</div>
</div>
</div>
)
}

View File

@ -1,81 +0,0 @@
import { ChangeEvent, Ref, useEffect } from "react";
import PinBox from "../forms/PinBox";
import { useLed } from "../../hooks";
import ControlSection from "../ControlSection";
import { PinState } from "../../types/board";
import { PatchLed } from "../../controllers/BoardController";
function LedItem ({ led, index }: { led: PinState, index: number }) {
const { removeLed, setLed, setLedPin } = useLed();
const handleChange = (e: ChangeEvent<HTMLInputElement | null>) => {
const pin = e.target?.value;
setLedPin(index, pin);
}
const toggleLed = () => {
setLed(led.pin, !led.state);
}
const handleRemove = () => {
removeLed(index);
}
useEffect(() => {
const pin: number = typeof led.pin == "string" ? Number.parseInt(led.pin) : led.pin;
PatchLed(pin, led.state);
}, [led.state]);
return (
<PinBox.Toggle
className="w-36 animate-fade-in"
minusBtn={true}
value={led.pin}
state={led.state}
onValueChange={handleChange}
onStateChange={toggleLed}
onDelete={handleRemove}
/>
)
}
function ControlLED ({ refto }: { refto?: Ref<HTMLDivElement> }) {
const { addLed, leds } = useLed();
const handleAdd = (): void => {
let anopin = 13;
for (let i = 0; i < leds.length; i++) {
if (leds.filter(led => led.pin == anopin).length > 0) {
anopin--;
}
else break;
}
addLed(anopin, false);
}
return (
<ControlSection
title="LED"
id="led"
description={<>
Light Emitting Diode {"("}LED{")"} is one of the simplest Arduino electronics components, it is a type of semiconductor diode that produces light when an electric current flows through it.
</>}
refto={refto}
stack={(<>
{leds.map((led, i) => (
<LedItem
led={led}
index={i}
key={i}
/>
))}
{leds.length < 14 && <PinBox.Add
className="w-36 mt-10"
onClick={handleAdd}
/>}
</>)}
/>
)
}
export default ControlLED;

View File

@ -1,142 +0,0 @@
import { ChangeEvent, MouseEventHandler, Ref, useEffect, useState } from "react";
import { usePhotoresistor } from "../../hooks";
import { DynamicPinState } from "../../types/board";
import CircleResistance from "../shapes/CircleResistance";
import TwoRowTab from "../info/TwoRowTab";
import EvoInput from "../forms/EvoInput";
import Button from "../forms/Button";
import { io } from "../../socket/socket.io";
function Card ({ index, resistor }: { index: number, resistor: DynamicPinState }) {
const { setResistorPin, setResistance, removeResistor } = usePhotoresistor();
const [isListen, setListen] = useState<boolean>(false);
const resistance: number = Math.floor(resistor.state);
const intensity: number = Math.floor(100 - (resistor.state / 1023 * 100));
const toggleListen = () => {
setListen(!isListen);
}
const handleChange = (e: ChangeEvent<HTMLInputElement | null>) => {
setResistorPin(index, e.target.value);
}
const handleDelete = () => {
removeResistor(index);
}
useEffect(() => {
const pin = typeof resistor.pin == "string" && resistor.pin[0] == "A" ?
resistor.pin[1] : resistor.pin;
if (isListen) {
io.emit("set-photoresistor", pin);
const handler = (value: string) => {
const res: number = Number.parseInt(value);
setResistance(resistor.pin, res);
}
io.on("photoresistor", handler);
return () => {
io.off("photoresistor", handler);
}
}
}, [io, isListen]);
return (
<div className="border border-border bg-secondary col-span-2 rounded-lg p-6 animate-size-fade-in">
<div className="flex justify-items-end">
<button className="ms-auto bg-finn hover:bg-secondary transition border border-border rounded-lg px-5" onClick={handleDelete}>
<i className="bi bi-dash text-xl"></i>
</button>
</div>
<div className="h-52 flex items-center justify-center">
<CircleResistance
intensity={intensity}
/>
</div>
<div className="flex flex-col mt-3 gap-3">
<TwoRowTab
prop="Resistance"
value={resistance}
/>
<TwoRowTab
prop="Light Intensity"
value={`${intensity}%`}
/>
<EvoInput
className="h-10"
name="Pin"
type="text"
value={resistor.pin.toString()}
onChange={(handleChange)}
/>
{!isListen ? (
<Button.Primary onClick={toggleListen} className="text-sm !p-0 h-11">Start</Button.Primary>
) : (
<Button.Danger onClick={toggleListen} className="text-sm !p-0 h-11">Stop</Button.Danger>
)}
</div>
</div>
)
}
function CardPlus ({ onClick }: { onClick?: MouseEventHandler<HTMLButtonElement> }) {
return (
<button
className="bg-finn h-[470px] hover:bg-secondary transition col-span-2 rounded-lg border border-border flex items-center justify-center"
onClick={onClick}
>
<i className="bi bi-plus text-6xl text-border"></i>
</button>
)
}
function ControlPhotoresistor ({ refto }: { refto?: Ref<HTMLDivElement> }) {
const { photoresistor: resistors, addResistor } = usePhotoresistor();
const handleAdd = (): void => {
let anopin = 5;
for (let i = 0; i < resistors.length; i++) {
if (resistors.filter(resistor => (resistor.pin == `A${anopin}` || resistor.pin == anopin)).length > 0) {
anopin--;
}
else break;
}
addResistor(`A${anopin}`, 0);
}
return (
<div className="container py-16" id="rgb-led" ref={refto}>
<div className="container-grid items-center relative">
<div className={`col-span-8`}>
<h2 className="text-4xl font-poppins font-bold leading-normal mb-4">
Photoresistor
</h2>
<div className="grid grid-cols-8 mb-8">
<p className="col-span-6">Also known as LDR {"("}Light Dependent Resistor{")"}, it is an electronic component whose resistance changes based on the intensity of light it receives. The higher the light intensity, the lower the resistance, When exposed to intense light, a photoresistor experiences a decrease in resistance due to photoconduction. The molecules in the photoresistor material become more active and allow electric current to flow through the material more easily.</p>
</div>
<div className={`grid grid-cols-8 gap-6`}>
{resistors.map((resistor, i) => (
<Card
key={i}
resistor={resistor}
index={i}
/>
))}
{resistors.length < 6 && <CardPlus
onClick={handleAdd}
/>}
</div>
</div>
</div>
</div>
)
}
export default ControlPhotoresistor;

View File

@ -1,98 +0,0 @@
import PinBox from "../forms/PinBox";
import ControlSection from "../ControlSection";
import { PatchPiezo } from "../../controllers/BoardController";
import { ChangeEvent, Ref, useEffect, useState } from "react";
import { usePiezo } from "../../hooks";
import { DynamicPinState } from "../../types/board";
import EvoInput from "../forms/EvoInput";
import Button from "../forms/Button";
function PiezoItem ({ piezo, index }: { piezo: DynamicPinState, index: number }) {
const { setFrequency, setPiezoPin, removePiezo } = usePiezo();
const [freq, setFreq] = useState<number | string>(piezo.state);
const handlePinChange = (e: ChangeEvent<HTMLInputElement | null>) => {
const pin = e.target?.value;
setPiezoPin(index, pin);
}
const handleFreqChange = (e: ChangeEvent<HTMLInputElement | null>) => {
setFreq(e.target.value);
}
const handleDelete = () => {
removePiezo(index);
}
const handlePatch = () => {
const pin: number = typeof piezo.pin == "string" ? Number.parseInt(piezo.pin) : piezo.pin;
PatchPiezo(pin, piezo.state);
}
useEffect(() => {
const f: number = typeof freq == "string" ? Number.parseInt(freq) : freq;
if (!Number.isNaN(freq)) setFrequency(index, f);
}, [freq])
return (
<div className="flex flex-col gap-3">
<PinBox
className="w-36 animate-fade-in"
minusBtn={true}
value={piezo.pin}
onValueChange={handlePinChange}
onDelete={handleDelete}
/>
<EvoInput
className="w-36 h-11"
name="Frequency"
type="number"
value={piezo.state.toString()}
onChange={handleFreqChange}
/>
<Button.Primary onClick={handlePatch} className="text-sm !py-0 h-11">Play</Button.Primary>
</div>
)
}
function ControlPiezo ({ refto }: { refto?: Ref<HTMLDivElement> }) {
const { piezo: piezos, addPiezo } = usePiezo();
const handleAdd = (): void => {
let anopin = 13;
for (let i = 0; i < piezos.length; i++) {
if (piezos.filter(piezo => piezo.pin == anopin).length > 0) {
anopin--;
}
else break;
}
addPiezo(anopin, 247);
}
return (
<ControlSection
title="Piezo Buzzer"
id="piezo"
description={<>
Piezo buzzer is a type of transducer that converts electrical signals into sound. It uses piezoelectric elements to generate mechanical vibrations that then produce sound. Piezo buzzer can produce sound with a given frequency depending on its characteristics
</>}
refto={refto}
stack={(<>
{piezos.map((piezo, i) => (
<PiezoItem
index={i}
piezo={piezo}
key={i}
/>
))}
{piezos.length < 14 && <PinBox.Add
className="w-36 mt-10"
onClick={handleAdd}
/>}
</>)}
/>
)
}
export default ControlPiezo;

View File

@ -1,332 +0,0 @@
import { ChangeEvent, useState } from "react";
import { usePiezoMusic } from "../../hooks";
import { PiezoMusic } from "../../types/board";
import EvoInput from "../forms/EvoInput";
import Input from "../forms/Input";
import EvoDropDown from "../forms/EvoDropDown";
import Button from "../forms/Button";
import { pitch } from "../../data/melodies";
import { PatchPiezoMusic } from "../../controllers/BoardController";
import SearchItemModal from "../modals/SearchItemModal";
function getNoteItems () {
const pitches = Object.keys(pitch);
const notes = pitches.map(n => {
const note = n.replace("S", "#");
return {name: note, value: note}
})
const additional = [
{
name: ". (Jump note)",
value: "."
},
{
name: "- (Hold note)",
value: "-"
}
]
return [...additional, ...notes];
}
function NoteItem ({note, index, parentIndex}: { note: string, index: number, parentIndex: number }) {
const { changeNote, removeNote } = usePiezoMusic();
const [dropAppear, setApper] = useState<boolean>(false);
const noteItems = getNoteItems();
const toggleDropdown = () => { setApper(!dropAppear) };
const handleChange = (item: { name: string, value: any }) => {
toggleDropdown();
changeNote(parentIndex, index, item.value);
}
const handleRemove = () => {
removeNote(parentIndex, index);
}
return (
<>
<button
className="min-w-32 h-32 transition border group border-border rounded-lg animate-size-fade-in relative"
>
<div className="absolute right-2 top-2 flex gap-1">
<button
className="border w-8 h-8 border-border transition hover:bg-finn rounded-md opacity-0 group-hover:opacity-100 flex items-center justify-center"
onClick={toggleDropdown}
>
<i className="bi bi-pencil text-xs"></i>
</button>
<button
className="border w-8 h-8 border-border transition hover:bg-finn rounded-md opacity-0 group-hover:opacity-100 flex items-center justify-center"
onClick={handleRemove}
>
<i className="bi bi-dash"></i>
</button>
</div>
<div className="text-xl">
{note}
</div>
{/* <EvoDropDown.Menu
className="!w-44 top-0 h-40 z-30 overflow-y-scroll v-scrollbar left-2"
appear={dropAppear}
items={noteItems}
onValueChange={handleChange}
/> */}
</button>
{dropAppear == true && <SearchItemModal
name="Edit Piezo Note"
items={noteItems}
initItem={{ name: note.replace("S", "#"), value: note.replace("S", "#") }}
onValueChange={handleChange}
onClose={toggleDropdown}
/>}
</>
)
}
function NotePlus ({ parentIndex }: { parentIndex: number }) {
const { addNote } = usePiezoMusic();
const [dropAppear, setApper] = useState<boolean>(false);
const noteItems = getNoteItems();
const toggleDropdown = () => { setApper(!dropAppear) };
const handleAddNote = (item: { name: string, value: any }) => {
addNote(parentIndex, item.value);
setApper(false);
}
return (
<>
<button className="border border-border bg-secondary-solid rounded-lg min-w-32 h-32" onClick={toggleDropdown}>
<i className="bi bi-plus text-2xl"></i>
{/* <EvoDropDown.Menu
className="!w-32 top-0 h-56 z-30 overflow-y-scroll v-scrollbar"
appear={dropAppear}
items={noteItems}
onValueChange={handleAddNote}
/> */}
</button>
{dropAppear == true && <SearchItemModal
name="Add Piezo Note"
items={noteItems}
onValueChange={handleAddNote}
onClose={toggleDropdown}
/>}
</>
)
}
function PiezoEditor ({ piezo, index }: { piezo: PiezoMusic, index: number }) {
const { setName, setPin, setBeat, setTempo, removePiezo } = usePiezoMusic();
const beatsItem = [
{
name: "1/2",
value: 1/2
},
{
name: "1/4",
value: 1/4
},
{
name: "1/8",
value: 1/8
},
{
name: "1/16",
value: 1/16
}
];
const initBeatItem = beatsItem.find((item) => item.value == piezo.beats);
const exportJson = () => {
const filename = "piezo-music_" + piezo.name
.toLocaleLowerCase()
.split("")
.map(char => char == " " ? "-" : char)
.join("") + ".json"
const json = JSON.stringify(piezo, null, 4);
const blob = new Blob([json], {
type: "application/json; charset=utf-8"
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute("href", url);
link.setAttribute("download", filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
const handle = {
changeName: (e: ChangeEvent<HTMLInputElement | null>) => {
setName(index, e.target.value);
},
pinChange: (e: ChangeEvent<HTMLInputElement | null>) => {
setPin(index, e.target.value);
},
beatChange: (item: { name: string, value: any }) => {
setBeat(index, item.value);
},
tempoChange: (e: ChangeEvent<HTMLInputElement | null>) => {
const tempo = Number.parseInt(e.target.value);
setTempo(index, tempo);
},
remove: () => {
removePiezo(index);
},
play: () => {
PatchPiezoMusic(piezo);
},
export: exportJson
}
return (
<div className="flex flex-col gap-5 bg-secondary border border-border rounded-lg p-5 animate-size-fade-in relative">
<button className="ms-auto absolute -right-24 top-1/2 -translate-y-1/2 bg-finn hover:bg-secondary transition border border-border rounded-lg px-5" onClick={handle.remove}>
<i className="bi bi-dash text-3xl"></i>
</button>
<div className="flex flex-row justify-between">
<div className="flex flex-row gap-3">
<Input
className="w-64"
placeholder="Enter name"
value={piezo.name}
onChange={handle.changeName}
/>
<EvoInput
className="w-36"
name="Pin"
type="number"
value={piezo.pin.toString()}
onChange={handle.pinChange}
/>
<EvoDropDown
className="w-36"
name="Beats"
items={beatsItem}
initItem={initBeatItem}
/>
<EvoInput
className="w-36"
name="Tempo"
type="number"
value={piezo.tempo.toString()}
onChange={handle.tempoChange}
/>
</div>
<div className="flex flex-row gap-3">
<Button.Secondary className="!py-0 !px-6 flex items-center gap-3" onClick={exportJson}>
<div className="text-sm">Export</div>
<i className="bi bi-upload text-sm"></i>
</Button.Secondary>
<Button.Primary className="!py-1 !px-8" onClick={handle.play}>
<i className="bi bi-play-fill"></i>
</Button.Primary>
</div>
</div>
<div className="relative">
<div className="v-scrollbar pb-4 flex flex-row flex-nowrap gap-4 overflow-x-scroll overflow-y-hidden relative">
{piezo.notes.map((note, i) => (
<NoteItem
note={note}
index={i}
parentIndex={index}
key={i}
/>
))}
<NotePlus parentIndex={index} />
</div>
</div>
</div>
)
}
function PiezoBarPlus () {
const { addPiezo } = usePiezoMusic();
const handleAdd = (): void => {
addPiezo({
name: "",
pin: 11,
notes: [],
beats: 1/4,
tempo: 100
});
}
const handleImport = (e: ChangeEvent<HTMLInputElement | null>): void => {
const file: File | null = e.target.files ? e.target.files[0] : null;
if (!file) return;
const reader = new FileReader();
reader.onload = (event: ProgressEvent<FileReader>) => {
const result = event.target ? event.target.result : null;
if (result && typeof result == "string") {
const data: PiezoMusic = JSON.parse(result);
const isValidFormat: boolean = data.name != null && data.pin != null && data.notes != null && data.beats != null && data.tempo != null;
if (isValidFormat) addPiezo(data);
}
}
reader.readAsText(file);
}
return (
<div className="flex flex-row gap-5">
<button
className="bg-finn h-36 flex-1 hover:bg-secondary transition col-span-2 rounded-lg border text-border border-border hover:border-slate-400 hover:text-slate-400 flex items-center justify-center"
onClick={handleAdd}
>
<i className="bi bi-plus text-5xl"></i>
<div className="font-roboto-mono text-lg">Add New</div>
</button>
<label
htmlFor="file"
className="bg-finn h-36 flex-1 hover:bg-secondary transition col-span-2 rounded-lg border text-border border-border hover:border-slate-400 hover:text-slate-400 flex items-center justify-center cursor-pointer"
>
<input type="file" id="file" className="hidden" onChange={handleImport} />
<i className="bi bi-download text-3xl me-3"></i>
<div className="font-roboto-mono text-lg">Import</div>
</label>
</div>
)
}
function ControlPiezoMusicEditor () {
const { piezeNotes } = usePiezoMusic();
return (
<div className="container grid items-center relative">
<div className={`col-span-8 w-[inherit]`}>
<h2 className="text-4xl font-poppins font-bold leading-normal mb-4">
Piezo Music Editor
</h2>
<div className="grid grid-cols-8 mb-8">
<p className="col-span-6">Lorem, ipsum dolor sit amet consectetur adipisicing elit. Omnis rem, perspiciatis voluptatibus dolor officiis voluptas asperiores reiciendis quisquam qui numquam quas illum velit in id, est expedita ipsa voluptatum eligendi?</p>
</div>
<div className={`flex flex-col gap-6`}>
{piezeNotes.map((piezo, i) => (
<PiezoEditor
piezo={piezo}
index={i}
key={i}
/>
))}
<PiezoBarPlus />
</div>
</div>
</div>
)
}
export default ControlPiezoMusicEditor;

View File

@ -1,190 +0,0 @@
import { PatchRgbLed } from "../../controllers/BoardController";
import { useRgbLed } from "../../hooks";
import { ChannelPinState, PinState } from "../../types/board";
import EvoInput from "../forms/EvoInput";
import Switch from "../forms/Switch";
import { MouseEventHandler, Ref, useEffect, useState } from "react";
interface HorizontalBarProps {
rgbLed: ChannelPinState;
index: number;
}
function HorizontalBar ({ rgbLed, index }: HorizontalBarProps) {
const { removeLed, setLed } = useRgbLed();
const handleUpdate = (led: ChannelPinState) => {
setLed(index, led);
}
const handleRemove = () => {
removeLed(index);
}
const [red, setRed] = useState<PinState>({
pin: rgbLed.red.pin.toString(),
state: rgbLed.red.state
});
const [green, setGreen] = useState<PinState>({
pin: rgbLed.green.pin.toString(),
state: rgbLed.green.state
});
const [blue, setBlue] = useState<PinState>({
pin: rgbLed.blue.pin.toString(),
state: rgbLed.blue.state
});
useEffect(() => {
handleUpdate({red, green, blue});
}, [red, green, blue]);
useEffect(() => {
PatchRgbLed(rgbLed);
}, [
rgbLed.red.state,
rgbLed.green.state,
rgbLed.blue.state
]);
return (
<div className="flex flex-col col-span-3 gap-2 animate-fade-in font-roboto-mono">
<button className="ms-auto bg-finn hover:bg-secondary transition border border-border rounded-lg px-5" onClick={handleRemove}>
<i className="bi bi-dash text-xl"></i>
</button>
<div className="bg-secondary animate-size-in rounded-lg border border-border p-4 flex gap-3">
<div className="flex-1 text-center flex flex-col gap-3">
<div className="text-[#FF4444]">Red</div>
<EvoInput
name="Pin"
value={red.pin.toString()}
type="number"
onChange={(e) => {
setRed({
pin: e.target.value,
state: red.state
});
}}
/>
<Switch
state={red.state}
onChange={() => {
setRed({
pin: red.pin,
state: !red.state
})
}}
/>
</div>
<div className="flex-1 text-center flex flex-col gap-3">
<div className="text-[#48FF44]">Green</div>
<EvoInput
name="Pin"
value={green.pin.toString()}
type="number"
onChange={(e) => {
setGreen({
pin: e.target.value,
state: green.state
});
}}
/>
<Switch
state={green.state}
onChange={() => {
setGreen({
pin: green.pin,
state: !green.state
})
}}
/>
</div>
<div className="flex-1 text-center flex flex-col gap-3">
<div className="text-[#4844FF]">Blue</div>
<EvoInput
name="Pin"
value={blue.pin.toString()}
type="number"
onChange={(e) => {
setBlue({
pin: e.target.value,
state: blue.state
});
}}
/>
<Switch
state={blue.state}
onChange={() => {
setBlue({
pin: blue.pin,
state: !blue.state
})
}}
/>
</div>
</div>
</div>
)
}
function HorizontalBarPlus ({ onClick }: { onClick?: MouseEventHandler<HTMLButtonElement> }) {
return (
<button
className="bg-finn h-40 hover:bg-secondary transition col-span-3 rounded-lg border border-border flex items-center justify-center mt-10"
onClick={onClick}
>
<i className="bi bi-plus text-6xl text-border"></i>
</button>
)
}
function ControlRgbLed ({ refto }: { refto?: Ref<HTMLDivElement> }) {
const { addLed, rgbLed } = useRgbLed();
const handleAdd = (): void => {
addLed({
red: {
pin: 13,
state: false
},
green: {
pin: 12,
state: false
},
blue: {
pin: 11,
state: false
}
})
}
return (
<div className="container py-16" id="rgb-led" ref={refto}>
<div className="container-grid items-center relative">
<div className={`col-span-8`}>
<h2 className="text-4xl font-poppins font-bold leading-normal mb-4">
RGB LED
</h2>
<div className="grid grid-cols-8 mb-8">
<p className="col-span-6">RGB LED is a type of Light Emitting Diode that has the ability to produce light in three basic colors, red {"("}R{")"}, green {"("}G{")"}, and blue {"("}B{")"}. By controlling the intensity of these three colors separately, RGB LEDs allow the creation of a variety of different colors</p>
</div>
<div className={`grid grid-cols-9 gap-6`}>
{rgbLed.map((led, i) => (
<HorizontalBar
key={i}
index={i}
rgbLed={led}
/>
))}
{rgbLed.length < 5 && <HorizontalBarPlus
onClick={handleAdd}
/>}
</div>
</div>
</div>
</div>
)
}
export default ControlRgbLed;

View File

@ -1,127 +0,0 @@
import { ChangeEvent, MouseEventHandler, Ref, useEffect, useState } from "react";
import { useServo } from "../../hooks";
import { DynamicPinState } from "../../types/board";
import EvoInput from "../forms/EvoInput";
import Button from "../forms/Button";
import { Slider } from "../forms/Slider";
import { PatchServo } from "../../controllers/BoardController";
function Bar ({ index, servo }: { index: number, servo: DynamicPinState }) {
const { setServoPin, setDegree, removeServo } = useServo();
const [percentage, setPercent] = useState(0);
const [isListen, setListen] = useState(false);
useEffect(() => {
const deg = Math.floor((percentage / 1000) * 180);
setDegree(servo.pin, deg);
}, [percentage]);
useEffect(() => {
if (isListen) PatchServo(servo.pin, servo.state);
}, [isListen, servo.state])
const toggleListen = () => {
setListen(!isListen);
}
const handleChange = (e: ChangeEvent<HTMLInputElement | null>) => {
const percent = Number.parseInt(e.target.value);
if (!Number.isNaN(percent)) setPercent(percent);
}
const handleChangePin = (e: ChangeEvent<HTMLInputElement | null>) => {
setServoPin(index, e.target.value);
}
const handleDelete = () => {
removeServo(index);
}
return (
<div className="border border-border bg-secondary rounded-lg p-6 animate-size-fade-in relative">
<button className="ms-auto absolute -right-24 top-1/2 -translate-y-1/2 bg-finn hover:bg-secondary transition border border-border rounded-lg px-5" onClick={handleDelete}>
<i className="bi bi-dash text-3xl"></i>
</button>
<div className="flex justify-between mb-8">
<div className="flex gap-3">
<EvoInput
name="Pin"
value={servo.pin.toString()}
onChange={handleChangePin}
type="text"
/>
{!isListen ? (
<Button.Primary onClick={toggleListen} className="text-sm !py-0 h-11">Start</Button.Primary>
) : (
<Button.Danger onClick={toggleListen} className="text-sm !py-0 h-11">Stop</Button.Danger>
)}
</div>
<div className="">{servo.state+"°"}</div>
</div>
<div className="">
<Slider
name={`Servo ${servo.pin}`}
value={percentage}
onChange={handleChange}
/>
</div>
</div>
)
}
function BarPlus ({ onClick }: { onClick?: MouseEventHandler<HTMLButtonElement> }) {
return (
<button
className="bg-finn h-36 hover:bg-secondary transition col-span-2 rounded-lg border border-border flex items-center justify-center"
onClick={onClick}
>
<i className="bi bi-plus text-6xl text-border"></i>
</button>
)
}
function ControlServo ({ refto }: { refto?: Ref<HTMLDivElement> }) {
const { motoServo, addServo } = useServo();
const handleAdd = (): void => {
let anopin = 5;
for (let i = 0; i < motoServo.length; i++) {
if (motoServo.filter(servo => (servo.pin == `A${anopin}` || servo.pin == anopin)).length > 0) {
anopin--;
}
else break;
}
addServo(`A${anopin}`, 0);
}
return (
<div className="container py-16" id="rgb-led" ref={refto}>
<div className="container-grid items-center relative">
<div className={`col-span-8`}>
<h2 className="text-4xl font-poppins font-bold leading-normal mb-4">
Servo
</h2>
<div className="grid grid-cols-8 mb-8">
<p className="col-span-6">Servo motors is a type of electric motor designed to provide precise motion and accurate control. Servo motors use a feedback mechanism to control their shaft position and rotational speed.</p>
</div>
<div className={`flex flex-col gap-6`}>
{motoServo.map((servo, i) => (
<Bar
key={i}
servo={servo}
index={i}
/>
))}
{motoServo.length < 6 && <BarPlus
onClick={handleAdd}
/>}
</div>
</div>
</div>
</div>
)
}
export default ControlServo;

View File

@ -1,57 +0,0 @@
import Button from "../forms/Button";
import { RefObject } from "react";
import MadeBy from "../info/MadeBy";
type ShortNav = {
name: string,
target: string
}
interface InHero {
img: string;
shortNav: ShortNav[];
refs?: RefObject<HTMLElement>[]
}
function Hero ({ img, shortNav, refs }: InHero) {
return (
<div className="h-screen container flex items-center">
<div className="container-grid items-center relative">
<div className="col-span-4">
<h2 className="font-poppins text-6xl font-bold leading-normal">
Lunar Vein: Arduino Client
</h2>
<p className="font-poppins text-xl">
Open-source API Based serial communication, it helps you testing the arduino board and some components without any code.
<small className="text-xs"> i don't even know anything about electronics and some IoT stuff lol</small>
</p>
<div className="flex flex-wrap gap-3 mt-5">
{shortNav.map(({ name }, i) => (
<Button
key={i}
onClick={() => {
if (refs && refs.length >= i)
refs[i].current?.scrollIntoView({ behavior: "smooth" });
}}
>{name}</Button>
))}
</div>
<MadeBy className="absolute left-0 -bottom-24" />
</div>
<div className="col-span-1"></div>
<div className="col-span-3">
<picture title="Lunar">
<img
className="w-full h-auto"
src={img}
alt="Lunar"
srcSet={img}
/>
</picture>
</div>
</div>
</div>
)
}
export default Hero;

View File

@ -1,73 +0,0 @@
import { ChangeEvent, useEffect, useState } from "react";
import Input from "../forms/Input";
type EvoDropDownItem = {
name: string,
value: any
}
interface SearchItemModalProps {
className?: string;
name?: string;
items: EvoDropDownItem[];
initItem?: EvoDropDownItem;
onValueChange?: (item: EvoDropDownItem) => void;
onClose?: () => void
}
export default function SearchItemModal ({ className, items, name, initItem, onClose, onValueChange }: SearchItemModalProps) {
const [filteredItems, setItems] = useState<EvoDropDownItem[]>(items);
const [currentItem, setItem] = useState<EvoDropDownItem>(initItem || items[0]);
const [searchVal, setSearch] = useState<string>("");
const handleSearch = (e: ChangeEvent<HTMLInputElement | null>) => {
setSearch(e.target.value);
}
useEffect(() => {
if (searchVal.length > 0) {
const filter = items.filter(item => item.name.toLocaleLowerCase().includes(searchVal.toLowerCase()));
setItems(filter);
}
else {
setItems(items);
}
}, [searchVal])
return (
<div className={`fixed left-0 top-0 bg-finn w-screen h-screen z-50 ${className}`}>
<div className="bg-black border border-border rounded-lg absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 h-96 w-96 p-3 flex flex-col justify-items-start">
<button className="p-1 absolute right-2 top-2" onClick={onClose}>
<i className="bi bi-x text-3xl"></i>
</button>
<header className="text-left">
<h1 className="text-2xl font-bold font-poppins px-2 py-4">{name}</h1>
<Input
className="w-full mb-3"
name="Search"
placeholder="Search..."
value={searchVal}
onChange={handleSearch}
/>
</header>
<div className="overflow-y-scroll flex-grow flex flex-col">
{filteredItems.map((item) => (
<button
className="py-2 px-3 flex justify-between items-center bg-transparent bg-opacity-100 hover:bg-indigo-300 hover:bg-opacity-20 rounded-md"
onClick={() => {
setItem(item);
if (onValueChange) onValueChange(item)}
}
>
<div className="text-left text-sm">
{item.name}
</div>
{currentItem.value == item.value && <div className="rounded-full bg-white w-2 h-2"></div>}
</button>
))}
</div>
</div>
</div>
)
}

View File

@ -1,40 +0,0 @@
interface CircleResistanceProps {
className?: string;
width?: number;
height?: number;
blur?: number;
intensity?: number;
}
export default function CircleResistance({
className,
width,
height,
blur,
intensity,
}: CircleResistanceProps) {
return (
<div
className={className}
style={{
borderRadius: 999,
width: width || 122,
height: height || 122,
backgroundColor: "rgba(255, 255, 255, 0.20)",
}}
>
<div
className="transition-opacity duration-75"
style={{
backgroundColor: "white",
borderRadius: 999,
width: width || 122,
height: height || 122,
filter: `blur(${blur || 12}px)`,
boxShadow: '0 0 52px 22px #FF2929',
opacity: intensity+"%"
}}
></div>
</div>
);
}

View File

@ -1,97 +0,0 @@
import { Dispatch, ReactNode, SetStateAction, createContext, useState } from "react";
import { ChannelPinState, DynamicPinState, PiezoMusic, PinMode, PinState } from "../types/board";
import ExampleMusic1 from "../data/music/ode-to-joy.json";
import ExampleMusic2 from "../data/music/fallen-down.json";
interface ControllerContextProps {
pinModes: PinMode[];
leds: PinState[];
rgbLed: ChannelPinState[];
piezo: DynamicPinState[];
piezeNotes: PiezoMusic[];
motoServo: DynamicPinState[];
photoresistor: DynamicPinState[];
setPinModes?: Dispatch<SetStateAction<PinMode[]>>;
setLeds?: Dispatch<SetStateAction<PinState[]>>;
setRgbLed?: Dispatch<SetStateAction<ChannelPinState[]>>;
setPiezo?: Dispatch<SetStateAction<DynamicPinState[]>>;
setNotes?: Dispatch<SetStateAction<PiezoMusic[]>>;
setMotoServo?: Dispatch<SetStateAction<DynamicPinState[]>>;
setPhotoresistor?: Dispatch<SetStateAction<DynamicPinState[]>>;
}
const INIT_VALUES: ControllerContextProps = {
pinModes: [],
leds: [
{
pin: 13,
state: false
}
],
rgbLed: [
{
red: {
pin: 13,
state: false
},
green: {
pin: 12,
state: false
},
blue: {
pin: 11,
state: false
}
}
],
piezo: [
{
pin: 6,
state: 262
}
],
piezeNotes: [
ExampleMusic1,
ExampleMusic2
],
motoServo: [
{
pin: "9",
state: 0
}
],
photoresistor: [
{
pin: "A0",
state: 0
}
],
}
export const BoardControllerContext = createContext<ControllerContextProps>(INIT_VALUES);
export function BoardControllerProvider ({ children }: { children: ReactNode }) {
const [pinModes, setPinModes] = useState<PinMode[]>(INIT_VALUES.pinModes);
const [leds, setLeds] = useState<PinState[]>(INIT_VALUES.leds);
const [rgbLed, setRgbLed] = useState<ChannelPinState[]>(INIT_VALUES.rgbLed);
const [piezo, setPiezo] = useState<DynamicPinState[]>(INIT_VALUES.piezo);
const [piezeNotes, setNotes] = useState<PiezoMusic[]>(INIT_VALUES.piezeNotes);
const [motoServo, setMotoServo] = useState<DynamicPinState[]>(INIT_VALUES.motoServo);
const [photoresistor, setPhotoresistor] = useState<DynamicPinState[]>(INIT_VALUES.photoresistor);
const contextValue: ControllerContextProps = {
pinModes, setPinModes,
leds, setLeds,
rgbLed, setRgbLed,
piezo, setPiezo,
piezeNotes, setNotes,
motoServo, setMotoServo,
photoresistor, setPhotoresistor
};
return <BoardControllerContext.Provider value={contextValue}>
{children}
</BoardControllerContext.Provider>
}

View File

@ -0,0 +1,50 @@
import { Request, Response } from "express";
import { suBoard } from "../../setup";
import * as Promises from "../../promises";
interface AnalogState {
pin: string,
value: number
}
export function analogWrite (req: Request, res: Response): Response<string | any> {
const { board } = suBoard;
const { pin, value }: AnalogState = req.body;
try {
board.analogWrite(pin[0] == "A" ? pin.slice(1, pin.length) : pin, value);
return res.status(200).json({
status: 200,
analog_state: {
[pin]: value
}
});
}
catch (err) {
console.error(err);
res.sendStatus(500);
}
}
export async function analogRead (req: Request, res: Response) {
const { board } = suBoard;
let { pin } = req.params;
pin = pin[0] == "A" ? pin.slice(1, pin.length) : pin;
try {
const value = await Promises.analogRead(board, pin);
return res.status(200).json({
status: 200,
analog_state: {
[pin]: value
}
})
}
catch (err) {
console.error(err);
res.sendStatus(500);
}
}

View File

@ -0,0 +1,56 @@
import { Request, Response } from "express";
import { suBoard } from "../../setup";
interface DigitalState {
pin: number | string,
state: string
}
export function digitalWrite (req: Request, res: Response): Response<string | any> {
const { board } = suBoard;
const { pin, state }: DigitalState = req.body;
let value: number;
try {
switch (state) {
case 'LOW' || 0: value = 0; break;
case 'HIGH' || 1: value = 1; break;
default:
return res.status(400).json({
status: 400,
message: `Invalid state ${state}, read the documentation`
})
}
const val = value == 1 ? "HIGH" : "LOW";
board.digitalWrite(pin, value);
return res.status(200).json({
status: 200,
pin_state: {
[pin]: val
}
});
}
catch (err) {
console.error(err);
res.sendStatus(500);
}
}
export function digitalRead (req: Request, res: Response): Response<string | any> {
const { board } = suBoard;
try {
const { pin } = req.params;
const value = board.pins[pin].value == 1 ? 'HIGH' : 'LOW';
return res.status(200).json({
pin_state: {
[pin]: value
}
});
}
catch (err) {
console.error(err);
res.sendStatus(500);
}
}

View File

@ -0,0 +1,87 @@
import { Request, Response } from "express";
import { suBoard } from "../../setup";
import { sPinModes } from "..";
import { Pin } from "johnny-five";
export function readPin (req: Request, res: Response): Response<string | any> {
const { board } = suBoard;
const pin: string = req.params.p;
const { mode } = board.pins[pin];
return res.status(200).json({
status: 200,
pins: [
{
pin: pin,
mode: mode
}
],
message: `Pin ${pin} is ${mode}`
});
}
export function readPins (req: Request, res: Response): Response<string | any> {
const { board } = suBoard;
const pins: string[] = req.body.p;
const pinModes = [];
pins.forEach((pin) => {
const { mode } = board.pins[pin];
pinModes.push({
pin: pin,
mode: mode
});
})
return res.status(200).json({
status: 200,
pins: [ ...pinModes ]
});
}
export function setPin (req: Request, res: Response): Response<string | any> {
const { board } = suBoard;
const pin: string = req.params.p;
const mode: sPinModes | string = req.params.m.toUpperCase();
board.pinMode(pin, Pin[mode]);
suBoard.PINS.digital.push(pin);
suBoard.sort();
return res.status(200).json({
status: 200,
pins: [
{
pin: pin,
mode: mode
}
],
message: `Pin ${pin} setted as ${mode}`
});
}
export function setPins (req: Request, res: Response): Response<string | any> {
const { board } = suBoard;
type PinModes = {
pin: string,
mode: sPinModes | string
}
const { pinModes }: { pinModes: PinModes[] } = req.body;
pinModes.forEach((p) => {
const { pin, mode } = p;
board.pinMode(pin, Pin[mode]);
suBoard.PINS.digital.push(pin);
suBoard.sort();
});
return res.status(200).json({
status: 200,
pins: [ ...pinModes ]
});
}

View File

@ -0,0 +1,185 @@
import { Request, Response } from "express";
import { suBoard } from "../../setup";
import { ChannelPins, voltage } from "..";
import { Led } from "johnny-five";
export function readLed (req: Request, res: Response): Response<string | any> {
const { board } = suBoard;
try {
const { p } = req.params;
const pin: number = Number.parseInt(p);
const pinState: voltage = board.pins[pin].value == 1 ? 'HIGH' : 'LOW';
return res.status(200).json({
status: 200,
pin_state: {
[pin]: pinState
},
message: `Led pin ${pin} is ${pinState}`
});
}
catch (err) {
console.error(err);
return res.status(500).json({
status: 200,
message: "Internal server error"
})
}
}
export function writeLed (req: Request, res: Response): Response<string | any> {
const { board } = suBoard;
const { p, a } = req.params;
const act: string = a.toLocaleLowerCase();
const pin: number = Number.parseInt(p);
let state: voltage;
let volt: voltage;
try {
switch (act) {
case 'on' || 'high':
state = 'HIGH';
volt = 1;
break;
case 'off' || 'low':
state = 'LOW';
volt = 0;
break;
default:
console.log(`${req.hostname} | ${pin} | LED: INVALID ACT`);
return res.status(400).json({
status: 400,
message: `Invalid act ${act}`
});
}
board.digitalWrite(pin, volt);
console.log(`${req.hostname} | ${pin} | LED: ${state}`);
res.status(200).json({
status: 200,
pin_state: {
[pin]: state
},
message: `Changed pin state ${pin} to ${state}`
});
}
catch (err) {
console.error(err);
res.status(500).json({
status: 500,
message: "Internal Server Error"
});
}
};
export function readRgbLed (req: Request, res: Response): Response<string | any> {
const r: number = Number.parseInt(req.body.r);
const g: number = Number.parseInt(req.body.g);
const b: number = Number.parseInt(req.body.b);
try {
const rgbPins: number[] = Object.values({ r, g, b });
for (let i = 0; i < rgbPins.length; i++) {
const pin = rgbPins[i];
if (Number.isNaN(pin)) {
return res.status(400).json({
status: 400,
message: `Invalid pin ${pin} param, it should be integer`
});
}
}
const led = new Led.RGB({
pins: {
red: r,
green: g,
blue: b
},
isAnode: true
})
led.red = new Led(r);
led.green = new Led(g);
led.blue = new Led(b);
return res.status(200).json({
status: 200,
pin_state: {
[r]: led.red.isOn,
[g]: led.green.isOn,
[b]: led.blue.isOn
}
});
}
catch (err) {
console.error(err);
return res.status(500).json({
status: 200,
message: "Internal server error"
})
}
}
export function writeRgbLed (req: Request, res: Response): Response<string | any> {
const r: ChannelPins = req.body.r;
const g: ChannelPins = req.body.g;
const b: ChannelPins = req.body.b;
const rgbLeds: ChannelPins[] = Object.values({ r, g, b });
try {
rgbLeds.forEach(led => {
if (Number.isNaN(led.pin)) {
return res.status(400).json({
status: 400,
message: `Invalid pin ${led.pin} param, it should be integer`
});
}
})
const led = new Led.RGB({
pins: {
red: r.pin,
green: g.pin,
blue: b.pin
},
isAnode: true
})
const isHigh: boolean | string = true || 'HIGH' || 'high';
led.red = new Led(r.pin);
led.green = new Led(g.pin);
led.blue = new Led(b.pin);
if (r.value == isHigh) led.red.on(); else led.red.off();
if (g.value == isHigh) led.green.on(); else led.green.off();
if (b.value == isHigh) led.blue.on(); else led.blue.off();
const pins: string = rgbLeds.map(c => c.pin.toString()).join(", ");
const values: string = rgbLeds.map(c => `${c.value}`).join(", ");
return res.status(200).json({
status: 200,
pin_state: {
[r.pin]: led.red.isOn,
[g.pin]: led.green.isOn,
[b.pin]: led.blue.isOn
},
message: `Success changed pins ${pins} to state ${values}`
});
}
catch (err) {
console.error(err);
return res.status(500).json({
status: 500,
message: "Internal Server Error"
});
}
};

View File

@ -0,0 +1,17 @@
import { Request, Response } from "express";
import { suBoard } from "../../setup";
import { analogRead } from "../../promises";
export async function readResistor (req: Request, res: Response): Promise<Response<string, any>> {
const { board } = suBoard;
const { p } = req.params;
const pin: string = req.params.p.startsWith("A") ? req.params.p.slice(0, p.length) : req.params.p;
const resistance = await analogRead(board, pin);
return res.status(200).json({
status: 200,
message: `Analog pin ${p} resistance as ${resistance}°`
});
}

View File

@ -0,0 +1,92 @@
import { Request, Response } from "express";
import { Piezo } from "johnny-five";
import { Pitch } from "../../melodies";
export function piezoTone (req: Request, res: Response): Response {
const pin: number = Number.parseInt(req.params.p);
const frequency: number = Number.parseInt(req.params.f);
const piezo: Piezo = new Piezo(pin);
piezo.tone(frequency * 7.3, 100);
return res.status(200).json({
status: 200,
pin_tone: {
[pin]: frequency
},
message: `Piezo ${pin} tone ${frequency}`
});
}
export function piezoNote (req: Request, res: Response): Response {
const { pin, note }: { pin: number, note: string } = req.body;
const notePitch = Pitch[note.toUpperCase()];
if (notePitch == null) {
return res.status(400).json({
status: 400,
message: `Invalid note ${note}`
});
}
const piezo: Piezo = new Piezo(pin);
piezo.play({
song: note.toUpperCase(),
beats: 1/2,
tempo: 100
})
return res.status(200).json({
status: 200,
pin_tone: {
[pin]: notePitch
},
pin_note: {
[pin]: note.toUpperCase()
},
message: `Piezo ${pin} tone note ${note}`
});
}
export function piezoNoTone (req: Request, res: Response): Response {
const { pin }: { pin: number } = req.body;
new Piezo(pin).noTone();
return res.sendStatus(200);
}
interface MusicSheet {
notes: string[];
beats: number;
tempo: 100
}
export function piezoPlayNotes (req: Request, res: Response): Response {
const { pin }: { pin: number } = req.body;
const { notes, beats, tempo }: MusicSheet = req.body;
const piezo: Piezo = new Piezo(pin);
const song: [frequency: string, duration: number][] = notes
.map((note, i): [frequency: string, duration: number] => {
const beat = notes[i+1] && notes[i+1].trim() == "-" ? beats*2 : beats;
return [note, beat]
})
.filter((note) => note[0].trim() != "-");
const notesS: string = notes.length > 4 ?
notes.slice(0, 4).join(", ")+"..." :
notes.join(", ");
piezo.play({ song, tempo });
return res.status(200).json({
status: 200,
pin_notes: {
[pin]: notes
},
message: `Piezo ${pin} play notes ${notesS}`
});
}

View File

@ -0,0 +1,23 @@
import { Request, Response } from "express";
import { suBoard } from "../../setup";
export function rotateServo (req: Request, res: Response): Response<string, any> {
const { board } = suBoard;
const { p } = req.params;
const pin: string = req.params.p.startsWith("A") ? req.params.p.slice(0, p.length) : req.params.p;
const angle: number = Number.parseInt(req.params.ang);
if (Number.isNaN(angle)) {
return res.status(400).json({
status: 400,
message: 'Invalid angle param, it should be integer'
});
}
board.servoWrite(pin, angle);
return res.status(200).json({
status: 200,
message: `Pin ${p} rotate as ${angle}°`
});
}

15
src/controller/index.d.ts vendored Normal file
View File

@ -0,0 +1,15 @@
export type digitalValue = 'ON' | 'OFF';
export type voltage = 'HIGH' | 'LOW' | 'high' | 'low' | 1 | 0;
export type sPinModes = 'INPUT' | 'OUTPUT' | 'ANALOG' | 'PWM' | 'SERVO';
export interface ChannelPins {
pin: number,
value: boolean
}
export interface ColorChannel {
r: string,
g: string,
b: string
}

View File

@ -1,63 +0,0 @@
import axios from "axios";
import { ChannelPinState, PiezoMusic } from "../types/board";
import { io } from "../socket/socket.io";
const url = "http://localhost:3000/api-arduino";
export async function PatchLed (pin: number, state: boolean) {
const act = state == true ? 'on' : 'off';
const res = await axios.patch(`${url}/led/${pin}/${act}`);
const data = res.data;
console.log("LED Response: ", data);
}
export async function PatchRgbLed (state: ChannelPinState) {
const { red, green, blue } = state;
const body = {
r: {
pin: red.pin,
value: red.state
},
g: {
pin: green.pin,
value: green.state
},
b: {
pin: blue.pin,
value: blue.state
}
}
const res = await axios.patch(`${url}/rgb-led`, body, {
headers: {
'Content-Type': 'application/json'
}
});
const data = res.data;
console.log("RGB LED Response: ", data);
}
export async function PatchPiezo (pin: number, freq: number) {
const res = await axios.patch(`${url}/piezo/${pin}/${freq}`);
const data = res.data;
console.log("Piezo Response: ", data);
}
export async function PatchPiezoMusic (music: PiezoMusic) {
const res = await axios.patch(`${url}/piezo/music`, music, {
headers: {
'Content-Type': 'application/json'
}
});
const data = res.data;
console.log("Piezo Response: ", data);
}
export async function PatchServo(pin: string | number, value: number) {
io.emit("servo", pin, value);
}

View File

@ -1,123 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
scroll-behavior: smooth;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type=number] {
-moz-appearance: textfield;
}
.v-scrollbar::-webkit-scrollbar {
width: 3px;
}
.v-scrollbar::-webkit-scrollbar-track {
@apply bg-transparent;
}
.v-scrollbar::-webkit-scrollbar-thumb {
@apply bg-primary cursor-pointer rounded-full;
}
}
@keyframes size-in {
from { transform: scale(0); }
to { transform: scale(1); }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes size-fade-in {
from {
transform: scale(0);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
@keyframes wewew {
0% { transform: scale(1, 1); }
12% { transform: scale(1.1, .9); }
25% { transform: scale(.9, 1.1); }
37% { transform: scale(1.1, .9); }
50% { transform: scale(1, 1); }
100% { transform: scale(1, 1); }
}
.container {
@apply w-[1280px] mx-auto;
}
.container-grid {
@apply grid grid-cols-8 gap-6;
}
@layer components {
.slider {
-webkit-appearance: none;
-webkit-transition: .15s;
color: #3a3affcc;
color: #3a3aff4d;
@apply appearance-none rounded-full w-full h-3 bg-primary bg-opacity-30 transition;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
@apply appearance-none transition-all rounded-full w-5 h-5 bg-primary cursor-pointer;
}
.slider::-webkit-slider-thumb:hover {
-webkit-appearance: none;
box-shadow: 0 0 0 8px #3838ff66;
}
.slider::-webkit-slider-thumb:active {
-webkit-appearance: none;
box-shadow: 0 0 0 12px #3838ff66;
@apply w-6 h-6;
}
.slider::-moz-range-thumb {
@apply w-5 h-5 bg-primary cursor-pointer;
}
.btn {
@apply px-5 py-4 transition font-roboto-mono rounded-lg transition-[transform] duration-75 scale-100 active:scale-90;
}
.switch {
@apply rounded-lg transition;
}
.switch > .handler {
@apply rounded-md p-[6px] transition-[margin];
}
.switch-on {
@apply border-primary bg-primary bg-opacity-40;
}
.switch-on > .handler {
@apply bg-primary ms-[50%];
}
.switch-off {
@apply border-disabled bg-transparent;
}
.switch-off > .handler {
@apply bg-disabled ms-0;
}
}

View File

@ -1,26 +0,0 @@
[
{
"name": "LED",
"target": "#led"
},
{
"name": "RGB LED",
"target": "#rgb-led"
},
{
"name": "Piezo",
"target": "#piezo"
},
{
"name": "Moto Servo",
"target": "#moto-servo"
},
{
"name": "Photoresistor",
"target": "#photoresistor"
},
{
"name": "Ultrasonic sensor",
"target": "#ultrasonic-sensor"
}
]

View File

@ -1,12 +0,0 @@
{
"name": "Fallen Down",
"pin": 11,
"notes": [
"F#5", "C#5", "F#5", "C#5", "F#5", "C#5", "F#5", "C#5", "F#5", "C#5", "F#5", "C#5",
"B4", "A4", "C#5", ".", "A4", "B4", "E5", "D#5", "E5", "F#5", "D#5", "B4",
"F#5", "B4", "F#5", "B4", "F#5", "B4", "F#5", "A#4", "F#5", "A#4", "G5", ".",
"F#5", "D5", "F#5", "D5", "E5", "F#5", "E5", "-", "D5", "-", "C#5", "-"
],
"beats": 0.5,
"tempo": 100
}

View File

@ -1,9 +0,0 @@
{
"name": "Ode to joy",
"pin": 11,
"notes": [
"E4", "E4", "F4", "G4", "G4", "F4", "E4", "D4", "C4", "C4", "D4", "E4", "D4", "-", "C4", "C4"
],
"beats": 0.5,
"tempo": 100
}

7
src/dev.ts Normal file
View File

@ -0,0 +1,7 @@
import { Board } from "johnny-five";
import { run } from "./server";
import { defaultBoardOption, suBoard } from "./setup";
suBoard.board = new Board(defaultBoardOption);
run();

View File

@ -0,0 +1,57 @@
import { Socket } from "socket.io";
import { Pin, Sensor } from "johnny-five";
import { suBoard } from "../setup";
const { board } = suBoard;
export default (socket: Socket) => {
console.log(`${socket.id} | ${socket.client.request.headers.host} | Joined`);
socket.on("servo", (p: string, ang: string, cb?: (msg?: string) => void) => {
const pin: number = Number.parseInt(p);
const angle: number = Number.parseInt(ang);
board.pinMode(pin, Pin.SERVO);
board.servoWrite(pin, angle);
if (cb) cb(`Set servo pin ${p} to ${angle} degrees`);
console.log(socket.id, pin, angle);
})
socket.on("set-photoresistor", (pin: string, cb?: (msg?: string) => void) => {
const room = `resistor-${pin}`;
if (!socket.rooms.has(room)) {
socket.join(room);
const sensor = new Sensor({
pin: pin,
board: board,
type: "analog",
freq: 250
});
sensor.on("change", () => {
socket.to(room).emit("photoresistor", sensor.value);
});
console.log(`New room: ${room}`);
console.log(`${socket.id} joined room ${room}`);
if (cb) cb(`Set pin resistor to pin ${pin}`);
}
else {
if (cb) cb(`Resistor pin ${pin} is already used, try to lisen to "photoresistor" room: ${room}`);
}
})
socket.on("join-photoresistor", (pin) => {
const room = `resistor-${pin}`;
if (!socket.rooms.has(room)) {
socket.join(room);
console.log(socket.rooms);
console.log(`${socket.id} Joined room ${room}`);
}
})
}

View File

@ -1,297 +0,0 @@
import { useContext } from "react";
import { BoardControllerContext } from "../contexts/BoardController";
import { ChannelPinState, PiezoMusic } from "../types/board";
// * -----------------------------------
// * I'm never use OOP in react lol
// * -----------------------------------
export function usePin () {
return useContext(BoardControllerContext);
}
export function useLed () {
const { leds, setLeds } = useContext(BoardControllerContext);
const getLed = (pin: number | string) => {
return leds.find(val => val.pin == pin);
}
const addLed = (pin: number | string, state?: boolean) => {
setLeds!(led => [...led, { pin, state: state || false }]);
}
const removeLed = (index: number) => {
const newLed = leds.filter((_led, i) => i != index);
setLeds!(newLed);
}
const setLed = (pin: number | string, state: boolean) => {
const newLed = leds.map(led => {
if (led.pin == pin) return { pin, state };
return led;
})
setLeds!(newLed);
}
const setLedPin = (index: number | string, newPin: number | string) => {
const newLed = leds.map((led, i) => {
if (i == index) return { pin: newPin, state: led.state };
return led;
})
setLeds!(newLed);
}
return { leds, getLed, setLed, setLedPin, addLed, removeLed };
}
export function useRgbLed () {
const { rgbLed, setRgbLed } = useContext(BoardControllerContext);
const getLed = (index: number) => {
const led = rgbLed.find((_val, i) => i == index);
return led;
}
const addLed = (channelPin: ChannelPinState) => {
const newLed: ChannelPinState[] = [
...rgbLed,
{
red: channelPin.red,
green: channelPin.green,
blue: channelPin.blue
}
];
setRgbLed!(newLed);
}
const removeLed = (index: number) => {
const newLed: ChannelPinState[] = rgbLed.filter((_led, i) => i != index);
setRgbLed!(newLed);
}
const setLed = (index: number, channelPin: ChannelPinState) => {
const newLed: ChannelPinState[] = rgbLed.map((led, i) => {
if (i == index) return channelPin;
return led;
})
setRgbLed!(newLed);
}
return { rgbLed, getLed, setLed, addLed, removeLed };
}
export function usePiezo () {
const { piezo, setPiezo } = useContext(BoardControllerContext);
const getPiezo = (pin: number | string) => {
return piezo.find(val => val.pin == pin);
}
const addPiezo = (pin: number | string, state: number) => {
const newPiezo = [...piezo, { pin, state }];
setPiezo!(newPiezo);
}
const removePiezo = (index: number) => {
const newPiezo = piezo.filter((_piezo, i) => i != index);
setPiezo!(newPiezo);
}
const setFrequency = (index: number, state: number) => {
setPiezo!(piezos => piezos.map((piezo, i) => {
if (i == index) return { pin: piezo.pin, state: state };
return piezo;
}));
}
const setPiezoPin = (index: number | string, newPin: number | string) => {
const newPiezo = piezo.map((piezo, i) => {
if (i == index) return { pin: newPin, state: piezo.state };
return piezo;
})
setPiezo!(newPiezo);
}
return { piezo, getPiezo, addPiezo, removePiezo, setFrequency, setPiezoPin };
}
export function usePiezoMusic () {
const { piezeNotes, setNotes } = useContext(BoardControllerContext);
const getPiezo = (pin: number | string) => {
return piezeNotes.find(val => val.pin == pin);
}
const addPiezo = (notes: PiezoMusic) => {
const newPiezo = [...piezeNotes, notes];
setNotes!(newPiezo);
}
const setPiezo = (index: number, piezo: PiezoMusic) => {
setNotes!(piezos => piezos.map((_piezo, i) => {
if (i == index) return piezo;
return _piezo;
}));
}
const removePiezo = (index: number) => {
const newPiezo = piezeNotes.filter((_piezo, i) => i != index);
setNotes!(newPiezo);
}
const addNote = (piezoIndex: number, note: string) => {
setNotes!(piezos => piezos.map((piezo, i) => {
if (piezoIndex == i) {
piezo.notes.push(note);
}
return piezo;
}));
}
const changeNote = (piezoIndex: number, noteIndex: number, note: string) => {
setNotes!(piezos => piezos.map((piezo, i) => {
if (piezoIndex == i) {
piezo.notes[noteIndex] = note;
}
return piezo;
}));
}
const removeNote = (piezoIndex: number, noteIndex: number) => {
setNotes!(piezos => piezos.map((piezo, i) => {
if (piezoIndex == i) {
piezo.notes = piezo.notes.filter((_p, i) => i != noteIndex);
}
return piezo;
}));
}
const setPin = (index: number, newPin: string | number) => {
const newPiezo: PiezoMusic[] = piezeNotes.map((piezo, i) => {
if (i == index) {
piezo.pin = newPin;
}
return piezo;
})
setNotes!(newPiezo);
}
const setName = (index: number, name: string) => {
const newPiezo: PiezoMusic[] = piezeNotes.map((piezo, i) => {
if (i == index) {
piezo.name = name;
}
return piezo;
})
setNotes!(newPiezo);
}
const setBeat = (index: number, beats: number) => {
const newPiezo: PiezoMusic[] = piezeNotes.map((piezo, i) => {
if (i == index) {
piezo.beats = beats;
}
return piezo;
})
setNotes!(newPiezo);
}
const setTempo = (index: number, tempo: number) => {
const newPiezo: PiezoMusic[] = piezeNotes.map((piezo, i) => {
if (i == index) {
piezo.tempo = tempo;
}
return piezo;
})
setNotes!(newPiezo);
}
return {
piezeNotes, setNotes, getPiezo, addPiezo, removePiezo, setPiezo,
setPin, setName, setBeat, setTempo,
addNote, changeNote, removeNote
};
}
export function useServo () {
const { motoServo, setMotoServo } = useContext(BoardControllerContext);
const getServo = (pin: number | string) => {
return motoServo.find(val => val.pin == pin);
}
const addServo = (pin: number | string, state: number) => {
const newPesistor = [...motoServo, { pin, state }];
setMotoServo!(newPesistor);
}
const removeServo = (index: number) => {
const newPesistor = motoServo.filter((_servo, i) => i != index);
setMotoServo!(newPesistor);
}
const setDegree = (pin: number | string, state: number) => {
const newPiezo = motoServo.map(servo => {
if (servo.pin == pin) return { pin, state };
return servo;
})
setMotoServo!(newPiezo);
}
const setServoPin = (index: number, newPin: number | string) => {
const newPesistor = motoServo.map((servo, i) => {
if (i == index) return { pin: newPin, state: servo.state };
return servo;
})
setMotoServo!(newPesistor);
}
return { motoServo, getServo, addServo, removeServo, setDegree, setServoPin };
}
export function usePhotoresistor () {
const { photoresistor, setPhotoresistor } = useContext(BoardControllerContext);
const getResistor = (pin: number | string) => {
return photoresistor.find(val => val.pin == pin);
}
const addResistor = (pin: number | string, state: number) => {
const newPesistor = [...photoresistor, { pin, state }];
setPhotoresistor!(newPesistor);
}
const removeResistor = (index: number) => {
const newPesistor = photoresistor.filter((_resist, i) => i != index);
setPhotoresistor!(newPesistor);
}
const setResistance = (pin: number | string, state: number) => {
const resistor = photoresistor.map(resist => {
if (resist.pin == pin) return { pin: resist.pin, state };
return resist;
})
setPhotoresistor!(resistor);
}
const setResistorPin = (index: number, newPin: number | string) => {
const newPesistor = photoresistor.map((resist, i) => {
if (i == index) return { pin: newPin, state: resist.state };
return resist;
})
setPhotoresistor!(newPesistor);
}
return { photoresistor, getResistor, addResistor, removeResistor, setResistance, setResistorPin };
}

3
src/index.ts Normal file
View File

@ -0,0 +1,3 @@
import main from "./app";
main();

View File

@ -1,10 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './css/index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View File

@ -1,4 +1,4 @@
export const pitch = {
export const Pitch = {
B0: 31,
C1: 33,
CS1: 35,

View File

@ -0,0 +1,16 @@
import { NextFunction, Request, Response } from "express";
import { suBoard } from "../setup";
const { board } = suBoard;
export function isBoardConnected (req: Request, res: Response, next: NextFunction) {
if (suBoard.connected) {
next();
}
else {
return res.status(500).json({
status: 500,
message: "Internal server error: No Boards connected"
})
}
}

35
src/middleware/pin.ts Normal file
View File

@ -0,0 +1,35 @@
import { NextFunction, Request, Response } from "express";
import { suBoard } from "../setup";
export function isPinNumeric (req: Request, res: Response, next: NextFunction) {
const pin: number = Number.parseInt(req.params.p);
if (Number.isNaN(pin)) {
return res.status(400).json({
status: 400,
message: 'Invalid pin param, it should be integer'
});
}
next();
}
export function isPinBeingUsed (req: Request, res: Response, next: NextFunction) {
const pin: number = Number.parseInt(req.params.p);
if (Number.isNaN(pin)) {
return res.status(400).json({
status: 400,
message: 'Invalid pin param, it should be integer'
});
}
if (suBoard.PINS.digital.indexOf(pin) != -1) {
return res.status(400).json({
status: 400,
message: `Pin ${pin}, is used`
});
}
next();
}

View File

@ -1,45 +0,0 @@
import { useRef } from "react";
import MainBody from "../components/MainBody";
// Section components
import Hero from "../components/landing/Hero";
import ControlLED from "../components/landing/ControlLED";
import LunarImg from "../assets/img/ocs/lunar-oc.png";
import ControlNav from "../data/control-navigation.json";
import ControlRgbLed from "../components/landing/ControlRgbLed";
import ControlPiezo from "../components/landing/ControlPiezo";
import ControlPhotoresistor from "../components/landing/ControlPhotoresistor";
import ControlServo from "../components/landing/ControlServo";
import Closing from "../components/landing/Closing";
import Footer from "../components/Footer";
import ControlPiezoMusicEditor from "../components/landing/ControlPiezoMusicEditor";
function MainPage () {
const led = useRef<HTMLDivElement | null>(null);
const rgbLed = useRef<HTMLDivElement | null>(null);
const piezo = useRef<HTMLDivElement | null>(null);
const servo = useRef<HTMLDivElement | null>(null);
const photoresistor = useRef<HTMLDivElement | null>(null)
return (<>
<MainBody>
<Hero
img={LunarImg}
refs={[led, rgbLed, piezo, servo, photoresistor]}
shortNav={ControlNav}
/>
<ControlLED refto={led} />
<ControlRgbLed refto={rgbLed} />
<ControlPiezo refto={piezo} />
<ControlPiezoMusicEditor />
<ControlServo refto={servo} />
<ControlPhotoresistor refto={photoresistor} />
<Closing />
<Footer />
</MainBody>
</>)
}
export default MainPage;

23
src/ports/index.ts Normal file
View File

@ -0,0 +1,23 @@
import readline from 'node:readline/promises';
import { ReadPorts } from "./read";
export async function selectPort (): Promise<string | any> {
const ports = await ReadPorts();
const line = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log(`Select number of ports (0-${ports.length})`);
ports.forEach((port, i) => {
console.log(`${i+1}. ${port.path} - ${port.name}${port.serialNumber ? " - Recomended" : ""}`);
})
const selectedInput: string = await line.question("Enter number: ");
const selectedNumber: number = Number.parseInt(selectedInput);
const selectedPort: { name?: string, path?: string } = ports[selectedNumber-1];
line.close();
return selectedPort.path;
}

8
src/ports/print.ts Normal file
View File

@ -0,0 +1,8 @@
import { ReadPorts } from "./read";
ReadPorts().then((ports) => {
console.log(`${ports.length} Ports available`);
ports.forEach((port, i) => {
console.log(`${i+1}. ${port.path} - ${port.name}`);
})
})

14
src/ports/read.ts Normal file
View File

@ -0,0 +1,14 @@
import * as SerialPort from 'serialport';
export async function ReadPorts (): Promise<{ name?: string, path?: string, serialNumber?: string | number }[]> {
const ports = await SerialPort.SerialPort.list();
return ports
.filter(port => port.pnpId != null)
.map(port => {
return {
name: port['friendlyName'],
path: port.path,
serialNumber: port.serialNumber
}
})
}

23
src/promises/index.ts Normal file
View File

@ -0,0 +1,23 @@
import { Board } from "johnny-five";
export function digitalRead (board: Board, pin: number): Promise<number> {
return new Promise((resolve, reject) => {
board.digitalRead(pin, (val) => {
resolve(val);
});
setTimeout(() => {
reject(new Error("Reading digital timeout"));
}, 5000);
});
}
export function analogRead (board: Board, pin: string): Promise<number> {
return new Promise((resolve, reject) => {
board.analogRead(pin, (val) => {
resolve(val);
})
setTimeout(() => {
reject(new Error("Reading analog timeout"));
}, 5000);
})
}

55
src/routes/api.ts Normal file
View File

@ -0,0 +1,55 @@
import { Response } from "express";
import { Router } from "express";
import { readPin, readPins, setPin, setPins } from "../controller/basic/pin";
import { digitalRead, digitalWrite } from "../controller/basic/digital";
import { analogRead, analogWrite } from "../controller/basic/analog";
import { readLed, readRgbLed, writeLed, writeRgbLed } from "../controller/components/led";
import { piezoNoTone, piezoNote, piezoPlayNotes, piezoTone } from "../controller/components/piezo";
import { rotateServo } from "../controller/components/servo";
import { readResistor } from "../controller/components/photoresistor";
import { isPinNumeric } from "../middleware/pin";
const router: Router = Router();
router.get('/hello', (req, res: Response): Response<string> => {
return res.status(200).send("Hello");
})
// PinMode
router.get('/pin/:p', readPin);
router.get('/pins', readPins);
router.patch('/pin/:p/:m', setPin);
router.patch('/pins', setPins);
// Digital read/write
router.get('/digital/:pin', digitalRead);
router.patch('/digital', digitalWrite);
// Analog read/write
router.get('/analog/:pin', analogRead);
router.patch('/analog', analogWrite);
// LED
router.get('/led/:p', isPinNumeric, readLed);
router.patch('/led/:p/:a', isPinNumeric, writeLed);
// RGB LED
router.post('/rgb-led', readRgbLed);
router.patch('/rgb-led/', writeRgbLed);
// Piezo
router.patch('/piezo/:p/:f', isPinNumeric, piezoTone);
router.patch('/piezo/note', piezoNote);
router.patch('/piezo/music', piezoPlayNotes);
router.patch('/piezo/stop', piezoNoTone);
// for real-time communication is deprecated and not recommended
// use other protocol like websocket instead, we're using socket.io
router.patch('/servo/:p/:m', rotateServo);
router.get('/photoresistor/:p', readResistor);
export default router;

9
src/routes/view.ts Normal file
View File

@ -0,0 +1,9 @@
import { Request, Response, Router } from "express";
const router = Router();
router.get('/', (req: Request, res: Response) => {
res.sendFile('./client/index.html', { root: __dirname + "../../" });
})
export default router;

73
src/server.ts Normal file
View File

@ -0,0 +1,73 @@
import http from 'node:http';
import path from 'node:path';
import express from 'express';
import cors from 'cors';
import { Server } from 'socket.io';
import chalk from 'chalk';
import { suBoard, comport } from './setup';
import { cfg } from './setup/config';
import { isBoardConnected } from './middleware/connection';
import view from './routes/view';
import api from './routes/api';
import socketHandler from './handlers/socketHandler';
const app = express();
const server: http.Server = http.createServer(app);
const io: Server = new Server(server, {
cors: {
origin: cfg.server.cors
}
}); // I have no experience at WebSocket, so.. forgive me :)
// Express middleware
app.use(cors({ origin: "*" }));
app.use(express.json());
app.use(express.static(path.join(__dirname, 'client')));
// Socket.io event handlers
io.on('connection', socketHandler);
// HTTP Routes
app.use('/', view);
app.use('/api-arduino', isBoardConnected, api); // Board API Controllers
// Run server
export function run () {
const { board } = suBoard;
const { port, host } = cfg.server;
try {
if (!board) {
throw new Error("Failed to use API, process canceled. No new API boards have been declared as of yet");
}
console.log("\nRunning Server...");
server.listen(port, () => {
console.log(`Server is connected and running in ${host} at port ${port} 🗣️🗣️🗣️`);
console.log(`* Enter ${chalk.bold(chalk.yellow("rs"))} to restart`);
console.log(`* Press ${chalk.bold(chalk.yellow("CTRL+C"))} to exit`);
console.log(`* URL: ${chalk.bold(`http://${host}:${port}/\n`)}`);
console.log(chalk.yellow(`Connecting to Board`));
board.on('ready', () => {
console.log(chalk.green(`Board at port ${comport} Connected!! (^o^)`));
suBoard.connected = true;
})
board.on('error', (err) => {
console.error(chalk.red("\nError while connecting to Board"));
console.error(chalk.red(err.message));
console.log("Enter 'rs' to try again");
})
board.on('exit', () => {
console.log("Bye bye");
})
});
} catch (err) {
console.error("Fatal error. ", err);
}
}

12
src/setup/config.ts Normal file
View File

@ -0,0 +1,12 @@
import { config } from 'dotenv';
config();
export const cfg = {
port: process.env.SERIAL_PORT || '/dev/ttyUSB0',
server: {
port: 3000,
host: 'localhost',
cors: "*"
}
}

14
src/setup/index.ts Normal file
View File

@ -0,0 +1,14 @@
import { SuBoard } from './support';
import { Board, BoardOption } from 'johnny-five';
import { config } from 'dotenv';
config();
export const comport: string = process.env.SERIAL_PORT || '/dev/ttyUSB0';
export const suBoard: SuBoard = new SuBoard();
export const defaultBoardOption: BoardOption = {
port: comport,
debug: false,
repl: false,
};

25
src/setup/support.ts Normal file
View File

@ -0,0 +1,25 @@
import { Board } from "johnny-five";
export interface SupportBoard {
port: string | null,
connected: boolean,
PINS: {
digital: number[],
analog: number[],
}
sort: () => void
}
export class SuBoard implements SupportBoard {
public board: Board | null;
public port = null;
public connected = false;
public PINS = {
digital: [],
analog: []
};
public sort() {
this.PINS.digital = this.PINS.digital.sort((a, b) => b - a);
this.PINS.analog = this.PINS.analog.sort((a, b) => b - a);
}
}

View File

@ -1,3 +0,0 @@
import { connect } from "socket.io-client";
export const io = connect("http://localhost:3000");

View File

@ -1,28 +0,0 @@
export type PinMode = {
pin: string | number,
mode: 'INPUT' | 'OUTPUT' | 'SERVO'
}
export type PinState = {
pin: number | string,
state: boolean
}
export type DynamicPinState = {
pin: string | number,
state: number
}
export interface ChannelPinState {
red: PinState,
green: PinState,
blue: PinState
}
export interface PiezoMusic {
name: string,
pin: number | string;
notes: string[];
beats: number;
tempo: number;
}

5
src/utils/network.ts Normal file
View File

@ -0,0 +1,5 @@
import { address } from 'ip';
export function getPublicIp () {
return address("public", "ipv4") || '127.0.0.1';
}

1
src/vite-env.d.ts vendored
View File

@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@ -1,34 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{ts,tsx,js,jsx}"
],
theme: {
extend: {
colors: {
'primary': '#3A3AFF',
'secondary': '#0B0B1D66',
'secondary-solid': '#0B0B1D',
'finn': '#00000066',
'finn-solid': '#000000',
'danger': '#FF016C',
'disabled': '#7373B0',
'border': '#FFFFFF66',
'background': '#06060C'
},
fontFamily: {
'poppins': '"Poppins", sans-serif',
'roboto-mono': '"Roboto Mono", monospace',
},
animation: {
'size-in': 'size-in .3s ease-in-out',
'fade-in': 'fade-in .3s ease-in-out',
'size-fade-in': 'size-fade-in .3s ease-in-out',
'wewew': 'wewew .9s ease-in-out infinite',
}
},
},
plugins: [],
}

13
test/serial/blink.ts Normal file
View File

@ -0,0 +1,13 @@
import Firmata from 'firmata';
import { Board, Pin, PinMode } from "johnny-five";
const board = new Board({
port: '/dev/ttyUSB0',
repl: false
});
board.on('ready', () => {
board.pinMode(13, Pin.OUTPUT);
board.digitalWrite(13, 1);
})

11
test/serial/checkBoard.ts Normal file
View File

@ -0,0 +1,11 @@
import { Board } from "johnny-five";
const board: Board = new Board({
port: '/dev/ttyUSB0',
debug: false,
repl: false
});
setInterval(() => {
console.log(board.isReady);
}, 400);

27
test/serial/events.ts Normal file
View File

@ -0,0 +1,27 @@
import { Board } from "johnny-five";
const board: Board = new Board({
port: '/dev/ttyUSB1',
debug: false,
repl: false
});
board.on('ready', () => {
console.log("Board is ready");
})
board.on('close', () => {
console.log("Board is close");
})
board.on('connect', () => {
console.log("Board is connect");
})
board.on('exit', () => {
console.log("Board is exit");
})
board.on('fail', () => {
console.log("Board is fail");
})

View File

@ -0,0 +1,15 @@
import { Board, Pin } from "johnny-five";
const board: Board = new Board({
port: '/dev/ttyUSB0',
debug: false,
repl: false
});
board.on("ready", async () => {
board.pinMode(14, Pin.INPUT);
board.analogRead(0, (val) => {
console.log(val);
})
});

24
test/serial/piezo.ts Normal file
View File

@ -0,0 +1,24 @@
import { Board, Piezo } from "johnny-five";
const board = new Board({
port: '/dev/ttyUSB0',
repl: false
});
board.on('ready', () => {
const piezo = new Piezo(11);
let ins = 0;
board.loop(500, async () => {
piezo.play({
song: "C4",
beats: 1/2,
tempo: 100
})
await new Promise(resolve => setTimeout(resolve, 100));
piezo.note("C4", 100);
await new Promise(resolve => setTimeout(resolve, 100));
piezo.tone(262 * 7.3, 100);
})
})

25
test/serial/rgbLed.js Normal file
View File

@ -0,0 +1,25 @@
const { Board, Led } = require("johnny-five");
const board = new Board({
port: '/dev/ttyUSB0',
repl: false,
debug: false
})
board.on('ready', () => {
const led = new Led.RGB([7, 6, 5]);
let ins = 252;
board.loop(10, () => {
led.color("#0000FF");
led.intensity(ins);
ins--;
if (ins < 1) {
ins = 255;
}
})
board.analogRead(0, (val) => {
console.log(val);
})
});

28
test/serial/rgbLed.ts Normal file
View File

@ -0,0 +1,28 @@
import { Board, Led } from "johnny-five";
const board = new Board({
port: '/dev/ttyUSB0',
repl: false,
debug: false
})
board.on('ready', () => {
const led = new Led.RGB({
pins: {
red: 7,
green: 6,
blue: 5,
},
isAnode: true
})
let ins = 100;
board.loop(10, () => {
led.intensity(ins);
ins--;
if (ins < 1) {
ins = 100;
}
})
});

19
test/serial/servo.ts Normal file
View File

@ -0,0 +1,19 @@
import { Board, Pin, PinMode } from "johnny-five";
const board: Board = new Board({
port: '/dev/ttyUSB0',
debug: true,
repl: false
});
board.on("ready", async () => {
board.pinMode(9, Pin.SERVO);
board.servoWrite(9, 0);
while (true) {
board.servoWrite(9, 180);
await new Promise(resolve => setTimeout(resolve, 1000));
board.servoWrite(9, 0);
await new Promise(resolve => setTimeout(resolve, 1000));
}
});

19
test/websocket/common.ts Normal file
View File

@ -0,0 +1,19 @@
import http from 'node:http';
import express from 'express';
import { Server } from "socket.io";
const app = express();
const server = http.createServer(app);
const io = new Server(server);
io.on("connection", (socket) => {
console.log("Connected");
socket.on("message", (message) => {
console.log(message);
})
})
server.listen(3001, "localhost", () => {
console.log("Common server is running");
})

View File

@ -1,25 +1,11 @@
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
"sourceMap": true,
"outDir": "dist"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
"include": ["src/**/*"],
"exclude": ["test/**/*"]
}

View File

@ -1,11 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

View File

@ -1,7 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})