From 63f70ba6e3e3aec62bf86247085d29776adfcef0 Mon Sep 17 00:00:00 2001 From: Li Hua Qian Date: Mon, 23 Oct 2023 13:53:29 +0800 Subject: [PATCH] Add event record service Along with the IOT2050 SM variant, some IOT2050 device specific events are recognized as being important for field applications, including: - Device power up - Device power loss - Device reboot - Device case open (uncover) - Device tilting - Device watchdog reset - External module events Although it's possible to collect all these events from variant source, such as syslog, or by customized coding to get the sensor event, it's valuable to provide an unified method or portal to collect all these predefined events from one place. Then this service is added to serve the above purpose, it reads the power up, power loss, eio, tilt, uncover, and (possibly) the watchdog reset events, then writes them into syslog and makes them be readable from gRPC interface. This service could be used directly, or as a base/reference for customization. Signed-off-by: Li Hua Qian --- recipes-app/iot2050-event-record/README.md | 94 ++++++++ recipes-app/iot2050-event-record/WATCHDOG.md | 81 +++++++ .../files/gRPC/EIOManager | 1 + .../gRPC/EventInterface/iot2050-event.proto | 61 +++++ .../gRPC/EventInterface/iot2050_event_pb2.py | 33 +++ .../gRPC/EventInterface/iot2050_event_pb2.pyi | 37 +++ .../EventInterface/iot2050_event_pb2_grpc.py | 99 ++++++++ .../files/iot2050-event-record.conf | 6 + .../files/iot2050-event-record.py | 215 ++++++++++++++++++ .../files/iot2050-event-record.service | 23 ++ .../files/iot2050-event-serve.py | 60 +++++ .../files/iot2050-event-serve.service | 20 ++ .../files/iot2050_event.py | 49 ++++ .../files/iot2050_event_global.py | 24 ++ .../iot2050-event-record_0.1.bb | 70 ++++++ recipes-core/images/iot2050-image-example.bb | 1 + 16 files changed, 874 insertions(+) create mode 100644 recipes-app/iot2050-event-record/README.md create mode 100644 recipes-app/iot2050-event-record/WATCHDOG.md create mode 120000 recipes-app/iot2050-event-record/files/gRPC/EIOManager create mode 100644 recipes-app/iot2050-event-record/files/gRPC/EventInterface/iot2050-event.proto create mode 100644 recipes-app/iot2050-event-record/files/gRPC/EventInterface/iot2050_event_pb2.py create mode 100644 recipes-app/iot2050-event-record/files/gRPC/EventInterface/iot2050_event_pb2.pyi create mode 100644 recipes-app/iot2050-event-record/files/gRPC/EventInterface/iot2050_event_pb2_grpc.py create mode 100644 recipes-app/iot2050-event-record/files/iot2050-event-record.conf create mode 100644 recipes-app/iot2050-event-record/files/iot2050-event-record.py create mode 100644 recipes-app/iot2050-event-record/files/iot2050-event-record.service create mode 100644 recipes-app/iot2050-event-record/files/iot2050-event-serve.py create mode 100644 recipes-app/iot2050-event-record/files/iot2050-event-serve.service create mode 100644 recipes-app/iot2050-event-record/files/iot2050_event.py create mode 100644 recipes-app/iot2050-event-record/files/iot2050_event_global.py create mode 100644 recipes-app/iot2050-event-record/iot2050-event-record_0.1.bb 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 165d85d40..8bc46c8dd 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"