Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add modbus server support to Dexter #84

Open
JamesNewton opened this issue May 26, 2020 · 1 comment
Open

Add modbus server support to Dexter #84

JamesNewton opened this issue May 26, 2020 · 1 comment
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@JamesNewton
Copy link
Collaborator

JamesNewton commented May 26, 2020

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

function modbus_setreg(reg, value) {
	console.log("modbus:", reg, ",", value)
	}

new Job({name: "modbus1", 
    do_list: [
        //Robot.out("modbus: 1, 123")
	function() {modbus_setreg(1, 123)}
        ]
    })
    

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:

>mbpoll -0 -1 -r 2 -t 0 -p 8502 192.168.1.142
>mbpoll -1 -r 3 -t 0 -p 8502 192.168.1.142
mbpoll 1.4-12 - FieldTalk(tm) Modbus(R) Master Simulator
Copyright © 2015-2019 Pascal JEAN, https://github.com/epsilonrt/mbpoll
This program comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under certain conditions; type 'mbpoll -w' for details.

Protocol configuration: Modbus TCP
Slave configuration...: address = [1]
                        start reference = 3, count = 1
Communication.........: 192.168.1.142, port 8502, t/o 1.00 s, poll rate 1000 ms
Data type.............: discrete output (coil)

-- Polling slave 1...
[3]: 	1

To set coil 1, use mbpoll -0 -1 -p 8502 -r 1 -t 0 192.168.1.142 1

@JamesNewton JamesNewton added the enhancement New feature or request label May 26, 2020
@JamesNewton
Copy link
Collaborator Author

This is being made part of the standard software in the "Stable_Conedrive" branch. Once that is released, we can close this issue.

@JamesNewton JamesNewton added the help wanted Extra attention is needed label Dec 2, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

1 participant