Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions synapse/cli/device_info_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ def visualize_configuration(info_dict, status):
frame_rate = source.get("frame_rate", "Unknown")
node_tree.add(f"Frame Rate: {frame_rate} hz")

display_map = source.get("display_map", None)
if display_map:
n_rows = len(display_map.get("row_map", []))
n_cols = len(display_map.get("col_map", []))
node_tree.add(
f"Display Map: {n_rows} x {n_cols} (custom pin mapping)"
)

optical_stim_status = nodes_status[index].get(
"optical_stimulation", None
)
Expand Down
93 changes: 90 additions & 3 deletions synapse/client/nodes/optical_stimulation.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,95 @@
from typing import Optional, List
from synapse.api.node_pb2 import NodeConfig, NodeType
from synapse.api.nodes.optical_stimulation_pb2 import OpticalStimulationConfig
from synapse.api.nodes.optical_stimulation_pb2 import (
OpticalStimulationConfig,
DisplayMap,
)
from synapse.client.node import Node

# Maximum logical display dimension and physical pin value supported by the
# LUX16K / MUX01 gateware (must match MUX01_MAX_DIM / the 8-bit pin width).
MAX_DISPLAY_DIM = 128
MAX_PHYSICAL_PIN = 0xFF


# Ready-made physical pin maps for the displays that were previously hardcoded
# in the MUX01 gateware. Pass these as row_map/col_map to OpticalStimulation.

# BIDIR_BIOHYBRID_V1 (NeRV512U biohybrid), 12 rows x 23 cols.
BIOHYBRID_NERV512U_V1_ROW_MAP = [0, 64, 8, 72, 16, 80, 24, 88, 32, 96, 40, 104]
BIOHYBRID_NERV512U_V1_COL_MAP = [
0, 1, 8, 9, 16, 17, 24, 25, 32, 33, 40, 41,
48, 49, 56, 57, 64, 65, 72, 73, 74, 75, 76,
]

# NeRV512U biohybrid, 23 rows x 23 cols (electronics branch maxr/512_led).
BIOHYBRID_NERV512U_23X23_ROW_MAP = [
0, 64, 8, 72, 16, 80, 25, 88, 24, 96, 33, 105,
32, 104, 41, 113, 40, 112, 49, 121, 48, 120, 57,
]
BIOHYBRID_NERV512U_23X23_COL_MAP = BIOHYBRID_NERV512U_V1_COL_MAP

# LED breakout board (60110001), 32 rows x 64 cols.
# Rows interleave 0..15 with 64..79; cols are 0..31 then 64..95.
LED_BOARD_60110001_ROW_MAP = [
v for i in range(16) for v in (i, i + 64)
]
LED_BOARD_60110001_COL_MAP = list(range(32)) + list(range(64, 96))


class OpticalStimulation(Node):
type = NodeType.kOpticalStimulation

def __init__(
self, peripheral_id: int, pixel_mask: List[int], bit_width: int, frame_rate: int, gain: float, send_receipts: bool = False
self,
peripheral_id: int,
pixel_mask: List[int],
bit_width: int,
frame_rate: int,
gain: float,
send_receipts: bool = False,
row_map: Optional[List[int]] = None,
col_map: Optional[List[int]] = None,
):
self.pixel_mask = pixel_mask
self.peripheral_id = peripheral_id
self.bit_width = bit_width
self.frame_rate = frame_rate
self.gain = gain
self.send_receipts = send_receipts
# Optional physical pin mapping. row_map[logical_row] / col_map[logical_col]
# give the physical chip pin; the lengths define the display dimensions.
# Both must be supplied together (or neither).
self.row_map = row_map
self.col_map = col_map
self._validate_display_map()

