From dec28617196619f8c7cba997947fae7744c56b07 Mon Sep 17 00:00:00 2001 From: Li Hua Qian Date: Mon, 23 Oct 2023 13:53:29 +0800 Subject: [PATCH] iot2050-event-record: Add event record service This patch is to read the power up, power loss, eio, tilt, uncover, and watchdog reset events, then writes them into syslog and makes them be readable for journald. Signed-off-by: Li Hua Qian --- recipes-app/iot2050-event-record/README.md | 75 ++++++++ .../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 | 5 + .../files/iot2050-event-record.py | 176 ++++++++++++++++++ .../files/iot2050-event-record.service | 23 +++ .../files/iot2050-event-serve.py | 60 ++++++ .../files/iot2050-event-serve.service | 22 +++ .../files/iot2050-event-wdt.py | 94 ++++++++++ .../files/iot2050_event.py | 49 +++++ .../files/iot2050_event_global.py | 41 ++++ .../iot2050-event-record_0.1.bb | 72 +++++++ recipes-core/images/iot2050-image-example.bb | 1 + 16 files changed, 849 insertions(+) create mode 100644 recipes-app/iot2050-event-record/README.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-wdt.py 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..e3e2eb694 --- /dev/null +++ b/recipes-app/iot2050-event-record/README.md @@ -0,0 +1,75 @@ +# 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. + +In addition: +- A event-serve service for writing and reading syslog. +- A event-record service for recording events. + +# Sensor events + +In default, the sensor events, i.e. tilted and uncovered events, are disabled. +If enabling logging of sensor events is expected, 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 example + +If watchdog events recording is expected, please run the following commands, +or write a new one referring to this example. +```sh +python3 /usr/lib/iot2050/event/iot2050-event-wdt.py +``` + +## How to record 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: + +```py +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 changes: + +```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/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..8e88404d7 --- /dev/null +++ b/recipes-app/iot2050-event-record/files/iot2050-event-record.conf @@ -0,0 +1,5 @@ +# 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..5b8d4a9e7 --- /dev/null +++ b/recipes-app/iot2050-event-record/files/iot2050-event-record.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +# +# Copyright (c) Siemens AG, 2023 +# +# Authors: +# Li Hua Qian +# +# SPDX-License-Identifier: MIT +# +import os +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": "{}" +} + +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 + +def read_eio_event(): + with grpc.insecure_channel(iot2050_eio_api_server) as channel: + stub = EIOManagerStub(channel) + response = stub.ReadEIOEvent(ReadEIOEventRequest()) + + if response.status: + print(f'Event Record reads eio event result: {response.status}') + print(f'Event Record reads eio event message: {response.message}') + + return response.event + +def has_eio_events(): + is_sm_board = False + with open("/sys/firmware/devicetree/base/model", "r") as f: + if "SIMATIC IOT2050 Advanced SM" in f.read(): + is_sm_board = True + + return is_sm_board + +def record_power_events(): + 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_events(): + 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_ACCEL_X_RAW = "/sys/bus/iio/devices/iio:device2/in_accel_x_raw" +IIO_ACCEL_Y_RAW = "/sys/bus/iio/devices/iio:device2/in_accel_y_raw" +IIO_ACCEL_Z_RAW = "/sys/bus/iio/devices/iio:device2/in_accel_z_raw" +IIO_LUX_RAW = "/sys/bus/iio/devices/iio:device0/in_illuminance0_raw" +ACCEL_CRITICAL_VALUE = 1000 +LUX_CRITICAL_VALUE = 300 +def record_sensor_events(): + with open(IIO_ACCEL_X_RAW, 'r') as x, \ + open(IIO_ACCEL_Y_RAW, 'r') as y, \ + open(IIO_ACCEL_Z_RAW, 'r') as z, \ + open(IIO_LUX_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(): + # Record the power events + record_power_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() + + to_record_eio_events = has_eio_events() + # 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..989672343 --- /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-eiosd.service + +[Service] +Type=exec +ExecStart=/usr/bin/iot2050-event-record +Restart=always +RestartSec=5 +RestartSteps=3 +RestartMaxDelaySec=30 +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..6a11c441b --- /dev/null +++ b/recipes-app/iot2050-event-record/files/iot2050-event-serve.service @@ -0,0 +1,22 @@ +# +# 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 +RestartSteps=3 +RestartMaxDelaySec=30 +StandardOutput=journal+console +StandardError=journal+console + +[Install] +WantedBy=multi-user.target diff --git a/recipes-app/iot2050-event-record/files/iot2050-event-wdt.py b/recipes-app/iot2050-event-record/files/iot2050-event-wdt.py new file mode 100644 index 000000000..e11017eea --- /dev/null +++ b/recipes-app/iot2050-event-record/files/iot2050-event-wdt.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# +# Copyright (c) Siemens AG, 2023 +# +# Authors: +# Li Hua Qian +# +# SPDX-License-Identifier: MIT +# +# This is an example for recording wacthdog reset event. +# +import array +import fcntl +import os +import psutil +import time +from datetime import datetime +import grpc +from gRPC.EventInterface.iot2050_event_pb2 import ( + WriteRequest, + ReadRequest +) +from gRPC.EventInterface.iot2050_event_pb2_grpc import EventRecordStub +from iot2050_event_global import iot2050_event_api_server + + +EVENT_STRINGS = { + "wdt": "{} watchdog reset is detected" +} + +WDTRESET_EVENT_TYPE = "IOT2050_EVENT.watchdog" + +# 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" + +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)) + +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 + +def feeding_the_watchdog(fd): + while True: + ret = os.write(fd, b'watchdog') + print("Feeding the watchdog ...") + 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!") + + if (WDIOF_CARDRESET & status[0]): + boot_time = datetime.fromtimestamp(psutil.boot_time()) + wdt_event = EVENT_STRINGS["wdt"].format(boot_time) + existed_events = read_event(WDTRESET_EVENT_TYPE) + if not wdt_event in existed_events: + write_event(WDTRESET_EVENT_TYPE, wdt_event) + + feeding_the_watchdog(fd) + + os.close(fd) + +if __name__ == "__main__": + record_wdt_events() 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..8c07706c1 --- /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 iot2050_event_identifier + +def write_event(event_type, event): + if not event: + raise EventIOError("Empty Events: no events to write") + journal_stream = journal.stream(iot2050_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(iot2050_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..6057ac5c8 --- /dev/null +++ b/recipes-app/iot2050-event-record/files/iot2050_event_global.py @@ -0,0 +1,41 @@ +# Copyright (c) Siemens AG, 2023 +# +# Authors: +# Li Hua Qian +# +# SPDX-License-Identifier: MIT +from dotenv import dotenv_values + +default_conf = { + # 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', +} + +local_conf = dotenv_values(".env") + +effective_conf = { + **default_conf, + **local_conf +} + +EVENT_API_SERVER_HOSTNAME = effective_conf['EVENT_API_SERVER_HOSTNAME'] +EVENT_API_SERVER_PORT = effective_conf['EVENT_API_SERVER_PORT'] +EVENT_IDENTIFIER = effective_conf['EVENT_IDENTIFIER'] +EIO_API_SERVER_HOSTNAME = effective_conf['EIO_API_SERVER_HOSTNAME'] +EIO_API_SERVER_PORT = effective_conf['EIO_API_SERVER_PORT'] + +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}" +iot2050_event_identifier = f"{EVENT_IDENTIFIER}" 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..b343ded11 --- /dev/null +++ b/recipes-app/iot2050-event-record/iot2050-event-record_0.1.bb @@ -0,0 +1,72 @@ +# +# 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-wdt.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-psutil" + +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-wdt.py ${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 82c8dc1a6..8f08adb3e 100644 --- a/recipes-core/images/iot2050-image-example.bb +++ b/recipes-core/images/iot2050-image-example.bb @@ -43,6 +43,7 @@ IMAGE_INSTALL += " \ tee-supplicant \ iot2050-eio-manager \ iot2050-conf-webui \ + iot2050-event-record \ " IOT2050_NODE_RED_SUPPORT ?= "1"