2019-08-10 12:52:46 +08:00
|
|
|
"""
|
|
|
|
|
purpose:
|
|
|
|
|
|
|
|
|
|
in order to analyze the data loss of bluetooth, a small code is needed.
|
|
|
|
|
Guess that there are three type of data loss. In order to prove this idea,
|
|
|
|
|
several tests are needed
|
|
|
|
|
|
|
|
|
|
types of data loss:
|
|
|
|
|
|
|
|
|
|
1. packet loss:
|
|
|
|
|
bluetooth notification does not need any negotiation.
|
|
|
|
|
if there is any packet loss during the connection,
|
|
|
|
|
it will not be lost forever
|
|
|
|
|
|
|
|
|
|
2. program delay causes loss in client(headstage):
|
|
|
|
|
too many effort on data packaging, it would cause
|
|
|
|
|
data delay
|
|
|
|
|
|
|
|
|
|
3. program delay causes loss in host(controller):
|
|
|
|
|
...todo and need to define
|
|
|
|
|
|
|
|
|
|
Assume that we have lost several bytes data. We want to analyze the type of data lost.
|
|
|
|
|
method:
|
|
|
|
|
compare ramp data with time stamp, there are several conditions
|
|
|
|
|
if data_delta * 1 / sampling_rate == time_delta:
|
|
|
|
|
this should be packet loss
|
|
|
|
|
else
|
|
|
|
|
this should be program delay
|
|
|
|
|
|
|
|
|
|
method to coped with the corresponding problem:
|
|
|
|
|
1. try 'Indication' to check data loss is reduced or not.
|
|
|
|
|
2. modified the procedure of data packaging
|
|
|
|
|
3. after the upper two problems are excluded, this problem should be left over
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
import os.path
|
|
|
|
|
import sys
|
|
|
|
|
from collections import Counter
|
2019-08-10 13:20:39 +08:00
|
|
|
from typing import List, Optional, Dict, Set, Any, Union
|
2019-08-10 12:52:46 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class Options:
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.skip = False
|
|
|
|
|
self.round = 2
|
|
|
|
|
self.overflow_pow = 10
|
2019-08-10 13:20:39 +08:00
|
|
|
self.graph: Union[bool, str] = False
|
|
|
|
|
self.graph_log_scale = False
|
2019-08-10 12:52:46 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class Result:
|
2019-08-10 13:20:39 +08:00
|
|
|
def __init__(self, data_file: str, options: Options):
|
2019-08-10 12:52:46 +08:00
|
|
|
# data file information
|
2019-08-10 13:20:39 +08:00
|
|
|
self.data_file = data_file
|
2019-08-10 12:52:46 +08:00
|
|
|
self.date_time: Optional[str] = None
|
|
|
|
|
self.device_name: Optional[str] = None
|
|
|
|
|
self.device_address: Optional[str] = None
|
|
|
|
|
self.parameter: Dict[str, str] = {}
|
|
|
|
|
self.channel: Set[int] = set()
|
|
|
|
|
self.total_duration: float = 0.0
|
|
|
|
|
|
|
|
|
|
# analysis data
|
|
|
|
|
self.data_count = 0
|
|
|
|
|
self.time_delta_counter: Dict[float, int] = Counter()
|
|
|
|
|
self.value_delta_counter: Dict[int, int] = Counter()
|
|
|
|
|
|
|
|
|
|
# analysis options
|
|
|
|
|
self.round = options.round
|
|
|
|
|
self.overflow = 2 ** options.overflow_pow
|
|
|
|
|
|
|
|
|
|
# temp data
|
|
|
|
|
self._prev_time: Optional[float] = None
|
|
|
|
|
self._prev_value: Optional[int] = None
|
|
|
|
|
self._close = False
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def sample_rate(self) -> int:
|
|
|
|
|
try:
|
|
|
|
|
sample_rate = self.parameter['SAMPLE_RATE']
|
|
|
|
|
except KeyError as e:
|
|
|
|
|
raise RuntimeError('data file do not contain SAMPLE_RATE') from e
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
return int(sample_rate)
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
raise RuntimeError('data file SAMPLE_RATE value not a value : ' + str(sample_rate)) from e
|
|
|
|
|
|
|
|
|
|
def update(self, time: float, channel: int, value: int):
|
|
|
|
|
if self._close:
|
|
|
|
|
raise RuntimeError('closed')
|
|
|
|
|
|
|
|
|
|
self.data_count += 1
|
|
|
|
|
|
|
|
|
|
if self._prev_time is not None:
|
|
|
|
|
delta = round(time - self._prev_time, self.round)
|
|
|
|
|
self.time_delta_counter[delta] += 1
|
|
|
|
|
|
|
|
|
|
if self._prev_value is not None:
|
|
|
|
|
delta = self._prev_value - value
|
|
|
|
|
self.value_delta_counter[delta] += 1
|
|
|
|
|
|
|
|
|
|
self._prev_time = time
|
|
|
|
|
self.channel.add(channel)
|
|
|
|
|
self._prev_value = value
|
|
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
|
self._close = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def calculate(txt_file: str, options: Options) -> Result:
|
|
|
|
|
if not os.path.exists(txt_file):
|
|
|
|
|
raise FileNotFoundError(txt_file)
|
|
|
|
|
|
2019-08-10 13:20:39 +08:00
|
|
|
result = Result(txt_file, options)
|
2019-08-10 12:52:46 +08:00
|
|
|
|
|
|
|
|
parsing_state = 0
|
|
|
|
|
|
|
|
|
|
with open(txt_file) as _file:
|
|
|
|
|
for line, content in enumerate(_file): # type: int, str
|
|
|
|
|
content = content.strip()
|
|
|
|
|
|
|
|
|
|
if len(content) == 0:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if parsing_state == 0:
|
|
|
|
|
if content.startswith('#'):
|
|
|
|
|
if result.date_time is None:
|
|
|
|
|
result.date_time = content[1:].strip()
|
|
|
|
|
|
|
|
|
|
elif content.startswith('# device_name'):
|
|
|
|
|
result.device_name = content[len('# device_name'):].strip()
|
|
|
|
|
|
|
|
|
|
elif content.startswith('# mac_address'):
|
|
|
|
|
result.device_address = content[len('# mac_address'):].strip()
|
|
|
|
|
|
|
|
|
|
elif content.startswith('# parameter'):
|
|
|
|
|
parsing_state = 1
|
|
|
|
|
|
|
|
|
|
elif parsing_state == 1:
|
|
|
|
|
if content.startswith('#'):
|
|
|
|
|
if content.startswith('# time_stamp'):
|
|
|
|
|
parsing_state = 2
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
part = content[2:].split(' ', maxsplit=2)
|
|
|
|
|
|
|
|
|
|
result.parameter[part[0]] = part[1]
|
|
|
|
|
|
|
|
|
|
elif parsing_state == 2:
|
|
|
|
|
if content.startswith('#'):
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
part = content.split(' ', maxsplit=3)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
time = part[0]
|
|
|
|
|
channel = part[1]
|
|
|
|
|
value = part[2]
|
|
|
|
|
except IndexError:
|
|
|
|
|
print('@%d' % (line + 1), 'incomplete data', ':', content)
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
check_pass = True
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
time = float(time)
|
|
|
|
|
except ValueError:
|
|
|
|
|
print('@%d' % (line + 1), 'incomplete time', ':', time)
|
|
|
|
|
check_pass = False
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
channel = int(channel)
|
|
|
|
|
except ValueError:
|
|
|
|
|
print('@%d' % (line + 1), 'incomplete channel', ':', channel)
|
|
|
|
|
check_pass = False
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
value = int(value)
|
|
|
|
|
except ValueError:
|
|
|
|
|
print('@%d' % (line + 1), 'incomplete value', ':', value)
|
|
|
|
|
check_pass = False
|
|
|
|
|
|
|
|
|
|
if check_pass:
|
|
|
|
|
result.update(time, channel, value)
|
|
|
|
|
|
|
|
|
|
result.close()
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def print_result(result: Result, options: Options):
|
|
|
|
|
def print_table(counter: Dict[Any, Any], key_format='%10d', value_format='%d'):
|
|
|
|
|
f = ' %s : %s' % (key_format, value_format)
|
|
|
|
|
for k in sorted(counter.keys()):
|
|
|
|
|
print(f % (k, counter[k]))
|
|
|
|
|
|
2019-08-10 13:20:39 +08:00
|
|
|
print(result.data_file)
|
2019-08-10 12:52:46 +08:00
|
|
|
print('data_count', result.data_count)
|
|
|
|
|
print('channel', ' '.join(list(map(str, sorted(result.channel)))))
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
sample_rate = result.sample_rate
|
|
|
|
|
print('sample_rate [1/s]', sample_rate)
|
|
|
|
|
print('1/sample_rate [ms]', 1000 / sample_rate)
|
|
|
|
|
except RuntimeError:
|
|
|
|
|
print('sample_rate', '(E)')
|
|
|
|
|
|
|
|
|
|
print('time_delta')
|
|
|
|
|
print_table(result.time_delta_counter, '%10.2f')
|
|
|
|
|
|
|
|
|
|
print('value_delta')
|
|
|
|
|
print_table(result.value_delta_counter)
|
|
|
|
|
|
2019-08-10 13:20:39 +08:00
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def paint_result(result: Result, options: Options):
|
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
|
|
|
|
|
fig, (at, av) = plt.subplots(2, 1)
|
|
|
|
|
|
|
|
|
|
time_delta_data = list(result.time_delta_counter)
|
|
|
|
|
expect_time_delta = 1000 / result.sample_rate
|
|
|
|
|
time_delta_bins = int((max(time_delta_data) - min(time_delta_data)) / expect_time_delta)
|
|
|
|
|
time_delta_weight = [result.time_delta_counter[k] for k in time_delta_data]
|
|
|
|
|
at.hist(time_delta_data,
|
|
|
|
|
bins=time_delta_bins,
|
|
|
|
|
weights=time_delta_weight,
|
|
|
|
|
log=options.graph_log_scale,
|
|
|
|
|
align='left')
|
|
|
|
|
|
|
|
|
|
value_delta_data = list(result.value_delta_counter)
|
|
|
|
|
value_delta_bins = max(value_delta_data) - min(value_delta_data)
|
|
|
|
|
value_delta_weight = [result.value_delta_counter[k] for k in value_delta_data]
|
|
|
|
|
av.hist(value_delta_data,
|
|
|
|
|
bins=value_delta_bins,
|
|
|
|
|
weights=value_delta_weight,
|
|
|
|
|
log=options.graph_log_scale,
|
|
|
|
|
align='left')
|
|
|
|
|
|
|
|
|
|
fig.tight_layout()
|
|
|
|
|
plt.show()
|
|
|
|
|
|
2019-08-10 12:52:46 +08:00
|
|
|
|
|
|
|
|
def print_help():
|
|
|
|
|
prg = sys.argv[0]
|
|
|
|
|
|
|
|
|
|
print(prg, '[OPTIONS]', 'FILE', '...')
|
|
|
|
|
print()
|
|
|
|
|
print('OPTIONS:')
|
|
|
|
|
print(' -h, --help : print help')
|
|
|
|
|
print(' --skip : skip if FILE not found')
|
|
|
|
|
print(' --round VALUE : floating number round')
|
|
|
|
|
print(' --overflow VALUE : overflow number. 2^VALUE')
|
2019-08-10 13:20:39 +08:00
|
|
|
print(' --graph[=FILE] : use matplotlib to generate graph')
|
|
|
|
|
print(' --graph-log-scale :')
|
2019-08-10 12:52:46 +08:00
|
|
|
print()
|
|
|
|
|
print('ARGUMENTS:')
|
|
|
|
|
print(' FILE : txt data file')
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(argv: List[str]):
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
:param argv:
|
2019-08-07 23:44:21 +08:00
|
|
|
:return:
|
2019-08-10 12:52:46 +08:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# df = pd.read_csv("C:/Users/yichin/Downloads/last-2019-07-18-16-30-15-7.csv", sep=",", skiprows=12, header=None)
|
|
|
|
|
# sel_df = pd.DataFrame(df)
|
|
|
|
|
# column, row = sel_df.shape
|
|
|
|
|
# sel_df = sel_df.fillna(value=0)
|
|
|
|
|
# delta_df = sel_df.diff(periods=1)
|
|
|
|
|
# delta_df = delta_df.fillna(value=0)
|
|
|
|
|
# delta_df = delta_df.drop(column - 1)
|
|
|
|
|
# delta_df.set_axis(['time_delta', 'data_delta'], axis='columns', inplace=True)
|
|
|
|
|
|
|
|
|
|
o = Options()
|
|
|
|
|
|
|
|
|
|
i = 0
|
|
|
|
|
while i < len(argv):
|
|
|
|
|
arg = argv[i]
|
|
|
|
|
|
|
|
|
|
if arg in ('-h', '--help'):
|
|
|
|
|
print_help()
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
elif arg == '--skip':
|
|
|
|
|
o.skip = True
|
|
|
|
|
i += 1
|
|
|
|
|
|
|
|
|
|
elif arg == '--round':
|
|
|
|
|
o.round = int(argv[i + 1])
|
|
|
|
|
i += 2
|
|
|
|
|
|
|
|
|
|
elif arg.startswith('--round='):
|
|
|
|
|
o.round = int(arg[len('--round='):])
|
|
|
|
|
i += 1
|
|
|
|
|
|
|
|
|
|
elif arg == '--overflow':
|
|
|
|
|
o.overflow_pow = int(argv[i + 1])
|
|
|
|
|
i += 2
|
|
|
|
|
|
|
|
|
|
elif arg.startswith('--overflow='):
|
|
|
|
|
o.overflow_pow = int(arg[len('--overflow='):])
|
|
|
|
|
i += 1
|
|
|
|
|
|
2019-08-10 13:20:39 +08:00
|
|
|
elif arg == '--graph':
|
|
|
|
|
o.graph = True
|
|
|
|
|
i += 1
|
|
|
|
|
|
|
|
|
|
elif arg.startswith('--graph='):
|
|
|
|
|
o.graph = arg[len('--graph='):]
|
|
|
|
|
i += 1
|
|
|
|
|
|
|
|
|
|
elif arg == '--graph-log-scale':
|
|
|
|
|
o.graph_log_scale = True
|
|
|
|
|
i += 1
|
|
|
|
|
|
2019-08-10 12:52:46 +08:00
|
|
|
elif arg.startswith('-'):
|
|
|
|
|
raise ValueError('unknown options : ' + arg)
|
2019-08-07 23:44:21 +08:00
|
|
|
|
2019-08-10 12:52:46 +08:00
|
|
|
else:
|
|
|
|
|
i += 1
|
|
|
|
|
try:
|
|
|
|
|
result = calculate(arg, o)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
if o.skip:
|
|
|
|
|
continue
|
|
|
|
|
else:
|
|
|
|
|
raise
|
2019-08-07 23:44:21 +08:00
|
|
|
|
2019-08-10 12:52:46 +08:00
|
|
|
else:
|
|
|
|
|
print_result(result, o)
|
2019-08-07 23:44:21 +08:00
|
|
|
|
2019-08-10 13:20:39 +08:00
|
|
|
if o.graph:
|
|
|
|
|
paint_result(result, o)
|
|
|
|
|
|
2019-08-07 19:45:09 +08:00
|
|
|
|
2019-07-17 19:00:59 +08:00
|
|
|
if __name__ == '__main__':
|
2019-08-10 12:52:46 +08:00
|
|
|
main(sys.argv[1:])
|