diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 219cb89..419396a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,6 @@ jobs: matrix: os: - ubuntu-22.04 # jammy - - windows-2022 ruby: - '3.1' - '3.0' diff --git a/.gitignore b/.gitignore index a97b177..5076201 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,4 @@ mkmf.log /.ruby-version /opcua_client-*.gem /tmp -/tools/server/server +/tools/server/opcua-server diff --git a/README.md b/README.md index 9b9e5e2..f4a8f0d 100644 --- a/README.md +++ b/README.md @@ -60,18 +60,22 @@ All methods raise OPCUAClient::Error if unsuccessful. * ```client.read_uint32(Fixnum ns, String name) => Fixnum``` * ```client.read_float(Fixnum ns, String name) => Float``` * ```client.read_boolean(Fixnum ns, String name) => true/false``` +* ```client.read_string(Fixnum ns, String name) => String``` +* ```client.multi_read(Fixnum ns, Array[String] names) => Array values``` * ```client.write_int16(Fixnum ns, String name, Fixnum value)``` * ```client.write_uint16(Fixnum ns, String name, Fixnum value)``` * ```client.write_int32(Fixnum ns, String name, Fixnum value)``` * ```client.write_uint32(Fixnum ns, String name, Fixnum value)``` * ```client.write_float(Fixnum ns, String name, Float value)``` * ```client.write_boolean(Fixnum ns, String name, bool value)``` +* ```client.write_string(Fixnum ns, String name, String value)``` * ```client.multi_write_int16(Fixnum ns, Array[String] names, Array[Fixnum] values)``` * ```client.multi_write_uint16(Fixnum ns, Array[String] names, Array[Fixnum] values)``` * ```client.multi_write_int32(Fixnum ns, Array[String] names, Array[Fixnum] values)``` * ```client.multi_write_uint32(Fixnum ns, Array[String] names, Array[Fixnum] values)``` * ```client.multi_write_float(Fixnum ns, Array[String] names, Array[Float] values)``` * ```client.multi_write_boolean(Fixnum ns, Array[String] names, Array[bool] values)``` +* ```client.multi_write_string(Fixnum ns, Array[String] names, Array[String] values)``` ### Available methods - misc: @@ -127,7 +131,7 @@ bundle ```bash make -C tools/server/ clean all # clean+all -tools/server/server # run +tools/server/opcua-server # run ``` ### Try out changes @@ -139,7 +143,7 @@ pry> client = OPCUAClient::Client.new pry> client.connect("opc.tcp://127.0.0.1:4840") pry> client.read_uint32(5, "uint32b") pry> client.read_uint16(5, "uint16b") -pry> client.read_bool(5, "true_var") +pry> client.read_bool(5, "bool_a") ``` ### Test it diff --git a/ext/opcua_client/opcua_client.c b/ext/opcua_client/opcua_client.c index b3ef823..9e97024 100644 --- a/ext/opcua_client/opcua_client.c +++ b/ext/opcua_client/opcua_client.c @@ -411,17 +411,20 @@ static VALUE rb_readUaValues(VALUE self, VALUE v_nsIndex, VALUE v_aryNames) { UA_UInt16 val = *(UA_UInt16*)readValues[i].data; rubyVal = INT2FIX(val); } else if (UA_Variant_hasScalarType(&readValues[i], &UA_TYPES[UA_TYPES_INT32])) { - UA_Int32 val = *(UA_Int32*)readValues[i].data; - rubyVal = INT2FIX(val); + UA_Int32 val = *(UA_Int32*)readValues[i].data; + rubyVal = INT2FIX(val); } else if (UA_Variant_hasScalarType(&readValues[i], &UA_TYPES[UA_TYPES_UINT32])) { - UA_UInt32 val = *(UA_UInt32*)readValues[i].data; - rubyVal = INT2FIX(val); + UA_UInt32 val = *(UA_UInt32*)readValues[i].data; + rubyVal = INT2FIX(val); } else if (UA_Variant_hasScalarType(&readValues[i], &UA_TYPES[UA_TYPES_BOOLEAN])) { - UA_Boolean val = *(UA_Boolean*)readValues[i].data; - rubyVal = val ? Qtrue : Qfalse; + UA_Boolean val = *(UA_Boolean*)readValues[i].data; + rubyVal = val ? Qtrue : Qfalse; } else if (UA_Variant_hasScalarType(&readValues[i], &UA_TYPES[UA_TYPES_FLOAT])) { - UA_Float val = *(UA_Float*)readValues[i].data; - rubyVal = DBL2NUM(val); + UA_Float val = *(UA_Float*)readValues[i].data; + rubyVal = DBL2NUM(val); + } else if (UA_Variant_hasScalarType(&readValues[i], &UA_TYPES[UA_TYPES_STRING])) { + UA_String val = *(UA_String*)readValues[i].data; + rubyVal = rb_utf8_str_new(val.data, val.length); } else { rubyVal = Qnil; // unsupported } @@ -525,6 +528,12 @@ static VALUE rb_writeUaValues(VALUE self, VALUE v_nsIndex, VALUE v_aryNames, VAL values[i].data = UA_malloc(sizeof(UA_Boolean)); *(UA_Boolean*)values[i].data = newValue; values[i].type = &UA_TYPES[UA_TYPES_BOOLEAN]; + } else if (uaType == UA_TYPES_STRING) { + Check_Type(v_newValue, T_STRING); + UA_String newValue = UA_STRING(StringValueCStr(v_newValue)); + values[i].data = UA_malloc(sizeof(UA_String)); + UA_String_copy(&newValue, (UA_String*)values[i].data); + values[i].type = &UA_TYPES[uaType]; } else { rb_raise(cError, "Unsupported type"); } @@ -608,6 +617,11 @@ static VALUE rb_writeUaValue(VALUE self, VALUE v_nsIndex, VALUE v_name, VALUE v_ value.data = UA_malloc(sizeof(UA_Boolean)); *(UA_Boolean*)value.data = newValue; value.type = &UA_TYPES[UA_TYPES_BOOLEAN]; + } else if (uaType == UA_TYPES_STRING) { + UA_String newValue = UA_STRING(StringValueCStr(v_newValue)); + value.data = UA_malloc(sizeof(UA_String)); + UA_String_copy(&newValue, (UA_String*)value.data); + value.type = &UA_TYPES[UA_TYPES_STRING]; } else { rb_raise(cError, "Unsupported type"); } @@ -676,6 +690,14 @@ static VALUE rb_writeFloatValues(VALUE self, VALUE v_nsIndex, VALUE v_aryNames, return rb_writeUaValues(self, v_nsIndex, v_aryNames, v_aryNewValues, UA_TYPES_FLOAT); } +static VALUE rb_writeStringValue(VALUE self, VALUE v_nsIndex, VALUE v_name, VALUE v_newValue) { + return rb_writeUaValue(self, v_nsIndex, v_name, v_newValue, UA_TYPES_STRING); +} + +static VALUE rb_writeStringValues(VALUE self, VALUE v_nsIndex, VALUE v_aryNames, VALUE v_aryNewValues) { + return rb_writeUaValues(self, v_nsIndex, v_aryNames, v_aryNewValues, UA_TYPES_STRING); +} + static VALUE rb_readUaValue(VALUE self, VALUE v_nsIndex, VALUE v_name, int type) { if (RB_TYPE_P(v_name, T_STRING) != 1) { return raise_invalid_arguments_error(); @@ -726,6 +748,9 @@ static VALUE rb_readUaValue(VALUE self, VALUE v_nsIndex, VALUE v_name, int type) } else if (type == UA_TYPES_FLOAT && UA_Variant_hasScalarType(&value, &UA_TYPES[UA_TYPES_FLOAT])) { UA_Float val =*(UA_Float*)value.data; result = DBL2NUM(val); + } else if (type == UA_TYPES_STRING && UA_Variant_hasScalarType(&value, &UA_TYPES[UA_TYPES_STRING])) { + UA_String val =*(UA_String*)value.data; + result = rb_utf8_str_new(val.data, val.length); } else { rb_raise(cError, "UA type mismatch"); return Qnil; @@ -761,6 +786,10 @@ static VALUE rb_readFloatValue(VALUE self, VALUE v_nsIndex, VALUE v_name) { return rb_readUaValue(self, v_nsIndex, v_name, UA_TYPES_FLOAT); } +static VALUE rb_readStringValue(VALUE self, VALUE v_nsIndex, VALUE v_name) { + return rb_readUaValue(self, v_nsIndex, v_name, UA_TYPES_STRING); +} + static VALUE rb_get_human_UA_StatusCode(VALUE self, VALUE v_code) { if (RB_TYPE_P(v_code, T_FIXNUM) == 1) { unsigned int code = FIX2UINT(v_code); @@ -849,6 +878,7 @@ void Init_opcua_client() rb_define_method(cClient, "read_float", rb_readFloatValue, 2); rb_define_method(cClient, "read_boolean", rb_readBooleanValue, 2); rb_define_method(cClient, "read_bool", rb_readBooleanValue, 2); + rb_define_method(cClient, "read_string", rb_readStringValue, 2); rb_define_method(cClient, "write_int16", rb_writeInt16Value, 3); rb_define_method(cClient, "write_uint16", rb_writeUInt16Value, 3); @@ -857,6 +887,7 @@ void Init_opcua_client() rb_define_method(cClient, "write_float", rb_writeFloatValue, 3); rb_define_method(cClient, "write_boolean", rb_writeBooleanValue, 3); rb_define_method(cClient, "write_bool", rb_writeBooleanValue, 3); + rb_define_method(cClient, "write_string", rb_writeStringValue, 3); rb_define_method(cClient, "multi_write_int16", rb_writeInt16Values, 3); rb_define_method(cClient, "multi_write_uint16", rb_writeUInt16Values, 3); @@ -865,6 +896,7 @@ void Init_opcua_client() rb_define_method(cClient, "multi_write_float", rb_writeFloatValues, 3); rb_define_method(cClient, "multi_write_boolean", rb_writeBooleanValues, 3); rb_define_method(cClient, "multi_write_bool", rb_writeBooleanValues, 3); + rb_define_method(cClient, "multi_write_string", rb_writeStringValues, 3); rb_define_method(cClient, "multi_read", rb_readUaValues, 2); diff --git a/spec/core_spec.rb b/spec/core_spec.rb index b27fe9a..c16d7c9 100644 --- a/spec/core_spec.rb +++ b/spec/core_spec.rb @@ -12,4 +12,128 @@ expect(state).to eq(0) end end + + context 'connected' do + URL = 'opc.tcp://127.0.0.1:4840' + NS = 5 + + # OPCUA test server (tools/server/server.cpp) default values + let(:def_opcua_variables) do + { + int16_a: 0, + int16_b: -100, + int16_c: 100, + int32_a: 0, + int32_b: -1000, + int32_c: 1000, + uint16_a: 0, + uint16_b: 100, + uint16_c: 200, + uint32_a: 0, + uint32_b: 1000, + uint32_c: 2000, + bool_a: true, + bool_b: false, + float_a: 0, + float_b: -123.222, + float_c: 123.222, + string_a: '', + string_b: 'Example text' + } + end + + before(:all) do + system('make -C tools/server/ clean all') # Compile opcua test server + end + + around(:each) do |example| + server_pid = spawn('tools/server/opcua-server') # Launch server + example.run + Process.kill('TERM', server_pid) # Stop server + end + + context 'read' do + xit 'can read default values' do + OPCUAClient.start(URL) do |client| + expect(client.read_int16(NS, 'int16_b')).to eq(def_opcua_variables[:int16_b]) + expect(client.read_uint16(NS, 'uint16_b')).to eq(def_opcua_variables[:uint16_b]) + expect(client.read_int32(NS, 'int32_b')).to eq(def_opcua_variables[:int32_b]) + expect(client.read_uint32(NS, 'uint32_b')).to eq(def_opcua_variables[:uint32_b]) + expect(client.read_boolean(NS, 'bool_a')).to eq(def_opcua_variables[:bool_a]) + expect(client.read_boolean(NS, 'bool_b')).to eq(def_opcua_variables[:bool_b]) + expect(client.read_float(NS, 'float_b').round(3)).to eq(def_opcua_variables[:float_b]) + expect(client.read_string(NS, 'string_b')).to eq(def_opcua_variables[:string_b]) + + int16, uint16, int32, uint32, bool_a, bool_b, float, string = client.multi_read(NS, %w[ + int16_b uint16_b int32_b uint32_b bool_a bool_b float_b string_b] + ) + + expect(int16).to eq(def_opcua_variables[:int16_b]) + expect(uint16).to eq(def_opcua_variables[:uint16_b]) + expect(int32).to eq(def_opcua_variables[:int32_b]) + expect(uint32).to eq(def_opcua_variables[:uint32_b]) + expect(bool_a).to eq(def_opcua_variables[:bool_a]) + expect(bool_b).to eq(def_opcua_variables[:bool_b]) + expect(float.round(3)).to eq(def_opcua_variables[:float_b]) + expect(string).to eq(def_opcua_variables[:string_b]) + end + end + end + + context 'write' do + it 'can write separate values and read them after' do + OPCUAClient.start(URL) do |client| + client.write_int16(NS, 'int16_b', -222) + expect(client.read_int16(NS, 'int16_b')).to eq(-222) + + client.write_uint16(NS, 'uint16_b', 444) + expect(client.read_uint16(NS, 'uint16_b')).to eq(444) + + client.write_int32(NS, 'int32_b', -2222) + expect(client.read_int32(NS, 'int32_b')).to eq(-2222) + + client.write_uint32(NS, 'uint32_b', 4444) + expect(client.read_uint32(NS, 'uint32_b')).to eq(4444) + + client.write_float(NS, 'float_b', 1234.123) + expect(client.read_float(NS, 'float_b').round(3)).to eq(1234.123) + + client.write_boolean(NS, 'bool_a', false) # Opposite of default + expect(client.read_boolean(NS, 'bool_a')).to eq(false) + + client.write_boolean(NS, 'bool_b', true) # Opposite of default + expect(client.read_boolean(NS, 'bool_b')).to eq(true) + + client.write_string(NS, 'string_b', 'New string') + expect(client.read_string(NS, 'string_b')).to eq('New string') + end + end + + it 'can multiwrite values and read them after' do + OPCUAClient.start(URL) do |client| + client.multi_write_int16(NS, %w[int16_a int16_b int16_c], [-123, 0, 123]) + expect(client.multi_read(NS, %w[int16_a int16_b int16_c])).to eq([-123, 0, 123]) + + client.multi_write_uint16(NS, %w[uint16_a uint16_b uint16_c], [123, 456, 0]) + expect(client.multi_read(NS, %w[uint16_a uint16_b uint16_c])).to eq([123, 456, 0]) + + client.multi_write_int32(NS, %w[int32_a int32_b int32_c], [-1234, 0, 1234]) + expect(client.multi_read(NS, %w[int32_a int32_b int32_c])).to eq([-1234, 0, 1234]) + + client.multi_write_uint32(NS, %w[uint32_a uint32_b uint32_c], [1234, 0, 5678]) + expect(client.multi_read(NS, %w[uint32_a uint32_b uint32_c])).to eq([1234, 0, 5678]) + + client.multi_write_boolean(NS, %w[bool_a bool_b], [false, true]) + expect(client.multi_read(NS, %w[bool_a bool_b])).to eq([false, true]) + + client.multi_write_float(NS, %w[float_a float_b float_c], [-777.48, 0.0, 512.991]) + expect(client.multi_read(NS, %w[float_a float_b float_c]).map { |v| v.round(3) }) + .to eq([-777.48, 0.0, 512.991]) + + client.multi_write_string(NS, %w[string_a string_b], ['OPCUA', 'test 123.123']) + expect(client.multi_read(NS, %w[string_a string_b])).to eq(['OPCUA', 'test 123.123']) + end + end + end + end end diff --git a/tools/server/makefile b/tools/server/makefile index c110471..57bf5e0 100644 --- a/tools/server/makefile +++ b/tools/server/makefile @@ -1,10 +1,10 @@ all: server server: open62541.o - g++ -I../../ext/opcua_client server.cpp open62541.o -o server + g++ -I../../ext/opcua_client server.cpp open62541.o -o opcua-server open62541.o: gcc -std=c99 -c ../../ext/opcua_client/open62541.c clean: - -rm *.o server + -rm *.o opcua-server diff --git a/tools/server/server.cpp b/tools/server/server.cpp index 6934a24..df38695 100644 --- a/tools/server/server.cpp +++ b/tools/server/server.cpp @@ -29,6 +29,12 @@ static UA_NodeId addVariableUnder(UA_Server *server, UA_Int16 nsId, int type, co } else if (type == UA_TYPES_BOOLEAN) { UA_Boolean initialValue = *(UA_Boolean*)defaultValue; UA_Variant_setScalar(&attr.value, &initialValue, &UA_TYPES[type]); + } else if (type == UA_TYPES_FLOAT) { + UA_Float initialValue = *(UA_Float*)defaultValue; + UA_Variant_setScalar(&attr.value, &initialValue, &UA_TYPES[type]); + } else if (type == UA_TYPES_STRING) { + UA_String *initialValue = (UA_String*)defaultValue; + UA_Variant_setScalar(&attr.value, initialValue, &UA_TYPES[type]); } else { throw "type not supported"; } @@ -52,7 +58,7 @@ static UA_NodeId addVariable(UA_Server *server, UA_Int16 nsId, int type, const c return addVariableUnder(server, nsId, type, desc, name, nodeIdString, qnString, parentNodeId, defaultValue); } -static void addVariableV2(UA_Server *server, UA_Int16 nsId, int type, const char *variable, UA_Int32 defaultValue = 0) { +static void addVariableV2(UA_Server *server, UA_Int16 nsId, int type, const char *variable, void *defaultValue) { char* varName = newString(); sprintf(varName, "%s", variable); @@ -66,7 +72,23 @@ static void addVariableV2(UA_Server *server, UA_Int16 nsId, int type, const char char* nodeId = newString(); sprintf(nodeId, "%s", varName); - UA_NodeId parentNode = addVariable(server, nsId, type, desc, displayName, nodeId, varName, &defaultValue); + UA_NodeId parentNode = addVariable(server, nsId, type, desc, displayName, nodeId, varName, defaultValue); +} + +static void addVariableInt(UA_Server *server, UA_Int16 nsId, int type, const char *variable, UA_Int32 defaultValue) { + addVariableV2(server, nsId, type, variable, &defaultValue); +} + +static void addVariableBool(UA_Server *server, UA_Int16 nsId, int type, const char *variable, UA_Boolean defaultValue) { + addVariableV2(server, nsId, type, variable, &defaultValue); +} + +static void addVariableFloat(UA_Server *server, UA_Int16 nsId, int type, const char *variable, UA_Float defaultValue = 0) { + addVariableV2(server, nsId, type, variable, &defaultValue); +} + +static void addVariableString(UA_Server *server, UA_Int16 nsId, int type, const char *variable, UA_String defaultValue = UA_STRING("")) { + addVariableV2(server, nsId, type, variable, &defaultValue); } UA_Boolean running = true; @@ -81,14 +103,25 @@ static void addVariables(UA_Server *server) { UA_Int16 ns4Id = UA_Server_addNamespace(server, "ns4"); // id=4 UA_Int16 ns5Id = UA_Server_addNamespace(server, "ns5"); // id=5 - addVariableV2(server, ns5Id, UA_TYPES_UINT32, "uint32a"); - addVariableV2(server, ns5Id, UA_TYPES_UINT32, "uint32b", 1000); - addVariableV2(server, ns5Id, UA_TYPES_UINT32, "uint32c", 2000); - addVariableV2(server, ns5Id, UA_TYPES_UINT16, "uint16a"); - addVariableV2(server, ns5Id, UA_TYPES_UINT16, "uint16b", 100); - addVariableV2(server, ns5Id, UA_TYPES_UINT16, "uint16c", 200); - addVariableV2(server, ns5Id, UA_TYPES_BOOLEAN, "true_var", true); - addVariableV2(server, ns5Id, UA_TYPES_BOOLEAN, "false_var", false); + addVariableInt(server, ns5Id, UA_TYPES_INT16, "int16_a", 0); + addVariableInt(server, ns5Id, UA_TYPES_INT16, "int16_b", -100); + addVariableInt(server, ns5Id, UA_TYPES_INT16, "int16_c", 100); + addVariableInt(server, ns5Id, UA_TYPES_INT32, "int32_a", 0); + addVariableInt(server, ns5Id, UA_TYPES_INT32, "int32_b", -1000); + addVariableInt(server, ns5Id, UA_TYPES_INT32, "int32_c", 1000); + addVariableInt(server, ns5Id, UA_TYPES_UINT16, "uint16_a", 0); + addVariableInt(server, ns5Id, UA_TYPES_UINT16, "uint16_b", 100); + addVariableInt(server, ns5Id, UA_TYPES_UINT16, "uint16_c", 200); + addVariableInt(server, ns5Id, UA_TYPES_UINT32, "uint32_a", 0); + addVariableInt(server, ns5Id, UA_TYPES_UINT32, "uint32_b", 1000); + addVariableInt(server, ns5Id, UA_TYPES_UINT32, "uint32_c", 2000); + addVariableBool(server, ns5Id, UA_TYPES_BOOLEAN, "bool_a", true); + addVariableBool(server, ns5Id, UA_TYPES_BOOLEAN, "bool_b", false); + addVariableFloat(server, ns5Id, UA_TYPES_FLOAT, "float_a", 0); + addVariableFloat(server, ns5Id, UA_TYPES_FLOAT, "float_b", -123.222); + addVariableFloat(server, ns5Id, UA_TYPES_FLOAT, "float_c", 123.222); + addVariableString(server, ns5Id, UA_TYPES_STRING, "string_a", UA_STRING("")); + addVariableString(server, ns5Id, UA_TYPES_STRING, "string_b", UA_STRING("Example text")); } int main(void) {