Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
620 lines (523 sloc) 19 KB
/*
Example how to send and receive data using the Quectel BG96 modem and built-in MQTT client.
Uses a simple Finite State Machine to introduce robustness against communication errors.
Note: If you have chosen to upload the code to RAM (default) in the Espruino IDE, you need
to interactively call "onInit();" on the device's JavaScript console after uploading.
Debug output to console can be controlled via variable connection_options.debug
Cryptographical files for securing the MQTT connection must have been uploaded to the Quectel BG96
module as files cert.pem, key.pem and cacert.pem before.
Low memory is an issue!
Use the "online minification" feature of the Espruino IDE if you run short on
memory (e.g. "Closure (online))
Although not completely reproduced: For standalone operation, please turn off debug output to console,
(debug: false) as this might lead to the system freezing up.
Copyright (C) 2019 Wolfgang Klenk <wolfgang.klenk@gmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
var ENTERING_STATE = 'Entering State';
var ERROR_IN_STATE = 'Error in State';
var STATE_SETUP_EXTERNAL_HARDWARE = 'Setup External Hardware';
var STATE_CONFIGURE_MODEM = 'Configure Modem';
var STATE_REGISTER_TO_NETWORK = 'Register To Network';
var STATE_OPEN_MQTT_NETWORK = 'Open MQTT Network';
var STATE_CONNECT_TO_SERVER = 'Connect To Server';
var STATE_PUBLISH_TELEMETRY_DATA = 'Publish Telemetry Data';
var STATE_GET_CURRENT_STATE = 'Get Current State';
var STATE_SUBSCRIBE_TO_DELTA_UPDATES = "Subscribe To Delta Updates";
var STATE_SLEEP = 'Sleep';
var STATE_RESET_MODEM = 'Reset Modem';
var STATE_POWER_DOWN = 'Power Down';
var at;
var bme280;
var errCnt = 0;
var updateCnt = 1;
var smRestartCnt = 0;
var ledOn = false;
var qmtstat = 0;
var sm = require("StateMachine").FSM();
// NB1 connectivity settings for 1NCE
/*
var connection_options = {
band: "B8",
apn: "iot.1nce.net",
operator: "26201",
debug: false
};
*/
// NB1 connectivity settings for Vodafone Germany
var connection_options = {
band: "B20",
apn: "vgesace.nb.iot",
operator: "26202",
debug: false // Print communication with BG96 module to console.
};
var mqtt_options = {
// AWS IoT
server: 'a136ivuau4uklv-ats.iot.eu-central-1.amazonaws.com',
port: 8883,
client_id: "klenk-iot-device"
};
var band_values = {
"B1": "1",
"B2": "2",
"B3": "4",
"B4": "8",
"B5": "10",
"B8": "80",
"B12": "800",
"B13": "1000",
"B18": "20000",
"B19": "40000",
"B20": "80000",
"B26": "2000000",
"B28": "8000000"
};
sendAtCommand = function (command, timeoutMs, waitForLine) {
return new Promise((resolve, reject) => {
var answer = "";
at.cmd(command + "\r\n", timeoutMs || 1E3, function processResponse(response) {
if (undefined === response || "ERROR" === response || response.startsWith("+CME ERROR")) {
reject(response ? (command + ": " + response) : (command + ": TIMEOUT"));
} else if (waitForLine ? (response.startsWith(waitForLine)) : ("OK" === response)) {
resolve(waitForLine ? response : answer);
} else {
answer += (answer ? "\n" : "") + response;
return processResponse;
}
});
});
};
sendAtCommandAndWaitForPrompt = function (command, timeoutMs, sendLineAfterPrompt, waitForLine) {
return new Promise((resolve, reject) => {
var prompt = '> ';
var answer = "";
if (sendLineAfterPrompt) {
at.register(prompt, (line) => {
at.unregister(prompt);
at.write(sendLineAfterPrompt + '\x1A');
return line.substr(2);
});
}
at.cmd(command + "\r\n", timeoutMs, function processResponse(response) {
if (undefined === response || "ERROR" === response || response.startsWith("+CME ERROR")) {
// Unregister the prompt '> ' in case something went wrong.
// If we don't, we get follow up errors when it is tried to again register the prompt.
at.unregister(prompt);
reject(response ? (command + ": " + response) : (command + ": TIMEOUT"));
} else if (waitForLine ? (response.startsWith(waitForLine)) : ("OK" === response)) {
resolve(waitForLine ? response : answer);
} else {
answer += (answer ? "\n" : "") + response;
return processResponse;
}
});
});
};
function controlLed(desiredLedState) {
if (desiredLedState === 'off') {
digitalWrite(LED1, false);
ledOn = false;
} else if (desiredLedState === 'on') {
digitalWrite(LED1, true);
ledOn = true;
}
}
//
// Finite State Machine: States
//
// Setup external hardware.
function e_SetupExternalHardware() {
if (connection_options.debug) console.log(ENTERING_STATE, STATE_SETUP_EXTERNAL_HARDWARE);
return new Promise((resolve, reject) => {
require("iTracker").setCellOn(true, (usart) => {
resolve(usart);
});
})
.then((usart) => {
if (connection_options.debug) console.log("External modules connected.");
at = require("AT").connect(usart);
if (connection_options.debug) {
at.debug(true);
}
return new Promise((resolve, reject) => {
bme280 = require("iTracker").setEnvOn(true, () => {
if (connection_options.debug) console.log("BME280 wiring set up.");
resolve();
});
});
})
.then(() => {
sm.signal('ok');
})
.catch((err) => {
if (connection_options.debug) console.log(ERROR_IN_STATE, STATE_SETUP_EXTERNAL_HARDWARE, err);
sm.signal('fail');
});
}
// Configure BG96 module and MQTT software stack
function e_ConfigureModem() {
if (connection_options.debug) console.log(ENTERING_STATE, STATE_CONFIGURE_MODEM);
sendAtCommand('AT&F0')
.then(() => sendAtCommand('ATE0'))
.then(() => sendAtCommand('AT+CPIN?')) // Fails on locked PIN
.then(() => {
var band_value = band_values[connection_options.band];
if (undefined === band_value) throw("Unknown band: " + connection_options.band);
return sendAtCommand('AT+QCFG="band",0,0,' + band_value + ',1');
})
.then(() => sendAtCommand('AT+QCFG="nwscanmode",3,1')) // Network Search Mode, LTE only
.then(() => sendAtCommand('AT+QCFG="nwscanseq",030102,1')) // Network Search Sequence, NB-Iot, GSM, CatM1
.then(() => sendAtCommand('AT+QCFG="iotopmode",1,1')) // LTE Search Mode: NB-IoT only
.then(() => sendAtCommand('AT+QCFG="servicedomain",1,1')) // Set PS domain, PS only
.then(() => sendAtCommand('AT+CGDCONT=1,"IP",' + JSON.stringify(connection_options.apn)))
.then(() => sendAtCommand('AT+CFUN=1'))
// Send keepalive message every 30 seconds
.then(() => sendAtCommand('AT+QMTCFG="keepalive",0,30'))
// SSL: Configure MQTT session into SSL mode
.then(() => sendAtCommand('AT+QMTCFG="SSL",0,1,2'))
// SSL: Configure trusted CA certificate
.then(() => sendAtCommand('AT+QSSLCFG="cacert",2,"cacert.pem"'))
// SSL: Configure client certificate
.then(() => sendAtCommand('AT+QSSLCFG="clientcert",2,"cert.pem"'))
// SSL: Configure private key
.then(() => sendAtCommand('AT+QSSLCFG="clientkey",2,"key.pem"'))
// SSL: Authentication mode: Server and client authentication
.then(() => sendAtCommand('AT+QSSLCFG="seclevel",2,2'))
// SSL: Authentication version. Accept all SSL versions
.then(() => sendAtCommand('AT+QSSLCFG="sslversion",2,4'))
// SSL: Cipher suite: Support all cipher suites
.then(() => sendAtCommand('AT+QSSLCFG="ciphersuite",2,0xFFFF'))
// SSL: Ignore the time of authentication.
.then(() => sendAtCommand('AT+QSSLCFG="ignorelocaltime",1'))
.then(() => {
sm.signal('ok');
})
.catch((err) => {
if (connection_options.debug) console.log(ERROR_IN_STATE, STATE_CONFIGURE_MODEM, err);
sm.signal('fail');
});
}
// Register to network
function e_RegisterToNetwork() {
if (connection_options.debug) console.log(ENTERING_STATE, STATE_REGISTER_TO_NETWORK);
// Manually register to network.
// Modem LED should flash on-off-off-off periodically to indicate network search
sendAtCommand('AT+COPS=1,2,' + JSON.stringify(connection_options.operator) + ',9', 1800000)
.then(() => {
sm.signal('ok');
})
.catch((err) => {
if (connection_options.debug) console.log('Error in state', STATE_REGISTER_TO_NETWORK, err);
sm.signal('fail');
});
}
// Open a network for MQTT client
function e_OpenMQTTNetwork() {
if (connection_options.debug) console.log(ENTERING_STATE, STATE_OPEN_MQTT_NETWORK);
sendAtCommand(
'AT+QMTOPEN=0,' + JSON.stringify(mqtt_options.server) + ',' + mqtt_options.port,
30000,
'+QMTOPEN:')
.then((line) => {
if (connection_options.debug) console.log("+QMTOPEN line:", line);
var qmtstat = '+QMTSTAT: ';
at.unregisterLine(qmtstat);
at.registerLine(qmtstat, (line) => {
line = line.split(",");
qmtstat = parseInt(line[1]);
if (connection_options.debug) console.log("+QMTSTAT Error Code:", qmtstat);
});
sm.signal('ok');
})
.catch((err) => {
if (connection_options.debug) console.log(ERROR_IN_STATE, STATE_OPEN_MQTT_NETWORK, err);
sm.signal('fail');
});
}
// Connect this client to MQTT server
function e_ConnectToServer() {
if (connection_options.debug) console.log(ENTERING_STATE, STATE_CONNECT_TO_SERVER);
sendAtCommand('AT+QMTCONN=0,'
+ JSON.stringify(mqtt_options.client_id),
15000,
'+QMTCONN:')
.then((line) => {
if (connection_options.debug) console.log("+QMTCONN line:", line);
sm.signal('ok');
})
.catch((err) => {
if (connection_options.debug) console.log(ERROR_IN_STATE, STATE_CONNECT_TO_SERVER, err);
sm.signal('fail');
});
}
// Request the current state from the AWS IoT Device Shadow
function e_GetCurrentState() {
if (connection_options.debug) console.log(ENTERING_STATE, STATE_GET_CURRENT_STATE);
// Register line +QMTRECV: 0,1,"$aws/things/..."/shadow/get/accepted"
var qmtrecv = '+QMTRECV: 0,1,' + JSON.stringify("$aws/things/" + mqtt_options.client_id + "/shadow/get/accepted");
at.unregisterLine(qmtrecv);
at.registerLine(qmtrecv, (line) => {
var openingBrace = line.indexOf('{');
if (connection_options.debug) console.log("+QMTRECV", line.split(",")[2], line.substr(openingBrace));
var payloadJson = JSON.parse(line.substr(openingBrace));
if (payloadJson.hasOwnProperty('state') && payloadJson.state.hasOwnProperty('desired') && payloadJson.state.desired.hasOwnProperty('led')) {
controlLed(payloadJson.state.desired.led);
}
});
// Subscribe to shadow/get/accepted
sendAtCommand('AT+QMTSUB=0,1,'
+ JSON.stringify("$aws/things/" + mqtt_options.client_id + "/shadow/get/accepted")
+ ',1',
15000,
'+QMTSUB:')
.then((line) => {
if (connection_options.debug) console.log("+QMTSUB line:", line);
// Publish empty message to shadow/get
return sendAtCommandAndWaitForPrompt('AT+QMTPUB=0,1,1,0,'
+ JSON.stringify("$aws/things/" + mqtt_options.client_id + "/shadow/get"),
15000,
'{}',
'+QMTPUB:'
);
})
.then((line) => {
if (connection_options.debug) console.log("+QMTPUB line:", line);
sm.signal('ok');
})
.catch((err) => {
if (connection_options.debug) console.log(ERROR_IN_STATE, STATE_CONNECT_TO_SERVER, err);
sm.signal('fail');
});
}
// Subscribe to AWS Device Shadow Delta Updates
// Will receive a message any time there is a difference between "desired" and "reported" led state.
function e_SubscribeToDeltaUpdates() {
if (connection_options.debug) console.log(ENTERING_STATE, STATE_SUBSCRIBE_TO_DELTA_UPDATES);
// Register line +QMTRECV: 0,1,"$aws/things/..."/shadow/get/accepted"
var qmtrecv = '+QMTRECV: 0,1,' + JSON.stringify("$aws/things/" + mqtt_options.client_id + "/shadow/update/delta");
at.unregisterLine(qmtrecv);
at.registerLine(qmtrecv, (line) => {
var openingBrace = line.indexOf('{');
if (connection_options.debug) console.log("+QMTRECV", line.split(",")[2], line.substr(openingBrace));
var payloadJson = JSON.parse(line.substr(openingBrace));
if (payloadJson.hasOwnProperty('state') && payloadJson.state.hasOwnProperty('led')) {
controlLed(payloadJson.state.led);
}
});
// Subscribe to shadow/update/delta
sendAtCommand('AT+QMTSUB=0,1,'
+ JSON.stringify("$aws/things/" + mqtt_options.client_id + "/shadow/update/delta")
+ ',1',
15000,
'+QMTSUB:')
.then((line) => {
if (connection_options.debug) console.log("+QMTSUB line:", line);
sm.signal('ok');
})
.catch((err) => {
if (connection_options.debug) console.log(ERROR_IN_STATE, STATE_SUBSCRIBE_TO_DELTA_UPDATES, err);
sm.signal('fail');
});
}
// Publish telemetry data via MQTT
function e_PublishTelemetryData() {
if (connection_options.debug) console.log(ENTERING_STATE, STATE_PUBLISH_TELEMETRY_DATA);
var currentTemperature = bme280.getData().temp.toFixed(2);
if (connection_options.debug) console.log("Current temperature: ", currentTemperature);
// Reported LED state
var ledStateString = 'off';
if (ledOn === true) {
ledStateString = 'on';
}
var memory = process.memory();
// AWS IoT Protocol
sendAtCommandAndWaitForPrompt('AT+QMTPUB=0,1,1,0,'
+ JSON.stringify("$aws/things/" + mqtt_options.client_id + "/shadow/update"),
5000,
'{' +
'"state" : {' +
' "reported" : {' +
' "temperature" : "' + currentTemperature + '",' +
' "led" : "' + ledStateString + '",' +
' "restarts" : ' + smRestartCnt + ',' +
' "updates" : ' + updateCnt + ',' +
' "memory" : {' +
' "free" : ' + memory.free + ',' +
' "usage" : ' + memory.usage + ',' +
' "total" : ' + memory.total + ',' +
' "history" : ' + memory.history + '' +
' }' +
' }' +
' }' +
'}',
'+QMTPUB:'
)
.then((line) => {
if (connection_options.debug) console.log("+QMTPUB line:", line);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 5000);
});
})
.then((line) => {
sm.signal('ok');
})
.catch((err) => {
if (connection_options.debug) console.log(ERROR_IN_STATE, STATE_PUBLISH_TELEMETRY_DATA, err);
sm.signal('fail');
});
}
function e_Sleep(result) {
if (connection_options.debug) console.log(ENTERING_STATE, STATE_SLEEP);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 300000);
})
.then(() => {
sm.signal('ok');
});
}
function e_ResetModem(result) {
if (connection_options.debug) console.log(ENTERING_STATE, STATE_RESET_MODEM);
sendAtCommand('AT+QPOWD', 10000, 'POWERED DOWN')
.then(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 10000);
});
})
.then(() => {
if (connection_options.debug) console.log('Powered down');
sm.signal('ok');
})
.catch((err) => {
if (connection_options.debug) console.log(ERROR_IN_STATE, STATE_RESET_MODEM, err);
sm.signal('ok');
});
}
//
// Finite State Machine: Transitions
//
function t_SetupExternalHardware(result) {
return {state: STATE_CONFIGURE_MODEM};
}
function t_ConfigureModem(result) {
return {state: STATE_REGISTER_TO_NETWORK};
}
function t_RegisterToNetwork(result) {
switch(result) {
case('ok'):
return {state: STATE_OPEN_MQTT_NETWORK};
default:
return {state: STATE_RESET_MODEM};
}
}
function t_OpenMQTTNetwork(result) {
if (qmtstat > 0) {
return {state: STATE_RESET_MODEM};
}
switch(result) {
case('ok'):
return {state: STATE_CONNECT_TO_SERVER};
default:
return {state: STATE_RESET_MODEM};
}
}
function t_ConnectToServer(result) {
if (qmtstat > 0) {
return {state: STATE_RESET_MODEM};
}
switch(result) {
case('ok'):
return {state: STATE_GET_CURRENT_STATE};
default:
return {state: STATE_RESET_MODEM};
}
}
function t_GetCurrentState(result) {
if (qmtstat > 0) {
return {state: STATE_RESET_MODEM};
}
switch(result) {
case('ok'):
return {state: STATE_SUBSCRIBE_TO_DELTA_UPDATES};
default:
return {state: STATE_RESET_MODEM};
}
}
function t_SubscribeToDeltaUpdates(result) {
if (qmtstat > 0) {
return {state: STATE_RESET_MODEM};
}
switch(result) {
case('ok'):
return {state: STATE_PUBLISH_TELEMETRY_DATA};
default:
return {state: STATE_RESET_MODEM};
}
}
function t_PublishTelemetryData(result) {
if (qmtstat > 0) {
return {state: STATE_RESET_MODEM};
}
switch(result) {
case('ok'):
errCnt = 0; // Reset error counter
updateCnt++;
return {state: STATE_SLEEP};
default:
errCnt++;
if (errCnt >= 3) {
errCnt = 0;
return {state: STATE_RESET_MODEM};
}
else {
return {state: STATE_SLEEP};
}
}
}
function t_Sleep(result) {
if (qmtstat > 0) {
return {state: STATE_RESET_MODEM};
}
return {state: STATE_PUBLISH_TELEMETRY_DATA};
}
function t_ResetModem(result) {
return {state: STATE_POWER_DOWN};
}
function onInit() {
Bluetooth.setConsole(true); // Don't want to have console on "Serial1" that is used for modem.
sm.define({name: STATE_SETUP_EXTERNAL_HARDWARE, enter:e_SetupExternalHardware, signal:t_SetupExternalHardware});
sm.define({name: STATE_CONFIGURE_MODEM, enter:e_ConfigureModem, signal:t_ConfigureModem});
sm.define({name: STATE_REGISTER_TO_NETWORK, enter:e_RegisterToNetwork, signal:t_RegisterToNetwork});
sm.define({name: STATE_OPEN_MQTT_NETWORK, enter:e_OpenMQTTNetwork, signal:t_OpenMQTTNetwork});
sm.define({name: STATE_CONNECT_TO_SERVER, enter:e_ConnectToServer, signal:t_ConnectToServer});
sm.define({name: STATE_GET_CURRENT_STATE, enter:e_GetCurrentState, signal:t_GetCurrentState});
sm.define({name: STATE_SUBSCRIBE_TO_DELTA_UPDATES, enter:e_SubscribeToDeltaUpdates, signal:t_SubscribeToDeltaUpdates});
sm.define({name: STATE_PUBLISH_TELEMETRY_DATA, enter:e_PublishTelemetryData, signal:t_PublishTelemetryData});
sm.define({name: STATE_SLEEP, enter:e_Sleep, signal:t_Sleep});
sm.define({name: STATE_RESET_MODEM, enter:e_ResetModem, signal:t_ResetModem});
sm.define({name: STATE_POWER_DOWN});
sm.init(STATE_SETUP_EXTERNAL_HARDWARE);
// If the state machine is in state "Power Down", then restart the state machine.
setInterval(() => {
if (connection_options.debug) console.log("Checking state machine state");
if (sm.state === STATE_POWER_DOWN) {
if (connection_options.debug) console.log('Restarting State Machine');
smRestartCnt++;
sm.init(STATE_SETUP_EXTERNAL_HARDWARE);
}
}, 600000);
}
You can’t perform that action at this time.