Skip to content

Commit d726680

Browse files
authored
Merge pull request #1080 from OpenC3/handle_custom_python_classes
Handle custom python classes
2 parents 3dd8714 + d8b1b66 commit d726680

File tree

8 files changed

+128
-36
lines changed

8 files changed

+128
-36
lines changed

openc3/lib/openc3/packets/limits_response.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
# All changes Copyright 2022, OpenC3, Inc.
1818
# All Rights Reserved
1919
#
20-
# This file may also be used under the terms of a commercial license
20+
# This file may also be used under the terms of a commercial license
2121
# if purchased from OpenC3, Inc.
2222

2323
# This file implements a class to handle responses to limits state changes.

openc3/lib/openc3/packets/packet_config.rb

+32-14
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
require 'openc3/packets/parsers/processor_parser'
3232
require 'openc3/packets/parsers/xtce_parser'
3333
require 'openc3/packets/parsers/xtce_converter'
34+
require 'openc3/utilities/python_proxy'
3435
require 'openc3/conversions'
3536
require 'openc3/processors'
3637
require 'nokogiri'
@@ -82,6 +83,9 @@ class PacketConfig
8283
# defined by that identification. Telemetry version
8384
attr_reader :tlm_id_value_hash
8485

86+
# @return [String] Language of current target (ruby or python)
87+
attr_reader :language
88+
8589
COMMAND = "Command"
8690
TELEMETRY = "Telemetry"
8791

@@ -117,7 +121,7 @@ def initialize
117121
# @param filename [String] The name of the configuration file
118122
# @param process_target_name [String] The target name. Pass nil when parsing
119123
# an xtce file to automatically determine the target name.
120-
def process_file(filename, process_target_name)
124+
def process_file(filename, process_target_name, language = 'ruby')
121125
# Handle .xtce files
122126
extension = File.extname(filename).to_s.downcase
123127
if extension == ".xtce" or extension == ".xml"
@@ -128,6 +132,7 @@ def process_file(filename, process_target_name)
128132
# Partial files are included into another file and thus aren't directly processed
129133
return if File.basename(filename)[0] == '_' # Partials start with underscore
130134

135+
@language = language
131136
@converted_type = nil
132137
@converted_bit_size = nil
133138
@proc_text = ''
@@ -424,7 +429,7 @@ def process_current_packet(parser, keyword, params)
424429

425430
# Define a processor class that will be called once when a packet is received
426431
when 'PROCESSOR'
427-
ProcessorParser.parse(parser, @current_packet, @current_cmd_or_tlm)
432+
ProcessorParser.parse(parser, @current_packet, @current_cmd_or_tlm, @language)
428433

429434
when 'DISABLE_MESSAGES'
430435
usage = "#{keyword}"
@@ -467,11 +472,19 @@ def process_current_packet(parser, keyword, params)
467472
usage = "#{keyword} <Accessor class name>"
468473
parser.verify_num_parameters(1, nil, usage)
469474
begin
470-
klass = OpenC3.require_class(params[0])
471-
if params.length > 1
472-
@current_packet.accessor = klass.new(@current_packet, *params[1..-1])
475+
if @language == 'ruby'
476+
klass = OpenC3.require_class(params[0])
477+
if params.length > 1
478+
@current_packet.accessor = klass.new(@current_packet, *params[1..-1])
479+
else
480+
@current_packet.accessor = klass.new(@current_packet)
481+
end
473482
else
474-
@current_packet.accessor = klass.new(@current_packet)
483+
if params.length > 1
484+
@current_packet.accessor = PythonProxy.new('Accessor', params[0], @current_packet, *params[1..-1])
485+
else
486+
@current_packet.accessor = PythonProxy.new('Accessor', params[0], @current_packet)
487+
end
475488
end
476489
rescue Exception => err
477490
raise parser.error(err)
@@ -530,13 +543,18 @@ def process_current_item(parser, keyword, params)
530543
usage = "#{keyword} <conversion class filename> <custom parameters> ..."
531544
parser.verify_num_parameters(1, nil, usage)
532545
begin
533-
klass = OpenC3.require_class(params[0])
534-
conversion = klass.new(*params[1..(params.length - 1)])
535-
@current_item.public_send("#{keyword.downcase}=".to_sym, conversion)
536-
if klass != ProcessorConversion and (conversion.converted_type.nil? or conversion.converted_bit_size.nil?)
537-
msg = "Read Conversion #{params[0]} on item #{@current_item.name} does not specify converted type or bit size"
538-
@warnings << msg
539-
Logger.instance.warn @warnings[-1]
546+
if @language == 'ruby'
547+
klass = OpenC3.require_class(params[0])
548+
conversion = klass.new(*params[1..(params.length - 1)])
549+
@current_item.public_send("#{keyword.downcase}=".to_sym, conversion)
550+
if klass != ProcessorConversion and (conversion.converted_type.nil? or conversion.converted_bit_size.nil?)
551+
msg = "Read Conversion #{params[0]} on item #{@current_item.name} does not specify converted type or bit size"
552+
@warnings << msg
553+
Logger.instance.warn @warnings[-1]
554+
end
555+
else
556+
conversion = PythonProxy.new('Conversion', params[0], *params[1..(params.length - 1)])
557+
@current_item.public_send("#{keyword.downcase}=".to_sym, conversion)
540558
end
541559
rescue Exception => err
542560
raise parser.error(err)
@@ -600,7 +618,7 @@ def process_current_item(parser, keyword, params)
600618
# Define a response class that will be called when the limits state of the
601619
# current item changes.
602620
when 'LIMITS_RESPONSE'
603-
LimitsResponseParser.parse(parser, @current_item, @current_cmd_or_tlm)
621+
LimitsResponseParser.parse(parser, @current_item, @current_cmd_or_tlm, @language)
604622

