harp-python 0.1.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- harp/__init__.py +5 -0
- harp/io.py +89 -25
- harp/model.py +67 -60
- harp/reader.py +151 -35
- harp/schema.py +39 -19
- harp_python-0.3.0.dist-info/METADATA +102 -0
- harp_python-0.3.0.dist-info/RECORD +11 -0
- {harp_python-0.1.0.dist-info → harp_python-0.3.0.dist-info}/WHEEL +1 -1
- harp_python-0.1.0.dist-info/METADATA +0 -69
- harp_python-0.1.0.dist-info/RECORD +0 -10
- {harp_python-0.1.0.dist-info → harp_python-0.3.0.dist-info}/LICENSE +0 -0
- {harp_python-0.1.0.dist-info → harp_python-0.3.0.dist-info}/top_level.txt +0 -0
harp/__init__.py
ADDED
harp/io.py
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from enum import IntEnum
|
|
1
3
|
from os import PathLike
|
|
2
4
|
from typing import Any, BinaryIO, Optional, Union
|
|
3
|
-
|
|
5
|
+
|
|
4
6
|
import numpy as np
|
|
5
7
|
import pandas as pd
|
|
8
|
+
from pandas._typing import Axes
|
|
9
|
+
|
|
10
|
+
REFERENCE_EPOCH = datetime(1904, 1, 1)
|
|
11
|
+
"""The reference epoch for UTC harp time."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MessageType(IntEnum):
|
|
15
|
+
"""Specifies the type of a Harp message."""
|
|
16
|
+
|
|
17
|
+
NA = 0
|
|
18
|
+
READ = 1
|
|
19
|
+
WRITE = 2
|
|
20
|
+
EVENT = 3
|
|
21
|
+
|
|
6
22
|
|
|
7
23
|
_SECONDS_PER_TICK = 32e-6
|
|
8
|
-
|
|
24
|
+
_messagetypes = [type.name for type in MessageType]
|
|
25
|
+
_payloadtypes = {
|
|
9
26
|
1: np.dtype(np.uint8),
|
|
10
27
|
2: np.dtype(np.uint16),
|
|
11
28
|
4: np.dtype(np.uint32),
|
|
@@ -20,39 +37,86 @@ payloadtypes = {
|
|
|
20
37
|
|
|
21
38
|
def read(
|
|
22
39
|
file: Union[str, bytes, PathLike[Any], BinaryIO],
|
|
40
|
+
address: Optional[int] = None,
|
|
41
|
+
dtype: Optional[np.dtype] = None,
|
|
42
|
+
length: Optional[int] = None,
|
|
23
43
|
columns: Optional[Axes] = None,
|
|
44
|
+
epoch: Optional[datetime] = None,
|
|
45
|
+
keep_type: bool = False,
|
|
24
46
|
):
|
|
25
|
-
"""
|
|
26
|
-
|
|
47
|
+
"""Read single-register Harp data from the specified file.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
file
|
|
52
|
+
Open file object or filename containing binary data from
|
|
53
|
+
a single device register.
|
|
54
|
+
address
|
|
55
|
+
Expected register address. If specified, the address of
|
|
56
|
+
the first message in the file is used for validation.
|
|
57
|
+
dtype
|
|
58
|
+
Expected data type of the register payload. If specified, the
|
|
59
|
+
payload type of the first message in the file is used for validation.
|
|
60
|
+
length
|
|
61
|
+
Expected number of elements in register payload. If specified, the
|
|
62
|
+
payload length of the first message in the file is used for validation.
|
|
63
|
+
columns
|
|
64
|
+
The optional column labels to use for the data values.
|
|
65
|
+
epoch
|
|
66
|
+
Reference datetime at which time zero begins. If specified,
|
|
67
|
+
the result data frame will have a datetime index.
|
|
68
|
+
keep_type
|
|
69
|
+
Specifies whether to include a column with the message type.
|
|
27
70
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
the data values.
|
|
32
|
-
:return: A pandas data frame containing harp event data, sorted by time.
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
A pandas data frame containing message data, sorted by time.
|
|
33
74
|
"""
|
|
34
75
|
data = np.fromfile(file, dtype=np.uint8)
|
|
35
76
|
if len(data) == 0:
|
|
36
|
-
return pd.DataFrame(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
77
|
+
return pd.DataFrame(columns=columns, index=pd.Index([], dtype=np.float64, name="Time"))
|
|
78
|
+
|
|
79
|
+
if address is not None and address != data[2]:
|
|
80
|
+
raise ValueError(f"expected address {address} but got {data[2]}")
|
|
81
|
+
|
|
82
|
+
index = None
|
|
83
|
+
stride = int(data[1] + 2)
|
|
84
|
+
nrows = len(data) // stride
|
|
85
|
+
payloadtype = data[4]
|
|
86
|
+
payloadoffset = 5
|
|
87
|
+
if payloadtype & 0x10 != 0:
|
|
88
|
+
seconds = np.ndarray(nrows, dtype=np.uint32, buffer=data, offset=payloadoffset, strides=stride)
|
|
89
|
+
payloadoffset += 4
|
|
90
|
+
micros = np.ndarray(nrows, dtype=np.uint16, buffer=data, offset=payloadoffset, strides=stride)
|
|
91
|
+
payloadoffset += 2
|
|
92
|
+
time = micros * _SECONDS_PER_TICK + seconds
|
|
93
|
+
payloadtype = payloadtype & ~np.uint8(0x10)
|
|
94
|
+
if epoch is not None:
|
|
95
|
+
time = epoch + pd.to_timedelta(time, "s") # type: ignore
|
|
96
|
+
index = pd.Series(time)
|
|
97
|
+
index.name = "Time"
|
|
98
|
+
|
|
99
|
+
payloadsize = stride - payloadoffset - 1
|
|
100
|
+
payloadtype = _payloadtypes[payloadtype]
|
|
101
|
+
if dtype is not None and dtype != payloadtype:
|
|
102
|
+
raise ValueError(f"expected payload type {dtype} but got {payloadtype}")
|
|
103
|
+
|
|
44
104
|
elementsize = payloadtype.itemsize
|
|
45
|
-
payloadshape = (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
105
|
+
payloadshape = (nrows, payloadsize // elementsize)
|
|
106
|
+
if length is not None and length != payloadshape[1]:
|
|
107
|
+
raise ValueError(f"expected payload length {length} but got {payloadshape[1]}")
|
|
108
|
+
|
|
49
109
|
payload = np.ndarray(
|
|
50
110
|
payloadshape,
|
|
51
111
|
dtype=payloadtype,
|
|
52
112
|
buffer=data,
|
|
53
|
-
offset=
|
|
113
|
+
offset=payloadoffset,
|
|
54
114
|
strides=(stride, elementsize),
|
|
55
115
|
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
116
|
+
|
|
117
|
+
result = pd.DataFrame(payload, index=index, columns=columns)
|
|
118
|
+
if keep_type:
|
|
119
|
+
msgtype = np.ndarray(nrows, dtype=np.uint8, buffer=data, offset=0, strides=stride)
|
|
120
|
+
msgtype = pd.Categorical.from_codes(msgtype, categories=_messagetypes) # type: ignore
|
|
121
|
+
result[MessageType.__name__] = msgtype
|
|
122
|
+
return result
|
harp/model.py
CHANGED
|
@@ -5,36 +5,43 @@
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
from enum import Enum
|
|
8
|
-
from typing import
|
|
9
|
-
|
|
10
|
-
from pydantic import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
8
|
+
from typing import Annotated, Dict, List, Optional, Union
|
|
9
|
+
|
|
10
|
+
from pydantic import (
|
|
11
|
+
BaseModel,
|
|
12
|
+
BeforeValidator,
|
|
13
|
+
ConfigDict,
|
|
14
|
+
Field,
|
|
15
|
+
RootModel,
|
|
16
|
+
field_serializer,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PayloadType(str, Enum):
|
|
21
|
+
U8 = "uint8"
|
|
22
|
+
S8 = "int8"
|
|
23
|
+
U16 = "uint16"
|
|
24
|
+
S16 = "int16"
|
|
25
|
+
U32 = "uint32"
|
|
26
|
+
S32 = "int32"
|
|
27
|
+
U64 = "uint64"
|
|
28
|
+
S64 = "int64"
|
|
29
|
+
Float = "float32"
|
|
23
30
|
|
|
24
31
|
|
|
25
32
|
class Access(Enum):
|
|
26
|
-
Read =
|
|
27
|
-
Write =
|
|
28
|
-
Event =
|
|
33
|
+
Read = "Read"
|
|
34
|
+
Write = "Write"
|
|
35
|
+
Event = "Event"
|
|
29
36
|
|
|
30
37
|
|
|
31
38
|
class MaskValueItem(BaseModel):
|
|
32
39
|
model_config = ConfigDict(
|
|
33
|
-
extra=
|
|
40
|
+
extra="forbid",
|
|
34
41
|
)
|
|
35
|
-
value: int = Field(..., description=
|
|
42
|
+
value: int = Field(..., description="Specifies the numerical mask value.")
|
|
36
43
|
description: Optional[str] = Field(
|
|
37
|
-
None, description=
|
|
44
|
+
None, description="Specifies a summary description of the mask value function."
|
|
38
45
|
)
|
|
39
46
|
|
|
40
47
|
def __int__(self):
|
|
@@ -47,14 +54,14 @@ class MaskValue(RootModel[Union[int, MaskValueItem]]):
|
|
|
47
54
|
|
|
48
55
|
class BitMask(BaseModel):
|
|
49
56
|
description: Optional[str] = Field(
|
|
50
|
-
None, description=
|
|
57
|
+
None, description="Specifies a summary description of the bit mask function."
|
|
51
58
|
)
|
|
52
59
|
bits: Dict[str, MaskValue]
|
|
53
60
|
|
|
54
61
|
|
|
55
62
|
class GroupMask(BaseModel):
|
|
56
63
|
description: Optional[str] = Field(
|
|
57
|
-
None, description=
|
|
64
|
+
None, description="Specifies a summary description of the group mask function."
|
|
58
65
|
)
|
|
59
66
|
values: Dict[str, MaskValue]
|
|
60
67
|
|
|
@@ -62,55 +69,55 @@ class GroupMask(BaseModel):
|
|
|
62
69
|
class MaskType(RootModel[str]):
|
|
63
70
|
root: str = Field(
|
|
64
71
|
...,
|
|
65
|
-
description=
|
|
72
|
+
description="Specifies the name of the bit mask or group mask used to represent the payload value.",
|
|
66
73
|
)
|
|
67
74
|
|
|
68
75
|
|
|
69
76
|
class InterfaceType(RootModel[str]):
|
|
70
77
|
root: str = Field(
|
|
71
78
|
...,
|
|
72
|
-
description=
|
|
79
|
+
description="Specifies the name of the type used to represent the payload value in the high-level interface.",
|
|
73
80
|
)
|
|
74
81
|
|
|
75
82
|
|
|
76
83
|
class Converter(Enum):
|
|
77
|
-
None_ =
|
|
78
|
-
Payload =
|
|
79
|
-
RawPayload =
|
|
84
|
+
None_ = "None"
|
|
85
|
+
Payload = "Payload"
|
|
86
|
+
RawPayload = "RawPayload"
|
|
80
87
|
|
|
81
88
|
|
|
82
89
|
class MinValue(RootModel[float]):
|
|
83
90
|
root: float = Field(
|
|
84
91
|
...,
|
|
85
|
-
description=
|
|
92
|
+
description="Specifies the minimum allowable value for the payload or payload member.",
|
|
86
93
|
)
|
|
87
94
|
|
|
88
95
|
|
|
89
96
|
class MaxValue(RootModel[float]):
|
|
90
97
|
root: float = Field(
|
|
91
98
|
...,
|
|
92
|
-
description=
|
|
99
|
+
description="Specifies the maximum allowable value for the payload or payload member.",
|
|
93
100
|
)
|
|
94
101
|
|
|
95
102
|
|
|
96
103
|
class DefaultValue(RootModel[float]):
|
|
97
104
|
root: float = Field(
|
|
98
105
|
...,
|
|
99
|
-
description=
|
|
106
|
+
description="Specifies the default value for the payload or payload member.",
|
|
100
107
|
)
|
|
101
108
|
|
|
102
109
|
|
|
103
110
|
class PayloadMember(BaseModel):
|
|
104
111
|
mask: Optional[int] = Field(
|
|
105
112
|
None,
|
|
106
|
-
description=
|
|
113
|
+
description="Specifies the mask used to read and write this payload member.",
|
|
107
114
|
)
|
|
108
115
|
offset: Optional[int] = Field(
|
|
109
116
|
None,
|
|
110
|
-
description=
|
|
117
|
+
description="Specifies the payload array offset where this payload member is stored.",
|
|
111
118
|
)
|
|
112
119
|
description: Optional[str] = Field(
|
|
113
|
-
None, description=
|
|
120
|
+
None, description="Specifies a summary description of the payload member."
|
|
114
121
|
)
|
|
115
122
|
minValue: Optional[MinValue] = None
|
|
116
123
|
maxValue: Optional[MaxValue] = None
|
|
@@ -121,23 +128,25 @@ class PayloadMember(BaseModel):
|
|
|
121
128
|
|
|
122
129
|
|
|
123
130
|
class Visibility(Enum):
|
|
124
|
-
public =
|
|
125
|
-
private =
|
|
131
|
+
public = "public"
|
|
132
|
+
private = "private"
|
|
126
133
|
|
|
127
134
|
|
|
128
135
|
class Register(BaseModel):
|
|
129
|
-
address:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
+
address: Annotated[
|
|
137
|
+
int,
|
|
138
|
+
Field(le=255, description="Specifies the unique 8-bit address of the register."),
|
|
139
|
+
]
|
|
140
|
+
type: Annotated[PayloadType, BeforeValidator(lambda v: PayloadType[v])]
|
|
141
|
+
length: Annotated[
|
|
142
|
+
Optional[int],
|
|
143
|
+
Field(ge=1, default=1, description="Specifies the length of the register payload."),
|
|
144
|
+
]
|
|
136
145
|
access: Union[Access, List[Access]] = Field(
|
|
137
|
-
..., description=
|
|
146
|
+
..., description="Specifies the expected use of the register."
|
|
138
147
|
)
|
|
139
148
|
description: Optional[str] = Field(
|
|
140
|
-
None, description=
|
|
149
|
+
None, description="Specifies a summary description of the register function."
|
|
141
150
|
)
|
|
142
151
|
minValue: Optional[MinValue] = None
|
|
143
152
|
maxValue: Optional[MaxValue] = None
|
|
@@ -145,40 +154,38 @@ class Register(BaseModel):
|
|
|
145
154
|
maskType: Optional[MaskType] = None
|
|
146
155
|
visibility: Optional[Visibility] = Field(
|
|
147
156
|
None,
|
|
148
|
-
description=
|
|
157
|
+
description="Specifies whether the register function is exposed in the high-level interface.",
|
|
149
158
|
)
|
|
150
159
|
volatile: Optional[bool] = Field(
|
|
151
160
|
None,
|
|
152
|
-
description=
|
|
161
|
+
description="Specifies whether register values can be saved in non-volatile memory.",
|
|
153
162
|
)
|
|
154
163
|
payloadSpec: Optional[Dict[str, PayloadMember]] = None
|
|
155
164
|
interfaceType: Optional[InterfaceType] = None
|
|
156
165
|
converter: Optional[Converter] = None
|
|
157
166
|
|
|
167
|
+
@field_serializer("type")
|
|
168
|
+
def _serialize_type(self, type: PayloadType):
|
|
169
|
+
return type.name
|
|
170
|
+
|
|
158
171
|
|
|
159
172
|
class Registers(BaseModel):
|
|
160
173
|
registers: Dict[str, Register] = Field(
|
|
161
174
|
...,
|
|
162
|
-
description=
|
|
175
|
+
description="Specifies the collection of registers implementing the device function.",
|
|
163
176
|
)
|
|
164
177
|
bitMasks: Optional[Dict[str, BitMask]] = Field(
|
|
165
178
|
None,
|
|
166
|
-
description=
|
|
179
|
+
description="Specifies the collection of masks available to be used with the different registers.",
|
|
167
180
|
)
|
|
168
181
|
groupMasks: Optional[Dict[str, GroupMask]] = Field(
|
|
169
182
|
None,
|
|
170
|
-
description=
|
|
183
|
+
description="Specifies the collection of group masks available to be used with the different registers.",
|
|
171
184
|
)
|
|
172
185
|
|
|
173
186
|
|
|
174
187
|
class Model(Registers):
|
|
175
|
-
device: str = Field(..., description=
|
|
176
|
-
whoAmI: int = Field(
|
|
177
|
-
|
|
178
|
-
)
|
|
179
|
-
firmwareVersion: str = Field(
|
|
180
|
-
..., description='Specifies the semantic version of the device firmware.'
|
|
181
|
-
)
|
|
182
|
-
hardwareTargets: str = Field(
|
|
183
|
-
..., description='Specifies the semantic version of the device hardware.'
|
|
184
|
-
)
|
|
188
|
+
device: str = Field(..., description="Specifies the name of the device.")
|
|
189
|
+
whoAmI: int = Field(..., description="Specifies the unique identifier for this device type.")
|
|
190
|
+
firmwareVersion: str = Field(..., description="Specifies the semantic version of the device firmware.")
|
|
191
|
+
hardwareTargets: str = Field(..., description="Specifies the semantic version of the device hardware.")
|
harp/reader.py
CHANGED
|
@@ -1,30 +1,72 @@
|
|
|
1
|
-
import
|
|
2
|
-
from
|
|
1
|
+
import os
|
|
2
|
+
from collections import UserDict
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
3
5
|
from functools import partial
|
|
6
|
+
from math import log2
|
|
7
|
+
from os import PathLike
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, BinaryIO, Callable, Iterable, Mapping, Optional, Protocol, Union
|
|
10
|
+
|
|
11
|
+
from numpy import dtype
|
|
4
12
|
from pandas import DataFrame, Series
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
from harp.io import read
|
|
13
|
+
from pandas._typing import Axes
|
|
14
|
+
|
|
15
|
+
from harp.io import MessageType, read
|
|
16
|
+
from harp.model import BitMask, GroupMask, Model, PayloadMember, Register
|
|
17
|
+
from harp.schema import read_schema
|
|
18
|
+
|
|
8
19
|
|
|
9
|
-
|
|
20
|
+
@dataclass
|
|
21
|
+
class _ReaderParams:
|
|
22
|
+
path: Path
|
|
23
|
+
epoch: Optional[datetime] = None
|
|
24
|
+
keep_type: bool = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _ReadRegister(Protocol):
|
|
28
|
+
def __call__(
|
|
29
|
+
self,
|
|
30
|
+
file: Optional[Union[str, bytes, PathLike[Any], BinaryIO]] = None,
|
|
31
|
+
epoch: Optional[datetime] = None,
|
|
32
|
+
keep_type: bool = False,
|
|
33
|
+
) -> DataFrame: ...
|
|
10
34
|
|
|
11
35
|
|
|
12
36
|
class RegisterReader:
|
|
13
37
|
register: Register
|
|
14
|
-
read:
|
|
38
|
+
read: _ReadRegister
|
|
15
39
|
|
|
16
|
-
def __init__(
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
register: Register,
|
|
43
|
+
read: _ReadRegister,
|
|
44
|
+
) -> None:
|
|
17
45
|
self.register = register
|
|
18
46
|
self.read = read
|
|
19
47
|
|
|
20
48
|
|
|
49
|
+
class RegisterMap(UserDict[str, RegisterReader]):
|
|
50
|
+
_address_map: Mapping[int, RegisterReader]
|
|
51
|
+
|
|
52
|
+
def __init__(self, registers: Mapping[str, RegisterReader]) -> None:
|
|
53
|
+
super().__init__(registers)
|
|
54
|
+
self._address_map = {value.register.address: value for value in registers.values()}
|
|
55
|
+
|
|
56
|
+
def __getitem__(self, key: Union[str, int]) -> RegisterReader:
|
|
57
|
+
if isinstance(key, int):
|
|
58
|
+
return self._address_map[key]
|
|
59
|
+
else:
|
|
60
|
+
return super().__getitem__(key)
|
|
61
|
+
|
|
62
|
+
|
|
21
63
|
class DeviceReader:
|
|
22
64
|
device: Model
|
|
23
|
-
registers:
|
|
65
|
+
registers: RegisterMap
|
|
24
66
|
|
|
25
|
-
def __init__(self, device: Model, registers:
|
|
67
|
+
def __init__(self, device: Model, registers: Mapping[str, RegisterReader]) -> None:
|
|
26
68
|
self.device = device
|
|
27
|
-
self.registers = registers
|
|
69
|
+
self.registers = RegisterMap(registers)
|
|
28
70
|
|
|
29
71
|
def __dir__(self) -> Iterable[str]:
|
|
30
72
|
return self.registers.keys()
|
|
@@ -33,12 +75,25 @@ class DeviceReader:
|
|
|
33
75
|
return self.registers[__name]
|
|
34
76
|
|
|
35
77
|
|
|
36
|
-
def
|
|
37
|
-
|
|
38
|
-
|
|
78
|
+
def _compose_parser(
|
|
79
|
+
f: Callable[[DataFrame], DataFrame],
|
|
80
|
+
g: Callable[..., DataFrame],
|
|
81
|
+
params: _ReaderParams,
|
|
82
|
+
) -> Callable[..., DataFrame]:
|
|
83
|
+
def parser(
|
|
84
|
+
file: Optional[Union[str, bytes, PathLike[Any], BinaryIO]] = None,
|
|
85
|
+
columns: Optional[Axes] = None,
|
|
86
|
+
epoch: Optional[datetime] = params.epoch,
|
|
87
|
+
keep_type: bool = params.keep_type,
|
|
88
|
+
):
|
|
89
|
+
df = g(file, columns, epoch, keep_type)
|
|
90
|
+
result = f(df)
|
|
91
|
+
type_col = df.get(MessageType.__name__)
|
|
92
|
+
if type_col is not None:
|
|
93
|
+
result[MessageType.__name__] = type_col
|
|
94
|
+
return result
|
|
39
95
|
|
|
40
|
-
|
|
41
|
-
return _camel_to_snake_regex.sub("_", id).lower()
|
|
96
|
+
return parser
|
|
42
97
|
|
|
43
98
|
|
|
44
99
|
def _create_bit_parser(mask: int):
|
|
@@ -49,10 +104,7 @@ def _create_bit_parser(mask: int):
|
|
|
49
104
|
|
|
50
105
|
|
|
51
106
|
def _create_bitmask_parser(bitMask: BitMask):
|
|
52
|
-
lookup = [
|
|
53
|
-
(_id_camel_to_snake(k), _create_bit_parser(int(v.root)))
|
|
54
|
-
for k, v in bitMask.bits.items()
|
|
55
|
-
]
|
|
107
|
+
lookup = [(k, _create_bit_parser(int(v.root))) for k, v in bitMask.bits.items()]
|
|
56
108
|
|
|
57
109
|
def parser(df: DataFrame):
|
|
58
110
|
return DataFrame({n: f(df[0]) for n, f in lookup}, index=df.index)
|
|
@@ -83,14 +135,14 @@ def _create_payloadmember_parser(device: Model, member: PayloadMember):
|
|
|
83
135
|
if offset is None:
|
|
84
136
|
offset = 0
|
|
85
137
|
|
|
86
|
-
shift =
|
|
138
|
+
shift = 0
|
|
87
139
|
if member.mask is not None:
|
|
88
140
|
shift = _mask_shift(member.mask)
|
|
89
141
|
|
|
90
142
|
lookup = None
|
|
91
143
|
if member.maskType is not None:
|
|
92
144
|
key = member.maskType.root
|
|
93
|
-
groupMask = device.groupMasks.get(key)
|
|
145
|
+
groupMask = None if device.groupMasks is None else device.groupMasks.get(key)
|
|
94
146
|
if groupMask is not None:
|
|
95
147
|
lookup = _create_groupmask_lookup(groupMask)
|
|
96
148
|
|
|
@@ -103,7 +155,7 @@ def _create_payloadmember_parser(device: Model, member: PayloadMember):
|
|
|
103
155
|
if member.mask is not None:
|
|
104
156
|
series = series & member.mask
|
|
105
157
|
if shift > 0:
|
|
106
|
-
series = Series(series.values >> shift, series.index)
|
|
158
|
+
series = Series(series.values >> shift, series.index) # type: ignore
|
|
107
159
|
if is_boolean:
|
|
108
160
|
series = series != 0
|
|
109
161
|
elif lookup is not None:
|
|
@@ -113,43 +165,107 @@ def _create_payloadmember_parser(device: Model, member: PayloadMember):
|
|
|
113
165
|
return parser
|
|
114
166
|
|
|
115
167
|
|
|
116
|
-
def _create_register_reader(
|
|
168
|
+
def _create_register_reader(register: Register, params: _ReaderParams):
|
|
169
|
+
def reader(
|
|
170
|
+
file: Optional[Union[str, bytes, PathLike[Any], BinaryIO]] = None,
|
|
171
|
+
columns: Optional[Axes] = None,
|
|
172
|
+
epoch: Optional[datetime] = params.epoch,
|
|
173
|
+
keep_type: bool = params.keep_type,
|
|
174
|
+
):
|
|
175
|
+
if file is None:
|
|
176
|
+
file = f"{params.path}_{register.address}.bin"
|
|
177
|
+
|
|
178
|
+
data = read(
|
|
179
|
+
file,
|
|
180
|
+
address=register.address,
|
|
181
|
+
dtype=dtype(register.type),
|
|
182
|
+
length=register.length,
|
|
183
|
+
columns=columns,
|
|
184
|
+
epoch=epoch,
|
|
185
|
+
keep_type=keep_type,
|
|
186
|
+
)
|
|
187
|
+
return data
|
|
188
|
+
|
|
189
|
+
return reader
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _create_register_parser(device: Model, name: str, params: _ReaderParams):
|
|
117
193
|
register = device.registers[name]
|
|
118
|
-
reader =
|
|
194
|
+
reader = _create_register_reader(register, params)
|
|
119
195
|
|
|
120
196
|
if register.maskType is not None:
|
|
121
197
|
key = register.maskType.root
|
|
122
|
-
bitMask = device.bitMasks.get(key)
|
|
198
|
+
bitMask = None if device.bitMasks is None else device.bitMasks.get(key)
|
|
123
199
|
if bitMask is not None:
|
|
124
200
|
parser = _create_bitmask_parser(bitMask)
|
|
125
|
-
reader =
|
|
201
|
+
reader = _compose_parser(parser, reader, params)
|
|
126
202
|
return RegisterReader(register, reader)
|
|
127
203
|
|
|
128
|
-
groupMask = device.groupMasks.get(key)
|
|
204
|
+
groupMask = None if device.groupMasks is None else device.groupMasks.get(key)
|
|
129
205
|
if groupMask is not None:
|
|
130
206
|
parser = _create_groupmask_parser(name, groupMask)
|
|
131
|
-
reader =
|
|
207
|
+
reader = _compose_parser(parser, reader, params)
|
|
132
208
|
return RegisterReader(register, reader)
|
|
133
209
|
|
|
134
210
|
if register.payloadSpec is not None:
|
|
135
211
|
payload_parsers = [
|
|
136
|
-
(
|
|
212
|
+
(key, _create_payloadmember_parser(device, member))
|
|
137
213
|
for key, member in register.payloadSpec.items()
|
|
138
214
|
]
|
|
139
215
|
|
|
140
216
|
def parser(df: DataFrame):
|
|
141
217
|
return DataFrame({n: f(df) for n, f in payload_parsers}, index=df.index)
|
|
142
218
|
|
|
143
|
-
reader =
|
|
219
|
+
reader = _compose_parser(parser, reader, params)
|
|
144
220
|
return RegisterReader(register, reader)
|
|
145
221
|
|
|
146
|
-
|
|
147
|
-
reader = partial(reader, columns=columns)
|
|
222
|
+
reader = partial(reader, columns=[name])
|
|
148
223
|
return RegisterReader(register, reader)
|
|
149
224
|
|
|
150
225
|
|
|
151
|
-
def create_reader(
|
|
226
|
+
def create_reader(
|
|
227
|
+
device: Union[str, PathLike, Model],
|
|
228
|
+
include_common_registers: bool = True,
|
|
229
|
+
epoch: Optional[datetime] = None,
|
|
230
|
+
keep_type: bool = False,
|
|
231
|
+
):
|
|
232
|
+
"""Creates a device reader object from the specified dataset or schema.
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
device
|
|
237
|
+
A path to the device schema, dataset folder, or parsed device schema object
|
|
238
|
+
describing the device.
|
|
239
|
+
include_common_registers
|
|
240
|
+
Specifies whether to include the set of Harp common registers in the
|
|
241
|
+
parsed device schema object. If a parsed device schema object is provided,
|
|
242
|
+
this parameter is ignored.
|
|
243
|
+
epoch
|
|
244
|
+
The default reference datetime at which time zero begins. If specified,
|
|
245
|
+
the data frames returned by each register reader will have a datetime index.
|
|
246
|
+
keep_type
|
|
247
|
+
Specifies whether to include a column with the message type by default.
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
A device reader object which can be used to read binary data for each
|
|
252
|
+
register or to access metadata about each register. Individual registers
|
|
253
|
+
can be accessed using dot notation using the name of the register as the
|
|
254
|
+
key.
|
|
255
|
+
"""
|
|
256
|
+
if isinstance(device, Model):
|
|
257
|
+
base_path = Path(device.device)
|
|
258
|
+
else:
|
|
259
|
+
path = Path(device).absolute().resolve()
|
|
260
|
+
is_dir = os.path.isdir(path)
|
|
261
|
+
if is_dir:
|
|
262
|
+
device = path / "device.yml"
|
|
263
|
+
|
|
264
|
+
device = read_schema(device, include_common_registers)
|
|
265
|
+
base_path = path / device.device if is_dir else path.parent / device.device
|
|
266
|
+
|
|
152
267
|
reg_readers = {
|
|
153
|
-
name:
|
|
268
|
+
name: _create_register_parser(device, name, _ReaderParams(base_path, epoch, keep_type))
|
|
269
|
+
for name in device.registers.keys()
|
|
154
270
|
}
|
|
155
271
|
return DeviceReader(device, reg_readers)
|
harp/schema.py
CHANGED
|
@@ -1,31 +1,51 @@
|
|
|
1
|
+
from importlib import resources
|
|
1
2
|
from os import PathLike
|
|
2
|
-
from pathlib import Path
|
|
3
3
|
from typing import TextIO, Union
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
from pydantic_yaml import parse_yaml_raw_as
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
from harp.model import Model, Registers
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def _read_common_registers(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
def _read_common_registers() -> Registers:
|
|
11
|
+
file = resources.files(__package__) / "common.yml"
|
|
12
|
+
with file.open("r") as fileIO:
|
|
13
|
+
return parse_yaml_raw_as(Registers, fileIO.read())
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_schema(file: Union[str, PathLike, TextIO], include_common_registers: bool = True) -> Model:
|
|
17
|
+
"""Read and parse a device schema from the specified file.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
file
|
|
22
|
+
Open file object or filename containing a YAML text stream describing
|
|
23
|
+
a device schema.
|
|
24
|
+
include_common_registers
|
|
25
|
+
Specifies whether to include the set of Harp common registers in the
|
|
26
|
+
returned device schema object.
|
|
16
27
|
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
A Pydantic model object representing the Harp device schema.
|
|
31
|
+
"""
|
|
17
32
|
|
|
18
|
-
|
|
19
|
-
file: Union[str, PathLike, TextIO], include_common_registers: bool = True
|
|
20
|
-
) -> Model:
|
|
21
|
-
try:
|
|
33
|
+
if isinstance(file, (str, PathLike)):
|
|
22
34
|
with open(file) as fileIO:
|
|
23
|
-
return read_schema(fileIO
|
|
24
|
-
|
|
35
|
+
return read_schema(fileIO)
|
|
36
|
+
else:
|
|
25
37
|
schema = parse_yaml_raw_as(Model, file.read())
|
|
26
|
-
if
|
|
27
|
-
common = _read_common_registers(
|
|
38
|
+
if "WhoAmI" not in schema.registers and include_common_registers:
|
|
39
|
+
common = _read_common_registers()
|
|
28
40
|
schema.registers = dict(common.registers, **schema.registers)
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
if common.bitMasks:
|
|
42
|
+
schema.bitMasks = (
|
|
43
|
+
common.bitMasks if schema.bitMasks is None else dict(common.bitMasks, **schema.bitMasks)
|
|
44
|
+
)
|
|
45
|
+
if common.groupMasks:
|
|
46
|
+
schema.groupMasks = (
|
|
47
|
+
common.groupMasks
|
|
48
|
+
if schema.groupMasks is None
|
|
49
|
+
else dict(common.groupMasks, **schema.groupMasks)
|
|
50
|
+
)
|
|
31
51
|
return schema
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: harp-python
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A low-level interface for loading binary Harp protocol data
|
|
5
|
+
Author-email: Goncalo Lopes <g.lopes@neurogears.org>
|
|
6
|
+
License: MIT License
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/harp-tech/harp-python/issues
|
|
8
|
+
Project-URL: Source Code, https://github.com/harp-tech/harp-python
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Programming Language :: Python
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Requires-Python: >=3.9.0
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: pydantic-yaml
|
|
20
|
+
Requires-Dist: pandas
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: datamodel-code-generator; extra == "dev"
|
|
23
|
+
Requires-Dist: pandas-stubs; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
26
|
+
Requires-Dist: pyright; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff; extra == "dev"
|
|
28
|
+
Requires-Dist: codespell; extra == "dev"
|
|
29
|
+
Provides-Extra: jupyter
|
|
30
|
+
Requires-Dist: ipykernel; extra == "jupyter"
|
|
31
|
+
Requires-Dist: matplotlib; extra == "jupyter"
|
|
32
|
+
|
|
33
|
+
# harp-python
|
|
34
|
+
|
|
35
|
+
A low-level interface to data collected with the [Harp binary protocol](https://harp-tech.org/protocol/BinaryProtocol-8bit.html).
|
|
36
|
+
|
|
37
|
+
## How to install
|
|
38
|
+
|
|
39
|
+
The source code is currently hosted on [GitHub](https://github.com/harp-tech/harp-python).
|
|
40
|
+
|
|
41
|
+
Binary installers for the latest released version are available at the [Python Package Index (PyPI)](https://pypi.org/project/harp-python) and can be installed using `pip`.
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
pip install harp-python
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The list of changes between each release can be found [here](https://github.com/harp-tech/harp-python/releases).
|
|
48
|
+
|
|
49
|
+
## Data model
|
|
50
|
+
|
|
51
|
+
The interface makes use of a Pydantic data model generated from Harp device schema definitions. The schema data classes are used to automatically generate binary readers for each device.
|
|
52
|
+
|
|
53
|
+
All binary data files from a single device need to be stored in the same folder alongside the device meta-schema, named `device.yml`. Each register file should have the following naming convention `<deviceName>_<registerAddress>.bin`.
|
|
54
|
+
|
|
55
|
+
For example, for a dataset collected with a `Behavior` device, you might have:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
📦device.harp
|
|
59
|
+
┣ 📜Behavior_0.bin
|
|
60
|
+
┣ 📜Behavior_1.bin
|
|
61
|
+
...
|
|
62
|
+
┗ 📜device.yml
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## How to use
|
|
66
|
+
|
|
67
|
+
### Create device reader object from schema
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import harp
|
|
71
|
+
reader = harp.create_reader("device.harp")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Read data from named register
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
reader.OperationControl.read()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Access register metadata
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
reader.OperationControl.register.address
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Create device reader object with UTC datetime format
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
reader = harp.create_reader("device.harp", epoch=harp.REFERENCE_EPOCH)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Read data with message type information
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
reader.OperationControl.read(keep_type=True)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Read data from a specific file
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
reader.OperationControl.read("data/Behavior_10.bin")
|
|
102
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
harp/__init__.py,sha256=P2PHGnd9Oz3LwrscBHKXqdYZimF7k57HcAaIaksRKc0,220
|
|
2
|
+
harp/common.yml,sha256=qcejMFu00JjI5rkubuEDdrUu_uFkN17f_Ljb6yBd-RM,4452
|
|
3
|
+
harp/io.py,sha256=an802KRCU-eSBXKWB7nXsomUBWcfAwEOZs9U188_MUM,4179
|
|
4
|
+
harp/model.py,sha256=oJim5Gc42H4MJwpLwd2Y7KHsoeI_Yw3CO0o6qn7pJbQ,5805
|
|
5
|
+
harp/reader.py,sha256=u-Taf6gmkQ5_PcsDRNskPBhZ7UsCcYL0WPlH0sEmjk0,8588
|
|
6
|
+
harp/schema.py,sha256=bUr4WS-en03mzisIc9eAYiG7kReMmHdEvVwrhEpTboo,1816
|
|
7
|
+
harp_python-0.3.0.dist-info/LICENSE,sha256=KxnuV3gNCp4WmLUC8DYWJvCKPdxMpwmWIzAYBYdqPrM,1104
|
|
8
|
+
harp_python-0.3.0.dist-info/METADATA,sha256=lWiDbZSxbm8NmtXXopNpmmUN_h0wN0nIYc_LOrXr0s4,3190
|
|
9
|
+
harp_python-0.3.0.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
|
|
10
|
+
harp_python-0.3.0.dist-info/top_level.txt,sha256=ztK1iHjgwmfXqxHCpu-e2uHIwdmodywYEUaWSODMvyM,5
|
|
11
|
+
harp_python-0.3.0.dist-info/RECORD,,
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: harp-python
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: A low-level interface for loading binary Harp protocol data
|
|
5
|
-
Author-email: Goncalo Lopes <g.lopes@neurogears.org>
|
|
6
|
-
License: MIT License
|
|
7
|
-
Project-URL: Bug Tracker, https://github.com/harp-tech/harp-python/issues
|
|
8
|
-
Project-URL: Source Code, https://github.com/harp-tech/harp-python
|
|
9
|
-
Classifier: Development Status :: 4 - Beta
|
|
10
|
-
Classifier: Programming Language :: Python
|
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
-
Classifier: Operating System :: OS Independent
|
|
15
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
-
Requires-Python: >=3.9.0
|
|
17
|
-
Description-Content-Type: text/markdown
|
|
18
|
-
License-File: LICENSE
|
|
19
|
-
Requires-Dist: pydantic-yaml
|
|
20
|
-
Requires-Dist: pandas
|
|
21
|
-
Provides-Extra: dev
|
|
22
|
-
Requires-Dist: datamodel-code-generator ; extra == 'dev'
|
|
23
|
-
Requires-Dist: black ; extra == 'dev'
|
|
24
|
-
Provides-Extra: jupyter
|
|
25
|
-
Requires-Dist: ipykernel ; extra == 'jupyter'
|
|
26
|
-
Requires-Dist: matplotlib ; extra == 'jupyter'
|
|
27
|
-
|
|
28
|
-
# harp-python
|
|
29
|
-
|
|
30
|
-
A low-level interface to data collected with the [Harp](https://harp-tech.org/) binary protocol.
|
|
31
|
-
|
|
32
|
-
## Data model
|
|
33
|
-
|
|
34
|
-
To regenerate Pydantic data models from device schema definitions, activate a virtual environment with `dev` dependencies, and run:
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
datamodel-codegen --input ./reflex-generator/schema/device.json --output harp/model.py --output-model-type pydantic_v2.BaseModel
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
> [!IMPORTANT]
|
|
41
|
-
> Currently code generation adds an unwanted field at the very end of the data model definition `registers: Optional[Any] = None`. This declaration needs to be removed for serialization to work properly.
|
|
42
|
-
|
|
43
|
-
## How to use
|
|
44
|
-
|
|
45
|
-
### Read Harp device schema from YML file
|
|
46
|
-
|
|
47
|
-
```python
|
|
48
|
-
from harp.schema import read_schema
|
|
49
|
-
schema = read_schema('device.yml')
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### Create device reader object from schema
|
|
53
|
-
|
|
54
|
-
```python
|
|
55
|
-
from harp.reader import create_reader
|
|
56
|
-
reader = create_reader(schema)
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### Read data from named register
|
|
60
|
-
|
|
61
|
-
```python
|
|
62
|
-
reader.OperationControl.read("data/Behavior_10.bin")
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### Access register metadata
|
|
66
|
-
|
|
67
|
-
```python
|
|
68
|
-
reader.OperationControl.register.address
|
|
69
|
-
```
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
harp/common.yml,sha256=qcejMFu00JjI5rkubuEDdrUu_uFkN17f_Ljb6yBd-RM,4452
|
|
2
|
-
harp/io.py,sha256=qqdkoZe4Tqg6SEF8ZQmgN0iOGDbdBkgsXF6HaoUhfmQ,1907
|
|
3
|
-
harp/model.py,sha256=20joms_-BLswhwPE4-AFpnGY8mh_qG6vvM8BdzOPhaU,5545
|
|
4
|
-
harp/reader.py,sha256=8jAKcphayOWINvJF-uXcqC3QH7s4g_VjlKfNd_SrG2E,4515
|
|
5
|
-
harp/schema.py,sha256=Xz5dA4dr13kr1vayB6debA5T2rNbFPfG7ssS3RPVFok,1202
|
|
6
|
-
harp_python-0.1.0.dist-info/LICENSE,sha256=KxnuV3gNCp4WmLUC8DYWJvCKPdxMpwmWIzAYBYdqPrM,1104
|
|
7
|
-
harp_python-0.1.0.dist-info/METADATA,sha256=K_U2ZcsXcxaInc_0JO7gXwQiKnaGpVuca-ct5HwIyj0,2193
|
|
8
|
-
harp_python-0.1.0.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
|
|
9
|
-
harp_python-0.1.0.dist-info/top_level.txt,sha256=ztK1iHjgwmfXqxHCpu-e2uHIwdmodywYEUaWSODMvyM,5
|
|
10
|
-
harp_python-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|