diff --git a/.env.example b/.env.example index 29a42fde..a9feeb93 100644 --- a/.env.example +++ b/.env.example @@ -17,6 +17,7 @@ MAPBOX_GEOCODING_API="https://api.mapbox.com/geocoding/v5/mapbox.places/" OSEM_API_URL="https://api.opensensemap.org/" DIRECTUS_URL="https://coelho.opensensemap.org" SENSORWIKI_API_URL="https://api.sensors.wiki/" +INGRESS_DOMAIN="ingress.opensensemap.org" MYBADGES_API_URL = "https://api.v2.mybadges.org/" MYBADGES_URL = "https://mybadges.org/" diff --git a/app/components/mydevices/dt/columns.tsx b/app/components/mydevices/dt/columns.tsx index b61314e4..dbba49c0 100644 --- a/app/components/mydevices/dt/columns.tsx +++ b/app/components/mydevices/dt/columns.tsx @@ -1,59 +1,59 @@ -"use client"; +'use client' -import { type ColumnDef } from "@tanstack/react-table"; -import { ArrowUpDown, ClipboardCopy, Ellipsis } from "lucide-react"; -import { Button } from "@/components/ui/button"; +import { type ColumnDef } from '@tanstack/react-table' +import { ArrowUpDown, ClipboardCopy, Ellipsis } from 'lucide-react' +import { Button } from '@/components/ui/button' import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "~/components/ui/dropdown-menu"; -import { type Device } from "~/schema"; + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '~/components/ui/dropdown-menu' +import { type Device } from '~/schema' export type SenseBox = { - id: string; - name: string; - exposure: Device["exposure"]; - // model: string; -}; + id: string + name: string + exposure: Device['exposure'] + // model: string; +} -const colStyle = "pl-0 dark:text-white"; +const colStyle = 'pl-0 dark:text-white' export const columns: ColumnDef[] = [ - { - accessorKey: "name", - header: ({ column }) => { - return ( - - ); - }, - }, - { - accessorKey: "exposure", - header: ({ column }) => { - return ( - - ); - }, - }, - /* { + { + accessorKey: 'name', + header: ({ column }) => { + return ( + + ) + }, + }, + { + accessorKey: 'exposure', + header: ({ column }) => { + return ( + + ) + }, + }, + /* { accessorKey: "model", header: ({ column }) => { return ( @@ -68,76 +68,79 @@ export const columns: ColumnDef[] = [ ); }, }, */ - { - accessorKey: "id", - header: () =>
Sensebox ID
, - cell: ({ row }) => { - const senseBox = row.original; + { + accessorKey: 'id', + header: () =>
Sensebox ID
, + cell: ({ row }) => { + const senseBox = row.original - return ( - //
-
- - {senseBox?.id} - - navigator.clipboard.writeText(senseBox?.id)} - className="ml-[6px] mr-1 inline-block h-4 w-4 align-text-bottom text-[#818a91] dark:text-white cursor-pointer" - /> -
- ); - }, - }, - { - id: "actions", - header: () =>
Actions
, - cell: ({ row }) => { - const senseBox = row.original; + return ( + //
+
+ + {senseBox?.id} + + navigator.clipboard.writeText(senseBox?.id)} + className="ml-[6px] mr-1 inline-block h-4 w-4 cursor-pointer align-text-bottom text-[#818a91] dark:text-white" + /> +
+ ) + }, + }, + { + id: 'actions', + header: () =>
Actions
, + cell: ({ row }) => { + const senseBox = row.original - return ( - - - - - - Actions - - - Overview - - - Show on map - - - Edit - - - Data upload - - - - Support - - - navigator.clipboard.writeText(senseBox?.id)} - className="cursor-pointer" - > - Copy ID - - - - ); - }, - }, -]; + return ( + + + + + + Actions + + + Overview + + + Script + + + Show on map + + + Edit + + + Data upload + + + + Support + + + navigator.clipboard.writeText(senseBox?.id)} + className="cursor-pointer" + > + Copy ID + + + + ) + }, + }, +] diff --git a/app/routes/api.boxes.$deviceId.script.ts b/app/routes/api.boxes.$deviceId.script.ts new file mode 100644 index 00000000..2d01952a --- /dev/null +++ b/app/routes/api.boxes.$deviceId.script.ts @@ -0,0 +1,113 @@ +import SketchTemplater from '@sensebox/sketch-templater' + +import { + type ActionFunction, + type ActionFunctionArgs, + type LoaderFunction, + type LoaderFunctionArgs, +} from 'react-router' +import { getDevice } from '~/models/device.server' + +const cfg = { + 'sketch-templater': { + // Ingress domain. Used in the generation of Arduino sketches + // No default + ingress_domain: process.env.INGRESS_DOMAIN || 'ingress.opensensemap.org', + }, +} +const templateSketcher = new SketchTemplater(cfg) +type Box = NonNullable>> +type BoxForSketch = Box & { + _id: string + sensors: Array +} + +const buildBoxForSketch = ( + box: Box, + formEntries: Record, +): BoxForSketch => ({ + ...box, + _id: box.id, + sensors: box.sensors.map((sensor) => ({ + ...sensor, + _id: sensor.id, + })), + ...formEntries, +}) + +const handleSketch = async ( + deviceId: string | undefined, + formEntries: Record, +): Promise => { + if (deviceId === undefined) { + return Response.json( + { code: 'Bad Request', message: 'Invalid device id specified' }, + { + status: 400, + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + }, + ) + } + + const box = await getDevice({ id: deviceId }) + if (!box) { + return Response.json( + { code: 'Not Found', message: 'Device not found' }, + { + status: 404, + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + }, + ) + } + + const boxForSketch = buildBoxForSketch(box, formEntries) + const encoding = '' + return templateSketcher.generateSketch(boxForSketch, { encoding }) +} + +export const loader: LoaderFunction = async ({ + request, + params, +}: LoaderFunctionArgs): Promise => { + try { + const url = new URL(request.url) + const formEntries = Object.fromEntries( + url.searchParams.entries(), + ) as Record + + return handleSketch(params.deviceId, formEntries) + } catch (err: any) { + return Response.json( + { + code: 'Internal Server Error', + message: err.message || 'An unexpected error occurred', + }, + { + status: 500, + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + }, + ) + } +} + +export const action: ActionFunction = async ({ + request, + params, +}: ActionFunctionArgs): Promise => { + try { + const formData = await request.formData() + const formEntries = Object.fromEntries(formData.entries()) + return handleSketch(params.deviceId, formEntries) + } catch (err: any) { + return Response.json( + { + code: 'Internal Server Error', + message: err.message || 'An unexpected error occurred', + }, + { + status: 500, + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + }, + ) + } +} diff --git a/app/routes/device.$deviceId.script.tsx b/app/routes/device.$deviceId.script.tsx new file mode 100644 index 00000000..15784d73 --- /dev/null +++ b/app/routes/device.$deviceId.script.tsx @@ -0,0 +1,290 @@ +import { ArrowLeft } from 'lucide-react' +import { useState } from 'react' +import { + redirect, + Link, + useLoaderData, + Form, + type LoaderFunctionArgs, +} from 'react-router' +import { useTranslation } from 'react-i18next' +import ErrorMessage from '~/components/error-message' +import { NavBar } from '~/components/nav-bar' +import { Button } from '~/components/ui/button' +import { Textarea } from '~/components/ui/textarea' +import { getDeviceWithoutSensors } from '~/models/device.server' +import { getSensorsFromDevice } from '~/models/sensor.server' +import { getUserId } from '~/utils/session.server' + +//***************************************************** +export async function loader({ request, params }: LoaderFunctionArgs) { + //* if user is not logged in, redirect to home + const userId = await getUserId(request) + if (!userId) return redirect('/') + + if (!params.deviceId) { + throw new Response('Device not found', { status: 502 }) + } + //* get device data + const deviceData = await getDeviceWithoutSensors({ id: params.deviceId }) + //* get sensors data + const sensorsData = await getSensorsFromDevice(params.deviceId) + + return { deviceData, sensorsData } +} + +//***************************************************** +export async function action() { + return {} +} + +//********************************** +export default function DeviceOnverview() { + const { deviceData } = useLoaderData() + const [sketch, setSketch] = useState(String || null) + const { t } = useTranslation('script') + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault() + if (!deviceData?.id) return + + const formData = new FormData(event.currentTarget) + + const enableDebug = (formData.get('enable_debug') !== null).toString() + const displayEnabled = (formData.get('display_enabled') !== null).toString() + + const params = new URLSearchParams() + for (const [key, value] of formData.entries()) { + if (typeof value === 'string') params.append(key, value) + } + + params.set('enable_debug', enableDebug) + params.set('display_enabled', displayEnabled) + + const response = await fetch( + `/api/boxes/${deviceData.id}/script?${params.toString()}`, + { method: 'GET' }, + ) + + const text = await response.text() + setSketch(text) + } + + return ( +
+ +
+
+ + {' '} + + {t('back_to_dashboard')} + +
+
+
+ {/* Heading */} +
+ {/* Title */} +
+
+ + {t('configuration')} + +
+
+
+ + {/* divider */} +
+ +
+ {/* */} + {/* PORT */} +
+ + +
+ +
+
+ + {/* Soil moisture & temp port */} +
+ + +
+ +
+
+ + {/* sound port */} +
+ + +
+ +
+
+ + {/* Windspeed port */} +
+ + +
+ +
+
+ + {/* WIFI SSID */} +
+ + +
+ +
+
+ + {/* WIFI PAasword */} +
+ + +
+ +
+
+
+ + + +
+ + + + {/* */} +
+ +
+ + {t('your_sketch')} + +