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.
@@ -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,3 @@
1
+ from .sender import BARCODE_FIRST_STATE_NAME, BpodBarcodeSender, inject_barcode_states
2
+
3
+ __all__ = ["BARCODE_FIRST_STATE_NAME", "BpodBarcodeSender", "inject_barcode_states"]
@@ -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,3 @@
1
+ from .sender import PigpioBarcodeSender, PigpioConnection, send_barcode_sequence
2
+
3
+ __all__ = ["PigpioBarcodeSender", "PigpioConnection", "send_barcode_sequence"]
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/ttl-barcoder.svg)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.