Files
controller-wisetopdataserver/python/biopro/recording/file.py
T

1533 lines
53 KiB
Python
Raw Normal View History

2021-12-20 14:52:55 +08:00
from io import BufferedReader, BufferedWriter
from time import time
from typing import BinaryIO, Iterable
import re
import zlib
import base64
import gc
from biopro.util.json import JSON
from biopro.util.stack import print_exception
2022-03-28 16:11:08 +08:00
from biopro.util.logger import calculate_time
2021-12-20 14:52:55 +08:00
from .data import RecordingData
from .loader import *
from json import loads as json_parse, dumps as _json_stringify
import json
import sys
from statistics import mean
import random
# from numba import jit
2022-03-18 17:12:40 +08:00
from copy import copy
2022-03-17 11:34:36 +08:00
2021-12-20 14:52:55 +08:00
def json_stringify(o) -> str:
return _json_stringify(o, separators=(',', ':'))
# import requests
import biopro.server._identify
T = TypeVar('T')
class RecordingMetaFile(JsonSerialize):
"""binary file format
**RecordingMetaFile header format**
::
struct {
# file format information
u32_B magic_number
u32_B version_number
u64_B create_time
u8 file_uuid[16]
u8 data_format[4]
# recording device
struct DeviceResponseInfo {
u8 device_name_size
u8 device_name[device_name_size]
u8 device_address[6] # little endian
u8 device_serial[6]
}
# recording parameter
struct DeviceConfiguration
# channel information
u8 channel_size
u8 channel[channel_size]
# recording file list
struct {
u8 file_name_size
u8 file_name[file_name_size]
} file_list[...] = {0}
}
magic_number
MAGIC_NUMBER
version_number
======= ====== ===========================
version number note
======= ====== ===========================
0.1
0.2 0x0001 data file storage include
======= ====== ===========================
create_time
unix time stamp
file_uuid
uuid of the moder file.
time_stamp
the time stamp of the first entry.
If value equals to 0xFFFFFFFF and file_name_size equals to 0, it is the end of the list.
If value equals to 0xFFFFFFFF but file_name_size not equals to 0, it is un-determined value.
**json format**
::
RecordingMetaFile = {
"file_size": int
"time_duration": int
"channel": List[int]
"configuration": DeviceConfiguration
"device": DeviceResponseInfo?
}
"""
# noinspection SpellCheckingInspection
FILE_EXT = '.bprm'
TAR_FILE_EXT = '.bprm.tar.xz'
# noinspection SpellCheckingInspection
MAGIC_NUMBER = META_MAGIC_NUMBER
__slots__ = ('_filename', '_filepath', '_id_db', '_create_time', '_file_version', '_file_uuid', '_data_format', '_last_time',
'_device', '_configuration', '_channel_mask', '_database',
'_dirty', '_last_modify_time', '_file_size_cache',
'_recording_file_ch', '_recording_sub_file', '_size',
'_parameter', '_parent', '_recording_file_name', '_recording_sub_mini', '_recording_mini_ch',
'_raw_serial_number', '_mini_serial_number', '_device_id', '_project_id')
2021-12-20 14:52:55 +08:00
def __init__(self, filepath: Union[str, Path], data_format: bytes = None, id_db = 0, database = None):
if isinstance(filepath, str):
filepath = Path(filepath)
name = re.split('[./]+',str(filepath))
self._filename = name[len(name)-2]
self._filepath = filepath.absolute().with_suffix(self.FILE_EXT)
self._id_db = id_db
self._database = database
# file header
self._create_time = int(1000 * time())
self._last_time = int(0)
self._file_version = RecordingFileVersion.VERSION_00_02
self._file_uuid = uuid.uuid1()
if data_format is None:
self._data_format = RecordingFileDataFormat.REC_DATA
elif data_format not in RecordingFileDataFormat.DATA:
raise IndexError('unknown data format : ' + repr(data_format))
else:
self._data_format = data_format
# recording device
# self._device = DeviceResponseInfo.UNKNOWN
self._device = ''
self._parameter = ''
self._parent = ''
self._recording_file_name = ''
self._device_id = -1
self._project_id = None
2021-12-20 14:52:55 +08:00
# recording parameter
self._configuration = None
# channel mask
self._channel_mask = ChannelMask()
# recording file list
# self._recording_file = []
self._recording_file_ch = []
self._recording_mini_ch = []
self._raw_serial_number = {}
self._mini_serial_number = {}
self._recording_sub_file = {}
self._recording_sub_mini = {}
2021-12-20 14:52:55 +08:00
self._size = 0
self._dirty = True
self._last_modify_time = 0
self._file_size_cache = None
def __eq__(self, o: object) -> bool:
if self is o:
return True
if not isinstance(o, RecordingMetaFile):
return False
return self._filepath == o._filepath
@classmethod
def is_recording_meta_file(cls, path: Union[str, Path]) -> bool:
if isinstance(path, str):
path = Path(path)
if not path.is_file():
return False
with path.open('br') as f:
try:
return cls._is_recording_meta_file(f)
except BaseException as e:
raise IOError('file ' + str(path)) from e
return False
@classmethod
def _is_recording_meta_file(cls, io: BinaryIO) -> bool:
header = io.read(4)
return header == RecordingMetaFile.MAGIC_NUMBER
@staticmethod
def load(filepath: Union[str, Path], database = None) -> 'RecordingMetaFile':
"""load meta file
:param filepath: file path
:return: meta file
:raises RuntimeError: not a meta file
:raises FileNotFoundError:
"""
if not RecordingMetaFile.is_recording_meta_file(filepath):
raise RuntimeError('not a recording meta file : ' + str(filepath))
meta = RecordingMetaFile(filepath, database = database)
# meta._filepath = filepath
meta.reload()
return meta
def reopen(self, data_format: bytes = None) -> 'RecordingMetaFile':
"""reopen file.
:param data_format: change data format
:return: new recording meta file
"""
if data_format is None:
data_format = self._data_format
ret = RecordingMetaFile(self._filepath, data_format, id_db = self._id_db, database = self._database)
ret._device = self._device
ret._configuration = self._configuration
ret._dirty = True
return ret
def clone(self, path: Path) -> 'RecordingMetaFile':
ret = RecordingMetaFile(path, self._data_format, id_db = self._id_db, database = self._database)
ret._device = self._device
ret._configuration = self._configuration
ret._dirty = True
return ret
def reload(self):
"""reload meta file
:raises RuntimeError: not a meta file
:raises FileNotFoundError:
"""
loader = RecordingMetaFileLoader.load(self._filepath)
if loader.header != self.MAGIC_NUMBER:
raise IOError('not a BioPro Recording File : ' + str(self._filepath))
def _require(it: Optional[T], message: str) -> T:
if it is None:
raise RuntimeError('BioPro Recording File lost field : ' + message)
return it
self._file_version = _require(loader.version, 'file version')
self._create_time = _require(loader.create_time, 'file create time')
self._last_time = _require(loader.last_time, 'file last time')
self._file_uuid = _require(loader.file_uuid, 'file UUID')
self._data_format = _require(loader.data_format, 'data format')
self._device = _require(loader.device_response, 'device information')
self._configuration = _require(loader.configuration, 'configurations')
self._channel_mask = _require(loader.channel_mask, 'channel_mask')
self._dirty = True
self._last_modify_time = self._filepath.stat().st_mtime
return None
def write(self, database = None):
# print('******', self._filepath)
_name = str(self._filepath).split('/')[-1]
_path = str(self._filepath).replace('/', '^')
meta_data = {
'path': _path,
'configuration': self._configuration.as_json(),
'version': str(self._file_version),
'uuid': str(self._file_uuid),
'data_format': 'rec',
'device': self._device,
'project': self._project_id,
2021-12-20 14:52:55 +08:00
}
if len(self._parent) > 0:
meta_data['parent'] = self._parent
if len(self._recording_file_name) > 0:
meta_data['name'] = self._recording_file_name
if len(self._parameter) > 0:
meta_data['parameter_set'] = self._parameter
if database is not None :
self._database = database
self._database.put_queue(['data_meta_write', meta_data, self._device_id, str(self._file_uuid), self._id_db])
# database.put_queue(['data_meta_write', self, meta_data, _path, self._id_db])
2021-12-20 14:52:55 +08:00
with self._filepath.open('wb') as _f:
f = FileEncoder(_f)
# magic number
f.write(self.MAGIC_NUMBER)
# version number
f.u32(self._file_version)
# create time
f.u64(self._create_time)
# last time
if self._last_time is None:
self._last_time = 0
f.u64(int(self._last_time))
# file uuid
f.write(self._file_uuid.bytes)
# data format
f.write(self._data_format)
# recording device
device = self._device
if isinstance(device, str):
device = json.loads(device)
_device = {}
_device['device_name'] = device['device_name']
_device['device_address'] = device['device_address']
_device = json_stringify(_device)
f.encode_string(_device)
encoder = DeviceConfigurationEncoder()
encoder.write(self._configuration, f)
#channel information
f.u8(self._channel_mask.size)
f.u16a(self._channel_mask.channels())
f.write(b'\0')
self._dirty = False
self._last_modify_time = self._filepath.stat().st_mtime
return None
def update_subfile(self, database = None):
meta_data = {
'raw_data': self._recording_sub_file,
'mini_data': self._recording_sub_mini,
2021-12-20 14:52:55 +08:00
'channels': str(self._channel_mask.channels()),
'size': str(self._size),
'time_duration': str(self._last_time),
'uuid': str(self._file_uuid),
}
_path = str(self._filepath).replace('/', '^')
2021-12-20 14:52:55 +08:00
if database is not None :
self._database = database
self._database.put_queue(['data_meta_write', meta_data, self._device_id, str(self._file_uuid), self._id_db])
# database.put_queue(['data_meta_write', self, meta_data, _path, self._id_db])
2021-12-20 14:52:55 +08:00
return None
2022-03-28 17:17:07 +08:00
def update_subfile_time_size(self, database = None):
meta_data = {
'channels': str(self._channel_mask.channels()),
'size': str(self._size),
'time_duration': str(self._last_time),
'uuid': str(self._file_uuid),
}
_path = str(self._filepath).replace('/', '^')
if database is not None :
self._database = database
self._database.put_queue(['data_meta_write', meta_data, self._device_id, str(self._file_uuid), self._id_db])
# database.put_queue(['data_meta_write', self, meta_data, _path, self._id_db])
return None
def update_subfile_raw(self, database = None):
meta_data = {
'raw_data': self._recording_sub_file,
}
_path = str(self._filepath).replace('/', '^')
if database is not None :
self._database = database
self._database.put_queue(['data_meta_write', meta_data, self._device_id, str(self._file_uuid), self._id_db])
# database.put_queue(['data_meta_write', self, meta_data, _path, self._id_db])
return None
def update_subfile_mini(self, database = None):
meta_data = {
'mini_data': self._recording_sub_mini,
}
_path = str(self._filepath).replace('/', '^')
if database is not None :
self._database = database
self._database.put_queue(['data_meta_write', meta_data, self._device_id, str(self._file_uuid), self._id_db])
# database.put_queue(['data_meta_write', self, meta_data, _path, self._id_db])
return None
2021-12-20 14:52:55 +08:00
@property
def filename(self) -> str:
"""
:return: filename without file extension
"""
return self._filepath.with_suffix('').name
@property
def filepath(self) -> Path:
return self._filepath
@property
def create_time(self) -> Optional[int]:
return self._create_time
@property
def last_time(self) -> Optional[int]:
return self._last_time
@property
def file_version(self) -> int:
return self._file_version.value
@property
def file_uuid(self) -> uuid.UUID:
return self._file_uuid
@property
def file_size(self) -> int:
return self._size
@property
def data_format(self) -> bytes:
return self._data_format
@property
def device(self) -> str:
return self._device
@device.setter
def device(self, device):
self._device = device
self._dirty = True
@property
def parameter(self) -> str:
return self._parameter
@parameter.setter
2021-12-20 14:52:55 +08:00
def parameter(self, parameter):
self._parameter = parameter
return None
@property
def parent(self) -> str:
return self._parent
@parent.setter
2021-12-20 14:52:55 +08:00
def parent(self, parent):
self._parent = parent
@property
def recording_file_name(self) -> str:
return self._recording_file_name
@recording_file_name.setter
2021-12-20 14:52:55 +08:00
def recording_file_name(self, recording_file_name):
self._recording_file_name = recording_file_name
@property
def configuration(self) -> DeviceConfiguration:
return self._configuration
@configuration.setter
def configuration(self, conf: DeviceConfiguration):
self._configuration = conf
self._dirty = True
return None
@property
def project_id(self) -> int:
return self._project_id
@project_id.setter
def project_id(self, project: int):
self._project_id = project
return None
2021-12-20 14:52:55 +08:00
@property
def channels(self) -> List[int]:
return self._channel_mask.channels()
def update_channels(self, channel: List[int]):
if self._channel_mask.add_channel(channel):
self._dirty = True
@property
def is_dirty(self) -> bool:
return self._dirty
@property
def is_modified(self) -> bool:
return self._last_modify_time == 0 or not self._filepath.exists() or (self._filepath.is_file() and self._filepath.stat().st_mtime > self._last_modify_time)
@property
def last_modified(self) -> int:
return self._last_modify_time
def new_recording_file(self, _channel: int, _start_time, database = None) -> 'RecordingFile':
if _channel not in self._raw_serial_number:
self._raw_serial_number[_channel] = 0
else:
self._raw_serial_number[_channel] += 1
serial_number = self._raw_serial_number[_channel]
# if _channel is not None:
# for rf in self._recording_file_ch:
# if rf._channel == _channel:
# serial_number = serial_number + 1
# else:
# raise RuntimeError('new file no channel')
n = '{}_{}_{}{}'.format(self.filename, _channel, serial_number, RecordingFile.FILE_EXT)
p = self._filepath.with_name(n)
f = RecordingFile(p, database = database)
f._meta_file = self
f._file_uuid = self._file_uuid
f._serial_number = serial_number
f._data_format = self._data_format
f._channel = _channel
self._dirty = True
_name = str(n)
_path = str(p).replace('/', '^')
raw_data = {
'name': _name,
'path': _path,
'parent': self._id_db,
'size': '0',
'uuid': str(self._file_uuid),
'serial_number': str(serial_number),
'data_format': 'rec',
'channel': int(_channel),
'start_time': str(_start_time),
}
return f, raw_data
def new_recording_mini(self, _channel: int, _start_time, scale, database = None) -> 'RecordingMini':
if scale not in self._mini_serial_number:
self._mini_serial_number[scale] = {}
if _channel not in self._mini_serial_number[scale]:
self._mini_serial_number[scale][_channel] = 0
else:
self._mini_serial_number[scale][_channel] += 1
serial_number = self._mini_serial_number[scale][_channel]
n = '{}_{}_{}{}'.format(self.filename, _channel, serial_number, RecordingMini.FILE_EXT)
p = self._filepath.with_name(n)
f = RecordingMini(p, scale = scale, database = database)
f._meta_file = self
f._file_uuid = self._file_uuid
f._serial_number = serial_number
f._data_format = self._data_format
f._channel = _channel
self._dirty = True
_name = str(n)
_path = str(p).replace('/', '^')
mini_data = {
'name': _name,
'path': _path,
'parent': self._id_db,
'size': '0',
'uuid': str(self._file_uuid),
'serial_number': str(serial_number),
'scale': scale,
'channel': int(_channel),
'start_time': str(_start_time),
}
return f, mini_data
def clear_recording_file(self):
if len(self._channel_mask) > 0:
self._channel_mask.clear()
self._dirty = True
return None
def change_filepath(self, target_path: Path) -> Path:
meta_path = self.filepath
new_name = target_path.with_suffix('').name
t = target_path.with_name(new_name).with_suffix(RecordingMetaFile.FILE_EXT)
meta_path.rename(t)
return t
def remove_file(self):
meta_path = self.filepath
meta_path.unlink()
return None
@classmethod
def remove_meta_file(cls, filepath: Union[str, Path]):
if isinstance(filepath, str):
filepath = Path(filepath)
if not RecordingMetaFile.is_recording_meta_file(filepath):
raise RuntimeError('not a recording meta file : ' + str(filepath))
try:
meta = cls.load(filepath)
meta.remove_file()
except:
# force remove
parent = filepath.parent
name = filepath.name
filepath.unlink()
for p in list(parent.iterdir()):
if p.name.startswith(name) and p.name.endswith(RecordingFile.FILE_EXT):
p.unlink()
return None
def as_json(self) -> JSON:
ret = {
'file_name': str(self._filename),
'file_path': str(self._filepath),
'file_size': self.file_size,
'time_duration': self._last_time,
'create_time': self._create_time,
'channel': self.channels,
'configuration': self._configuration.as_json(),
'device': self._device,
}
return ret
class RecordingFile:
"""
**RecordingFile header format**
::
struct {
u32_B magic_number;
u32_B version_number
u64_B create_time;
# reference meta
u8 file_uuid[16];
u8 serial_number;
u8 data_format[4];
union {
# raw data section
# data_format = b'raw\0'
struct {
u8 raw_data[...]
}
# recording data section
# data_format = b'rec\0'
struct {
struct RecordingData[...];
}
}
}
create_time
unix time stamp
file_uuid
meta file uuid.
serial_number
the serial number of the split files. for mother file, the number equal to 0.
"""
__slots__ = ('_filepath', '_create_time', '_file_version', '_file_uuid', '_serial_number', '_data_format',
'_meta_file', '_file_object', '_channel', '_status', '_id_db', '_database', '_size')
FILE_EXT = '.bprf'
MAGIC_NUMBER = b'BPRF'
def __init__(self, filepath: Path, id_db = 0, database = None):
# file path
self._filepath = filepath
self._id_db = id_db
self._database = database
self._size = 0
# file header
self._create_time = int(1000 * time())
self._file_version = RecordingFileVersion.VERSION_00_02
self._file_uuid = None
self._serial_number = None
self._data_format = None
self._channel = None
# file status (open/close)
self._status = False
# meta file
self._meta_file = None
# cache file object
self._file_object = None
@classmethod
def is_recording_file(cls, path: Union[str, Path]) -> bool:
if isinstance(path, str):
path = Path(path)
if not path.is_file():
return False
with path.open('br') as f:
try:
return cls._is_recording_file(f)
except BaseException as e:
raise IOError('file ' + str(path)) from e
@classmethod
def _is_recording_file(cls, io: BinaryIO) -> bool:
header = io.read(4)
return header == RecordingFile.MAGIC_NUMBER
@staticmethod
def load(filepath: Union[str, Path]):
if isinstance(filepath, str):
filepath = Path(filepath)
if not RecordingFile.is_recording_file(filepath):
raise IOError('not a recording file : ' + str(filepath))
f = RecordingFile(filepath, database = self._database)
f._load()
return f
@property
def filepath(self) -> Path:
return self._filepath
@property
def filename(self) -> str:
"""
:return: filename with file extension
"""
return self._filepath.name
@property
def create_time(self) -> Optional[int]:
return self._create_time
@property
def file_version(self) -> Optional[int]:
return self._file_version
@property
def file_uuid(self) -> uuid.UUID:
return self._file_uuid
@property
def serial_number(self) -> int:
return self._serial_number
@property
def data_format(self) -> bytes:
return self._data_format
def open(self):
self._status = True
return None
def close(self, _end_time):
if self._database is not None:
_data = {
'end_time': str(_end_time),
'size': str(self._size)
}
2021-12-20 14:52:55 +08:00
if self._meta_file._id_db != 0:
_data['parent'] = self._meta_file._id_db
self._database.put_queue(['data_raw_update', self._id_db, self._channel, _data])
self._status = False
return None
def update_endtime(self, _end_time):
if self._database is not None:
_data = {
'end_time': str(_end_time),
'size': str(self._size)
}
self._database.put_queue(['data_raw_update', self._id_db, self._channel, _data])
return None
def flush(self):
if self._file_object is not None and self._file_object.writable():
self._file_object.flush()
def _ensure_read(self) -> IO:
return None
def _ensure_write(self) -> BufferedWriter:
return None
def _load(self):
"""load file header"""
return None
def _write(self, f: FileEncoder):
return None
def read(self, size: Optional[int] = None) -> bytes:
return None
# def write(self, content: Union[bytes, RecordingData]) -> int:
def write(self, content: str, channels: list) -> int:
if not isinstance(content, str):
raise RuntimeError('wrong data format : ' + repr(self._data_format))
self._meta_file.update_channels(channels)
sz = sys.getsizeof(content)
self._size += sz
return sz
def _write_recording_data(self, data: RecordingData):
# check channel of data in channel of meta
meta = self._meta_file
if meta is not None:
meta.update_channels(data.channels())
return None
def read_block(self) -> Optional[RecordingData]:
if self._data_format != RecordingFileDataFormat.REC_DATA:
raise RuntimeError('wrong data format : ' + repr(self._data_format))
return RecordingData.read_block(self._ensure_read())
def read_block_iter(self) -> Iterable[RecordingData]:
if self._data_format != RecordingFileDataFormat.REC_DATA:
raise RuntimeError('wrong data format : ' + repr(self._data_format))
auto_close = self._file_object is None
file = self._ensure_read()
try:
data = RecordingData.read_block(file)
while data is not None:
yield data
data = RecordingData.read_block(file)
finally:
if auto_close:
self.close()
return None
class RecordingMini:
__slots__ = ('_filepath', '_create_time', '_file_version', '_file_uuid', '_serial_number', '_data_format',
'_meta_file', '_file_object', '_channel', '_status', '_id_db', '_database', '_size',
'_scale')
FILE_EXT = '.bpri'
MAGIC_NUMBER = b'BPRI'
def __init__(self, filepath: Path, scale = 100, id_db = 0, database = None):
# file path
self._filepath = filepath
self._id_db = id_db
self._database = database
self._size = 0
# file header
self._create_time = int(1000 * time())
self._file_version = RecordingFileVersion.VERSION_00_02
self._file_uuid = None
self._serial_number = None
self._data_format = None
self._channel = None
# file status (open/close)
self._status = False
# meta file
self._meta_file = None
# cache file object
self._file_object = None
# scale
self._scale = scale
@property
def filepath(self) -> Path:
return self._filepath
@property
def size(self) -> int:
return self._size
@property
def filename(self) -> str:
"""
:return: filename with file extension
"""
return self._filepath.name
@property
def create_time(self) -> Optional[int]:
return self._create_time
@property
def file_version(self) -> Optional[int]:
return self._file_version
@property
def file_uuid(self) -> uuid.UUID:
return self._file_uuid
@property
def serial_number(self) -> int:
return self._serial_number
@property
def data_format(self) -> bytes:
return self._data_format
@property
def scale(self) -> int:
return self._scale
def open(self):
self._status = True
def close(self, _end_time):
if self._database is not None:
_data = {
'end_time': str(_end_time),
'size': str(self._size)
}
2021-12-20 14:52:55 +08:00
if self._meta_file._id_db != 0:
_data['parent'] = self._meta_file._id_db
self._database.put_queue(['data_mini_update', self._id_db, self._channel, _data])
# self._database.put_queue(['data_mini_update', self._id_db, self._channel, _data])
def _load(self):
"""load file header"""
return None
def read(self, size: Optional[int] = None) -> bytes:
return None
# def write(self, content: Union[bytes, RecordingData]) -> int:
def write(self, data_mean: list) -> int:
if len(data_mean) > 0:
self._size += sys.getsizeof(data_mean)
return 0
class RecordingFileWriter:
__slots__ = ('_meta', '_recording_file', '_recording_file_dict', '_channel_list', '_database',
'splitting_threshold_time', 'splitting_threshold_size', '_writer_batch_size',
'_splitting_size', '_time', '_time_now', '_recording_mini_dict',
'_data_value_ch', '_close', '_data_mini_ch',
'_mini_scale_list', '_time_real_time', '_data_rl', '_data_db',
'_raw_save', '_mini_save', '_data_time_ch', '_data_value_ch_for_rl',
2022-03-28 16:11:08 +08:00
'_data_time_ch_for_rl', '_device_id', '_send_data', '_data_mqtt_ch', '_id_db_save', '_raw_create_not_done',
'_mini_create_not_done')
2021-12-20 14:52:55 +08:00
def __init__(self, meta: RecordingMetaFile, device_id, database = None):
self._meta = meta
# self._recording_file = None
self._device_id = device_id
self._database = database
self._mini_scale_list = [10, 100, 1000]
self._recording_file_dict = {}
self._recording_mini_dict = {
'10': {},
'100': {},
'1000': {},
# '10000': {}
}
self._raw_save = {
'id': {},
2022-03-18 17:12:40 +08:00
'data': {},
'end_time': {},
'size': {}
2021-12-20 14:52:55 +08:00
}
self._mini_save = {
'10': {
'id': {},
'start_time': {},
'data_mean': {},
'data_random': {},
'data_bar': {}
},
'100': {
'id': {},
'start_time': {},
'data_mean': {},
'data_random': {},
'data_bar': {}
},
'1000': {
'id': {},
'start_time': {},
'data_mean': {},
'data_random': {},
'data_bar': {}
}
}
self._channel_list = []
# save data chache
self._data_rl = {}
self._data_db = {}
self._data_value_ch = {}
self._data_value_ch_for_rl = {}
self._data_time_ch_for_rl = {}
self._data_time_ch = {}
2022-03-11 10:34:44 +08:00
self._id_db_save = {}
2021-12-20 14:52:55 +08:00
# mini data
self._data_mini_ch = {}
# mqtt real time
self._data_mqtt_ch = {}
self._time = {}
self._time_now = None
self._time_real_time = {}
self._close = False
self._send_data = {}
# splitting
self.splitting_threshold_time = 30 * 60 * 1000 # one minute
2022-03-21 14:20:29 +08:00
self.splitting_threshold_size = 16 * 1024 # 16 * 16KB
2021-12-20 14:52:55 +08:00
self._writer_batch_size = 8192
self._splitting_size = None
2022-03-28 16:11:08 +08:00
self._raw_create_not_done = True
self._mini_create_not_done = True
2021-12-20 14:52:55 +08:00
@property
def meta_file(self) -> RecordingMetaFile:
return self._meta
@property
def channel_list(self):
return self._channel_list
def __enter__(self):
self.open()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
return None
def channels_update(self, channels):
channels.sort()
self._channel_list.extend(channels)
return self._channel_list
def open(self):
self._meta.clear_recording_file()
if self._meta.is_dirty:
self._meta.write(database = self._database)
self._splitting_size = 0
return None
def close(self, mqtt_thread):
2021-12-20 14:52:55 +08:00
self._close = True
if len(self._recording_file_dict) > 0:
for ch in self._data_db.keys():
if len(self._data_rl[ch]) > 0:
self._data_rl[ch].append(str(int(self._time_now)))
mes = ' '.join(self._data_rl[ch])
mqtt_thread[ch].on_message(mes)
self._data_rl[ch].clear()
self._send_data[ch] = False
2021-12-20 14:52:55 +08:00
if self._recording_file_dict[ch]._status:
_data = ' '.join(self._data_db[ch])
self._raw_save['data'][ch] = _data
self._raw_save['id'][ch] = self._recording_file_dict[ch]._id_db
self._recording_file_dict[ch].write(_data, self._channel_list)
self._recording_file_dict[ch].close(self._time_now)
self._meta._size += self._recording_file_dict[ch]._size
# self._data_db.clear()
if self._database is not None:
2022-03-11 10:34:44 +08:00
self._database.put_queue(['data_raw_recording', self._raw_save['id'], self._channel_list, self._raw_save['data'], self._id_db_save])
2021-12-20 14:52:55 +08:00
# self._database.put_queue(['data_raw_recording', self._raw_save['id'], self._channel_list, self._raw_save['data']])
self._recording_file_dict.clear()
for scale in self._mini_scale_list:
if len(self._recording_mini_dict[str(scale)]) > 0:
for ch in self._recording_mini_dict[str(scale)].keys():
if self._recording_mini_dict[str(scale)][ch]._status:
self._mini_save[str(scale)]['id'][ch] = self._recording_mini_dict[str(scale)][ch]._id_db
self._mini_save[str(scale)]['start_time'][ch] = str(self._data_mini_ch[ch][str(scale)]['start_time'])
self._mini_save[str(scale)]['data_mean'][ch] = self._data_mini_ch[ch][str(scale)]['mean'].copy()
# self._mini_save[str(scale)]['data_random'][ch] = self._data_mini_ch[ch][str(scale)]['random'].copy()
# self._mini_save[str(scale)]['data_bar'][ch] = self._data_mini_ch[ch][str(scale)]['bar'].copy()
# start_time = str(self._data_mini_ch[ch][str(scale)]['start_time'])
# data_mean = self._data_mini_ch[ch][str(scale)]['mean'].copy()
# data_random = self._data_mini_ch[ch][str(scale)]['random'].copy()
# print('channel:', ch, ', data: ', data_mean)
self._recording_mini_dict[str(scale)][ch].write(self._mini_save[str(scale)]['data_mean'][ch])
self._recording_mini_dict[str(scale)][ch].close(self._time_now)
self._data_mini_ch[ch][str(scale)]['mean'].clear()
# self._data_mini_ch[ch][str(scale)]['random'].clear()
# del self._data_mini_ch[ch][str(scale)]['mean']
# del self._data_mini_ch[ch][str(scale)]['random']
if self._database is not None:
self._database.put_queue(['data_mini_recording', self._mini_save[str(scale)]['id'], self._channel_list, self._mini_save[str(scale)]['start_time'], self._mini_save[str(scale)]['data_mean']])
# self._database.put_queue(['data_mini_recording', self._recording_mini_dict[str(scale)], self._mini_save[str(scale)]['id'], self._channel_list, self._mini_save[str(scale)]['start_time'], self._mini_save[str(scale)]['data_mean'], self._mini_save[str(scale)]['data_random']])
self._data_mini_ch.clear()
self._recording_mini_dict.clear()
# del self._data_mini_ch
# del self._recording_mini_dict
gc.collect()
self._meta._last_time = self._time_now
self._meta.update_subfile(database = self._database)
# self._meta.write(database = self._database)
print('close2')
return True
def flush(self):
if len(self._recording_file_dict) > 0:
for ch in self._channel_list:
if ch in self._recording_file_dict:
self._recording_file_dict[ch].flush()
return None
def get_bar(self, data_ch: list, time_ch: list):
ret = []
_first_time = -1
_last_time = -1
_max = max(data_ch)
_max_index = data_ch.index(_max)
_min = min(data_ch)
_min_index = data_ch.index(_min)
if time_ch is not None and len(time_ch) >= 2:
_first_time = time_ch[0]
_last_time = time_ch[-1]
if _max_index < _min_index:
if _first_time > 0:
ret.append(str(_first_time))
ret.append(str(_max))
if _last_time > 0:
ret.append(str(_last_time))
ret.append(str(_min))
else:
if _first_time > 0:
ret.append(str(_first_time))
ret.append(str(_min))
if _last_time > 0:
ret.append(str(_last_time))
ret.append(str(_max))
return ret
def get_data_iter(self, d, mqtt_thread):
# print('****d size', d.data_size)
for t, c, v in d.entry_iter():
if c in self._data_db:
### send real-time
if len(self._data_rl[c]) > 0 and self._send_data[c]:
self._data_rl[c].append(str(int(t)))
mes = ' '.join(self._data_rl[c])
# self._data_mqtt_ch[c] = self._data_rl[c].copy()
# self._data_mqtt_ch[c].append(str(int(t)))
mqtt_thread[c].on_message(mes)
self._data_rl[c].clear()
self._send_data[c] = False
### send real-time
sample_rate_rl = 1
# rec data
if c < 256:
sample_rate_rl = int(d.sample_rate / ( 500 * len(self.channel_list) )) #* len(self.channel_list)
# others
else:
sample_rate_rl = int(d.sample_rate_channel_upper_256 / ( 1000 * len(self.channel_list)))
if self._close is False:
self._data_value_ch[c].append(int(v))
self._data_value_ch_for_rl[c].append(int(v))
self._data_time_ch_for_rl[c].append(int(t))
# self._data_time_ch[c].append(int(t))
for scale in self._mini_scale_list:
if self._data_mini_ch[c][str(scale)]['start_time'] is None:
self._data_mini_ch[c][str(scale)]['start_time'] = str(int(t))
if len(self._data_value_ch_for_rl[c]) >= sample_rate_rl:
if len(self._data_value_ch_for_rl[c]) == 1:
if len(self._data_rl[c]) == 0:
self._data_rl[c].append(str(int(t)))
# self._data_rl[c].append(str(self._data_time_ch_for_rl[c][0]))
self._data_rl[c].append(str(self._data_value_ch_for_rl[c][0]))
else:
if len(self._data_rl[c]) == 0:
self._data_rl[c].append(str(int(t)))
# self._data_rl[c].append(str(self._data_time_ch_for_rl[c][0]))
# self._data_rl[c].append(str(self._data_value_ch_for_rl[c][0]))
_max = max(self._data_value_ch_for_rl[c])
_max_index = self._data_value_ch_for_rl[c].index(_max)
_min = min(self._data_value_ch_for_rl[c])
_min_index = self._data_value_ch_for_rl[c].index(_min)
# _mean = mean(self._data_value_ch_for_rl[c])
# _first_time = self._data_time_ch_for_rl[c][0]
# _last_time = self._data_time_ch_for_rl[c][-1]
if _max_index < _min_index:
# self._data_rl[c].append(str(_first_time))
self._data_rl[c].append(str(_max))
# self._data_rl[c].append(str(_last_time))
self._data_rl[c].append(str(_min))
else:
# self._data_rl[c].append(str(_first_time))
self._data_rl[c].append(str(_min))
# self._data_rl[c].append(str(_last_time))
self._data_rl[c].append(str(_max))
self._data_value_ch_for_rl[c].clear()
self._data_time_ch_for_rl[c].clear()
# mini picture
if len(self._data_value_ch[c]) >= 10:
self._data_mini_ch[c]['10']['mean'].append( int(mean(self._data_value_ch[c][0:9])) )
# self._data_mini_ch[c]['10']['random'].append( str(self._data_value_ch[c][random.randint(0,9)]) )
# _bar = self.get_bar(self._data_value_ch[c], None)
# self._data_mini_ch[c]['10']['bar'].extend(_bar)
self._data_value_ch[c].clear()
if int(len(self._data_mini_ch[c]['10']['mean']) / 10) - self._data_mini_ch[c]['10']['dec'] > 0:
self._data_mini_ch[c]['100']['mean'].append( int(mean(self._data_mini_ch[c]['10']['mean'][-10:])) )
# self._data_mini_ch[c]['100']['random'].append( str(self._data_mini_ch[c]['10']['random'][random.randint(-10,-1)]) )
# _bar = self.get_bar(self._data_mini_ch[c]['10']['bar'], None)
# self._data_mini_ch[c]['100']['bar'].extend(_bar)
self._data_mini_ch[c]['10']['dec'] = int(len(self._data_mini_ch[c]['10']['mean']) / 10)
if int(len(self._data_mini_ch[c]['100']['mean']) / 10) - self._data_mini_ch[c]['100']['dec'] > 0:
self._data_mini_ch[c]['1000']['mean'].append( int(mean(self._data_mini_ch[c]['100']['mean'][-10:])) )
# self._data_mini_ch[c]['1000']['random'].append( str(self._data_mini_ch[c]['100']['random'][random.randint(-10,-1)]) )
# _bar = self.get_bar(self._data_mini_ch[c]['100']['bar'], None)
# self._data_mini_ch[c]['1000']['bar'].extend(_bar)
self._data_mini_ch[c]['100']['dec'] = int(len(self._data_mini_ch[c]['100']['mean']) / 10)
# if int(len(self._data_mini_ch[c]['1000']['mean']) / 10) - self._data_mini_ch[c]['1000']['dec'] > 0:
# self._data_mini_ch[c]['10000']['mean'].append( int(mean(self._data_mini_ch[c]['1000']['mean'][-10:])) )
# self._data_mini_ch[c]['10000']['random'].append( str(self._data_mini_ch[c]['1000']['random'][random.randint(-10,-1)]) )
# self._data_mini_ch[c]['1000']['dec'] = int(len(self._data_mini_ch[c]['1000']['mean']) / 10)
# add normal data
self._data_db[c].append(str(int(t)))
self._data_db[c].append(str(v))
self._time_now = int(t)
return
2022-03-28 16:11:08 +08:00
2022-04-07 12:29:18 +08:00
# @calculate_time(1)
2021-12-20 14:52:55 +08:00
def write(self, data: Union[bytes, RecordingData, List[bytes], List[RecordingData]], mqtt_thread) -> int:
# check size
ths = self.splitting_threshold_size
tht = self.splitting_threshold_time
if self._close is False:
if len(self._recording_file_dict) == 0:
self._switch_recording_file()
elif ths is not None and self._splitting_size > ths * len(self._channel_list):
self._switch_recording_file()
for scale in self._mini_scale_list:
if len(self._recording_mini_dict[str(scale)]) == 0:
self._switch_recording_mini(scale = scale)
elif ths is not None and self._recording_mini_dict[str(scale)][list(self._recording_mini_dict[str(scale)].keys())[0]].size > ths:
self._switch_recording_mini(scale = scale)
# write data
if isinstance(data, list):
for d in data:
if self._close is False:
if ths is not None and self._splitting_size > ths * len(self._channel_list):
self._switch_recording_file()
for scale in self._mini_scale_list:
if ths is not None and self._recording_mini_dict[str(scale)][list(self._recording_mini_dict[str(scale)].keys())[0]].size > ths:
self._switch_recording_mini(scale = scale)
sz = 0
# init
if len(self._data_value_ch) == 0:
for ch in self._channel_list:
self._data_value_ch[ch] = []
self._data_value_ch_for_rl[ch] = []
self._data_time_ch_for_rl[ch] = []
self._data_time_ch[ch] = []
self._send_data[ch] = False
self._data_mqtt_ch[ch] = []
self._data_rl[ch] = []
self._data_db[ch] = []
self._time[ch] = 0
self._time_real_time[ch] = 0
if len(self._data_mini_ch) == 0:
for ch in self._channel_list:
self._data_mini_ch[ch] = {}
for scale in self._mini_scale_list:
self._data_mini_ch[ch][str(scale)] = {
'start_time': None,
'mean': [],
'random': [],
'bar': [],
'dec': 0,
}
self.get_data_iter(d, mqtt_thread)
2022-03-28 16:11:08 +08:00
if len(self._recording_file_dict) > 0:
for ch in self._data_db.keys():
if self._time_now - self._time_real_time[ch] > 1000000:
self._send_data[ch] = True
self._time_real_time[ch] = self._time_now
2022-03-18 17:12:40 +08:00
for ch in self._recording_file_dict:
if self._recording_file_dict[ch]._id_db == 0:
return None
2022-03-28 16:11:08 +08:00
for ch in self._data_db.keys():
for scale in self._mini_scale_list:
if self._recording_mini_dict[str(scale)][ch]._id_db == 0:
return None
if self._raw_create_not_done :
self._raw_create_not_done = False
2022-03-28 17:17:07 +08:00
self._meta.update_subfile_raw(database = self._database)
2022-03-28 16:11:08 +08:00
if self._mini_create_not_done :
self._mini_create_not_done = False
2022-03-28 17:17:07 +08:00
self._meta.update_subfile_mini(database = self._database)
2022-03-18 17:12:40 +08:00
2021-12-20 14:52:55 +08:00
data_save = False
mini_save = False
if len(self._recording_file_dict) > 0:
for ch in self._data_db.keys():
2022-03-28 16:11:08 +08:00
if self._time_now - self._time[ch] > 5000000:
2021-12-20 14:52:55 +08:00
if self._recording_file_dict[ch]._status:
_data = ' '.join(self._data_db[ch])
write_sz = self._recording_file_dict[ch].write(_data, self._channel_list)
self._raw_save['data'][ch] = _data
self._raw_save['id'][ch] = self._recording_file_dict[ch]._id_db
2022-03-18 17:12:40 +08:00
self._raw_save['end_time'][ch] = self._time_now
self._raw_save['size'][ch] = self._recording_file_dict[ch]._size
2021-12-20 14:52:55 +08:00
self._data_db[ch].clear()
self._time[ch] = self._time_now
self._meta._last_time = self._time_now
self._splitting_size += write_sz
data_save = True
2022-03-28 17:27:04 +08:00
if len(self._data_mini_ch[ch]['1000']['mean']) >= 10:
2021-12-20 14:52:55 +08:00
mini_save = True
for scale in self._mini_scale_list:
2022-03-28 16:11:08 +08:00
str_mean = [str(int) for int in self._data_mini_ch[ch][str(scale)]['mean']]
data_mean = str(self._data_mini_ch[ch][str(scale)]['start_time']) + ' ' + ' '.join(str_mean) + '"***"'
2021-12-20 14:52:55 +08:00
self._mini_save[str(scale)]['id'][ch] = self._recording_mini_dict[str(scale)][ch]._id_db
self._mini_save[str(scale)]['start_time'][ch] = str(self._data_mini_ch[ch][str(scale)]['start_time'])
2022-03-28 16:11:08 +08:00
self._mini_save[str(scale)]['data_mean'][ch] = data_mean
2021-12-20 14:52:55 +08:00
# self._mini_save[str(scale)]['data_random'][ch] = self._data_mini_ch[ch][str(scale)]['random'].copy()
# self._mini_save[str(scale)]['data_bar'][ch] = self._data_mini_ch[ch][str(scale)]['bar'].copy()
self._recording_mini_dict[str(scale)][ch].write(self._mini_save[str(scale)]['data_mean'][ch])
self._data_mini_ch[ch][str(scale)]['mean'].clear()
# self._data_mini_ch[ch][str(scale)]['random'].clear()
self._data_mini_ch[ch][str(scale)]['dec'] = 0
self._data_mini_ch[ch][str(scale)]['start_time'] = None
if data_save is True:
if self._database is not None:
2022-03-18 17:12:40 +08:00
recording_input = ['data_raw_recording_new', copy(self._raw_save['id']), copy(self._channel_list), copy(self._raw_save['data']), copy(self._raw_save['end_time']), copy(self._raw_save['size'])]
self._database.put_queue(recording_input)
2022-03-28 17:17:07 +08:00
self._meta.update_subfile_time_size(database = self._database)
2021-12-20 14:52:55 +08:00
if mini_save is True:
if self._database is not None:
for scale in self._mini_scale_list:
2022-03-28 16:11:08 +08:00
self._database.put_queue(['data_mini_recording_new', copy(self._mini_save[str(scale)]['id']), self._channel_list, copy(self._mini_save[str(scale)]['data_mean'])])
2022-03-28 17:17:07 +08:00
self._meta.update_subfile_time_size(database = self._database)
2021-12-20 14:52:55 +08:00
del data
return None
def _switch_recording_file(self):
2022-03-28 16:11:08 +08:00
self._raw_create_not_done = True
2021-12-20 14:52:55 +08:00
if len(self._recording_file_dict) > 0:
for ch in self._recording_file_dict.keys():
2022-03-11 10:34:44 +08:00
self._id_db_save[ch] = self._recording_file_dict[ch]._id_db
2021-12-20 14:52:55 +08:00
self._recording_file_dict[ch].close(self._time_now)
self._meta._size += self._recording_file_dict[ch]._size
start_time = 0
if self._time_now is not None:
start_time = self._time_now
raw_data_dict = {}
for ch in self._channel_list:
self._recording_file_dict[ch], raw_data_tmp = self._meta.new_recording_file(ch, start_time, database = self._database)
raw_data_dict[ch] = raw_data_tmp
self._recording_file_dict[ch].open()
if self._database is not None:
self._database.put_queue(['data_raw_create', raw_data_dict, self._channel_list, self._device_id])
self._meta._last_time = self._time_now
2022-03-28 17:17:07 +08:00
# self._meta.update_subfile(database = self._database)
2021-12-20 14:52:55 +08:00
self._splitting_size = 0
return None
def _switch_recording_mini(self, scale):
2022-03-28 16:11:08 +08:00
self._mini_create_not_done = True
2021-12-20 14:52:55 +08:00
if len(self._recording_mini_dict[str(scale)]) > 0:
for ch in self._recording_mini_dict[str(scale)].keys():
self._recording_mini_dict[str(scale)][ch].close(self._time_now)
start_time = 0
if self._time_now is not None:
start_time = self._time_now
mini_data_dict = {}
for ch in self._channel_list:
self._recording_mini_dict[str(scale)][ch], mini_data_tmp = self._meta.new_recording_mini(ch, start_time, scale = scale, database = self._database)
mini_data_dict[ch] = mini_data_tmp
self._recording_mini_dict[str(scale)][ch].open()
if self._database is not None:
self._database.put_queue(['data_mini_create', mini_data_dict, self._channel_list, scale, self._device_id])
self._meta._last_time = self._time_now
2022-03-28 17:17:07 +08:00
# self._meta.update_subfile(database = self._database)
2021-12-20 14:52:55 +08:00
return None
def update_meta_id(self, _id):
self._meta._id_db = _id
return None
def update_raw_dict_id(self, _channel, _id):
self._recording_file_dict[int(_channel)]._id_db = _id
if int(_channel) not in self._meta._recording_sub_file:
self._meta._recording_sub_file[int(_channel)] = []
self._meta._recording_sub_file[int(_channel)].append(_id)
2021-12-20 14:52:55 +08:00
return None
def update_mini_dict_id(self, _scale, _channel, _id):
self._recording_mini_dict[str(_scale)][int(_channel)]._id_db = _id
if int(_channel) not in self._meta._recording_sub_mini:
self._meta._recording_sub_mini[int(_channel)] = {}
if int(_scale) not in self._meta._recording_sub_mini[int(_channel)]:
self._meta._recording_sub_mini[int(_channel)][int(_scale)] = []
self._meta._recording_sub_mini[int(_channel)][int(_scale)].append(_id)
2021-12-20 14:52:55 +08:00
return None