Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/client api #13

Draft
wants to merge 39 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
bf9b9a1
initial commit
jlnav Dec 8, 2022
cd18c45
trying cffi approach - can successfully call nrm_client_create from py
jlnav Dec 9, 2022
49be7f4
prototyping interface of NRMClient class
jlnav Dec 13, 2022
a8f91ab
attempt to add rest of client functions and relevant typedefs to ffi.…
jlnav Dec 16, 2022
b4af60d
tentative reorganization
jlnav Dec 16, 2022
ce9c27c
tentatively creating separate files for the additional structures, ad…
jlnav Dec 16, 2022
0118bdf
some notes, and cleaning junk out of ctrl-c-ctrl-v'd sensor.py
jlnav Dec 20, 2022
cbaba37
some more silliness
jlnav Jan 18, 2023
dd9a218
refactors interface for assigning nrm objects to client, initializes …
jlnav Jan 23, 2023
0d496f3
molding out test_client.py
jlnav Jan 26, 2023
ddb3329
need to access object creators underneath
jlnav Jan 26, 2023
9cff0fe
some reorganization, adjustments based on not needing PyObject
jlnav Jan 27, 2023
4b4fb87
fix imports, black reformatting, place .so in separate dir, client ru…
jlnav Jan 27, 2023
4da2377
trying to set up wrappers and registration
jlnav Feb 2, 2023
e38faa9
we can pass handles to the Client class through to C, where they can …
jlnav Feb 3, 2023
1cf7c36
making callback wrappers match listener_fn signatures with pyclient p…
jlnav Feb 8, 2023
4139709
removing providing uuid on creation of most objects, name is all that…
jlnav Feb 9, 2023
6646b07
implement start_event and start_actuate on the frontend, small fixes
jlnav Feb 10, 2023
5adc256
adding event and actuate to callback tests
jlnav Feb 10, 2023
d6533b2
on py frontend, using normal start_listener's
jlnav Feb 10, 2023
ec7a3e8
create pyinfo struct, passing through pyclient handle and cffi extern…
jlnav Feb 13, 2023
fc9385a
initial round of cleaning up, documenting
jlnav Feb 13, 2023
f10653b
fix
jlnav Feb 13, 2023
3b4751c
tentatively creating close methods for each component. need to determ…
jlnav Feb 17, 2023
4a56b88
tentative changes for correct component destruction, adding client.re…
jlnav Feb 21, 2023
5b915b4
bringing api inline with recent libnrm update
jlnav Feb 23, 2023
048b53b
tentative changes for setup.py integration
jlnav Feb 23, 2023
a610080
fix path
jlnav Feb 23, 2023
4849a20
add _build dir
jlnav Feb 24, 2023
f1bb2b7
updates to build.py to match nrm.h
jlnav Oct 12, 2023
e8eafc8
tiny fix
jlnav Oct 12, 2023
90f47cb
tentatively complete send_exit and send_tick
jlnav Oct 12, 2023
268b51b
removing a bunch of old config files, small adjust to build.py, remov…
jlnav Oct 13, 2023
0a905a9
remove scripts=[bin/nrmd]
jlnav Oct 13, 2023
2bbebe7
tiny fix
jlnav Oct 19, 2023
ea52139
try reading nrm.h, appending contents so cffi can compile it
jlnav Oct 19, 2023
8ca1d7e
rearrange
jlnav Oct 19, 2023
e89c882
more universal way of finding nrm.h
jlnav Oct 19, 2023
791370c
tentative progress, now skipping nrm_log_perror preprocessor block, b…
jlnav Oct 19, 2023
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# NRM Python

Python binaries and notebook infrastructure on top of nrm-core.
Python binaries and notebook infrastructure on top of libnrm.

Currently contains the official nrm daemon implementation, as well as
additional python modules to talk to the NRM from a python application, both
Expand Down
12 changes: 12 additions & 0 deletions nrm/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
###############################################################################
# Copyright 2023 UChicago Argonne, LLC.
# (c.f. AUTHORS, LICENSE)
#
# This file is part of the NRM project.
# For more info, see https://github.com/anlsys/nrm-python
#
# SPDX-License-Identifier: BSD-3-Clause
###############################################################################

from .components import Actuator, Scope, Sensor, Slice
from .client import Client
15 changes: 0 additions & 15 deletions bin/nrmd → nrm/api/_build/__init__.py
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
###############################################################################
# Copyright 2019 UChicago Argonne, LLC.
# (c.f. AUTHORS, LICENSE)
Expand All @@ -8,17 +7,3 @@
#
# SPDX-License-Identifier: BSD-3-Clause
###############################################################################