def _validate_display_map(self):
has_row = bool(self.row_map)
has_col = bool(self.col_map)
if has_row != has_col:
raise ValueError(
"row_map and col_map must both be provided together (or neither)"
)
if not has_row:
return
if len(self.row_map) > MAX_DISPLAY_DIM or len(self.col_map) > MAX_DISPLAY_DIM:
raise ValueError(
f"display map dimensions {len(self.row_map)}x{len(self.col_map)} "
f"exceed maximum {MAX_DISPLAY_DIM}x{MAX_DISPLAY_DIM}"
)
for pin in list(self.row_map) + list(self.col_map):
if pin < 0 or pin > MAX_PHYSICAL_PIN:
raise ValueError(
f"display map pin {pin} out of range [0, {MAX_PHYSICAL_PIN}]"
)
if self.pixel_mask and len(self.pixel_mask) != len(self.row_map) * len(
self.col_map
):
raise ValueError(
"pixel_mask length must equal len(row_map) * len(col_map) "
"when a display map is provided"
)

def _to_proto(self):
n = NodeConfig()
Expand All @@ -26,6 +100,11 @@ def _to_proto(self):
p.frame_rate = self.frame_rate
p.gain = self.gain
p.send_receipts = self.send_receipts
if self.row_map and self.col_map:
display_map = DisplayMap()
display_map.row_map.extend(self.row_map)
display_map.col_map.extend(self.col_map)
p.display_map.CopyFrom(display_map)
n.optical_stimulation.CopyFrom(p)
return n

Expand All @@ -37,11 +116,19 @@ def _from_proto(proto: Optional[OpticalStimulationConfig]):
if not isinstance(proto, OpticalStimulationConfig):
raise ValueError("proto is not of type OpticalStimulationConfig")

row_map = None
col_map = None
if proto.HasField("display_map"):
row_map = list(proto.display_map.row_map)
col_map = list(proto.display_map.col_map)

return OpticalStimulation(
peripheral_id=proto.peripheral_id,
pixel_mask=list(proto.pixel_mask),
bit_width=proto.bit_width,
frame_rate=proto.frame_rate,
gain=proto.gain,
send_receipts=proto.send_receipts
send_receipts=proto.send_receipts,
row_map=row_map,
col_map=col_map,
)
49 changes: 49 additions & 0 deletions synapse/tests/client/test_optical_stimulation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import pytest

from synapse.api.nodes.optical_stimulation_pb2 import OpticalStimulationConfig
from synapse.client.nodes.optical_stimulation import (
BIOHYBRID_NERV512U_23X23_COL_MAP,
BIOHYBRID_NERV512U_23X23_ROW_MAP,
OpticalStimulation,
)

ROWS = BIOHYBRID_NERV512U_23X23_ROW_MAP
COLS = BIOHYBRID_NERV512U_23X23_COL_MAP


def make(**kw):
args = dict(peripheral_id=101, pixel_mask=[], bit_width=8, frame_rate=30, gain=1.0)
args.update(kw)
return OpticalStimulation(**args)


def test_display_map_round_trips():
proto = make(row_map=ROWS, col_map=COLS)._to_proto()
assert list(proto.optical_stimulation.display_map.row_map) == ROWS
assert list(proto.optical_stimulation.display_map.col_map) == COLS

node = OpticalStimulation._from_proto(proto.optical_stimulation)
assert node.row_map == ROWS
assert node.col_map == COLS


def test_no_display_map_leaves_field_unset():
proto = make()._to_proto()
assert not proto.optical_stimulation.HasField("display_map")
node = OpticalStimulation._from_proto(OpticalStimulationConfig())
assert node.row_map is None and node.col_map is None


@pytest.mark.parametrize(
"kw",
[
dict(row_map=ROWS),
dict(col_map=COLS),
dict(row_map=[0] * 129, col_map=COLS),
dict(row_map=[256], col_map=COLS),
dict(row_map=ROWS, col_map=COLS, pixel_mask=[1] * 10),
],
)
def test_invalid_display_map_rejected(kw):
with pytest.raises(ValueError):
make(**kw)
Loading