Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e4c1e99e1 | ||
|
|
0222f34621 | ||
|
|
97dbdade45 | ||
|
|
4ab27bbabc | ||
|
|
5c7af613ff | ||
|
|
6040ce4c1d | ||
|
|
733e76a4bd | ||
|
|
ff7d0d4ee1 | ||
|
|
60277dfbd7 | ||
|
|
67695e8d47 | ||
|
|
f9a14fa2f1 | ||
|
|
2ae3674869 | ||
|
|
40c7fbda9c | ||
|
|
cbb89defe1 | ||
|
|
b4f66069d2 | ||
|
|
31fc14310b | ||
|
|
0576e1becf | ||
|
|
2f7454147a | ||
|
|
51b06ae868 | ||
|
|
734d1a29ad | ||
|
|
a16e8cc4ea | ||
|
|
d3ced437ec | ||
|
|
691fc3318e | ||
|
|
ccb71ecbd9 | ||
|
|
1d50ec3791 | ||
|
|
368806f5ed | ||
|
|
65e088f8e8 | ||
|
|
657c1db80d | ||
|
|
9cf8ddd921 | ||
|
|
600dfbe2a4 | ||
|
|
5b1c8e83a8 | ||
|
|
52da5fa2cb | ||
|
|
1075c7c7ad |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 556 KiB |
+5
-1
@@ -63,6 +63,10 @@
|
||||
### Completed
|
||||
- camera scripts
|
||||
- adminjs configuration
|
||||
- integrate camera scripts to system
|
||||
|
||||
### In Progress
|
||||
- connect camera scripts to core script and adminjs
|
||||
- ~connect camera scripts to core script and adminjs~
|
||||
|
||||
### Next Steps
|
||||
- Debugging and refactor
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
services:
|
||||
mosquitto:
|
||||
image: eclipse-mosquitto:latest
|
||||
container_name: mosquitto
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "1883:1883"
|
||||
volumes:
|
||||
- ./mosquitto/config:/mosquitto/config
|
||||
- ./mosquitto/data:/mosquitto/data
|
||||
- ./mosquitto/log:/mosquitto/log
|
||||
nodered:
|
||||
image: nodered/node-red:latest
|
||||
container_name: nodered
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "1880:1880"
|
||||
volumes:
|
||||
- nodered_data:/data
|
||||
redis:
|
||||
image: redis:7
|
||||
container_name: redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6379:6379"
|
||||
command: ["redis-server", "/etc/redis/redis.conf"]
|
||||
volumes:
|
||||
- ./redis/data:/data
|
||||
- ./redis/redis.conf:/etc/redis/redis.conf
|
||||
influxdb:
|
||||
image: influxdb:2
|
||||
container_name: influxdb
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8086:8086"
|
||||
volumes:
|
||||
- ./influxdb/data:/var/lib/influxdb2
|
||||
- ./influxdb/config:/etc/influxdb2
|
||||
|
||||
volumes:
|
||||
nodered_data:
|
||||
external: true
|
||||
@@ -1,3 +1,12 @@
|
||||
node_modules
|
||||
.git
|
||||
npm-debug.log
|
||||
|
||||
dist
|
||||
.env*
|
||||
*.log
|
||||
.gitignore
|
||||
README.md
|
||||
*.md
|
||||
.prettierrc*
|
||||
data
|
||||
|
||||
@@ -1,19 +1,75 @@
|
||||
FROM node:24-slim
|
||||
|
||||
# ─── Stage 1: Build ───────────────────────────────────────────────────────────
|
||||
FROM node:24-slim AS builder
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update -y && apt-get install -y openssl
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
openssl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install all deps (including devDependencies for build)
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
# Generate Prisma client (needed for TypeScript types)
|
||||
COPY prisma ./prisma
|
||||
RUN npx prisma generate
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
# Pre-bundle AdminJS components at build time
|
||||
RUN npm run bundle:adminjs && cp .adminjs/components.bundle.js .adminjs/bundle.js
|
||||
|
||||
# Compile TypeScript
|
||||
RUN npm run build
|
||||
|
||||
# ─── Stage 2: Production ──────────────────────────────────────────────────────
|
||||
FROM node:24-slim AS production
|
||||
WORKDIR /app
|
||||
|
||||
# Install native runtime libs
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libdlib19.1 \
|
||||
libjpeg62-turbo \
|
||||
libblas3 \
|
||||
libopenblas0 \
|
||||
libatlas3-base \
|
||||
liblapack3 \
|
||||
openssl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install production deps only
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# Copy compiled JS from builder
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# AdminJS ComponentLoader validates source file paths exist at startup
|
||||
COPY --from=builder /app/src ./src
|
||||
|
||||
# Copy pre-built AdminJS bundle
|
||||
COPY --from=builder /app/.adminjs ./.adminjs
|
||||
|
||||
# Copy pre-built Go binaries
|
||||
COPY --from=builder /app/bin ./bin
|
||||
|
||||
# Copy Prisma schema + generated client
|
||||
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
|
||||
COPY --from=builder /app/prisma ./prisma
|
||||
|
||||
# Set permissions for non-root user
|
||||
RUN chown -R node:node /app/.adminjs /app/dist /app/src /app/bin
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
USER node
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV ADMIN_JS_SKIP_BUNDLE="true"
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
@@ -0,0 +1,44 @@
|
||||
build:
|
||||
docker build -t officesense:latest .
|
||||
|
||||
run:
|
||||
docker run --rm -it --init -p 80:80 \
|
||||
-e ADMIN_EMAIL=admin@officesense.com \
|
||||
-e ADMIN_PASSWORD=supersecret \
|
||||
-e ADMIN_COOKIE_SECRET=some-long-random-string \
|
||||
-e NODE_ENV=production \
|
||||
-e DATABASE_URL=file:/app/data/officesense.sqlite \
|
||||
--device /dev/video0:/dev/video0 \
|
||||
-v $(shell pwd)/data:/app/data \
|
||||
officesense:latest
|
||||
|
||||
dev:
|
||||
docker run --rm -it --init -p 80:80 \
|
||||
-v $(shell pwd):/app \
|
||||
-w /app \
|
||||
-e ADMIN_EMAIL=admin@officesense.com \
|
||||
-e ADMIN_PASSWORD=supersecret \
|
||||
-e ADMIN_COOKIE_SECRET=some-long-random-string \
|
||||
-e NODE_ENV=development \
|
||||
--device /dev/video0:/dev/video0 \
|
||||
node:24-slim \
|
||||
bash -c "apt-get update && apt-get install -y libdlib19.1 libjpeg62-turbo libblas3 libopenblas0 libatlas3-base liblapack3 && node --run dev"
|
||||
|
||||
migrate:
|
||||
docker run --rm \
|
||||
-e DATABASE_URL=file:/app/data/officesense.sqlite \
|
||||
-v $(shell pwd)/data:/app/data \
|
||||
officesense:latest \
|
||||
npx prisma migrate deploy
|
||||
|
||||
sh:
|
||||
docker run --rm -it -v $(shell pwd):/app -w /app node:24-slim sh
|
||||
|
||||
format:
|
||||
docker run --rm -it --init \
|
||||
-v $(shell pwd):/app \
|
||||
-w /app \
|
||||
node:24-slim \
|
||||
bash -c "node --run format"
|
||||
|
||||
.PHONY: build run dev migrate sh format
|
||||
Generated
+31
@@ -11,8 +11,10 @@
|
||||
"dependencies": {
|
||||
"@adminjs/express": "^6.1.1",
|
||||
"@adminjs/prisma": "^5.0.4",
|
||||
"@influxdata/influxdb-client": "^1.35.0",
|
||||
"@prisma/client": "^6.19.3",
|
||||
"adminjs": "^7.8.17",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"express-session": "^1.19.0",
|
||||
"mqtt": "^5.15.1",
|
||||
@@ -20,6 +22,7 @@
|
||||
"redis": "^5.12.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@adminjs/bundler": "^3.0.0",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/express-session": "^1.19.0",
|
||||
"@types/node": "^25.7.0",
|
||||
@@ -28,6 +31,16 @@
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@adminjs/bundler": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@adminjs/bundler/-/bundler-3.0.0.tgz",
|
||||
"integrity": "sha512-s8ItuELPQEXDIEI2jc5TB2iA1jGtNOdZrWrweJvIeE20ORE50m1Ws6uS+bLsAuZCGh6dHOxCWOKFZ/wDQg4TKg==",
|
||||
"dev": true,
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"peerDependencies": {
|
||||
"adminjs": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@adminjs/design-system": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@adminjs/design-system/-/design-system-4.1.1.tgz",
|
||||
@@ -2364,6 +2377,12 @@
|
||||
"react": ">=0.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@influxdata/influxdb-client": {
|
||||
"version": "1.35.0",
|
||||
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client/-/influxdb-client-1.35.0.tgz",
|
||||
"integrity": "sha512-woWMi8PDpPQpvTsRaUw4Ig+nOGS/CWwAwS66Fa1Vr/EkW+NEwxI8YfPBsdBMn33jK2Y86/qMiiuX/ROHIkJLTw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
@@ -4906,6 +4925,18 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.4.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
|
||||
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
|
||||
@@ -8,12 +8,14 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx src/index.ts",
|
||||
"bundle:adminjs": "tsx src/api/management/bundler.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
||||
"format:check": "prettier --check \"src/**/*.{ts,tsx}\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@adminjs/bundler": "^3.0.0",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/express-session": "^1.19.0",
|
||||
"@types/node": "^25.7.0",
|
||||
@@ -24,12 +26,14 @@
|
||||
"dependencies": {
|
||||
"@adminjs/express": "^6.1.1",
|
||||
"@adminjs/prisma": "^5.0.4",
|
||||
"@influxdata/influxdb-client": "^1.35.0",
|
||||
"@prisma/client": "^6.19.3",
|
||||
"adminjs": "^7.8.17",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"express-session": "^1.19.0",
|
||||
"mqtt": "^5.15.1",
|
||||
"prisma": "^6.19.3",
|
||||
"redis": "^5.12.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import AdminJS from "adminjs";
|
||||
import AdminJSExpress from "@adminjs/express";
|
||||
import * as AdminJSPrisma from "@adminjs/prisma";
|
||||
import session from "express-session";
|
||||
import { componentLoader, Components } from "./components.js";
|
||||
import { componentLoader, Components } from "./components.bundler.js";
|
||||
|
||||
import userResource from "./resources/adminjs.user.resource.js";
|
||||
import tagResource from "./resources/adminjs.tag.resource.js";
|
||||
@@ -16,13 +16,14 @@ AdminJS.registerAdapter({
|
||||
async function createAdmin() {
|
||||
const admin = new AdminJS({
|
||||
rootPath: "/admin",
|
||||
resources: [userResource, tagResource, roomResource],
|
||||
branding: { companyName: "OfficeSense", logo: false },
|
||||
dashboard: { component: Components.Dashboard },
|
||||
componentLoader,
|
||||
resources: [userResource, tagResource, roomResource],
|
||||
dashboard: { component: Components.Dashboard },
|
||||
});
|
||||
|
||||
await admin.watch();
|
||||
if (process.env.NODE_ENV === "development") await admin.watch();
|
||||
else await admin.initialize();
|
||||
|
||||
const router = AdminJSExpress.buildAuthenticatedRouter(
|
||||
admin,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { bundle } from "@adminjs/bundler";
|
||||
import { componentLoader } from "./components.bundler.js";
|
||||
|
||||
void (async () => {
|
||||
await bundle({
|
||||
componentLoader,
|
||||
destinationDir: "./.adminjs",
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ComponentLoader } from "adminjs";
|
||||
import path from "path";
|
||||
import * as url from "url";
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
|
||||
export const componentLoader = new ComponentLoader();
|
||||
|
||||
const add = (filePath: string, componentName: string): string =>
|
||||
componentLoader.add(componentName, path.join(__dirname, filePath));
|
||||
|
||||
export const Components = {
|
||||
Dashboard: add("components/Dashboard", "Dashboard"),
|
||||
UploadFace: add("components/UploadFace", "UploadFace"),
|
||||
FaceEmbeddingField: add("components/FaceEmbeddingField", "FaceEmbeddingField"),
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import { ComponentLoader } from "adminjs";
|
||||
import { fileURLToPath } from "url";
|
||||
import path from "path";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export const componentLoader = new ComponentLoader();
|
||||
|
||||
export const Components = {
|
||||
Dashboard: componentLoader.add(
|
||||
"Dashboard",
|
||||
path.join(__dirname, "./components/Dashboard.tsx"),
|
||||
"components"
|
||||
),
|
||||
UploadFace: componentLoader.add(
|
||||
"UploadFace",
|
||||
path.join(__dirname, "./components/UploadFace.tsx"),
|
||||
"components"
|
||||
),
|
||||
FaceEmbeddingField: componentLoader.add(
|
||||
"FaceEmbeddingField",
|
||||
path.join(__dirname, "./components/FaceEmbeddingField.tsx"),
|
||||
"components"
|
||||
),
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { BasePropertyProps } from "adminjs";
|
||||
import type { BasePropertyProps } from "adminjs";
|
||||
import { Label } from "@adminjs/design-system";
|
||||
|
||||
const FaceEmbeddingField: React.FC<BasePropertyProps> = ({ record, property }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import { Box, H3, Text, Button, MessageBox } from "@adminjs/design-system";
|
||||
import { ActionProps, useRecord } from "adminjs";
|
||||
import type { ActionProps } from "adminjs";
|
||||
|
||||
const UploadFace: React.FC<ActionProps> = ({ record, action }) => {
|
||||
const [preview, setPreview] = useState<string | null>(null);
|
||||
@@ -18,7 +18,7 @@ const UploadFace: React.FC<ActionProps> = ({ record, action }) => {
|
||||
reader.onload = () => {
|
||||
const result = reader.result as string;
|
||||
setPreview(result);
|
||||
setBase64(result.split(",")[1]); // strip data:image/jpeg;base64,
|
||||
setBase64(result.split(",")[1] ?? null); // strip data:image/jpeg;base64,
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { prisma } from "../../../lib/prisma.js";
|
||||
import { getModelByName } from "@adminjs/prisma";
|
||||
import { spawn } from "child_process";
|
||||
import { Components } from "../components.js";
|
||||
import { Components } from "../components.bundler.js";
|
||||
import { fileURLToPath } from "url";
|
||||
import path from "path";
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
interface Config {
|
||||
api: {
|
||||
address: string;
|
||||
@@ -13,6 +16,13 @@ interface Config {
|
||||
redis: {
|
||||
host: string;
|
||||
port: number;
|
||||
password: string;
|
||||
};
|
||||
influx: {
|
||||
url: string;
|
||||
token: string;
|
||||
org: string;
|
||||
bucket: string;
|
||||
};
|
||||
core: {
|
||||
hysteresis: number;
|
||||
@@ -24,35 +34,46 @@ interface Config {
|
||||
lossThreshold: number;
|
||||
userTTL: number;
|
||||
verifyTimeout: number;
|
||||
cameraInterval: number;
|
||||
};
|
||||
}
|
||||
|
||||
const config: Config = {
|
||||
api: {
|
||||
address: "0.0.0.0",
|
||||
port: 80,
|
||||
address: process.env.API_ADDRESS ?? "0.0.0.0",
|
||||
port: parseInt(process.env.API_PORT ?? "80"),
|
||||
},
|
||||
mqtt: {
|
||||
host: "192.168.1.2",
|
||||
port: 1883,
|
||||
username: "user",
|
||||
password: "pass",
|
||||
topic: "scanners/+",
|
||||
host: process.env.MQTT_HOST ?? "10.24.4.13",
|
||||
port: parseInt(process.env.MQTT_PORT ?? "1883"),
|
||||
username: process.env.MQTT_USERNAME ?? "user",
|
||||
password: process.env.MQTT_PASSWORD ?? "pass",
|
||||
topic: process.env.MQTT_TOPIC ?? "scanners/+",
|
||||
},
|
||||
redis: {
|
||||
host: "192.168.1.2",
|
||||
port: 6379,
|
||||
host: process.env.REDIS_HOST ?? "10.24.4.13",
|
||||
port: parseInt(process.env.REDIS_PORT ?? "6379"),
|
||||
password: process.env.REDIS_PASSWORD ?? "redispassword",
|
||||
},
|
||||
influx: {
|
||||
url: process.env.INFLUX_URL ?? "http://10.24.4.13:8086",
|
||||
token: process.env.INFLUX_TOKEN ?? "",
|
||||
org: process.env.INFLUX_ORG ?? "officesense",
|
||||
bucket: process.env.INFLUX_BUCKET ?? "officesense",
|
||||
},
|
||||
core: {
|
||||
hysteresis: 6,
|
||||
candidateHysteresis: 3,
|
||||
debounceMS: 3000,
|
||||
minSamples: 4,
|
||||
transitionTTL: 5 * 60 * 1000,
|
||||
transitionCleanupInterval: 60 * 1000,
|
||||
lossThreshold: 5000,
|
||||
userTTL: 3 * 60 * 1000,
|
||||
verifyTimeout: 30 * 1000,
|
||||
hysteresis: parseInt(process.env.CORE_HYSTERESIS ?? "6"),
|
||||
candidateHysteresis: parseInt(process.env.CORE_CANDIDATE_HYSTERESIS ?? "5"),
|
||||
debounceMS: parseInt(process.env.CORE_DEBOUNCE_MS ?? String(3 * 1000)),
|
||||
minSamples: parseInt(process.env.CORE_MIN_SAMPLES ?? "4"),
|
||||
transitionTTL: parseInt(process.env.CORE_TRANSITION_TTL ?? String(5 * 60 * 1000)),
|
||||
transitionCleanupInterval: parseInt(
|
||||
process.env.CORE_TRANSITION_CLEANUP_INTERVAL ?? String(60 * 1000)
|
||||
),
|
||||
lossThreshold: parseInt(process.env.CORE_LOSS_THRESHOLD ?? String(8 * 1000)),
|
||||
userTTL: parseInt(process.env.CORE_USER_TTL ?? String(3 * 60 * 1000)),
|
||||
verifyTimeout: parseInt(process.env.CORE_VERIFY_TIMEOUT ?? String(5 * 60 * 1000)),
|
||||
cameraInterval: parseInt(process.env.CORE_CAMERA_INTERVAL ?? String(5 * 60 * 1000)),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -3,15 +3,45 @@ import { prisma } from "../lib/prisma.js";
|
||||
import { RoomTransition, transitions } from "./transition.js";
|
||||
import config from "../config/config.js";
|
||||
import { type RedisUserData } from "../api/livedata/livedata.repository.js";
|
||||
import { getInflux } from "../influx/influx.js";
|
||||
import { Point } from "@influxdata/influxdb-client";
|
||||
|
||||
const adjectives = ["Crazy", "Silent", "Dark", "Fast", "Lucky", "Wild", "Epic"];
|
||||
|
||||
const nouns = ["Tiger", "Wolf", "Falcon", "Shadow", "Ninja", "Dragon", "Phoenix"];
|
||||
|
||||
const running = new Set<string>();
|
||||
const userRooms = new Map<string, string>();
|
||||
const userPseudo = new Map<string, string>();
|
||||
|
||||
async function writeRoomEvent(
|
||||
event: "enter" | "leave",
|
||||
pseudoID: string,
|
||||
roomID: string,
|
||||
rssi: number
|
||||
) {
|
||||
try {
|
||||
const room = await prisma.room.findUnique({
|
||||
where: { id: roomID },
|
||||
select: { name: true },
|
||||
});
|
||||
|
||||
const roomName = room?.name ?? roomID;
|
||||
|
||||
const point = new Point("room_events")
|
||||
.tag("pseudo_id", pseudoID)
|
||||
.tag("room_id", roomID)
|
||||
.tag("room_name", roomName)
|
||||
.tag("event", event)
|
||||
.floatField("rssi", rssi);
|
||||
|
||||
getInflux().writePoint(point);
|
||||
|
||||
console.log(`[InfluxDB] Wrote '${event}' event for pseudo ${pseudoID} in room ${roomName}`);
|
||||
} catch (err) {
|
||||
console.error("[InfluxDB] Failed to write event:", err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateRoomOccupancyListener(message: string, channel: string) {
|
||||
console.log(`[Redis] Key ${message} expired.`);
|
||||
|
||||
@@ -21,10 +51,11 @@ export async function updateRoomOccupancyListener(message: string, channel: stri
|
||||
|
||||
const redis = getRedis();
|
||||
|
||||
if (!lastRoomID) {
|
||||
console.log("[!] Key not found in local map");
|
||||
} else {
|
||||
if (!lastRoomID) console.log("[!] Key not found in local map");
|
||||
else {
|
||||
await redis.decr(`room:${lastRoomID}`);
|
||||
|
||||
if (userPseudoID) await writeRoomEvent("leave", userPseudoID, lastRoomID, 0);
|
||||
}
|
||||
|
||||
await redis.del(`_user:${userPseudoID}`);
|
||||
@@ -35,11 +66,8 @@ export async function updateRoomOccupancyListener(message: string, channel: stri
|
||||
|
||||
function generateNickname() {
|
||||
const adjective = adjectives[Math.floor(Math.random() * adjectives.length)];
|
||||
|
||||
const noun = nouns[Math.floor(Math.random() * nouns.length)];
|
||||
|
||||
const number = Math.floor(Math.random() * 1000);
|
||||
|
||||
return `${adjective}${noun}${number}`;
|
||||
}
|
||||
|
||||
@@ -58,7 +86,7 @@ export async function analyzeData(
|
||||
}
|
||||
) {
|
||||
if (running.has(metrics.tagID)) {
|
||||
console.log(`[!] Skipping TAG_ID: ${metrics.tagID} already in process.`);
|
||||
console.log(`[!] Skipping TAG_ID: ${metrics.tagID} ROOM_ID: ${roomID} already in process.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -103,14 +131,14 @@ export async function analyzeData(
|
||||
);
|
||||
|
||||
await redis.set(`_user:${user.pseudoID}`, userID);
|
||||
|
||||
await redis.incr(`room:${roomID}`);
|
||||
|
||||
userRooms.set(userID, roomID);
|
||||
userPseudo.set(userID, user.pseudoID);
|
||||
|
||||
console.log("[Redis] Stored new user in redis.");
|
||||
await writeRoomEvent("enter", user.pseudoID, roomID, metrics.rssi);
|
||||
|
||||
console.log("[Redis] Stored new user in redis.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,7 +151,6 @@ export async function analyzeData(
|
||||
resObj["timestamp"] = Date.now();
|
||||
|
||||
await redis.set(`user:${userID}`, JSON.stringify(resObj), { PX: config.core.userTTL });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -140,7 +167,10 @@ export async function analyzeData(
|
||||
) {
|
||||
console.log(`[!] Transition done ${resObj["room"]} -> ${roomID}`);
|
||||
|
||||
await redis.decr(`room:${resObj["room"]}`);
|
||||
const previousRoomID = resObj["room"];
|
||||
const pseudoID = resObj["userID"];
|
||||
|
||||
await redis.decr(`room:${previousRoomID}`);
|
||||
await redis.incr(`room:${roomID}`);
|
||||
|
||||
userRooms.set(userID, roomID);
|
||||
@@ -151,7 +181,12 @@ export async function analyzeData(
|
||||
resObj["verified"] = false;
|
||||
|
||||
await redis.set(`user:${userID}`, JSON.stringify(resObj), { PX: config.core.userTTL });
|
||||
} else console.log("[!] Transition declined");
|
||||
|
||||
await writeRoomEvent("leave", pseudoID, previousRoomID, metrics.rssi);
|
||||
await writeRoomEvent("enter", pseudoID, roomID, metrics.rssi);
|
||||
} else {
|
||||
console.log("[!] Transition declined");
|
||||
}
|
||||
} finally {
|
||||
running.delete(metrics.tagID);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { initRedis, initSubRedis } from "../redis/redis.js";
|
||||
import { initCamera } from "./camera.js";
|
||||
import * as mqtt from "./mqtt.js";
|
||||
import { cleanupWorker } from "./transition.js";
|
||||
import { initInflux } from "../influx/influx.js";
|
||||
|
||||
export async function bootstrap() {
|
||||
try {
|
||||
@@ -17,6 +18,8 @@ export async function bootstrap() {
|
||||
await initSubRedis();
|
||||
await mqtt.start();
|
||||
|
||||
initInflux();
|
||||
|
||||
cleanupWorker();
|
||||
|
||||
await initCamera();
|
||||
|
||||
@@ -100,7 +100,7 @@ const setUnverified = async (uuid: string) => {
|
||||
if (!raw) return;
|
||||
const session = JSON.parse(raw);
|
||||
session.verified = false;
|
||||
await redis.set(key, JSON.stringify(session));
|
||||
await redis.set(key, JSON.stringify(session), { KEEPTTL: true, XX: true });
|
||||
console.log(`[Camera] Unverified user ${uuid} due to timeout`);
|
||||
};
|
||||
|
||||
@@ -120,7 +120,7 @@ const run = async () => {
|
||||
if (!raw) return;
|
||||
const session = JSON.parse(raw);
|
||||
session.verified = true;
|
||||
await redis.set(key, JSON.stringify(session));
|
||||
await redis.set(key, JSON.stringify(session), { KEEPTTL: true, XX: true });
|
||||
|
||||
setTimeout(() => setUnverified(r.uuid), config.core.verifyTimeout);
|
||||
|
||||
@@ -132,7 +132,7 @@ const run = async () => {
|
||||
} catch (e) {
|
||||
console.error("camera run error:", e);
|
||||
}
|
||||
setTimeout(run, 1000);
|
||||
setTimeout(run, config.core.cameraInterval);
|
||||
};
|
||||
|
||||
export { run as initCamera };
|
||||
|
||||
@@ -40,11 +40,10 @@ export class RoomTransition {
|
||||
this.lastSeen = now;
|
||||
|
||||
const stronger = candidateRSSI > currRSSI + config.core.hysteresis;
|
||||
|
||||
const signalLost = now - lastCurrentSeen > config.core.lossThreshold;
|
||||
|
||||
console.log(`[TRANSITION] stronger = ${stronger}`);
|
||||
console.log(`[TRANSITION] signalLost = ${signalLost}`);
|
||||
console.log(`[TRANSITION] signalLost = ${signalLost} (prev=${lastCurrentSeen} now=${now})`);
|
||||
|
||||
if (!stronger && !signalLost) {
|
||||
this.reset();
|
||||
@@ -82,7 +81,11 @@ export class RoomTransition {
|
||||
console.log(`[TRANSITION] enoughConfirmations = ${enoughConfirmations}`);
|
||||
console.log(`[TRANSITION] signalLost = ${signalLost}`);
|
||||
|
||||
if ((enoughTime && enoughConfirmations) || signalLost) {
|
||||
if (enoughTime && enoughConfirmations) {
|
||||
this.reset();
|
||||
return true;
|
||||
}
|
||||
if (signalLost && this.candidate.roomID !== null && enoughConfirmations) {
|
||||
this.reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { bootstrap } from "./core/bootstrap.js";
|
||||
app.use("/lookup", lookupRouter);
|
||||
app.use("/livedata", livedataRouter);
|
||||
app.use("/admin", mapperRouter);
|
||||
app.use("/admin/frontend/assets", express.static("/app/.adminjs"));
|
||||
app.use("/admin", adminRouter);
|
||||
|
||||
app.listen(config.api.port, config.api.address, () => {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { InfluxDB, type WriteApi } from "@influxdata/influxdb-client";
|
||||
import config from "../config/config.js";
|
||||
|
||||
let writeApi: WriteApi | null = null;
|
||||
|
||||
export function initInflux(): void {
|
||||
const client = new InfluxDB({
|
||||
url: config.influx.url,
|
||||
token: config.influx.token,
|
||||
});
|
||||
|
||||
writeApi = client.getWriteApi(config.influx.org, config.influx.bucket, "ms");
|
||||
|
||||
writeApi.useDefaultTags({ host: "officesense-pi" });
|
||||
|
||||
console.log("[InfluxDB] Write API initialized.");
|
||||
}
|
||||
|
||||
export function getInflux(): WriteApi {
|
||||
if (!writeApi) throw new Error("[InfluxDB] Not initialized. Call initInflux() first.");
|
||||
return writeApi;
|
||||
}
|
||||
@@ -5,10 +5,16 @@ import { updateRoomOccupancyListener } from "../core/analyze.js";
|
||||
let redisClient: RedisClientType;
|
||||
let subRedisClient: RedisClientType;
|
||||
|
||||
const redisOptions = {
|
||||
socket: {
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
},
|
||||
password: config.redis.password,
|
||||
};
|
||||
|
||||
export async function initSubRedis(): Promise<void> {
|
||||
subRedisClient = createClient({
|
||||
url: `redis://${config.redis.host}:${config.redis.port}`,
|
||||
});
|
||||
subRedisClient = createClient(redisOptions);
|
||||
|
||||
subRedisClient.on("error", (err) => {
|
||||
console.log("[Redis] SubRedis error:", err.message);
|
||||
@@ -24,9 +30,7 @@ export async function initSubRedis(): Promise<void> {
|
||||
}
|
||||
|
||||
export async function initRedis(): Promise<RedisClientType> {
|
||||
redisClient = createClient({
|
||||
url: `redis://${config.redis.host}:${config.redis.port}`,
|
||||
});
|
||||
redisClient = createClient(redisOptions);
|
||||
|
||||
redisClient.on("error", (err) => {
|
||||
console.log("[Redis] Redis error:", err.message);
|
||||
|
||||
@@ -128,6 +128,11 @@ void mqttTask(void *)
|
||||
{
|
||||
while (!mqtt.connected())
|
||||
{
|
||||
if (WiFi.status() != WL_CONNECTED)
|
||||
{
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
continue;
|
||||
}
|
||||
// Serial.printf("MQTT Disconnected. Connecting to %s:%d\n", config.mqtt.host.c_str(), config.mqtt.port);
|
||||
if (mqtt.connect(config.dev_name.c_str(), config.mqtt.username.c_str(), config.mqtt.password.c_str()))
|
||||
Serial.println("MQTT connected.");
|
||||
@@ -253,6 +258,9 @@ void saveConfig()
|
||||
|
||||
prefs.putString(WIFI_SSID_CONFIG_KEY, config.wifi.ssid);
|
||||
prefs.putString(WIFI_PASS_CONFIG_KEY, config.wifi.password);
|
||||
prefs.putString(WIFI_IP_CONFIG_KEY, config.wifi.ip);
|
||||
prefs.putString(WIFI_GW_CONFIG_KEY, config.wifi.gateway);
|
||||
prefs.putString(WIFI_SUBNET_CONFIG_KEY, config.wifi.subnet);
|
||||
|
||||
prefs.putString(DEV_NAME_CONFIG_KEY, config.dev_name);
|
||||
prefs.putString(TAG_NAME_CONFIG_KEY, config.tag_name);
|
||||
@@ -280,6 +288,9 @@ void loadConfig()
|
||||
|
||||
config.wifi.ssid = prefs.getString(WIFI_SSID_CONFIG_KEY, WIFI_SSID);
|
||||
config.wifi.password = prefs.getString(WIFI_PASS_CONFIG_KEY, WIFI_PASS);
|
||||
config.wifi.ip = prefs.getString(WIFI_IP_CONFIG_KEY, WIFI_IP);
|
||||
config.wifi.gateway = prefs.getString(WIFI_GW_CONFIG_KEY, WIFI_GW);
|
||||
config.wifi.subnet = prefs.getString(WIFI_SUBNET_CONFIG_KEY, WIFI_SUBNET);
|
||||
|
||||
config.dev_name = prefs.getString(DEV_NAME_CONFIG_KEY, DEV_NAME);
|
||||
config.tag_name = prefs.getString(TAG_NAME_CONFIG_KEY, TAG_NAME);
|
||||
@@ -324,6 +335,12 @@ void handleCommand(String line)
|
||||
config.wifi.ssid = value;
|
||||
else if (key == WIFI_PASS_CONFIG_KEY)
|
||||
config.wifi.password = value;
|
||||
else if (key == WIFI_IP_CONFIG_KEY)
|
||||
config.wifi.ip = value;
|
||||
else if (key == WIFI_GW_CONFIG_KEY)
|
||||
config.wifi.gateway = value;
|
||||
else if (key == WIFI_SUBNET_CONFIG_KEY)
|
||||
config.wifi.subnet = value;
|
||||
else if (key == DEV_NAME_CONFIG_KEY)
|
||||
config.dev_name = value;
|
||||
else if (key == TAG_NAME_CONFIG_KEY)
|
||||
@@ -364,6 +381,12 @@ void handleCommand(String line)
|
||||
Serial.println(config.wifi.ssid);
|
||||
else if (key == WIFI_PASS_CONFIG_KEY)
|
||||
Serial.println(config.wifi.password);
|
||||
else if (key == WIFI_IP_CONFIG_KEY)
|
||||
Serial.println(config.wifi.ip);
|
||||
else if (key == WIFI_GW_CONFIG_KEY)
|
||||
Serial.println(config.wifi.gateway);
|
||||
else if (key == WIFI_SUBNET_CONFIG_KEY)
|
||||
Serial.println(config.wifi.subnet);
|
||||
else if (key == DEV_NAME_CONFIG_KEY)
|
||||
Serial.println(config.dev_name);
|
||||
else if (key == TAG_NAME_CONFIG_KEY)
|
||||
@@ -388,6 +411,25 @@ void handleCommand(String line)
|
||||
Serial.println("Commands:");
|
||||
Serial.println(" set <key> <value> - Set a configuration value");
|
||||
Serial.println(" show <key> - Show a configuration value");
|
||||
Serial.println(" <key>:");
|
||||
Serial.println(" int.cln");
|
||||
Serial.println(" int.tagto");
|
||||
Serial.println(" int.urtry");
|
||||
Serial.println(" int.ucclean");
|
||||
Serial.println(" api.url");
|
||||
Serial.println(" api.uuidchk");
|
||||
Serial.println(" mq.host");
|
||||
Serial.println(" mq.port");
|
||||
Serial.println(" mq.user");
|
||||
Serial.println(" mq.pass");
|
||||
Serial.println(" mq.topic");
|
||||
Serial.println(" wf.ssid");
|
||||
Serial.println(" wf.pass");
|
||||
Serial.println(" wf.ip");
|
||||
Serial.println(" wf.gw");
|
||||
Serial.println(" wf.subnet");
|
||||
Serial.println(" dev.name");
|
||||
Serial.println(" tag.name");
|
||||
Serial.println(" save - Save configuration to non-volatile storage");
|
||||
Serial.println(" reset - Reset configuration to defaults");
|
||||
Serial.println(" reboot - Reboot the device");
|
||||
@@ -395,13 +437,47 @@ void handleCommand(String line)
|
||||
}
|
||||
}
|
||||
|
||||
void wifiBegin()
|
||||
{
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.disconnect();
|
||||
delay(100);
|
||||
|
||||
IPAddress local_ip, gateway, subnet;
|
||||
local_ip.fromString(config.wifi.ip);
|
||||
|
||||
if (local_ip != IPAddress(0, 0, 0, 0))
|
||||
{
|
||||
gateway.fromString(config.wifi.gateway);
|
||||
subnet.fromString(config.wifi.subnet);
|
||||
WiFi.config(local_ip, gateway, subnet);
|
||||
}
|
||||
|
||||
WiFi.begin(config.wifi.ssid.c_str(), config.wifi.password.c_str());
|
||||
}
|
||||
|
||||
void setup()
|
||||
{
|
||||
Serial.begin(115200);
|
||||
|
||||
loadConfig();
|
||||
|
||||
WiFi.begin(config.wifi.ssid.c_str(), config.wifi.password.c_str());
|
||||
WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info)
|
||||
{
|
||||
switch (event)
|
||||
{
|
||||
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
|
||||
Serial.println("WiFi connected.");
|
||||
break;
|
||||
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
|
||||
Serial.printf("IP: %s\n", WiFi.localIP().toString().c_str());
|
||||
break;
|
||||
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
|
||||
Serial.printf("WiFi disconnected, reason: %d\n", info.wifi_sta_disconnected.reason);
|
||||
break;
|
||||
} });
|
||||
|
||||
wifiBegin();
|
||||
|
||||
MQTT_PUB_TOPIC = config.mqtt.topic + config.dev_name;
|
||||
|
||||
@@ -432,6 +508,22 @@ void setup()
|
||||
void loop()
|
||||
{
|
||||
BleEvent ev;
|
||||
static uint32_t last_wifi_check = 0;
|
||||
|
||||
if (millis() - last_wifi_check >= WIFI_TEST_INTERVAL)
|
||||
{
|
||||
last_wifi_check = millis();
|
||||
|
||||
wl_status_t status = WiFi.status();
|
||||
|
||||
if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL)
|
||||
{
|
||||
Serial.printf("WiFi failed (status %d), retrying...\n", status);
|
||||
WiFi.disconnect();
|
||||
delay(100);
|
||||
wifiBegin();
|
||||
}
|
||||
}
|
||||
|
||||
while (xQueueReceive(ble_queue, &ev, 0))
|
||||
processEvent(ev);
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
#define MQTT_TOPIC_CONFIG_KEY "mq.topic"
|
||||
#define WIFI_SSID_CONFIG_KEY "wf.ssid"
|
||||
#define WIFI_PASS_CONFIG_KEY "wf.pass"
|
||||
#define WIFI_IP_CONFIG_KEY "wf.ip"
|
||||
#define WIFI_GW_CONFIG_KEY "wf.gw"
|
||||
#define WIFI_SUBNET_CONFIG_KEY "wf.subnet"
|
||||
#define DEV_NAME_CONFIG_KEY "dev.name"
|
||||
#define TAG_NAME_CONFIG_KEY "tag.name"
|
||||
|
||||
@@ -26,7 +29,7 @@
|
||||
#define UUID_CACHE_RETRY_TIMEOUT 5 * 60 * 1000 // ms
|
||||
#define UUID_CACHE_CLEAN_TIMEOUT 60 * 60 * 1000 // ms
|
||||
#define API_URL "http://192.168.1.2"
|
||||
#define UUID_CHECK_ENDPOINT "/check"
|
||||
#define UUID_CHECK_ENDPOINT "/lookup/check"
|
||||
#define TAG_NAME "X6TAG"
|
||||
#define MQTT_HOST "192.168.1.2"
|
||||
#define MQTT_PORT 1883
|
||||
@@ -35,7 +38,11 @@
|
||||
#define MQTT_TOPIC "scanners/"
|
||||
#define WIFI_SSID "COSMOTE-489882"
|
||||
#define WIFI_PASS "x32hbh54673ngccdsfa9"
|
||||
#define WIFI_IP "0.0.0.0"
|
||||
#define WIFI_GW "0.0.0.0"
|
||||
#define WIFI_SUBNET "255.255.255.0"
|
||||
#define DEV_NAME "572b29cb-6a93-480e-adf0-c5c44e9a58df"
|
||||
#define WIFI_TEST_INTERVAL 5 * 1000 // ms
|
||||
|
||||
struct Config
|
||||
{
|
||||
@@ -66,6 +73,9 @@ struct Config
|
||||
{
|
||||
String ssid;
|
||||
String password;
|
||||
String ip;
|
||||
String gateway;
|
||||
String subnet;
|
||||
} wifi;
|
||||
|
||||
String dev_name;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "nimble/nimble_port_freertos.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
#include "driver/usb_serial_jtag.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "main.h"
|
||||
|
||||
static config_t config;
|
||||
@@ -36,8 +37,8 @@ static void start_adv(void)
|
||||
|
||||
params.conn_mode = BLE_GAP_CONN_MODE_NON;
|
||||
params.disc_mode = BLE_GAP_DISC_MODE_GEN;
|
||||
params.itvl_min = 0x80; // ~100ms
|
||||
params.itvl_max = 0x80;
|
||||
params.itvl_min = config.adv_interval;
|
||||
params.itvl_max = config.adv_interval;
|
||||
|
||||
rc = ble_gap_adv_start(
|
||||
BLE_ADDR_PUBLIC,
|
||||
@@ -54,6 +55,7 @@ static void start_adv(void)
|
||||
else
|
||||
{
|
||||
ESP_LOGI(config.name, "advertising started");
|
||||
gpio_set_level(GPIO_NUM_15, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +141,7 @@ static esp_err_t save_config()
|
||||
if (err != ESP_OK)
|
||||
goto exit;
|
||||
|
||||
err = nvs_set_u32(nvs, "adv_int", config.adv_interval);
|
||||
err = nvs_set_u16(nvs, "adv_int", config.adv_interval);
|
||||
|
||||
if (err != ESP_OK)
|
||||
goto exit;
|
||||
@@ -177,7 +179,7 @@ static esp_err_t load_config()
|
||||
|
||||
nvs_get_str(nvs, "name", config.name, &len);
|
||||
|
||||
nvs_get_u32(nvs, "adv_int", &config.adv_interval);
|
||||
nvs_get_u16(nvs, "adv_int", &config.adv_interval);
|
||||
|
||||
nvs_close(nvs);
|
||||
|
||||
@@ -219,9 +221,9 @@ void serial_task(void *arg)
|
||||
}
|
||||
else if (strncmp(line, "set adv_interval ", 17) == 0)
|
||||
{
|
||||
uint32_t interval = atoi(line + 17);
|
||||
float interval = atof(line + 17);
|
||||
if (interval > 0)
|
||||
config.adv_interval = interval;
|
||||
config.adv_interval = (uint16_t)(interval / 0.625f);
|
||||
}
|
||||
else if (strcmp(line, "show uuid") == 0)
|
||||
{
|
||||
@@ -235,7 +237,7 @@ void serial_task(void *arg)
|
||||
}
|
||||
else if (strcmp(line, "show adv_interval") == 0)
|
||||
{
|
||||
int len = snprintf(buf, sizeof(buf), "%lu\n", (unsigned long)config.adv_interval);
|
||||
int len = snprintf(buf, sizeof(buf), "%.3f\n", config.adv_interval * 0.625f);
|
||||
usb_serial_jtag_write_bytes(buf, len, 20 / portTICK_PERIOD_MS);
|
||||
}
|
||||
else if (strcmp(line, "save") == 0)
|
||||
@@ -293,6 +295,10 @@ void serial_task(void *arg)
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
gpio_reset_pin(GPIO_NUM_15);
|
||||
gpio_set_direction(GPIO_NUM_15, GPIO_MODE_OUTPUT);
|
||||
gpio_set_level(GPIO_NUM_15, 1);
|
||||
|
||||
ESP_ERROR_CHECK(nvs_flash_init());
|
||||
|
||||
usb_serial_jtag_driver_config_t cfg = USB_SERIAL_JTAG_DRIVER_CONFIG_DEFAULT();
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
|
||||
#define DEFAULT_UUID "12345678-9abc-def0-1234-56789abcdef0"
|
||||
#define DEFAULT_NAME "X6TAG"
|
||||
#define DEFAULT_ADV_INTERVAL 0x80
|
||||
#define DEFAULT_ADV_INTERVAL 800
|
||||
|
||||
typedef struct
|
||||
{
|
||||
ble_uuid128_t uuid;
|
||||
char uuid_str[37];
|
||||
char name[32];
|
||||
uint32_t adv_interval;
|
||||
uint16_t adv_interval;
|
||||
} config_t;
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user