import sys
from nrm.sharedlib import WrapEither, Lib
import nrm.daemon
import os
from ctypes.util import find_library

nrmcoreso = find_library("nrm-core")
if not nrmcoreso:
nrmcoreso = os.environ.get("NRMSO")

with Lib(nrmcoreso) as lib:
cfg = lib.cli(sys.argv[1:])
nrm.daemon.runner(cfg, lib)
78 changes: 78 additions & 0 deletions nrm/api/build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from cffi import FFI
import subprocess
import os

ffi = FFI()

ffi.set_source(
"nrm.api._build._nrm_cffi",
"""
#include "nrm.h"
""",
libraries=["nrm"],
)

def my_temporary_header_locator():
return subprocess.Popen(["find", "../libnrm", "-name", "nrm.h"], stdout=subprocess.PIPE).communicate()[0].decode().split("\n")[0]

nrmh = my_temporary_header_locator()

cdef_base = \
"""

typedef int... time_t;

typedef struct timespec{
time_t tv_sec;
long tv_nsec;
};

typedef struct timespec nrm_time_t;

nrm_time_t nrm_time_fromns(int64_t ns);
typedef char *nrm_string_t;
typedef nrm_string_t nrm_uuid_t;
typedef struct nrm_vector_s nrm_vector_t;
typedef struct nrm_scope nrm_scope_t;

extern "Python" int _event_listener_wrap(nrm_string_t sensor_uuid,
nrm_time_t time,
nrm_scope_t *scope,
double value,
void *arg);

extern "Python" int _actuate_listener_wrap(nrm_uuid_t *uuid,
double value,
void *arg);

nrm_string_t nrm_string_fromchar(const char *buf);

void nrm_time_gettime(nrm_time_t *now);

int nrm_scope_destroy(nrm_scope_t *scope);

"""

with open(nrmh, "r") as f:
lines = f.readlines()

avoid_tokens = ["#", "extern \"C\" {", " nrm_log_printf("]
avoid_block = [
"\tdo { \\\n",
"\t\tchar *__nrm_errstr = strerror(errno); \\\n",
"\t\tnrm_log_printf(NRM_LOG_ERROR, __FILE__, __LINE__, \\\n",
"\t\t __VA_ARGS__); \\\n",
"\t\tnrm_log_printf(NRM_LOG_ERROR, __FILE__, __LINE__, \\\n",
"\t\t \"perror: %s\\n\", __nrm_errstr); \\\n",
"\t} while (0)\n",
]

for line in lines:
if not any([line.startswith(token) for token in avoid_tokens]) and line not in avoid_block:
cdef_base += line


ffi.cdef(cdef_base)

if __name__ == "__main__":
ffi.compile(verbose=True)
188 changes: 188 additions & 0 deletions nrm/api/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import time
from loguru import logger
from dataclasses import dataclass, field
from typing import Callable, List, Union

from nrm.api._build._nrm_cffi import ffi, lib
from nrm.api.components import (
NRMActuators,
NRMScopes,
NRMSensors,
NRMSlices,
Actuator,
Scope,
Sensor,
Slice,
)


@dataclass
class Client:
"""Client class for interacting with NRM C interface. Use as a context switcher.
Tentative usage:
```
from nrm.api import Client, Actuator
with Client("tcp://127.0.0.1", 2345, 3456) as nrmc:
...
nrmc.scopes["uuid"] = my_scope

...
nrmc.send_event(sensor, scope, 1234)
...

```
"""

scopes: NRMScopes = field(default_factory=NRMScopes)
sensors: NRMSensors = field(default_factory=NRMSensors)
slices: NRMSlices = field(default_factory=NRMSlices)
actuators: NRMActuators = field(default_factory=NRMActuators)

def __enter__(
self, uri: str = "tcp://127.0.0.1", pub_port: int = 2345, rpc_port: int = 3456
):
self._c_client_p = ffi.new("nrm_client_t **")
self._c_uri = ffi.new("char[]", bytes(uri, "utf-8"))
self._pyinfo_p = ffi.new("pyinfo_t **")
self._py_client_h = ffi.new_handle(self)
self.pub_port = pub_port
self.rpc_port = rpc_port

assert not lib.nrm_init(
ffi.NULL, ffi.NULL
), "NRM library did not initialize successfully"
logger.debug("NRM initialized")

assert not lib.nrm_client_create(
self._c_client_p, self._c_uri, self.pub_port, self.rpc_port
), "Python Client was unable to instantiate an underlying NRM C client"
logger.debug("NRM client created")
self._c_client = self._c_client_p[0]

