Commit 76433dcd authored by Eric Duminil's avatar Eric Duminil
Browse files

Adding S8_UART library

parent 25b41d2e
......@@ -9,6 +9,7 @@
// The SCD30 from Sensirion is a high quality Nondispersive Infrared (NDIR) based CO₂ sensor capable of detecting 400 to 10000ppm with an accuracy of ±(30ppm+3%).
// https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library
#include "src/lib/SparkFun_SCD30_Arduino_Library/src/SparkFun_SCD30_Arduino_Library.h" // From: http://librarymanager/All#SparkFun_SCD30
#include "src/lib/S8_UART/s8_uart.h"
namespace config {
const uint16_t measurement_timestep_bootup = 5; // [s] Measurement timestep during acclimatization.
......
MIT License
Copyright (c) 2021 Josep Comas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
\ No newline at end of file
# S8_UART
S8 Library for serial communication (UART)
This library is for **Senseair S8 CO2** sensor to use with Arduino framework using serial communication (UART).
**Senseair S8 LP (004-0-0053)** is recommended for CO2 measurement in indoors for air quality application.
## Installation
### PlatformIO
Download S8_UART library and copy extracted folder into **PlatformIO\Projects** folder. Open **Visual Studio Code** and select the directory of the project. Modify **platformio.ini** file according your board and requirements. Open .cpp file of example and modify the parameters of **configuration section**. Build and upload the example selected of platformio.ini file.
### Arduino IDE
Go to **Documents\Arduino\libraries** and create the **S8_UART** directory. Download S8_UART library and copy the files of **src** folder into the new created folder.
To test the examples, copy the folder of the example into **Documents\Arduino\Projects** folder, rename the **.cpp** file of the example to **.ino**. Open the .ino file and modify the parameters of **configuration section**. In the menu of IDE select your board and compile and upload the program.
## Debug
Modify **CORE_DEBUG_LEVEL** variable to **1** in platformio.ini file to show only errors (in console) and to **5** value for full messages.
\ No newline at end of file
#include <stdint.h>
/* ModBus CRC routine extracted from https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf */
/* Table of CRC values for high–order byte */
static const uint8_t auchCRCHi[] = {
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40
} ;
/* Table of CRC values for low–order byte */
static const uint8_t auchCRCLo[] = {
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4,
0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD,
0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7,
0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,
0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE,
0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2,
0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,
0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB,
0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91,
0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88,
0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80,
0x40
};
/* The function returns the CRC as a unsigned short type */
uint16_t modbus_CRC16 (uint8_t *puchMsg, uint16_t usDataLen ) {
/*
puchMsg -> message to calculate CRC upon
usDataLen -> quantity of bytes in message
*/
uint8_t uchCRCHi = 0xFF ; /* high byte of CRC initialized */
uint8_t uchCRCLo = 0xFF ; /* low byte of CRC initialized */
uint16_t uIndex ; /* will index into CRC lookup table */
while (usDataLen--) /* pass through message buffer */
{
uIndex = uchCRCLo ^ *puchMsg++ ; /* calculate the CRC */
uchCRCLo = uchCRCHi ^ auchCRCHi[uIndex] ;
uchCRCHi = auchCRCLo[uIndex] ;
}
return (uchCRCHi << 8 | uchCRCLo) ;
}
#ifndef _MODBUS_H
#define _MODBUS_H
/* The function returns the CRC as a unsigned short type
puchMsg -> message to calculate CRC upon
usDataLen -> quantity of bytes in message */
uint16_t modbus_CRC16 (uint8_t *puchMsg, uint16_t usDataLen );
#endif
/***************************************************************************************************************************
SenseAir S8 Library for Serial Modbus Communication
Copyright (c) 2021 Josep Comas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
***************************************************************************************************************************/
#include "s8_uart.h"
#include "modbus_crc.h"
#include "utils.h"
/* Initialize */
S8_UART::S8_UART(Stream &serial)
{
mySerial = &serial;
}
/* Get firmware version */
void S8_UART::get_firmware_version(char firmver[]) {
if (firmver == NULL) {
return;
}
strcpy(firmver, "");
// Ask software version
send_cmd(MODBUS_FUNC_READ_INPUT_REGISTERS, MODBUS_IR29, 0x0001);
// Wait response
memset(buf_msg, 0, S8_LEN_BUF_MSG);
uint8_t nb = serial_read_bytes(7, S8_TIMEOUT);
// Check response and get data
if (valid_response_len(MODBUS_FUNC_READ_INPUT_REGISTERS, nb, 7)) {
snprintf(firmver, S8_LEN_FIRMVER, "%0u.%0u", buf_msg[3], buf_msg[4]);
LOG_DEBUG_INFO("Firmware version: ", firmver);
} else {
LOG_DEBUG_ERROR("Firmware version not available!");
}
}
/* Get CO2 value in ppm */
int16_t S8_UART::get_co2() {
int16_t co2 = 0;
// Ask CO2 value
send_cmd(MODBUS_FUNC_READ_INPUT_REGISTERS, MODBUS_IR4, 0x0001);
// Wait response
memset(buf_msg, 0, S8_LEN_BUF_MSG);
uint8_t nb = serial_read_bytes(7, S8_TIMEOUT);
// Check response and get data
if (valid_response_len(MODBUS_FUNC_READ_INPUT_REGISTERS, nb, 7)) {
co2 = ((buf_msg[3] << 8) & 0xFF00) | (buf_msg[4] & 0x00FF);
LOG_DEBUG_INFO("CO2 value = ", co2, " ppm");
} else {
LOG_DEBUG_ERROR("Error getting CO2 value!");
}
return co2;
}
/* Read ABC period */
int16_t S8_UART::get_ABC_period() {
int16_t period = 0;
// Ask ABC period
send_cmd(MODBUS_FUNC_READ_HOLDING_REGISTERS, MODBUS_HR32, 0x0001);
// Wait response
memset(buf_msg, 0, S8_LEN_BUF_MSG);
uint8_t nb = serial_read_bytes(7, S8_TIMEOUT);
// Check response and get data
if (valid_response_len(MODBUS_FUNC_READ_HOLDING_REGISTERS, nb, 7)) {
period = ((buf_msg[3] << 8) & 0xFF00) | (buf_msg[4] & 0x00FF);
LOG_DEBUG_INFO("ABC period = ", period, " hours");
} else {
LOG_DEBUG_ERROR("Error getting ABC period!");
}
return period;
}
/* Setup ABC period, default 180 hours (7.5 days) */
bool S8_UART::set_ABC_period(int16_t period) {
uint8_t buf_msg_sent[8];
bool result = false;
if (period >= 0 && period <= 4800) { // 0 = disable ABC algorithm
// Ask set ABC period
send_cmd(MODBUS_FUNC_WRITE_SINGLE_REGISTER, MODBUS_HR32, period);
// Save bytes sent
memcpy(buf_msg_sent, buf_msg, 8);
// Wait response
memset(buf_msg, 0, S8_LEN_BUF_MSG);
serial_read_bytes(8, S8_TIMEOUT);
// Check response
if (memcmp(buf_msg_sent, buf_msg, 8) == 0) {
result = true;
LOG_DEBUG_INFO("Successful setting of ABC period");
} else {
LOG_DEBUG_ERROR("Error in setting of ABC period!");
}
} else {
LOG_DEBUG_ERROR("Invalid ABC period!");
}
return result;
}
/* Read acknowledgement flags */
int16_t S8_UART::get_acknowledgement() {
int16_t flags = 0;
// Ask acknowledgement flags
send_cmd(MODBUS_FUNC_READ_HOLDING_REGISTERS, MODBUS_HR1, 0x0001);
// Wait response
memset(buf_msg, 0, S8_LEN_BUF_MSG);
uint8_t nb = serial_read_bytes(7, S8_TIMEOUT);
// Check response and get data
if (valid_response_len(MODBUS_FUNC_READ_HOLDING_REGISTERS, nb, 7)) {
flags = ((buf_msg[3] << 8) & 0xFF00) | (buf_msg[4] & 0x00FF);
LOG_DEBUG_INFO_BINARY("Acknowledgement flags = b", flags);
} else {
LOG_DEBUG_ERROR("Error getting acknowledgement flags!");
}
return flags;
}
/* Read acknowledgement flags */
bool S8_UART::clear_acknowledgement() {
uint8_t buf_msg_sent[8];
bool result = false;
// Ask clear acknowledgement flags
send_cmd(MODBUS_FUNC_WRITE_SINGLE_REGISTER, MODBUS_HR1, 0x0000);
// Save bytes sent
memcpy(buf_msg_sent, buf_msg, 8);
// Wait response
memset(buf_msg, 0, S8_LEN_BUF_MSG);
serial_read_bytes(8, S8_TIMEOUT);
// Check response
if (memcmp(buf_msg_sent, buf_msg, 8) == 0) {
result = true;
LOG_DEBUG_INFO("Successful clearing acknowledgement flags");
} else {
LOG_DEBUG_ERROR("Error clearing acknowledgement flags!");
}
return result;
}
/* Start a manual calibration (go to outdoors, wait 5 minutes o more and then you issue this command) */
bool S8_UART::manual_calibration() {
bool result = clear_acknowledgement();
if (result) {
result = send_special_command(S8_CO2_BACKGROUND_CALIBRATION);
if (result) {
LOG_DEBUG_INFO("Manual calibration in background has started");
} else {
LOG_DEBUG_ERROR("Error starting manual calibration!");
}
}
return result;
}
/* Send special command (high = command, low = parameter) */
/*
Command = 0x7C,
Parameter = 0x06 CO2 background calibration
Parameter = 0x07 CO2 zero calibration
*/
bool S8_UART::send_special_command(int16_t command) {
uint8_t buf_msg_sent[8];
bool result = false;
// Ask set user special command
send_cmd(MODBUS_FUNC_WRITE_SINGLE_REGISTER, MODBUS_HR2, command);
// Save bytes sent
memcpy(buf_msg_sent, buf_msg, 8);
// Wait response
memset(buf_msg, 0, S8_LEN_BUF_MSG);
serial_read_bytes(8, S8_TIMEOUT);
// Check response
if (memcmp(buf_msg_sent, buf_msg, 8) == 0) {
result = true;
LOG_DEBUG_INFO("Successful setting user special command");
} else {
LOG_DEBUG_ERROR("Error in setting user special command!");
}
return result;
}
/* Read meter status */
int16_t S8_UART::get_meter_status() {
int16_t status = 0;
// Ask meter status
send_cmd(MODBUS_FUNC_READ_INPUT_REGISTERS, MODBUS_IR1, 0x0001);
// Wait response
memset(buf_msg, 0, S8_LEN_BUF_MSG);
uint8_t nb = serial_read_bytes(7, S8_TIMEOUT);
// Check response and get data
if (valid_response_len(MODBUS_FUNC_READ_INPUT_REGISTERS, nb, 7)) {
status = ((buf_msg[3] << 8) & 0xFF00) | (buf_msg[4] & 0x00FF);
LOG_DEBUG_INFO_BINARY("Meter status = b", status);
} else {
LOG_DEBUG_ERROR("Error getting meter status!");
}
return status;
}
/* Read alarm status */
int16_t S8_UART::get_alarm_status() {
int16_t status = 0;
// Ask alarm status
send_cmd(MODBUS_FUNC_READ_INPUT_REGISTERS, MODBUS_IR2, 0x0001);
// Wait response
memset(buf_msg, 0, S8_LEN_BUF_MSG);
uint8_t nb = serial_read_bytes(7, S8_TIMEOUT);
// Check response and get data
if (valid_response_len(MODBUS_FUNC_READ_INPUT_REGISTERS, nb, 7)) {
status = ((buf_msg[3] << 8) & 0xFF00) | (buf_msg[4] & 0x00FF);
LOG_DEBUG_INFO_BINARY("Alarm status = b", status);
} else {
LOG_DEBUG_ERROR("Error getting alarm status!");
}
return status;
}
/* Read output status */
int16_t S8_UART::get_output_status() {
int16_t status = 0;
// Ask output status
send_cmd(MODBUS_FUNC_READ_INPUT_REGISTERS, MODBUS_IR3, 0x0001);
// Wait response
memset(buf_msg, 0, S8_LEN_BUF_MSG);
uint8_t nb = serial_read_bytes(7, S8_TIMEOUT);
// Check response and get data
if (valid_response_len(MODBUS_FUNC_READ_INPUT_REGISTERS, nb, 7)) {
status = ((buf_msg[3] << 8) & 0xFF00) | (buf_msg[4] & 0x00FF);
LOG_DEBUG_INFO_BINARY("Output status = b", status);
} else {
LOG_DEBUG_ERROR("Error getting output status!");
}
return status;
}
/* Read PWM output (0x3FFF = 100%)
Raw PWM output to ppm: (raw_PWM_output / 16383.0) * 2000.0)
2000.0 is max range of sensor (2000 ppm for normal version, extended version is 10000 ppm)
*/
int16_t S8_UART::get_PWM_output() {
int16_t pwm = 0;
// Ask PWM output
send_cmd(MODBUS_FUNC_READ_INPUT_REGISTERS, MODBUS_IR22, 0x0001);
// Wait response
memset(buf_msg, 0, S8_LEN_BUF_MSG);
uint8_t nb = serial_read_bytes(7, S8_TIMEOUT);
// Check response and get data
if (valid_response_len(MODBUS_FUNC_READ_INPUT_REGISTERS, nb, 7)) {
pwm = ((buf_msg[3] << 8) & 0xFF00) | (buf_msg[4] & 0x00FF);
LOG_DEBUG_INFO("PWM output (raw) = ", pwm);
LOG_DEBUG_INFO("PWM output (to ppm, normal version) = ", (pwm / 16383.0) * 2000.0, " ppm");
//LOG_DEBUG_INFO("PWM output (to ppm, extended version) = ", (pwm / 16383.0) * 10000.0, " ppm");
} else {
LOG_DEBUG_ERROR("Error getting PWM output!");
}
return pwm;
}
/* Read sensor type ID */
int32_t S8_UART::get_sensor_type_ID() {
int32_t sensorType = 0;
// Ask sensor type ID (high)
send_cmd(MODBUS_FUNC_READ_INPUT_REGISTERS, MODBUS_IR26, 0x0001);
// Wait response
memset(buf_msg, 0, S8_LEN_BUF_MSG);
uint8_t nb = serial_read_bytes(7, S8_TIMEOUT);
// Check response and get data
if (valid_response_len(MODBUS_FUNC_READ_INPUT_REGISTERS, nb, 7)) {
// Save sensor type ID (high)
sensorType = (((int32_t)buf_msg[4] << 16) & 0x00FF0000);
// Ask sensor type ID (low)
send_cmd(MODBUS_FUNC_READ_INPUT_REGISTERS, MODBUS_IR27, 0x0001);
// Wait response
memset(buf_msg, 0, S8_LEN_BUF_MSG);
nb = serial_read_bytes(7, S8_TIMEOUT);
// Check response and get data
if (valid_response_len(MODBUS_FUNC_READ_INPUT_REGISTERS, nb, 7)) {
sensorType |= ((buf_msg[3] << 8) & 0x0000FF00) | (buf_msg[4] & 0x000000FF);
LOG_DEBUG_INFO_HEX("Sensor type ID = 0x", sensorType, 3);
} else {
LOG_DEBUG_ERROR("Error getting sensor type ID (low)!");
}
} else {
LOG_DEBUG_ERROR("Error getting sensor type ID (high)!");
}
return sensorType;
}
/* Read sensor ID */
int32_t S8_UART::get_sensor_ID() {
int32_t sensorID = 0;
// Ask sensor ID (high)
send_cmd(MODBUS_FUNC_READ_INPUT_REGISTERS, MODBUS_IR30, 0x0001);
// Wait response
memset(buf_msg, 0, S8_LEN_BUF_MSG);
uint8_t nb = serial_read_bytes(7, S8_TIMEOUT);
// Check response and get data
if (valid_response_len(MODBUS_FUNC_READ_INPUT_REGISTERS, nb, 7)) {
// Save sensor ID (high)
sensorID = (((int32_t)buf_msg[3] << 24) & 0xFF000000) | (((int32_t)buf_msg[4] << 16) & 0x00FF0000);
// Ask sensor ID (low)
send_cmd(MODBUS_FUNC_READ_INPUT_REGISTERS, MODBUS_IR31, 0x0001);
// Wait response
memset(buf_msg, 0, S8_LEN_BUF_MSG);
nb = serial_read_bytes(7, S8_TIMEOUT);
// Check response and get data
if (valid_response_len(MODBUS_FUNC_READ_INPUT_REGISTERS, nb, 7)) {
sensorID |= ((buf_msg[3] << 8) & 0x0000FF00) | (buf_msg[4] & 0x000000FF);
LOG_DEBUG_INFO_HEX("Sensor ID = 0x", sensorID, 4);
} else {
LOG_DEBUG_ERROR("Error getting sensor ID (low)!");
}
} else {
LOG_DEBUG_ERROR("Error getting sensor ID (high)!");
}
return sensorID;
}
/* Read memory map version */
int16_t S8_UART::get_memory_map_version() {
int16_t mmVersion = 0;
// Ask memory map version
send_cmd(MODBUS_FUNC_READ_INPUT_REGISTERS, MODBUS_IR28, 0x0001);
// Wait response
memset(buf_msg, 0, S8_LEN_BUF_MSG);
uint8_t nb = serial_read_bytes(7, S8_TIMEOUT);
// Check response and get data
if (valid_response_len(MODBUS_FUNC_READ_INPUT_REGISTERS, nb, 7)) {
mmVersion = ((buf_msg[3] << 8) & 0xFF00) | (buf_msg[4] & 0x00FF);
LOG_DEBUG_INFO("Memory map version = ", mmVersion);
} else {
LOG_DEBUG_ERROR("Error getting memory map version!");
}
return mmVersion;
}
/* Check valid response and length of received message */
bool S8_UART::valid_response_len(uint8_t func, uint8_t nb, uint8_t len) {
bool result = false;
if (nb == len) {
result = valid_response(func, nb);
} else {
LOG_DEBUG_ERROR("Unexpected length!");
}
return result;
}
/* Check if it is a valid message response of the sensor */
bool S8_UART::valid_response(uint8_t func, uint8_t nb) {
uint16_t crc16;
bool result = false;
if (nb >= 7) {
crc16 = modbus_CRC16(buf_msg, nb-2);
if ((buf_msg[nb-2] == (crc16 & 0x00FF)) && (buf_msg[nb-1] == ((crc16 >> 8) & 0x00FF))) {
if (buf_msg[0] == MODBUS_ANY_ADDRESS && (buf_msg[1] == MODBUS_FUNC_READ_HOLDING_REGISTERS || buf_msg[1] == MODBUS_FUNC_READ_INPUT_REGISTERS) && buf_msg[2] == nb-5) {
LOG_DEBUG_VERBOSE("Valid response");
result = true;
} else {
LOG_DEBUG_ERROR("Unexpected response!");
}
} else {
LOG_DEBUG_ERROR("Checksum/length is invalid!");
}
} else {
LOG_DEBUG_ERROR("Invalid length!");
}
return result;
}
/* Send command */
void S8_UART::send_cmd( uint8_t func, uint16_t reg, uint16_t value) {
uint16_t crc16;
if (((func == MODBUS_FUNC_READ_HOLDING_REGISTERS || func == MODBUS_FUNC_READ_INPUT_REGISTERS) && value >= 1) || (func == MODBUS_FUNC_WRITE_SINGLE_REGISTER)) {
buf_msg[0] = MODBUS_ANY_ADDRESS; // Address
buf_msg[1] = func; // Function
buf_msg[2] = (reg >> 8) & 0x00FF; // High-input register
buf_msg[3] = reg & 0x00FF; // Low-input register
buf_msg[4] = (value >> 8) & 0x00FF; // High-word to read or setup
buf_msg[5] = value & 0x00FF; // Low-word to read or setup
crc16 = modbus_CRC16(buf_msg, 6);
buf_msg[6] = crc16 & 0x00FF;
buf_msg[7] = (crc16 >> 8) & 0x00FF;
serial_write_bytes(8);
}
}
/* Send bytes to sensor */
void S8_UART::serial_write_bytes(uint8_t size) {
LOG_DEBUG_VERBOSE_PACKET("Bytes to send: ", (uint8_t *)buf_msg, size);
mySerial->write(buf_msg, size);
mySerial->flush();
}
/* Read answer of sensor */
uint8_t S8_UART::serial_read_bytes(uint8_t max_bytes, uint32_t timeout_ms) {
uint32_t start_t = millis();
uint32_t end_t = start_t;
bool readed = false;
uint8_t nb = 0;
if (max_bytes > 0 && timeout_ms > 0) {
while (((end_t - start_t) <= timeout_ms) && !readed) {
if(mySerial->available()) {
nb = mySerial->readBytes(buf_msg, max_bytes);
readed = true;
}
end_t = millis();
}
if (readed) {
if (nb > 0) {
LOG_DEBUG_VERBOSE_PACKET("Bytes received: ", (uint8_t *)buf_msg, nb);
} else {
LOG_DEBUG_ERROR("Unexpected reading serial port!");
}
} else {
LOG_DEBUG_ERROR("Timeout reading serial port!");
}
} else {
LOG_DEBUG_ERROR("Invalid parameters!");
}
return nb;
}
/***************************************************************************************************************************
SenseAir S8 Library for Serial Modbus Communication
Copyright (c) 2021 Josep Comas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Example of packet (send, hexadecimal bytes):
<FE> <04> <00> <03> <00> <01> <D5> <C5>
<FE> -> Any address (1 byte = 8 bits)
<04> -> Function code (1 byte)
<00> <03> -> Input Register 4 (IR4, #3) (2 bytes = 16 bits), get CO2 measure
<00> <01> -> Read 1 Word (2 bytes)
<D5> <C5> -> CRC (2 bytes)
Answer:
<FE> <04> <02> <01> <90> <AC> <D8>
<FE> -> Any address (1 byte)
<04> -> Function code (1 byte)
<02> -> Length (number of bytes)
<01> <90> -> CO2 value (2 bytes)
<AC> <D8> -> CRC (2 bytes)
Reference: "Modbus on Senseair S8" https://rmtplusstoragesenseair.blob.core.windows.net/docs/Dev/publicerat/TDE2067.pdf
***************************************************************************************************************************/
#ifndef _S8_UART_H
#define _S8_UART_H
#include "Arduino.h"
#include "utils.h"
#define S8_BAUDRATE 9600 // Device to S8 Serial baudrate (should not be changed)
#define S8_TIMEOUT 5000ul // Timeout for communication in milliseconds
#define S8_LEN_BUF_MSG 20 // Max length of buffer for communication with the sensor
#define S8_LEN_FIRMVER 10 // Length of software version
// Modbus
#define MODBUS_ANY_ADDRESS 0XFE // S8 uses any address
#define MODBUS_FUNC_READ_HOLDING_REGISTERS 0X03 // Read holding registers (HR)
#define MODBUS_FUNC_READ_INPUT_REGISTERS 0x04 // Read input registers (IR)
#define MODBUS_FUNC_WRITE_SINGLE_REGISTER 0x06 // Write single register (SR)
// Input registers for S8
#define MODBUS_IR1 0x0000 // MeterStatus
#define MODBUS_IR2 0x0001 // AlarmStatus
#define MODBUS_IR3 0x0002 // OutputStatus
#define MODBUS_IR4 0x0003 // Space CO2
#define MODBUS_IR22 0x0015 // PWM Output
#define MODBUS_IR26 0x0019 // Sensor Type ID High
#define MODBUS_IR27 0x001A // Sensor Type ID Low
#define MODBUS_IR28 0x001B // Memory Map version
#define MODBUS_IR29 0x001C // FW version Main.Sub
#define MODBUS_IR30 0x001D // Sensor ID High
#define MODBUS_IR31 0x001E // Sensor ID Low
// Holding registers for S8
#define MODBUS_HR1 0x0000 // Acknowledgement Register
#define MODBUS_HR2 0x0001 // Special Command Register
#define MODBUS_HR32 0x001F // ABC Period
// Meter status
#define S8_MASK_METER_FATAL_ERROR 0x0001 // Fatal error
#define S8_MASK_METER_OFFSET_REGULATION_ERROR 0x0002 // Offset regulation error
#define S8_MASK_METER_ALGORITHM_ERROR 0x0004 // Algorithm error
#define S8_MASK_METER_OUTPUT_ERROR 0x0008 // Output error
#define S8_MASK_METER_SELF_DIAG_ERROR 0x0010 // Self diagnostics error
#define S8_MASK_METER_OUT_OF_RANGE 0x0020 // Out of range
#define S8_MASK_METER_MEMORY_ERROR 0x0040 // Memory error
#define S8_MASK_METER_ANY_ERROR 0x007F // Mask to detect the previous errors (fatal error ... memory error)
// Output status
#define S8_MASK_OUTPUT_ALARM 0x0001 // Alarm output status (inverted due to Open Collector)
#define S8_MASK_OUTPUT_PWM 0x0002 // PWM output status (=1 -> full output)
// Acknowledgement flags
#define S8_MASK_CO2_BACKGROUND_CALIBRATION 0x0020 // CO2 Background calibration performed = 1
#define S8_MASK_CO2_NITROGEN_CALIBRATION 0x0040 // CO2 Nitrogen calibration performed = 1
// Calibration definitions for special command
#define S8_CO2_BACKGROUND_CALIBRATION 0x7C06 // CO2 Background calibration
#define S8_CO2_ZERO_CALIBRATION 0x7C07 // CO2 Zero calibration
struct S8_sensor {
char firm_version[S8_LEN_FIRMVER + 1];
int16_t co2;
int16_t abc_period;
int16_t ack;
int16_t meter_status;
int16_t alarm_status;
int16_t output_status;
int16_t pwm_output;
int32_t sensor_type_id;
int32_t sensor_id;
int16_t map_version;
};
class S8_UART
{
public:
S8_UART(Stream &serial); // Initialize
/* Information about the sensor */
void get_firmware_version(char firmwver[]); // Get firmware version
int32_t get_sensor_type_ID(); // Get sensor type ID
int32_t get_sensor_ID(); // Get sensor ID
int16_t get_memory_map_version(); // Get memory map version
/* Commands to get CO2 value */
int16_t get_co2(); // Get CO2 value in ppm
int16_t get_PWM_output(); // Get PWM output
/* Automatic calibration */
int16_t get_ABC_period(); // Get ABC period in hours
bool set_ABC_period(int16_t period); // Set ABC period (4 - 4800 hours, 0 to disable)
/* Manual calibration */
bool manual_calibration(); // Start a manual calibration (it clears acknowledgement flags and it calls to
// send_special_command with background calibration command)
// (go to outdoors, wait 5 minutes o more and then you call this command)
/* Bits information */
int16_t get_acknowledgement(); // Get acknowledgement flags
bool clear_acknowledgement(); // Clear acknowledgement flags
int16_t get_meter_status(); // Get meter status
int16_t get_alarm_status(); // Get alarm status
int16_t get_output_status(); // Get output status
/* To execute special commands (ex: manual calibration) */
bool send_special_command(int16_t command); // Send special command
private:
Stream* mySerial; // Serial communication with the sensor
uint8_t buf_msg[S8_LEN_BUF_MSG]; // Buffer for communication messages with the sensor
void serial_write_bytes(uint8_t size); // Send bytes to sensor
uint8_t serial_read_bytes(uint8_t max_bytes, uint32_t timeout_seconds); // Read received bytes from sensor
bool valid_response(uint8_t func, uint8_t nb); // Check if response is valid according to sent command
bool valid_response_len(uint8_t func, uint8_t nb, uint8_t len); // Check if response is valid according to sent command and checking expected total length
void send_cmd(uint8_t func, uint16_t reg, uint16_t value); // Send command
};
#endif
/***************************************************************************************************************************
Utilities Library
Copyright (c) 2021 Josep Comas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
***************************************************************************************************************************/
#include "utils.h"
/* Print nibble as hex */
void printNibble(char byte) {
char value = byte & 0xF; // Cast to nibble
if (value >= 10) {
value += 7;
}
Serial.print((char)(48 + value));
}
/* Print a byte as hex */
void printByte(char byte) {
printNibble(byte >> 4); // high value
printNibble(byte); // low value
}
/* Print several bytes as hex */
void printHex(char *bytes, int size, bool space) {
for (int i=0; i<size; i++) {
printByte(bytes[i]);
if (space)
Serial.print(" ");
}
}
/* Print an integer as hexadecimal value */
void printIntToHex(int32_t value, int size) {
if (size > 3)
printByte(value >> 24);
if (size > 2)
printByte(value >> 16);
if (size > 1)
printByte(value >> 8);
printByte(value);
}
/* Show a number in binary */
void printBinary(int16_t number) {
int16_t k;
for (int8_t c = 15; c >= 0; c--)
{
k = number >> c;
if (k & 1) {
Serial.print("1");
} else {
Serial.print("0");
}
}
}
/***************************************************************************************************************************
Utilities Library
Copyright (c) 2021 Josep Comas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
***************************************************************************************************************************/
#ifndef _JC_UTILS_H
#define _JC_UTILS_H
// Boards with a second hardware serial port we don't use sofware serial library
#if defined ARDUINO_ARCH_SAMD || defined ARDUINO_ARCH_SAM21D || defined ARDUINO_ARCH_ESP32 || defined ARDUINO_SAM_DUE || \
defined ARDUINO_ARCH_APOLLO3 || defined ARDUINO_ARCH_RP2040
#undef USE_SOFTWARE_SERIAL
#else
#define USE_SOFTWARE_SERIAL
#endif
#include "Arduino.h"
#ifdef USE_SOFTWARE_SERIAL
#include <SoftwareSerial.h>
#endif
/*
Overloading macro
https://stackoverflow.com/questions/11761703/overloading-macro-on-number-of-arguments
*/
// get number of arguments with __NARG__
#define __NARG__(...) __NARG_I_(__VA_ARGS__,__RSEQ_N())
#define __NARG_I_(...) __ARG_N(__VA_ARGS__)
#define __ARG_N( \
_1, _2, _3, _4, _5, _6, _7, _8, _9,_10, \
_11,_12,_13,_14,_15,_16,_17,_18,_19,_20, \
_21,_22,_23,_24,_25,_26,_27,_28,_29,_30, \
_31,_32,_33,_34,_35,_36,_37,_38,_39,_40, \
_41,_42,_43,_44,_45,_46,_47,_48,_49,_50, \
_51,_52,_53,_54,_55,_56,_57,_58,_59,_60, \
_61,_62,_63,N,...) N
#define __RSEQ_N() \
63,62,61,60, \
59,58,57,56,55,54,53,52,51,50, \
49,48,47,46,45,44,43,42,41,40, \
39,38,37,36,35,34,33,32,31,30, \
29,28,27,26,25,24,23,22,21,20, \
19,18,17,16,15,14,13,12,11,10, \
9,8,7,6,5,4,3,2,1,0
// general definition for any function name
#define _VFUNC_(name, n) name##n
#define _VFUNC(name, n) _VFUNC_(name, n)
#define VFUNC(func, ...) _VFUNC(func, __NARG__(__VA_ARGS__)) (__VA_ARGS__)
/* End Overloading macro */
/* Debug defines & macros */
#define LOG_DEBUG_LEVEL_NONE (0) // Don't show debug info
#define LOG_DEBUG_LEVEL_ERROR (1) // Show errors
#define LOG_DEBUG_LEVEL_WARN (2) // Show warnings
#define LOG_DEBUG_LEVEL_INFO (3) // Show information
#define LOG_DEBUG_LEVEL_DEBUG (4) // Not used, reserved for compatibility
#define LOG_DEBUG_LEVEL_VERBOSE (5) // Show additional information
#ifdef CORE_DEBUG_LEVEL
#define LOG_DEBUG_LEVEL CORE_DEBUG_LEVEL
#else
#define LOG_DEBUG_LEVEL LOG_DEBUG_LEVEL_NONE
#endif
#if (LOG_DEBUG_LEVEL > LOG_DEBUG_LEVEL_NONE)
// Using Serial.print(value) instead of printf for automatic type printing
// Other debug libraries:
// https://github.com/bblanchon/ArduinoTrace
// https://github.com/hideakitai/DebugLog
#define _LOG_DEBUG(...) VFUNC(_LOG_DEBUG, __VA_ARGS__)
#define _LOG_DEBUG1(info) { Serial.println(info); }
#define _LOG_DEBUG2(info, value) { Serial.print(info); Serial.print(value); Serial.println(""); }
#define _LOG_DEBUG3(info, value, info2) { Serial.print(info); Serial.print(value); Serial.println(info2); }
#define _LOG_DEBUG_SHOW_LEVEL(level) { Serial.print("[DEBUG-"); Serial.print(level); Serial.print("]: "); }
#define LOG_DEBUG_ERROR(...) { _LOG_DEBUG_SHOW_LEVEL("ERROR") _LOG_DEBUG(__VA_ARGS__) }
#if (LOG_DEBUG_LEVEL >= LOG_DEBUG_LEVEL_WARN)
#define LOG_DEBUG_WARN(...) { _LOG_DEBUG_SHOW_LEVEL("WARNING") _LOG_DEBUG(__VA_ARGS__) }
#else
#define LOG_DEBUG_WARN(...)
#endif
#if (LOG_DEBUG_LEVEL >= LOG_DEBUG_LEVEL_INFO)
#define LOG_DEBUG_INFO(...) { _LOG_DEBUG_SHOW_LEVEL("INFO") _LOG_DEBUG(__VA_ARGS__) }
#define LOG_DEBUG_INFO_BINARY(info, flags) { _LOG_DEBUG_SHOW_LEVEL("INFO") Serial.print(info); printBinary(flags); Serial.println(""); }
#define LOG_DEBUG_INFO_HEX(info, value, size) { _LOG_DEBUG_SHOW_LEVEL("INFO") Serial.print(info); printIntToHex(value, size); Serial.println(""); }
#else
#define LOG_DEBUG_INFO(...)
#define LOG_DEBUG_INFO_BINARY(info, flags)
#define LOG_DEBUG_INFO_HEX(info, value, size)
#endif
#if (LOG_DEBUG_LEVEL >= LOG_DEBUG_LEVEL_VERBOSE)
#define LOG_DEBUG_VERBOSE(...) { _LOG_DEBUG_SHOW_LEVEL("VERBOSE") _LOG_DEBUG(__VA_ARGS__) }
#define LOG_DEBUG_VERBOSE_PACKET(info, buf, size) { _LOG_DEBUG_SHOW_LEVEL("VERBOSE") Serial.print(info); \
printHex(buf, size, true); Serial.print("("); Serial.print(size); Serial.println(" bytes)"); }
#else
#define LOG_DEBUG_VERBOSE(...)
#define LOG_DEBUG_VERBOSE_PACKET(info, buf, size)
#endif
#else
#define LOG_DEBUG_ERROR(...)
#define LOG_DEBUG_WARN(...)
#define LOG_DEBUG_INFO(...)
#define LOG_DEBUG_INFO_BINARY(info, flags)
#define LOG_DEBUG_INFO_HEX(info, value, size)
#define LOG_DEBUG_VERBOSE(...)
#define LOG_DEBUG_VERBOSE_PACKET(info, buf, size)
#endif
/* End Debug defines & macros */
/* Print nibble as hex */
void printNibble(char byte);
/* Print a byte as hex */
void printByte(char byte);
/* Print several bytes as hex */
void printHex(char *bytes, int size, bool space);
/* Print an integer as hexadecimal value */
void printIntToHex(int32_t value, int size);
/* Show a number in binary */
void printBinary(int16_t number);
#endif
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment