Skip to content
Open
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
4 changes: 2 additions & 2 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ pyphantom = {editable = true, path = "."}

[dev-packages]
"pytest" = "*"
"pcapy" = "*"
"pcapy-ng" = "*"

[requires]
python_version = "3.7"
python_version = "3.11"

[scripts]
test = "py.test -v"
254 changes: 134 additions & 120 deletions Pipfile.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

Communicate with Vision Research Phantom® cameras with python

Flex multi-bracket responses are parsed with a ``FlexLoader`` (YAML 1.2 Core) so values such as ``-2e-06`` resolve as floats, matching real CineStation / camera firmware output.

## Installation

Expand Down
4 changes: 3 additions & 1 deletion pyphantom/fakecam/fakecam-ph7.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

import yaml

from pyphantom.flex import FlexLoader

from .fakecam_data import state, answers

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -200,7 +202,7 @@ def load_takes():
if os.path.exists("{}/takes-ph7/{}.raw".format(script_path, take_index)):
with open(yaml_file) as y:
clean = " ".join(y.read().split()).replace("\\", "")
take_info = yaml.safe_load(clean)
take_info = yaml.load(clean, Loader=FlexLoader)
# use first key of take_info because we renumber the takes
state["fc{}".format(take_index)] = take_info[list(take_info.keys())[0]]
logger.info("Take {} loaded".format(take_index))
Expand Down
3 changes: 2 additions & 1 deletion pyphantom/fakecam/fakecam.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import yaml

from pyphantom.flex import FlexLoader
from pyphantom.fakecam import ximg_send
from pyphantom.fakecam.fakecam_data import state, answers

