RADIUSdesk MQTT Implementation
Introduction
MESHdesk and APdesk traditionally makes use of a heartbeat system to communicate and report to the back-end.
We now also include a MQTT based implementation to allow real-time communication between the mesh nodes or access points and the back-end.
This implementation is used as a compliment to the heartbeat system, making it more robust while offering you added real-time communication.
The MQTT implementation is not compulsory in order to have a working deployment but it does offer a lot of advantages.
It is ideal for hardware that is used in a IOT environment where you need immediate execution of commands.
Architecture
ExtJS GUI
The ExtJS
GUI can be used to send commands to the mesh nodes and access points managed by MESHdesk and APdesk respectively.
The communication between ExtJS and the CakePHP application consists of REST-like
API calls using HTTP or HTTPS.
This means essentially that these actions can easily be automated or done with another
GUI should the need arise.
CakePHP
If MQTT support is enabled on the system and someone initiate a command execution action from the
GUI, the controller code handling this request will send the request to the
API Gateway.
This communication between the CakePHP controller and the
API Gateway also consist of REST-like
API calls using HTTP or HTTPS.
API Gateway
Mesh nodes and access points
The mesh nodes and access points communicate with the CakePHP back-end using HTTP/HTTPS to fetch its configuration and do reporting.
If the system has MQTT support enabled the mesh node or access point will configure itself to publish and subscribe to certain topics on the Mosquitto MQTT Broker.
The system works on a command and response principle.
The mesh node or access point
subscribe to a topic where it will expect
commands from the
API Gateway.
The mesh node or access point will
publish to a topic where the
API Gateway expect
responses.
The
API Gateway will
publish to a topic where the mesh node or access point expect
commands.
The
API Gateway will
subscribe to a topic where the mesh node or access points
publish their
responses.
Enable MQTT
Looking at the code
Command -> CakePHP Controller
if ($cfg['api_mqtt_enabled'] == "1"){
//Talk to MQTT Broker
$data = $this->_get_node_mac_mesh_id($formData['node_id']);
$payload = [
'mode' => 'mesh',
'node_id' => $formData['node_id'],
'mac' => strtoupper($data['mac']),
'mesh_id' => strtoupper($data['ssid']),
'cmd_id' => $entity->id,
'cmd' => $formData['command'],
'action' => $formData['action'],
];
if($this->_check_server($client, $cfg['api_gateway_url'], 5)){
try {
$client->request('POST', $cfg['api_gateway_url'] . '/rd/mesh/command', ['json' => ['message' => $payload]]);
} catch (\Exception $e) {
// Do Nothing
}
}
}
Command -> API Gateway
The
API call to the
API Gateway will execute this piece of code in the
/opt/Rdcore-API-Gateway/routes/rdmesh.js file
router.post('/mesh/command', function(req, res){
//var data = JSON.parse(req.body.message);
var data = req.body.message;
var message = JSON.stringify(data);
console.log(message);
client.publish('/RD/MESH/' + data.node_id + '/COMMAND', message);
console.log("Published command to Mesh node: " + data.mac + " MODE "+data.mode);
res.json(message);
});
Command -> mqtt.lua
client.ON_MESSAGE = function(mid, topic, payload)
-- Parse/Decode JSON Payload
local jsonStr = luci_json.parse(payload)
-- Check if message belongs to us (MAC Address)
Response -> mqtt.lua
Depending on the type of command the code in Lua will determine the correct response.
Here is a snippet in /etc/MESHdesk/mqtt.lua which respond to the execute action.
This is part of the code which are processing the command that the mesh node or access point received (inside the ON_MESSAGE event)
--Here depending on the value of jsonStr['action'] we will either just execute the command or execute and report the output
if(jsonStr['action'] == 'execute')then
print("MODE IS "..mode);
if(mode == 'mesh')then
message = luci_json.stringify({mode=mode,node_id=nodeId,mesh_id=meshId,mac=macAddr,cmd_id=cmdId,status='os_command'});
end
if(mode == 'ap')then
message = luci_json.stringify({mode=mode,ap_id=apId,mac=macAddr,cmd_id=cmdId,status='os_command'});
end
local cl_execute = mqtt.new();
cl_execute:login_set(MQTT_USER, MQTT_PASS)
cl_execute:connect(MQTT_HOST)
--Connected now publish
cl_execute.ON_CONNECT = function()
cl_execute:publish(cmdTopic, message, qos, retain);
end
--Done publishing - now execute command
cl_execute.ON_PUBLISH = function()
cl_execute:disconnect();
os.execute(jsonStr['cmd']);
end
cl_execute:loop_forever();
end
Response -> API Gateway
The
API Gateway subscribe to the topic which the mesh node or access point publishes to.
Here is a snippet from the
/opt/Rdcore-API-Gateway/routes/rdmesh.js file that execute some code when a message is received on that topic
default:
request.put({
url: mesh_controller + '/cake3/rd_cake/node-actions/node-command.json',
form: data
},
function (err, res, body) {
if (err) {
console.error('Error Occurred: ' + err);
}
console.log(body);
}
);
break;
Response -> CakePHP Controller
Finally we can look at the CakePHP code that process the response so our system know and can indicate the mesh node or access point did receive the instruction.
Lets look at the /var/www/html/cake3/rd_cake/src/Controller/NodeActionsController.php file.
//--This comes from the NodeJS API Gateway Application in response to 'execute' type node_actions
//--This comes from the NodeJS API Gateway Application in FIRST response to 'execute_and_reply' type node_actions
public function nodeCommand(){
if($this->request->is('put')){
$data = $this->request->data;
if((!empty($data['node_id']))||(!empty($data['ap_id']))){
// update command status to fetched
$model = 'NodeActions';
if($data['mode'] == 'ap'){
$model = 'ApActions';
}
$entity = $this->{$model}->find()->where(['id' => $data['cmd_id']])->first();
if($entity){
$entity->status = 'fetched';
$this->{$model}->save($entity);
}
$this->set(array(
'data' => $data,
'success' => true,
'_serialize' => array('data','success')
));
} else {
$this->set(array(
'message' => 'Node ID not found',
'success' => false,
'_serialize' => array('message','success')
));
}
} else {
$this->set(array(
'message' => 'Send only PUT request',
'success' => false,
'_serialize' => array('message','success')
));
}
}