Skip to content

Commit

Permalink
Fix http_server_interface.py and update example docs
Browse files Browse the repository at this point in the history
  • Loading branch information
jmthomas committed Nov 26, 2024
1 parent 5cccaeb commit c0c1f3c
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 29 deletions.
39 changes: 39 additions & 0 deletions examples/openc3-cosmos-http-example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,42 @@ This plugin provides an example of using the HttpClientInterface and the HttpSer
1. Click the paperclip icon and choose your plugin.gem file
1. Fill out plugin parameters
1. Click Install

## Use the Clients

NOTE: The COSMOS Demo must be installed for the Clients to work

First connect either the PYTHON_CLIENT_INT or the RUBY_CLIENT_INT (they can't both be connected). Then go to Packet Viewer and view the PYTHON_CLIENT / RUBY_CLIENT TLM_RESPONSE packet to see the value retrieved from the COSMOS API (INST HEALTH_STATUS TEMP1).

## Use the Server

NOTE: The COSMOS Demo does NOT need to be installed for the Server to work

NOTE: You must expose the server ports in the compose.yaml. Add the following:

```yaml
openc3-operator:
ports:
- 127.0.0.1:9090:9090
- 127.0.0.1:9191:9191
```
The main way to interact with the server is to use curl as follows:
```bash
% curl "127.0.0.1:9090/webhook"
RESPONSE_TEXT=Webhook+Received%21%
% curl "127.0.0.1:9191/webhook"
RESPONSE_TEXT=Webhook+Received%21%
```

You can also post data to the server:

```bash
% curl -H "Content-Type: application/json" --request POST --data '{"temp":"123"}' "127.0.0.1:9090/webhook"
RESPONSE_TEXT=Webhook+Received%21%
% curl -H "Content-Type: application/json" --request POST --data '{"temp":"123"}' "127.0.0.1:9191/webhook"
RESPONSE_TEXT=Webhook+Received%21%
```

The request data is mirrored back to the PYTHON_SERVER / RUBY_SERVER REQUEST packet.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ COMMAND <%= target_name %> TLM BIG_ENDIAN "Gets a telemetry point"
TEMPLATE '{"jsonrpc": "2.0", "method": "tlm", "params": [], "keyword_params": {"type":"CONVERTED", "cache_timeout":0.1, "scope":"DEFAULT"}, "id": 0 }'
PARAMETER HTTP_PATH 0 0 DERIVED nil nil "/openc3-api/api"
PARAMETER HTTP_METHOD 0 0 DERIVED nil nil "POST"
PARAMETER HTTP_PACKET 0 0 DERIVED nil nil "TLMRESPONSE"
PARAMETER HTTP_PACKET 0 0 DERIVED nil nil "TLM_RESPONSE"
PARAMETER HTTP_QUERY_SCOPE 0 0 DERIVED nil nil "DEFAULT"
KEY scope
PARAMETER HTTP_HEADER_CONTENT_TYPE 0 0 DERIVED nil nil "application/json-rpc"
Expand All @@ -25,7 +25,7 @@ COMMAND <%= target_name %> TLM BIG_ENDIAN "Gets a telemetry point"
APPEND_PARAMETER ID 32 UINT MIN MAX 0
KEY $.id

TELEMETRY <%= target_name %> TLMRESPONSE BIG_ENDIAN "Handles the TLM command response"
TELEMETRY <%= target_name %> TLM_RESPONSE BIG_ENDIAN "Handles the TLM command response"
ACCESSOR HttpAccessor JsonAccessor
TEMPLATE '{"jsonrpc": "2.0", "result": 0.0, "id": 0}'
ITEM HTTP_STATUS 0 0 DERIVED
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ TELEMETRY <%= target_name %> REQUEST BIG_ENDIAN "Captures the Request Data"
TEMPLATE ""
APPEND_ITEM TEMPERATURE 32 FLOAT
KEY temperature
ITEM HTTP_QUERY_TEMP 0 0 DERIVED
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ COMMAND <%= target_name %> TLM BIG_ENDIAN "Gets a telemetry point"
TEMPLATE '{"jsonrpc": "2.0", "method": "tlm", "params": [], "keyword_params": {"type":"CONVERTED", "cache_timeout":0.1, "scope":"DEFAULT"}, "id": 0 }'
PARAMETER HTTP_PATH 0 0 DERIVED nil nil "/openc3-api/api"
PARAMETER HTTP_METHOD 0 0 DERIVED nil nil "POST"
PARAMETER HTTP_PACKET 0 0 DERIVED nil nil "TLMRESPONSE"
PARAMETER HTTP_PACKET 0 0 DERIVED nil nil "TLM_RESPONSE"
PARAMETER HTTP_QUERY_SCOPE 0 0 DERIVED nil nil "DEFAULT"
KEY scope
PARAMETER HTTP_HEADER_CONTENT_TYPE 0 0 DERIVED nil nil "application/json-rpc"
Expand All @@ -25,7 +25,7 @@ COMMAND <%= target_name %> TLM BIG_ENDIAN "Gets a telemetry point"
APPEND_PARAMETER ID 32 UINT MIN MAX 0
KEY $.id

TELEMETRY <%= target_name %> TLMRESPONSE BIG_ENDIAN "Handles the TLM command response"
TELEMETRY <%= target_name %> TLM_RESPONSE BIG_ENDIAN "Handles the TLM command response"
ACCESSOR HttpAccessor JsonAccessor
TEMPLATE '{"jsonrpc": "2.0", "result": 0.0, "id": 0}'
ITEM HTTP_STATUS 0 0 DERIVED
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ TELEMETRY <%= target_name %> REQUEST BIG_ENDIAN "Captures the Request Data"
TEMPLATE ""
APPEND_ITEM TEMPERATURE 32 FLOAT
KEY temperature
ITEM HTTP_QUERY_TEMP 0 0 DERIVED
7 changes: 3 additions & 4 deletions openc3/python/openc3/accessors/form_accessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,10 @@ def write_item(cls, item, value, buffer):

if isinstance(value, list):
for value_value in value:
ary.append([item.key, value_value])
ary.append((item.key, value_value))
else:
ary.append([item.key, value])

buffer.replace(urllib.parse.urlencode(ary))
ary.append((item.key, value))
buffer[:] = urllib.parse.urlencode(ary).encode()
return value

# If this is set it will enforce that buffer data is encoded
Expand Down
43 changes: 22 additions & 21 deletions openc3/python/openc3/interfaces/http_server_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@


class Handler(BaseHTTPRequestHandler):
def do_POST(self):
self.handle_request()

def do_GET(self):
self.handle_request()

def handle_request(self):
if self.server.lookup.get(self.path):
packets = self.server.lookup[self.path]
status = 200
Expand Down Expand Up @@ -55,7 +61,7 @@ def do_GET(self):
try:
packet_name = packet.read("HTTP_PACKET")
except Exception:
# No packet name means dont save the request as telemetry
# No packet name means don't save the request as telemetry
pass
if packet_name:
data = b""
Expand All @@ -68,7 +74,7 @@ def do_GET(self):

if self.headers:
extra["HTTP_HEADERS"] = {}
for key, value in headers.items():
for key, value in self.headers.items():
extra["HTTP_HEADERS"][key.lower()] = value

queries = {}
Expand All @@ -83,7 +89,7 @@ def do_GET(self):
for key, value in queries.items():
extra["HTTP_QUERIES"][key] = value

self.server.request_queue.put([data, extra])
self.server.request_queue.put((data, extra))


class HttpServerInterface(Interface):
Expand All @@ -93,8 +99,6 @@ def __init__(self, port=80):
self.listen_address = "0.0.0.0" # Default to ANY
self.port = int(port)
self.server = None
self.request_queue = queue.Queue()
self.build_path_lookup()

def build_path_lookup(self):
self.lookup = {}
Expand All @@ -110,7 +114,7 @@ def build_path_lookup(self):
f"HttpServerInterface Packet {target_name} {packet_name} unable to read HTTP_PATH\n{repr(error)}"
)
if path:
self.lookup[path] = self.lookup[path] or []
self.lookup[path] = self.lookup.get(path) or []
self.lookup[path].append(packet)

