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 ADDED
@@ -0,0 +1,5 @@
1
+ from harp.io import REFERENCE_EPOCH, MessageType, read
2
+ from harp.reader import create_reader
3
+ from harp.schema import read_schema
4
+
5
+ __all__ = ["REFERENCE_EPOCH", "MessageType", "read", "create_reader", "read_schema"]
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
- from pandas._typing import Axes
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
- payloadtypes = {
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
- Read single-register Harp data from the specified file.
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
- :param str file or str or Path: Open file object or filename containing
29
- binary data from a single device register.
30
- :param str or array-like names: The optional column labels to use for
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
- columns=columns, index=pd.Index([], dtype=np.float64, name="time")
38
- )
39
-
40
- stride = data[1] + 2
41
- length = len(data) // stride
42
- payloadsize = stride - 12
43
- payloadtype = payloadtypes[data[4] & ~0x10]
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 = (length, payloadsize // elementsize)
46
- seconds = np.ndarray(length, dtype=np.uint32, buffer=data, offset=5, strides=stride)
47
- micros = np.ndarray(length, dtype=np.uint16, buffer=data, offset=9, strides=stride)
48
- seconds = micros * _SECONDS_PER_TICK + seconds
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=11,
113
+ offset=payloadoffset,
54
114
  strides=(stride, elementsize),
55
115
  )
56
- time = pd.Series(seconds)
57
- time.name = "time"
58
- return pd.DataFrame(payload, index=time, columns=columns)
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 Any, Dict, List, Optional, Union
9
-
10
- from pydantic import BaseModel, ConfigDict, Field, RootModel, conint
11
-
12
-
13
- class Type(Enum):
14
- U8 = 'U8'
15
- S8 = 'S8'
16
- U16 = 'U16'
17
- S16 = 'S16'
18
- U32 = 'U32'
19
- S32 = 'S32'
20
- U64 = 'U64'
21
- S64 = 'S64'
22
- Float = 'Float'
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 = 'Read'
27
- Write = 'Write'
28
- Event = '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='forbid',
40
+ extra="forbid",
34
41
  )
35
- value: int = Field(..., description='Specifies the numerical mask value.')
42
+ value: int = Field(..., description="Specifies the numerical mask value.")
36
43
  description: Optional[str] = Field(
37
- None, description='Specifies a summary description of the mask value function.'
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='Specifies a summary description of the bit mask function.'
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='Specifies a summary description of the group mask function.'
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='Specifies the name of the bit mask or group mask used to represent the payload value.',
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='Specifies the name of the type used to represent the payload value in the high-level interface.',
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_ = 'None'
78
- Payload = 'Payload'
79
- RawPayload = '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='Specifies the minimum allowable value for the payload or payload member.',
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='Specifies the maximum allowable value for the payload or payload member.',
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='Specifies the default value for the payload or payload member.',
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='Specifies the mask used to read and write this payload member.',
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='Specifies the payload array offset where this payload member is stored.',
117
+ description="Specifies the payload array offset where this payload member is stored.",
111
118
  )
112
119
  description: Optional[str] = Field(
113
- None, description='Specifies a summary description of the payload member.'
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 = 'public'
125
- private = 'private'
131
+ public = "public"
132
+ private = "private"
126
133
 
127
134
 
128
135
  class Register(BaseModel):
129
- address: conint(le=255) = Field(
130
- ..., description='Specifies the unique 8-bit address of the register.'
131
- )
132
- type: Type
133
- length: Optional[conint(ge=1)] = Field(
134
- None, description='Specifies the length of the register payload.'
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='Specifies the expected use of the register.'
146
+ ..., description="Specifies the expected use of the register."
138
147
  )
139
148
  description: Optional[str] = Field(
140
- None, description='Specifies a summary description of the register function.'
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='Specifies whether the register function is exposed in the high-level interface.',
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='Specifies whether register values can be saved in non-volatile memory.',
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='Specifies the collection of registers implementing the device function.',
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='Specifies the collection of masks available to be used with the different registers.',
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='Specifies the collection of group masks available to be used with the different registers.',
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='Specifies the name of the device.')
176
- whoAmI: int = Field(
177
- ..., description='Specifies the unique identifier for this device type.'
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 re
2
- from math import log2
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 typing import Iterable, Callable, Union
6
- from harp.model import BitMask, GroupMask, MaskValueItem, Model, PayloadMember, Register
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
- _camel_to_snake_regex = re.compile(r"(?<!^)(?=[A-Z])")
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: Callable[[str], DataFrame]
38
+ read: _ReadRegister
15
39
 
16
- def __init__(self, register: Register, read: Callable[[str], DataFrame]) -> None:
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: dict[str, RegisterReader]
65
+ registers: RegisterMap
24
66
 
25
- def __init__(self, device: Model, registers: dict[str, RegisterReader]) -> None:
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 _compose(f, g):
37
- return lambda *a, **kw: f(g(*a, **kw))
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
- def _id_camel_to_snake(id: str):
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 = None
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(device: Model, name: str):
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 = read
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 = _compose(parser, 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 = _compose(parser, 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
- (_id_camel_to_snake(key), _create_payloadmember_parser(device, member))
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 = _compose(parser, reader)
219
+ reader = _compose_parser(parser, reader, params)
144
220
  return RegisterReader(register, reader)
145
221
 
146
- columns = [_id_camel_to_snake(name)]
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(device: Model):
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: _create_register_reader(device, name) for name in device.registers.keys()
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
- from harp.model import Model, Registers
4
+
5
5
  from pydantic_yaml import parse_yaml_raw_as
6
6
 
7
- _common_yaml_path = Path(__file__).absolute().parent.joinpath("common.yml")
7
+ from harp.model import Model, Registers
8
8
 
9
9
 
10
- def _read_common_registers(file: Union[str, PathLike, TextIO]) -> Registers:
11
- try:
12
- with open(file) as fileIO:
13
- return _read_common_registers(fileIO)
14
- except TypeError:
15
- return parse_yaml_raw_as(Registers, file.read())
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
- def read_schema(
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, include_common_registers)
24
- except TypeError:
35
+ return read_schema(fileIO)
36
+ else:
25
37
  schema = parse_yaml_raw_as(Model, file.read())
26
- if not "WhoAmI" in schema.registers and include_common_registers:
27
- common = _read_common_registers(_common_yaml_path)
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
- schema.bitMasks = dict(common.bitMasks, **schema.bitMasks)
30
- schema.groupMasks = dict(common.groupMasks, **schema.groupMasks)
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.3)
2
+ Generator: setuptools (75.5.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,