Some support for modbus via the Job Engine or PC versions of DDE has been documented here:
https://github.com/HaddingtonDynamics/Dexter/wiki/Dexter-ModBus
This allows jobs to act as modbus clients and make requests of other modbus devices which include a server function. It is also possible to start a job, start a modbus server via the library in that job, and then act as a modbus server as long as the job is running.
It may be more reliable and simpler to include a modbus server function as a part of the node server which already provides a websocket proxy and web server:
https://github.com/HaddingtonDynamics/Dexter/wiki/nodejs-webserver
The primary advantage is that this is generally always running as long as the robot is on, and since it typically isn't changed, it can (hopefully) be more reliable.
The expected use case is that a modbus device could send a request to Dexter and know that it would be recieved. The question is: What to do with that request?
Dexter doesn't have relays or coils or even individual actuators other than it's joints. However, it can run jobs. One idea is to map setCoil requests to starting jobs. E.g. setCoil 1 on would start /srv/samba/share/dde_app/modbus1.dde. The same request with "off" instead of "on" would kill that job. ReadCoil 1 would return the status of the job: true for running, false if not running.
While the job is running, it could output data back to the modbus system via a special version of the "out" command and so set holding register values for other modbus devices to read.
Of course, the job could also send modbus commands to other devices directly.
We can imagine a new Dexter owner who wants to integrate the arm into an existing assembly line. They might use DDE's record panel to put the robot into follow mode, record a simple movement that picks up a box and sets it down out of the way, and then save that job to the robot as "/srv/samba/share/dde_apps/modbus1.dde" and then program the line to send a setCoil 1 true when the box is ready. Done, and basically zero programming.
But then, maybe they need to send back a message to the line to tell it that it can send Dexter the next box. This code can be added to the job with some programming as per the existing example, or with a single "out" command a register can be set and the line can be programmed to check that register on a regular interval. (It is probably possible to make the sending of a simple modbus command from a job much easier, but that would have to be done in DDE / Job Engine)
To allow Dexter to always response to ModBus commands from another source including the basic functionality of setting and getting registers, and starting a job, then reading back values we can add some code to the built in web server. To use this, you must first SSH into Dexter and then
cd /srv/samba/share
node install modbus-serial
and then add the following to /srv/samba/share/www/httpd.js
// ModBus client server
const ModbusRTU = require("modbus-serial");
var modbus_reg = []
function modbus_startjob(job_name) {
console.log(job_name)
let jobfile = DDE_APPS_FOLDER + job_name + ".dde"
let job_process = get_job_name_to_process(job_name)
if(!job_process){
console.log("spawning " + jobfile)
//https://nodejs.org/api/child_process.html
//https://blog.cloudboost.io/node-js-child-process-spawn-178eaaf8e1f9
//a jobfile than ends in "/keep_alive" is handled specially in core/index.js
job_process = spawn('node',
["core define_and_start_job " + jobfile],
{cwd: DDE_INSTALL_FOLDER, shell: true}
)
set_job_name_to_process(job_name, job_process)
console.log("Spawned " + DDE_APPS_FOLDER + job_name + ".dde as process id " + job_process)
job_process.stdout.on('data', function(data) {
console.log("\n\n" + job_name + ">'" + data + "'\n")
let data_str = data.toString()
if (data_str.substr(0,7) == "modbus:") { //expecting 'modbus: 4, 123' or something like that
[addr, value] = data_str.substr(7).split(",").map(x => parseInt(x) || 0)
modbus_reg[addr] = value
//TODO: Change this to something that allows multiple values to be set in one out.
}
})
job_process.stderr.on('data', function(data) {
console.log("\n\n" + job_name + "!>'" + data + "'\n")
//remove_job_name_to_process(job_name) //error doesn't mean end.
})
job_process.on('close', function(code) {
console.log("\n\nJob: " + job_name + ".dde closed with code: " + code)
//if(code !== 0){ } //who do we tell if a job crashed?
remove_job_name_to_process(job_name)
})
}
else {
console.log("\n" + job_name + " already running as process " + job_process)
} //finished with !job_process
}
var vector = {
//TODO: Figure out what to return as inputs.
// Possible: Values from a file?
// e.g. modbus.json has an array where jobs can store data to be read out here.
// maybe that is the modbus_reg array as a json file?
getInputRegister: function(addr) { //doesn't get triggered by QModMaster for some reason.
//This does work mbpoll -1 -p 8502 -r 2 -t 3 192.168.0.142
console.log("read input", addr)
return addr; //just sample data
},
getMultipleInputRegisters: function(startAddr, length) {
console.log("read inputs from", startAddr, "for", length);
var values = [];
for (var i = startAddr; i < length; i++) {
values[i] = startAddr + i; //just sample return data
}
return values;
},
getHoldingRegister: function(addr) {
let value = modbus_reg[addr] || 0
console.log("read register", addr, "is", value)
return value
},
getMultipleHoldingRegisters: function(startAddr, length) {
console.log("read registers from", startAddr, "for", length);
let values = []
for (var i = 0; i < length; i++) {
values[i] = modbus_reg[i] || 0
}
return values
},
setRegister: function(addr, value) {
console.log("set register", addr, "to", value)
modbus_reg[addr] = value
return
},
getCoil: function(addr) { //return 0 or 1 only.
let value = ((addr % 2) === 0) //just sample return data
console.log("read coil", addr, "is", value)
return value
//TODO Return the status of the job modbuscoil<addr>.dde
// e.g. 1 if it's running, 0 if it's not.
},
setCoil: function(addr, value) { //gets true or false as a value.
console.log("set coil", addr, " ", value)
if (value) { modbus_startjob("modbus" + addr) }
else { console.log("stop") }
//TODO Start or kill job modbuscoil<addr>.dde depending on <value>
// Maybe pass in with modbus_reg as a user_data? or they can access the file?
return;
},
readDeviceIdentification: function(addr) {
return {
0x00: "HaddingtonDynamics",
0x01: "Dexter",
0x02: "1.1",
0x05: "HDI",
0x97: "MyExtendedObject1",
0xAB: "MyExtendedObject2"
};
}
};
// set the server to answer for modbus requests
console.log("ModbusTCP listening on modbus://0.0.0.0:8502");
var serverTCP = new ModbusRTU.ServerTCP(vector, { host: "0.0.0.0", port: 8502, debug: true, unitID: 1 });
serverTCP.on("initialized", function() {
console.log("initialized");
});
serverTCP.on("socketError", function(err) {
console.error(err);
serverTCP.close(closed);
});
function closed() {
console.log("server closed");
}
The following sample job sets register 1 to 123 (and could, of course, move the robot or do whatever) and is activated when Dexter is told to set coil 1 to true.
`/srv/samba/share/dde_apps/modbus1.dde
Notes:
https://sourceforge.net/projects/qmodmaster/ is a wonderful tool for Windows, GUI, easy to use, clear interface. Hit the CAT5 icon, enter Dexters IP and port 8502, then select unit ID 1, select the address, length, and on you go.
https://github.com/epsilonrt/mbpoll provides modbus client testing tool for Ubuntu. e.g. mbpoll -1 -p 8502 -t 0 192.168.1.142 connects to the Dexter at .142 on the local network, via port 8502, and reads coil 0. To set a coil, add a 0 or 1 at the end of the command. To read coil 1, the option is -r 2 unless you include the -0 option, then it's -r 1. e.g. to read coil 2, either of these provide the same response:
Some support for modbus via the Job Engine or PC versions of DDE has been documented here: https://github.com/HaddingtonDynamics/Dexter/wiki/Dexter-ModBus This allows jobs to act as modbus clients and make requests of other modbus devices which include a server function. It is also possible to start a job, start a modbus server via the library in that job, and then act as a modbus server as long as the job is running.
It may be more reliable and simpler to include a modbus server function as a part of the node server which already provides a websocket proxy and web server: https://github.com/HaddingtonDynamics/Dexter/wiki/nodejs-webserver The primary advantage is that this is generally always running as long as the robot is on, and since it typically isn't changed, it can (hopefully) be more reliable.
The expected use case is that a modbus device could send a request to Dexter and know that it would be recieved. The question is: What to do with that request?
Dexter doesn't have relays or coils or even individual actuators other than it's joints. However, it can run jobs. One idea is to map setCoil requests to starting jobs. E.g. setCoil 1 on would start /srv/samba/share/dde_app/modbus1.dde. The same request with "off" instead of "on" would kill that job. ReadCoil 1 would return the status of the job: true for running, false if not running.
While the job is running, it could output data back to the modbus system via a special version of the "out" command and so set holding register values for other modbus devices to read.
Of course, the job could also send modbus commands to other devices directly.
We can imagine a new Dexter owner who wants to integrate the arm into an existing assembly line. They might use DDE's record panel to put the robot into follow mode, record a simple movement that picks up a box and sets it down out of the way, and then save that job to the robot as "/srv/samba/share/dde_apps/modbus1.dde" and then program the line to send a setCoil 1 true when the box is ready. Done, and basically zero programming.
But then, maybe they need to send back a message to the line to tell it that it can send Dexter the next box. This code can be added to the job with some programming as per the existing example, or with a single "out" command a register can be set and the line can be programmed to check that register on a regular interval. (It is probably possible to make the sending of a simple modbus command from a job much easier, but that would have to be done in DDE / Job Engine)
To allow Dexter to always response to ModBus commands from another source including the basic functionality of setting and getting registers, and starting a job, then reading back values we can add some code to the built in web server. To use this, you must first SSH into Dexter and then
and then add the following to /srv/samba/share/www/httpd.js
The following sample job sets register 1 to 123 (and could, of course, move the robot or do whatever) and is activated when Dexter is told to set coil 1 to true. `/srv/samba/share/dde_apps/modbus1.dde
Or see the complete file here:
https://github.com/HaddingtonDynamics/Dexter/blob/Stable_Conedrive/Firmware/www/httpd.js
Notes: https://sourceforge.net/projects/qmodmaster/ is a wonderful tool for Windows, GUI, easy to use, clear interface. Hit the CAT5 icon, enter Dexters IP and port 8502, then select unit ID 1, select the address, length, and on you go.
https://github.com/epsilonrt/mbpoll provides modbus client testing tool for Ubuntu. e.g.
mbpoll -1 -p 8502 -t 0 192.168.1.142
connects to the Dexter at .142 on the local network, via port 8502, and reads coil 0. To set a coil, add a 0 or 1 at the end of the command. To read coil 1, the option is-r 2
unless you include the-0
option, then it's-r 1
. e.g. to read coil 2, either of these provide the same response:To set coil 1, use
mbpoll -0 -1 -p 8502 -r 1 -t 0 192.168.1.142 1