====== 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 =====
* Consider the following diagram and then the subsequent discussion of each of the components.
{{:technical:mqtt-implementation.png|}}
==== 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 ====
* The API Gateway is a Node.js based web service that acts as a middle man.
* The MQTT implementation uses a command and response principle.
* The API Gateway
* Receive instructions from CakePHP and translate them to MQTT **publish** actions (Command) which are published to the Mosquitto MQTT Broker.
* Subscribe to MQTT topics (Response) on the Mosquitto MQTT Broker which will get input from the mesh nodes and access points and translate them to HTTP/HTTPS based API calls to CakePHP.
==== 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 =====
* There are two components of the MQTT setup that needs to be configured
* Configuration settings for mesh nodes and access points (MESHdesk and APdesk)
* Configuration settings for the MQTT API Gateway.
===== Looking at the code =====
==== Command -> CakePHP Controller ====
* Lets look at the **/var/www/html/cake3/rd_cake/src/Controller/NodeActionsController.php** file.
* When an action is added to a node and MQTT is enabled on the system this code is executed:
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 ====
* Here is a snippet in **/etc/MESHdesk/mqtt.lua** which shows what it will do when a message is published from the API Gateway.
* This is the command which it will then respond to.
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')
));
}
}