├── .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 | 
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 |
--------------------------------------------------------------------------------