605623
# Define a printf style formatting string for the current telemetry item
606624
when 'FORMAT_STRING'

openc3/lib/openc3/packets/packet_item.rb

+3-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
require 'openc3/packets/structure_item'
2424
require 'openc3/packets/packet_item_limits'
2525
require 'openc3/conversions/conversion'
26+
require 'openc3/utilities/python_proxy'
2627
require 'openc3/io/json_rpc' # Includes needed as_json code
2728

2829
module OpenC3
@@ -129,7 +130,7 @@ def format_string=(format_string)
129130

130131
def read_conversion=(read_conversion)
131132
if read_conversion
132-
raise ArgumentError, "#{@name}: read_conversion must be a OpenC3::Conversion but is a #{read_conversion.class}" unless OpenC3::Conversion === read_conversion
133+
raise ArgumentError, "#{@name}: read_conversion must be a OpenC3::Conversion but is a #{read_conversion.class}" unless OpenC3::Conversion === read_conversion or OpenC3::PythonProxy === read_conversion
133134

134135
@read_conversion = read_conversion.clone
135136
else
@@ -139,7 +140,7 @@ def read_conversion=(read_conversion)
139140

140141
def write_conversion=(write_conversion)
141142
if write_conversion
142-
raise ArgumentError, "#{@name}: write_conversion must be a OpenC3::Conversion but is a #{write_conversion.class}" unless OpenC3::Conversion === write_conversion
143+
raise ArgumentError, "#{@name}: write_conversion must be a OpenC3::Conversion but is a #{write_conversion.class}" unless OpenC3::Conversion === write_conversion or OpenC3::PythonProxy === write_conversion
143144

144145
@write_conversion = write_conversion.clone
145146
else

openc3/lib/openc3/packets/packet_item_limits.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
# if purchased from OpenC3, Inc.
2222

2323
require 'openc3/packets/limits_response'
24+
require 'openc3/utilities/python_proxy'
2425

2526
module OpenC3
2627
# Maintains knowledge of limits for a PacketItem
@@ -96,7 +97,7 @@ def state=(state)
9697

9798
def response=(response)
9899
if response
99-
raise ArgumentError, "response must be a OpenC3::LimitsResponse but is a #{response.class}" unless OpenC3::LimitsResponse === response
100+
raise ArgumentError, "response must be a OpenC3::LimitsResponse but is a #{response.class}" unless OpenC3::LimitsResponse === response or OpenC3::PythonProxy === response
100101

101102
@response = response.clone
102103
else

openc3/lib/openc3/packets/parsers/limits_response_parser.rb

+18-7
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,23 @@
2020
# This file may also be used under the terms of a commercial license
2121
# if purchased from OpenC3, Inc.
2222

23+
require 'openc3/utilities/python_proxy'
24+
2325
module OpenC3
2426
class LimitsResponseParser
2527
# @param parser [ConfigParser] Configuration parser
2628
# @param item [Packet] The current item
2729
# @param cmd_or_tlm [String] Whether this is a command or telemetry packet
28-
def self.parse(parser, item, cmd_or_tlm)
29-
parser = LimitsResponseParser.new(parser)
30+
def self.parse(parser, item, cmd_or_tlm, language = 'ruby')
31+
parser = LimitsResponseParser.new(parser, language)
3032
parser.verify_parameters(cmd_or_tlm)
3133
parser.create_limits_response(item)
3234
end
3335

3436
# @param parser [ConfigParser] Configuration parser
35-
def initialize(parser)
37+
def initialize(parser, language = 'ruby')
3638
@parser = parser
39+
@language = language
3740
end
3841

3942
# @param cmd_or_tlm [String] Whether this is a command or telemetry packet
@@ -48,12 +51,20 @@ def verify_parameters(cmd_or_tlm)
4851

4952
# @param item [PacketItem] The item the limits response should be added to
5053
def create_limits_response(item)
51-
klass = OpenC3.require_class(@parser.parameters[0])
54+
if @language == 'ruby'
55+
klass = OpenC3.require_class(@parser.parameters[0])
5256

