ttl-barcoder 0.4.1__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.
- ttl_barcoder/__init__.py +34 -0
- ttl_barcoder/core/__init__.py +29 -0
- ttl_barcoder/core/barcode_ttl.py +104 -0
- ttl_barcoder/core/config.py +138 -0
- ttl_barcoder/core/decoder.py +76 -0
- ttl_barcoder/core/encoder.py +59 -0
- ttl_barcoder/core/generator.py +124 -0
- ttl_barcoder/hardware/__init__.py +0 -0
- ttl_barcoder/hardware/bpod/__init__.py +3 -0
- ttl_barcoder/hardware/bpod/sender.py +50 -0
- ttl_barcoder/hardware/pigpio/__init__.py +3 -0
- ttl_barcoder/hardware/pigpio/sender.py +112 -0
- ttl_barcoder/py.typed +0 -0
- ttl_barcoder-0.4.1.dist-info/METADATA +167 -0
- ttl_barcoder-0.4.1.dist-info/RECORD +17 -0
- ttl_barcoder-0.4.1.dist-info/WHEEL +4 -0
- ttl_barcoder-0.4.1.dist-info/licenses/LICENSE +29 -0
ttl_barcoder/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
from ttl_barcoder.core import (
|
|
4
|
+
BarcodeConfig,
|
|
5
|
+
BarcodeDecoder,
|
|
6
|
+
BarcodeTTL,
|
|
7
|
+
RandomGenerator,
|
|
8
|
+
TimestampGenerator,
|
|
9
|
+
TimestampPrecision,
|
|
10
|
+
TimingEncoder,
|
|
11
|
+
TTLGenerator,
|
|
12
|
+
TTLType,
|
|
13
|
+
create_generator,
|
|
14
|
+
get_preset,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
__version__ = version("ttl-barcoder")
|
|
19
|
+
except PackageNotFoundError:
|
|
20
|
+
__version__ = "unknown"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"BarcodeConfig",
|
|
24
|
+
"BarcodeDecoder",
|
|
25
|
+
"BarcodeTTL",
|
|
26
|
+
"RandomGenerator",
|
|
27
|
+
"TimestampGenerator",
|
|
28
|
+
"TimestampPrecision",
|
|
29
|
+
"TimingEncoder",
|
|
30
|
+
"TTLGenerator",
|
|
31
|
+
"TTLType",
|
|
32
|
+
"create_generator",
|
|
33
|
+
"get_preset",
|
|
34
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from ttl_barcoder.core.barcode_ttl import BarcodeTTL
|
|
2
|
+
from ttl_barcoder.core.config import (
|
|
3
|
+
BarcodeConfig,
|
|
4
|
+
TimestampPrecision,
|
|
5
|
+
TTLType,
|
|
6
|
+
get_preset,
|
|
7
|
+
)
|
|
8
|
+
from ttl_barcoder.core.decoder import BarcodeDecoder
|
|
9
|
+
from ttl_barcoder.core.encoder import TimingEncoder
|
|
10
|
+
from ttl_barcoder.core.generator import (
|
|
11
|
+
RandomGenerator,
|
|
12
|
+
TimestampGenerator,
|
|
13
|
+
TTLGenerator,
|
|
14
|
+
create_generator,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"BarcodeConfig",
|
|
19
|
+
"TimestampPrecision",
|
|
20
|
+
"TTLType",
|
|
21
|
+
"get_preset",
|
|
22
|
+
"TTLGenerator",
|
|
23
|
+
"TimestampGenerator",
|
|
24
|
+
"RandomGenerator",
|
|
25
|
+
"create_generator",
|
|
26
|
+
"TimingEncoder",
|
|
27
|
+
"BarcodeDecoder",
|
|
28
|
+
"BarcodeTTL",
|
|
29
|
+
]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ttl_barcoder.core.config import BarcodeConfig, TTLType
|
|
4
|
+
from ttl_barcoder.core.decoder import BarcodeDecoder
|
|
5
|
+
from ttl_barcoder.core.encoder import TimingEncoder
|
|
6
|
+
from ttl_barcoder.core.generator import (
|
|
7
|
+
TimestampGenerator,
|
|
8
|
+
TTLGenerator,
|
|
9
|
+
create_generator,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BarcodeTTL:
|
|
14
|
+
"""Main interface for TTL barcode generation and decoding."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, config: BarcodeConfig | None = None) -> None:
|
|
17
|
+
self.config = config or BarcodeConfig.default()
|
|
18
|
+
self.generator: TTLGenerator = create_generator(self.config)
|
|
19
|
+
self.encoder = TimingEncoder(
|
|
20
|
+
bit_duration_ms=self.config.bit_duration_ms,
|
|
21
|
+
init_duration_ms=self.config.init_duration_ms,
|
|
22
|
+
)
|
|
23
|
+
self.decoder = BarcodeDecoder(
|
|
24
|
+
barcode_bits=self.config.barcode_bits,
|
|
25
|
+
bit_duration_ms=self.config.bit_duration_ms,
|
|
26
|
+
init_duration_ms=self.config.init_duration_ms,
|
|
27
|
+
tolerance=self.config.tolerance,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def prepare(self) -> tuple[int, float, list[tuple[bool, float]]]:
|
|
31
|
+
"""Capture wall time and return (barcode_value, wall_time, timing_sequence).
|
|
32
|
+
|
|
33
|
+
Wall time is captured before generate() so it matches the encoded timestamp.
|
|
34
|
+
"""
|
|
35
|
+
import time
|
|
36
|
+
|
|
37
|
+
wall_time = time.time()
|
|
38
|
+
barcode_value = self.generator.generate(timestamp=wall_time)
|
|
39
|
+
timing_sequence = self.get_sequence(barcode=barcode_value)
|
|
40
|
+
return barcode_value, wall_time, timing_sequence
|
|
41
|
+
|
|
42
|
+
def get_sequence(self, barcode: int | None = None) -> list[tuple[bool, float]]:
|
|
43
|
+
"""Return (level, duration_ms) timing sequence for hardware transmission."""
|
|
44
|
+
if barcode is None:
|
|
45
|
+
barcode = self.generator.generate()
|
|
46
|
+
bits = self.generator.encode_bits(barcode)
|
|
47
|
+
return self.encoder.encode_level_durations(bits)
|
|
48
|
+
|
|
49
|
+
def get_sequence_from_timestamp(self, timestamp: float) -> list[tuple[bool, float]]:
|
|
50
|
+
"""Get timing sequence from a specific Unix timestamp (timestamp TTL only)."""
|
|
51
|
+
if self.config.ttl_type != TTLType.timestamp:
|
|
52
|
+
raise ValueError("get_sequence_from_timestamp requires TTLType.timestamp")
|
|
53
|
+
assert isinstance(self.generator, TimestampGenerator)
|
|
54
|
+
barcode = self.generator.generate(timestamp=timestamp)
|
|
55
|
+
return self.get_sequence(barcode=barcode)
|
|
56
|
+
|
|
57
|
+
def get_multiple_sequences(
|
|
58
|
+
self,
|
|
59
|
+
count: int = 1,
|
|
60
|
+
interval_s: float = 5.0,
|
|
61
|
+
start_timestamp: float | None = None,
|
|
62
|
+
) -> list[list[tuple[bool, float]]]:
|
|
63
|
+
if self.config.ttl_type == TTLType.timestamp:
|
|
64
|
+
assert isinstance(self.generator, TimestampGenerator)
|
|
65
|
+
barcodes = self.generator.generate_sequence(
|
|
66
|
+
count=count, interval_s=interval_s, start_timestamp=start_timestamp
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
barcodes = [self.generator.generate() for _ in range(count)]
|
|
70
|
+
return [self.get_sequence(b) for b in barcodes]
|
|
71
|
+
|
|
72
|
+
def decode_edges(
|
|
73
|
+
self, edge_timestamps: list[float], edge_levels: list[bool]
|
|
74
|
+
) -> tuple[float, int] | None:
|
|
75
|
+
"""Decode edge timestamps to (timestamp, barcode_value) or None."""
|
|
76
|
+
return self.decoder.decode_edges(
|
|
77
|
+
edge_timestamps=edge_timestamps, edge_levels=edge_levels
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def recover_timestamp(
|
|
81
|
+
self, barcode_value: int, reference_time: float | None = None
|
|
82
|
+
) -> float:
|
|
83
|
+
"""Recover original timestamp from barcode value with wraparound handling."""
|
|
84
|
+
if self.config.ttl_type != TTLType.timestamp:
|
|
85
|
+
raise ValueError("recover_timestamp requires TTLType.timestamp")
|
|
86
|
+
assert isinstance(self.generator, TimestampGenerator)
|
|
87
|
+
return self.generator.recover_timestamp(
|
|
88
|
+
barcode_value=barcode_value, reference_time=reference_time
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def default_config(cls) -> BarcodeConfig:
|
|
93
|
+
return BarcodeConfig.default()
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def info(self) -> dict:
|
|
97
|
+
return {
|
|
98
|
+
"config": self.config.info(),
|
|
99
|
+
"generator": self.generator.info,
|
|
100
|
+
"encoder": self.encoder.info,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
def __str__(self) -> str:
|
|
104
|
+
return f"BarcodeTTL({self.config})"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TTLType(str, Enum):
|
|
10
|
+
"""Type of barcode value to generate."""
|
|
11
|
+
|
|
12
|
+
timestamp = "timestamp"
|
|
13
|
+
random = "random"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TimestampPrecision(str, Enum):
|
|
17
|
+
"""Timestamp quantization precision."""
|
|
18
|
+
|
|
19
|
+
seconds = "s"
|
|
20
|
+
milliseconds = "ms"
|
|
21
|
+
microseconds = "us"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Time units per second for each precision level
|
|
25
|
+
PRECISION_UNITS_PER_SECOND: dict[TimestampPrecision, float] = {
|
|
26
|
+
TimestampPrecision.seconds: 1.0,
|
|
27
|
+
TimestampPrecision.milliseconds: 1_000.0,
|
|
28
|
+
TimestampPrecision.microseconds: 1_000_000.0,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BarcodeConfig(BaseModel):
|
|
33
|
+
"""Barcode configuration with Pydantic v2 validation."""
|
|
34
|
+
|
|
35
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
|
36
|
+
|
|
37
|
+
ttl_type: TTLType = TTLType.timestamp
|
|
38
|
+
barcode_bits: int = Field(
|
|
39
|
+
default=37, ge=16, le=64, description="Number of bits in barcode (16-64)"
|
|
40
|
+
)
|
|
41
|
+
timestamp_precision: TimestampPrecision = Field(
|
|
42
|
+
default=TimestampPrecision.milliseconds,
|
|
43
|
+
description="Timestamp quantization precision (s/ms/us)",
|
|
44
|
+
)
|
|
45
|
+
bit_duration_ms: float = Field(
|
|
46
|
+
default=35.0, gt=0, le=1000, description="Duration of each bit pulse in ms"
|
|
47
|
+
)
|
|
48
|
+
init_duration_ms: float = Field(
|
|
49
|
+
default=10.0, gt=0, le=100, description="Duration of init pulses in ms"
|
|
50
|
+
)
|
|
51
|
+
tolerance: float = Field(
|
|
52
|
+
default=0.25, ge=0.05, le=0.5, description="Timing tolerance (0.05-0.5)"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def default(cls) -> BarcodeConfig:
|
|
57
|
+
"""Create default configuration."""
|
|
58
|
+
return cls()
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_dict(cls, config_dict: dict[str, Any]) -> BarcodeConfig:
|
|
62
|
+
"""Create configuration from dictionary."""
|
|
63
|
+
return cls(**config_dict)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def coverage_years(self) -> float | None:
|
|
67
|
+
"""Coverage in years (timestamp TTL only)."""
|
|
68
|
+
if self.ttl_type != TTLType.timestamp:
|
|
69
|
+
return None
|
|
70
|
+
units_per_second = PRECISION_UNITS_PER_SECOND[self.timestamp_precision]
|
|
71
|
+
coverage_seconds = (2**self.barcode_bits) / units_per_second
|
|
72
|
+
return coverage_seconds / (365.25 * 24 * 3600)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def total_duration_ms(self) -> float:
|
|
76
|
+
"""Total barcode duration in milliseconds."""
|
|
77
|
+
return 6 * self.init_duration_ms + self.barcode_bits * self.bit_duration_ms
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def safety_ratio(self) -> float:
|
|
81
|
+
"""Safety ratio (bit_duration vs tolerance window)."""
|
|
82
|
+
return self.bit_duration_ms / (2 * self.bit_duration_ms * self.tolerance)
|
|
83
|
+
|
|
84
|
+
def info(self) -> dict[str, Any]:
|
|
85
|
+
"""Configuration summary as dictionary."""
|
|
86
|
+
data = self.model_dump()
|
|
87
|
+
data["coverage_years"] = self.coverage_years
|
|
88
|
+
data["total_duration_ms"] = self.total_duration_ms
|
|
89
|
+
data["safety_ratio"] = self.safety_ratio
|
|
90
|
+
return data
|
|
91
|
+
|
|
92
|
+
def __str__(self) -> str:
|
|
93
|
+
parts = [f"{self.ttl_type.value}", f"{self.barcode_bits}-bit"]
|
|
94
|
+
if self.ttl_type == TTLType.timestamp:
|
|
95
|
+
parts.append(f"{self.timestamp_precision.value} precision")
|
|
96
|
+
if self.coverage_years is not None:
|
|
97
|
+
parts.append(f"{self.coverage_years:.1f}yr coverage")
|
|
98
|
+
parts += [
|
|
99
|
+
f"{self.bit_duration_ms}ms bits",
|
|
100
|
+
f"{self.total_duration_ms:.0f}ms total",
|
|
101
|
+
]
|
|
102
|
+
return f"BarcodeConfig({', '.join(parts)})"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# Preset configurations
|
|
106
|
+
PRESETS: dict[str, BarcodeConfig] = {
|
|
107
|
+
"default": BarcodeConfig(),
|
|
108
|
+
"high_speed": BarcodeConfig(
|
|
109
|
+
barcode_bits=32,
|
|
110
|
+
timestamp_precision=TimestampPrecision.milliseconds,
|
|
111
|
+
bit_duration_ms=25.0,
|
|
112
|
+
init_duration_ms=8.0,
|
|
113
|
+
),
|
|
114
|
+
"high_precision": BarcodeConfig(
|
|
115
|
+
barcode_bits=42,
|
|
116
|
+
timestamp_precision=TimestampPrecision.microseconds,
|
|
117
|
+
bit_duration_ms=50.0,
|
|
118
|
+
init_duration_ms=15.0,
|
|
119
|
+
),
|
|
120
|
+
"conservative": BarcodeConfig(
|
|
121
|
+
barcode_bits=37,
|
|
122
|
+
timestamp_precision=TimestampPrecision.milliseconds,
|
|
123
|
+
bit_duration_ms=50.0,
|
|
124
|
+
init_duration_ms=15.0,
|
|
125
|
+
tolerance=0.20,
|
|
126
|
+
),
|
|
127
|
+
"random": BarcodeConfig(
|
|
128
|
+
ttl_type=TTLType.random,
|
|
129
|
+
barcode_bits=32,
|
|
130
|
+
),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_preset(name: str) -> BarcodeConfig:
|
|
135
|
+
"""Get a preset configuration by name."""
|
|
136
|
+
if name not in PRESETS:
|
|
137
|
+
raise ValueError(f"Unknown preset '{name}'. Available: {list(PRESETS.keys())}")
|
|
138
|
+
return PRESETS[name]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BarcodeDecoder:
|
|
5
|
+
"""Decode edge timestamps back to barcode values."""
|
|
6
|
+
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
barcode_bits: int = 37,
|
|
10
|
+
bit_duration_ms: float = 35.0,
|
|
11
|
+
init_duration_ms: float = 10.0,
|
|
12
|
+
tolerance: float = 0.25,
|
|
13
|
+
):
|
|
14
|
+
self.barcode_bits = barcode_bits
|
|
15
|
+
self.bit_duration_ms = bit_duration_ms
|
|
16
|
+
self.init_duration_ms = init_duration_ms
|
|
17
|
+
self.tolerance = tolerance
|
|
18
|
+
|
|
19
|
+
self.init_wrapper_ms = 3 * init_duration_ms
|
|
20
|
+
self.min_init = init_duration_ms * (1 - tolerance)
|
|
21
|
+
self.max_init = init_duration_ms * (1 + tolerance)
|
|
22
|
+
|
|
23
|
+
def decode_edges(
|
|
24
|
+
self, edge_timestamps: list[float], edge_levels: list[bool]
|
|
25
|
+
) -> tuple[float, int] | None:
|
|
26
|
+
"""Decode edge timestamps to (timestamp, barcode_value) or None on failure."""
|
|
27
|
+
if len(edge_timestamps) < 6:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
start_time = edge_timestamps[0]
|
|
31
|
+
rel_times_ms = [(t - start_time) * 1000 for t in edge_timestamps]
|
|
32
|
+
|
|
33
|
+
if not self._validate_init_pattern(rel_times_ms):
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
data_times = rel_times_ms[2:-2]
|
|
37
|
+
data_levels = edge_levels[2:-2]
|
|
38
|
+
|
|
39
|
+
if not data_times:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
bits = self._decode_bits(data_times, data_levels)
|
|
43
|
+
barcode_value = sum(bits[i] * (2**i) for i in range(len(bits)))
|
|
44
|
+
return (start_time, barcode_value)
|
|
45
|
+
|
|
46
|
+
def _validate_init_pattern(self, rel_times_ms: list[float]) -> bool:
|
|
47
|
+
# Require ≥1 init-duration gap (not 2): BNC idle-LOW + old encoder starting LOW
|
|
48
|
+
# leaves only 1 detectable gap. HIGH-LOW-HIGH encoder fix gives 2; requiring 1
|
|
49
|
+
# handles both encoder versions without breaking the fixed path.
|
|
50
|
+
if len(rel_times_ms) < 4:
|
|
51
|
+
return False
|
|
52
|
+
time_diffs = np.diff(rel_times_ms)
|
|
53
|
+
init_candidates = sum(
|
|
54
|
+
1 for diff in time_diffs[:3] if self.min_init <= diff <= self.max_init
|
|
55
|
+
)
|
|
56
|
+
return init_candidates >= 1
|
|
57
|
+
|
|
58
|
+
def _decode_bits(
|
|
59
|
+
self, data_times: list[float], data_levels: list[bool]
|
|
60
|
+
) -> list[int]:
|
|
61
|
+
bits = []
|
|
62
|
+
current_level = False
|
|
63
|
+
edge_idx = 0
|
|
64
|
+
for bit in range(self.barcode_bits):
|
|
65
|
+
bit_sample_time = (
|
|
66
|
+
self.init_wrapper_ms
|
|
67
|
+
+ bit * self.bit_duration_ms
|
|
68
|
+
+ self.bit_duration_ms / 2
|
|
69
|
+
)
|
|
70
|
+
while (
|
|
71
|
+
edge_idx < len(data_times) and data_times[edge_idx] <= bit_sample_time
|
|
72
|
+
):
|
|
73
|
+
current_level = data_levels[edge_idx]
|
|
74
|
+
edge_idx += 1
|
|
75
|
+
bits.append(1 if current_level else 0)
|
|
76
|
+
return bits
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from typing import NamedTuple
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TimingSegment(NamedTuple):
|
|
5
|
+
"""Single timing segment in a barcode sequence."""
|
|
6
|
+
|
|
7
|
+
level: bool # Signal level (True=HIGH, False=LOW)
|
|
8
|
+
duration_ms: float # Duration in milliseconds
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TimingEncoder:
|
|
12
|
+
"""Encode barcode bits into timing sequences for hardware drivers."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, bit_duration_ms: float = 35.0, init_duration_ms: float = 10.0):
|
|
15
|
+
self.bit_duration_ms = bit_duration_ms
|
|
16
|
+
self.init_duration_ms = init_duration_ms
|
|
17
|
+
self.init_sequence_ms = 3 * init_duration_ms
|
|
18
|
+
|
|
19
|
+
def encode_timing_sequence(self, bits: list[bool]) -> list[TimingSegment]:
|
|
20
|
+
sequence = []
|
|
21
|
+
|
|
22
|
+
# Start initialization: HIGH-LOW-HIGH
|
|
23
|
+
# Must start HIGH (not LOW) so the first edge always fires even when
|
|
24
|
+
# the BNC output is at LOW idle between trials. Starting LOW would
|
|
25
|
+
# suppress the first edge, leaving only 1 init gap visible to the
|
|
26
|
+
# decoder and causing init validation to fail for ~50% of barcodes
|
|
27
|
+
# (those where bit[0]=0, which produce no edge at t=30ms).
|
|
28
|
+
sequence.append(TimingSegment(True, self.init_duration_ms))
|
|
29
|
+
sequence.append(TimingSegment(False, self.init_duration_ms))
|
|
30
|
+
sequence.append(TimingSegment(True, self.init_duration_ms))
|
|
31
|
+
|
|
32
|
+
for bit in bits:
|
|
33
|
+
sequence.append(TimingSegment(bit, self.bit_duration_ms))
|
|
34
|
+
|
|
35
|
+
# End initialization: LOW-HIGH-LOW
|
|
36
|
+
sequence.append(TimingSegment(False, self.init_duration_ms))
|
|
37
|
+
sequence.append(TimingSegment(True, self.init_duration_ms))
|
|
38
|
+
sequence.append(TimingSegment(False, self.init_duration_ms))
|
|
39
|
+
|
|
40
|
+
return sequence
|
|
41
|
+
|
|
42
|
+
def encode_state_durations(self, bits: list[bool]) -> list[float]:
|
|
43
|
+
return [seg.duration_ms for seg in self.encode_timing_sequence(bits)]
|
|
44
|
+
|
|
45
|
+
def encode_level_durations(self, bits: list[bool]) -> list[tuple[bool, float]]:
|
|
46
|
+
return [
|
|
47
|
+
(seg.level, seg.duration_ms) for seg in self.encode_timing_sequence(bits)
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def get_total_duration(self, num_bits: int) -> float:
|
|
51
|
+
return 2 * self.init_sequence_ms + num_bits * self.bit_duration_ms
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def info(self) -> dict:
|
|
55
|
+
return {
|
|
56
|
+
"bit_duration_ms": self.bit_duration_ms,
|
|
57
|
+
"init_duration_ms": self.init_duration_ms,
|
|
58
|
+
"init_sequence_ms": self.init_sequence_ms,
|
|
59
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ttl_barcoder.core.config import (
|
|
9
|
+
PRECISION_UNITS_PER_SECOND,
|
|
10
|
+
BarcodeConfig,
|
|
11
|
+
TimestampPrecision,
|
|
12
|
+
TTLType,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TTLGenerator(ABC):
|
|
17
|
+
"""Abstract base for TTL barcode value generators."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, barcode_bits: int) -> None:
|
|
20
|
+
self.barcode_bits = barcode_bits
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def generate(self, timestamp: float | None = None) -> int:
|
|
24
|
+
"""Generate a barcode value."""
|
|
25
|
+
|
|
26
|
+
def encode_bits(self, value: int) -> list[bool]:
|
|
27
|
+
"""Encode barcode value as bit array (LSB first)."""
|
|
28
|
+
value = value % (2**self.barcode_bits)
|
|
29
|
+
return [bool((value >> i) & 1) for i in range(self.barcode_bits)]
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def max_value(self) -> int:
|
|
33
|
+
"""Maximum possible barcode value."""
|
|
34
|
+
return (2**self.barcode_bits) - 1
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def info(self) -> dict:
|
|
38
|
+
return {"barcode_bits": self.barcode_bits, "max_value": self.max_value}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TimestampGenerator(TTLGenerator):
|
|
42
|
+
"""Generate barcodes from Unix timestamps at configurable precision."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, barcode_bits: int, precision: TimestampPrecision) -> None:
|
|
45
|
+
super().__init__(barcode_bits)
|
|
46
|
+
self.precision = precision
|
|
47
|
+
self._units_per_second: float = PRECISION_UNITS_PER_SECOND[precision]
|
|
48
|
+
|
|
49
|
+
def generate(self, timestamp: float | None = None) -> int:
|
|
50
|
+
"""Generate barcode from timestamp (defaults to current time)."""
|
|
51
|
+
if timestamp is None:
|
|
52
|
+
timestamp = time.time()
|
|
53
|
+
units = int(timestamp * self._units_per_second)
|
|
54
|
+
return units % (2**self.barcode_bits)
|
|
55
|
+
|
|
56
|
+
def generate_sequence(
|
|
57
|
+
self,
|
|
58
|
+
count: int = 1,
|
|
59
|
+
interval_s: float = 5.0,
|
|
60
|
+
start_timestamp: float | None = None,
|
|
61
|
+
) -> list[int]:
|
|
62
|
+
"""Generate a sequence of barcodes at fixed time intervals."""
|
|
63
|
+
if start_timestamp is None:
|
|
64
|
+
start_timestamp = time.time()
|
|
65
|
+
return [self.generate(start_timestamp + i * interval_s) for i in range(count)]
|
|
66
|
+
|
|
67
|
+
def recover_timestamp(
|
|
68
|
+
self, barcode_value: int, reference_time: float | None = None
|
|
69
|
+
) -> float:
|
|
70
|
+
"""Recover timestamp from barcode value, resolving wraparound."""
|
|
71
|
+
if reference_time is None:
|
|
72
|
+
reference_time = time.time()
|
|
73
|
+
ref_units = int(reference_time * self._units_per_second)
|
|
74
|
+
window_size = 2**self.barcode_bits
|
|
75
|
+
window_number = ref_units // window_size
|
|
76
|
+
candidates = []
|
|
77
|
+
for w in (window_number - 1, window_number, window_number + 1):
|
|
78
|
+
candidate_units = w * window_size + barcode_value
|
|
79
|
+
candidate_time = candidate_units / self._units_per_second
|
|
80
|
+
candidates.append((abs(candidate_time - reference_time), candidate_time))
|
|
81
|
+
return min(candidates)[1]
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def coverage_seconds(self) -> float:
|
|
85
|
+
return (2**self.barcode_bits) / self._units_per_second
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def coverage_years(self) -> float:
|
|
89
|
+
return self.coverage_seconds / (365.25 * 24 * 3600)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def info(self) -> dict:
|
|
93
|
+
return {
|
|
94
|
+
**super().info,
|
|
95
|
+
"type": "timestamp",
|
|
96
|
+
"precision": self.precision.value,
|
|
97
|
+
"units_per_second": self._units_per_second,
|
|
98
|
+
"coverage_years": self.coverage_years,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class RandomGenerator(TTLGenerator):
|
|
103
|
+
"""Generate random n-bit barcode values using numpy."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, barcode_bits: int) -> None:
|
|
106
|
+
super().__init__(barcode_bits)
|
|
107
|
+
self._rng = np.random.default_rng()
|
|
108
|
+
|
|
109
|
+
def generate(self, timestamp: float | None = None) -> int:
|
|
110
|
+
"""Generate a random barcode value."""
|
|
111
|
+
return int(self._rng.integers(0, 2**self.barcode_bits))
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def info(self) -> dict:
|
|
115
|
+
return {**super().info, "type": "random"}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def create_generator(config: BarcodeConfig) -> TTLGenerator:
|
|
119
|
+
"""Create TimestampGenerator or RandomGenerator from config."""
|
|
120
|
+
if config.ttl_type == TTLType.timestamp:
|
|
121
|
+
return TimestampGenerator(config.barcode_bits, config.timestamp_precision)
|
|
122
|
+
if config.ttl_type == TTLType.random:
|
|
123
|
+
return RandomGenerator(config.barcode_bits)
|
|
124
|
+
raise ValueError(f"Unknown TTL type: {config.ttl_type!r}")
|
|
File without changes
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from pybpod import StateMachine
|
|
5
|
+
|
|
6
|
+
BPOD_AVAILABLE = True
|
|
7
|
+
except ImportError:
|
|
8
|
+
StateMachine = None
|
|
9
|
+
BPOD_AVAILABLE = False
|
|
10
|
+
|
|
11
|
+
BARCODE_FIRST_STATE_NAME = "barcode_start"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def inject_barcode_states(
|
|
15
|
+
sma,
|
|
16
|
+
timing_sequence: list[tuple[bool, float]],
|
|
17
|
+
bnc_channel: Any,
|
|
18
|
+
first_state_name: str = BARCODE_FIRST_STATE_NAME,
|
|
19
|
+
last_state_name: str = "exit",
|
|
20
|
+
):
|
|
21
|
+
"""Inject barcode timing states into a pybpodapi StateMachine in-place."""
|
|
22
|
+
n = len(timing_sequence)
|
|
23
|
+
for i, (level, duration_ms) in enumerate(timing_sequence):
|
|
24
|
+
state_name = first_state_name if i == 0 else f"{first_state_name}_seg_{i}"
|
|
25
|
+
next_state = (
|
|
26
|
+
last_state_name if i == n - 1 else f"{first_state_name}_seg_{i + 1}"
|
|
27
|
+
)
|
|
28
|
+
sma.add_state(
|
|
29
|
+
state_name=state_name,
|
|
30
|
+
state_timer=duration_ms / 1000.0,
|
|
31
|
+
state_change_conditions={"Tup": next_state},
|
|
32
|
+
output_actions=[(bnc_channel, 1 if level else 0)],
|
|
33
|
+
)
|
|
34
|
+
return sma
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BpodBarcodeSender:
|
|
38
|
+
"""Stateful wrapper around inject_barcode_states for class-based workflows."""
|
|
39
|
+
|
|
40
|
+
def inject_states(
|
|
41
|
+
self,
|
|
42
|
+
sma,
|
|
43
|
+
timing_sequence: list[tuple[bool, float]],
|
|
44
|
+
bnc_channel: Any,
|
|
45
|
+
first_state_name: str = BARCODE_FIRST_STATE_NAME,
|
|
46
|
+
last_state_name: str = "exit",
|
|
47
|
+
):
|
|
48
|
+
return inject_barcode_states(
|
|
49
|
+
sma, timing_sequence, bnc_channel, first_state_name, last_state_name
|
|
50
|
+
)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
import pigpio
|
|
6
|
+
|
|
7
|
+
PIGPIO_AVAILABLE = True
|
|
8
|
+
except ImportError:
|
|
9
|
+
pigpio = None
|
|
10
|
+
PIGPIO_AVAILABLE = False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PigpioBarcodeSender:
|
|
14
|
+
"""Prepares barcode pulses for pigpio without managing the connection."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, pin: int = 18):
|
|
17
|
+
if not PIGPIO_AVAILABLE:
|
|
18
|
+
raise ImportError("pigpio not available. Install with: pip install pigpio")
|
|
19
|
+
self.pin = pin
|
|
20
|
+
|
|
21
|
+
def prepare_pulses(self, timing_sequence: list[tuple[bool, float]]) -> list[Any]:
|
|
22
|
+
"""Convert (level, duration_ms) pairs to pigpio pulse objects."""
|
|
23
|
+
pulses = []
|
|
24
|
+
pin_mask = 1 << self.pin
|
|
25
|
+
for level, duration_ms in timing_sequence:
|
|
26
|
+
duration_us = int(duration_ms * 1000)
|
|
27
|
+
if level:
|
|
28
|
+
pulses.append(pigpio.pulse(pin_mask, 0, duration_us))
|
|
29
|
+
else:
|
|
30
|
+
pulses.append(pigpio.pulse(0, pin_mask, duration_us))
|
|
31
|
+
return pulses
|
|
32
|
+
|
|
33
|
+
def prepare_wave(self, timing_sequence: list[tuple[bool, float]]) -> int:
|
|
34
|
+
raise NotImplementedError("Use PigpioConnection.prepare_wave() instead")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PigpioConnection:
|
|
38
|
+
"""Manages pigpio daemon connection and wave transmission."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, pin: int = 18, host: str = "localhost", port: int = 8888):
|
|
41
|
+
if not PIGPIO_AVAILABLE:
|
|
42
|
+
raise ImportError("pigpio not available. Install with: pip install pigpio")
|
|
43
|
+
self.pin = pin
|
|
44
|
+
self.host = host
|
|
45
|
+
self.port = port
|
|
46
|
+
self.pi: Any = None
|
|
47
|
+
self.connected = False
|
|
48
|
+
self.sender = PigpioBarcodeSender(pin)
|
|
49
|
+
|
|
50
|
+
def connect(self) -> bool:
|
|
51
|
+
try:
|
|
52
|
+
self.pi = pigpio.pi(self.host, self.port)
|
|
53
|
+
if not self.pi.connected:
|
|
54
|
+
return False
|
|
55
|
+
self.pi.set_mode(self.pin, pigpio.OUTPUT)
|
|
56
|
+
self.pi.wave_tx_stop()
|
|
57
|
+
self.pi.wave_clear()
|
|
58
|
+
self.connected = True
|
|
59
|
+
return True
|
|
60
|
+
except Exception as e:
|
|
61
|
+
print(f"Failed to connect to pigpio daemon: {e}")
|
|
62
|
+
self.connected = False
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def send_sequence(self, timing_sequence: list[tuple[bool, float]]) -> bool:
|
|
66
|
+
"""Send timing sequence via GPIO wave, blocking until complete."""
|
|
67
|
+
if not self.connected and not self.connect():
|
|
68
|
+
return False
|
|
69
|
+
try:
|
|
70
|
+
self.pi.wave_tx_stop()
|
|
71
|
+
self.pi.wave_clear()
|
|
72
|
+
pulses = self.sender.prepare_pulses(timing_sequence)
|
|
73
|
+
self.pi.wave_add_generic(pulses)
|
|
74
|
+
wid = self.pi.wave_create()
|
|
75
|
+
if wid >= 0:
|
|
76
|
+
self.pi.wave_send_once(wid)
|
|
77
|
+
while self.pi.wave_tx_busy():
|
|
78
|
+
time.sleep(0.001)
|
|
79
|
+
self.pi.wave_delete(wid)
|
|
80
|
+
return True
|
|
81
|
+
else:
|
|
82
|
+
print("Failed to create pigpio wave")
|
|
83
|
+
return False
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(f"Failed to send sequence: {e}")
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
def disconnect(self):
|
|
89
|
+
if self.pi is not None:
|
|
90
|
+
try:
|
|
91
|
+
self.pi.wave_tx_stop()
|
|
92
|
+
self.pi.wave_clear()
|
|
93
|
+
self.pi.stop()
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
self.pi = None
|
|
97
|
+
self.connected = False
|
|
98
|
+
|
|
99
|
+
def __enter__(self):
|
|
100
|
+
self.connect()
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
104
|
+
self.disconnect()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def send_barcode_sequence(
|
|
108
|
+
timing_sequence: list[tuple[bool, float]], pin: int = 18
|
|
109
|
+
) -> bool:
|
|
110
|
+
"""Send a timing sequence via GPIO (convenience wrapper around PigpioConnection)."""
|
|
111
|
+
with PigpioConnection(pin) as gpio:
|
|
112
|
+
return gpio.send_sequence(timing_sequence)
|
ttl_barcoder/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ttl-barcoder
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Modular barcode generation for TTL synchronization with clean hardware separation
|
|
5
|
+
Project-URL: Homepage, https://github.com/murineshiftwork/ttl-barcoder
|
|
6
|
+
Project-URL: Documentation, https://murineshiftwork.github.io/ttl-barcoder/
|
|
7
|
+
Project-URL: Issue Tracker, https://github.com/murineshiftwork/ttl-barcoder/issues
|
|
8
|
+
Author-email: "Lars B. Rollik" <L.B.Rollik@protonmail.com>
|
|
9
|
+
License: BSD-3-Clause
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: barcode,bpod,daq,gpio,neuroscience,pigpio,raspberry-pi,synchronization,ttl
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator
|
|
22
|
+
Classifier: Topic :: System :: Hardware :: Hardware Drivers
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: numpy>=1.20
|
|
25
|
+
Requires-Dist: pydantic>=2.0
|
|
26
|
+
Provides-Extra: bpod
|
|
27
|
+
Requires-Dist: pybpod-api; extra == 'bpod'
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: commitizen; extra == 'dev'
|
|
30
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
31
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
35
|
+
Provides-Extra: docs
|
|
36
|
+
Requires-Dist: mkdocs-material; extra == 'docs'
|
|
37
|
+
Provides-Extra: pigpio
|
|
38
|
+
Requires-Dist: pigpio>=1.78; extra == 'pigpio'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# TTL Barcoder
|
|
42
|
+
|
|
43
|
+
[](https://pypi.org/project/ttl-barcoder)
|
|
44
|
+
|
|
45
|
+
Generate and decode binary barcodes over TTL signals to synchronize multiple data acquisition systems.
|
|
46
|
+
Barcodes encode a timestamp or random value as a sequence of timed HIGH/LOW pulses, transmittable over any digital output.
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from ttl_barcoder.core import BarcodeTTL, BarcodeConfig, TTLType, TimestampPrecision
|
|
53
|
+
|
|
54
|
+
# Timestamp barcode (default) — encodes current time at ms precision
|
|
55
|
+
barcoder = BarcodeTTL()
|
|
56
|
+
sequence = barcoder.get_sequence() # [(level: bool, duration_ms: float), ...]
|
|
57
|
+
|
|
58
|
+
# Random barcode
|
|
59
|
+
config = BarcodeConfig(ttl_type=TTLType.random, barcode_bits=32)
|
|
60
|
+
barcoder = BarcodeTTL(config)
|
|
61
|
+
sequence = barcoder.get_sequence()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Bpod
|
|
65
|
+
```python
|
|
66
|
+
from ttl_barcoder.core import BarcodeTTL
|
|
67
|
+
from ttl_barcoder.hardware.bpod import inject_barcode_states
|
|
68
|
+
|
|
69
|
+
barcoder = BarcodeTTL()
|
|
70
|
+
sequence = barcoder.get_sequence()
|
|
71
|
+
inject_barcode_states(sma, sequence, bnc_channel='BNC1', first_state_name='send_sync', last_state_name='next')
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Raspberry Pi GPIO
|
|
75
|
+
```python
|
|
76
|
+
from ttl_barcoder.hardware.pigpio import send_barcode_sequence
|
|
77
|
+
|
|
78
|
+
send_barcode_sequence(barcoder.get_sequence(), pin=18)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install ttl-barcoder # core only
|
|
86
|
+
pip install ttl-barcoder[bpod] # + Bpod
|
|
87
|
+
pip install ttl-barcoder[pigpio] # + Raspberry Pi GPIO
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
## Configuration
|
|
92
|
+
|
|
93
|
+
`BarcodeConfig` is a Pydantic model — all fields are validated on construction.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from ttl_barcoder.core import BarcodeConfig, TTLType, TimestampPrecision
|
|
97
|
+
|
|
98
|
+
config = BarcodeConfig(
|
|
99
|
+
ttl_type=TTLType.timestamp, # or TTLType.random
|
|
100
|
+
barcode_bits=37, # 16–64 bits
|
|
101
|
+
timestamp_precision=TimestampPrecision.milliseconds, # s / ms / us
|
|
102
|
+
bit_duration_ms=35.0,
|
|
103
|
+
init_duration_ms=10.0,
|
|
104
|
+
tolerance=0.25,
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Presets**: `default`, `high_speed`, `conservative`, `high_precision`, `random`
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from ttl_barcoder.core import get_preset
|
|
112
|
+
config = get_preset("conservative")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
| Preset | Bits | Precision | Bit duration | TX duration | Coverage |
|
|
116
|
+
|------------------|------|-----------|--------------|-------------|----------|
|
|
117
|
+
| `default` | 37 | ms | 35 ms | 1355 ms | 4.4 yr |
|
|
118
|
+
| `high_speed` | 32 | ms | 25 ms | 848 ms | 49 days |
|
|
119
|
+
| `conservative` | 37 | ms | 50 ms | 1940 ms | 4.4 yr |
|
|
120
|
+
| `high_precision` | 42 | us | 50 ms | 2190 ms | 51 days |
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
## Architecture
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
ttl_barcoder/
|
|
127
|
+
├── core/
|
|
128
|
+
│ ├── config.py # BarcodeConfig (Pydantic), TTLType, TimestampPrecision
|
|
129
|
+
│ ├── generator.py # TTLGenerator ABC → TimestampGenerator / RandomGenerator
|
|
130
|
+
│ ├── encoder.py # bits → (level, duration_ms) timing sequence
|
|
131
|
+
│ ├── decoder.py # edge timestamps → barcode value
|
|
132
|
+
│ └── barcode_ttl.py # BarcodeTTL — main interface combining the above
|
|
133
|
+
└── hardware/
|
|
134
|
+
├── bpod/ # Bpod StateMachine integration
|
|
135
|
+
└── pigpio/ # Raspberry Pi GPIO via pigpio
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- The generator is selected via a factory (`create_generator(config)`) based on `TTLType`.
|
|
139
|
+
- `TimestampGenerator` quantizes Unix time at the configured precision
|
|
140
|
+
- `RandomGenerator` draws from a numpy RNG. Both share the same `encode_bits` / `max_value` interface on the `TTLGenerator` base class.
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
## Examples
|
|
144
|
+
|
|
145
|
+
- `examples/dry_simulation.py` — full walkthrough, no hardware needed
|
|
146
|
+
- `examples/bpod_loopback.py` — Bpod StateMachine with loopback test
|
|
147
|
+
- `examples/pigpio_send.py` — Raspberry Pi GPIO transmission
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
## Contributing
|
|
151
|
+
|
|
152
|
+
1. Fork and create a feature branch
|
|
153
|
+
2. Add tests for new functionality
|
|
154
|
+
3. Run `pytest`
|
|
155
|
+
4. Submit a pull request
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
## Acknowledgments
|
|
159
|
+
|
|
160
|
+
- Based on barcode synchronization from University of Colorado ONE Core
|
|
161
|
+
- Inspired by Open Ephys protocols
|
|
162
|
+
- Built for the neuroscience and scientific DAQ community
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
## License & sources
|
|
166
|
+
|
|
167
|
+
This software is released under the **[BSD 3-Clause License](LICENSE)**.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
ttl_barcoder/__init__.py,sha256=KbookvXeQAyFUwv19dqFSzVJy1GPXNHEqxSpzHTZJKE,662
|
|
2
|
+
ttl_barcoder/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
ttl_barcoder/core/__init__.py,sha256=0wiD3l1-XO-UIgnvNZMOhtg4HFp9UJTF-ybS7T2EPLM,649
|
|
4
|
+
ttl_barcoder/core/barcode_ttl.py,sha256=mQ3wx4nDvkygl2CTwjls_5Dqlm1So2gq-lm1iXN7AL8,4195
|
|
5
|
+
ttl_barcoder/core/config.py,sha256=jpiYLA2ARPwtKy5yGommv7yPTze9RUPwFvXG6vF9OZk,4442
|
|
6
|
+
ttl_barcoder/core/decoder.py,sha256=cvxyuKSvbHZMUqI2JraGMO7Um6r3ci50tezsYaF6IxA,2660
|
|
7
|
+
ttl_barcoder/core/encoder.py,sha256=mWiuc1QfKNzmHiR9yUHldL_0R2-rjCz7BX9Q_Y6HWyE,2369
|
|
8
|
+
ttl_barcoder/core/generator.py,sha256=5w1_4x8vOXjUJ7nF0WkLKqk5HplGJePK_5UkzDJJ2eM,4295
|
|
9
|
+
ttl_barcoder/hardware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
ttl_barcoder/hardware/bpod/__init__.py,sha256=W9WnUbJO9wdeno4PYFzlSKRkHaVBoV0tiZzyT8qLpsQ,173
|
|
11
|
+
ttl_barcoder/hardware/bpod/sender.py,sha256=AtVq9TDel3-P-cRR1fHY4eUj1R3-iBZ-QPDmidmDS1Y,1494
|
|
12
|
+
ttl_barcoder/hardware/pigpio/__init__.py,sha256=j0CxEFA-38CrmBHTxVwYJ6ZD63v4DSUd_q2rkbmeuxM,161
|
|
13
|
+
ttl_barcoder/hardware/pigpio/sender.py,sha256=JZ0S-aNFDWk6mq1CaXj-QuVzVlPpTQvL7Ele6pYop5I,3717
|
|
14
|
+
ttl_barcoder-0.4.1.dist-info/METADATA,sha256=zXx7rGzTelAADAA1JiK1ylUKFjNC1qcTGTjQOw8nkls,5828
|
|
15
|
+
ttl_barcoder-0.4.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
16
|
+
ttl_barcoder-0.4.1.dist-info/licenses/LICENSE,sha256=C65BqnvorE42oDpFOq6DByL3mR_Xq9rQlecMPiuvIfM,1522
|
|
17
|
+
ttl_barcoder-0.4.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Lars B. Rollik
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
17
|
+
may be used to endorse or promote products derived from this software
|
|
18
|
+
without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|