assert not lib.nrm_pyinfo_create(
self._pyinfo_p,
self._py_client_h,
lib._event_listener_wrap,
lib._actuate_listener_wrap,
), "Python Client was unable to instantiate an underlying NRM C structure"
logger.debug("C pyinfo structure populated")
self._pyinfo = self._pyinfo_p[0]

for d in [self.scopes, self.sensors, self.slices, self.actuators]:
d._set_client(self._c_client)
logger.debug(
"NRM client assigned to Scopes, Sensors, Slices, and Actuators dict subclasses"
)
logger.info("Client instance initialized. Starting")
return self

def __exit__(self, exc_type, exc_value, traceback):
logger.info("Destroying Client instance")
lib.nrm_client_destroy(self._c_client_p)

def actuate(self, actuator: "Actuator", value: float) -> int:
"""Perform an actuation given an actuator and a value to set. Register this actuator
with the client first:
```
from nrm.api import Client, Actuator
act = Actuator("my-actuator")
act.set_choices(1.0, 2.0, 3.0, 4.0)
with Client("tcp://127.0.0.1", 2345, 3456) as nrmc:
```
NOTE: The given value should've already been set with `actuator_instance.set_values()`.

Parameters
----------
"""
logger.debug(f"ACTUATING with (actuator: {actuator}), (value: {value})")
return lib.nrm_client_actuate(self._c_client, actuator._actuator_ptr[0], value)

def send_event(self, sensor: "Sensor", scope: "Scope", value: float) -> int:
"""
Parameters
----------
"""
timespec = lib.nrm_time_fromns(time.time_ns())
logger.debug(
f"SENDING EVENT with (sensor: {sensor}), (value: {value}), (Value: {value})"
)
return lib.nrm_client_send_event(
self._c_client, timespec, sensor._sensor_ptr[0], scope._scope_ptr, value
)

def set_event_listener(self, event_listener: Callable) -> int:
"""
Parameters
----------
"""
self._event_listener = event_listener
logger.debug(f"Setting event Python callback: {event_listener}")
return lib.nrm_client_set_event_Pylistener(self._c_client, self._pyinfo)

def start_event_listener(self, topic: str) -> int:
"""
Parameters
----------
"""
topic = ffi.new("char []", bytes(topic, "utf-8"))
topic_as_nrm_string_t = ffi.new("nrm_string_t *", topic)
logger.debug(f"Starting event listener. Will call: {self._event_listener}")
return lib.nrm_client_start_event_listener(self._c_client, topic)

def set_actuate_listener(self, actuate_listener: Callable) -> int:
"""
Parameters
----------
"""
self._actuate_listener = actuate_listener
logger.debug(f"Setting actuate Python callback: {actuate_listener}")
return lib.nrm_client_set_actuate_Pylistener(self._c_client, self._pyinfo)

def start_actuate_listener(self) -> int:
"""
Returns
-------
"""
logger.debug(f"Starting actuate listener. Will call: {self._actuate_listener}")
return lib.nrm_client_start_actuate_listener(self._c_client)

def remove(self, obj) -> int:

if isinstance(obj, Actuator):
return lib.nrm_client_remove_actuator(self._c_client, obj._actuator_ptr[0])
elif isinstance(obj, Slice):
return lib.nrm_client_remove_slice(self._c_client, obj._slice_ptr[0])
elif isinstance(obj, Sensor):
return lib.nrm_client_remove_sensor(self._c_client, obj._sensor_ptr[0])
else:
return lib.nrm_client_remove_scope(self._c_client, obj._scope_ptr)

def send_exit(self) -> int:
logger.debug("Sending daemon EXIT request")
return lib.nrm_client_send_exit(self._c_client)

def send_tick(self) -> int:
logger.debug("Sending daemon TICK request")
return lib.nrm_client_send_tick(self._c_client)

def _event(self, sensor_uuid, time, scope, value):
logger.debug(f"Calling event callback: {self._event_listener}")
return self._event_listener(sensor_uuid, time, scope, value)

def _actuate(self, uuid, value):
logger.debug(f"Calling actuate callback: {self._actuate_listener}")
return self._actuate_listener(uuid, value)


@ffi.def_extern()
def _event_listener_wrap(
sensor_uuid, timespec, scope, value, py_client
): # see build.py for the C declaration of this and _actuate_listener_wrap
logger.debug("In event extern wrapper. Unpacking client")
return ffi.from_handle(py_client)._event(sensor_uuid, timespec, scope, value)


@ffi.def_extern()
def _actuate_listener_wrap(uuid, value, py_client):
logger.debug("In actuate extern wrapper. Unpacking client")
return ffi.from_handle(py_client)._actuate(uuid, value)
Loading
Loading