53-
if @parser.parameters[1]
54-
item.limits.response = klass.new(*@parser.parameters[1..(@parser.parameters.length - 1)])
57+
if @parser.parameters[1]
58+
item.limits.response = klass.new(*@parser.parameters[1..(@parser.parameters.length - 1)])
59+
else
60+
item.limits.response = klass.new
61+
end
5562
else
56-
item.limits.response = klass.new
63+
if @parser.parameters[1]
64+
item.limits.response = PythonProxy.new('LimitsResponse', @parser.parameters[0], *@parser.parameters[1..(@parser.parameters.length - 1)])
65+
else
66+
item.limits.response = PythonProxy.new('LimitsResponse', @parser.parameters[0], [])
67+
end
5768
end
5869
rescue Exception => err
5970
raise @parser.error(err, @usage)

openc3/lib/openc3/packets/parsers/processor_parser.rb

+19-10
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,23 @@
2121
# if purchased from OpenC3, Inc.
2222

2323
require 'openc3/processors'
24+
require 'openc3/utilities/python_proxy'
2425

2526
module OpenC3
2627
class ProcessorParser
2728
# @param parser [ConfigParser] Configuration parser
2829
# @param packet [Packet] The current packet
2930
# @param cmd_or_tlm [String] Whether this is a command or telemetry packet
30-
def self.parse(parser, packet, cmd_or_tlm)
31-
parser = ProcessorParser.new(parser)
31+
def self.parse(parser, packet, cmd_or_tlm, language = 'ruby')
32+
parser = ProcessorParser.new(parser, language)
3233
parser.verify_parameters(cmd_or_tlm)
3334
parser.create_processor(packet)
3435
end
3536

3637
# @param parser [ConfigParser] Configuration parser
37-
def initialize(parser)
38+
def initialize(parser, language = 'ruby')
3839
@parser = parser
40+
@language = language
3941
end
4042

4143
# @param cmd_or_tlm [String] Whether this is a command or telemetry packet
@@ -50,16 +52,23 @@ def verify_parameters(cmd_or_tlm)
5052

5153
# @param packet [Packet] The packet the processor should be added to
5254
def create_processor(packet)
53-
# require should be performed in target.txt
54-
klass = OpenC3.require_class(@parser.parameters[1])
55+
if @language == 'ruby'
56+
# require should be performed in target.txt
57+
klass = OpenC3.require_class(@parser.parameters[1])
5558

56-
if @parser.parameters[2]
57-
processor = klass.new(*@parser.parameters[2..(@parser.parameters.length - 1)])
59+
if @parser.parameters[2]
60+
processor = klass.new(*@parser.parameters[2..(@parser.parameters.length - 1)])
61+
else
62+
processor = klass.new
63+
end
64+
raise ArgumentError, "processor must be a OpenC3::Processor but is a #{processor.class}" unless OpenC3::Processor === processor
5865
else
59-
processor = klass.new
66+
if @parser.parameters[2]
67+
processor = PythonProxy.new('Processor', @parser.parameters[1], *@parser.parameters[2..(@parser.parameters.length - 1)])
68+
else
69+
processor = PythonProxy.new('Processor', @parser.parameters[1], [])
70+
end
6071
end
61-
raise ArgumentError, "processor must be a OpenC3::Processor but is a #{processor.class}" unless OpenC3::Processor === processor
62-
6372
processor.name = get_processor_name()
6473
packet.processors[processor.name] = processor
6574
rescue Exception => err

openc3/lib/openc3/system/system.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def add_target(target_name, target_config_dir)
179179
@targets[target.name] = target
180180
errors = [] # Store all errors processing the cmd_tlm files
181181
target.cmd_tlm_files.each do |cmd_tlm_file|
182-
@packet_config.process_file(cmd_tlm_file, target.name)
182+
@packet_config.process_file(cmd_tlm_file, target.name, target.language)
183183
rescue Exception => error
184184
errors << "Error processing #{cmd_tlm_file}:\n#{error.message}"
185185
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# encoding: ascii-8bit
2+
3+
# Copyright 2024 OpenC3, Inc.
4+
# All Rights Reserved.
5+
#
6+
# This program is free software; you can modify and/or redistribute it
7+
# under the terms of the GNU Affero General Public License
8+
# as published by the Free Software Foundation; version 3 with
9+
# attribution addendums as found in the LICENSE.txt
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU Affero General Public License for more details.
15+
#
16+
# This file may also be used under the terms of a commercial license
17+
# if purchased from OpenC3, Inc.
18+
19+
# TODO: Delegate to actual Python to verify that classes exist
20+
# and to get proper data from them like converted_type
21+
22+
module OpenC3
23+
class PythonProxy
24+
attr_accessor :name
25+
attr_accessor :args
26+
27+
def initialize(type, class_name, *params)
28+
@type = type
29+
@class_name = class_name
30+
@params = params
31+
@args = params
32+
@name = nil
33+
end
34+
35+
def class
36+
return @class_name
37+
end
38+
39+
def as_json(*args, **kw_args)
40+
case @type
41+
when "Processor"
42+
return { 'name' => @name, 'class' => @class_name, 'params' => @params }
43+
when "Conversion"
44+
return { 'class' => @class_name, 'params' => @params }
45+
when "LimitsResponse"
46+
return { "class" => @class_name, 'params' => @params }
47+
else
48+
raise "Unknown PythonProxy type: #{@type}"
49+
end
50+
end
51+
end
52+
end

0 commit comments

Comments
 (0)