prisma migration and fix dropdowns
This commit is contained in:
@ -1,25 +1,32 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
// 1. Define the database connection
|
||||
datasource db {
|
||||
provider = "postgresql" // Or "mysql" if you decide to switch
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// 2. Define the Prisma Client generator
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
// 3. Define your data models
|
||||
|
||||
model Device {
|
||||
id String @id @default(uuid())
|
||||
imei String @unique
|
||||
name String?
|
||||
telemetry Telemetry[]
|
||||
@@map("devices")
|
||||
}
|
||||
|
||||
model CommandTemplate {
|
||||
id Int @id @default(autoincrement())
|
||||
name String // e.g., "Lights On", "Reboot Device"
|
||||
type String // e.g., "lights", "reboot"
|
||||
payload Json // The JSON payload, e.g., {"on": true}
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
commands Command[]
|
||||
}
|
||||
|
||||
model Telemetry {
|
||||
id String @id @default(uuid())
|
||||
deviceId String
|
||||
device Device @relation(fields: [deviceId], references [id])
|
||||
recordedAt DateTime @default(now)
|
||||
device Device @relation(fields: [deviceId], references: [id])
|
||||
recordedAt DateTime @default(now())
|
||||
lat Float
|
||||
lng Float
|
||||
altitude Float?
|
||||
@ -29,274 +36,22 @@ model Telemetry {
|
||||
battery Float?
|
||||
isCarOn Boolean?
|
||||
raw Json?
|
||||
@@map("telemetry")
|
||||
}
|
||||
|
||||
model Command {
|
||||
id Int @id @default(autoincrement())
|
||||
deviceId String
|
||||
device Device @relation(fields: [deviceId], references [id])
|
||||
device Device @relation(fields: [deviceId], references: [id])
|
||||
type String
|
||||
payload Json
|
||||
createdAt DateTime @default(now())
|
||||
@@map("commands")
|
||||
}iniclude <HardwareSerial.h>
|
||||
|
||||
// Pins
|
||||
#define LTE_RX_PIN 16 // ESP32 RX <- SIM7080 TX
|
||||
#define LTE_TX_PIN 17 // ESP32 TX -> SIM7080 RX
|
||||
#define PWRKEY_PIN 4 // SIM7080 PWRKEY
|
||||
|
||||
// Network and endpoint
|
||||
const char* APN = "hologram";
|
||||
const char* BASE_URL = "http://laravel-server.lab.audasmedia.com.au";
|
||||
const char* PATH = "/api/gps";
|
||||
|
||||
// -------- Utilities --------
|
||||
String sendAT(const String& cmd, uint32_t timeoutMs) {
|
||||
Serial.print(">> "); Serial.println(cmd);
|
||||
while (Serial2.available()) (void)Serial2.read();
|
||||
Serial2.println(cmd);
|
||||
|
||||
String resp;
|
||||
uint32_t t0 = millis();
|
||||
while (millis() - t0 < timeoutMs) {
|
||||
while (Serial2.available()) {
|
||||
char c = (char)Serial2.read();
|
||||
resp += c;
|
||||
Serial.print(c);
|
||||
t0 = millis(); // extend timeout on input
|
||||
}
|
||||
}
|
||||
resp.trim();
|
||||
Serial.println("\n[Resp]\n" + resp);
|
||||
return resp;
|
||||
}
|
||||
|
||||
void powerOnSIM7080() {
|
||||
pinMode(PWRKEY_PIN, OUTPUT);
|
||||
digitalWrite(PWRKEY_PIN, LOW);
|
||||
delay(2000);
|
||||
digitalWrite(PWRKEY_PIN, HIGH);
|
||||
delay(1000);
|
||||
digitalWrite(PWRKEY_PIN, LOW);
|
||||
delay(30000); // full boot
|
||||
}
|
||||
|
||||
bool waitLTE(uint32_t ms = 120000) {
|
||||
uint32_t t0 = millis();
|
||||
while (millis() - t0 < ms) {
|
||||
String r = sendAT("AT+CEREG?", 4000);
|
||||
if (r.indexOf("+CEREG: 0,1") != -1 || r.indexOf("+CEREG: 0,5") != -1) return true;
|
||||
delay(3000);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Simple reusable HTTP POST via SH client
|
||||
bool httpPostSH(const String& baseUrl, const String& path, const String& jsonBody) {
|
||||
// Configure HTTP client
|
||||
sendAT("AT+SHCONF=\"URL\",\"" + baseUrl + "\"", 5000);
|
||||
sendAT("AT+SHCONF=\"BODYLEN\",1024", 2000);
|
||||
sendAT("AT+SHCONF=\"HEADERLEN\",350", 2000);
|
||||
|
||||
// Connect (no micromanagement; rely on OK)
|
||||
String r = sendAT("AT+SHCONN", 15000);
|
||||
if (r.indexOf("OK") == -1) {
|
||||
Serial.println("SHCONN failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clear and set headers
|
||||
sendAT("AT+SHCHEAD", 2000);
|
||||
sendAT("AT+SHAHEAD=\"Content-Type\",\"application/json\"", 2000);
|
||||
|
||||
// Send body
|
||||
String cmd = "AT+SHBOD=" + String(jsonBody.length()) + ",10000";
|
||||
sendAT(cmd, 2000);
|
||||
Serial2.print(jsonBody);
|
||||
delay(200);
|
||||
|
||||
// POST (3 = POST)
|
||||
r = sendAT("AT+SHREQ=\"" + path + "\",3", 20000);
|
||||
// Expect URC like: +SHREQ: "POST",<HTTP_CODE>,<len>
|
||||
// Print status line if present
|
||||
int p = r.indexOf("+SHREQ:");
|
||||
if (p != -1) {
|
||||
Serial.println("Status/URC: " + r.substring(p));
|
||||
}
|
||||
|
||||
// If there is a body, try to read (non-fatal if none)
|
||||
sendAT("AT+SHREAD=0,2048", 4000);
|
||||
|
||||
// Disconnect
|
||||
sendAT("AT+SHDISC", 3000);
|
||||
return true;
|
||||
}
|
||||
|
||||
// -------- Arduino setup/loop --------
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Serial2.begin(9600, SERIAL_8N1, LTE_RX_PIN, LTE_TX_PIN);
|
||||
Serial.println("--- SIM7080: Simple Connect + HTTP POST ---");
|
||||
|
||||
powerOnSIM7080();
|
||||
|
||||
// Basic checks
|
||||
sendAT("AT", 2000);
|
||||
sendAT("AT+CMEE=1", 2000);
|
||||
sendAT("AT+CPIN?", 2000);
|
||||
sendAT("AT+CSQ", 2000);
|
||||
|
||||
// Force Cat‑M1 + APN attach
|
||||
sendAT("AT+CNMP=38", 5000);
|
||||
sendAT("AT+CMNB=1", 5000);
|
||||
sendAT("AT+CGDCONT=1,\"IP\",\"" + String(APN) + "\"", 5000);
|
||||
sendAT("AT+CGATT=1", 10000);
|
||||
|
||||
// Wait for LTE registration
|
||||
if (!waitLTE()) {
|
||||
Serial.println("No LTE registration; stopping.");
|
||||
while (true) delay(1000);
|
||||
}
|
||||
|
||||
// Activate SIM7080 APP PDP (required for HTTP)
|
||||
sendAT("AT+CNCFG=0,1,\"" + String(APN) + "\"", 5000);
|
||||
sendAT("AT+CNACT=0,1", 45000);
|
||||
sendAT("AT+CNACT?", 3000);
|
||||
|
||||
smsSetup();
|
||||
|
||||
// Optional DNS
|
||||
sendAT("AT+CDNSCFG=\"8.8.8.8\",\"1.1.1.1\"", 3000);
|
||||
|
||||
// Build your JSON (adjust as needed)
|
||||
String json = "{\"device_id\":\"sim7080g-01\",\"lat\":-33.865143,"
|
||||
"\"lng\":151.2099,\"speed\":12.5,\"altitude\":30.2}";
|
||||
|
||||
// Do the POST
|
||||
bool ok = httpPostSH(BASE_URL, PATH, json);
|
||||
Serial.println(ok ? "HTTP POST sent (see status above)" : "HTTP POST failed");
|
||||
|
||||
Serial.println("--- Done ---");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Non-blocking read to capture unsolicited lines like +CMTI
|
||||
static String urc;
|
||||
while (Serial2.available()) {
|
||||
char c = (char)Serial2.read();
|
||||
urc += c;
|
||||
if (c == '\n') {
|
||||
urc.trim();
|
||||
if (urc.startsWith("+CMTI:")) {
|
||||
// +CMTI: "SM",<idx>
|
||||
int comma = urc.lastIndexOf(',');
|
||||
if (comma != -1) {
|
||||
int idx = urc.substring(comma + 1).toInt();
|
||||
String body = smsReadAndDelete(idx);
|
||||
Serial.println("SMS (URC): " + body);
|
||||
handleSmsCommand(body);
|
||||
}
|
||||
}
|
||||
urc = "";
|
||||
}
|
||||
}
|
||||
|
||||
// Optional periodic poll in case URC was missed
|
||||
static uint32_t lastPoll = 0;
|
||||
if (millis() - lastPoll > 60000) { // every 60 s
|
||||
smsPollUnread();
|
||||
lastPoll = millis();
|
||||
}
|
||||
|
||||
// Your normal work here...
|
||||
}
|
||||
|
||||
// Call once after PDP is active (or right after registration)
|
||||
void smsSetup() {
|
||||
sendAT("AT+CMGF=1", 2000); // text mode
|
||||
sendAT("AT+CPMS=\"SM\",\"SM\",\"SM\"", 2000); // SIM storage
|
||||
sendAT("AT+CNMI=2,1,0,0,0", 2000); // new SMS URC: +CMTI: "SM",<idx>
|
||||
// Optional: show extended text params
|
||||
// sendAT("AT+CSDH=1", 2000);
|
||||
}
|
||||
|
||||
// Read & delete one SMS by index; returns the body
|
||||
String smsReadAndDelete(int idx) {
|
||||
String r = sendAT("AT+CMGR=" + String(idx), 4000);
|
||||
// Extract last non-empty line before OK
|
||||
int okPos = r.lastIndexOf("\nOK");
|
||||
String s = (okPos>0) ? r.substring(0, okPos) : r;
|
||||
int lastLf = s.lastIndexOf('\n');
|
||||
if (lastLf != -1) s = s.substring(lastLf + 1);
|
||||
s.trim();
|
||||
|
||||
sendAT("AT+CMGD=" + String(idx), 2000);
|
||||
return s;
|
||||
}
|
||||
|
||||
// Optional: poll unread SMS (if you want to scan periodically)
|
||||
void smsPollUnread() {
|
||||
String list = sendAT("AT+CMGL=\"REC UNREAD\"", 5000);
|
||||
// list may contain many entries like:
|
||||
// +CMGL: <idx>,"REC UNREAD",...
|
||||
int pos = 0;
|
||||
while (true) {
|
||||
int p = list.indexOf("+CMGL:", pos);
|
||||
if (p == -1) break;
|
||||
int comma = list.indexOf(',', p);
|
||||
if (comma == -1) break;
|
||||
// Extract index after "+CMGL: "
|
||||
int idxStart = list.indexOf(' ', p);
|
||||
if (idxStart == -1 || idxStart > comma) break;
|
||||
int idx = list.substring(idxStart + 1, comma).toInt();
|
||||
String body = smsReadAndDelete(idx);
|
||||
Serial.println("SMS (polled): " + body);
|
||||
handleSmsCommand(body);
|
||||
pos = comma + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void handleSmsCommand(String msg) {
|
||||
Serial.println("Command received via SMS: " + msg);
|
||||
|
||||
msg.trim();
|
||||
if (msg.isEmpty()) {
|
||||
Serial.println("Empty body");
|
||||
return;
|
||||
}
|
||||
|
||||
// Very simple JSON extraction: look for device_id and speed keys
|
||||
auto extract = [&](const String& key)->String {
|
||||
String pat = String("\"") + key + "\":";
|
||||
int k = msg.indexOf(pat);
|
||||
if (k < 0) return "";
|
||||
k += pat.length();
|
||||
|
||||
// Skip quotes/spaces
|
||||
while (k < (int)msg.length() && (msg[k]==' ' || msg[k]=='\"')) k++;
|
||||
|
||||
// If quoted value
|
||||
if (k < (int)msg.length() && msg[k-1] == '\"') {
|
||||
int end = msg.indexOf('\"', k);
|
||||
if (end > k) return msg.substring(k, end);
|
||||
}
|
||||
|
||||
// Numeric value
|
||||
int end = k;
|
||||
while (end < (int)msg.length() && (isDigit(msg[end]) || msg[end]=='.' || msg[end]=='-' )) end++;
|
||||
return msg.substring(k, end);
|
||||
};
|
||||
|
||||
String deviceId = extract("device_id");
|
||||
String speed = extract("speed");
|
||||
|
||||
Serial.println("Parsed device_id=" + deviceId + " speed=" + speed);
|
||||
|
||||
// Example action: if speed present
|
||||
if (speed.length()) {
|
||||
Serial.println("Action: set speed to " + speed);
|
||||
// TODO: apply speed to your app logic
|
||||
}
|
||||
model CommandTemplate {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
type String
|
||||
payload Json
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
@ -62,17 +62,24 @@ export function CommandManager({ selectedImei }: CommandManagerProps) {
|
||||
placeholder="Template Name (e.g., Lights On)"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="p-2 text-white rounded-md bg-white/10"
|
||||
className="p-2 text-white rounded-md bg-gray-800 border border-gray-600"
|
||||
/>
|
||||
<select
|
||||
value={type}
|
||||
onChange={(e) => setType(e.target.value as DeviceCommandType)}
|
||||
className="p-2 text-white rounded-md bg-white/10"
|
||||
>
|
||||
<option value="reboot">Reboot Device</option>
|
||||
<option value="lights">Lights On/Off</option>
|
||||
{/* Add other types */}
|
||||
</select>
|
||||
|
||||
<select
|
||||
value={type} // This value is tied to the 'type' state
|
||||
onChange={(e) => setType(e.target.value as DeviceCommandType)} // This updates the state when you select an option
|
||||
className="p-2 text-white rounded-md bg-gray-800 border border-gray-600"
|
||||
>
|
||||
<option value="reboot" className="bg-gray-800 text-white">Reboot Device</option>
|
||||
<option value="lights" className="bg-gray-800 text-white">Lights On/Off</option>
|
||||
<option value="camera" className="bg-gray-800 text-white">Camera On/Off</option>
|
||||
<option value="sleep" className="bg-gray-800 text-white">Set Sleep Interval</option>
|
||||
<option value="telemetry_sec" className="bg-gray-800 text-white">Set Telemetry Interval</option>
|
||||
<option value="poll_sec" className="bg-gray-800 text-white">Set Poll Interval</option>
|
||||
<option value="wifi" className="bg-gray-800 text-white">Set WiFi Credentials</option>
|
||||
<option value="ring_fence" className="bg-gray-800 text-white">Set Geo-Fence</option>
|
||||
<option value="ota" className="bg-gray-800 text-white">Firmware OTA</option>
|
||||
</select>
|
||||
<textarea
|
||||
placeholder='JSON Payload, e.g., {"on": true}'
|
||||
value={payload}
|
||||
@ -90,9 +97,18 @@ export function CommandManager({ selectedImei }: CommandManagerProps) {
|
||||
</div>
|
||||
|
||||
{/* List of saved templates */}
|
||||
<div className="flex flex-col gap-2 p-4 rounded-md bg-white/10">
|
||||
// In file: src/app/_components/CommandManager.tsx
|
||||
|
||||
{/* List of saved templates */}
|
||||
<div className="flex flex-col gap-2 p-4 rounded-md bg-white/10">
|
||||
<h3 className="text-lg font-bold">Saved Commands</h3>
|
||||
{templatesQuery.isLoading && <p>Loading templates...</p>}
|
||||
|
||||
{/* ADD THIS CHECK for an empty list */}
|
||||
{templatesQuery.data && templatesQuery.data.length === 0 && (
|
||||
<p className="text-gray-400">No templates saved. Create one on the left!</p>
|
||||
)}
|
||||
|
||||
{templatesQuery.data?.map((template) => (
|
||||
<div key={template.id} className="flex items-center justify-between p-2 rounded-md bg-black/20">
|
||||
<div>
|
||||
@ -115,7 +131,7 @@ export function CommandManager({ selectedImei }: CommandManagerProps) {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,13 +1,12 @@
|
||||
// In file: src/components/DeviceMapClient.tsx
|
||||
// In file: src/app/_components/DynamicDeviceMap.tsx
|
||||
"use client";
|
||||
|
||||
// This file now contains your DeviceMap component, ensuring it's always a client component.
|
||||
import { useState } from "react";
|
||||
import { MapContainer, TileLayer, Marker, Polyline, useMapEvents } from "react-leaflet";
|
||||
import { useState, useEffect } from "react";
|
||||
import { MapContainer, TileLayer, Marker, Polyline, useMap, useMapEvents } from "react-leaflet";
|
||||
import L from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
// Fix default Leaflet icon issue in React
|
||||
// Fix default Leaflet icon paths
|
||||
delete (L.Icon.Default as any)._getIconUrl;
|
||||
L.Icon.Default.mergeOptions({
|
||||
iconRetinaUrl: '/leaflet/marker-icon-2x.png',
|
||||
@ -15,15 +14,23 @@ L.Icon.Default.mergeOptions({
|
||||
shadowUrl: '/leaflet/marker-shadow.png',
|
||||
});
|
||||
|
||||
// Helper component that forces the map to update its view
|
||||
function MapViewUpdater({ center }: { center: L.LatLngExpression }) {
|
||||
const map = useMap();
|
||||
useEffect(() => {
|
||||
// When the 'center' prop changes, fly the map to the new location
|
||||
map.flyTo(center, map.getZoom());
|
||||
}, [center, map]);
|
||||
return null;
|
||||
}
|
||||
|
||||
interface MapProps {
|
||||
interface FenceMapProps {
|
||||
latestGPS?: { lat: number; lng: number };
|
||||
onFenceChange: (fence: Array<{ lat: number; lng: number }>) => void;
|
||||
}
|
||||
|
||||
const FenceMap = ({ latestGPS, onFenceChange }: MapProps) => {
|
||||
const FenceMap = ({ latestGPS, onFenceChange }: FenceMapProps) => {
|
||||
const [fence, setFence] = useState<Array<{ lat: number; lng: number }>>([]);
|
||||
|
||||
useMapEvents({
|
||||
click: (e) => {
|
||||
const newFence = [...fence, { lat: e.latlng.lat, lng: e.latlng.lng }];
|
||||
@ -31,7 +38,6 @@ const FenceMap = ({ latestGPS, onFenceChange }: MapProps) => {
|
||||
onFenceChange(newFence);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{fence.length > 1 && <Polyline pathOptions={{ color: "red" }} positions={fence} />}
|
||||
@ -40,13 +46,18 @@ const FenceMap = ({ latestGPS, onFenceChange }: MapProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const DeviceMap = ({ latestGPS, onFenceChange }: { latestGPS?: { lat: number; lng: number }; onFenceChange: (fence: Array<{ lat: number; lng: number }>) => void; }) => {
|
||||
const center = latestGPS ? [latestGPS.lat, latestGPS.lng] : [-37.76, 145.04]; // Default center
|
||||
export const DeviceMap = ({ latestGPS, onFenceChange }: { latestGPS?: { lat: number; lng: number } | null; onFenceChange: (fence: Array<{ lat: number; lng: number }>) => void; }) => {
|
||||
// Use live GPS if available, otherwise default to a reasonable fallback
|
||||
const center: L.LatLngExpression = latestGPS ? [latestGPS.lat, latestGPS.lng] : [-37.76, 145.04];
|
||||
|
||||
// For debugging, let's see what coordinates this component is receiving
|
||||
console.log("DeviceMap received latestGPS:", latestGPS);
|
||||
|
||||
return (
|
||||
<MapContainer center={center} zoom={13} style={{ height: "300px", width: "100%" }}>
|
||||
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
|
||||
<FenceMap latestGPS={latestGPS} onFenceChange={onFenceChange} />
|
||||
<MapViewUpdater center={center} /> {/* This component will handle map movement */}
|
||||
</MapContainer>
|
||||
);
|
||||
};
|
||||
@ -58,11 +58,15 @@ export default function Dashboard() {
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold">Map & Geo-Fence</h2>
|
||||
<div className="mt-2 rounded-md overflow-hidden">
|
||||
<div className="mt-2 h-[300px] rounded-md overflow-hidden bg-gray-800 flex items-center justify-center">
|
||||
{telemetry ? (
|
||||
<DeviceMap latestGPS={telemetry} onFenceChange={setFence} />
|
||||
) : (
|
||||
<p className="text-gray-400">Waiting for GPS data...</p>
|
||||
)}
|
||||
</div>
|
||||
<p>Geo-fence points: {fence.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-6">
|
||||
|
||||
Reference in New Issue
Block a user