diff --git a/recipes-app/iot2050-event-record/README.md b/recipes-app/iot2050-event-record/README.md new file mode 100644 index 000000000..92323f102 --- /dev/null +++ b/recipes-app/iot2050-event-record/README.md @@ -0,0 +1,94 @@ +# IOT2050 Event Record + +IOT2050 Event Record is using for reading and recording events, such as +power up, power loss, tilted, uncovered, watchdog reset and eio events. + +The core is a RPC service implemented with the help of gPRC. + +## Event record services + +The `iot2050-event-record.service` and `iot2050-event-serve.service` are systemd +services, they could be managed by `systemctl`. The `iot2050-event-record` collects events from various source then consume the API exposed by `iot2050-event-serve` +to wrap the collected events as `IOT2050-EventRecord` events and save them to +syslog. Then these wrapped events could be read by `journalctl` or by gRPC APIs. + +## Predefined events + +### Power events and Extended IO(EIO) events + +Power events and EIO events are injected to `journal(syslog)` by default. + +To check them on IOT2050: + +```sh +root@iot2050-debian:~# journalctl SYSLOG_IDENTIFIER=IOT2050-EventRecord +Oct 23 22:36:12 iot2050-debian IOT2050-EventRecord[323]: IOT2050_EVENTS.power: 2023-10-23 22:36:00 the device is powered up +Oct 23 22:40:21 iot2050-debian IOT2050-EventRecord[323]: IOT2050_EVENTS.power: 2023-10-23 22:34:4 [2] power loss +Oct 23 22:40:21 iot2050-debian IOT2050-EventRecord[323]: IOT2050_EVENTS.eio: 2023-10-23 22:12:54 [11] slot1 lost +``` + +### Sensor events + +By default, the sensor events, i.e. tilted and uncovered events, are disabled. +To enable them, please create a systemd drop-in for `iot2050-event-record.service`, +as follows: + +```sh +cp /usr/lib/iot2050/event/iot2050-event-record.conf /etc/systemd/system/iot2050-event-record.service.d/ +``` + +### Watchdog events + +If watchdog event recording is expected, please refer to [WATCHDOG.md](./WATCHDOG.md). + +## Development + +### How to inject a new event? + +First, please link the `EventInterface` to the customized application. +```sh +# In IOT DUT +ln -s /usr/lib/iot2050/event/gRPC/EventInterface /path/to/customized-app/gRPC/EventInterface + +# In Source code +ln -s recipes-app/iot2050-event-record/files/gRPC/EventInterface /path/to/customized-app/gRPC/EventInterface +``` + +Then, use the `Write` and `Read` functions to communicate with +`iot2050-event-serve.service`, as follows: + +```python +import grpc +from gRPC.EventInterface.iot2050_event_pb2 import ( + WriteRequest, + ReadRequest +) +from gRPC.EventInterface.iot2050_event_pb2_grpc import EventRecordStub + + +def write_event(event_type, event): + with grpc.insecure_channel(iot2050_event_api_server) as channel: + stub = EventRecordStub(channel) + response = stub.Write(WriteRequest(event_type=event_type, event=event)) + + if response.status: + print(f'Event Record writes event result: {response.status}') + print(f'Event Record writes event message: {response.message}') + +def read_event(event_type): + with grpc.insecure_channel(iot2050_event_api_server) as channel: + stub = EventRecordStub(channel) + response = stub.Read(ReadRequest(event_type=event_type)) + return response.event +``` + +And, please find the api definition in `gRPC/EventInterface/iot2050-event.proto`. + +### Regenerate the gRPC python modules if proto file changed + +If the `proto` file needs to be changed when customizing a new application +to inject event, please update the gRPC in the original path as follows. + +```sh +python3 -m grpc_tools.protoc -I. --python_out=. --pyi_out=. --grpc_python_out=. gRPC/EventInterface/iot2050-event.proto +``` diff --git a/recipes-app/iot2050-event-record/WATCHDOG.md b/recipes-app/iot2050-event-record/WATCHDOG.md new file mode 100644 index 000000000..990151683 --- /dev/null +++ b/recipes-app/iot2050-event-record/WATCHDOG.md @@ -0,0 +1,81 @@ +Due to the fact that the watchdog is non-stoppable, getting a watchdog +event always requires opening the watchdog and feeding the watchdog. So +the watchdog event could not be included into the `iot2050-event-record` +service. + +This README file explains how to get the watchdog reset status and how to +inject it into `iot2050-event-record` service. + +# How to get the watchdog reset status? + +The `wdt_example.py` below shows how to get the watchdog reset status. + +```py +import array +import fcntl +import os +import psutil +import time +from datetime import datetime + +# Implement _IOR function for wdt kernel ioctl function +_IOC_NRBITS = 8 +_IOC_TYPEBITS = 8 +_IOC_SIZEBITS = 14 +_IOC_DIRBITS = 2 + +_IOC_NRSHIFT = 0 +_IOC_TYPESHIFT =(_IOC_NRSHIFT+_IOC_NRBITS) +_IOC_SIZESHIFT =(_IOC_TYPESHIFT+_IOC_TYPEBITS) +_IOC_DIRSHIFT =(_IOC_SIZESHIFT+_IOC_SIZEBITS) + +_IOC_NONE = 0 +_IOC_WRITE = 1 +_IOC_READ = 2 +def _IOC(direction,type,nr,size): + return (((direction) << _IOC_DIRSHIFT) | + ((type) << _IOC_TYPESHIFT) | + ((nr) << _IOC_NRSHIFT) | + ((size) << _IOC_SIZESHIFT)) +def _IOR(type, number, size): + return _IOC(_IOC_READ, type, number, size) + +WDIOC_GETBOOTSTATUS = _IOR(ord('W'), 2, 4) +WDIOF_CARDRESET = 0x20 +WDT_PATH = "/dev/watchdog" + +EVENT_STRINGS = { + "wdt": "{} watchdog reset is detected", + "no-wdt": "{} watchdog reset isn't detected" +} + +def feeding_the_watchdog(fd): + while True: + ret = os.write(fd, b'watchdog') + print("Feeding the watchdog ...") + # Let's say the watchdog timeout is more than 30 s + time.sleep(30) + +def record_wdt_events(): + status = array.array('h', [0]) + fd = os.open(WDT_PATH, os.O_RDWR) + if fcntl.ioctl(fd, WDIOC_GETBOOTSTATUS, status, 1) < 0: + print("Failed to get wdt boot status!") + + boot_time = datetime.fromtimestamp(psutil.boot_time()) + if (WDIOF_CARDRESET & status[0]): + print(EVENT_STRINGS["wdt"].format(boot_time)) + else: + print(EVENT_STRINGS["no-wdt"].format(boot_time)) + + feeding_the_watchdog(fd) + + os.close(fd) + +if __name__ == "__main__": + record_wdt_events() +``` + +# How to inject it into iot2050-event-record? + +Please refer to [README.md](./README.md). diff --git a/recipes-app/iot2050-event-record/files/gRPC/EIOManager b/recipes-app/iot2050-event-record/files/gRPC/EIOManager new file mode 120000 index 000000000..7550022ee --- /dev/null +++ b/recipes-app/iot2050-event-record/files/gRPC/EIOManager @@ -0,0 +1 @@ +../../../iot2050-eio-manager/files/gRPC/EIOManager \ No newline at end of file diff --git a/recipes-app/iot2050-event-record/files/gRPC/EventInterface/iot2050-event.proto b/recipes-app/iot2050-event-record/files/gRPC/EventInterface/iot2050-event.proto new file mode 100644 index 000000000..adf007721 --- /dev/null +++ b/recipes-app/iot2050-event-record/files/gRPC/EventInterface/iot2050-event.proto @@ -0,0 +1,61 @@ +/* + * Copyright (c) Siemens AG, 2023 + * + * Authors: + * Li Hua Qian + * + * SPDX-License-Identifier: MIT + */ + +syntax = "proto3"; + +package eventrecord; + +service EventRecord { + rpc Write (WriteRequest) returns (WriteReply) {} + rpc Read (ReadRequest) returns (ReadReply) {} +} + +/* ----------------- Write event ----------------- */ +/* WriteRequest + * - event_type: a string to present event type + * - "IOT2050_EVENT.xxx" means IOT2050 standard events + * - "" or other strings mean customized events + * - event: the event content to write + */ +message WriteRequest { + string event_type = 1; + string event = 2; +} + +/* WriteReply + * - status: 0 means successful + * others mean error + * - message: the detail write message + */ +message WriteReply { + int32 status = 1; + string message = 2; +} + +/* ----------------- Read event ----------------- */ +/* ReadRequest + * - event_type: a string to present event type + * - "IOT2050_EVENT.xxx" means IOT2050 standard events + * - "" means to all types of events + */ +message ReadRequest { + string event_type = 1; +} + +/* ReadReply + * - status: 0 means successful + * others mean error + * - message: the detail write message + * - event: the read back event content + */ +message ReadReply { + int32 status = 1; + string message = 2; + string event = 3; +} diff --git a/recipes-app/iot2050-event-record/files/gRPC/EventInterface/iot2050_event_pb2.py b/recipes-app/iot2050-event-record/files/gRPC/EventInterface/iot2050_event_pb2.py new file mode 100644 index 000000000..0f4218fce --- /dev/null +++ b/recipes-app/iot2050-event-record/files/gRPC/EventInterface/iot2050_event_pb2.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: gRPC/EventInterface/iot2050-event.proto +"""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\'gRPC/EventInterface/iot2050-event.proto\x12\x0b\x65ventrecord\"1\n\x0cWriteRequest\x12\x12\n\nevent_type\x18\x01 \x01(\t\x12\r\n\x05\x65vent\x18\x02 \x01(\t\"-\n\nWriteReply\x12\x0e\n\x06status\x18\x01 \x01(\x05\x12\x0f\n\x07message\x18\x02 \x01(\t\"!\n\x0bReadRequest\x12\x12\n\nevent_type\x18\x01 \x01(\t\";\n\tReadReply\x12\x0e\n\x06status\x18\x01 \x01(\x05\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\r\n\x05\x65vent\x18\x03 \x01(\t2\x88\x01\n\x0b\x45ventRecord\x12=\n\x05Write\x12\x19.eventrecord.WriteRequest\x1a\x17.eventrecord.WriteReply\"\x00\x12:\n\x04Read\x12\x18.eventrecord.ReadRequest\x1a\x16.eventrecord.ReadReply\"\x00\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'gRPC.EventInterface.iot2050_event_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_WRITEREQUEST']._serialized_start=56 + _globals['_WRITEREQUEST']._serialized_end=105 + _globals['_WRITEREPLY']._serialized_start=107 + _globals['_WRITEREPLY']._serialized_end=152 + _globals['_READREQUEST']._serialized_start=154 + _globals['_READREQUEST']._serialized_end=187 + _globals['_READREPLY']._serialized_start=189 + _globals['_READREPLY']._serialized_end=248 + _globals['_EVENTRECORD']._serialized_start=251 + _globals['_EVENTRECORD']._serialized_end=387 +# @@protoc_insertion_point(module_scope) diff --git a/recipes-app/iot2050-event-record/files/gRPC/EventInterface/iot2050_event_pb2.pyi b/recipes-app/iot2050-event-record/files/gRPC/EventInterface/iot2050_event_pb2.pyi new file mode 100644 index 000000000..58f9e7ecb --- /dev/null +++ b/recipes-app/iot2050-event-record/files/gRPC/EventInterface/iot2050_event_pb2.pyi @@ -0,0 +1,37 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class WriteRequest(_message.Message): + __slots__ = ["event_type", "event"] + EVENT_TYPE_FIELD_NUMBER: _ClassVar[int] + EVENT_FIELD_NUMBER: _ClassVar[int] + event_type: str + event: str + def __init__(self, event_type: _Optional[str] = ..., event: _Optional[str] = ...) -> None: ... + +class WriteReply(_message.Message): + __slots__ = ["status", "message"] + STATUS_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + status: int + message: str + def __init__(self, status: _Optional[int] = ..., message: _Optional[str] = ...) -> None: ... + +class ReadRequest(_message.Message): + __slots__ = ["event_type"] + EVENT_TYPE_FIELD_NUMBER: _ClassVar[int] + event_type: str + def __init__(self, event_type: _Optional[str] = ...) -> None: ... + +class ReadReply(_message.Message): + __slots__ = ["status", "message", "event"] + STATUS_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + EVENT_FIELD_NUMBER: _ClassVar[int] + status: int + message: str + event: str + def __init__(self, status: _Optional[int] = ..., message: _Optional[str] = ..., event: _Optional[str] = ...) -> None: ... diff --git a/recipes-app/iot2050-event-record/files/gRPC/EventInterface/iot2050_event_pb2_grpc.py b/recipes-app/iot2050-event-record/files/gRPC/EventInterface/iot2050_event_pb2_grpc.py new file mode 100644 index 000000000..0b06bc5d7 --- /dev/null +++ b/recipes-app/iot2050-event-record/files/gRPC/EventInterface/iot2050_event_pb2_grpc.py @@ -0,0 +1,99 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from gRPC.EventInterface import iot2050_event_pb2 as gRPC_dot_EventInterface_dot_iot2050__event__pb2 + + +class EventRecordStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Write = channel.unary_unary( + '/eventrecord.EventRecord/Write', + request_serializer=gRPC_dot_EventInterface_dot_iot2050__event__pb2.WriteRequest.SerializeToString, + response_deserializer=gRPC_dot_EventInterface_dot_iot2050__event__pb2.WriteReply.FromString, + ) + self.Read = channel.unary_unary( + '/eventrecord.EventRecord/Read', + request_serializer=gRPC_dot_EventInterface_dot_iot2050__event__pb2.ReadRequest.SerializeToString, + response_deserializer=gRPC_dot_EventInterface_dot_iot2050__event__pb2.ReadReply.FromString, + ) + + +class EventRecordServicer(object): + """Missing associated documentation comment in .proto file.""" + + def Write(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Read(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_EventRecordServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Write': grpc.unary_unary_rpc_method_handler( + servicer.Write, + request_deserializer=gRPC_dot_EventInterface_dot_iot2050__event__pb2.WriteRequest.FromString, + response_serializer=gRPC_dot_EventInterface_dot_iot2050__event__pb2.WriteReply.SerializeToString, + ), + 'Read': grpc.unary_unary_rpc_method_handler( + servicer.Read, + request_deserializer=gRPC_dot_EventInterface_dot_iot2050__event__pb2.ReadRequest.FromString, + response_serializer=gRPC_dot_EventInterface_dot_iot2050__event__pb2.ReadReply.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'eventrecord.EventRecord', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class EventRecord(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def Write(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/eventrecord.EventRecord/Write', + gRPC_dot_EventInterface_dot_iot2050__event__pb2.WriteRequest.SerializeToString, + gRPC_dot_EventInterface_dot_iot2050__event__pb2.WriteReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def Read(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/eventrecord.EventRecord/Read', + gRPC_dot_EventInterface_dot_iot2050__event__pb2.ReadRequest.SerializeToString, + gRPC_dot_EventInterface_dot_iot2050__event__pb2.ReadReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/recipes-app/iot2050-event-record/files/iot2050-event-record.conf b/recipes-app/iot2050-event-record/files/iot2050-event-record.conf new file mode 100644 index 000000000..f601bfe7d --- /dev/null +++ b/recipes-app/iot2050-event-record/files/iot2050-event-record.conf @@ -0,0 +1,6 @@ +# In IoT2050-SM, there are sensors for tilted and uncovered detection. If you +# want to enable logging of sensor events, i.e. tilted and uncovered events. +# Please copy this file to /etc/systemd/system/iot2050-event-record.service.d/. + +[Service] +Environment="RECORD_SENSOR_EVENTS=True" diff --git a/recipes-app/iot2050-event-record/files/iot2050-event-record.py b/recipes-app/iot2050-event-record/files/iot2050-event-record.py new file mode 100644 index 000000000..1a0361d6b --- /dev/null +++ b/recipes-app/iot2050-event-record/files/iot2050-event-record.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# +# Copyright (c) Siemens AG, 2023 +# +# Authors: +# Li Hua Qian +# +# SPDX-License-Identifier: MIT +# +import os +import sys +import psutil +import threading +import time +from datetime import datetime +from systemd import journal +from enum import Enum +import grpc +from gRPC.EventInterface.iot2050_event_pb2 import ( + WriteRequest, + ReadRequest +) +from gRPC.EventInterface.iot2050_event_pb2_grpc import EventRecordStub +from gRPC.EIOManager.iot2050_eio_pb2_grpc import EIOManagerStub +from gRPC.EIOManager.iot2050_eio_pb2 import ( + ReadEIOEventRequest +) +from iot2050_event_global import ( + iot2050_event_api_server, + iot2050_eio_api_server +) + +EVENT_TYPES = { + "power": "IOT2050_EVENTS.power", + "tilt": "IOT2050_EVENTS.tilted", + "uncover": "IOT2050_EVENTS.uncovered", + "eio": "IOT2050_EVENTS.eio", +} +EVENT_STRINGS = { + "power": "{} the device is powered up", + "tilt": "{} the device is tilted", + "uncover": "{} the device is uncovered", + "common": "{}" +} + + +TIMEOUT_SEC = 15 +def grpc_server_on(channel) -> bool: + try: + grpc.channel_ready_future(channel).result(timeout=TIMEOUT_SEC) + return True + except grpc.FutureTimeoutError: + return False + +def is_grpc_servers_ready(): + with grpc.insecure_channel(iot2050_event_api_server) as channel: + if not grpc_server_on(channel): + print(f"ipv4:{iot2050_event_api_server}: Failed to connect to remote host: Connection refused") + return False + + return True + +def is_eiod_servers_existed(): + with grpc.insecure_channel(iot2050_eio_api_server) as channel: + if not grpc_server_on(channel): + return False + + return True + +def write_event(event_type, event): + with grpc.insecure_channel(iot2050_event_api_server) as channel: + stub = EventRecordStub(channel) + response = stub.Write(WriteRequest(event_type=event_type, event=event)) + + if response.status: + print(f'Event Record writes event result: {response.status}') + print(f'Event Record writes event message: {response.message}') + +def read_event(event_type): + with grpc.insecure_channel(iot2050_event_api_server) as channel: + stub = EventRecordStub(channel) + response = stub.Read(ReadRequest(event_type=event_type)) + return response.event + +error_messages = [] +def read_eio_event(): + with grpc.insecure_channel(iot2050_eio_api_server) as channel: + stub = EIOManagerStub(channel) + response = stub.ReadEIOEvent(ReadEIOEventRequest()) + + # Only print the error info once to avoid interference + if response.status and not response.message in error_messages: + error_messages.append(response.message) + print(f'Event Record reads eio event result: {response.status}') + print(f'Event Record reads eio event message: {response.message}') + + return response.event + +def record_power_events(has_eio=False): + try: + existed_events = read_event(EVENT_TYPES["power"]) + # power up + boot_time = datetime.fromtimestamp(psutil.boot_time()) + power_up_event = EVENT_STRINGS["power"].format(boot_time) + if not power_up_event in existed_events: + write_event(EVENT_TYPES["power"], power_up_event) + + # power loss, which is recorded by eio + if has_eio: + for event in read_eio_event().splitlines(): + if not "power loss" in event or \ + event in existed_events: + continue + write_event(EVENT_TYPES["power"], event) + except Exception as e: + print(e) + +def record_eio_events(): + '''Record all eio events, not including power up and power loss events''' + while True: + existed_events = read_event(EVENT_TYPES["eio"]) + for event in read_eio_event().splitlines(): + if "power" in event or \ + event in existed_events: + continue + write_event(EVENT_TYPES["eio"], event) + time.sleep(5) + + + +IIO_IMU_PATH = "/sys/devices/platform/bus@100000/2030000.i2c/i2c-5/5-006a/" +IIO_PRO_PATH = "/sys/devices/platform/bus@100000/2030000.i2c/i2c-5/5-0044/" +ACCEL_CRITICAL_VALUE = 1000 +LUX_CRITICAL_VALUE = 300 +def record_sensor_events(): + accel_x_raw = "{}/in_accel_x_raw" + accel_y_raw = "{}/in_accel_y_raw" + accel_z_raw = "{}/in_accel_z_raw" + pro_raw = "{}/in_proximity0_raw" + imu_w = os.walk(IIO_IMU_PATH) + pro_w = os.walk(IIO_PRO_PATH) + for (dirpath, dirnames, filenames) in imu_w: + if "in_accel_x_raw" in filenames: + accel_x_raw = accel_x_raw.format(dirpath) + accel_y_raw = accel_y_raw.format(dirpath) + accel_z_raw = accel_z_raw.format(dirpath) + break + for (dirpath, dirnames, filenames) in pro_w: + if "in_proximity0_raw" in filenames: + pro_raw = pro_raw.format(dirpath) + break + with open(accel_x_raw, 'r') as x, \ + open(accel_y_raw, 'r') as y, \ + open(accel_z_raw, 'r') as z, \ + open(pro_raw, 'r') as l: + is_uncovered = False + while True: + # Detect tilt sensor event + x.seek(0) + y.seek(0) + z.seek(0) + old_x = int(x.read()) + old_y = int(y.read()) + old_z = int(z.read()) + + time.sleep(0.3) + + x.seek(0) + y.seek(0) + z.seek(0) + if abs(int(x.read()) - old_x) > ACCEL_CRITICAL_VALUE or \ + abs(int(y.read()) - old_y) > ACCEL_CRITICAL_VALUE or \ + abs(int(z.read()) - old_z) > ACCEL_CRITICAL_VALUE: + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + tilted_event = EVENT_STRINGS["tilt"].format(now) + write_event(EVENT_TYPES["tilt"], tilted_event) + + # Detect tamper sensor event + l.seek(0) + lux = int(l.read()) + if lux < LUX_CRITICAL_VALUE and not is_uncovered: + is_uncovered = True + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + uncover_event = EVENT_STRINGS["uncover"].format(now) + write_event(EVENT_TYPES["uncover"], uncover_event) + elif lux > LUX_CRITICAL_VALUE and is_uncovered: + is_uncovered = False + +def event_record(): + if not is_grpc_servers_ready(): + sys.exit(1) + to_record_eio_events = is_eiod_servers_existed() + + # Record the power events + record_power_events(has_eio=to_record_eio_events) + + to_record_sensor_events = os.getenv('RECORD_SENSOR_EVENTS') + # Record the tilt/tamper sensor events + if to_record_sensor_events: + sensor_thread = threading.Thread(target=record_sensor_events) + sensor_thread.start() + + # Record the eio event + if to_record_eio_events: + eio_thread = threading.Thread(target=record_eio_events) + eio_thread.start() + + if to_record_sensor_events: + sensor_thread.join() + if to_record_eio_events: + eio_thread.join() + + +if __name__ == "__main__": + event_record() diff --git a/recipes-app/iot2050-event-record/files/iot2050-event-record.service b/recipes-app/iot2050-event-record/files/iot2050-event-record.service new file mode 100644 index 000000000..5cec00662 --- /dev/null +++ b/recipes-app/iot2050-event-record/files/iot2050-event-record.service @@ -0,0 +1,23 @@ +# +# Copyright (c) Siemens AG, 2023 +# +# This file is subject to the terms and conditions of the MIT License. See +# COPYING.MIT file in the top-level directory. +# + +[Unit] +Description=IOT2050 Event Record daemon +After=iot2050-event-serve.service iot2050-eiod.service +Requires=iot2050-event-serve.service +Wants=iot2050-eiod.service + +[Service] +Type=exec +ExecStart=/usr/bin/iot2050-event-record +Restart=on-failure +RestartSec=5 +StandardOutput=journal+console +StandardError=journal+console + +[Install] +WantedBy=multi-user.target diff --git a/recipes-app/iot2050-event-record/files/iot2050-event-serve.py b/recipes-app/iot2050-event-record/files/iot2050-event-serve.py new file mode 100644 index 000000000..8afc348b3 --- /dev/null +++ b/recipes-app/iot2050-event-record/files/iot2050-event-serve.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# +# Copyright (c) Siemens AG, 2023 +# +# Authors: +# Li Hua Qian +# +# SPDX-License-Identifier: MIT +from concurrent import futures +import datetime +import grpc +from iot2050_event import ( + write_event, + read_event, + EventIOError +) +from iot2050_event_global import ( + EVENT_API_SERVER_HOSTNAME, + EVENT_API_SERVER_PORT +) +from gRPC.EventInterface.iot2050_event_pb2 import ( + WriteRequest, WriteReply, + ReadRequest, ReadReply +) +from gRPC.EventInterface.iot2050_event_pb2_grpc import ( + EventRecordServicer, + add_EventRecordServicer_to_server +) + + +class EventRecordServicer(EventRecordServicer): + + def Write(self, request: WriteRequest, context): + try: + write_event(request.event_type, request.event) + except EventIOError as e: + return WriteReply(status=1, message=f'{e}') + + return WriteReply(status=0, message='OK') + + def Read(self, request: ReadRequest, context): + event = read_event(request.event_type) + + return ReadReply(status=0, message='OK', event=event) + + +def serve(): + iot2050_event_api_server = "{}:{}".format( + EVENT_API_SERVER_HOSTNAME, EVENT_API_SERVER_PORT) + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + add_EventRecordServicer_to_server( + EventRecordServicer(), server + ) + server.add_insecure_port(iot2050_event_api_server) + server.start() + server.wait_for_termination() + + +if __name__ == "__main__": + serve() diff --git a/recipes-app/iot2050-event-record/files/iot2050-event-serve.service b/recipes-app/iot2050-event-record/files/iot2050-event-serve.service new file mode 100644 index 000000000..7e0fa85df --- /dev/null +++ b/recipes-app/iot2050-event-record/files/iot2050-event-serve.service @@ -0,0 +1,20 @@ +# +# Copyright (c) Siemens AG, 2023 +# +# This file is subject to the terms and conditions of the MIT License. See +# COPYING.MIT file in the top-level directory. +# + +[Unit] +Description=IOT2050 Event Record daemon + +[Service] +Type=exec +ExecStart=/usr/bin/iot2050-event-serve +Restart=always +RestartSec=5 +StandardOutput=journal+console +StandardError=journal+console + +[Install] +WantedBy=multi-user.target diff --git a/recipes-app/iot2050-event-record/files/iot2050_event.py b/recipes-app/iot2050-event-record/files/iot2050_event.py new file mode 100644 index 000000000..f512c382e --- /dev/null +++ b/recipes-app/iot2050-event-record/files/iot2050_event.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# +# Copyright (c) Siemens AG, 2023 +# +# Authors: +# Li Hua Qian +# +# SPDX-License-Identifier: MIT +# +import json +from systemd import journal +from iot2050_event_global import EVENT_IDENTIFIER + +def write_event(event_type, event): + if not event: + raise EventIOError("Empty Events: no events to write") + journal_stream = journal.stream(EVENT_IDENTIFIER) + log_to_record = event_type + ": " + event + journal_stream.write(log_to_record) + +def read_all_events(journal_reader): + events = [] + for entry in journal_reader: + events.append(entry['MESSAGE']) + events_str = json.dumps(events, indent=4) + + return events_str + +def read_specified_event(event_type, journal_reader): + events = [] + for entry in journal_reader: + if event_type in entry['MESSAGE']: + events.append(entry['MESSAGE']) + events_str = json.dumps(events, indent=4) + + return events_str + +def read_event(event_type): + journal_reader = journal.Reader() + journal_reader.add_match("SYSLOG_IDENTIFIER={}".format(EVENT_IDENTIFIER)) + + if not event_type: + return read_all_events(journal_reader) + else: + return read_specified_event(event_type, journal_reader) + +class EventIOError(Exception): + def __init__(self, *args: object) -> None: + super().__init__(*args) diff --git a/recipes-app/iot2050-event-record/files/iot2050_event_global.py b/recipes-app/iot2050-event-record/files/iot2050_event_global.py new file mode 100644 index 000000000..3b16d22e2 --- /dev/null +++ b/recipes-app/iot2050-event-record/files/iot2050_event_global.py @@ -0,0 +1,24 @@ +# Copyright (c) Siemens AG, 2023 +# +# Authors: +# Li Hua Qian +# +# SPDX-License-Identifier: MIT + +# IOT2050 Event API server hostname +EVENT_API_SERVER_HOSTNAME = 'localhost' + +# IOT2050 Event API server port +EVENT_API_SERVER_PORT = '5050' + +# IOT2050 Event Log identifier +EVENT_IDENTIFIER = 'IOT2050-EventRecord' + +# IOT2050 Extended IO API server hostname +EIO_API_SERVER_HOSTNAME = 'localhost' + +# IOT2050 Extended IO API server port +EIO_API_SERVER_PORT = '5020' + +iot2050_event_api_server = f"{EVENT_API_SERVER_HOSTNAME}:{EVENT_API_SERVER_PORT}" +iot2050_eio_api_server = f"{EIO_API_SERVER_HOSTNAME}:{EIO_API_SERVER_PORT}" diff --git a/recipes-app/iot2050-event-record/iot2050-event-record_0.1.bb b/recipes-app/iot2050-event-record/iot2050-event-record_0.1.bb new file mode 100644 index 000000000..62755fb63 --- /dev/null +++ b/recipes-app/iot2050-event-record/iot2050-event-record_0.1.bb @@ -0,0 +1,70 @@ +# +# Copyright (c) Siemens AG, 2023 +# +# Authors: +# Li Hua Qian +# +# This file is subject to the terms and conditions of the MIT License. See +# COPYING.MIT file in the top-level directory. +# + +inherit dpkg-raw + +DESCRIPTION = "IOT2050 Event Record Service" +MAINTAINER = "huaqian.li@siemens.com" + +SRC_URI = " \ + file://gRPC/EventInterface/iot2050_event_pb2_grpc.py \ + file://gRPC/EventInterface/iot2050_event_pb2.py \ + file://gRPC/EventInterface/iot2050_event_pb2.pyi \ + file://gRPC/EventInterface/iot2050-event.proto \ + file://gRPC/EIOManager/iot2050_eio_pb2_grpc.py \ + file://gRPC/EIOManager/iot2050_eio_pb2.py \ + file://gRPC/EIOManager/iot2050_eio_pb2.pyi \ + file://gRPC/EIOManager/iot2050-eio.proto \ + file://iot2050-event-record.py \ + file://iot2050-event-record.service \ + file://iot2050-event-record.conf \ + file://iot2050-event-serve.py \ + file://iot2050-event-serve.service \ + file://iot2050_event.py \ + file://iot2050_event_global.py" + +S = "${WORKDIR}/src" + +DEBIAN_DEPENDS = "python3, python3-grpcio, python3-psutil, python3-systemd" + +do_install() { + install -v -d ${D}/usr/lib/iot2050/event/ + install -v -m 755 ${WORKDIR}/iot2050-event-record.py ${D}/usr/lib/iot2050/event/ + install -v -m 755 ${WORKDIR}/iot2050-event-record.conf ${D}/usr/lib/iot2050/event/ + install -v -m 755 ${WORKDIR}/iot2050-event-serve.py ${D}/usr/lib/iot2050/event/ + install -v -m 755 ${WORKDIR}/iot2050_event.py ${D}/usr/lib/iot2050/event/ + install -v -m 755 ${WORKDIR}/iot2050_event_global.py ${D}/usr/lib/iot2050/event/ + install -v -d ${D}/usr/lib/iot2050/event/gRPC/EventInterface/ + install -v -m 755 ${WORKDIR}/gRPC/EventInterface/iot2050_event_pb2_grpc.py \ + ${D}/usr/lib/iot2050/event/gRPC/EventInterface/ + install -v -m 755 ${WORKDIR}/gRPC/EventInterface/iot2050_event_pb2.py \ + ${D}/usr/lib/iot2050/event/gRPC/EventInterface/ + install -v -m 755 ${WORKDIR}/gRPC/EventInterface/iot2050_event_pb2.pyi \ + ${D}/usr/lib/iot2050/event/gRPC/EventInterface/ + install -v -m 755 ${WORKDIR}/gRPC/EventInterface/iot2050-event.proto \ + ${D}/usr/lib/iot2050/event/gRPC/EventInterface/ + install -v -d ${D}/usr/lib/iot2050/event/gRPC/EIOManager/ + install -v -m 755 ${WORKDIR}/gRPC/EIOManager/iot2050_eio_pb2_grpc.py \ + ${D}/usr/lib/iot2050/event/gRPC/EIOManager/ + install -v -m 755 ${WORKDIR}/gRPC/EIOManager/iot2050_eio_pb2.py \ + ${D}/usr/lib/iot2050/event/gRPC/EIOManager/ + install -v -m 755 ${WORKDIR}/gRPC/EIOManager/iot2050_eio_pb2.pyi \ + ${D}/usr/lib/iot2050/event/gRPC/EIOManager/ + install -v -m 755 ${WORKDIR}/gRPC/EIOManager/iot2050-eio.proto \ + ${D}/usr/lib/iot2050/event/gRPC/EIOManager/ + + install -v -d ${D}/usr/bin/ + ln -sf ../lib/iot2050/event/iot2050-event-record.py ${D}/usr/bin/iot2050-event-record + ln -sf ../lib/iot2050/event/iot2050-event-serve.py ${D}/usr/bin/iot2050-event-serve + + install -v -d ${D}/lib/systemd/system/ + install -v -m 644 ${WORKDIR}/iot2050-event-record.service ${D}/lib/systemd/system/ + install -v -m 644 ${WORKDIR}/iot2050-event-serve.service ${D}/lib/systemd/system/ +} diff --git a/recipes-core/images/iot2050-image-example.bb b/recipes-core/images/iot2050-image-example.bb index 3c907a857..cc9fdae3c 100644 --- a/recipes-core/images/iot2050-image-example.bb +++ b/recipes-core/images/iot2050-image-example.bb @@ -41,6 +41,7 @@ IMAGE_INSTALL += " \ libteec1 \ optee-client-dev \ tee-supplicant \ + iot2050-event-record \ " IOT2050_NODE_RED_SUPPORT ?= "1"