Expand Down Expand Up @@ -285,7 +286,7 @@ def load_takes():
if os.path.exists("{}/{}.raw".format(takes_path, take_index)):
with open(yaml_file) as y:
clean = " ".join(y.read().split()).replace("\\", "")
take_info = yaml.safe_load(clean)
take_info = yaml.load(clean, Loader=FlexLoader)
# use first key of take_info because we renumber the takes
state["fc{}".format(take_index)] = take_info[list(take_info.keys())[0]]
logger.info("Take {} loaded".format(take_index))
Expand Down
4 changes: 2 additions & 2 deletions pyphantom/fakecam/takes/1.data
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ fc67 : { \
edger : 0, \
edgeth : 0, \
gamma : 2.2, \
rgamma : 0, \
bgamma : 0, \
rgamma : -2e-06, \
bgamma : -2e-06, \
toe : 1, \
gain : 1, \
offset : 0, \
Expand Down
30 changes: 29 additions & 1 deletion pyphantom/flex.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import errno
import logging
import re
import socket
import time
from multiprocessing import Lock
Expand Down Expand Up @@ -35,6 +36,33 @@ class FrameOutsideRangeError(Exception):
pass


class FlexLoader(yaml.SafeLoader):
"""YAML loader matching Phantom Flex wire format: YAML 1.2 Core floats that PyYAML's
SafeLoader (YAML 1.1) leaves as strings — e.g. ``-2e-06`` (scientific without ``.``),
``.inf``, ``.nan``.
"""

pass


# Integer-mantissa scientific notation (YAML 1.1 does not treat these as floats).
FlexLoader.add_implicit_resolver(
"tag:yaml.org,2002:float",
re.compile(r"^[-+]?\d+[eE][-+]?\d+$"),
list("+-0123456789"),
)
FlexLoader.add_implicit_resolver(
"tag:yaml.org,2002:float",
re.compile(r"^[-+]?\.inf$", re.IGNORECASE),
list("-+."),
)
FlexLoader.add_implicit_resolver(
"tag:yaml.org,2002:float",
re.compile(r"^\.nan$", re.IGNORECASE),
list("."),
)


def parse_simple(response):
clean = response.replace(" ", "").split("{")[1].split("}")[0].split(",")

Expand Down Expand Up @@ -75,7 +103,7 @@ def parse_response(response):
clean = "X :" + clean

try:
return yaml.safe_load(clean)["X"]
return yaml.load(clean, Loader=FlexLoader)["X"]
except yaml.parser.ParserError:
raise

Expand Down
137 changes: 137 additions & 0 deletions tests/fixtures/fc0_flex_response.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
fc0 : { \
res : 4096 x 2304, \
bin : 0, \
rate : 938, \
exp : 533049, \
shoff : 0, \
edrexp : 0, \
ptframes : 10, \
bcount : 0, \
bperiod : 1065142, \
hqenable : 0, \
frcount : 9588, \
ramp : "", \
state : { STR }, \
start : 0x1840000, \
len : 0x7E743000, \
frsize : 11796480, \
frspace : 2121543680, \
firstfr : -2887, \
lastfr : -1552, \
format : 266, \
decimation : 1, \
in : -2887, \
out : -1552, \
aexpmode : 0, \
aexpcomp : 0, \
audiopktsize : 0, \
trigtime : { \
secs : 1776415322, \
frac : 336880 \
}, \
cam : { \
syncimg : 0, \
master : 0, \
tcmode : 1, \
trigpol : 0, \
trigfilt : 32, \
frdelay : 0, \
startonacq : 1, \
aux1mode : 0, \
aux2mode : 0, \
aux3mode : 0, \
memgateen : 0, \
membpp : 12, \
tsformat : 1, \
mode : 0 \
}, \
info : { \
hwver : 4001, \
sver : 906101034, \
fver : 169, \
pver : 16, \
model : "Phantom Flex4K", \
sensor : 4000, \
snsversion : 105, \
snsserial : 0, \
kernel : 587, \
swver : 167360320, \
xver : 1013571, \
serial : 16395, \
cfa : 3, \
filter : 0, \
name : "16395", \
mdepths : 0x1000, \
genlockstatus : 0, \
snstemp : 35, \
camtemp : 54, \
features : "bref blk4 shtr attach earlyimg notify log dualp aux2 wide mm24 v4k genlock audio prores shtr mag" \
}, \
adj : { \
red : 1, \
green : 1, \
blue : 1, \
wbtemp : 4950, \
wbcc : 55, \
wbred : 1.370309432, \
wbblue : 1.866422519, \
edgeg : 0, \
edger : 0, \
edgeth : 0, \
gamma : 2.2, \
rgamma : -2e-06, \
bgamma : -2e-06, \
toe : 1, \
gain : 1, \
offset : 0, \
flare : 0, \
hue : 0, \
sat : 1, \
rped : 0, \
gped : 0, \
bped : 0, \
tone : "EI1250 0.024 0.100 0.056 0.200 0.134 0.400 0.224 0.600 0.271 0.700 0.600 0.960", \
mmsat : "", \
mmhue : "", \
chroma : 1, \
matrix : 1, \
log : 0, \
sdimin : 32, \
sdimax : 960, \
cmatrix : "cal 2.04935 -0.516239 0.0386356 -0.135381 0.757609 0.636798 -0.0564543 -0.552734 2.97495", \
umatrix : "", \
filter : "" \
}, \
meta : { \
name : "", \
uuid : "00000000-400b-4000-e9e1-f25a000523f0", \
system : 21, \
pbrate : 25, \
tcrate : 25, \
trigtc : "14:26:54.01", \
pax : 1296, \
pay : 2304, \
paox : 0, \
paoy : 0, \
vox : 0, \
voy : 0, \
vw : 4096, \
vh : 2304, \
vow : 1911, \
voh : 1075, \
ox : 0, \
oy : 0, \
w : 0, \
h : 0, \
ow : 0, \
oh : 0, \
crop : 0, \
resize : 0, \
lens : "", \
fstop : 0, \
flen : 0, \
gps : "", \
comment : "", \
xset : "" \
} \
}
83 changes: 76 additions & 7 deletions tests/test_flex.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import math
import os

import pytest
import yaml

from pyphantom import flex
from pyphantom.flex import FlexLoader


@pytest.fixture(scope="module")
Expand All @@ -18,8 +23,9 @@ def fin():

# FIXME: Find a nicer way to test structures than calling `str()` on them
def test_flag(cam):
assert cam.ask("get c1.state") == ["RDY"]
assert str(cam.structures.c1.state) == str(["RDY"])
# c1 is deepcopy(fc1) after load_takes; take YAML uses `state : { STR }` -> dict
assert cam.ask("get c1.state") == {"STR": ""}
assert str(cam.structures.c1.state) == str({"STR": ""})


def test_simple(cam):
Expand All @@ -34,11 +40,74 @@ def test_simple_with_colon(cam):

def test_dict(cam):
assert cam.ask("get defc") == {
"exp": 1250000,
"meta": {"crop": 0, "oh": 0, "ow": 0, "resize": 0},
"rate": 400,
"res": "4096x2304",
"aexpcomp": 0,
"aexpmode": 0,
"bcount": 0,
"bperiod": 10416317,
"decimation": 1,
"edrexp": 0,
"exp": 3333332,
"frcount": 5000,
"frsize": 2949120,
"hqenable": 0,
"meta": {"crop": 1, "h": 1080, "oh": 0, "ow": 0, "ox": 0, "oy": 0, "w": 1920},
"ptframes": 1,
"ramp": "",
"rate": 150,
"res": "2048 x 1152",
"shoff": 0,
}
assert str(cam.structures.defc) == str(
{"rate": 400, "res": "4096x2304", "exp": 1250000, "meta": {"crop": 0, "resize": 0, "ow": 0, "oh": 0}}
{
"rate": 150,
"res": "2048 x 1152",
"exp": 3333332,
"edrexp": 0,
"ptframes": 1,
"shoff": 0,
"ramp": "",
"bcount": 0,
"bperiod": 10416317,
"hqenable": 0,
"decimation": 1,
"frcount": 5000,
"frsize": 2949120,
"aexpmode": 0,
"aexpcomp": 0,
"meta": {"ox": 0, "oy": 0, "w": 1920, "h": 1080, "ow": 0, "oh": 0, "crop": 1},
}
)


def test_parse_response_scientific_without_dot():
"""Multi-bracket Flex replies use yaml.load; -2e-06 must be float (YAML 1.2), not str."""
r = flex.parse_response(
"Ok! fc0 : { adj : { rgamma : -2e-06, bgamma : -2e-06 } }"
)
assert isinstance(r["adj"]["rgamma"], float)
assert isinstance(r["adj"]["bgamma"], float)
assert math.isclose(r["adj"]["rgamma"], -2e-06, rel_tol=1e-9)
assert math.isclose(r["adj"]["bgamma"], -2e-06, rel_tol=1e-9)


def test_flex_loader_inf_nan():
"""FlexLoader extends SafeLoader for YAML 1.2 Core .inf / .nan (not exercised by single-bracket parse_simple)."""
doc = "X : { x : .inf, y : .nan, z : -2e-06 }"
loaded = yaml.load(doc, Loader=FlexLoader)["X"]
assert math.isinf(loaded["x"]) and loaded["x"] > 0
assert math.isnan(loaded["y"])
assert isinstance(loaded["z"], float)
assert math.isclose(loaded["z"], -2e-06, rel_tol=1e-9)


def test_parse_response_fc0_fixture():
fixture_path = os.path.join(os.path.dirname(__file__), "fixtures", "fc0_flex_response.txt")
with open(fixture_path, encoding="utf-8") as f:
body = f.read()
r = flex.parse_response("Ok! " + body)
assert isinstance(r, dict)
assert r["adj"]["gamma"] == 2.2
assert isinstance(r["adj"]["rgamma"], float)
assert isinstance(r["adj"]["bgamma"], float)
assert math.isclose(r["adj"]["rgamma"], -2e-06, rel_tol=1e-9)
assert math.isclose(r["adj"]["bgamma"], -2e-06, rel_tol=1e-9)