Writing your first service
In this section we provide a simple but complete example of a custom Connhex Edge service written in Javascript
using the Node.js
framework.
In this example we will:
- read mocked battery charge and fuel level values
- build a SenML message containing our sensors readings
- build a heartbeat message to notify the Connhex Edge agent that our service is up and running
- start an update loop where our messages are built and published at scheduled intervals
- subscribe to a particular NATS subject handling incoming messages from our remote Connhex instance
tip
The NATS.js library used in this example can be installed using the command npm install nats
.
Folder structure
├── src
│ ├── index.js
│ ├── diagnostic.js
│ └── common.js
├── package.json
└── node_modules
index.js
const { connect } = require('nats');
const diagnosticService = require('./diagnostic');
(async () => {
const nc = await connect();
console.log('NATS connected');
const diagnosticServiceInstance = await diagnosticService.start(nc);
const handleExit = (signal) => {
console.log(`Received ${signal}.`);
nc.drain()
.then(() => diagnosticServiceInstance.dispose())
.then(() => process.exit(0));
};
process.on('SIGINT', handleExit);
process.on('SIGQUIT', handleExit);
process.on('SIGTERM', handleExit);
})();
diagnostic.js
const { startHeartbeat, publishMessage } = require('./common');
const serviceName = 'diagnostic';
// Implement your own logic to get current values.
const getBatteryCharge = () => Math.floor(Math.random() * (100 + 1));
const getFuelLevel = () => Math.floor(Math.random() * (100 + 1));
// Build a SenML message.
function buildDiagnosticMessage() {
const now = Date.now() / 1000;
return [
{
t: now,
m: {
name: 'battery-charge',
},
unit: '%EL',
value: getBatteryCharge(),
},
{
t: now,
m: {
name: 'fuel-level',
},
unit: '%FL',
value: getFuelLevel(),
},
];
}
async function sendDiagnostic(nc) {
const message = buildDiagnosticMessage();
await publishMessage(nc, serviceName, message);
console.log('Diagnostic published');
}
const startUpdateLoop = (nc, interval) => {
const intervalId = setInterval(() => sendDiagnostic(nc), interval);
return {
stop: () => clearInterval(intervalId),
};
};
exports.start = async function start(nc) {
console.log(`Starting ${serviceName} service...`);
const heartbeat = startHeartbeat(nc, serviceName);
// Send diagnostic message every 5 minutes.
const updateLoop = startUpdateLoop(nc, 5 * 60 * 1000);
const commandsSub = listenForAgentCommands(
nc,
serviceName,
async (subject, data) => {
// Implement your own message handling logic.
console.log(`Received command: [${subject}]`, data);
}
);
return {
dispose: async () => {
console.log(`Stopping ${serviceName} service...`);
updateLoop.stop();
heartbeat.stop();
if (!commandsSub.isClosed()) await commandsSub.drain();
},
};
};
SenML format
In this example we format our outgoing message in the SenML format. Note that it is not strictly required. As discussed in the Messaging section Connhex supports multiple message formats, such as generic JSON.
common.js
const { JSONCodec } = require('nats');
const jc = JSONCodec();
// Encode and publish a message.
exports.publishMessage = (nc, serviceName, msg) =>
nc.publish(`edge.${serviceName}`, jc.encode(msg));
// Sends a heartbeat message every `heartbeatIntervalMs` ms to notify
// connhex-edge-agent that this service is working properly.
// By default the connhex-edge-agent checks that all services have sent
// a heartbeat message within the last 10s (customizable).
// If a service didn't, it is be marked as offline.
const heartbeatIntervalMs = 10000;
exports.startHeartbeat = (nc, serviceName) => {
const interval = setInterval(() => {
// Add here your custom logic to check that everything is ok.
nc.publish(`heartbeat.${serviceName}.service`);
}, heartbeatIntervalMs);
return {
stop: () => clearInterval(interval),
};
};
// Listen for commands received.
exports.listenForAgentCommands = (nc, serviceName, handler) => {
// Subscribe to NATS subject.
// ">" is used as wildcard, check https://docs.nats.io/nats-concepts/subjects#characters-allowed-for-subject-names
const sub = nc.subscribe(`commands.${serviceName}.>`);
(async () => {
for await (const m of sub) {
handler(
m.subject.replace(`commands.${serviceName}.`, ''),
jc.decode(m.data)
);
}
})();
return sub;
};