diff --git a/surveillance-system/services.pkl b/surveillance-system/services.pkl
index 3ddc844..dbd1bd9 100644
--- a/surveillance-system/services.pkl
+++ b/surveillance-system/services.pkl
@@ -5,45 +5,45 @@ bindings {
name = "cameraCapture"
`local` = true
scheme = "http"
- host = "services"
- port = 8000
- endPoint = "/bell/on"
+ host = "localhost"
+ port = 8001
+ endPoint = "/capture"
method = "POST"
}
new HttpServiceImplementationBinding {
name = "detectPersons"
`local` = false
scheme = "http"
- host = "services"
- port = 8000
- endPoint = "/bell/off"
+ host = "localhost"
+ port = 8002
+ endPoint = "/detect"
method = "POST"
}
new HttpServiceImplementationBinding {
name = "alarmOn"
`local` = false
scheme = "http"
- host = "services"
- port = 8000
- endPoint = "/gate/up"
+ host = "localhost"
+ port = 8001
+ endPoint = "/alarm/on"
method = "POST"
}
new HttpServiceImplementationBinding {
name = "alarmOff"
`local` = false
scheme = "http"
- host = "services"
- port = 8000
- endPoint = "/gate/down"
+ host = "localhost"
+ port = 8001
+ endPoint = "/alarm/off"
method = "POST"
}
new HttpServiceImplementationBinding {
name = "analyze"
`local` = false
scheme = "http"
- host = "services"
- port = 8000
- endPoint = "/light/on"
+ host = "localhost"
+ port = 8003
+ endPoint = "/analyze"
method = "POST"
}
}
\ No newline at end of file
diff --git a/surveillance-system/services/README.md b/surveillance-system/services/README.md
new file mode 100644
index 0000000..8b4bd80
--- /dev/null
+++ b/surveillance-system/services/README.md
@@ -0,0 +1,28 @@
+# Services
+
+All services required by the video surveillance use case are found in [services](services) sub folder.
+- `iotservices`
+ Camera service which simulates image capturing. Adds random noise to each image to ensure uniqueness.
+- `edgeservices`
+ Person detection service which checks whether the provided image contains any persons.
+- `cloudservices`
+ Service which simulates face analysis. Marks persons as threats (20% chance) or no threat (otherwise).
+
+Used by both the Cirrina and Sonataflow version of the smart factory use case.
+
+## Usage with Docker Compose
+
+To run all services locally on a single device, you can use Docker Compose:
+
+```
+docker compose up
+```
+
+By default, the services will use protobuf to serialize/deserialize response/request data. This can be disabled by
+setting an environment variable beforehand:
+
+```
+PROTO=false docker compose up
+```
+
+Individual Dockerfiles for separate manual builds can be found in the respective service folders.
\ No newline at end of file
diff --git a/surveillance-system/services/cloudservices/ContextVariable_pb2.py b/surveillance-system/services/cloudservices/ContextVariable_pb2.py
new file mode 100644
index 0000000..0f2e815
--- /dev/null
+++ b/surveillance-system/services/cloudservices/ContextVariable_pb2.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: ContextVariable.proto
+# Protobuf Python Version: 4.25.3
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x43ontextVariable.proto\x12\x08\x65xchange\"?\n\x0f\x43ontextVariable\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1e\n\x05value\x18\x02 \x01(\x0b\x32\x0f.exchange.Value\";\n\x10\x43ontextVariables\x12\'\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x19.exchange.ContextVariable\"\x89\x01\n\x05Value\x12\x11\n\x07integer\x18\x01 \x01(\x05H\x00\x12\x0f\n\x05\x66loat\x18\x02 \x01(\x02H\x00\x12\x0e\n\x04long\x18\x03 \x01(\x03H\x00\x12\x10\n\x06\x64ouble\x18\x04 \x01(\x01H\x00\x12\x10\n\x06string\x18\x05 \x01(\tH\x00\x12\x0e\n\x04\x62ool\x18\x06 \x01(\x08H\x00\x12\x0f\n\x05\x62ytes\x18\x07 \x01(\x0cH\x00\x42\x07\n\x05valueBK\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\x15\x43ontextVariableProtosP\x00\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'ContextVariable_pb2', _globals)
+if _descriptor._USE_C_DESCRIPTORS == False:
+ _globals['DESCRIPTOR']._options = None
+ _globals['DESCRIPTOR']._serialized_options = b'\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\025ContextVariableProtosP\000'
+ _globals['_CONTEXTVARIABLE']._serialized_start=35
+ _globals['_CONTEXTVARIABLE']._serialized_end=98
+ _globals['_CONTEXTVARIABLES']._serialized_start=100
+ _globals['_CONTEXTVARIABLES']._serialized_end=159
+ _globals['_VALUE']._serialized_start=162
+ _globals['_VALUE']._serialized_end=299
+# @@protoc_insertion_point(module_scope)
diff --git a/surveillance-system/services/cloudservices/Dockerfile b/surveillance-system/services/cloudservices/Dockerfile
new file mode 100644
index 0000000..775e462
--- /dev/null
+++ b/surveillance-system/services/cloudservices/Dockerfile
@@ -0,0 +1,13 @@
+FROM python:3.11
+
+COPY requirements.txt /tmp/requirements.txt
+RUN pip install --no-cache-dir -r /tmp/requirements.txt
+
+WORKDIR /app
+COPY ContextVariable_pb2.py .
+COPY Event_pb2.py .
+COPY services.py .
+
+ENV PROTO="true"
+
+CMD ["uvicorn", "services:app", "--host", "0.0.0.0", "--workers", "33", "--port", "8000"]
\ No newline at end of file
diff --git a/surveillance-system/services/cloudservices/Event_pb2.py b/surveillance-system/services/cloudservices/Event_pb2.py
new file mode 100644
index 0000000..9f4a29a
--- /dev/null
+++ b/surveillance-system/services/cloudservices/Event_pb2.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: Event.proto
+# Protobuf Python Version: 4.25.3
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+import ContextVariable_pb2 as ContextVariable__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x45vent.proto\x12\x08\x65xchange\x1a\x15\x43ontextVariable.proto\"\xb7\x01\n\x05\x45vent\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12(\n\x07\x63hannel\x18\x03 \x01(\x0e\x32\x17.exchange.Event.Channel\x12\'\n\x04\x64\x61ta\x18\x04 \x03(\x0b\x32\x19.exchange.ContextVariable\"A\n\x07\x43hannel\x12\x0c\n\x08INTERNAL\x10\x00\x12\x0c\n\x08\x45XTERNAL\x10\x01\x12\n\n\x06GLOBAL\x10\x02\x12\x0e\n\nPERIPHERAL\x10\x03\x42\x41\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\x0b\x45ventProtosP\x00\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'Event_pb2', _globals)
+if _descriptor._USE_C_DESCRIPTORS == False:
+ _globals['DESCRIPTOR']._options = None
+ _globals['DESCRIPTOR']._serialized_options = b'\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\013EventProtosP\000'
+ _globals['_EVENT']._serialized_start=49
+ _globals['_EVENT']._serialized_end=232
+ _globals['_EVENT_CHANNEL']._serialized_start=167
+ _globals['_EVENT_CHANNEL']._serialized_end=232
+# @@protoc_insertion_point(module_scope)
diff --git a/surveillance-system/services/cloudservices/requirements.txt b/surveillance-system/services/cloudservices/requirements.txt
new file mode 100644
index 0000000..1b099f6
--- /dev/null
+++ b/surveillance-system/services/cloudservices/requirements.txt
@@ -0,0 +1,6 @@
+numpy<2
+fastapi==0.104.1
+opencv-python-headless==4.8.1.78
+opencv-contrib-python-headless==4.8.1.78
+uvicorn
+protobuf
\ No newline at end of file
diff --git a/surveillance-system/services/cloudservices/services.py b/surveillance-system/services/cloudservices/services.py
new file mode 100644
index 0000000..893c2c6
--- /dev/null
+++ b/surveillance-system/services/cloudservices/services.py
@@ -0,0 +1,109 @@
+import ContextVariable_pb2
+
+import cv2
+import uvicorn
+
+import numpy as np
+
+from fastapi import FastAPI, Request, HTTPException, Response
+
+import json
+import os
+import random
+import base64
+import time
+import hashlib
+
+local_random = random.Random(os.getpid())
+
+app = FastAPI()
+
+# Decide whether protobuf should be used (optional environment variable)
+proto = "PROTO" not in os.environ or os.environ["PROTO"].lower() in ["true", "t", "1"]
+
+
+# For logging detection times
+def log_hash(data: bytes):
+ # Compute a consistent hash
+ sha256 = hashlib.sha256()
+ sha256.update(data)
+ hash = sha256.hexdigest()
+
+ # Acquire the current timestamp in milliseconds
+ timestamp = time.time_ns() / 1_000_000.0
+ log_entry = f"{hash},{timestamp}\n"
+
+ # Append to log file
+ with open("/tmp/log_analysis.csv", "a") as log_file:
+ log_file.write(log_entry)
+
+
+@app.post("/analyze")
+async def detect(request: Request):
+ time_start = time.time_ns() / 1_000_000.0
+
+ if proto:
+ # Read the raw request body
+ body = await request.body()
+
+ # Parse the protobuf message
+ context_variables = ContextVariable_pb2.ContextVariables()
+ context_variables.ParseFromString(body)
+
+ # Extract the image from the protobuf message
+ image_bytes = None
+ for context_variable in context_variables.data:
+ if context_variable.name == "image":
+ image_bytes = context_variable.value.bytes
+ break
+ else:
+ # Parse the request variables
+ context_variables = await request.json()
+
+ # Get the image from the request json
+ if "image" in context_variables:
+ image_bytes = base64.b64decode(context_variables["image"].encode("utf-8"))
+
+ if image_bytes is None:
+ raise HTTPException(status_code=400, detail="No image provided in the request")
+
+ # Read the image
+ np_arr = np.frombuffer(image_bytes, np.uint8)
+ image = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
+
+ if image is None:
+ raise HTTPException(status_code=400, detail="Failed to decode the image")
+
+ is_threat = local_random.random() < (1.0 / 5.0)
+
+ # Prepare output data
+ if proto:
+ # Create response protobuf message
+ response_context_variables = ContextVariable_pb2.ContextVariables()
+ threats_context_variable = ContextVariable_pb2.ContextVariable(
+ name="hasThreat",
+ value=ContextVariable_pb2.Value(bool=is_threat),
+ )
+ response_context_variables.data.append(threats_context_variable)
+
+ # Serialize the response to protobuf format
+ response = response_context_variables.SerializeToString()
+ media_type = "application/x-protobuf"
+ else:
+ response = json.dumps({"hasThreat": is_threat})
+ media_type = "application/json"
+
+ time_end = time.time_ns() / 1_000_000.0
+
+ with open("/tmp/time_analysis.csv", "a") as log_file:
+ log_file.write(f"{time_end - time_start}\n")
+
+ # Log data
+ log_hash(image_bytes)
+
+ # Return the protobuf response
+ return Response(content=response, media_type=media_type)
+
+
+if __name__ == "__main__":
+ uvicorn.run(app, host="0.0.0.0", port=8000)
diff --git a/surveillance-system/services/compose.yaml b/surveillance-system/services/compose.yaml
new file mode 100644
index 0000000..a35d452
--- /dev/null
+++ b/surveillance-system/services/compose.yaml
@@ -0,0 +1,25 @@
+services:
+ iotservices:
+ build:
+ context: ./iotservices
+ dockerfile: Dockerfile
+ ports:
+ - "8001:8000"
+ environment:
+ - "PROTO=${PROTO:-true}"
+ edgeservices:
+ build:
+ context: ./edgeservices
+ dockerfile: Dockerfile
+ ports:
+ - "8002:8000"
+ environment:
+ - "PROTO=${PROTO:-true}"
+ cloudservices:
+ build:
+ context: ./cloudservices
+ dockerfile: Dockerfile
+ ports:
+ - "8003:8000"
+ environment:
+ - "PROTO=${PROTO:-true}"
\ No newline at end of file
diff --git a/surveillance-system/services/edgeservices/ContextVariable_pb2.py b/surveillance-system/services/edgeservices/ContextVariable_pb2.py
new file mode 100644
index 0000000..0f2e815
--- /dev/null
+++ b/surveillance-system/services/edgeservices/ContextVariable_pb2.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: ContextVariable.proto
+# Protobuf Python Version: 4.25.3
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x43ontextVariable.proto\x12\x08\x65xchange\"?\n\x0f\x43ontextVariable\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1e\n\x05value\x18\x02 \x01(\x0b\x32\x0f.exchange.Value\";\n\x10\x43ontextVariables\x12\'\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x19.exchange.ContextVariable\"\x89\x01\n\x05Value\x12\x11\n\x07integer\x18\x01 \x01(\x05H\x00\x12\x0f\n\x05\x66loat\x18\x02 \x01(\x02H\x00\x12\x0e\n\x04long\x18\x03 \x01(\x03H\x00\x12\x10\n\x06\x64ouble\x18\x04 \x01(\x01H\x00\x12\x10\n\x06string\x18\x05 \x01(\tH\x00\x12\x0e\n\x04\x62ool\x18\x06 \x01(\x08H\x00\x12\x0f\n\x05\x62ytes\x18\x07 \x01(\x0cH\x00\x42\x07\n\x05valueBK\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\x15\x43ontextVariableProtosP\x00\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'ContextVariable_pb2', _globals)
+if _descriptor._USE_C_DESCRIPTORS == False:
+ _globals['DESCRIPTOR']._options = None
+ _globals['DESCRIPTOR']._serialized_options = b'\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\025ContextVariableProtosP\000'
+ _globals['_CONTEXTVARIABLE']._serialized_start=35
+ _globals['_CONTEXTVARIABLE']._serialized_end=98
+ _globals['_CONTEXTVARIABLES']._serialized_start=100
+ _globals['_CONTEXTVARIABLES']._serialized_end=159
+ _globals['_VALUE']._serialized_start=162
+ _globals['_VALUE']._serialized_end=299
+# @@protoc_insertion_point(module_scope)
diff --git a/surveillance-system/services/edgeservices/Dockerfile b/surveillance-system/services/edgeservices/Dockerfile
new file mode 100644
index 0000000..775e462
--- /dev/null
+++ b/surveillance-system/services/edgeservices/Dockerfile
@@ -0,0 +1,13 @@
+FROM python:3.11
+
+COPY requirements.txt /tmp/requirements.txt
+RUN pip install --no-cache-dir -r /tmp/requirements.txt
+
+WORKDIR /app
+COPY ContextVariable_pb2.py .
+COPY Event_pb2.py .
+COPY services.py .
+
+ENV PROTO="true"
+
+CMD ["uvicorn", "services:app", "--host", "0.0.0.0", "--workers", "33", "--port", "8000"]
\ No newline at end of file
diff --git a/surveillance-system/services/edgeservices/Event_pb2.py b/surveillance-system/services/edgeservices/Event_pb2.py
new file mode 100644
index 0000000..9f4a29a
--- /dev/null
+++ b/surveillance-system/services/edgeservices/Event_pb2.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: Event.proto
+# Protobuf Python Version: 4.25.3
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+import ContextVariable_pb2 as ContextVariable__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x45vent.proto\x12\x08\x65xchange\x1a\x15\x43ontextVariable.proto\"\xb7\x01\n\x05\x45vent\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12(\n\x07\x63hannel\x18\x03 \x01(\x0e\x32\x17.exchange.Event.Channel\x12\'\n\x04\x64\x61ta\x18\x04 \x03(\x0b\x32\x19.exchange.ContextVariable\"A\n\x07\x43hannel\x12\x0c\n\x08INTERNAL\x10\x00\x12\x0c\n\x08\x45XTERNAL\x10\x01\x12\n\n\x06GLOBAL\x10\x02\x12\x0e\n\nPERIPHERAL\x10\x03\x42\x41\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\x0b\x45ventProtosP\x00\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'Event_pb2', _globals)
+if _descriptor._USE_C_DESCRIPTORS == False:
+ _globals['DESCRIPTOR']._options = None
+ _globals['DESCRIPTOR']._serialized_options = b'\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\013EventProtosP\000'
+ _globals['_EVENT']._serialized_start=49
+ _globals['_EVENT']._serialized_end=232
+ _globals['_EVENT_CHANNEL']._serialized_start=167
+ _globals['_EVENT_CHANNEL']._serialized_end=232
+# @@protoc_insertion_point(module_scope)
diff --git a/surveillance-system/services/edgeservices/requirements.txt b/surveillance-system/services/edgeservices/requirements.txt
new file mode 100644
index 0000000..1b099f6
--- /dev/null
+++ b/surveillance-system/services/edgeservices/requirements.txt
@@ -0,0 +1,6 @@
+numpy<2
+fastapi==0.104.1
+opencv-python-headless==4.8.1.78
+opencv-contrib-python-headless==4.8.1.78
+uvicorn
+protobuf
\ No newline at end of file
diff --git a/surveillance-system/services/edgeservices/services.py b/surveillance-system/services/edgeservices/services.py
new file mode 100644
index 0000000..7b9574b
--- /dev/null
+++ b/surveillance-system/services/edgeservices/services.py
@@ -0,0 +1,111 @@
+import ContextVariable_pb2
+
+import cv2
+import uvicorn
+
+import numpy as np
+
+from fastapi import FastAPI, Request, HTTPException, Response
+
+import json
+import os
+import random
+import base64
+import time
+
+app = FastAPI()
+
+# Decide whether protobuf should be used (optional environment variable)
+proto = "PROTO" not in os.environ or os.environ["PROTO"].lower() in ["true", "t", "1"]
+
+# Create a global HOG descriptor
+hog = cv2.HOGDescriptor()
+hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())
+
+
+# Utility function for debugging
+# def generate_random_colors(n: int):
+# random.seed(0)
+#
+# return [
+# (random.randint(128, 255), random.randint(128, 255), random.randint(128, 255))
+# for _ in range(n)
+# ]
+
+
+@app.post("/detect")
+async def detect(request: Request):
+ time_start = time.time_ns() / 1_000_000.0
+
+ if proto:
+ # Read the raw request body
+ body = await request.body()
+
+ # Parse the protobuf message
+ context_variables = ContextVariable_pb2.ContextVariables()
+ context_variables.ParseFromString(body)
+
+ # Extract the image from the protobuf message
+ image_bytes = None
+ for context_variable in context_variables.data:
+ if context_variable.name == "image":
+ image_bytes = context_variable.value.bytes
+ break
+ else:
+ # Parse the request variables
+ context_variables = await request.json()
+
+ # Get the image from the request json
+ if "image" in context_variables:
+ image_bytes = base64.b64decode(context_variables["image"].encode("utf-8"))
+
+ if image_bytes is None:
+ raise HTTPException(status_code=400, detail="No image provided in the request")
+
+ # Read the image
+ np_arr = np.frombuffer(image_bytes, np.uint8)
+ image = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
+
+ if image is None:
+ raise HTTPException(status_code=400, detail="Failed to decode the image")
+
+ # Perform detection
+ (regions, _) = hog.detectMultiScale(
+ image, winStride=(4, 4), padding=(4, 4), scale=1.05
+ )
+
+ # For drawing detections:
+ # image_dbg = image.copy()
+ # colors = generate_random_colors(len(regions))
+ # for color, (x, y, w, h) in zip(colors, regions):
+ # cv2.rectangle(image_dbg, (x, y), (x + w, y + h), color, 2)
+ # cv2.imwrite("image_detected.jpg", image_dbg)
+
+ # Prepare output data
+ if proto:
+ # Create response protobuf message
+ response_context_variables = ContextVariable_pb2.ContextVariables()
+ detections_context_variable = ContextVariable_pb2.ContextVariable(
+ name="hasDetectedPersons",
+ value=ContextVariable_pb2.Value(bool=len(regions) > 0),
+ )
+ response_context_variables.data.append(detections_context_variable)
+
+ # Serialize the response to protobuf format
+ response = response_context_variables.SerializeToString()
+ media_type = "application/x-protobuf"
+ else:
+ response = json.dumps({"hasDetectedPersons": len(regions) > 0})
+ media_type = "application/json"
+
+ time_end = time.time_ns() / 1_000_000.0
+
+ with open("/tmp/time_detection.csv", "a") as log_file:
+ log_file.write(f"{time_end - time_start}\n")
+
+ # Return the protobuf response
+ return Response(content=response, media_type=media_type)
+
+
+if __name__ == "__main__":
+ uvicorn.run(app, host="0.0.0.0", port=8000)
diff --git a/surveillance-system/services/iotservices/ContextVariable_pb2.py b/surveillance-system/services/iotservices/ContextVariable_pb2.py
new file mode 100644
index 0000000..a4ba5b8
--- /dev/null
+++ b/surveillance-system/services/iotservices/ContextVariable_pb2.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: ContextVariable.proto
+# Protobuf Python Version: 4.25.3
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
+ b'\n\x15\x43ontextVariable.proto\x12\x08\x65xchange"?\n\x0f\x43ontextVariable\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1e\n\x05value\x18\x02 \x01(\x0b\x32\x0f.exchange.Value";\n\x10\x43ontextVariables\x12\'\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x19.exchange.ContextVariable"\x89\x01\n\x05Value\x12\x11\n\x07integer\x18\x01 \x01(\x05H\x00\x12\x0f\n\x05\x66loat\x18\x02 \x01(\x02H\x00\x12\x0e\n\x04long\x18\x03 \x01(\x03H\x00\x12\x10\n\x06\x64ouble\x18\x04 \x01(\x01H\x00\x12\x10\n\x06string\x18\x05 \x01(\tH\x00\x12\x0e\n\x04\x62ool\x18\x06 \x01(\x08H\x00\x12\x0f\n\x05\x62ytes\x18\x07 \x01(\x0cH\x00\x42\x07\n\x05valueBK\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\x15\x43ontextVariableProtosP\x00\x62\x06proto3'
+)
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "ContextVariable_pb2", _globals)
+if _descriptor._USE_C_DESCRIPTORS == False:
+ _globals["DESCRIPTOR"]._options = None
+ _globals["DESCRIPTOR"]._serialized_options = (
+ b"\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\025ContextVariableProtosP\000"
+ )
+ _globals["_CONTEXTVARIABLE"]._serialized_start = 35
+ _globals["_CONTEXTVARIABLE"]._serialized_end = 98
+ _globals["_CONTEXTVARIABLES"]._serialized_start = 100
+ _globals["_CONTEXTVARIABLES"]._serialized_end = 159
+ _globals["_VALUE"]._serialized_start = 162
+ _globals["_VALUE"]._serialized_end = 299
+# @@protoc_insertion_point(module_scope)
diff --git a/surveillance-system/services/iotservices/Dockerfile b/surveillance-system/services/iotservices/Dockerfile
new file mode 100644
index 0000000..9c3b770
--- /dev/null
+++ b/surveillance-system/services/iotservices/Dockerfile
@@ -0,0 +1,14 @@
+FROM python:3.11
+
+COPY requirements.txt /tmp/requirements.txt
+RUN pip install --no-cache-dir -r /tmp/requirements.txt
+
+WORKDIR /app
+COPY ContextVariable_pb2.py .
+COPY Event_pb2.py .
+COPY services.py .
+COPY resources resources
+
+ENV PROTO="true"
+
+CMD ["uvicorn", "services:app", "--host", "0.0.0.0", "--workers", "33", "--port", "8000"]
\ No newline at end of file
diff --git a/surveillance-system/services/iotservices/Event_pb2.py b/surveillance-system/services/iotservices/Event_pb2.py
new file mode 100644
index 0000000..9f4a29a
--- /dev/null
+++ b/surveillance-system/services/iotservices/Event_pb2.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: Event.proto
+# Protobuf Python Version: 4.25.3
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+import ContextVariable_pb2 as ContextVariable__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x45vent.proto\x12\x08\x65xchange\x1a\x15\x43ontextVariable.proto\"\xb7\x01\n\x05\x45vent\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12(\n\x07\x63hannel\x18\x03 \x01(\x0e\x32\x17.exchange.Event.Channel\x12\'\n\x04\x64\x61ta\x18\x04 \x03(\x0b\x32\x19.exchange.ContextVariable\"A\n\x07\x43hannel\x12\x0c\n\x08INTERNAL\x10\x00\x12\x0c\n\x08\x45XTERNAL\x10\x01\x12\n\n\x06GLOBAL\x10\x02\x12\x0e\n\nPERIPHERAL\x10\x03\x42\x41\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\x0b\x45ventProtosP\x00\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'Event_pb2', _globals)
+if _descriptor._USE_C_DESCRIPTORS == False:
+ _globals['DESCRIPTOR']._options = None
+ _globals['DESCRIPTOR']._serialized_options = b'\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\013EventProtosP\000'
+ _globals['_EVENT']._serialized_start=49
+ _globals['_EVENT']._serialized_end=232
+ _globals['_EVENT_CHANNEL']._serialized_start=167
+ _globals['_EVENT_CHANNEL']._serialized_end=232
+# @@protoc_insertion_point(module_scope)
diff --git a/surveillance-system/services/iotservices/requirements.txt b/surveillance-system/services/iotservices/requirements.txt
new file mode 100644
index 0000000..1b099f6
--- /dev/null
+++ b/surveillance-system/services/iotservices/requirements.txt
@@ -0,0 +1,6 @@
+numpy<2
+fastapi==0.104.1
+opencv-python-headless==4.8.1.78
+opencv-contrib-python-headless==4.8.1.78
+uvicorn
+protobuf
\ No newline at end of file
diff --git a/surveillance-system/services/iotservices/resources/1.avi b/surveillance-system/services/iotservices/resources/1.avi
new file mode 100644
index 0000000..bc95938
Binary files /dev/null and b/surveillance-system/services/iotservices/resources/1.avi differ
diff --git a/surveillance-system/services/iotservices/resources/2.avi b/surveillance-system/services/iotservices/resources/2.avi
new file mode 100644
index 0000000..911e3b4
Binary files /dev/null and b/surveillance-system/services/iotservices/resources/2.avi differ
diff --git a/surveillance-system/services/iotservices/resources/3.avi b/surveillance-system/services/iotservices/resources/3.avi
new file mode 100644
index 0000000..6b10c47
Binary files /dev/null and b/surveillance-system/services/iotservices/resources/3.avi differ
diff --git a/surveillance-system/services/iotservices/resources/4.avi b/surveillance-system/services/iotservices/resources/4.avi
new file mode 100644
index 0000000..efe5f60
Binary files /dev/null and b/surveillance-system/services/iotservices/resources/4.avi differ
diff --git a/surveillance-system/services/iotservices/resources/5.avi b/surveillance-system/services/iotservices/resources/5.avi
new file mode 100644
index 0000000..c33a9d0
Binary files /dev/null and b/surveillance-system/services/iotservices/resources/5.avi differ
diff --git a/surveillance-system/services/iotservices/services.py b/surveillance-system/services/iotservices/services.py
new file mode 100644
index 0000000..e85a18e
--- /dev/null
+++ b/surveillance-system/services/iotservices/services.py
@@ -0,0 +1,158 @@
+import ContextVariable_pb2
+
+import cv2
+import uvicorn
+
+import numpy as np
+
+from fastapi import FastAPI, HTTPException, Request, Response
+from google.protobuf.json_format import MessageToDict
+from starlette.status import HTTP_200_OK
+
+import base64
+import json
+import hashlib
+import os
+import time
+
+FPS = 30
+
+ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
+
+VIDEOS_CAPTURES = {
+ 0: cv2.VideoCapture(os.path.join(ROOT_DIR, "resources", "1.avi")),
+ 1: cv2.VideoCapture(os.path.join(ROOT_DIR, "resources", "2.avi")),
+ 2: cv2.VideoCapture(os.path.join(ROOT_DIR, "resources", "3.avi")),
+ 3: cv2.VideoCapture(os.path.join(ROOT_DIR, "resources", "4.avi")),
+ 4: cv2.VideoCapture(os.path.join(ROOT_DIR, "resources", "5.avi")),
+}
+
+app = FastAPI()
+
+# Decide whether protobuf should be used (optional environment variable)
+proto = "PROTO" not in os.environ or os.environ["PROTO"].lower() in ["true", "t", "1"]
+
+
+# Get the current frame number
+def get_frame_number() -> int:
+ return round(int(time.time()) * FPS)
+
+
+# For logging detection times
+def log_hash(data: bytes):
+ # Compute a consistent hash
+ sha256 = hashlib.sha256()
+ sha256.update(data)
+ hash = sha256.hexdigest()
+
+ # Acquire the current timestamp in milliseconds
+ timestamp = time.time_ns() / 1_000_000.0
+ log_entry = f"{hash},{timestamp}\n"
+
+ # Append to log file
+ with open("/tmp/log_send.csv", "a") as log_file:
+ log_file.write(log_entry)
+
+
+@app.post("/alarm/on")
+async def alarm_on():
+ return Response(status_code=HTTP_200_OK)
+
+
+@app.post("/alarm/off")
+async def alarm_on():
+ return Response(status_code=HTTP_200_OK)
+
+
+@app.post("/capture")
+async def capture(request: Request):
+ video_number = None
+
+ # Attempt to read the video number
+ if proto:
+ # Read the raw request body
+ body = await request.body()
+
+ # Parse the protobuf message
+ context_variables = ContextVariable_pb2.ContextVariables()
+ context_variables.ParseFromString(body)
+
+ # Extract specific values
+ for context_variable in context_variables.data:
+ context_variable_dict = MessageToDict(context_variable)
+
+ # Check for cameraId and delay
+ if context_variable_dict["name"] == "cameraId":
+ video_number = context_variable_dict["value"].get("integer")
+ else:
+ # Parse the request variables
+ context_variables = await request.json()
+
+ # Get context variables from the request json
+ if "cameraId" in context_variables:
+ video_number = int(context_variables["cameraId"])
+
+ if video_number not in VIDEOS_CAPTURES:
+ raise HTTPException(
+ status_code=400, detail=f"Invalid video_number: {video_number}"
+ )
+
+ # Acquire the video frame
+ cap = VIDEOS_CAPTURES[video_number]
+
+ if not cap.isOpened():
+ print(f"Failed to open video {video_number}")
+ raise HTTPException(
+ status_code=400, detail=f"Failed to open video {video_number}"
+ )
+
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
+ frame_number = get_frame_number() % total_frames
+ cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
+
+ ret, frame = cap.read()
+ if not ret:
+ raise HTTPException(status_code=400, detail="Unable to capture frame")
+
+ frame = cv2.resize(frame, (640, 480))
+
+ # Introduce entropy to make every frame unique
+ random_values = (np.random.rand(1, frame.shape[1], frame.shape[2]) * 256).astype(
+ np.uint8
+ )
+
+ frame[:1, :, :] = random_values
+
+ # JPEG compression
+ jpeg_params = [int(cv2.IMWRITE_JPEG_QUALITY), 80]
+
+ _, buffer = cv2.imencode(".jpg", frame, jpeg_params)
+
+ buffer_bytes = buffer.tobytes()
+
+ # Prepare output data
+ if proto:
+ # Create response protobuf message
+ response_context_variables = ContextVariable_pb2.ContextVariables()
+ image_context_variable = ContextVariable_pb2.ContextVariable(
+ name="image", value=ContextVariable_pb2.Value(bytes=buffer_bytes)
+ )
+ response_context_variables.data.append(image_context_variable)
+
+ # Serialize the response to protobuf format
+ response = response_context_variables.SerializeToString()
+ media_type = "application/x-protobuf"
+ else:
+ buffer_base64 = base64.b64encode(buffer_bytes).decode("utf-8")
+
+ response = json.dumps({"image": buffer_base64})
+ media_type = "application/json"
+
+ log_hash(buffer_bytes)
+
+ # Return the protobuf response
+ return Response(content=response, media_type=media_type)
+
+
+if __name__ == "__main__":
+ uvicorn.run(app, host="0.0.0.0", port=8000)