├── .prettierrc.json ├── .eslintrc.json ├── screendemo.gif ├── src └── app │ ├── favicon.ico │ ├── lib │ ├── classNames.ts │ └── haversine.ts │ ├── layout.tsx │ ├── api │ ├── aircraft │ │ └── route.ts │ └── getroute │ │ └── route.ts │ ├── components │ ├── SlideHolder.tsx │ └── PlaneAnimation.tsx │ ├── globals.css │ └── page.tsx ├── postcss.config.js ├── next.config.js ├── next-env.d.ts ├── logging.conf ├── tailwind.config.js ├── public ├── vercel.svg └── next.svg ├── package.json ├── tsconfig.json ├── .gitignore └── README.md /.prettierrc.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /screendemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetclock/jetscreen-v2/HEAD/screendemo.gif -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetclock/jetscreen-v2/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | module.exports = nextConfig; 5 | -------------------------------------------------------------------------------- /src/app/lib/classNames.ts: -------------------------------------------------------------------------------- 1 | export const classNames = ( 2 | ...classes: (string | false | null | undefined)[] 3 | ) => { 4 | return classes.filter(Boolean).join(" "); 5 | }; 6 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /logging.conf: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root 3 | 4 | [handlers] 5 | keys=consoleHandler 6 | 7 | [formatters] 8 | keys=simpleFormatter 9 | 10 | [logger_root] 11 | level=DEBUG 12 | handlers=consoleHandler 13 | 14 | [handler_consoleHandler] 15 | class=StreamHandler 16 | level=DEBUG 17 | formatter=simpleFormatter 18 | args=(sys.stdout,) 19 | 20 | [formatter_simpleFormatter] 21 | format=%(asctime)s - %(threadName)-10s - %(name)s - %(levelname)s - %(message)s 22 | datefmt= -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import { Inter } from "next/font/google"; 3 | 4 | const inter = Inter({ subsets: ["latin"] }); 5 | 6 | export const metadata = { 7 | title: "JetScreen", 8 | }; 9 | 10 | export default function RootLayout({ 11 | children, 12 | }: { 13 | children: React.ReactNode; 14 | }) { 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 12 | 'gradient-conic': 13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } 19 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jetscreen", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@splidejs/react-splide": "^0.7.12", 13 | "@types/node": "20.3.0", 14 | "@types/react": "18.2.11", 15 | "@types/react-dom": "18.2.4", 16 | "autoprefixer": "10.4.14", 17 | "eslint": "8.42.0", 18 | "eslint-config-next": "13.4.5", 19 | "next": "13.4.5", 20 | "postcss": "8.4.24", 21 | "react": "18.2.0", 22 | "react-dom": "18.2.0", 23 | "swr": "^2.1.5", 24 | "tailwindcss": "3.3.2", 25 | "typescript": "5.1.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/lib/haversine.ts: -------------------------------------------------------------------------------- 1 | // Haversine formula to calculate distance between two lat/lon points 2 | export const haversineDistance = ( 3 | lat1: number, 4 | lon1: number, 5 | lat2: number, 6 | lon2: number 7 | ) => { 8 | const R = 6371; // Earth radius in kilometers 9 | const toRadians = (deg: number) => (deg * Math.PI) / 180; 10 | const deltaLat = toRadians(lat2 - lat1); 11 | const deltaLon = toRadians(lon2 - lon1); 12 | const a = 13 | Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) + 14 | Math.cos(toRadians(lat1)) * 15 | Math.cos(toRadians(lat2)) * 16 | Math.sin(deltaLon / 2) * 17 | Math.sin(deltaLon / 2); 18 | const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 19 | return R * c; 20 | }; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /src/app/api/aircraft/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | export const revalidate = 0; 3 | const API_URL = process.env.LOCAL_ADSB_URL || ""; 4 | 5 | export async function GET() { 6 | try { 7 | // Fetch data from the external API 8 | const response = await fetch(API_URL); 9 | 10 | // Handle unsuccessful requests 11 | if (!response.ok) { 12 | return NextResponse.json( 13 | { error: "Failed to fetch aircraft data" }, 14 | { status: response.status } 15 | ); 16 | } 17 | 18 | // Parse the JSON data from the response 19 | const data = await response.json(); 20 | 21 | // Return the data as a JSON response 22 | return NextResponse.json(data); 23 | } catch (error) { 24 | // Handle any errors that occur during the fetch 25 | return NextResponse.json( 26 | { error: "An error occurred while fetching data" }, 27 | { status: 500 } 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/api/getroute/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | const FLIGHT_DETAILS_URL = process.env.NEXT_PUBLIC_FLIGHT_DETAILS_URL || ""; 4 | 5 | export async function GET(request: NextRequest) { 6 | const { searchParams } = request.nextUrl; 7 | 8 | // Extract the callsign from the query parameters 9 | const callsign = searchParams.get("callsign"); 10 | if (!callsign) { 11 | return NextResponse.json( 12 | { error: "No callsign provided" }, 13 | { status: 400 } 14 | ); 15 | } 16 | 17 | try { 18 | // Fetch data from the external API 19 | const flightDetails = await fetch(`${FLIGHT_DETAILS_URL}${callsign}`); 20 | 21 | // Handle unsuccessful requests 22 | if (!flightDetails.ok) { 23 | return NextResponse.json( 24 | { error: "Failed to fetch flight details" }, 25 | { status: flightDetails.status } 26 | ); 27 | } 28 | 29 | // Parse the JSON data from the response 30 | const flightInfo = await flightDetails.json(); 31 | 32 | // Return the data as a JSON response 33 | return NextResponse.json(flightInfo); 34 | } catch (error) { 35 | console.error("Error fetching flight details:", error); // Log the error for debugging 36 | return NextResponse.json( 37 | { error: "An error occurred while fetching flight data" }, 38 | { status: 500 } 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/components/SlideHolder.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { Splide, SplideSlide } from "@splidejs/react-splide"; 4 | import "@splidejs/react-splide/css"; 5 | import { classNames } from "../lib/classNames"; 6 | 7 | type Props = { 8 | slides: any; 9 | splideRef: any; 10 | }; 11 | 12 | const options = { 13 | type: "loop", 14 | arrows: false, 15 | direction: "ttb", 16 | height: "100vh", 17 | autoplay: true, 18 | interval: 10000, 19 | speed: 1000, 20 | pagination: false, 21 | }; 22 | 23 | const SlideHolder = ({ slides, splideRef }: Props) => { 24 | return ( 25 | <> 26 | {/* @ts-expect-error Server Component */} 27 | 28 | {slides.map((slide: any, index: number) => { 29 | return ( 30 | 31 |
32 | {slides.map((item: any, index: number) => ( 33 |
40 | {item.stat} 41 |
{item.title}
42 |
43 | ))} 44 |
45 |
46 | ); 47 | })} 48 |
49 | 50 | ); 51 | }; 52 | 53 | export default SlideHolder; 54 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | #splash { 7 | background: #000000; 8 | background-repeat: repeat-y; 9 | position: fixed; 10 | left: 0; 11 | top: 0; 12 | width: 100%; 13 | height: 100%; 14 | animation: splash 3s ease-in; 15 | animation-fill-mode: forwards; 16 | -webkit-animation-fill-mode: forwards; 17 | } 18 | 19 | #loader { 20 | position: absolute; 21 | left: 50%; 22 | top: 0; 23 | transform: translate(-50%, 0); 24 | } 25 | 26 | #loader:after { 27 | content: ''; 28 | position: absolute; 29 | left: 50%; 30 | margin-left: -8px; 31 | bottom: -170px; 32 | width: 3px; 33 | background: #fff; 34 | background: linear-gradient(to bottom, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 1) 50%, rgba(255, 255, 255, 0) 100%); 35 | height: 200px; 36 | } 37 | 38 | #loader:before { 39 | content: ''; 40 | position: absolute; 41 | left: 50%; 42 | margin-left: 8px; 43 | bottom: -190px; 44 | width: 3px; 45 | background: #000; 46 | background: linear-gradient(to bottom, rgba(0, 0, 0, .2) 0%, rgba(0, 0, 0, .2) 50%, rgba(0, 0, 0, 0) 100%); 47 | height: 200px; 48 | } 49 | 50 | #splash .anim { 51 | height: 100%; 52 | position: absolute; 53 | left: 50%; 54 | width: 100px; 55 | transform: translate(-50%, 100%); 56 | animation: loader 4s linear; 57 | animation-fill-mode: forwards; 58 | -webkit-animation-fill-mode: forwards; 59 | } 60 | 61 | @keyframes loader { 62 | 0% { 63 | transform: translate(-50%, 110%); 64 | } 65 | 66 | 30% { 67 | transform: translate(-50%, 50%); 68 | } 69 | 70 | 100% { 71 | transform: translate(-50%, 0%); 72 | } 73 | } 74 | 75 | @keyframes splash { 76 | 0% { 77 | transform: translate(0%, 0%); 78 | } 79 | 80 | 50% { 81 | transform: translate(0%, 0%); 82 | } 83 | 84 | 100% { 85 | transform: translate(0%, -100%); 86 | } 87 | } -------------------------------------------------------------------------------- /src/app/components/PlaneAnimation.tsx: -------------------------------------------------------------------------------- 1 | interface PlaneAnimationProps {} 2 | 3 | export const PlaneAnimation = ({}: PlaneAnimationProps) => { 4 | return ( 5 |
6 |
7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 26 | 27 | 28 | 40 | 41 | 42 |
43 |
44 |
45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useEffect, useRef, useState } from "react"; 3 | import SlideHolder from "./components/SlideHolder"; 4 | import { PlaneAnimation } from "./components/PlaneAnimation"; 5 | import { haversineDistance } from "./lib/haversine"; 6 | 7 | const CENTER_LAT = parseFloat(process.env.NEXT_PUBLIC_CENTER_LAT || "0"); 8 | const CENTER_LON = parseFloat(process.env.NEXT_PUBLIC_CENTER_LON || "0"); 9 | const RADIUS_KM = parseFloat(process.env.NEXT_PUBLIC_RADIUS_KM || "0"); 10 | const LOCAL_AIRPORT_LIST = (process.env.NEXT_PUBLIC_LOCAL_AIRPORT_CODES || "").split(","); 11 | 12 | export default function Home() { 13 | const [statePlaneData, setStatePlaneData] = useState(null); 14 | const [currentTime, setCurrentTime] = useState(""); 15 | const currentCallsign = useRef(""); 16 | const splideRef = useRef(null); 17 | 18 | // Code to determine if we use the origin or destination: 19 | let planeData = statePlaneData?.origin || {} 20 | planeData.whichOne = "Origin" 21 | if (LOCAL_AIRPORT_LIST.includes(statePlaneData?.origin?.iata_code)) { 22 | planeData = statePlaneData?.destination 23 | planeData.whichOne = "Destination" 24 | } 25 | 26 | const planeSlide = [ 27 | { 28 | title: `${planeData.whichOne} City`, 29 | stat: planeData?.municipality, 30 | width: "w-7/12", 31 | }, 32 | { 33 | title: "Airport Code", 34 | stat: planeData?.iata_code, 35 | width: "w-5/12", 36 | }, 37 | ]; 38 | 39 | const slides = [ 40 | { 41 | title: "Current Time", 42 | stat: currentTime, 43 | width: "w-full", 44 | }, 45 | ]; 46 | 47 | const getPlanesAround = async () => { 48 | try { 49 | const planesAround = await fetch("/api/aircraft"); 50 | const response = await planesAround.json(); 51 | 52 | const planeDistances = response.aircraft 53 | .map((plane: any) => { 54 | if (!plane.flight || !plane.lat || !plane.lon) { 55 | return null; 56 | } 57 | const distance = haversineDistance( 58 | CENTER_LAT, 59 | CENTER_LON, 60 | plane.lat, 61 | plane.lon 62 | ); 63 | if (distance > RADIUS_KM) { 64 | return null; 65 | } 66 | return { ...plane, distance }; 67 | }) 68 | .filter((plane: any) => plane !== null); 69 | 70 | if (planeDistances.length === 0) { 71 | setStatePlaneData(null); 72 | return; 73 | } 74 | 75 | try { 76 | const nearestPlaneCallsign = planeDistances[0]?.flight.trim(); 77 | if (currentCallsign.current === nearestPlaneCallsign) { 78 | return; 79 | } 80 | currentCallsign.current = nearestPlaneCallsign; 81 | const flightDetails = await fetch( 82 | `/api/getroute?callsign=${nearestPlaneCallsign}` 83 | ); 84 | const flightInfo = await flightDetails.json(); 85 | 86 | if (flightDetails.ok) { 87 | const flightRoute = flightInfo.response.flightroute; 88 | console.log("Flight route:", flightRoute); 89 | setStatePlaneData(flightRoute); 90 | } else { 91 | console.error("Error fetching flight details:", flightInfo.error); 92 | } 93 | } catch (error) { 94 | console.error("Failed to fetch aircraft data", error); 95 | } 96 | } catch (error) { 97 | console.error("Failed to fetch aircraft data", error); 98 | } 99 | }; 100 | 101 | useEffect(() => { 102 | const timeInterval = setInterval(() => { 103 | setCurrentTime(new Date().toLocaleTimeString()); 104 | }, 1000); 105 | return () => clearInterval(timeInterval); 106 | }, []); 107 | 108 | useEffect(() => { 109 | getPlanesAround(); 110 | // Fetch aircraft data every 10 seconds 111 | const planeInterval = setInterval(() => { 112 | getPlanesAround(); 113 | }, 10000); 114 | return () => clearInterval(planeInterval); 115 | }, []); 116 | 117 | return ( 118 |
119 | {statePlaneData && } 120 | 124 |
125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jetscreen 2 | ![](https://github.com/oliverrees/jetscreen/blob/main/screendemo.gif) 3 | 4 | If you live on a flight path or often have planes flying overhead, you might wonder where in the world they've come from. Jetscreen uses live ADS-B data to identify planes flying overhead, and then looks up the origin via the [adsbdb API](https://www.adsbdb.com/). 5 | 6 | ## Requirements 7 | To run Jetscreen, you'll need the following hardware: 8 | - A Raspberry Pi (tested on a 3B+) 9 | - A monitor (the demo uses [WaveShare's 7.9in display](https://www.waveshare.com/wiki/7.9inch_HDMI_LCD)) 10 | - A USB ADS-B receiver (the demo uses the [FlightAware Pro Stick Plus](https://uk.flightaware.com/adsb/prostick/)) 11 | 12 | ### 1. Set up the Raspberry Pi 13 | Ensure your Raspberry Pi is configured with an operating system like Raspbian, and connected to a monitor and keyboard. You'll also need an internet connection via Wi-Fi or Ethernet. 14 | 15 | First, update the system by running: 16 | ```bash 17 | sudo apt update && sudo apt upgrade -y 18 | ``` 19 | 20 | ### 2. Setup ADS-B Receiver 21 | Follow the instructions for your specific USB ADS-B receiver (like the [FlightAware Pro Stick Plus](https://uk.flightaware.com/adsb/prostick/)) to install the required software. This should expose a local URL where you can access the live ADS-B data stream (for the FlightAware Pro Stick Plus, it's `http://[local IP]:8080/data/aircraft.json`). 22 | 23 | 24 | ### 3. Install Node.js and npm 25 | Jetscreen is built on Next.js, which requires Node.js and npm to run. Install them using the following commands: 26 | 27 | ```bash 28 | sudo apt install nodejs npm -y 29 | ``` 30 | 31 | To verify the installation: 32 | ```bash 33 | node -v 34 | npm -v 35 | ``` 36 | 37 | Ensure you have the latest versions. 38 | 39 | ### 4. Clone the Jetscreen Repository 40 | Clone the **Jetscreen** repository from GitHub: 41 | 42 | ```bash 43 | git clone https://github.com/oliverrees/jetscreen-v2.git 44 | ``` 45 | 46 | Navigate into the project folder: 47 | ```bash 48 | cd jetscreen-v2 49 | ``` 50 | 51 | ### 5. Install Dependencies 52 | Run the following command to install the necessary packages: 53 | 54 | ```bash 55 | npm install 56 | ``` 57 | 58 | ### 6. Create the `.env.local` File 59 | Inside the root directory of the project, create a `.env.local` file to store environment variables: 60 | 61 | ```bash 62 | touch .env.local 63 | ``` 64 | 65 | Edit this file to include the following variables: 66 | ```bash 67 | LOCAL_ADSB_URL=http://192.168.1.100:8080/data/aircraft.json 68 | NEXT_PUBLIC_FLIGHT_DETAILS_URL=https://api.adsbdb.com/v0/callsign/ 69 | NEXT_PUBLIC_CENTER_LAT=51.47674088740635 70 | NEXT_PUBLIC_CENTER_LON=-0.23339838187103154 71 | NEXT_PUBLIC_RADIUS_KM=2 72 | NEXT_PUBLIC_LOCAL_AIRPORT_CODES=YVR,YYX 73 | ``` 74 | 75 | #### Explanation of Environment Variables: 76 | - `LOCAL_ADSB_URL`: The URL of your local ADS-B data stream (replace with your actual IP and port). 77 | - `NEXT_PUBLIC_FLIGHT_DETAILS_URL`: The adsbdb API URL to fetch flight details by callsign - [hexdb.io](https://hexdb.io) is another option. 78 | - `NEXT_PUBLIC_CENTER_LAT`: The latitude of the center point of the map where you're tracking planes (replace with your location). 79 | - `NEXT_PUBLIC_CENTER_LON`: The longitude of the center point (replace with your location). 80 | - `NEXT_PUBLIC_RADIUS_KM`: The radius (in kilometers) for which you want to track planes around your center point. 81 | - `NEXT_PUBLIC_LOCAL_AIRPORT_CODES`: (Optional) The IATA codes for the local airports near you, as a comma separated list. If the flight origin is your local airport's IATA code, the Destination information will be displayed instead. 82 | 83 | ### 7. Start the Development Server 84 | To start the server in development mode, run the following command: 85 | 86 | ```bash 87 | npm run dev 88 | ``` 89 | 90 | This will start the server, and you can view Jetscreen by navigating to `http://localhost:3000` in your browser. 91 | 92 | ### 8. Optional: Run Jetscreen in Production 93 | For production mode, first build the app: 94 | 95 | ```bash 96 | npm run build 97 | ``` 98 | 99 | Then, start the production server: 100 | 101 | ```bash 102 | npm start 103 | ``` 104 | 105 | Now, you can access Jetscreen from any device on the same network by navigating to the Raspberry Pi's IP address and port `3000` in a browser. 106 | 107 | 108 | ### 9. Set up the browser on the Raspberry Pi 109 | To run Jetscreen in full-screen mode on the Raspberry Pi, you can use a browser like Chromium. Install it using the following command: 110 | 111 | ```bash 112 | sudo apt install chromium-browser -y 113 | ``` 114 | 115 | To open Jetscreen in full-screen mode, run: 116 | 117 | ```bash 118 | chromium-browser --kiosk http://localhost:3000 119 | ``` 120 | 121 | 122 | 123 | 124 | 125 | 126 | --------------------------------------------------------------------------------