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
1 change: 0 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ jobs:
matrix:
os:
- ubuntu-22.04 # jammy
- windows-2022
ruby:
- '3.1'
- '3.0'
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ mkmf.log
/.ruby-version
/opcua_client-*.gem
/tmp
/tools/server/server
/tools/server/opcua-server
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
48 changes: 40 additions & 8 deletions ext/opcua_client/opcua_client.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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);
Copy link

Copilot AI Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memory leak potential: if UA_String_copy fails, the allocated memory from UA_malloc will not be freed. Consider checking the return value of UA_String_copy and freeing the allocated memory on failure.

Suggested change
UA_String_copy(&newValue, (UA_String*)values[i].data);
UA_StatusCode copyStatus = UA_String_copy(&newValue, (UA_String*)values[i].data);
if (copyStatus != UA_STATUSCODE_GOOD) {
UA_free(values[i].data);
return raise_ua_status_error(copyStatus);
}

Copilot uses AI. Check for mistakes.
values[i].type = &UA_TYPES[uaType];
} else {
rb_raise(cError, "Unsupported type");
}
Expand Down Expand Up @@ -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);
Copy link

Copilot AI Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memory leak potential: same issue as in the multi-write function. If UA_String_copy fails, the allocated memory from UA_malloc will not be freed.

Suggested change
UA_String_copy(&newValue, (UA_String*)value.data);
UA_StatusCode copyStatus = UA_String_copy(&newValue, (UA_String*)value.data);
if (copyStatus != UA_STATUSCODE_GOOD) {
UA_free(value.data);
rb_raise(cError, "UA_String_copy failed");
}

Copilot uses AI. Check for mistakes.
value.type = &UA_TYPES[UA_TYPES_STRING];
} else {
rb_raise(cError, "Unsupported type");
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);

Expand Down
124 changes: 124 additions & 0 deletions spec/core_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions tools/server/makefile
Original file line number Diff line number Diff line change
@@ -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
53 changes: 43 additions & 10 deletions tools/server/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Expand All @@ -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);

Expand All @@ -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) {
Copy link

Copilot AI Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default parameter value should be 0.0f instead of 0 to maintain type consistency with UA_Float.

Suggested change
static void addVariableFloat(UA_Server *server, UA_Int16 nsId, int type, const char *variable, UA_Float defaultValue = 0) {
static void addVariableFloat(UA_Server *server, UA_Int16 nsId, int type, const char *variable, UA_Float defaultValue = 0.0f) {

Copilot uses AI. Check for mistakes.
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;
Expand All @@ -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) {
Expand Down