# Supported Options
Expand All @@ -126,10 +130,14 @@ def connection_string(self):

# Connects the interface to its target(s)
def connect(self):
super().connect()
# Can't build the lookup until after init because the target_names are not set
self.build_path_lookup()
self.server = ThreadingHTTPServer((self.listen_address, self.port), Handler)
self.server_thread = Thread(target=self.server.serve_forever())
self.server.request_queue = queue.Queue()
self.server.lookup = self.lookup
self.server_thread = Thread(target=self.server.serve_forever)
self.server_thread.start()
super().connect()

def connected(self):
if self.server:
Expand All @@ -143,23 +151,16 @@ def disconnect(self):
self.server.shutdown()
self.server_thread.join()
self.server = None
try:
while True:
# raises queue.Empty once empty
self.request_queue.get_nowait()
except queue.Empty:
pass
super().disconnect()
self.request_queue.put(None)

def convert_packet_to_data(self, _packet):
def convert_packet_to_data(self, packet):
raise RuntimeError("Commands cannot be sent to HttpServerInterface")

def write_interface(self, _data, _extra=None):
def write_interface(self, data, extra=None):
raise RuntimeError("Commands cannot be sent to HttpServerInterface")

def read_interface(self):
data, extra = self.request_queue.get(block=True)
data, extra = self.server.request_queue.get(block=True)
if data is None:
return data, extra
self.read_interface_base(data, extra)
Expand All @@ -170,7 +171,7 @@ def read_interface(self):
# @param data [String] Raw packet data
# @return [Packet] OpenC3 Packet with buffer filled with data
def convert_data_to_packet(self, data, extra=None):
packet = Packet(None, None, "BIG_ENDIAN", None, str(data))
packet = Packet(None, None, "BIG_ENDIAN", None, data)
packet.accessor = HttpAccessor(packet)
if extra:
# Identify the response
Expand All @@ -179,8 +180,8 @@ def convert_data_to_packet(self, data, extra=None):
if request_target_name and request_packet_name:
packet.target_name = str(request_target_name).upper()
packet.packet_name = str(request_packet_name).upper()
extra.delete("HTTP_REQUEST_TARGET_NAME")
extra.delete("HTTP_REQUEST_PACKET_NAME")
extra.pop("HTTP_REQUEST_TARGET_NAME", None)
extra.pop("HTTP_REQUEST_PACKET_NAME", None)
packet.extra = extra

return packet

0 comments on commit c0c1f3c

Please sign in to comment.