Author SHA1 Message Date
admin 3977e8d17b Tag config management code 2026-05-20 17:44:46 +03:00
Kostas DrakontidisandGitHub a50c48a92d Merge pull request #12 from dkwstas/main
Change tag name & remove mfg data
2026-05-19 23:29:19 +03:00
admin 96233d2ae5 Scanner config management code 2026-05-19 23:02:09 +03:00
Kostas Drakontidis 86cc46ea9b Change tag name & remove mfg data 2026-05-15 17:32:45 +03:00
Kostas DrakontidisandGitHub d5738d460c Add equipment functionality testing to completed section 2026-05-15 14:34:07 +03:00
Kostas DrakontidisandGitHub 80f7c89852 Update weekly progress log with new entries 2026-05-14 23:48:10 +03:00
Kostas DrakontidisandGitHub 744d145ec2 Enhance architecture documentation for OfficeSense system
Expanded the architecture document to include detailed descriptions of the OfficeSense system, its components, data flow, design decisions, and validation plan.
2026-05-14 23:34:14 +03:00
Kostas DrakontidisandGitHub a775109b36 Merge pull request #7 from dkwstas/pi_code_init
Initial Pi code for public lookup api
2026-05-13 23:28:48 +03:00
Kostas Drakontidis 97712364ca Initial Pi code for public lookup api 2026-05-13 23:27:55 +03:00
Kostas DrakontidisandGitHub e349ef92b7 Merge pull request #5 from dkwstas/ble_scanner_init
Initial scanner code
2026-05-13 18:18:31 +03:00
Kostas Drakontidis bcc3e97c80 Initial scanner code 2026-05-13 18:17:00 +03:00
Kostas DrakontidisandGitHub 095da18f73 Merge pull request #2 from dkwstas/ble_tag_init
Initial tag code
2026-05-07 00:08:13 +03:00
28 changed files with 4601 additions and 40 deletions
+50 -9
View File
@@ -1,21 +1,62 @@
# System Architecture
## Overview
Describe the project at a high level.
OfficeSense is a Smart Office Presence system that detects user presence and identifies the room where each user is located using Bluetooth Low Energy (BLE) technology. Users carry BLE tags that periodically broadcast pseudonymized identifiers, while BLE scanners installed in different rooms detect these signals and forward the data to a Raspberry Pi for processing.
The system follows a privacy-by-design approach by using identifiers instead of personal data during communication. In addition to BLE-based presence detection, a camera connected to the Raspberry Pi performs face-recognition-based authentication to verify that the detected BLE tag belongs to the actual user.
The architecture combines edge computing, real-time processing, and lightweight storage technologies to provide low-latency monitoring, room occupancy tracking, and secure user authentication through real-time dashboards.
## Components
- sensors / input devices
- processing node
- communication
- storage
- dashboard / app
- actuators / notifications
- BLE Tags (Seeed Studio XIAO ESP32-C6)
- BLE Scanners (Waveshare ESP32-S3 Zero)
- USB Camera
- Processing Node (Raspberry Pi)
- Communication (BLE, WiFi, MQTT, HTTP, NGSI-LD)
- Storage (SQLite, Redis, Optional InfluxDB)
- Dashboard (Node-RED, AdminJS, Optional Grafana)
## Data Flow
Describe how data moves through the system.
1. A BLE tag periodically broadcasts a pseudonymized UUID.
2. Nearby BLE scanners receive the signal and measure RSSI values.
3. The scanners:
- validate UUIDs,
- apply EMA filtering to RSSI values,
- cache validation responses,
- package the data and publish to the MQTT broker over WiFi.
4. The Raspberry Pi receives MQTT messages and processes them.
5. The system estimates the users room based on strongest RSSI values.
6. When a new presence event occurs:
- the camera is triggered,
- image frames are captured,
- the face recognition service verifies the user identity.
7. The system compares:
- BLE tag identity
- face recognition identity
8. The authentication result and room location are stored in Redis.
9. Persistent information is stored in SQLite.
10. Backend APIs provide data to dashboards for real-time visualization.
11. If no BLE signal is received for a predefined timeout period, the user is automatically marked as absent.
## Decisions and Tradeoffs
Explain key design choices.
- Edge Computing with Raspberry Pi
- Reduces latency
- Minimizes cloud dependency
- Improves privacy and reliability
- Hybrid Storage Architecture
- SQLite for reliable persistent storage
- Redis for fast real-time operations
- Event-Driven Processing
- BLE detections trigger processing only when needed
- Reduces unnecessary computation
- Privacy-by-Design
- Uses pseudonymized UUIDs instead of direct user identities
- MQTT Communication
- Lightweight and suitable for IoT environments
- Supports asynchronous communication
- RSSI-based localization is less accurate than more advanced positioning systems
- Face recognition increases security but also adds computational overhead
## Validation Plan
Describe how you will test the system.
+20 -9
View File
@@ -1,7 +1,5 @@
# Weekly Progress Log
Update this file every week.
## Week 1
### Completed
- Repository created
@@ -18,19 +16,32 @@ Update this file every week.
- Finalize architecture
- Create first Issues
### Team Contribution
- Student 1:
- Student 2:
## Week 2
### Completed
- Full BLE Tag code
### In Progress
### Problems / Risks
### Next Steps
- UUID modification during Runtime
### Team Contribution
- Student 1:
- Student 2:
## Week 3
### Completed
- Full BLE Scanner code
- SQLite schema with Prisma ORM
- UUID lookup for public API
- Connected and tested functionality of all equipment
### In Progress
- Core script to receive MQTT data, process and store in Redis
- NGSI-LD data for public API
- Admin API with CRUD functions
### Problems / Risks
- WiFi won't reconnect
- no auth to use public API
### Next Steps
- WiFi self-healing and MQTT reconnection & testing
+3
View File
@@ -0,0 +1,3 @@
node_modules
.git
npm-debug.log
+5
View File
@@ -0,0 +1,5 @@
node_modules
# Keep environment variables out of version control
.env
**/generated/prisma
+19
View File
@@ -0,0 +1,19 @@
FROM node:24-slim
WORKDIR /app
RUN apt-get update -y && apt-get install -y openssl
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx prisma generate
RUN npm run build
EXPOSE 80
CMD ["node", "dist/index.js"]
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
{
"name": "officesense",
"version": "1.0.0",
"description": "",
"license": "ISC",
"author": "",
"main": "index.js",
"type": "module",
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/express": "^5.0.6",
"@types/node": "^25.7.0",
"prisma": "^7.8.0",
"tsx": "^4.21.0",
"typescript": "^6.0.3"
},
"dependencies": {
"@prisma/adapter-better-sqlite3": "^7.8.0",
"@prisma/client": "^7.8.0",
"dotenv": "^17.4.2",
"express": "^5.2.1"
}
}
+14
View File
@@ -0,0 +1,14 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: process.env["DATABASE_URL"],
},
});
@@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL PRIMARY KEY,
"firstName" TEXT NOT NULL,
"lastName" TEXT NOT NULL,
"faceEmbedding" BLOB NOT NULL
);
-- CreateTable
CREATE TABLE "Tag" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
CONSTRAINT "Tag_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Tag_userId_key" ON "Tag"("userId");
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"
+22
View File
@@ -0,0 +1,22 @@
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "sqlite"
}
model User {
id String @id @default(uuid())
firstName String
lastName String
faceEmbedding Bytes?
tag Tag?
}
model Tag {
id String @id @default(uuid())
userId String @unique
user User @relation(fields: [userId], references: [id])
}
@@ -0,0 +1,16 @@
import type { Request, Response } from "express";
import { existsByUUID } from "./lookup.repository.js";
interface Params {
uuid: string;
}
export async function check(req: Request<Params>, res: Response) {
const { uuid } = req.params;
const exists = await existsByUUID(uuid);
if (!exists) return res.sendStatus(404);
return res.sendStatus(200);
}
@@ -0,0 +1,9 @@
import { prisma } from "../../lib/prisma.js";
export async function existsByUUID(uuid: string): Promise<boolean> {
const user = await prisma.user.findUnique({
where: { id: uuid }
});
return !!user;
}
@@ -0,0 +1,8 @@
import express from "express";
import { check } from "./lookup.controller.js";
const router = express.Router();
router.get("/check/:uuid", check);
export default router;
+12
View File
@@ -0,0 +1,12 @@
import express from "express";
import lookupRoutes from "./api/lookup/lookup.routes.js";
const app = express();
const PORT = 80;
const ADDRESS = "0.0.0.0";
app.use("/", lookupRoutes);
app.listen(PORT, ADDRESS, () => {
console.log(`Server listening on ${ADDRESS}:${PORT}`);
});
+10
View File
@@ -0,0 +1,10 @@
import "dotenv/config";
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
import { PrismaClient } from "../generated/prisma/client.js";
const connectionString = `${process.env.DATABASE_URL}`;
const adapter = new PrismaBetterSqlite3({ url: connectionString });
const prisma = new PrismaClient({ adapter });
export { prisma };
+49
View File
@@ -0,0 +1,49 @@
{
// Visit https://aka.ms/tsconfig to read more about this file
"compilerOptions": {
// File Layout
"rootDir": "./src",
"outDir": "./dist",
// Environment Settings
// See also https://aka.ms/tsconfig/module
"module": "NodeNext",
"target": "ES2022",
"moduleResolution": "NodeNext",
"types": [],
"esModuleInterop": true,
"ignoreDeprecations": "6.0",
// For nodejs:
// "lib": ["esnext"],
// "types": ["node"],
// and npm install -D @types/node
// Other Outputs
"sourceMap": true,
"declaration": true,
"declarationMap": true,
// Stricter Typechecking Options
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// Style Options
// "noImplicitReturns": true,
// "noImplicitOverride": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "noFallthroughCasesInSwitch": true,
// "noPropertyAccessFromIndexSignature": true,
// Recommended Options
"strict": true,
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true,
},
"include": ["src/**/*"],
"exclude": [
"node_modules",
"generated",
"dist",
"prisma.config.ts"
]
}
+5
View File
@@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
+37
View File
@@ -0,0 +1,37 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the convention is to give header files names that end with `.h'.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
+46
View File
@@ -0,0 +1,46 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into the executable file.
The source code of each library should be placed in a separate directory
("lib/your_library_name/[Code]").
For example, see the structure of the following example libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
Example contents of `src/main.c` using Foo and Bar:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
The PlatformIO Library Dependency Finder will find automatically dependent
libraries by scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html
+25
View File
@@ -0,0 +1,25 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:esp32-s3-devkitc-1]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200
board_upload.flash_size = 4MB
board_build.partitions = default.csv
build_flags =
-DARDUINO_USB_CDC_ON_BOOT=1
-DBOARD_HAS_PSRAM
-DCORE_DEBUG_LEVEL=0
lib_deps =
h2zero/NimBLE-Arduino
knolleary/PubSubClient
+473
View File
@@ -0,0 +1,473 @@
#include <unordered_map>
#include "main.hpp"
#include "tag/Tag.hpp"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include <HTTPClient.h>
#include <PubSubClient.h>
#include <Preferences.h>
#include "esp_system.h"
Preferences prefs;
Config config;
CB cb;
String MQTT_PUB_TOPIC;
String serial_buffer = "";
std::unordered_map<std::string, Tag> tags;
std::unordered_map<std::string, UUIDCacheEntry> uuid_cache;
SemaphoreHandle_t uuid_mtx;
QueueHandle_t ble_queue;
QueueHandle_t mqtt_queue;
QueueHandle_t http_queue;
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
void cleanupCache()
{
uint32_t now = millis();
xSemaphoreTake(uuid_mtx, portMAX_DELAY);
for (auto it = uuid_cache.begin(); it != uuid_cache.end();)
{
uint32_t age = now - it->second.last_check;
if (age > config.interval.uuid_cache_clean_timeout)
{
Serial.printf("Erasing %s from cache after being inactive for %dms.\n",
it->first.c_str(),
age);
it = uuid_cache.erase(it);
}
else
++it;
}
xSemaphoreGive(uuid_mtx);
}
void cleanupTags()
{
uint32_t now = millis();
for (auto it = tags.begin(); it != tags.end();)
{
if ((now - it->second.getLastSeen()) >= config.interval.tag_timeout)
{
Serial.printf("Erasing %s from memory after being inactive for %dms.\n",
it->first.c_str(),
now - it->second.getLastSeen());
it = tags.erase(it);
}
else
++it;
}
}
void httpTask(void *)
{
HttpEvent ev;
while (true)
{
if (xQueueReceive(http_queue, &ev, portMAX_DELAY))
{
HTTPClient http;
String url = config.api.url + config.api.uuid_check_endpoint + String("/") + String(ev.uuid);
http.setTimeout(3000);
http.setReuse(false);
if (!http.begin(url))
{
Serial.println("Failed HTTP begin.");
continue;
}
int code = http.GET();
if (code < 0)
{
xSemaphoreTake(uuid_mtx, portMAX_DELAY);
uuid_cache[ev.uuid] = {
UUIDState::RETRY,
millis()};
xSemaphoreGive(uuid_mtx);
http.end();
continue;
}
bool valid = (code == 200);
http.end();
xSemaphoreTake(uuid_mtx, portMAX_DELAY);
uuid_cache[ev.uuid] = {
(valid) ? UUIDState::VALID : UUIDState::INVALID,
millis()};
#ifdef DEBUG
Serial.printf("[%s] is now marked as %s\n", ev.uuid, (valid) ? "valid" : "invalid");
#endif
xSemaphoreGive(uuid_mtx);
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
void mqttTask(void *)
{
NetEvent ev;
while (true)
{
while (!mqtt.connected())
{
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.");
vTaskDelay(pdMS_TO_TICKS(1000));
}
if (xQueueReceive(mqtt_queue, &ev, pdMS_TO_TICKS(100)))
{
char payload[128];
snprintf(payload, sizeof(payload),
"{\"uuid\":\"%s\",\"rssi\":%.2f}",
ev.uuid,
ev.ema);
bool ok = mqtt.publish(MQTT_PUB_TOPIC.c_str(), payload);
if (!ok)
Serial.println("Failed to publish on MQTT.");
}
else
mqtt.loop();
}
}
void processEvent(const BleEvent &event)
{
xSemaphoreTake(uuid_mtx, portMAX_DELAY);
auto it = uuid_cache.find(event.uuid);
uint32_t now = millis();
if (it == uuid_cache.end() || (it->second.state == UUIDState::RETRY))
{
uuid_cache[event.uuid] = {UUIDState::PENDING, now};
xSemaphoreGive(uuid_mtx);
HttpEvent he;
strlcpy(he.uuid, event.uuid, sizeof(event.uuid));
xQueueSend(http_queue, &he, 0);
return;
}
if (it->second.state == UUIDState::INVALID &&
now - it->second.last_check > config.interval.uuid_cache_retry_timeout)
{
it->second.state = UUIDState::PENDING;
it->second.last_check = now;
xSemaphoreGive(uuid_mtx);
HttpEvent he;
strlcpy(he.uuid, event.uuid, sizeof(event.uuid));
xQueueSend(http_queue, &he, 0);
return;
}
UUIDState state = it->second.state;
xSemaphoreGive(uuid_mtx);
if (state != UUIDState::VALID)
return;
Tag &tag = tags[event.uuid];
tag.addSample(event.rssi);
if (tag.shouldPublish())
{
NetEvent ne;
strlcpy(ne.uuid, event.uuid, sizeof(event.uuid));
ne.ema = tag.getEMA();
if (!xQueueSend(mqtt_queue, &ne, 0))
Serial.println("Failed to send MQTT data over MQTT Queue.");
}
}
void CB::onResult(const NimBLEAdvertisedDevice *d)
{
if (!d->haveServiceUUID() || !d->haveName())
return;
NimBLEUUID uuid = d->getServiceUUID(0);
const String name(d->getName().c_str());
if ((uuid.bitSize() != 128) || (name != config.tag_name))
return;
BleEvent event;
strlcpy(event.uuid, uuid.toString().c_str(), sizeof(event.uuid));
event.rssi = d->getRSSI();
xQueueSend(ble_queue, &event, pdMS_TO_TICKS(5));
}
void saveConfig()
{
prefs.begin("config", false);
prefs.putUInt(CLEANUP_INTERVAL_CONFIG_KEY, config.interval.cleanup);
prefs.putUInt(TAG_TIMEOUT_CONFIG_KEY, config.interval.tag_timeout);
prefs.putUInt(UUID_CACHE_RETRY_TIMEOUT_CONFIG_KEY, config.interval.uuid_cache_retry_timeout);
prefs.putUInt(UUID_CACHE_CLEAN_TIMEOUT_CONFIG_KEY, config.interval.uuid_cache_clean_timeout);
prefs.putString(API_URL_CONFIG_KEY, config.api.url);
prefs.putString(UUID_CHECK_ENDPOINT_CONFIG_KEY, config.api.uuid_check_endpoint);
prefs.putString(MQTT_HOST_CONFIG_KEY, config.mqtt.host);
prefs.putUShort(MQTT_PORT_CONFIG_KEY, config.mqtt.port);
prefs.putString(MQTT_USER_CONFIG_KEY, config.mqtt.username);
prefs.putString(MQTT_PASS_CONFIG_KEY, config.mqtt.password);
prefs.putString(MQTT_TOPIC_CONFIG_KEY, config.mqtt.topic);
prefs.putString(WIFI_SSID_CONFIG_KEY, config.wifi.ssid);
prefs.putString(WIFI_PASS_CONFIG_KEY, config.wifi.password);
prefs.putString(DEV_NAME_CONFIG_KEY, config.dev_name);
prefs.putString(TAG_NAME_CONFIG_KEY, config.tag_name);
prefs.end();
}
void loadConfig()
{
prefs.begin("config", true);
config.interval.cleanup = prefs.getUInt(CLEANUP_INTERVAL_CONFIG_KEY, CLEANUP_INTERVAL);
config.interval.tag_timeout = prefs.getUInt(TAG_TIMEOUT_CONFIG_KEY, TAG_TIMEOUT);
config.interval.uuid_cache_retry_timeout = prefs.getUInt(UUID_CACHE_RETRY_TIMEOUT_CONFIG_KEY, UUID_CACHE_RETRY_TIMEOUT);
config.interval.uuid_cache_clean_timeout = prefs.getUInt(UUID_CACHE_CLEAN_TIMEOUT_CONFIG_KEY, UUID_CACHE_CLEAN_TIMEOUT);
config.api.url = prefs.getString(API_URL_CONFIG_KEY, API_URL);
config.api.uuid_check_endpoint = prefs.getString(UUID_CHECK_ENDPOINT_CONFIG_KEY, UUID_CHECK_ENDPOINT);
config.mqtt.host = prefs.getString(MQTT_HOST_CONFIG_KEY, MQTT_HOST);
config.mqtt.port = prefs.getUShort(MQTT_PORT_CONFIG_KEY, MQTT_PORT);
config.mqtt.username = prefs.getString(MQTT_USER_CONFIG_KEY, MQTT_USER);
config.mqtt.password = prefs.getString(MQTT_PASS_CONFIG_KEY, MQTT_PASS);
config.mqtt.topic = prefs.getString(MQTT_TOPIC_CONFIG_KEY, MQTT_TOPIC);
config.wifi.ssid = prefs.getString(WIFI_SSID_CONFIG_KEY, WIFI_SSID);
config.wifi.password = prefs.getString(WIFI_PASS_CONFIG_KEY, WIFI_PASS);
config.dev_name = prefs.getString(DEV_NAME_CONFIG_KEY, DEV_NAME);
config.tag_name = prefs.getString(TAG_NAME_CONFIG_KEY, TAG_NAME);
prefs.end();
}
void handleCommand(String line)
{
if (line.startsWith("set "))
{
int p1 = line.indexOf(' ', 4);
String key = line.substring(4, p1);
String value = line.substring(p1 + 1);
if (key.length() == 0 || value.length() == 0)
return;
if (key == CLEANUP_INTERVAL_CONFIG_KEY)
config.interval.cleanup = (uint32_t)value.toInt();
else if (key == TAG_TIMEOUT_CONFIG_KEY)
config.interval.tag_timeout = (uint32_t)value.toInt();
else if (key == UUID_CACHE_RETRY_TIMEOUT_CONFIG_KEY)
config.interval.uuid_cache_retry_timeout = (uint32_t)value.toInt();
else if (key == UUID_CACHE_CLEAN_TIMEOUT_CONFIG_KEY)
config.interval.uuid_cache_clean_timeout = (uint32_t)value.toInt();
else if (key == API_URL_CONFIG_KEY)
config.api.url = value;
else if (key == UUID_CHECK_ENDPOINT_CONFIG_KEY)
config.api.uuid_check_endpoint = value;
else if (key == MQTT_HOST_CONFIG_KEY)
config.mqtt.host = value;
else if (key == MQTT_PORT_CONFIG_KEY)
config.mqtt.port = value.toInt();
else if (key == MQTT_USER_CONFIG_KEY)
config.mqtt.username = value;
else if (key == MQTT_PASS_CONFIG_KEY)
config.mqtt.password = value;
else if (key == MQTT_TOPIC_CONFIG_KEY)
config.mqtt.topic = value;
else if (key == WIFI_SSID_CONFIG_KEY)
config.wifi.ssid = value;
else if (key == WIFI_PASS_CONFIG_KEY)
config.wifi.password = value;
else if (key == DEV_NAME_CONFIG_KEY)
config.dev_name = value;
else if (key == TAG_NAME_CONFIG_KEY)
config.tag_name = value;
}
else if (line == "save")
saveConfig();
else if (line.startsWith("show "))
{
String key = line.substring(5);
if (key.length() == 0)
return;
if (key == CLEANUP_INTERVAL_CONFIG_KEY)
Serial.println(config.interval.cleanup);
else if (key == TAG_TIMEOUT_CONFIG_KEY)
Serial.println(config.interval.tag_timeout);
else if (key == UUID_CACHE_RETRY_TIMEOUT_CONFIG_KEY)
Serial.println(config.interval.uuid_cache_retry_timeout);
else if (key == UUID_CACHE_CLEAN_TIMEOUT_CONFIG_KEY)
Serial.println(config.interval.uuid_cache_clean_timeout);
else if (key == API_URL_CONFIG_KEY)
Serial.println(config.api.url);
else if (key == UUID_CHECK_ENDPOINT_CONFIG_KEY)
Serial.println(config.api.uuid_check_endpoint);
else if (key == MQTT_HOST_CONFIG_KEY)
Serial.println(config.mqtt.host);
else if (key == MQTT_PORT_CONFIG_KEY)
Serial.println(config.mqtt.port);
else if (key == MQTT_USER_CONFIG_KEY)
Serial.println(config.mqtt.username);
else if (key == MQTT_PASS_CONFIG_KEY)
Serial.println(config.mqtt.password);
else if (key == MQTT_TOPIC_CONFIG_KEY)
Serial.println(config.mqtt.topic);
else if (key == WIFI_SSID_CONFIG_KEY)
Serial.println(config.wifi.ssid);
else if (key == WIFI_PASS_CONFIG_KEY)
Serial.println(config.wifi.password);
else if (key == DEV_NAME_CONFIG_KEY)
Serial.println(config.dev_name);
else if (key == TAG_NAME_CONFIG_KEY)
Serial.println(config.tag_name);
}
else if (line == "reboot")
{
Serial.println("Rebooting...");
Serial.flush();
esp_restart();
}
else if (line == "help")
{
Serial.println("Commands:");
Serial.println(" set <key> <value> - Set a configuration value");
Serial.println(" show <key> - Show a configuration value");
Serial.println(" save - Save configuration to non-volatile storage");
Serial.println(" reboot - Reboot the device");
Serial.println(" help - Show this help message");
}
}
void setup()
{
Serial.begin(115200);
loadConfig();
WiFi.begin(config.wifi.ssid.c_str(), config.wifi.password.c_str());
MQTT_PUB_TOPIC = config.mqtt.topic + config.dev_name;
mqtt.setServer(config.mqtt.host.c_str(), config.mqtt.port);
mqtt.setKeepAlive(60);
mqtt.setSocketTimeout(5);
ble_queue = xQueueCreate(100, sizeof(BleEvent));
mqtt_queue = xQueueCreate(100, sizeof(NetEvent));
http_queue = xQueueCreate(100, sizeof(HttpEvent));
uuid_mtx = xSemaphoreCreateMutex();
xTaskCreatePinnedToCore(mqttTask, "mqtt", 4096, NULL, 1, NULL, 1);
xTaskCreatePinnedToCore(httpTask, "http", 4096, NULL, 1, NULL, 1);
NimBLEDevice::init(config.dev_name.c_str());
NimBLEScan *scan = NimBLEDevice::getScan();
scan->setScanCallbacks(&cb);
scan->setActiveScan(false);
scan->setDuplicateFilter(false);
scan->start(0, false, true);
}
void loop()
{
BleEvent ev;
while (xQueueReceive(ble_queue, &ev, 0))
processEvent(ev);
static uint32_t last_cleanup = 0;
if (millis() - last_cleanup >= config.interval.cleanup)
{
cleanupTags();
cleanupCache();
last_cleanup = millis();
}
while (Serial.available())
{
char c = Serial.read();
if (c == '\r')
continue;
if (c == '\n')
{
Serial.print(c);
serial_buffer.trim();
if (serial_buffer.length() > 0)
handleCommand(serial_buffer);
serial_buffer = "";
Serial.printf("%s> ", config.dev_name.c_str());
}
else if (c == '\b')
{
if (serial_buffer.length() > 0)
{
serial_buffer.remove(serial_buffer.length() - 1);
Serial.print("\b \b");
}
}
else
{
Serial.print(c);
serial_buffer += c;
}
}
}
+112
View File
@@ -0,0 +1,112 @@
#ifndef MAIN_HPP
#define MAIN_HPP
#include <Arduino.h>
#include <NimBLEDevice.h>
#include <string>
#define CLEANUP_INTERVAL_CONFIG_KEY "int.cln"
#define TAG_TIMEOUT_CONFIG_KEY "int.tagto"
#define UUID_CACHE_RETRY_TIMEOUT_CONFIG_KEY "int.urtry"
#define UUID_CACHE_CLEAN_TIMEOUT_CONFIG_KEY "int.ucclean"
#define API_URL_CONFIG_KEY "api.url"
#define UUID_CHECK_ENDPOINT_CONFIG_KEY "api.uuidchk"
#define MQTT_HOST_CONFIG_KEY "mq.host"
#define MQTT_PORT_CONFIG_KEY "mq.port"
#define MQTT_USER_CONFIG_KEY "mq.user"
#define MQTT_PASS_CONFIG_KEY "mq.pass"
#define MQTT_TOPIC_CONFIG_KEY "mq.topic"
#define WIFI_SSID_CONFIG_KEY "wf.ssid"
#define WIFI_PASS_CONFIG_KEY "wf.pass"
#define DEV_NAME_CONFIG_KEY "dev.name"
#define TAG_NAME_CONFIG_KEY "tag.name"
#define CLEANUP_INTERVAL 5 * 1000 // ms
#define TAG_TIMEOUT 60 * 1000 // ms
#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 TAG_NAME "X6TAG"
#define MQTT_HOST "192.168.1.2"
#define MQTT_PORT 1883
#define MQTT_USER "user"
#define MQTT_PASS "pass"
#define MQTT_TOPIC "scanners/"
#define WIFI_SSID "COSMOTE-489882"
#define WIFI_PASS "x32hbh54673ngccdsfa9"
#define DEV_NAME "Scanner_Room_A"
struct Config
{
struct Interval
{
uint32_t cleanup;
uint32_t tag_timeout;
uint32_t uuid_cache_retry_timeout;
uint32_t uuid_cache_clean_timeout;
} interval;
struct API
{
String url;
String uuid_check_endpoint;
} api;
struct MQTT
{
String host;
uint16_t port;
String username;
String password;
String topic;
} mqtt;
struct WiFi
{
String ssid;
String password;
} wifi;
String dev_name;
String tag_name;
};
struct BleEvent
{
char uuid[37];
int8_t rssi;
};
struct NetEvent
{
char uuid[37];
float ema;
};
struct HttpEvent
{
char uuid[37];
};
enum class UUIDState
{
PENDING,
RETRY,
VALID,
INVALID
};
struct UUIDCacheEntry
{
UUIDState state;
uint32_t last_check;
};
class CB : public NimBLEScanCallbacks
{
public:
void onResult(const NimBLEAdvertisedDevice *d) override;
};
#endif
+177
View File
@@ -0,0 +1,177 @@
#include <iostream>
#include "Tag.hpp"
uint32_t Tag::getLastSeen() const { return this->last_seen; }
void Tag::addSample(int8_t rssi)
{
#ifdef DEBUG
Serial.printf("[addSample()]: rssi_window[%d] = %d\n", this->win_idx, rssi);
Serial.flush();
#endif
this->rssi_window[this->win_idx] = rssi;
this->win_idx = (this->win_idx + 1) % 5;
#ifdef DEBUG
Serial.printf("[addSample()]: count = %d", this->count);
Serial.flush();
#endif
if (this->count < 5)
{
this->count++;
#ifdef DEBUG
Serial.printf(" -> %d", this->count);
Serial.flush();
#endif
}
#ifdef DEBUG
Serial.println();
Serial.flush();
#endif
int8_t median = getMedian();
#ifdef DEBUG
Serial.printf("[addSample()]: median = %d\n", median);
Serial.flush();
#endif
// EMA update
if (!this->init)
{
#ifdef DEBUG
Serial.printf("[addSample()]: init = false\n");
Serial.flush();
#endif
this->ema = median;
#ifdef DEBUG
Serial.printf("[addSample()]: ema = %f\n", this->ema);
Serial.flush();
#endif
this->init = true;
}
else
{
this->ema = 0.8f * this->ema + 0.2f * median;
#ifdef DEBUG
Serial.printf("[addSample()]: ema = %f\n", this->ema);
Serial.flush();
#endif
}
last_seen = millis();
}
int8_t Tag::getMedian() const
{
if (this->count == 0)
{
#ifdef DEBUG
Serial.printf("[getMedian()]: count = 0\n");
Serial.flush();
#endif
return 0;
}
int8_t sorted[5];
#ifdef DEBUG
Serial.printf("[getMedian()]: rssi_window[] = {");
Serial.flush();
#endif
for (uint8_t i = 0; i < this->count; i++)
{
#ifdef DEBUG
Serial.printf(" %d", rssi_window[i]);
Serial.flush();
#endif
sorted[i] = this->rssi_window[i];
}
#ifdef DEBUG
Serial.printf(" }\n");
Serial.flush();
#endif
std::sort(sorted, sorted + this->count);
return sorted[this->count / 2];
}
float Tag::getEMA() const { return this->ema; }
bool Tag::isReady() const { return count == 5; }
bool Tag::shouldPublish()
{
if (!this->isReady())
{
#ifdef DEBUG
Serial.printf("[shouldPublish()]: isReady = false\n");
Serial.flush();
#endif
return false;
}
uint32_t now = millis();
uint32_t time_delta = (this->prev_time == 0 ? INTERVAL : now - this->prev_time);
float ema_delta = std::abs(this->ema - this->prev_ema);
bool time_trigger = time_delta >= INTERVAL;
bool ema_trigger = ema_delta >= EMA_THRESHOLD;
#ifdef DEBUG
Serial.printf("[shouldPublish()]: time_delta = %u\n"
"[shouldPublish()]: ema_delta = %f\n"
"[shouldPublish()]: time_trigger = %s\n"
"[shouldPublish()]: ema_trigger = %s\n",
time_delta,
ema_delta,
(time_trigger ? "true" : "false"),
(ema_trigger ? "true" : "false"));
Serial.flush();
#endif
if (time_trigger || ema_trigger)
{
#ifdef DEBUG
Serial.printf("[shouldPublish()]: return true\n");
Serial.flush();
#endif
this->prev_time = now;
this->prev_ema = this->ema;
return true;
}
#ifdef DEBUG
Serial.printf("[shouldPublish()]: return false\n");
Serial.flush();
#endif
return false;
}
void Tag::reset()
{
this->win_idx = 0;
this->count = 0;
this->ema = 0;
this->init = false;
for (uint8_t i = 0; i < 5; i++)
this->rssi_window[i] = 0;
}
+41
View File
@@ -0,0 +1,41 @@
#ifndef TAG_HPP
#define TAG_HPP
#include <Arduino.h>
/*
* EMA_THRESHOLD Options:
* 1 - High Sensitivity / Fine Tracking
* 2 - Static Object / Presence Detection
* 3 - General Tracking (Default)
* 4-5 - Noisy Environment / Interference
*/
#define EMA_THRESHOLD 3
#define INTERVAL 5000 // ms
#define WINDOW_SIZE 5
// #define DEBUG 1
class Tag
{
private:
int8_t rssi_window[WINDOW_SIZE];
uint8_t win_idx = 0;
uint8_t count = 0;
uint32_t prev_time = 0;
uint32_t last_seen = 0;
float ema = 0;
float prev_ema = 0;
bool init = false;
public:
Tag() = default;
uint32_t getLastSeen() const;
void addSample(int8_t rssi);
int8_t getMedian() const;
float getEMA() const;
bool isReady() const;
bool shouldPublish();
void reset();
};
#endif
+11
View File
@@ -0,0 +1,11 @@
This directory is intended for PlatformIO Test Runner and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
+249 -22
View File
@@ -1,21 +1,13 @@
#include <stdio.h>
#include <string.h>
#include "nvs_flash.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "services/gap/ble_svc_gap.h"
#include "driver/usb_serial_jtag.h"
#include "main.h"
static const char *TAG = "BLE";
static const ble_uuid128_t service_uuid =
BLE_UUID128_INIT(
0xF0, 0xDE, 0xBC, 0x9A,
0x78, 0x56, 0x34, 0x12,
0xF0, 0xDE, 0xBC, 0x9A,
0x78, 0x56, 0x34, 0x12);
static config_t config;
static void start_adv(void)
{
@@ -24,19 +16,18 @@ static void start_adv(void)
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
fields.uuids128 = (ble_uuid128_t *)&service_uuid;
fields.name = (uint8_t *)config.name;
fields.name_len = strlen(config.name);
fields.name_is_complete = 1;
fields.uuids128 = (ble_uuid128_t *)&config.uuid;
fields.num_uuids128 = 1;
fields.uuids128_is_complete = 1;
uint8_t mfg_data[5] = {0x34, 0x34, 0x34, 0x34, 0x34};
fields.mfg_data = mfg_data;
fields.mfg_data_len = sizeof(mfg_data);
int rc = ble_gap_adv_set_fields(&fields);
if (rc != 0)
{
ESP_LOGE(TAG, "adv set fields failed: %d", rc);
ESP_LOGE(config.name, "adv set fields failed: %d", rc);
return;
}
@@ -58,17 +49,17 @@ static void start_adv(void)
if (rc != 0)
{
ESP_LOGE(TAG, "adv start failed: %d", rc);
ESP_LOGE(config.name, "adv start failed: %d", rc);
}
else
{
ESP_LOGI(TAG, "advertising started");
ESP_LOGI(config.name, "advertising started");
}
}
static void on_sync(void)
{
ESP_LOGI(TAG, "BLE synced");
ESP_LOGI(config.name, "BLE synced");
start_adv();
}
@@ -78,15 +69,251 @@ static void host_task(void *param)
nimble_port_run();
}
static bool uuid_from_string(const char *str, ble_uuid128_t *uuid)
{
unsigned int b[16];
int rc = sscanf(str,
"%02x%02x%02x%02x-"
"%02x%02x-"
"%02x%02x-"
"%02x%02x-"
"%02x%02x%02x%02x%02x%02x",
&b[15], &b[14], &b[13], &b[12],
&b[11], &b[10],
&b[9], &b[8],
&b[7], &b[6],
&b[5], &b[4], &b[3],
&b[2], &b[1], &b[0]);
if (rc != 16)
return false;
uuid->u.type = BLE_UUID_TYPE_128;
for (int i = 0; i < 16; i++)
uuid->value[i] = (uint8_t)b[i];
return true;
}
static esp_err_t reset_config()
{
nvs_handle_t nvs;
esp_err_t err = nvs_open("config", NVS_READWRITE, &nvs);
if (err != ESP_OK)
return err;
err = nvs_erase_all(nvs);
if (err != ESP_OK)
goto exit;
err = nvs_commit(nvs);
exit:
nvs_close(nvs);
return err;
}
static esp_err_t save_config()
{
nvs_handle_t nvs;
esp_err_t err = nvs_open("config", NVS_READWRITE, &nvs);
if (err != ESP_OK)
return err;
err = nvs_set_str(nvs, "uuid", config.uuid_str);
if (err != ESP_OK)
goto exit;
err = nvs_set_str(nvs, "name", config.name);
if (err != ESP_OK)
goto exit;
err = nvs_set_u32(nvs, "adv_int", config.adv_interval);
if (err != ESP_OK)
goto exit;
err = nvs_commit(nvs);
exit:
nvs_close(nvs);
return err;
}
static esp_err_t load_config()
{
nvs_handle_t nvs;
esp_err_t err = nvs_open("config", NVS_READONLY, &nvs);
strcpy(config.uuid_str, DEFAULT_UUID);
strcpy(config.name, DEFAULT_NAME);
config.adv_interval = DEFAULT_ADV_INTERVAL;
uuid_from_string(config.uuid_str, &config.uuid);
if (err != ESP_OK)
return err;
size_t len;
len = sizeof(config.uuid_str);
nvs_get_str(nvs, "uuid", config.uuid_str, &len);
len = sizeof(config.name);
nvs_get_str(nvs, "name", config.name, &len);
nvs_get_u32(nvs, "adv_int", &config.adv_interval);
nvs_close(nvs);
uuid_from_string(config.uuid_str, &config.uuid);
return ESP_OK;
}
void serial_task(void *arg)
{
char line[128];
char buf[64];
int pos = 0;
while (1)
{
uint8_t ch;
int len = usb_serial_jtag_read_bytes(&ch, 1, portMAX_DELAY);
if (len > 0)
{
if (ch == '\n' || ch == '\r')
{
usb_serial_jtag_write_bytes("\n", 1, 20 / portTICK_PERIOD_MS);
line[pos] = '\0';
if (pos > 0)
{
if (strncmp(line, "set uuid ", 9) == 0)
{
if (uuid_from_string(line + 9, &config.uuid))
strcpy(config.uuid_str, line + 9);
}
else if (strncmp(line, "set name ", 9) == 0)
{
strncpy(config.name, line + 9, sizeof(config.name) - 1);
config.name[sizeof(config.name) - 1] = '\0';
}
else if (strncmp(line, "set adv_interval ", 17) == 0)
{
uint32_t interval = atoi(line + 17);
if (interval > 0)
config.adv_interval = interval;
}
else if (strcmp(line, "show uuid") == 0)
{
usb_serial_jtag_write_bytes(config.uuid_str, strlen(config.uuid_str), 20 / portTICK_PERIOD_MS);
usb_serial_jtag_write_bytes("\n", 1, 20 / portTICK_PERIOD_MS);
}
else if (strcmp(line, "show name") == 0)
{
usb_serial_jtag_write_bytes(config.name, strlen(config.name), 20 / portTICK_PERIOD_MS);
usb_serial_jtag_write_bytes("\n", 1, 20 / portTICK_PERIOD_MS);
}
else if (strcmp(line, "show adv_interval") == 0)
{
int len = snprintf(buf, sizeof(buf), "%lu\n", (unsigned long)config.adv_interval);
usb_serial_jtag_write_bytes(buf, len, 20 / portTICK_PERIOD_MS);
}
else if (strcmp(line, "save") == 0)
save_config();
else if (strcmp(line, "reset") == 0)
{
reset_config();
esp_restart();
}
else if (strcmp(line, "reboot") == 0)
{
usb_serial_jtag_write_bytes("Rebooting...\n", 13, 20 / portTICK_PERIOD_MS);
esp_restart();
}
else if (strcmp(line, "help") == 0)
{
usb_serial_jtag_write_bytes("Commands:\n"
" set uuid <uuid>\n"
" set name <name>\n"
" set adv_interval <ms>\n"
" show uuid\n"
" show name\n"
" show adv_interval\n"
" save\n"
" reset\n"
" reboot\n"
" help\n",
145, 20 / portTICK_PERIOD_MS);
}
}
pos = 0;
int len = snprintf(buf, sizeof(buf), "%s> ", config.name);
usb_serial_jtag_write_bytes(buf, len, 20 / portTICK_PERIOD_MS);
}
else if (ch == 0x08)
{
if (pos > 0)
{
pos--;
usb_serial_jtag_write_bytes("\b \b", 3, 20 / portTICK_PERIOD_MS);
}
}
else
{
if (pos < sizeof(line) - 1)
{
line[pos++] = ch;
usb_serial_jtag_write_bytes((char *)&ch, 1, 20 / portTICK_PERIOD_MS);
}
}
}
}
}
void app_main(void)
{
ESP_ERROR_CHECK(nvs_flash_init());
usb_serial_jtag_driver_config_t cfg = USB_SERIAL_JTAG_DRIVER_CONFIG_DEFAULT();
usb_serial_jtag_driver_install(&cfg);
load_config();
nimble_port_init();
ble_svc_gap_init();
ble_svc_gap_device_name_set("XIAO_C6");
ble_svc_gap_device_name_set("X6TAG");
ble_hs_cfg.sync_cb = on_sync;
xTaskCreate(
serial_task,
"serial_task",
4096,
NULL,
5,
NULL);
nimble_port_freertos_init(host_task);
}
+18
View File
@@ -0,0 +1,18 @@
#ifndef MAIN_H
#define MAIN_H
#include "host/ble_hs.h"
#define DEFAULT_UUID "12345678-9abc-def0-1234-56789abcdef0"
#define DEFAULT_NAME "X6TAG"
#define DEFAULT_ADV_INTERVAL 0x80
typedef struct
{
ble_uuid128_t uuid;
char uuid_str[37];
char name[32];
uint32_t adv_interval;
} config_t;
#endif