Initial commit: Next.js UWB positioning webapp
- Complete Next.js 14 app with TypeScript and Tailwind CSS - ESP32 serial communication via API routes - Real-time UWB positioning visualization - Interactive 2D warehouse mapping with Canvas - Device connection interface with auto-detection - AT command parsing for UWBHelper library integration - Clean project structure with comprehensive documentation
This commit is contained in:
commit
fa75faa69d
20 changed files with 7600 additions and 0 deletions
7
.eslintrc.json
Normal file
7
.eslintrc.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals",
|
||||
"rules": {
|
||||
"@next/next/no-img-element": "off",
|
||||
"react-hooks/exhaustive-deps": "warn"
|
||||
}
|
||||
}
|
||||
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# Testing
|
||||
/coverage
|
||||
|
||||
# Next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# Debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Local env files
|
||||
.env*.local
|
||||
|
||||
# Vercel
|
||||
.vercel
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
171
CLAUDE.md
Normal file
171
CLAUDE.md
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with the UWB Positioning Web Application.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a **Next.js 14** web application for visualizing and controlling the ESP32-S3 Ultra-Wideband (UWB) indoor positioning system. It provides real-time positioning visualization and offline data analysis capabilities.
|
||||
|
||||
**Key Architecture**: React-based frontend with Node.js API routes, connects to ESP32 via USB serial, displays real-time positioning data with 2D warehouse visualization.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Setup & Development
|
||||
```bash
|
||||
npm install # Install dependencies
|
||||
npm run dev # Start development server (http://localhost:3000)
|
||||
npm run build # Build for production
|
||||
npm run start # Start production server
|
||||
npm run lint # Run ESLint
|
||||
```
|
||||
|
||||
### API Testing
|
||||
```bash
|
||||
curl http://localhost:3000/api/serial/ports # List available serial ports
|
||||
curl -X POST http://localhost:3000/api/serial/connect # Connect to device
|
||||
curl http://localhost:3000/api/uwb/data # Get positioning data
|
||||
```
|
||||
|
||||
## Core Architecture
|
||||
|
||||
### Frontend Structure
|
||||
```
|
||||
app/
|
||||
├── page.tsx # Main dashboard page
|
||||
├── layout.tsx # Root layout with metadata
|
||||
└── globals.css # Global styles (Tailwind CSS)
|
||||
|
||||
components/
|
||||
├── PositioningVisualizer.tsx # Main visualization component
|
||||
└── DeviceConnection.tsx # Serial connection interface
|
||||
```
|
||||
|
||||
### Backend API Routes
|
||||
```
|
||||
pages/api/
|
||||
├── serial/
|
||||
│ ├── ports.ts # GET: List ESP32 devices
|
||||
│ └── connect.ts # POST/DELETE/GET: Device connection
|
||||
└── uwb/
|
||||
└── data.ts # GET/POST: UWB positioning data
|
||||
```
|
||||
|
||||
### Data Flow Architecture
|
||||
```
|
||||
ESP32 (USB Serial) → API Routes → React Components → Canvas Visualization
|
||||
↓
|
||||
WebSocket Updates → Real-time Display
|
||||
```
|
||||
|
||||
## ESP32 Integration Context
|
||||
|
||||
### Hardware Communication
|
||||
- **Serial Connection**: 115200 baud, USB to ESP32-S3
|
||||
- **AT Command Protocol**: Direct integration with UWBHelper library
|
||||
- **Device Detection**: Automatic ESP32 port identification (Silicon Labs CP210x, FTDI, CH340)
|
||||
- **Real-time Data**: 1-second polling for AT+RANGE responses
|
||||
|
||||
### UWB Data Processing
|
||||
- **Range Data Format**: `AT+RANGE=tid:X,timer:X,timerSys:X,mask:X,seq:X,range:(X,X,X...),rssi:(X,X,X...),ancid:(X,X,X...)`
|
||||
- **Network Configuration**: ID 1234, 8 anchors max, 64 tags supported
|
||||
- **Distance Units**: Centimeters from ESP32 → Meters in webapp
|
||||
- **Coordinate System**: Standard Cartesian (0,0) at bottom-left
|
||||
|
||||
### TypeScript Data Structures
|
||||
```typescript
|
||||
// Core types from ESP32 project
|
||||
interface DeviceData {
|
||||
deviceId: number; // Anchor/Tag ID
|
||||
distance: number; // Range in meters
|
||||
rssi: number; // Signal strength
|
||||
lastUpdate: number; // Timestamp
|
||||
active: boolean; // Connection status
|
||||
}
|
||||
|
||||
interface RangeResult {
|
||||
tagId: number; // Mobile tag ID
|
||||
ranges: number[8]; // Distances to 8 anchors
|
||||
rssi: number[8]; // Signal strengths
|
||||
anchorIds: number[8]; // Connected anchor IDs
|
||||
// ... timing and sequence data
|
||||
}
|
||||
```
|
||||
|
||||
## Visualization Features
|
||||
|
||||
### 2D Warehouse Map
|
||||
- **Canvas Rendering**: HTML5 Canvas with coordinate transformation
|
||||
- **Grid System**: 2m grid spacing, 12m x 10m default warehouse
|
||||
- **Real-time Updates**: Live anchor positions and tag movement
|
||||
- **Visual Indicators**: Color-coded confidence levels (green=high, yellow=low)
|
||||
|
||||
### Interactive Elements
|
||||
- **Device Connection**: Dropdown port selection with auto-detection
|
||||
- **Status Dashboard**: Active anchors, tag position, confidence metrics
|
||||
- **Real-time Data**: 1-second polling with automatic updates
|
||||
|
||||
## Configuration Constants
|
||||
|
||||
Key values matching ESP32 firmware:
|
||||
- `MAX_ANCHORS: 8` - Maximum connected anchors per tag
|
||||
- `UWB_TAG_COUNT: 64` - Network capacity
|
||||
- `NETWORK_ID: 1234` - UWB network identifier
|
||||
- `BAUD_RATE: 115200` - Serial communication speed
|
||||
- `POLL_INTERVAL: 1000ms` - Real-time data update rate
|
||||
|
||||
## API Endpoints Reference
|
||||
|
||||
### Serial Communication
|
||||
- `GET /api/serial/ports` - List available COM ports with ESP32 filtering
|
||||
- `POST /api/serial/connect` - Connect to device: `{port: "COM3", baudRate: 115200}`
|
||||
- `DELETE /api/serial/connect` - Disconnect from device
|
||||
- `GET /api/serial/connect` - Get connection status
|
||||
|
||||
### UWB Data
|
||||
- `GET /api/uwb/data` - Get latest positioning data
|
||||
- `POST /api/uwb/data` - Send AT command: `{command: "AT+GETRPT?"}`
|
||||
|
||||
## Development Roadmap Context
|
||||
|
||||
This webapp implements **Phase 4** of the UWB positioning project (see main project ROADMAP.md):
|
||||
- **Current**: Real-time visualization with ESP32 serial connection
|
||||
- **Next**: Dual-file upload (raw_positioning.csv + anchor_coordinates.csv)
|
||||
- **Future**: Path playback, session analysis, warehouse mapping tools
|
||||
|
||||
## Tech Stack Details
|
||||
|
||||
### Frontend
|
||||
- **Framework**: Next.js 14 with App Router
|
||||
- **Language**: TypeScript with strict mode
|
||||
- **Styling**: Tailwind CSS 3.3+
|
||||
- **UI Components**: Custom React components with Canvas rendering
|
||||
- **State Management**: React hooks (useState, useEffect, useCallback)
|
||||
|
||||
### Backend
|
||||
- **Runtime**: Node.js with Next.js API routes
|
||||
- **Serial Communication**: SerialPort library for ESP32 connection
|
||||
- **Data Processing**: Custom AT command parsing
|
||||
- **WebSocket**: Real-time data streaming (planned)
|
||||
|
||||
### Development Tools
|
||||
- **Linting**: ESLint with Next.js configuration
|
||||
- **TypeScript**: Strict type checking with ESP32 data structures
|
||||
- **Hot Reload**: Next.js development server with fast refresh
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Serial Port Access**: Requires native Node.js modules, client-side limitations handled via API routes
|
||||
- **ESP32 Detection**: Automatic filtering for common ESP32 USB-to-Serial chips
|
||||
- **Data Validation**: AT command format validation and error handling
|
||||
- **Coordinate System**: Matches ESP32 firmware expectations (meters, Cartesian)
|
||||
- **Real-time Performance**: 1-second polling optimized for warehouse-scale positioning
|
||||
|
||||
## File Upload Integration (Future)
|
||||
|
||||
The webapp is designed to support the dual-file approach from the main project:
|
||||
- **raw_positioning.csv**: Tag distance measurements with timestamps
|
||||
- **anchor_coordinates.csv**: Calibrated anchor positions from auto-positioning
|
||||
- **Offline Analysis**: Calculate tag paths using anchor coordinates + raw distances
|
||||
- **Session Playback**: Timeline scrubbing and path visualization
|
||||
|
||||
This webapp serves as the visualization frontend for the complete ESP32-S3 UWB positioning system, bridging real-time hardware data with interactive web-based analysis tools.
|
||||
53
README.md
Normal file
53
README.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# UWB Positioning Web Application
|
||||
|
||||
Next.js web application for visualizing and analyzing UWB indoor positioning data.
|
||||
|
||||
## Features
|
||||
|
||||
- **Real-time Positioning**: Live visualization of tag positions via WebSocket
|
||||
- **Data Analysis**: Upload and analyze positioning session files
|
||||
- **Interactive Maps**: 2D warehouse visualization with anchor positions
|
||||
- **Path Tracking**: Tag movement analysis and playback
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
uwb-webapp/
|
||||
├── app/ # Next.js App Router pages
|
||||
├── components/ # React components
|
||||
├── lib/ # Utilities and configurations
|
||||
├── pages/api/ # API routes
|
||||
├── public/ # Static assets
|
||||
└── utils/ # Helper functions
|
||||
```
|
||||
|
||||
## Context from ESP32 Project
|
||||
|
||||
This webapp connects to the ESP32-S3 UWB positioning system:
|
||||
|
||||
- **Hardware**: Makerfabs MaUWB modules (ESP32-S3 + DW3000 UWB + OLED)
|
||||
- **Network**: 8 anchors + 1 mobile tag (ID 1234, 6.8Mbps)
|
||||
- **Data Flow**: Tag → USB → PC → WebApp
|
||||
- **Files**: raw_positioning.csv + anchor_coordinates.csv
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- `GET /api/serial` - List available serial ports
|
||||
- `POST /api/connect` - Connect to UWB device
|
||||
- `GET /api/data` - Get positioning data stream
|
||||
- `POST /api/upload` - Upload CSV files for analysis
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: Next.js 14 with App Router
|
||||
- **Language**: TypeScript
|
||||
- **Serial**: Node SerialPort for ESP32 communication
|
||||
- **WebSocket**: Real-time data streaming
|
||||
- **Visualization**: Canvas/SVG for 2D positioning display
|
||||
9
app/globals.css
Normal file
9
app/globals.css
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
19
app/layout.tsx
Normal file
19
app/layout.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { Metadata } from 'next'
|
||||
import './globals.css'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'UWB Positioning System',
|
||||
description: 'ESP32-S3 Ultra-Wideband Indoor Positioning Visualization',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
22
app/page.tsx
Normal file
22
app/page.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import PositioningVisualizer from '@/components/PositioningVisualizer'
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<header className="bg-white shadow-sm border-b">
|
||||
<div className="max-w-6xl mx-auto px-6 py-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
UWB Indoor Positioning System
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
ESP32-S3 Warehouse Positioning & Visualization
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="max-w-6xl mx-auto px-6 py-8">
|
||||
<PositioningVisualizer />
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
193
components/DeviceConnection.tsx
Normal file
193
components/DeviceConnection.tsx
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { SerialPortInfo } from '@/lib/serial-utils'
|
||||
|
||||
interface DeviceConnectionProps {
|
||||
onConnectionChange: (connected: boolean) => void
|
||||
onDataReceived: (data: any) => void
|
||||
}
|
||||
|
||||
export default function DeviceConnection({
|
||||
onConnectionChange,
|
||||
onDataReceived
|
||||
}: DeviceConnectionProps) {
|
||||
const [availablePorts, setAvailablePorts] = useState<SerialPortInfo[]>([])
|
||||
const [selectedPort, setSelectedPort] = useState<string>('')
|
||||
const [isConnected, setIsConnected] = useState(false)
|
||||
const [isConnecting, setIsConnecting] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Load available serial ports
|
||||
useEffect(() => {
|
||||
loadPorts()
|
||||
}, [])
|
||||
|
||||
// Poll for data when connected
|
||||
useEffect(() => {
|
||||
if (!isConnected) return
|
||||
|
||||
const pollInterval = setInterval(async () => {
|
||||
try {
|
||||
const response = await fetch('/api/uwb/data')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (data.rangeData || data.deviceData?.length > 0) {
|
||||
onDataReceived(data)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Data polling error:', error)
|
||||
}
|
||||
}, 1000) // Poll every second
|
||||
|
||||
return () => clearInterval(pollInterval)
|
||||
}, [isConnected, onDataReceived])
|
||||
|
||||
const loadPorts = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/serial/ports')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setAvailablePorts(data.esp32Ports || [])
|
||||
|
||||
// Auto-select first ESP32 port if available
|
||||
if (data.esp32Ports?.length > 0 && !selectedPort) {
|
||||
setSelectedPort(data.esp32Ports[0].path)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load ports:', error)
|
||||
setError('Failed to load available ports')
|
||||
}
|
||||
}
|
||||
|
||||
const connect = async () => {
|
||||
if (!selectedPort) return
|
||||
|
||||
setIsConnecting(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/serial/connect', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
port: selectedPort,
|
||||
baudRate: 115200
|
||||
})
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
setIsConnected(true)
|
||||
onConnectionChange(true)
|
||||
|
||||
// Send initial test command
|
||||
await sendCommand('AT?')
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
setError(errorData.message || 'Connection failed')
|
||||
}
|
||||
} catch (error) {
|
||||
setError(`Connection error: ${error}`)
|
||||
} finally {
|
||||
setIsConnecting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const disconnect = async () => {
|
||||
try {
|
||||
await fetch('/api/serial/connect', { method: 'DELETE' })
|
||||
setIsConnected(false)
|
||||
onConnectionChange(false)
|
||||
} catch (error) {
|
||||
console.error('Disconnect error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const sendCommand = async (command: string) => {
|
||||
try {
|
||||
await fetch('/api/uwb/data', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ command })
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Command send error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Device Connection</h2>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-100 border border-red-300 text-red-700 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Select ESP32 Device
|
||||
</label>
|
||||
<select
|
||||
value={selectedPort}
|
||||
onChange={(e) => setSelectedPort(e.target.value)}
|
||||
disabled={isConnected}
|
||||
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100"
|
||||
>
|
||||
<option value="">Select a port...</option>
|
||||
{availablePorts.map(port => (
|
||||
<option key={port.path} value={port.path}>
|
||||
{port.path} - {port.manufacturer || 'Unknown'}
|
||||
{port.productId && ` (${port.productId})`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{!isConnected ? (
|
||||
<button
|
||||
onClick={connect}
|
||||
disabled={!selectedPort || isConnecting}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isConnecting ? 'Connecting...' : 'Connect'}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={disconnect}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700"
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={loadPorts}
|
||||
disabled={isConnecting}
|
||||
className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 disabled:bg-gray-400"
|
||||
>
|
||||
Refresh Ports
|
||||
</button>
|
||||
|
||||
<div className={`flex items-center gap-2 ${isConnected ? 'text-green-600' : 'text-gray-400'}`}>
|
||||
<div className={`w-3 h-3 rounded-full ${isConnected ? 'bg-green-500' : 'bg-gray-300'}`} />
|
||||
<span className="text-sm font-medium">
|
||||
{isConnected ? 'Connected' : 'Disconnected'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{availablePorts.length === 0 && (
|
||||
<p className="text-sm text-gray-500">
|
||||
No ESP32 devices detected. Make sure your device is connected and drivers are installed.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
233
components/PositioningVisualizer.tsx
Normal file
233
components/PositioningVisualizer.tsx
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { AnchorPosition, TagPosition, DeviceData, RangeResult } from '@/lib/uwb-types'
|
||||
import DeviceConnection from './DeviceConnection'
|
||||
|
||||
interface VisualizerProps {
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
export default function PositioningVisualizer({
|
||||
width = 800,
|
||||
height = 600
|
||||
}: VisualizerProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const [anchors, setAnchors] = useState<AnchorPosition[]>([])
|
||||
const [tagPosition, setTagPosition] = useState<TagPosition | null>(null)
|
||||
const [connectedDevices, setConnectedDevices] = useState<DeviceData[]>([])
|
||||
const [isConnected, setIsConnected] = useState(false)
|
||||
const [lastDataUpdate, setLastDataUpdate] = useState<number>(0)
|
||||
|
||||
// Mock data for development
|
||||
useEffect(() => {
|
||||
const mockAnchors: AnchorPosition[] = [
|
||||
{ anchorId: 0, x: 0, y: 0, confidence: 0.9, valid: true },
|
||||
{ anchorId: 1, x: 10, y: 0, confidence: 0.8, valid: true },
|
||||
{ anchorId: 2, x: 10, y: 8, confidence: 0.9, valid: true },
|
||||
{ anchorId: 3, x: 0, y: 8, confidence: 0.7, valid: true },
|
||||
{ anchorId: 4, x: 5, y: 4, confidence: 0.8, valid: true },
|
||||
]
|
||||
|
||||
const mockTag: TagPosition = {
|
||||
x: 3.5,
|
||||
y: 2.1,
|
||||
timestamp: Date.now(),
|
||||
confidence: 0.85
|
||||
}
|
||||
|
||||
setAnchors(mockAnchors)
|
||||
setTagPosition(mockTag)
|
||||
}, [])
|
||||
|
||||
// Canvas rendering
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
// Set up coordinate system (flip Y axis for standard cartesian)
|
||||
ctx.save()
|
||||
ctx.scale(1, -1)
|
||||
ctx.translate(0, -height)
|
||||
|
||||
// Scale to fit data (assuming 12m x 10m warehouse area)
|
||||
const scale = Math.min(width / 12, height / 10)
|
||||
ctx.scale(scale, scale)
|
||||
ctx.translate(1, 1) // Small offset from edges
|
||||
|
||||
// Draw grid
|
||||
ctx.strokeStyle = '#e5e7eb'
|
||||
ctx.lineWidth = 1 / scale
|
||||
for (let i = 0; i <= 12; i += 2) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(i, 0)
|
||||
ctx.lineTo(i, 10)
|
||||
ctx.stroke()
|
||||
}
|
||||
for (let i = 0; i <= 10; i += 2) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, i)
|
||||
ctx.lineTo(12, i)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// Draw anchors
|
||||
anchors.forEach(anchor => {
|
||||
if (!anchor.valid) return
|
||||
|
||||
ctx.fillStyle = anchor.confidence > 0.8 ? '#10b981' : '#f59e0b'
|
||||
ctx.beginPath()
|
||||
ctx.arc(anchor.x, anchor.y, 0.3, 0, 2 * Math.PI)
|
||||
ctx.fill()
|
||||
|
||||
// Anchor ID label
|
||||
ctx.save()
|
||||
ctx.scale(1, -1) // Flip text back
|
||||
ctx.fillStyle = '#374151'
|
||||
ctx.font = `${0.4}px Arial`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText(
|
||||
anchor.anchorId.toString(),
|
||||
anchor.x,
|
||||
-anchor.y + 0.15
|
||||
)
|
||||
ctx.restore()
|
||||
})
|
||||
|
||||
// Draw tag position
|
||||
if (tagPosition) {
|
||||
ctx.fillStyle = '#3b82f6'
|
||||
ctx.beginPath()
|
||||
ctx.arc(tagPosition.x, tagPosition.y, 0.2, 0, 2 * Math.PI)
|
||||
ctx.fill()
|
||||
|
||||
// Tag confidence indicator
|
||||
ctx.strokeStyle = `rgba(59, 130, 246, ${tagPosition.confidence})`
|
||||
ctx.lineWidth = 3 / scale
|
||||
ctx.beginPath()
|
||||
ctx.arc(tagPosition.x, tagPosition.y, 0.5, 0, 2 * Math.PI)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
}, [anchors, tagPosition, width, height])
|
||||
|
||||
const connectToDevice = async () => {
|
||||
try {
|
||||
// This will be implemented with actual serial connection
|
||||
setIsConnected(true)
|
||||
console.log('Connecting to UWB device...')
|
||||
} catch (error) {
|
||||
console.error('Failed to connect:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle real-time data updates
|
||||
const handleDataReceived = useCallback((data: any) => {
|
||||
setLastDataUpdate(Date.now())
|
||||
|
||||
if (data.rangeData) {
|
||||
// Process range data for positioning
|
||||
const rangeData: RangeResult = data.rangeData
|
||||
console.log('Range data received:', rangeData)
|
||||
|
||||
// Update device data from range results
|
||||
const deviceUpdates: DeviceData[] = []
|
||||
for (let i = 0; i < 8; i++) {
|
||||
if (rangeData.ranges[i] > 0 && rangeData.anchorIds[i] > 0) {
|
||||
deviceUpdates.push({
|
||||
deviceId: rangeData.anchorIds[i],
|
||||
distance: rangeData.ranges[i],
|
||||
rssi: rangeData.rssi[i],
|
||||
lastUpdate: Date.now(),
|
||||
active: true
|
||||
})
|
||||
}
|
||||
}
|
||||
setConnectedDevices(deviceUpdates)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleConnectionChange = useCallback((connected: boolean) => {
|
||||
setIsConnected(connected)
|
||||
if (!connected) {
|
||||
setConnectedDevices([])
|
||||
setLastDataUpdate(0)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Device Connection */}
|
||||
<DeviceConnection
|
||||
onConnectionChange={handleConnectionChange}
|
||||
onDataReceived={handleDataReceived}
|
||||
/>
|
||||
|
||||
{/* Status Panel */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">System Status</h2>
|
||||
|
||||
{/* Device Status */}
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="font-medium text-gray-600">Active Anchors:</span>
|
||||
<span className="ml-2">{anchors.filter(a => a.valid).length}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-600">Tag Position:</span>
|
||||
<span className="ml-2">
|
||||
{tagPosition ?
|
||||
`(${tagPosition.x.toFixed(1)}, ${tagPosition.y.toFixed(1)})` :
|
||||
'No data'
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-600">Confidence:</span>
|
||||
<span className="ml-2">
|
||||
{tagPosition ?
|
||||
`${(tagPosition.confidence * 100).toFixed(0)}%` :
|
||||
'N/A'
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Visualization Canvas */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h3 className="text-lg font-medium mb-4">Warehouse Map</h3>
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={width}
|
||||
height={height}
|
||||
className="block"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center gap-6 text-sm text-gray-600">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-green-500 rounded-full" />
|
||||
<span>High Confidence Anchor</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-yellow-500 rounded-full" />
|
||||
<span>Low Confidence Anchor</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-blue-500 rounded-full" />
|
||||
<span>Mobile Tag</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
71
lib/serial-utils.ts
Normal file
71
lib/serial-utils.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
// Utility functions for serial communication and UWB data processing
|
||||
|
||||
export interface SerialPortInfo {
|
||||
path: string
|
||||
manufacturer?: string
|
||||
serialNumber?: string
|
||||
pnpId?: string
|
||||
locationId?: string
|
||||
productId?: string
|
||||
vendorId?: string
|
||||
}
|
||||
|
||||
export class UWBDataProcessor {
|
||||
private static instance: UWBDataProcessor
|
||||
private dataBuffer: string = ''
|
||||
|
||||
static getInstance(): UWBDataProcessor {
|
||||
if (!UWBDataProcessor.instance) {
|
||||
UWBDataProcessor.instance = new UWBDataProcessor()
|
||||
}
|
||||
return UWBDataProcessor.instance
|
||||
}
|
||||
|
||||
// Process incoming serial data stream
|
||||
processDataStream(chunk: string): string[] {
|
||||
this.dataBuffer += chunk
|
||||
const lines: string[] = []
|
||||
|
||||
// Split on line endings
|
||||
const parts = this.dataBuffer.split(/\r?\n/)
|
||||
|
||||
// Keep last incomplete line in buffer
|
||||
this.dataBuffer = parts.pop() || ''
|
||||
|
||||
// Return complete lines
|
||||
return parts.filter(line => line.trim().length > 0)
|
||||
}
|
||||
|
||||
// Check if ESP32 device based on port info
|
||||
static isESP32Device(port: SerialPortInfo): boolean {
|
||||
const esp32Vendors = ['10C4', '1A86', '0403'] // Silicon Labs, QinHeng, FTDI
|
||||
const esp32Manufacturers = ['Silicon Labs', 'FTDI', 'QinHeng Electronics']
|
||||
|
||||
return (
|
||||
(port.vendorId && esp32Vendors.includes(port.vendorId)) ||
|
||||
(port.manufacturer && esp32Manufacturers.some(m =>
|
||||
port.manufacturer?.includes(m)
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
// Validate UWB command format
|
||||
static validateUWBCommand(command: string): boolean {
|
||||
const validCommands = [
|
||||
'AT?', 'AT+GETVER?', 'AT+RESTART', 'AT+RESTORE', 'AT+SAVE',
|
||||
'AT+SETCFG=', 'AT+GETCFG?', 'AT+SETANT=', 'AT+GETANT?',
|
||||
'AT+SETCAP=', 'AT+GETCAP?', 'AT+SETRPT=', 'AT+GETRPT?',
|
||||
'AT+SLEEP=', 'AT+SETPOW=', 'AT+GETPOW?', 'AT+DATA=',
|
||||
'AT+RDATA', 'AT+SETPAN=', 'AT+GETPAN?'
|
||||
]
|
||||
|
||||
return validCommands.some(cmd =>
|
||||
command.startsWith(cmd) || command === cmd
|
||||
)
|
||||
}
|
||||
|
||||
// Clear data buffer
|
||||
clearBuffer(): void {
|
||||
this.dataBuffer = ''
|
||||
}
|
||||
}
|
||||
67
lib/uwb-types.ts
Normal file
67
lib/uwb-types.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// UWB Device Data Types (from ESP32 project context)
|
||||
|
||||
export interface DeviceData {
|
||||
deviceId: number;
|
||||
distance: number;
|
||||
rssi: number;
|
||||
lastUpdate: number;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface AnchorPosition {
|
||||
anchorId: number;
|
||||
x: number;
|
||||
y: number;
|
||||
confidence: number;
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
export interface RangeResult {
|
||||
tagId: number;
|
||||
mask: number;
|
||||
sequence: number;
|
||||
ranges: number[]; // 8 elements
|
||||
rssi: number[]; // 8 elements
|
||||
anchorIds: number[]; // 8 elements
|
||||
timer: number;
|
||||
timerSys: number;
|
||||
}
|
||||
|
||||
export interface TagPosition {
|
||||
x: number;
|
||||
y: number;
|
||||
timestamp: number;
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
// Configuration constants from ESP32 project
|
||||
export const UWB_CONFIG = {
|
||||
MAX_ANCHORS: 8,
|
||||
UWB_TAG_COUNT: 64,
|
||||
NETWORK_ID: 1234,
|
||||
DEVICE_TIMEOUT: 5000,
|
||||
BAUD_RATE: 115200,
|
||||
DATA_RATE: 1, // 6.8Mbps
|
||||
RANGE_FILTER: 1
|
||||
} as const;
|
||||
|
||||
// Serial data parsing types
|
||||
export interface SerialData {
|
||||
type: 'range' | 'config' | 'status';
|
||||
raw: string;
|
||||
parsed?: RangeResult | DeviceData[];
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
// File upload types for offline analysis
|
||||
export interface PositioningSession {
|
||||
rawData: RangeResult[];
|
||||
anchors: AnchorPosition[];
|
||||
tagPath: TagPosition[];
|
||||
sessionInfo: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
duration: number;
|
||||
totalPoints: number;
|
||||
};
|
||||
}
|
||||
12
next.config.js
Normal file
12
next.config.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
webpack: (config) => {
|
||||
// Handle serialport native bindings for server-side
|
||||
config.externals.push({
|
||||
'serialport': 'commonjs serialport'
|
||||
});
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
6377
package-lock.json
generated
Normal file
6377
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
41
package.json
Normal file
41
package.json
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"name": "uwb-positioning-webapp",
|
||||
"version": "1.0.0",
|
||||
"description": "Next.js web application for UWB indoor positioning system visualization and control",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"keywords": [
|
||||
"uwb",
|
||||
"positioning",
|
||||
"indoor",
|
||||
"warehouse",
|
||||
"visualization",
|
||||
"nextjs"
|
||||
],
|
||||
"author": "UWB Positioning Team",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/parser-readline": "^12.0.0",
|
||||
"next": "^14.2.32",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"serialport": "^12.0.0",
|
||||
"ws": "^8.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.8.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/ws": "^8.5.0",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"eslint": "^8.52.0",
|
||||
"eslint-config-next": "14.0.0",
|
||||
"postcss": "^8.4.0",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"typescript": "^5.2.0"
|
||||
}
|
||||
}
|
||||
91
pages/api/serial/connect.ts
Normal file
91
pages/api/serial/connect.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { SerialPort } from 'serialport'
|
||||
import { ReadlineParser } from '@serialport/parser-readline'
|
||||
|
||||
// Global connection state
|
||||
let serialConnection: SerialPort | null = null
|
||||
let parser: ReadlineParser | null = null
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
if (req.method === 'POST') {
|
||||
const { port, baudRate = 115200 } = req.body
|
||||
|
||||
if (!port) {
|
||||
return res.status(400).json({ message: 'Port is required' })
|
||||
}
|
||||
|
||||
try {
|
||||
// Close existing connection
|
||||
if (serialConnection?.isOpen) {
|
||||
serialConnection.close()
|
||||
}
|
||||
|
||||
// Create new connection
|
||||
serialConnection = new SerialPort({
|
||||
path: port,
|
||||
baudRate: baudRate,
|
||||
autoOpen: false
|
||||
})
|
||||
|
||||
// Set up parser for line-based data
|
||||
parser = serialConnection.pipe(new ReadlineParser({ delimiter: '\r\n' }))
|
||||
|
||||
// Open connection
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
serialConnection!.open((err) => {
|
||||
if (err) reject(err)
|
||||
else resolve()
|
||||
})
|
||||
})
|
||||
|
||||
res.status(200).json({
|
||||
message: 'Connected successfully',
|
||||
port,
|
||||
baudRate,
|
||||
isOpen: serialConnection.isOpen
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Serial connection error:', error)
|
||||
res.status(500).json({
|
||||
message: 'Failed to connect',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
else if (req.method === 'DELETE') {
|
||||
// Disconnect
|
||||
try {
|
||||
if (serialConnection?.isOpen) {
|
||||
serialConnection.close()
|
||||
serialConnection = null
|
||||
parser = null
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Disconnected successfully' })
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
message: 'Failed to disconnect',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
else if (req.method === 'GET') {
|
||||
// Get connection status
|
||||
res.status(200).json({
|
||||
connected: serialConnection?.isOpen || false,
|
||||
port: serialConnection?.path || null
|
||||
})
|
||||
}
|
||||
|
||||
else {
|
||||
res.status(405).json({ message: 'Method not allowed' })
|
||||
}
|
||||
}
|
||||
|
||||
export { serialConnection, parser }
|
||||
37
pages/api/serial/ports.ts
Normal file
37
pages/api/serial/ports.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { SerialPort } from 'serialport'
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
if (req.method !== 'GET') {
|
||||
return res.status(405).json({ message: 'Method not allowed' })
|
||||
}
|
||||
|
||||
try {
|
||||
const ports = await SerialPort.list()
|
||||
|
||||
// Filter for ESP32 devices (common VID/PID patterns)
|
||||
const esp32Ports = ports.filter(port =>
|
||||
port.vendorId === '10C4' || // Silicon Labs CP210x
|
||||
port.vendorId === '1A86' || // QinHeng Electronics CH340
|
||||
port.vendorId === '0403' || // FTDI
|
||||
port.manufacturer?.includes('Silicon Labs') ||
|
||||
port.manufacturer?.includes('FTDI') ||
|
||||
port.productId === 'EA60' // CP2102
|
||||
)
|
||||
|
||||
res.status(200).json({
|
||||
allPorts: ports,
|
||||
esp32Ports,
|
||||
count: esp32Ports.length
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error listing serial ports:', error)
|
||||
res.status(500).json({
|
||||
message: 'Failed to list serial ports',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
}
|
||||
}
|
||||
112
pages/api/uwb/data.ts
Normal file
112
pages/api/uwb/data.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { serialConnection, parser } from '../serial/connect'
|
||||
import { RangeResult, DeviceData } from '@/lib/uwb-types'
|
||||
|
||||
// Data storage for real-time updates
|
||||
let latestRangeData: RangeResult | null = null
|
||||
let latestDeviceData: DeviceData[] = []
|
||||
|
||||
// Parse UWB range data (from ESP32 UWBHelper parseRangeData format)
|
||||
function parseUWBRangeData(data: string): RangeResult | DeviceData[] | null {
|
||||
if (!data.startsWith("AT+RANGE=")) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Parse tid (Tag ID)
|
||||
const tidMatch = data.match(/tid:(\d+)/)
|
||||
if (!tidMatch) return null
|
||||
|
||||
const tagId = parseInt(tidMatch[1])
|
||||
|
||||
// Parse detailed range data format
|
||||
const maskMatch = data.match(/mask:(\d+)/)
|
||||
const seqMatch = data.match(/seq:(\d+)/)
|
||||
const timerMatch = data.match(/timer:(\d+)/)
|
||||
const timerSysMatch = data.match(/timerSys:(\d+)/)
|
||||
|
||||
// Parse range data
|
||||
const rangeMatch = data.match(/range:\(([^)]+)\)/)
|
||||
const rssiMatch = data.match(/rssi:\(([^)]+)\)/)
|
||||
const ancidMatch = data.match(/ancid:\(([^)]+)\)/)
|
||||
|
||||
if (rangeMatch && rssiMatch) {
|
||||
const ranges = rangeMatch[1].split(',').map(r => parseFloat(r) / 100) // Convert cm to meters
|
||||
const rssi = rssiMatch[1].split(',').map(r => parseFloat(r))
|
||||
const anchorIds = ancidMatch ? ancidMatch[1].split(',').map(a => parseInt(a)) : Array(8).fill(0)
|
||||
|
||||
const result: RangeResult = {
|
||||
tagId,
|
||||
mask: maskMatch ? parseInt(maskMatch[1]) : 0,
|
||||
sequence: seqMatch ? parseInt(seqMatch[1]) : 0,
|
||||
ranges: ranges.slice(0, 8).concat(Array(8 - ranges.length).fill(0)),
|
||||
rssi: rssi.slice(0, 8).concat(Array(8 - rssi.length).fill(0)),
|
||||
anchorIds: anchorIds.slice(0, 8).concat(Array(8 - anchorIds.length).fill(0)),
|
||||
timer: timerMatch ? parseInt(timerMatch[1]) : 0,
|
||||
timerSys: timerSysMatch ? parseInt(timerSysMatch[1]) : 0
|
||||
}
|
||||
|
||||
latestRangeData = result
|
||||
return result
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Set up data listener when connection is established
|
||||
if (parser) {
|
||||
parser.on('data', (data: string) => {
|
||||
const parsed = parseUWBRangeData(data.trim())
|
||||
if (parsed) {
|
||||
console.log('UWB Data received:', parsed)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
if (req.method === 'GET') {
|
||||
if (!serialConnection?.isOpen) {
|
||||
return res.status(400).json({
|
||||
message: 'Serial connection not established'
|
||||
})
|
||||
}
|
||||
|
||||
// Return latest data
|
||||
res.status(200).json({
|
||||
rangeData: latestRangeData,
|
||||
deviceData: latestDeviceData,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
}
|
||||
|
||||
else if (req.method === 'POST') {
|
||||
// Send command to UWB device
|
||||
const { command } = req.body
|
||||
|
||||
if (!serialConnection?.isOpen) {
|
||||
return res.status(400).json({
|
||||
message: 'Serial connection not established'
|
||||
})
|
||||
}
|
||||
|
||||
if (!command) {
|
||||
return res.status(400).json({ message: 'Command is required' })
|
||||
}
|
||||
|
||||
try {
|
||||
serialConnection.write(command + '\r\n')
|
||||
res.status(200).json({ message: 'Command sent', command })
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
message: 'Failed to send command',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
res.status(405).json({ message: 'Method not allowed' })
|
||||
}
|
||||
}
|
||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
18
tailwind.config.js
Normal file
18
tailwind.config.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
backgroundImage: {
|
||||
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
||||
'gradient-conic':
|
||||
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "es6"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue