add floor plan

This commit is contained in:
Yan Zhou 2025-06-12 18:53:02 +02:00
parent 5935064606
commit 5d689e6005
5 changed files with 254 additions and 41 deletions

View file

@ -5,7 +5,30 @@ import type { PageServerLoad } from "./$types";
import { connect } from "mqtt";
export const load: PageServerLoad = async ({ params }) => {
{
// Convert slug to number for floor lookup
const floorNumber = Number(params.slug);
// First check if we have a saved floor configuration in the floors table
const floorData = await db.select({
floor: table.floors.floor,
url: table.floors.url
}).from(table.floors).where(eq(table.floors.floor, floorNumber));
if (floorData.length > 0 && floorData[0].url && floorData[0].url !== "/") {
try {
// Try to parse the saved configuration
const config = JSON.parse(floorData[0].url);
return {
slug: params.slug,
floorConfig: config,
hasConfig: true
};
} catch (e) {
console.error("Error parsing floor configuration:", e);
}
}
// Fallback to the old canvas drawing system
const floor_cnt = await db.select({ floor: table.plans.floor, json: table.plans.plan }).from(table.plans).where(eq(table.plans.floor, params.slug));
if (floor_cnt.length == 0) {
await db.insert(table.plans).values({ floor: params.slug, plan: {
@ -31,7 +54,10 @@ export const load: PageServerLoad = async ({ params }) => {
}});
}
}
const floor_ = await db.select({ floor: table.plans.floor, json: table.plans.plan }).from(table.plans).where(eq(table.plans.floor, params.slug));
return { slug: params.slug, floor: floor_ };
return {
slug: params.slug,
floor: floor_,
hasConfig: false
};
};

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { Thermometer, Droplets, Gauge, Mountain, Cpu } from "@lucide/svelte";
const { data } = $props();
@ -37,7 +38,7 @@
}
});
let canvasEl: HTMLCanvasElement;
let canvasEl = $state<HTMLCanvasElement>();
let ctx;
function drawFloorplan(data) {
@ -81,14 +82,68 @@
ctx.stroke();
});
}
onMount(() => {
if (!data.hasConfig && data.floor?.[0]?.json) {
ctx = canvasEl.getContext("2d");
drawFloorplan(data.floor[0].json);
}
});
</script>
<div style="display: flex; justify-content: center; align-items: center; height: 100%;">
<h1 class="text-center text-4xl font-bold">Floor {data.slug}</h1>
<span>{mqttMessage}</span>
<canvas bind:this={canvasEl} class="obj-contain" width="800%" height="600%"></canvas>
<div class="container mx-auto p-6">
<h1 class="mb-6 text-center text-4xl font-bold">Floor {data.slug}</h1>
{#if data.hasConfig && data.floorConfig}
<!-- Display saved floor plan configuration -->
<div class="relative mx-auto max-w-4xl">
{#if data.floorConfig.image}
<div class="relative overflow-hidden rounded-lg border bg-gray-50">
<img src={data.floorConfig.image} alt="Floor plan" class="h-auto w-full" />
{#if data.floorConfig.devices}
{#each data.floorConfig.devices as device}
<div
class="absolute -translate-x-1/2 -translate-y-1/2 transform rounded-lg border-2 border-blue-500 bg-white p-3 shadow-lg"
style="left: {device.x}%; top: {device.y}%;"
>
<div class="flex items-center gap-2">
<Cpu class="h-5 w-5 text-blue-600" />
<span class="text-xs font-medium">{device.name}</span>
</div>
<div class="mt-2 grid grid-cols-2 gap-1">
<div class="flex items-center gap-1">
<Thermometer class="h-3 w-3 text-orange-500" />
<span class="text-xs">23°C</span>
</div>
<div class="flex items-center gap-1">
<Droplets class="h-3 w-3 text-blue-500" />
<span class="text-xs">45%</span>
</div>
<div class="flex items-center gap-1">
<Gauge class="h-3 w-3 text-green-500" />
<span class="text-xs">1013</span>
</div>
<div class="flex items-center gap-1">
<Mountain class="h-3 w-3 text-purple-500" />
<span class="text-xs">120m</span>
</div>
</div>
</div>
{/each}
{/if}
</div>
<div class="mt-4 rounded-lg bg-gray-100 p-4">
<p class="text-sm text-gray-600">MQTT Status: {mqttMessage}</p>
</div>
{/if}
</div>
{:else}
<!-- Fallback to canvas drawing -->
<div class="flex flex-col items-center">
<span class="mb-4">{mqttMessage}</span>
<canvas bind:this={canvasEl} class="border" width="800" height="600"></canvas>
</div>
{/if}
</div>

View file

@ -4,7 +4,24 @@ import { fail } from "@sveltejs/kit";
import { eq } from "drizzle-orm";
export const load = async (event) => {
return {};
// Fetch all available floors
const floors = await db
.select({ floor: table.floors.floor })
.from(table.floors)
.orderBy(table.floors.floor);
// Provide mock ESP8266 devices for now
const devices = [
{ id: "esp8266-001", name: "ESP8266 #001", type: "esp8266", status: "online" },
{ id: "esp8266-002", name: "ESP8266 #002", type: "esp8266", status: "online" },
{ id: "esp8266-003", name: "ESP8266 #003", type: "esp8266", status: "offline" },
{ id: "esp8266-004", name: "ESP8266 #004", type: "esp8266", status: "online" },
];
return {
floors: floors.map((f) => f.floor),
devices: devices,
};
};
export const actions = {
@ -41,4 +58,49 @@ export const actions = {
await db.delete(table.floors).where(eq(table.floors.floor, n));
},
savefloor: async (event) => {
const formData = await event.request.formData();
const floorNumber = formData.get("floorNumber");
const floorPlanImage = formData.get("floorPlanImage");
const devices = formData.get("devices");
const n = Number(floorNumber);
if (isNaN(n)) return fail(400, { message: "Invalid floor number!" });
if (!floorPlanImage || !devices) {
return fail(400, { message: "Missing floor plan or device configuration!" });
}
try {
const deviceData = JSON.parse(devices);
// Check if floor exists
const exists = await db
.select({ floor: table.floors.floor })
.from(table.floors)
.where(eq(table.floors.floor, n));
if (exists.length === 0) {
return fail(400, { message: `Floor ${n} does not exist! Please create it first.` });
}
// Update floor with configuration
// Note: In a real implementation, you would store this data properly
// For now, we'll just update the url field as a JSON string
const floorConfig = {
image: floorPlanImage,
devices: deviceData,
};
await db
.update(table.floors)
.set({ url: JSON.stringify(floorConfig) })
.where(eq(table.floors.floor, n));
return { success: true, message: `Floor ${n} configuration saved successfully!` };
} catch (error) {
console.error("Error saving floor configuration:", error);
return fail(500, { message: "Failed to save floor configuration!" });
}
},
};

View file

@ -10,19 +10,29 @@
import CardTitle from "$lib/components/ui/card/card-title.svelte";
import { Thermometer, Droplets, Gauge, Mountain, Upload, Cpu } from "@lucide/svelte";
const { form } = $props();
const { form, data } = $props();
let floorPlanImage = $state(null);
let availableDevices = $state([
{ id: "esp8266-1", name: "ESP8266 #1", type: "esp8266", status: "online" },
{ id: "esp8266-2", name: "ESP8266 #2", type: "esp8266", status: "online" },
{ id: "esp8266-3", name: "ESP8266 #3", type: "esp8266", status: "offline" },
{ id: "esp8266-4", name: "ESP8266 #4", type: "esp8266", status: "online" },
]);
let availableDevices = $state(data.devices || []);
let placedDevices = $state([]);
let draggedDevice = $state(null);
let floorPlanRef = $state(null);
let selectedFloorNumber = $state("");
let saveMessage = $state(null);
$effect(() => {
if (data.floors && data.floors.length > 0 && !selectedFloorNumber) {
selectedFloorNumber = data.floors[0].toString();
}
});
$effect(() => {
// Update devices when data changes
if (data.devices) {
availableDevices = data.devices;
}
});
function handleFileUpload(event) {
const file = event.target.files?.[0];
@ -168,6 +178,65 @@
<p class="mt-2 text-sm text-gray-600">
Drag ESP8266 devices from below onto the floor plan to place them in specific rooms.
</p>
{#if floorPlanImage && placedDevices.length > 0}
<div class="mt-4 rounded-lg border bg-blue-50 p-4">
<h4 class="mb-3 font-semibold">Save Floor Configuration</h4>
<form
method="post"
action="?/savefloor"
use:enhance={() => {
return async ({ result }) => {
if (result.type === "success") {
saveMessage = { type: "success", text: result.data.message };
// Clear form after successful save
selectedFloorNumber = "";
} else if (result.type === "failure") {
saveMessage = {
type: "error",
text: result.data?.message || "Failed to save configuration",
};
}
};
}}
>
<input type="hidden" name="floorPlanImage" value={floorPlanImage} />
<input type="hidden" name="devices" value={JSON.stringify(placedDevices)} />
<div class="mb-3 flex items-end gap-4">
<div class="flex-1">
<Label for="floorNumber">Select Floor</Label>
<select
name="floorNumber"
id="floorNumber"
bind:value={selectedFloorNumber}
class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
required
>
{#if data.floors && data.floors.length > 0}
{#each data.floors as floor}
<option value={floor.toString()}>
Floor {floor}
</option>
{/each}
{/if}
</select>
</div>
<Button type="submit" variant="default" disabled={!selectedFloorNumber}>
Save Floor Configuration
</Button>
</div>
</form>
{#if saveMessage}
<p
class="mt-2 text-sm {saveMessage.type === 'success'
? 'text-green-600'
: 'text-red-600'}"
>
{saveMessage.text}
</p>
{/if}
</div>
{/if}
{/if}
<div class="pt-4">

View file

@ -18,6 +18,7 @@ function connectMqtt() {
// Replace with your MQTT broker details
const BROKER_URL = "mqtt://kada49.it:1883"; // Example public broker
//const BROKER_URL = "mqtt://test.mosquitto.org:1883";
const TOPIC = "esp8266/+/data"; // Replace with your desired topic
client = mqtt.connect(BROKER_URL);