Compare commits

...

20 Commits

Author SHA1 Message Date
Samuel Siburian d7529ccb89 Merge pull request #375 from airgradienthq/feat/sps30-ooa
Add SPS30 PM sensor support for OPEN_AIR_OUTDOOR
2026-05-27 09:31:20 +04:00
samuelbles07 783b765df6 fix(sps30): use explicit GPIO pins for Serial1
Serial1 on ESP32-C3 defaults to wrong GPIOs. SPS30 wrapper
called begin(115200) without pin args, so Serial1 mapped to
floating pins instead of GPIO 0/1 (S8 connector).

Mirror PMS5003T::begin() pattern: Serial0 uses UART0 defaults,
Serial1 uses SenseAirS8 rx/tx pins from BoardDef.
2026-05-26 11:14:44 +04:00
samuelbles07 d47331c9de feat(outdoor): add SPS30 PM sensor support for OPEN_AIR_OUTDOOR
Indoor SPS30 support (a5e3ea2) only covered ONE_INDOOR.
Outdoor needs 1×SPS30, 2×SPS30, or SPS30+PMS5003T mixed configs.

- Rename hasSensorSPS30 → hasSensorSPS30_1, add hasSensorSPS30_2
- Rename sps30 → sps30_1, add sps30_2 instance
- Parameterize updateSPS30() by sensor ref + channel
- Per-port outdoor detection: PMS5003T first, SPS30 fallback
- SGP41 compensation only from PMS5003T T/RH channels;
  skipped entirely for 2×SPS30 (uses default 25°C/50%RH)
- buildOutdoor/OpenMetrics: firmware version field only for
  PMS5003T channels, PM gating broadened for SPS30
2026-05-25 12:25:25 +04:00
Samuel Siburian 1906cc1606 Merge pull request #374 from airgradienthq/feat/go-iaqs
feat(led): add IAQS score LED bar mode
2026-05-22 15:53:17 +04:00
samuelbles07 2829e1f5a8 fix(oled): center IAQS details 2026-05-22 15:34:45 +04:00
samuelbles07 7068ede0fc feat(oled): show IAQS score panel 2026-05-21 00:27:02 +04:00
samuelbles07 733ddf17ef feat(led): 1:1 IAQS score mapping with notification override
- iaqsHandleLeds renders one LED per score tick (score 10 -> 1 LED,
  score 0 -> 11 LEDs); old 9-LED cap removed.
- In IAQS mode, WiFiLost/ServerLost/SensorConfigFailed clear the bar
  and light only LED 0 with the status color, so the notification
  stays visible at any score. PM/CO2 modes keep their overlay path.
2026-05-20 23:13:14 +05:00
samuelbles07 0b7c8c0cb7 feat(led): add IAQS score LED bar mode
New 'iaqs' value for ledBarMode renders the GO IAQS Starter Score
(PM2.5 + CO2) on the ONE_INDOOR LED bar. Color from category
(Good/Moderate/Unhealthy), LEDs lit from severity; 9-LED cap
mirrors existing PM/CO2 modes so LEDs 0-1 stay free for
connectivity overlays.
2026-05-18 18:46:09 +05:00
samuelbles07 2fd53ac303 Remove BLE write logs 2026-05-13 12:43:29 +05:00
samuelbles07 1c8fc0ffa2 Remove BLE write logs 2026-05-13 12:41:23 +05:00
samuelbles07 e5a3ddccc4 Prepare release 3.6.5
Exclude update count 0.3 and 5.0 for sps30
Update lib deps on how to compile docs
2026-05-13 12:32:07 +05:00
Samuel Siburian ff61145b16 Merge pull request #373 from airgradienthq/feat/multi-dns-a-record
Bump airgradient-client: multi-A-record DNS failover for WiFi HTTPS client
2026-05-13 11:56:34 +05:00
Samuel Siburian a5e3ea2a7b Merge pull request #370 from airgradienthq/feat/sps30-support
feat: add SPS30 PM sensor support for ONE_INDOOR
2026-05-13 11:56:03 +05:00
Samuel Siburian 0e1b517685 Merge branch 'develop' into feat/sps30-support 2026-05-13 11:51:06 +05:00
samuelbles07 4b24bf8819 Bump airgradient-client: support multi DNS IP record 2026-05-12 22:51:18 +05:00
samuelbles07 b370f5f245 fix(sps30): support ESP8266 builds and fix detection after PMS5003
- Replace #error with inline stub class on ESP8266 so AirGradient.h
  compiles on all platforms
- Fully reset serial port before SPS30 detection (end/begin cycle,
  flush RX buffer) to clear stale 9600-baud state from PMS5003
- Add deviceReset() call to put sensor in known state before probing
2026-04-30 16:16:21 +05:00
samuelbles07 03789e53ce Add Sensirion SPS30 deps to github action 2026-04-30 15:55:17 +05:00
samuelbles07 8b4bf500a3 style: revert unintended clang-format changes
Keep only the logical SPS30 edits; restore original formatting
in OneOpenAir.ino and AgValue.cpp.
2026-04-30 15:36:31 +05:00
samuelbles07 170cbf753e feat: add SPS30 PM sensor support for ONE_INDOOR
Add Sensirion SPS30 as an alternative PM sensor on the ONE_INDOOR
board, auto-detected at boot when PMS5003 is not found on Serial0.

- Add SPS30 UART wrapper class (src/SPS30/) following PMS5003 pattern
- Auto-detect: try PMS5003 (9600 baud) first, fall back to SPS30
  (115200 baud) on the same serial port
- Map SPS30 mass concentrations to both Ae and SP fields
- Convert SPS30 number concentrations from #/cm³ to #/0.1L (×100)
- PM0.3 and PM5.0 bins set to invalid (SPS30 lacks these)
- Remove bundled SensirionCore v0.6.0 in favor of PlatformIO-managed
  v0.7.3 to avoid duplicate symbol conflicts
2026-04-30 15:27:31 +05:00
samuelbles07 68ee315352 build: add -fno-exceptions to reduce binary size
Firmware exceeded the 1.87 MB app partition (by ~5 KB) after
adding SPS30 library. Disabling C++ exceptions saves ~185 KB.
2026-04-30 15:24:36 +05:00
55 changed files with 967 additions and 2825 deletions
+4
View File
@@ -80,6 +80,10 @@ jobs:
- source-path: ./
- name: NimBLE-Arduino
version: 2.3.7
- name: Sensirion Core
version: 0.7.3
- name: Sensirion UART SPS30
version: 1.0.0
- name: ArduinoJson
version: 7.4.3
cli-compile-flags: |
+5 -1
View File
@@ -28,7 +28,11 @@ Using library manager install the latest version (Tools ➝ Manage Libraries...
#### Version >= 3.6.0
- Ensure `NimBLE-Arduino` by h2zero library version `2.3.7` is installed using Arduino library manager
- Ensure following library is installed using Arduino library manager
- `NimBLE-Arduino` by h2zero `^2.3.7`
- `Sensirion Core` by Sensirion `^0.7.3`
- `Sensirion UART SPS30` by Sensirion `^1.0.0`
- `ArduinoJson` by Benoit Blanchon `^7.4.3`
- Follow steps of ">= 3.3.0"
3. On tools tab, follow settings below
+1 -1
View File
@@ -142,7 +142,7 @@ If the monitor is set up on the AirGradient dashboard, it will also receive the
| `country` | Country where the device is. | String | Country code as [ALPHA-2 notation](https://www.iban.com/country-codes) | `{"country": "TH"}` |
| `model` | Hardware identifier (only GET). | String | I-9PSL-DE | `{"model": "I-9PSL-DE"}` |
| `pmStandard` | Particle matter standard used on the display. | String | `ugm3`: ug/m3 <br> `us-aqi`: USAQI | `{"pmStandard": "ugm3"}` |
| `ledBarMode` | Mode in which the led bar can be set. | String | `co2`: LED bar displays CO2 <br>`pm`: LED bar displays PM <br>`off`: Turn off LED bar | `{"ledBarMode": "off"}` |
| `ledBarMode` | Mode in which the led bar can be set. | String | `co2`: LED bar displays CO2 <br>`pm`: LED bar displays PM <br>`iaqs`: LED bar displays GO IAQS Starter Score (PM2.5 + CO2) <br>`off`: Turn off LED bar | `{"ledBarMode": "off"}` |
| `displayBrightness` | Brightness of the Display. | Number | 0-100 | `{"displayBrightness": 50}` |
| `ledBarBrightness` | Brightness of the LEDBar. | Number | 0-100 | `{"ledBarBrightness": 40}` |
| `abcDays` | Number of days for CO2 automatic baseline calibration. | Number | Maximum 200 days. Default 8 days. | `{"abcDays": 8}` |
+159 -75
View File
@@ -140,6 +140,7 @@ static void configUpdateHandle(void);
static void updateDisplayAndLedBar(void);
static void updateTvoc(void);
static void updatePm(void);
static void updateSPS30(SPS30 &sensor, int channel);
static void sendDataToServer(void);
static void tempHumUpdate(void);
static void co2Update(void);
@@ -334,7 +335,8 @@ void loop() {
if (configuration.hasSensorS8) {
co2Schedule.run();
}
if (configuration.hasSensorPMS1 || configuration.hasSensorPMS2) {
if (configuration.hasSensorPMS1 || configuration.hasSensorPMS2 ||
configuration.hasSensorSPS30_1 || configuration.hasSensorSPS30_2) {
pmsSchedule.run();
}
if (ag->isOne()) {
@@ -822,12 +824,18 @@ static void oneIndoorInit(void) {
dispSensorNotFound("S8");
}
/** Init PMS5003 */
/** Init PMS5003, fallback to SPS30 if not found */
if (ag->pms5003.begin(Serial0) == false) {
Serial.println("PMS sensor not found");
Serial.println("PMS5003 not found, trying SPS30...");
configuration.hasSensorPMS1 = false;
dispSensorNotFound("PMS");
if (ag->sps30_1.begin(Serial0)) {
Serial.println("SPS30 detected on Serial0");
configuration.hasSensorSPS30_1 = true;
} else {
Serial.println("SPS30 not found either");
dispSensorNotFound("PM sensor");
}
}
}
static void openAirInit(void) {
@@ -880,56 +888,100 @@ static void openAirInit(void) {
}
}
/** Attempt to detect PM sensors */
if (fwMode == FW_MODE_O_1PST) {
bool pmInitSuccess = false;
/**
* Attempt to detect PM sensors on available serial ports.
* Per-port order: PMS5003T first (@9600), fallback to SPS30 (@115200).
* For single-PM modes the detected sensor is always assigned to channel 1.
* For dual-PM modes Serial0 → channel 1, Serial1 → channel 2.
*/
auto detectPmOnSerial = [](HardwareSerial &serial, PMS5003T &pms, SPS30 &sps,
const char *portName) -> int {
// Returns: 0 = none, 1 = PMS5003T, 2 = SPS30
if (pms.begin(serial)) {
Serial.printf("Detected PMS5003T on %s\n", portName);
return 1;
}
Serial.printf("PMS5003T not found on %s, trying SPS30...\n", portName);
if (sps.begin(serial)) {
Serial.printf("Detected SPS30 on %s\n", portName);
return 2;
}
Serial.printf("No PM sensor detected on %s\n", portName);
return 0;
};
if (fwMode == FW_MODE_O_1PST || fwMode == FW_MODE_O_1PS) {
// Single PM channel expected — try Serial0 first, fallback Serial1
configuration.hasSensorPMS1 = false;
configuration.hasSensorPMS2 = false;
bool pmFound = false;
if (serial0Available) {
if (ag->pms5003t_1.begin(Serial0) == false) {
configuration.hasSensorPMS1 = false;
Serial.println("No PM sensor detected on Serial0");
} else {
int result =
detectPmOnSerial(Serial0, ag->pms5003t_1, ag->sps30_1, "Serial0");
if (result == 1) {
configuration.hasSensorPMS1 = true;
serial0Available = false;
pmInitSuccess = true;
Serial.println("Detected PM 1 on Serial0");
pmFound = true;
} else if (result == 2) {
configuration.hasSensorSPS30_1 = true;
serial0Available = false;
pmFound = true;
}
}
if (pmInitSuccess == false) {
if (serial1Available) {
if (ag->pms5003t_1.begin(Serial1) == false) {
configuration.hasSensorPMS1 = false;
Serial.println("No PM sensor detected on Serial1");
} else {
serial1Available = false;
Serial.println("Detected PM 1 on Serial1");
}
if (!pmFound && serial1Available) {
int result =
detectPmOnSerial(Serial1, ag->pms5003t_1, ag->sps30_1, "Serial1");
if (result == 1) {
configuration.hasSensorPMS1 = true;
serial1Available = false;
} else if (result == 2) {
configuration.hasSensorSPS30_1 = true;
serial1Available = false;
}
}
configuration.hasSensorPMS2 = false; // Disable PM2
} else {
if (ag->pms5003t_1.begin(Serial0) == false) {
configuration.hasSensorPMS1 = false;
Serial.println("No PM sensor detected on Serial0");
} else {
Serial.println("Detected PM 1 on Serial0");
}
if (ag->pms5003t_2.begin(Serial1) == false) {
configuration.hasSensorPMS2 = false;
Serial.println("No PM sensor detected on Serial1");
} else {
Serial.println("Detected PM 2 on Serial1");
// Dual PM channel modes (O_1PPT / O_1PP) — Serial0 → ch1, Serial1 → ch2
configuration.hasSensorPMS1 = false;
configuration.hasSensorPMS2 = false;
// Channel 1 on Serial0
int result1 =
detectPmOnSerial(Serial0, ag->pms5003t_1, ag->sps30_1, "Serial0");
if (result1 == 1) {
configuration.hasSensorPMS1 = true;
} else if (result1 == 2) {
configuration.hasSensorSPS30_1 = true;
}
// Channel 2 on Serial1
int result2 =
detectPmOnSerial(Serial1, ag->pms5003t_2, ag->sps30_2, "Serial1");
if (result2 == 1) {
configuration.hasSensorPMS2 = true;
} else if (result2 == 2) {
configuration.hasSensorSPS30_2 = true;
}
// Check if we should downgrade from two-PM to single-PM mode
if (fwMode == FW_MODE_O_1PP) {
int count = (configuration.hasSensorPMS1 ? 1 : 0) + (configuration.hasSensorPMS2 ? 1 : 0);
if (count == 1) {
bool ch1HasPm =
configuration.hasSensorPMS1 || configuration.hasSensorSPS30_1;
bool ch2HasPm =
configuration.hasSensorPMS2 || configuration.hasSensorSPS30_2;
if (ch1HasPm != ch2HasPm) {
fwMode = FW_MODE_O_1P;
}
}
}
/** update the PMS poll period base on fw mode and sensor available */
if (fwMode != FW_MODE_O_1PST) {
if (configuration.hasSensorPMS1 && configuration.hasSensorPMS2) {
/** Update the PMS poll period based on fw mode and sensor availability */
if (fwMode != FW_MODE_O_1PST && fwMode != FW_MODE_O_1PS) {
bool ch1HasPm =
configuration.hasSensorPMS1 || configuration.hasSensorSPS30_1;
bool ch2HasPm =
configuration.hasSensorPMS2 || configuration.hasSensorSPS30_2;
if (ch1HasPm && ch2HasPm) {
pmsSchedule.setPeriod(2000);
}
}
@@ -1308,19 +1360,53 @@ static void updatePMS5003() {
}
}
static void updateSPS30(SPS30 &sensor, int channel) {
if (sensor.readValues()) {
// Mass concentrations — mapped to both Ae and SP (SPS30 has no distinction)
measurements.update(Measurements::PM01, sensor.getPm01Ae(), channel);
measurements.update(Measurements::PM25, sensor.getPm25Ae(), channel);
measurements.update(Measurements::PM10, sensor.getPm10Ae(), channel);
measurements.update(Measurements::PM01_SP, sensor.getPm01Sp(), channel);
measurements.update(Measurements::PM25_SP, sensor.getPm25Sp(), channel);
measurements.update(Measurements::PM10_SP, sensor.getPm10Sp(), channel);
// Number concentrations (already converted to #/0.1L by wrapper)
measurements.update(Measurements::PM05_PC, sensor.getPm05ParticleCount(), channel);
measurements.update(Measurements::PM01_PC, sensor.getPm01ParticleCount(), channel);
measurements.update(Measurements::PM25_PC, sensor.getPm25ParticleCount(), channel);
measurements.update(Measurements::PM10_PC, sensor.getPm10ParticleCount(), channel);
} else {
measurements.update(Measurements::PM01, utils::getInvalidPmValue(), channel);
measurements.update(Measurements::PM25, utils::getInvalidPmValue(), channel);
measurements.update(Measurements::PM10, utils::getInvalidPmValue(), channel);
measurements.update(Measurements::PM01_SP, utils::getInvalidPmValue(), channel);
measurements.update(Measurements::PM25_SP, utils::getInvalidPmValue(), channel);
measurements.update(Measurements::PM10_SP, utils::getInvalidPmValue(), channel);
measurements.update(Measurements::PM01_PC, utils::getInvalidPmValue(), channel);
measurements.update(Measurements::PM25_PC, utils::getInvalidPmValue(), channel);
measurements.update(Measurements::PM5_PC, utils::getInvalidPmValue(), channel);
measurements.update(Measurements::PM10_PC, utils::getInvalidPmValue(), channel);
}
}
static void updatePm(void) {
if (ag->isOne()) {
updatePMS5003();
if (configuration.hasSensorSPS30_1) {
updateSPS30(ag->sps30_1, 1);
} else {
updatePMS5003();
}
return;
}
// Open Air Monitor series, can have two PMS5003T sensor
bool newPMS1Value = false;
bool newPMS2Value = false;
// Open Air Monitor series — each channel can be PMS5003T or SPS30.
// Track which channels produced valid PMS5003T T/RH for SGP41 compensation.
bool newPmsTempHumCh1 = false;
bool newPmsTempHumCh2 = false;
// Read PMS channel 1 if available
int channel = 1;
// ---- Channel 1 ----
if (configuration.hasSensorPMS1) {
int channel = 1;
if (ag->pms5003t_1.connected()) {
measurements.update(Measurements::PM01, ag->pms5003t_1.getPm01Ae(), channel);
measurements.update(Measurements::PM25, ag->pms5003t_1.getPm25Ae(), channel);
@@ -1334,11 +1420,8 @@ static void updatePm(void) {
measurements.update(Measurements::PM25_PC, ag->pms5003t_1.getPm25ParticleCount(), channel);
measurements.update(Measurements::Temperature, ag->pms5003t_1.getTemperature(), channel);
measurements.update(Measurements::Humidity, ag->pms5003t_1.getRelativeHumidity(), channel);
// flag that new valid PMS value exists
newPMS1Value = true;
newPmsTempHumCh1 = true;
} else {
// PMS channel 1 now is not connected, update using invalid value
measurements.update(Measurements::PM01, utils::getInvalidPmValue(), channel);
measurements.update(Measurements::PM25, utils::getInvalidPmValue(), channel);
measurements.update(Measurements::PM10, utils::getInvalidPmValue(), channel);
@@ -1352,11 +1435,13 @@ static void updatePm(void) {
measurements.update(Measurements::Temperature, utils::getInvalidTemperature(), channel);
measurements.update(Measurements::Humidity, utils::getInvalidHumidity(), channel);
}
} else if (configuration.hasSensorSPS30_1) {
updateSPS30(ag->sps30_1, 1);
}
// Read PMS channel 2 if available
channel = 2;
// ---- Channel 2 ----
if (configuration.hasSensorPMS2) {
int channel = 2;
if (ag->pms5003t_2.connected()) {
measurements.update(Measurements::PM01, ag->pms5003t_2.getPm01Ae(), channel);
measurements.update(Measurements::PM25, ag->pms5003t_2.getPm25Ae(), channel);
@@ -1370,11 +1455,8 @@ static void updatePm(void) {
measurements.update(Measurements::PM25_PC, ag->pms5003t_2.getPm25ParticleCount(), channel);
measurements.update(Measurements::Temperature, ag->pms5003t_2.getTemperature(), channel);
measurements.update(Measurements::Humidity, ag->pms5003t_2.getRelativeHumidity(), channel);
// flag that new valid PMS value exists
newPMS2Value = true;
newPmsTempHumCh2 = true;
} else {
// PMS channel 2 now is not connected, update using invalid value
measurements.update(Measurements::PM01, utils::getInvalidPmValue(), channel);
measurements.update(Measurements::PM25, utils::getInvalidPmValue(), channel);
measurements.update(Measurements::PM10, utils::getInvalidPmValue(), channel);
@@ -1388,30 +1470,32 @@ static void updatePm(void) {
measurements.update(Measurements::Temperature, utils::getInvalidTemperature(), channel);
measurements.update(Measurements::Humidity, utils::getInvalidHumidity(), channel);
}
} else if (configuration.hasSensorSPS30_2) {
updateSPS30(ag->sps30_2, 2);
}
// SGP41 compensation — only uses T/RH from PMS5003T channels (SPS30 has no T/RH)
if (configuration.hasSensorSGP) {
float temp, hum;
if (newPMS1Value && newPMS2Value) {
// Both PMS has new valid value
temp = (measurements.getFloat(Measurements::Temperature, 1) +
measurements.getFloat(Measurements::Temperature, 2)) /
2.0f;
hum = (measurements.getFloat(Measurements::Humidity, 1) +
measurements.getFloat(Measurements::Humidity, 2)) /
2.0f;
} else if (newPMS1Value) {
// Only PMS1 has new valid value
temp = measurements.getFloat(Measurements::Temperature, 1);
hum = measurements.getFloat(Measurements::Humidity, 1);
} else {
// Only PMS2 has new valid value
temp = measurements.getFloat(Measurements::Temperature, 2);
hum = measurements.getFloat(Measurements::Humidity, 2);
if (newPmsTempHumCh1 || newPmsTempHumCh2) {
float temp, hum;
if (newPmsTempHumCh1 && newPmsTempHumCh2) {
temp = (measurements.getFloat(Measurements::Temperature, 1) +
measurements.getFloat(Measurements::Temperature, 2)) /
2.0f;
hum = (measurements.getFloat(Measurements::Humidity, 1) +
measurements.getFloat(Measurements::Humidity, 2)) /
2.0f;
} else if (newPmsTempHumCh1) {
temp = measurements.getFloat(Measurements::Temperature, 1);
hum = measurements.getFloat(Measurements::Humidity, 1);
} else {
temp = measurements.getFloat(Measurements::Temperature, 2);
hum = measurements.getFloat(Measurements::Humidity, 2);
}
ag->sgp41.setCompensationTemperatureHumidity(temp, hum);
}
// Update compensation temperature and humidity for SGP41
ag->sgp41.setCompensationTemperatureHumidity(temp, hum);
// When no PMS5003T channel provides T/RH (e.g. 2× SPS30), SGP41 keeps
// its previous compensation values (default 25 °C / 50 %RH on first run).
}
}
+34 -15
View File
@@ -77,14 +77,28 @@ String OpenMetrics::getPayload(void) {
int nox = utils::getInvalidNOx();
int noxRaw = utils::getInvalidNOx();
// Convenience flags: does this channel have any PM sensor?
bool ch1HasPm = config.hasSensorPMS1 || config.hasSensorSPS30_1;
bool ch2HasPm = config.hasSensorPMS2 || config.hasSensorSPS30_2;
// Get values
if (config.hasSensorPMS1 && config.hasSensorPMS2) {
_temp = (measure.getFloat(Measurements::Temperature, 1) +
measure.getFloat(Measurements::Temperature, 2)) /
2.0f;
_hum = (measure.getFloat(Measurements::Humidity, 1) +
measure.getFloat(Measurements::Humidity, 2)) /
2.0f;
if (ch1HasPm && ch2HasPm) {
// Two PM channels — average T/RH only from PMS5003T channels
if (config.hasSensorPMS1 && config.hasSensorPMS2) {
_temp = (measure.getFloat(Measurements::Temperature, 1) +
measure.getFloat(Measurements::Temperature, 2)) /
2.0f;
_hum = (measure.getFloat(Measurements::Humidity, 1) +
measure.getFloat(Measurements::Humidity, 2)) /
2.0f;
} else if (config.hasSensorPMS1) {
_temp = measure.getFloat(Measurements::Temperature, 1);
_hum = measure.getFloat(Measurements::Humidity, 1);
} else if (config.hasSensorPMS2) {
_temp = measure.getFloat(Measurements::Temperature, 2);
_hum = measure.getFloat(Measurements::Humidity, 2);
}
// PM values averaged across both channels regardless of brand
pm01 = (measure.get(Measurements::PM01, 1) + measure.get(Measurements::PM01, 2)) / 2.0f;
float correctedPm25_1 = measure.getCorrectedPM25(false, 1);
float correctedPm25_2 = measure.getCorrectedPM25(false, 2);
@@ -100,7 +114,7 @@ String OpenMetrics::getPayload(void) {
_hum = measure.getFloat(Measurements::Humidity);
}
if (config.hasSensorPMS1) {
if (ch1HasPm) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(false, 1);
pm25 = round(correctedPm);
@@ -108,18 +122,23 @@ String OpenMetrics::getPayload(void) {
pm03PCount = measure.get(Measurements::PM03_PC);
}
} else {
if (config.hasSensorPMS1) {
_temp = measure.getFloat(Measurements::Temperature, 1);
_hum = measure.getFloat(Measurements::Humidity, 1);
// Outdoor single-channel: T/RH only from PMS5003T, PM from any sensor
if (ch1HasPm) {
if (config.hasSensorPMS1) {
_temp = measure.getFloat(Measurements::Temperature, 1);
_hum = measure.getFloat(Measurements::Humidity, 1);
}
pm01 = measure.get(Measurements::PM01, 1);
float correctedPm = measure.getCorrectedPM25(false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10, 1);
pm03PCount = measure.get(Measurements::PM03_PC, 1);
}
if (config.hasSensorPMS2) {
_temp = measure.getFloat(Measurements::Temperature, 2);
_hum = measure.getFloat(Measurements::Humidity, 2);
if (ch2HasPm) {
if (config.hasSensorPMS2) {
_temp = measure.getFloat(Measurements::Temperature, 2);
_hum = measure.getFloat(Measurements::Humidity, 2);
}
pm01 = measure.get(Measurements::PM01, 2);
float correctedPm = measure.getCorrectedPM25(false, 2);
pm25 = round(correctedPm);
@@ -154,7 +173,7 @@ String OpenMetrics::getPayload(void) {
}
// Add measurements that valid to the metrics
if (config.hasSensorPMS1 || config.hasSensorPMS2) {
if (ch1HasPm || ch2HasPm) {
if (utils::isValidPm(pm01)) {
add_metric("pm1",
"PM1.0 concentration as measured by the AirGradient PMS "
+1 -1
View File
@@ -1,5 +1,5 @@
name=AirGradient Air Quality Sensor
version=3.6.4
version=3.6.5
author=AirGradient <support@airgradient.com>
maintainer=AirGradient <support@airgradient.com>
sentence=ESP32-C3 / ESP8266 library for air quality monitor measuring PM, CO2, Temperature, TVOC and Humidity with OLED display.
+3 -1
View File
@@ -27,7 +27,9 @@ lib_deps =
Update
DNSServer
h2zero/NimBLE-Arduino@^2.1.0
bblanchon/ArduinoJson@^7
sensirion/Sensirion Core@^0.7.3
sensirion/Sensirion UART SPS30@^1.0.0
bblanchon/ArduinoJson@^7.4.3
[env:esp8266]
platform = espressif8266
+10 -2
View File
@@ -19,6 +19,7 @@ const char *LED_BAR_MODE_NAMES[] = {
[LedBarModeOff] = "off",
[LedBarModePm] = "pm",
[LedBarModeCO2] = "co2",
[LedBarModeIaqs] = "iaqs",
};
const char *PM_CORRECTION_ALGORITHM_NAMES[] = {
@@ -112,6 +113,8 @@ String Configuration::getLedBarModeName(LedBarMode mode) {
return String(LED_BAR_MODE_NAMES[LedBarModePm]);
} else if (mode == LedBarModeCO2) {
return String(LED_BAR_MODE_NAMES[LedBarModeCO2]);
} else if (mode == LedBarModeIaqs) {
return String(LED_BAR_MODE_NAMES[LedBarModeIaqs]);
}
return String("unknown");
}
@@ -740,7 +743,8 @@ bool Configuration::parse(String data, bool isLocal) {
String mode = root[jprop_ledBarMode];
if (mode == getLedBarModeName(LedBarMode::LedBarModeCO2) ||
mode == getLedBarModeName(LedBarMode::LedBarModeOff) ||
mode == getLedBarModeName(LedBarMode::LedBarModePm)) {
mode == getLedBarModeName(LedBarMode::LedBarModePm) ||
mode == getLedBarModeName(LedBarMode::LedBarModeIaqs)) {
String oldMode = jconfig[jprop_ledBarMode];
if (mode != oldMode) {
jconfig[jprop_ledBarMode] = mode;
@@ -1190,6 +1194,9 @@ LedBarMode Configuration::getLedBarMode(void) {
if (mode == getLedBarModeName(LedBarModePm)) {
return LedBarModePm;
}
if (mode == getLedBarModeName(LedBarModeIaqs)) {
return LedBarModeIaqs;
}
return LedBarModeOff;
}
@@ -1398,7 +1405,8 @@ void Configuration::toConfig(const char *buf) {
String mode = jconfig[jprop_ledBarMode];
if (mode != getLedBarModeName(LedBarMode::LedBarModeCO2) &&
mode != getLedBarModeName(LedBarMode::LedBarModeOff) &&
mode != getLedBarModeName(LedBarMode::LedBarModePm)) {
mode != getLedBarModeName(LedBarMode::LedBarModePm) &&
mode != getLedBarModeName(LedBarMode::LedBarModeIaqs)) {
isConfigFieldInvalid = true;
} else {
isConfigFieldInvalid = false;
+7 -3
View File
@@ -1,11 +1,11 @@
#ifndef _AG_CONFIG_H_
#define _AG_CONFIG_H_
#include "App/AppDef.h"
#include "Main/PrintLog.h"
#include "AirGradient.h"
#include <Arduino.h>
#include "App/AppDef.h"
#include "Libraries/Arduino_JSON/src/Arduino_JSON.h"
#include "Main/PrintLog.h"
#include <Arduino.h>
#define MAX_SATELLITES 10
@@ -75,6 +75,10 @@ public:
bool hasSensorS8 = true;
bool hasSensorPMS1 = true;
bool hasSensorPMS2 = true;
bool hasSensorSPS30_1 =
false; ///< SPS30 detected on PM channel 1 (auto-detect)
bool hasSensorSPS30_2 =
false; ///< SPS30 detected on PM channel 2 (auto-detect)
bool hasSensorSGP = true;
bool hasSensorSHT = true;
+91 -18
View File
@@ -1,5 +1,6 @@
#include "AgOledDisplay.h"
#include "Libraries/U8g2/src/U8g2lib.h"
#include "Main/GoIaqs.h"
#include "Main/utils.h"
/** Cast U8G2 */
@@ -411,28 +412,100 @@ void OledDisplay::showDashboard(DashboardStatus status) {
DISP()->drawUTF8(55, 61, "ug/m³");
}
/** Draw tvocIndexlabel */
DISP()->setFont(u8g2_font_t0_12_tf);
DISP()->drawStr(100, 27, "VOC:");
if (config.getLedBarMode() == LedBarMode::LedBarModeIaqs) {
/** Draw IAQS panel (column 3 replaces VOC/NOx when LED bar mode
* is set to iaqs). Three rows aligned with the CO2/PM2.5 column
* rhythm: header (y=27), big score + grade letter (y=48),
* dominant pollutant (y=61). */
const int IAQS_COL_X = 100;
const int IAQS_COL_W = 28;
const int SCORE_GRADE_GAP = 1;
const int ARROW_DOM_GAP = 2;
const int ARROW_DOM_BOTH_GAP = 1;
DISP()->setFont(u8g2_font_t0_12_tf);
DISP()->drawStr(IAQS_COL_X, 27, "IAQS");
/** Draw tvocIndexvalue */
int tvoc = round(value.getAverage(Measurements::TVOC));
if (utils::isValidVOC(tvoc)) {
sprintf(strBuf, "%d", tvoc);
} else {
sprintf(strBuf, "%s", "-");
}
DISP()->drawStr(100, 39, strBuf);
float pm25Avg = value.getAverage(Measurements::PM25);
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
pm25Avg = value.getCorrectedPM25(true);
}
float co2Avg = value.getAverage(Measurements::CO2);
bool pmOk = utils::isValidPm((int)round(pm25Avg));
bool coOk = utils::isValidCO2((int)round(co2Avg));
/** Draw NOx label */
int nox = round(value.getAverage(Measurements::NOx));
DISP()->drawStr(100, 53, "NOx:");
if (utils::isValidNOx(nox)) {
sprintf(strBuf, "%d", nox);
if (!pmOk || !coOk) {
DISP()->setFont(u8g2_font_t0_22b_tf);
int scoreW = DISP()->getStrWidth("-");
DISP()->drawStr(IAQS_COL_X + ((IAQS_COL_W - scoreW) / 2), 48, "-");
} else {
int pmScore = GoIaqs::pm25Score(pm25Avg);
int coScore = GoIaqs::co2Score(co2Avg);
int totalIaqs = GoIaqs::totalScore(pmScore, coScore);
GoIaqs::Category cat = GoIaqs::categoryOf(totalIaqs);
GoIaqs::Dominant dom = GoIaqs::dominantOf(pmScore, coScore);
/** Row 2: big score + letter grade centered in the IAQS column. */
sprintf(strBuf, "%d", totalIaqs);
DISP()->setFont(u8g2_font_t0_22b_tf);
int scoreW = DISP()->getStrWidth(strBuf);
char gradeStr[2] = {GoIaqs::letterOf(cat), '\0'};
DISP()->setFont(u8g2_font_t0_12_tf);
int gradeW = DISP()->getStrWidth(gradeStr);
int scoreX = IAQS_COL_X +
((IAQS_COL_W - scoreW - SCORE_GRADE_GAP - gradeW) / 2);
DISP()->setFont(u8g2_font_t0_22b_tf);
DISP()->drawStr(scoreX, 48, strBuf);
DISP()->setFont(u8g2_font_t0_12_tf);
DISP()->drawStr(scoreX + scoreW + SCORE_GRADE_GAP, 48, gradeStr);
/** Row 3: dominant pollutant. */
const char *domStr = "BOTH";
if (dom == GoIaqs::DominantPm25) {
domStr = "PM";
} else if (dom == GoIaqs::DominantCo2) {
domStr = "CO2";
}
DISP()->setFont(u8g2_font_t0_12_tf);
const int ARROW_W = 5;
int arrowDomGap = (dom == GoIaqs::DominantBoth) ? ARROW_DOM_BOTH_GAP
: ARROW_DOM_GAP;
int domW = DISP()->getStrWidth(domStr);
int domX = IAQS_COL_X +
((IAQS_COL_W - ARROW_W - arrowDomGap - domW) / 2);
int arrowY = 57;
DISP()->drawLine(domX, arrowY, domX + ARROW_W, arrowY);
DISP()->drawLine(domX + ARROW_W, arrowY, domX + ARROW_W - 2,
arrowY - 2);
DISP()->drawLine(domX + ARROW_W, arrowY, domX + ARROW_W - 2,
arrowY + 2);
DISP()->drawStr(domX + ARROW_W + arrowDomGap, 61, domStr);
}
} else {
sprintf(strBuf, "%s", "-");
/** Draw tvocIndexlabel */
DISP()->setFont(u8g2_font_t0_12_tf);
DISP()->drawStr(100, 27, "VOC:");
/** Draw tvocIndexvalue */
int tvoc = round(value.getAverage(Measurements::TVOC));
if (utils::isValidVOC(tvoc)) {
sprintf(strBuf, "%d", tvoc);
} else {
sprintf(strBuf, "%s", "-");
}
DISP()->drawStr(100, 39, strBuf);
/** Draw NOx label */
int nox = round(value.getAverage(Measurements::NOx));
DISP()->drawStr(100, 53, "NOx:");
if (utils::isValidNOx(nox)) {
sprintf(strBuf, "%d", nox);
} else {
sprintf(strBuf, "%s", "-");
}
DISP()->drawStr(100, 63, strBuf);
}
DISP()->drawStr(100, 63, strBuf);
} while (DISP()->nextPage());
} else if (ag->isBasic()) {
ag->display.clear();
+75 -6
View File
@@ -1,5 +1,6 @@
#include "AgStateMachine.h"
#include "AgOledDisplay.h"
#include "Main/GoIaqs.h"
#define LED_TEST_BLINK_DELAY 50 /** ms */
#define LED_FAST_BLINK_DELAY 250 /** ms */
@@ -62,6 +63,9 @@ bool StateMachine::sensorhandleLeds(void) {
case LedBarMode::LedBarModePm:
totalLedUsed = pm25handleLeds();
break;
case LedBarMode::LedBarModeIaqs:
totalLedUsed = iaqsHandleLeds();
break;
default:
ag->ledBar.clear();
break;
@@ -267,6 +271,54 @@ int StateMachine::pm25handleLeds(void) {
return totalUsed;
}
/**
* @brief Show GO IAQS Starter Score LED status.
*
* Computes the score from PM2.5 (corrected when enabled) and CO2 averages,
* then lights a number of LEDs (from the right end) proportional to the
* severity, all painted with the category color (Good/Moderate/Unhealthy).
*
* Mapping is 1:1 between LEDs and score: ledsLit = numLeds - totalScore,
* so score 10 lights 1 LED (LED 10) and score 0 lights all 11 LEDs.
* Connectivity-status overlays (WiFiLost, ServerLost, SensorConfigFailed)
* are handled at the dispatcher level for IAQS mode: the entire bar is
* turned off and only LED 0 is lit with the notification color, so the
* notification is always unambiguous regardless of the IAQS score.
*
* @return number of LEDs used on the bar.
*/
int StateMachine::iaqsHandleLeds(void) {
float pm25Value = value.getAverage(Measurements::PM25);
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
pm25Value = value.getCorrectedPM25(true);
}
float co2Value = value.getAverage(Measurements::CO2);
int pmScore = GoIaqs::pm25Score(pm25Value);
int coScore = GoIaqs::co2Score(co2Value);
int total = GoIaqs::totalScore(pmScore, coScore);
GoIaqs::Rgb color = GoIaqs::colorOf(GoIaqs::categoryOf(total));
int numLeds = ag->ledBar.getNumberOfLeds();
/** 1:1 LED-per-score mapping: worse score -> more LEDs lit. */
int ledsLit = numLeds - total;
if (ledsLit < 1) {
ledsLit = 1;
}
if (ledsLit > numLeds) {
ledsLit = numLeds;
}
for (int i = 0; i < ledsLit; i++) {
ag->ledBar.setColor(color.r, color.g, color.b, numLeds - 1 - i);
}
Serial.printf("GO IAQS = %d [pm25 score %d, co2 score %d, leds %d]\n", total,
pmScore, coScore, ledsLit);
return ledsLit;
}
void StateMachine::co2Calibration(void) {
if (config.isCo2CalibrationRequested() && config.hasSensorS8) {
logInfo("CO2 Calibration");
@@ -764,9 +816,16 @@ void StateMachine::handleLeds(AgStateMachineState state) {
/** Connection to WiFi network failed credentials incorrect encryption not
* supported etc. */
if (ag->isOne()) {
bool allUsed = sensorhandleLeds();
if (allUsed == false) {
if (config.getLedBarMode() == LedBarMode::LedBarModeIaqs) {
/** IAQS mode: suppress the score bar so only the notification LED
* is visible. */
ag->ledBar.clear();
ag->ledBar.setColor(255, 0, 0, 0);
} else {
bool allUsed = sensorhandleLeds();
if (allUsed == false) {
ag->ledBar.setColor(255, 0, 0, 0);
}
}
} else {
ag->statusLed.setOff();
@@ -777,9 +836,14 @@ void StateMachine::handleLeds(AgStateMachineState state) {
/** Connected to WiFi network but the server cannot be reached through the
* internet, e.g. blocked by firewall */
if (ag->isOne()) {
bool allUsed = sensorhandleLeds();
if (allUsed == false) {
if (config.getLedBarMode() == LedBarMode::LedBarModeIaqs) {
ag->ledBar.clear();
ag->ledBar.setColor(233, 183, 54, 0);
} else {
bool allUsed = sensorhandleLeds();
if (allUsed == false) {
ag->ledBar.setColor(233, 183, 54, 0);
}
}
} else {
ag->statusLed.setOff();
@@ -790,9 +854,14 @@ void StateMachine::handleLeds(AgStateMachineState state) {
/** Server is reachable but there is some configuration issue to be fixed on
* the server side */
if (ag->isOne()) {
bool allUsed = sensorhandleLeds();
if (allUsed == false) {
if (config.getLedBarMode() == LedBarMode::LedBarModeIaqs) {
ag->ledBar.clear();
ag->ledBar.setColor(139, 24, 248, 0);
} else {
bool allUsed = sensorhandleLeds();
if (allUsed == false) {
ag->ledBar.setColor(139, 24, 248, 0);
}
}
} else {
ag->statusLed.setOff();
+1
View File
@@ -27,6 +27,7 @@ private:
bool sensorhandleLeds(void);
int co2handleLeds(void);
int pm25handleLeds(void);
int iaqsHandleLeds(void);
void co2Calibration(void);
void ledBarTest(void);
void ledBarPowerUpTest(void);
+27 -25
View File
@@ -125,7 +125,7 @@ void Measurements::printCurrentAverage() {
}
}
if (config.hasSensorPMS1) {
if (config.hasSensorPMS1 || config.hasSensorSPS30_1) {
printCurrentPMAverage(1);
if (!config.hasSensorSHT) {
if (utils::isValidTemperature(_temperature[0].update.avg)) {
@@ -140,7 +140,7 @@ void Measurements::printCurrentAverage() {
}
}
}
if (config.hasSensorPMS2) {
if (config.hasSensorPMS2 || config.hasSensorSPS30_2) {
printCurrentPMAverage(2);
if (!config.hasSensorSHT) {
if (utils::isValidTemperature(_temperature[1].update.avg)) {
@@ -1161,38 +1161,39 @@ String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi)
JSONVar Measurements::buildOutdoor(bool localServer, AgFirmwareMode fwMode) {
JSONVar outdoor;
bool ch1HasPm = config.hasSensorPMS1 || config.hasSensorSPS30_1;
bool ch2HasPm = config.hasSensorPMS2 || config.hasSensorSPS30_2;
if (fwMode == FW_MODE_O_1P || fwMode == FW_MODE_O_1PS || fwMode == FW_MODE_O_1PST) {
// buildPMS params:
/// Because only have 1 PMS, allCh is set to false
/// But enable temp hum from PMS
/// compensated values if requested by local server
/// Set ch based on hasSensorPMSx
if (config.hasSensorPMS1) {
// Single PM channel — pick whichever channel is populated
// Enable temp/hum from PMS; compensated values if requested by local server
if (ch1HasPm) {
outdoor = buildPMS(1, false, true, localServer);
if (!localServer) {
// Firmware version only available for PMS5003T
if (!localServer && config.hasSensorPMS1) {
outdoor[json_prop_pmFirmware] =
pms5003TFirmwareVersion(ag->pms5003t_1.getFirmwareVersion());
}
} else {
} else if (ch2HasPm) {
outdoor = buildPMS(2, false, true, localServer);
if (!localServer) {
if (!localServer && config.hasSensorPMS2) {
outdoor[json_prop_pmFirmware] =
pms5003TFirmwareVersion(ag->pms5003t_2.getFirmwareVersion());
}
}
} else {
// FW_MODE_O_1PPT && FW_MODE_O_1PP: Outdoor monitor that have 2 PMS sensor
// buildPMS params:
/// Have 2 PMS sensor, allCh is set to true (ch params ignored)
/// Enable temp hum from PMS
/// compensated values if requested by local server
// FW_MODE_O_1PPT / FW_MODE_O_1PP: two PM channels, average via allCh
outdoor = buildPMS(1, true, true, localServer);
// PMS5003T version
// Per-channel firmware version — only for PMS5003T channels
if (!localServer) {
outdoor["channels"]["1"][json_prop_pmFirmware] =
pms5003TFirmwareVersion(ag->pms5003t_1.getFirmwareVersion());
outdoor["channels"]["2"][json_prop_pmFirmware] =
pms5003TFirmwareVersion(ag->pms5003t_2.getFirmwareVersion());
if (config.hasSensorPMS1) {
outdoor["channels"]["1"][json_prop_pmFirmware] =
pms5003TFirmwareVersion(ag->pms5003t_1.getFirmwareVersion());
}
if (config.hasSensorPMS2) {
outdoor["channels"]["2"][json_prop_pmFirmware] =
pms5003TFirmwareVersion(ag->pms5003t_2.getFirmwareVersion());
}
}
}
@@ -1202,15 +1203,16 @@ JSONVar Measurements::buildOutdoor(bool localServer, AgFirmwareMode fwMode) {
JSONVar Measurements::buildIndoor(bool localServer) {
JSONVar indoor;
if (config.hasSensorPMS1) {
if (config.hasSensorPMS1 || config.hasSensorSPS30_1) {
// buildPMS params:
/// PMS channel 1 (indoor only have 1 PMS; hence allCh false)
/// Not include temperature and humidity from PMS sensor
/// Include compensated calculation
indoor = buildPMS(1, false, false, true);
if (!localServer) {
// Indoor is using PMS5003
indoor[json_prop_pmFirmware] = this->pms5003FirmwareVersion(ag->pms5003.getFirmwareVersion());
if (!localServer && config.hasSensorPMS1) {
// PMS firmware version only available for PMS5003
indoor[json_prop_pmFirmware] =
this->pms5003FirmwareVersion(ag->pms5003.getFirmwareVersion());
}
}
+1 -2
View File
@@ -846,8 +846,7 @@ void WifiConnector::CharacteristicCallbacks::onRead(NimBLECharacteristic *pChara
}
void WifiConnector::CharacteristicCallbacks::onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) {
Serial.printf("%s : onWrite(), value: %s\n", pCharacteristic->getUUID().toString().c_str(),
pCharacteristic->getValue().c_str());
Serial.printf("%s : onWrite()\n", pCharacteristic->getUUID().toString().c_str());
auto bleCred = NimBLEUUID(BLE_CRED_CHAR_UUID);
if (pCharacteristic->getUUID().equals(bleCred)) {
+4 -6
View File
@@ -6,9 +6,9 @@
#endif
AirGradient::AirGradient(BoardType type)
: pms5003(type), pms5003t_1(type), pms5003t_2(type), s8(type), sgp41(type),
display(type), boardType(type), button(type), statusLed(type),
ledBar(type), watchdog(type), sht(type) {}
: pms5003(type), pms5003t_1(type), pms5003t_2(type), sps30_1(type),
sps30_2(type), s8(type), sgp41(type), display(type), boardType(type),
button(type), statusLed(type), ledBar(type), watchdog(type), sht(type) {}
/**
* @brief Get pin number for I2C SDA
@@ -61,9 +61,7 @@ String AirGradient::getBoardName(void) {
* @return true ONE_INDOOR
* @return false Other
*/
bool AirGradient::isOne(void) {
return boardType == BoardType::ONE_INDOOR;
}
bool AirGradient::isOne(void) { return boardType == BoardType::ONE_INDOOR; }
bool AirGradient::isOpenAir(void) {
return boardType == BoardType::OPEN_AIR_OUTDOOR;
+15 -3
View File
@@ -7,18 +7,18 @@
#include "Main/LedBar.h"
#include "Main/PushButton.h"
#include "Main/StatusLed.h"
#include "Main/utils.h"
#include "PMS/PMS5003.h"
#include "PMS/PMS5003T.h"
#include "S8/S8.h"
#include "SPS30/SPS30.h"
#include "Sgp41/Sgp41.h"
#include "Sht/Sht.h"
#include "Main/utils.h"
#ifndef GIT_VERSION
#define GIT_VERSION "3.6.4-snap"
#define GIT_VERSION "3.6.5-snap"
#endif
#ifndef ESP8266
// Airgradient server root ca certificate
const char *const AG_SERVER_ROOT_CA =
@@ -81,6 +81,18 @@ public:
*/
PMS5003T pms5003t_2;
/**
* @brief Sensirion SPS30 particulate matter sensor (UART), channel 1.
* Used as alternative PM sensor via auto-detection on Serial0.
*/
SPS30 sps30_1;
/**
* @brief Sensirion SPS30 particulate matter sensor (UART), channel 2.
* Used on OPEN_AIR_OUTDOOR when a second SPS30 is detected.
*/
SPS30 sps30_2;
/**
* @brief SenseAirS8 CO2 sensor
*/
+3
View File
@@ -81,6 +81,9 @@ enum LedBarMode {
/** Use LED bar for show CO2 value level */
LedBarModeCO2,
/** Use LED bar to show GO IAQS Starter Score (PM2.5 + CO2) */
LedBarModeIaqs,
};
enum ConfigurationControl {
-14
View File
@@ -1,14 +0,0 @@
---
Language: Cpp
BasedOnStyle: LLVM
IndentWidth: 4
AlignAfterOpenBracket: Align
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: false
IndentCaseLabels: true
SpacesBeforeTrailingComments: 2
PointerAlignment: Left
AlignEscapedNewlines: Left
ForEachMacros: ['TEST_GROUP', 'TEST']
...
-11
View File
@@ -1,11 +0,0 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
trim_trailing_whitespace = true
[*.bat]
end_of_line = crlf
-3
View File
@@ -1,3 +0,0 @@
/examples/AllCommandsShdlc/build/
/examples/AllCommandsI2c/build/
.vscode
@@ -1,39 +0,0 @@
stages:
- validate
- test
compile_test_shdlc:
stage: test
image:
name: registry.gitlab.sensirion.lokal/sensirion/docker/docker-arduino:0.5.0
tags: [docker, linux]
script:
- tests/compile_test.py -s examples/AllCommandsShdlc/
compile_test_i2c:
stage: test
image:
name: registry.gitlab.sensirion.lokal/sensirion/docker/docker-arduino:0.5.0
tags: [docker, linux]
script:
- tests/compile_test.py -s examples/AllCommandsI2c/
syntax_check:
stage: validate
image:
name: registry.gitlab.sensirion.lokal/sensirion/docker/docker-python:3.8-20.04-2.7.0
tags: [linux, docker]
before_script:
# For performance and stability reasons, use offline installation.
- pip install --no-index --find-links=/pip editorconfig-checker
- pip install flake8
- apt-get update && apt-get install -yq clang-format-6.0
script:
- tests/syntax_check.sh
cppcheck:
stage: validate
image: registry.gitlab.sensirion.lokal/sensirion/docker/docker-cppcheck:1.0.0
tags: [linux, docker]
script:
- tests/run_cppcheck.sh
-161
View File
@@ -1,161 +0,0 @@
Changelog
=========
All notable changes to this project will be documented in this file.
The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.0.0/>`_
and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`_.
`Unreleased`_
-------------
`0.6.0`_ 2022-06-22
-------------------
- Fix compiler warnings in SensirionErrors.cpp
- Allow drivers to choose CRC function
`0.5.3`_ 2021-10-19
-------------------
- Add support for sensor specific errors
- Update keywords.txt
`0.5.2`_ 2021-08-03
-------------------
Fixed
.....
- Fix CRC insertion in ``SensirionI2CTxFrame`` when more then one parameter
is sent to the sensor.
`0.5.1`_ 2021-07-08
-------------------
Changed
.......
- Adjusted deprecation warnings
`0.5.0`_ 2021-07-07
-------------------
Added
.....
- Enable SensirionTxFrame to incorporate Uint8 and Uint16 commands
`0.4.3`_ 2021-02-12
-------------------
Added
.....
- Added ``const`` modifier to functions which process MOSI array data.
`0.4.2`_ 2021-01-29
-------------------
Changed
.......
- Renamed the library header from ``SensirionCoreArduinoLibrary.h`` to ``SensirionCore.h``.
We keep the old header for legacy support.
`0.4.1`_ 2021-01-28
-------------------
Fixed
.....
- Properly handle I2C write errors
`0.4.0`_ 2021-01-20
-------------------
Added
.....
- Documentation for all functions.
Breaking
........
- Change interface of ``errorToString()`` function to include length of the
provided buffer.
Removed
.......
- Removed ``reset()`` function from ``SensirionI2CTxFrame`` since the
functionality is not needed.
`0.3.0`_ 2021-01-13
-------------------
Added
.....
- Core implementation for I2C communication. This includes a RX and TX frame
and a I2C communication class.
Changed
.......
- SHDLC and I2C RX frame inherit from a RX frame base class.
- ESP8266 test board from esp8266:esp8266:arduino to esp8266:esp8266:generic.
- Sorted errors into general, SHDLC and I2C errors.
- Replace C style casts with ``static_cast``.
`0.2.0`_ 2021-01-11
-------------------
Added
.....
- Explanation what SHDLC is in README.
- ``SensirionErrors.h`` to ``SensirionCoreArduinoLibrary.h``.
- ``sendAndReceiveFrame()`` function to ``SensirionShdlcCommunication``. This
function combines ``sendFrame()`` and ``receiveFrame()`` into one function and
adds additional error checking.
Changed
.......
- Rename DeviceError to ExecutionError.
- Move check for execution error after the whole frame is read and checksum is
checked. This prevents that a wrong checksum can't be displayed as an
execution error.
Removed
.......
- ``reset()`` function from ``SensirionShdlcTxFrame`` and ``SensirionShdlcRxFrame``,
since one can just create a new frame object which has the same effect.
`0.1.0`_ 2021-01-07
-------------------
- Initial release
.. _Unreleased: https://github.com/Sensirion/arduino-core/compare/0.6.0...main
.. _0.6.0: https://github.com/Sensirion/arduino-core/compare/0.6.0...0.5.3
.. _0.5.3: https://github.com/Sensirion/arduino-core/compare/0.5.2...0.5.3
.. _0.5.2: https://github.com/Sensirion/arduino-core/compare/0.5.1...0.5.2
.. _0.5.1: https://github.com/Sensirion/arduino-core/compare/0.5.0...0.5.1
.. _0.5.0: https://github.com/Sensirion/arduino-core/compare/0.4.3...0.5.0
.. _0.4.3: https://github.com/Sensirion/arduino-core/compare/0.4.2...0.4.3
.. _0.4.2: https://github.com/Sensirion/arduino-core/compare/0.4.1...0.4.2
.. _0.4.1: https://github.com/Sensirion/arduino-core/compare/0.4.0...0.4.1
.. _0.4.0: https://github.com/Sensirion/arduino-core/compare/0.3.0...0.4.0
.. _0.3.0: https://github.com/Sensirion/arduino-core/compare/0.2.0...0.3.0
.. _0.2.0: https://github.com/Sensirion/arduino-core/compare/0.1.0...0.2.0
.. _0.1.0: https://github.com/Sensirion/arduino-core/releases/tag/0.1.0
-29
View File
@@ -1,29 +0,0 @@
BSD 3-Clause License
Copyright (c) 2020, Sensirion AG
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-137
View File
@@ -1,137 +0,0 @@
# Sensirion Arduino Core Library
This library provides SHDLC and I2C protocol implementations for Sensirion
sensors. There shouldn't be a reason to use it directly, but is required by the
sensor driver libraries provided here:
- [SCD4x](https://github.com/Sensirion/arduino-i2c-scd4x)
- [SVM40-I2C](https://github.com/Sensirion/arduino-i2c-svm40)
- [SVM40-UART](https://github.com/Sensirion/arduino-uart-svm40)
- [SFA3x-I2C](https://github.com/Sensirion/arduino-i2c-sfa3x)
- [SFA3x-UART](https://github.com/Sensirion/arduino-uart-sfa3x)
# More Drivers
Not looking for Arduino drivers? Check out our other drivers here:
- [Embedded](https://github.com/Sensirion/info#repositories)
- [Python](https://github.com/Sensirion/info#python-drivers)
# Usage
## SHDLC
SHDLC (Sensirion High-Level Data Link Control) is a byte-oriented master-slave
communication protocol based on [ISO
HDLC](https://en.wikipedia.org/wiki/High-Level_Data_Link_Control). It is used
to control some of Sensirions devices (for example mass flow controllers). The
detailed protocol documentation is not publicly available (yet). If you need
it, please contact our [customer
support](https://www.sensirion.com/en/about-us/contact/).
This library provides the following classes for communication with Sensirion
Sensors using the SHDLC protocol.
- `SensirionShdlcTxFrame`
- `SensirionShdlcRxFrame`
- `SensirionShdlcCommunication`
### Example Usage
First initialize an instance of `SensirionShdlcTxFrame` and
`SensirionShdlcRxFrame` with a properly sized buffer. A good worst case
estimation for the buffer size is `2 * (n+6)` where `n` is the number of bytes
you want to send. After that you can build your frame by first calling
`begin()`. Information about the correct COMMAND and ADDRESS can be found on
the data sheet of your sensor. Then you can add data to the frame by using
different add member functions. See the code below for examples. After adding
your data finish the frame by calling `finish()`.
To send this frame to the sensor you first need to initialize the correct
Stream object (Serial,UART,...) to talk to your sensor. Don't forget to also
call the `.begin()` function with the right configuration. Then call the static
function `sendAndReceiveFrame()` from `SensirionShdlcCommunication` as shown
below. You need to replace `STREAMOBJECT` with the initialized Stream object of
your choice. Additionally you need to provide a timeout for to receive data
back, consult the data sheet of your sensor for information on the best timeout
value.
You can decode the frame by using the different get members to convert the
received data to desired data types.
All functions return a error code if an error occurs during execution and zero
otherwise.
```cpp
uint8_t txBuffer[256];
uint8_t rxBuffer[256];
SensirionShdlcTxFrame txFrame(txBuffer, 256);
SensirionShdlcRxFrame rxFrame(rxBuffer, 256);
txFrame.begin(COMMAND, ADDRESS, DATALENGTH);
txFrame.addUInt8(UINT8);
txFrame.addUInt32(UINT32);
txFrame.finish();
SensirionShdlcCommunication::sendAndReceiveFrame(STREAMOBJECT, txFrame, rxFrame, TIMEOUT);
rxFrame.getUInt16(UINT16);
rxFrame.getFloat(FLOAT);
```
## I2C
This library provides the following classes for communication with Sensirion
Sensors using the I2C protocol.
- `SensirionI2cTxFrame`
- `SensirionI2cRxFrame`
- `SensirionI2cCommunication`
### Example Usage
First initialize an instance of `SensirionI2CTxFrame` and `SensirionI2CRxFrame`
with a buffer sized the amount of data to read times 1.5. This is needed to
account for the CRC which is added after every second byte. After that you can
build your frame by first calling `addCommand()` to add the command at the
beginning of the frame. Information about the different COMMANDs can be found
on the data sheet of your sensor. Then you can add data to the frame by using
different add member functions. See the code below for examples.
To send this frame to the sensor you first need to initialize a Wire object.
Don't forget to also call the `.begin()` function with the right configuration.
Then call the static function `sendFrame()` form `SensirionI2CCommunication` as
shown below. You can find the ADDRESS on the data sheet of the sensor. You also
need to replace `WIREOBJECT` with the initialized Wire object. Then wait the in
the data sheet documented `READ_DELAY` before receiving the reply from the
sensor by calling `receiveFrame()` with the same Wire object.
You then can decode the frame by using the different get members to convert the
received data to desired data types.
All functions return a error code if an error occurs during execution and zero
otherwise.
```cpp
uint8_t txBuffer[256];
uint8_t rxBuffer[256];
SensirionShdlcTxFrame txFrame(txBuffer, 256);
SensirionShdlcRxFrame rxFrame(rxBuffer, 256);
txFrame.addCommand(COMMAND);
txFrame.addUInt8(UINT8);
txFrame.addUInt32(UINT32);
SensirionShdlcCommunication::sendFrame(ADDRESS, txFrame, WIREOBJECT);
delay(READ_DELAY);
SensirionShdlcCommunication::receiveFrame(ADDRESS, rxFrame, WIREOBJECT);
rxFrame.getUInt16(UINT16);
rxFrame.getFloat(FLOAT);
```
@@ -1,62 +0,0 @@
#include <SensirionCore.h>
#include <Wire.h>
#include <stdint.h>
uint8_t txBuffer[256];
uint8_t rxBuffer[256];
SensirionI2CTxFrame txFrame(txBuffer, 256);
SensirionI2CRxFrame rxFrame(rxBuffer, 256);
void setup() {
Wire.begin();
}
void loop() {
uint16_t mockCommand = 42;
uint16_t error = txFrame.addCommand(mockCommand);
uint32_t mockUInt32 = 42;
error |= txFrame.addUInt32(mockUInt32);
int32_t mockInt32 = 42;
error |= txFrame.addInt32(mockInt32);
uint16_t mockUInt16 = 42;
error |= txFrame.addUInt16(mockUInt16);
int16_t mockInt16 = 42;
error |= txFrame.addInt16(mockInt16);
uint8_t mockUInt8 = 42;
error |= txFrame.addUInt8(mockUInt8);
int8_t mockInt8 = 42;
error |= txFrame.addInt8(mockInt8);
float mockFloat = 42.0f;
error |= txFrame.addFloat(mockFloat);
bool mockBool = true;
error |= txFrame.addBool(mockBool);
uint8_t mockBytes[] = {42, 42, 42, 42};
error |= txFrame.addBytes(mockBytes, 4);
uint8_t mockAddress = 42;
error |= SensirionI2CCommunication::sendFrame(mockAddress, txFrame, Wire);
size_t mockNumBytes = 42;
error |= SensirionI2CCommunication::receiveFrame(mockAddress, mockNumBytes,
rxFrame, Wire);
error |= rxFrame.getUInt32(mockUInt32);
error |= rxFrame.getInt32(mockInt32);
error |= rxFrame.getUInt16(mockUInt16);
error |= rxFrame.getInt16(mockInt16);
error |= rxFrame.getUInt8(mockUInt8);
error |= rxFrame.getInt8(mockInt8);
error |= rxFrame.getFloat(mockFloat);
error |= rxFrame.getBytes(mockBytes, 4);
}
@@ -1,73 +0,0 @@
#include <SensirionCore.h>
#include <stdint.h>
uint8_t txBuffer[256];
uint8_t rxBuffer[256];
SensirionShdlcTxFrame txFrame(txBuffer, 256);
SensirionShdlcRxFrame rxFrame(rxBuffer, 256);
void setup() {
Serial.begin(115200);
}
void loop() {
uint8_t mockCommand = 42;
uint8_t mockAddress = 42;
uint8_t mockDataLength = 42;
uint16_t error = txFrame.begin(mockCommand, mockAddress, mockDataLength);
uint32_t mockUInt32 = 42;
error |= txFrame.addUInt32(mockUInt32);
int32_t mockInt32 = 42;
error |= txFrame.addInt32(mockInt32);
uint16_t mockUInt16 = 42;
error |= txFrame.addUInt16(mockUInt16);
int16_t mockInt16 = 42;
error |= txFrame.addInt16(mockInt16);
uint8_t mockUInt8 = 42;
error |= txFrame.addUInt8(mockUInt8);
int8_t mockInt8 = 42;
error |= txFrame.addInt8(mockInt8);
float mockFloat = 42.0f;
error |= txFrame.addFloat(mockFloat);
bool mockBool = true;
error |= txFrame.addBool(mockBool);
uint8_t mockBytes[] = {42, 42, 42, 42};
error |= txFrame.addBytes(mockBytes, 4);
error |= txFrame.finish();
error |= SensirionShdlcCommunication::sendFrame(txFrame, Serial);
error |= SensirionShdlcCommunication::sendAndReceiveFrame(
Serial, txFrame, rxFrame, 10000000);
error |=
SensirionShdlcCommunication::receiveFrame(rxFrame, Serial, 1000000);
error |= rxFrame.getUInt32(mockUInt32);
error |= rxFrame.getInt32(mockInt32);
error |= rxFrame.getUInt16(mockUInt16);
error |= rxFrame.getInt16(mockInt16);
error |= rxFrame.getUInt8(mockUInt8);
error |= rxFrame.getInt8(mockInt8);
error |= rxFrame.getFloat(mockFloat);
error |= rxFrame.getBytes(mockBytes, 4);
mockCommand = rxFrame.getCommand();
mockAddress = rxFrame.getAddress();
mockDataLength = rxFrame.getDataLength();
uint8_t mockState = rxFrame.getState();
if (mockState) {
// There is an error in the device.
}
}
-56
View File
@@ -1,56 +0,0 @@
#######################################
# Syntax Coloring Map
#######################################
#######################################
# Datatypes (KEYWORD1)
#######################################
SensirionShdlcCommunication KEYWORD1
SensirionShdlcRxFrame KEYWORD1
SensirionShdlcTxFrame KEYWORD1
SensirionI2CTxFrame KEYWORD1
SensirionI2CRxFrame KEYWORD1
SensirionI2CCommunication KEYWORD1
# SensirionErrors.h
HighLevelError KEYWORD1
LowLevelError KEYWORD1
#######################################
# Methods and Functions (KEYWORD2)
#######################################
sendFrame KEYWORD2
receiveFrame KEYWORD2
sendAndReceiveFrame KEYWORD2
addUInt32 KEYWORD2
addInt32 KEYWORD2
addUInt16 KEYWORD2
addInt16 KEYWORD2
addUInt8 KEYWORD2
addInt8 KEYWORD2
addFloat KEYWORD2
addBytes KEYWORD2
addBool KEYWORD2
addCommand KEYWORD2
begin KEYWORD2
finish KEYWORD2
reset KEYWORD2
getUInt32 KEYWORD2
getInt32 KEYWORD2
getUInt16 KEYWORD2
getInt16 KEYWORD2
getUInt8 KEYWORD2
getInt8 KEYWORD2
getFloat KEYWORD2
getBytes KEYWORD2
getCommand KEYWORD2
getAddress KEYWORD2
getDataLength KEYWORD2
getState KEYWORD2
# SensirionErrors.h
errorToString KEYWORD2
#######################################
# Constants (LITERAL1)
#######################################
@@ -1,9 +0,0 @@
name=Sensirion Core
version=0.6.0
author=Sensirion
maintainer=Sensirion
sentence=Library containing code base for Sensirion Sensor Libraries.
paragraph=All Libraries for Sensirion Sensors use this library as a code base. In this library the Sensirion specific parts for I2C and UART communication are implemented. It provides dynamic frame construction, checksum calculation and buffer handling.
category=Communication
url=https://github.com/Sensirion/arduino-core/
includes=SensirionCore.h
@@ -1,46 +0,0 @@
/*
* Copyright (c) 2021, Sensirion AG
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Sensirion AG nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef _SENSIRION_CORE_H_
#define _SENSIRION_CORE_H_
#include "SensirionCrc.h"
#include "SensirionErrors.h"
#include "SensirionRxFrame.h"
#include "SensirionShdlcCommunication.h"
#include "SensirionShdlcRxFrame.h"
#include "SensirionShdlcTxFrame.h"
#include "SensirionI2CCommunication.h"
#include "SensirionI2CRxFrame.h"
#include "SensirionI2CTxFrame.h"
#endif /* _SENSIRION_CORE_H_ */
@@ -1,51 +0,0 @@
/*
*
* THIS IS A LEGACY FILE AND WILL BE REMOVED SOON.
*
* Copyright (c) 2020, Sensirion AG
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Sensirion AG nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef _SENSIRION_CORE_ARDUINO_LIBRARY_H_
#define _SENSIRION_CORE_ARDUINO_LIBRARY_H_
#pragma GCC warning \
"Legacy file SensirionCoreArdunioLibrary.h included. Please include SensirionCore.h instead."
#include "SensirionErrors.h"
#include "SensirionRxFrame.h"
#include "SensirionShdlcCommunication.h"
#include "SensirionShdlcRxFrame.h"
#include "SensirionShdlcTxFrame.h"
#include "SensirionI2CCommunication.h"
#include "SensirionI2CRxFrame.h"
#include "SensirionI2CTxFrame.h"
#endif /* _SENSIRION_CORE_ARDUION_LIBRARY_H_ */
@@ -1,64 +0,0 @@
/*
* Copyright (c) 2022, Sensirion AG
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Sensirion AG nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "SensirionCrc.h"
uint8_t generateCRCGeneric(const uint8_t* data, size_t count, uint8_t init,
uint8_t polynomial) {
uint8_t crc = init;
/* calculates 8-Bit checksum with given polynomial */
for (size_t current_byte = 0; current_byte < count; ++current_byte) {
crc ^= (data[current_byte]);
for (uint8_t crc_bit = 8; crc_bit > 0; --crc_bit) {
if (crc & 0x80)
crc = (crc << 1) ^ polynomial;
else
crc = (crc << 1);
}
}
return crc;
}
uint8_t generateCRC31_ff(const uint8_t* data, size_t count) {
return generateCRCGeneric(data, count, 0xff, 0x31);
}
uint8_t generateCRC31_00(const uint8_t* data, size_t count) {
return generateCRCGeneric(data, count, 0x00, 0x31);
}
uint8_t generateCRC(const uint8_t* data, size_t count, CrcPolynomial type) {
if (CRC31_00 == type) {
return generateCRC31_00(data, count);
}
return generateCRC31_ff(data, count);
}
@@ -1,59 +0,0 @@
/*
* Copyright (c) 2022, Sensirion AG
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Sensirion AG nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef _SENSIRION_CRC_H_
#define _SENSIRION_CRC_H_
#include <stdint.h>
#include <stdlib.h>
enum CrcPolynomial : uint8_t {
CRC31_00 = 0x0,
CRC31_ff = 0x1,
};
uint8_t generateCRCGeneric(const uint8_t* data, size_t count, uint8_t init,
uint8_t polynomial);
uint8_t generateCRC31_ff(const uint8_t* data, size_t count);
uint8_t generateCRC31_00(const uint8_t* data, size_t count);
/**
* @brief Generate a crc for data given a polynomial type
*
* @param data data to calculate CRC for
* @param count the array size of data
* @param poly CRC polynomal to use
*/
uint8_t generateCRC(const uint8_t* data, size_t count, CrcPolynomial type);
#endif /* _SENSIRION_CRC_H_ */
@@ -1,148 +0,0 @@
/*
* Copyright (c) 2020, Sensirion AG
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Sensirion AG nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "SensirionErrors.h"
#include <stdint.h>
#include <stdio.h>
#include <string.h>
void errorToString(uint16_t error, char errorMessage[],
size_t errorMessageSize) {
uint16_t highLevelError = error & 0xFF00;
uint16_t lowLevelError = error & 0x00FF;
if (error & HighLevelError::SensorSpecificError) {
snprintf(errorMessage, errorMessageSize, "Sensor specific error: 0x%2x",
lowLevelError);
return;
}
switch (highLevelError) {
case HighLevelError::NoError:
if (!error) {
strncpy(errorMessage, "No error", errorMessageSize);
return;
}
break;
case HighLevelError::WriteError:
switch (lowLevelError) {
case LowLevelError::SerialWriteError:
strncpy(errorMessage, "Error writing to serial",
errorMessageSize);
return;
case LowLevelError::InternalBufferSizeError:
strncpy(errorMessage,
"Data too long to fit in transmit buffer",
errorMessageSize);
return;
case LowLevelError::I2cAddressNack:
strncpy(errorMessage,
"Received NACK on transmit of address",
errorMessageSize);
return;
case LowLevelError::I2cDataNack:
strncpy(errorMessage, "Received NACK on transmit of data",
errorMessageSize);
return;
case LowLevelError::I2cOtherError:
strncpy(errorMessage, "Error writing to I2C bus",
errorMessageSize);
return;
}
break;
case HighLevelError::ReadError:
switch (lowLevelError) {
case LowLevelError::NonemptyFrameError:
strncpy(errorMessage, "Frame already contains data",
errorMessageSize);
return;
case LowLevelError::TimeoutError:
strncpy(errorMessage, "Timeout while reading data",
errorMessageSize);
return;
case LowLevelError::ChecksumError:
strncpy(errorMessage, "Checksum is wrong",
errorMessageSize);
return;
case LowLevelError::CRCError:
strncpy(errorMessage, "Wrong CRC found", errorMessageSize);
return;
case LowLevelError::WrongNumberBytesError:
strncpy(errorMessage, "Number of bytes not a multiple of 3",
errorMessageSize);
return;
case LowLevelError::NotEnoughDataError:
strncpy(errorMessage, "Not enough data received",
errorMessageSize);
return;
case LowLevelError::InternalBufferSizeError:
strncpy(errorMessage, "Internal I2C buffer too small",
errorMessageSize);
return;
}
break;
case HighLevelError::ExecutionError: {
char format[] = "Execution error, status register: 0x%x";
snprintf(errorMessage, errorMessageSize, format, lowLevelError);
return;
}
case HighLevelError::TxFrameError:
switch (lowLevelError) {
case LowLevelError::BufferSizeError:
strncpy(errorMessage, "Not enough space in buffer",
errorMessageSize);
return;
}
break;
case HighLevelError::RxFrameError:
switch (lowLevelError) {
case LowLevelError::BufferSizeError:
strncpy(errorMessage, "Not enough space in buffer",
errorMessageSize);
return;
case LowLevelError::NoDataError:
strncpy(errorMessage, "No more data in frame",
errorMessageSize);
return;
case LowLevelError::RxAddressError:
strncpy(errorMessage, "Wrong address in return frame",
errorMessageSize);
return;
case LowLevelError::RxCommandError:
strncpy(errorMessage, "Wrong command in return frame",
errorMessageSize);
return;
}
}
strncpy(errorMessage, "Error processing error", errorMessageSize);
return;
}
@@ -1,87 +0,0 @@
/*
* Copyright (c) 2020, Sensirion AG
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Sensirion AG nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef _SENSIRION_ERRORS_H_
#define _SENSIRION_ERRORS_H_
#include <stdint.h>
#include <stdlib.h>
enum HighLevelError : uint16_t {
// general errors
NoError = 0,
WriteError = 0x0100,
ReadError = 0x0200,
TxFrameError = 0x0300,
RxFrameError = 0x0400,
// shdlc errors
ExecutionError = 0x0500,
// i2c errors
// Sensor specific errors. All errors higher than that are depending on the
// sensor used.
SensorSpecificError = 0x8000,
};
enum LowLevelError : uint16_t {
// general errors
NonemptyFrameError,
NoDataError,
BufferSizeError,
// shdlc errors
StopByteError,
ChecksumError,
TimeoutError,
RxCommandError,
RxAddressError,
SerialWriteError,
// i2c errors
WrongNumberBytesError,
CRCError,
I2cAddressNack,
I2cDataNack,
I2cOtherError,
NotEnoughDataError,
InternalBufferSizeError,
};
/**
* errorToString() - Convert error code to a human readable error message
*
* @param error Error code to be converted.
* @param errorMessage String where the error text can be
* stored.
* @param errorMessageSize Size in bytes of the string buffer for the error
* message.
*/
void errorToString(uint16_t error, char errorMessage[],
size_t errorMessageSize);
#endif /* _SENSIRION_ERRORS_H_ */
@@ -1,119 +0,0 @@
/*
* Copyright (c) 2020, Sensirion AG
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Sensirion AG nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "SensirionI2CCommunication.h"
#include <stdint.h>
#include <stdlib.h>
#include "Arduino.h"
#include "SensirionCrc.h"
#include "SensirionErrors.h"
#include "SensirionI2CRxFrame.h"
#include "SensirionI2CTxFrame.h"
static void clearRxBuffer(TwoWire& i2cBus) {
while (i2cBus.available()) {
(void)i2cBus.read();
}
}
uint16_t SensirionI2CCommunication::sendFrame(uint8_t address,
SensirionI2CTxFrame& frame,
TwoWire& i2cBus) {
i2cBus.beginTransmission(address);
size_t writtenBytes = i2cBus.write(frame._buffer, frame._index);
uint8_t i2c_error = i2cBus.endTransmission();
if (writtenBytes != frame._index) {
return WriteError | I2cOtherError;
}
// translate Arduino errors, see
// https://www.arduino.cc/en/Reference/WireEndTransmission
switch (i2c_error) {
case 0:
return NoError;
case 1:
return WriteError | InternalBufferSizeError;
case 2:
return WriteError | I2cAddressNack;
case 3:
return WriteError | I2cDataNack;
default:
return WriteError | I2cOtherError;
}
}
uint16_t SensirionI2CCommunication::receiveFrame(uint8_t address,
size_t numBytes,
SensirionI2CRxFrame& frame,
TwoWire& i2cBus,
CrcPolynomial poly) {
size_t readAmount;
size_t i = 0;
#ifdef I2C_BUFFER_LENGTH
const uint8_t sizeBuffer =
(static_cast<uint8_t>(I2C_BUFFER_LENGTH) / static_cast<uint8_t>(3)) * 3;
#elif defined(BUFFER_LENGTH)
const uint8_t sizeBuffer =
(static_cast<uint8_t>(BUFFER_LENGTH) / static_cast<uint8_t>(3)) * 3;
#else
const uint8_t sizeBuffer = 30;
#endif
if (numBytes % 3) {
return ReadError | WrongNumberBytesError;
}
if ((numBytes / 3) * 2 > frame._bufferSize) {
return ReadError | BufferSizeError;
}
if (numBytes > sizeBuffer) {
return ReadError | InternalBufferSizeError;
}
readAmount = i2cBus.requestFrom(address, static_cast<uint8_t>(numBytes),
static_cast<uint8_t>(true));
if (numBytes != readAmount) {
return ReadError | NotEnoughDataError;
}
do {
frame._buffer[i++] = i2cBus.read();
frame._buffer[i++] = i2cBus.read();
uint8_t actualCRC = i2cBus.read();
uint8_t expectedCRC = generateCRC(&frame._buffer[i - 2], 2, poly);
if (actualCRC != expectedCRC) {
clearRxBuffer(i2cBus);
return ReadError | CRCError;
}
readAmount -= 3;
} while (readAmount > 0);
frame._numBytes = i;
return NoError;
}
@@ -1,83 +0,0 @@
/*
* Copyright (c) 2020, Sensirion AG
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Sensirion AG nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef SENSIRION_I2C_COMMUNICATION_H_
#define SENSIRION_I2C_COMMUNICATION_H_
#include <stdint.h>
#include <stdlib.h>
#include "Arduino.h"
#include <Wire.h>
#include "SensirionI2CRxFrame.h"
#include "SensirionI2CTxFrame.h"
class SensirionI2CTxFrame;
class SensirionI2CRxFrame;
/*
* SensirionI2CCommunication - Class which is responsible for the communication
* via a I2C bus. It provides functionality to send and receive frames from a
* Sensirion sensor. The data is sent and received in a SensirionI2cTxFrame or
* SensirionI2cRxFrame respectively.
*/
class SensirionI2CCommunication {
public:
/**
* sendFrame() - Sends frame to sensor
*
* @param address I2C address of the sensor.
* @param frame Tx frame object containing a finished frame to send to
* the sensor.
* @param i2cBus TwoWire object to communicate with the sensor.
*
* @return NoError on success, an error code otherwise
*/
static uint16_t sendFrame(uint8_t address, SensirionI2CTxFrame& frame,
TwoWire& i2cBus);
/**
* receiveFrame() - Receive Frame from sensor
*
* @param address I2C address of the sensor.
* @param numBytes Number of bytes to receive.
* @param frame Rx frame to store the received data in.
* @param i2cBus TwoWire object to communicate with the sensor.
* @param poly CRC polynomal to use. Defaults to 0x31 with init 0xFF
*
* @return NoError on success, an error code otherwise
*/
static uint16_t receiveFrame(uint8_t address, size_t numBytes,
SensirionI2CRxFrame& frame, TwoWire& i2cBus,
CrcPolynomial poly = CRC31_ff);
};
#endif /* SENSIRION_I2C_COMMUNICATION_H_ */
@@ -1,64 +0,0 @@
/*
* Copyright (c) 2020, Sensirion AG
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Sensirion AG nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef SENSIRION_I2C_RX_FRAME_H_
#define SENSIRION_I2C_RX_FRAME_H_
#include <stdint.h>
#include <stdlib.h>
#include "SensirionI2CCommunication.h"
#include "SensirionRxFrame.h"
/**
* SenirionI2CRxFrame - Class which decodes the through I2C received data into
* common data types. It contains a buffer which is filled by the
* SensirionI2CCommunication class. By calling the different decode function
* inherited from the SensirionRxFrame base class the raw data can be decoded
* into different data types.
*/
class SensirionI2CRxFrame : public SensirionRxFrame {
friend class SensirionI2CCommunication;
public:
/**
* Constructor
*
* @param buffer Buffer in which the receive frame will be
* stored.
* @param bufferSize Number of bytes in the buffer for the receive frame.
*
*/
SensirionI2CRxFrame(uint8_t buffer[], size_t bufferSize)
: SensirionRxFrame(buffer, bufferSize){};
};
#endif /* SENSIRION_I2C_RX_FRAME_H_ */
@@ -1,144 +0,0 @@
/*
* Copyright (c) 2020, Sensirion AG
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Sensirion AG nor the names of its
* contributors may be used to endorse or promote products derived from
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "SensirionI2CTxFrame.h"
#include <stdint.h>
#include <stdlib.h>
#include "SensirionCrc.h"
#include "SensirionErrors.h"
SensirionI2CTxFrame::SensirionI2CTxFrame(uint8_t buffer[], size_t bufferSize,
size_t numCommandBytes,
CrcPolynomial poly)
: _buffer(buffer), _bufferSize(bufferSize), _index(numCommandBytes),
_numCommandBytes(numCommandBytes), _polynomial_type(poly) {
}
SensirionI2CTxFrame::SensirionI2CTxFrame(uint8_t buffer[], size_t bufferSize,
CrcPolynomial poly)
: SensirionI2CTxFrame(buffer, bufferSize, 2, poly) {
}
SensirionI2CTxFrame SensirionI2CTxFrame::createWithUInt8Command(
uint8_t command, uint8_t buffer[], size_t bufferSize, CrcPolynomial poly) {
SensirionI2CTxFrame instance =
SensirionI2CTxFrame(buffer, bufferSize, 1, poly);
instance._buffer[0] = command;
return instance;
}
SensirionI2CTxFrame SensirionI2CTxFrame::createWithUInt16Command(
uint16_t command, uint8_t buffer[], size_t bufferSize, CrcPolynomial poly) {
SensirionI2CTxFrame instance =
SensirionI2CTxFrame(buffer, bufferSize, 2, poly);
instance._buffer[0] = static_cast<uint8_t>((command & 0xFF00) >> 8);
instance._buffer[1] = static_cast<uint8_t>((command & 0x00FF) >> 0);
return instance;
}
uint16_t SensirionI2CTxFrame::addCommand(uint16_t command) {
if (_bufferSize < 2) {
return TxFrameError | BufferSizeError;
}
_buffer[0] = static_cast<uint8_t>((command & 0xFF00) >> 8);
_buffer[1] = static_cast<uint8_t>((command & 0x00FF) >> 0);
return NoError;
}
uint16_t SensirionI2CTxFrame::addUInt32(uint32_t data) {
uint16_t error = _addByte(static_cast<uint8_t>((data & 0xFF000000) >> 24));
error |= _addByte(static_cast<uint8_t>((data & 0x00FF0000) >> 16));
error |= _addByte(static_cast<uint8_t>((data & 0x0000FF00) >> 8));
error |= _addByte(static_cast<uint8_t>((data & 0x000000FF) >> 0));
return error;
}
uint16_t SensirionI2CTxFrame::addInt32(int32_t data) {
return addUInt32(static_cast<uint32_t>(data));
}
uint16_t SensirionI2CTxFrame::addUInt16(uint16_t data) {
uint16_t error = _addByte(static_cast<uint8_t>((data & 0xFF00) >> 8));
error |= _addByte(static_cast<uint8_t>((data & 0x00FF) >> 0));
return error;
}
uint16_t SensirionI2CTxFrame::addInt16(int16_t data) {
return addUInt16(static_cast<uint16_t>(data));
}
uint16_t SensirionI2CTxFrame::addUInt8(uint8_t data) {
return _addByte(data);
}
uint16_t SensirionI2CTxFrame::addInt8(int8_t data) {
return _addByte(static_cast<uint8_t>(data));
}
uint16_t SensirionI2CTxFrame::addBool(bool data) {
return _addByte(static_cast<uint8_t>(data));
}
uint16_t SensirionI2CTxFrame::addFloat(float data) {
union {
uint32_t uInt32Data;
float floatData;
} convert;
convert.floatData = data;
return addUInt32(convert.uInt32Data);
}
uint16_t SensirionI2CTxFrame::addBytes(const uint8_t data[],
size_t dataLength) {
uint16_t error = 0;
for (size_t i = 0; i < dataLength; i++) {
error |= _addByte(data[i]);
}
return error;
}
uint16_t SensirionI2CTxFrame::_addByte(uint8_t data) {
if (_bufferSize <= _index) {
return TxFrameError | BufferSizeError;
}
_buffer[_index++] = data;
if ((_index - _numCommandBytes) % 3 == 2) {
if (_bufferSize <= _index) {
return TxFrameError | BufferSizeError;
}
uint8_t crc = generateCRC(&_buffer[_index - 2], 2, _polynomial_type);
_buffer[_index++] = crc;
}
return NoError;
}
@@ -1,197 +0,0 @@
/*
* Copyright (c) 2020, Sensirion AG
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Sensirion AG nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef SENSIRION_I2C_TX_FRAME_H_
#define SENSIRION_I2C_TX_FRAME_H_
#include <stdint.h>
#include <stdlib.h>
#include "SensirionCrc.h"
#include "SensirionI2CCommunication.h"
/*
* SensirionI2CTxFrame - Class which helps to build a correct I2C frame for
* Sensirion Sensors. The different addDatatype() functions add the frame data
* and the addCommand() function writes the command at the beginning. Using
* these functions one can easily construct a I2C frame for Sensirion sensors.
*/
class SensirionI2CTxFrame {
friend class SensirionI2CCommunication;
public:
/**
* Factory to create a SensirionI2CTxFrame using a UInt8 command.
*
* @param command Command to add to the send frame.
* @param buffer Buffer in which the send frame will be stored.
* @param bufferSize Number of bytes in the buffer for the send frame.
* @param poly CRC polynomal to use. Defaults to 0x31 with init 0xFF
*
* @return the constructed SensirionI2CTxFrame.
*/
static SensirionI2CTxFrame
createWithUInt8Command(uint8_t command, uint8_t buffer[], size_t bufferSize,
CrcPolynomial poly = CRC31_ff);
/**
* Factory to create a SensirionI2CTxFrame using a UInt16 command.
*
* @param command Command to add to the send frame.
* @param buffer Buffer in which the send frame will be stored.
* @param bufferSize Number of bytes in the buffer for the send frame.
* @param poly CRC polynomal to use. Defaults to 0x31 with init 0xFF
*
* @return the constructed SensirionI2CTxFrame.
*/
static SensirionI2CTxFrame
createWithUInt16Command(uint16_t command, uint8_t buffer[],
size_t bufferSize, CrcPolynomial poly = CRC31_ff);
/**
* Constructor
*
* @param buffer Buffer in which the send frame will be stored.
* @param bufferSize Number of bytes in the buffer for the send frame.
* @param poly CRC polynomal to use. Defaults to 0x31 with init 0xFF
*
* @deprecated Use createWithUInt16Command() instead
*/
SensirionI2CTxFrame(uint8_t buffer[], size_t bufferSize,
CrcPolynomial poly = CRC31_ff);
/**
* addCommand() - Add command to the send frame.
*
* @param command Command to add to the send frame.
*
* @return NoError on success, an error code otherwise
*
* @deprecated Use createWithUInt16Command() instead
*/
uint16_t addCommand(uint16_t command);
/**
* addUInt32() - Add unsigned 32bit integer to the send frame.
*
* @param data Unsigned 32bit integer to add to the send frame.
*
* @return NoError on success, an error code otherwise
*/
uint16_t addUInt32(uint32_t data);
/**
* addInt32() - Add signed 32bit integer to the send frame.
*
* @param data Signed 32bit integer to add to the send frame.
*
* @return NoError on success, an error code otherwise
*/
uint16_t addInt32(int32_t data);
/**
* addUInt16() - Add unsigned 16bit integer to the send frame.
*
* @param data Unsigned 16bit integer to add to the send frame.
*
* @return NoError on success, an error code otherwise
*/
uint16_t addUInt16(uint16_t data);
/**
* addInt16() - Add signed 16bit integer to the send frame.
*
* @param data Signed 16bit integer to add to the send frame.
*
* @return NoError on success, an error code otherwise
*/
uint16_t addInt16(int16_t data);
/**
* addUInt8() - Add unsigned 8bit integer to the send frame.
*
* @param data Unsigned 8bit integer to add to the send frame.
*
* @return NoError on success, an error code otherwise
*/
uint16_t addUInt8(uint8_t data);
/**
* addInt8() - Add signed 8bit integer to the send frame.
*
* @param data Signed 8bit integer to add to the send frame.
*
* @return NoError on success, an error code otherwise
*/
uint16_t addInt8(int8_t data);
/**
* addBool() - Add boolean to the send frame.
*
* @param data Boolean to add to the send frame.
*
* @return NoError on success, an error code otherwise
*/
uint16_t addBool(bool data);
/**
* addFloat() - Add float to the send frame.
*
* @param data Float to add to the send frame.
*
* @return NoError on success, an error code otherwise
*/
uint16_t addFloat(float data);
/**
* addBytes() - Add byte array to the send frame.
*
* @param data Byte array to add to the send frame.
* @param dataLength Number of bytes to add to the send frame.
*
* @return NoError on success, an error code otherwise
*/
uint16_t addBytes(const uint8_t data[], size_t dataLength);
private:
SensirionI2CTxFrame(uint8_t buffer[], size_t bufferSize,
size_t numCommandBytes, CrcPolynomial poly = CRC31_ff);
uint16_t _addByte(uint8_t data);
uint8_t* _buffer;
size_t _bufferSize;
size_t _index;
size_t _numCommandBytes;
CrcPolynomial _polynomial_type;
};
#endif /* SENSIRION_I2C_TX_FRAME_H_ */
@@ -1,129 +0,0 @@
/*
* Copyright (c) 2020, Sensirion AG
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Sensirion AG nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "SensirionRxFrame.h"
#include <stdint.h>
#include <stdlib.h>
#include "SensirionErrors.h"
SensirionRxFrame::SensirionRxFrame(uint8_t buffer[], size_t bufferSize)
: _buffer(buffer), _bufferSize(bufferSize), _index(0), _numBytes(0) {
}
uint16_t SensirionRxFrame::getUInt32(uint32_t& data) {
if (_numBytes < 4) {
return RxFrameError | NoDataError;
}
data = static_cast<uint32_t>(_buffer[_index++]) << 24;
data |= static_cast<uint32_t>(_buffer[_index++]) << 16;
data |= static_cast<uint32_t>(_buffer[_index++]) << 8;
data |= static_cast<uint32_t>(_buffer[_index++]);
_numBytes -= 4;
return NoError;
}
uint16_t SensirionRxFrame::getInt32(int32_t& data) {
uint32_t ret;
uint16_t error = getUInt32(ret);
data = static_cast<int32_t>(ret);
return error;
}
uint16_t SensirionRxFrame::getUInt16(uint16_t& data) {
if (_numBytes < 2) {
return RxFrameError | NoDataError;
}
data = static_cast<uint16_t>(_buffer[_index++]) << 8;
data |= static_cast<uint16_t>(_buffer[_index++]);
_numBytes -= 2;
return NoError;
}
uint16_t SensirionRxFrame::getInt16(int16_t& data) {
uint16_t ret;
uint16_t error = getUInt16(ret);
data = static_cast<int16_t>(ret);
return error;
}
uint16_t SensirionRxFrame::getUInt8(uint8_t& data) {
if (_numBytes < 1) {
return RxFrameError | NoDataError;
}
data = _buffer[_index++];
_numBytes -= 1;
return NoError;
}
uint16_t SensirionRxFrame::getInt8(int8_t& data) {
if (_numBytes < 1) {
return RxFrameError | NoDataError;
}
data = static_cast<int8_t>(_buffer[_index++]);
_numBytes -= 1;
return NoError;
}
uint16_t SensirionRxFrame::getBool(bool& data) {
if (_numBytes < 1) {
return RxFrameError | NoDataError;
}
data = static_cast<bool>(_buffer[_index++]);
_numBytes -= 1;
return NoError;
}
uint16_t SensirionRxFrame::getFloat(float& data) {
union {
uint32_t uInt32Data;
float floatData;
} convert;
uint16_t error = getUInt32(convert.uInt32Data);
data = convert.floatData;
return error;
}
uint16_t SensirionRxFrame::getBytes(uint8_t data[], size_t maxBytes) {
if (_numBytes < 1) {
return RxFrameError | NoDataError;
}
size_t readAmount = maxBytes;
if (_numBytes < maxBytes) {
readAmount = _numBytes;
}
for (size_t i = 0; i < readAmount; i++) {
data[i] = _buffer[_index++];
}
_numBytes -= readAmount;
return NoError;
}
@@ -1,147 +0,0 @@
/*
* Copyright (c) 2020, Sensirion AG
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Sensirion AG nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef SENSIRION_RX_FRAME_H_
#define SENSIRION_RX_FRAME_H_
#include <stdint.h>
#include <stdlib.h>
/**
* SenirionRxFrame - Base class for SensirionShdlcRxFrame and
* SensirionI2cRxFrame. It decodes received data into common data types. The
* data is contained in a buffer which is filled by the one of the two
* communication classes. By calling the different decode function the raw data
* can be decoded into different data types.
*/
class SensirionRxFrame {
friend class SensirionI2CCommunication;
friend class SensirionShdlcCommunication;
public:
/**
* Constructor
*
* @param buffer Buffer in which the receive frame will be stored.
* @param bufferSize Number of bytes in the buffer for the receive frame.
*/
SensirionRxFrame(uint8_t buffer[], size_t bufferSize);
/**
* getUInt32() - Get unsigned 32bit integer from the received data.
*
* @param data Memory to store unsigned 32bit integer in.
*
* @return NoError on success, an error code otherwise
*/
uint16_t getUInt32(uint32_t& data);
/**
* getInt32() - Get signed 32bit integer from the received data.
*
* @param data Memory to store signed 32bit integer in.
*
* @return NoError on success, an error code otherwise
*/
uint16_t getInt32(int32_t& data);
/**
* getUInt16() - Get unsigned 16bit integer from the received data.
*
* @param data Memory to store unsigned 16bit integer in.
*
* @return NoError on success, an error code otherwise
*/
uint16_t getUInt16(uint16_t& data);
/**
* getInt16() - Get signed 16bit integer from the received data.
*
* @param data Memory to store signed 16bit integer in.
*
* @return NoError on success, an error code otherwise
*/
uint16_t getInt16(int16_t& data);
/**
* getUInt8() - Get unsigned 8bit integer from the received data.
*
* @param data Memory to store unsigned 8bit integer in.
*
* @return NoError on success, an error code otherwise
*/
uint16_t getUInt8(uint8_t& data);
/**
* getInt8() - Get signed 8bit integer from the received data.
*
* @param data Memory to store signed 8bit integer in.
*
* @return NoError on success, an error code otherwise
*/
uint16_t getInt8(int8_t& data);
/**
* getBool() - Get Boolean from the received data.
*
* @param data Memory to store boolean in.
*
* @return NoError on success, an error code otherwise
*/
uint16_t getBool(bool& data);
/**
* getFloat() - Get float from the received data.
*
* @param data Memory to store float in.
*
* @return NoError on success, an error code otherwise
*/
uint16_t getFloat(float& data);
/**
* getBytes() - Get an array of bytes from the received data.
*
* @param data Buffer to store the bytes in.
* @param maxBytes Maximal amount of bytes to read from the received data.
*
* @return NoError on success, an error code otherwise
*/
uint16_t getBytes(uint8_t data[], size_t maxBytes);
private:
uint8_t* _buffer = 0;
size_t _bufferSize = 0;
size_t _index = 0;
size_t _numBytes = 0;
};
#endif /* SENSIRION_RX_FRAME_H_ */
@@ -1,184 +0,0 @@
/*
* Copyright (c) 2020, Sensirion AG
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Sensirion AG nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "SensirionShdlcCommunication.h"
#include <stdint.h>
#include <stdlib.h>
#include "Arduino.h"
#include "SensirionErrors.h"
#include "SensirionShdlcRxFrame.h"
#include "SensirionShdlcTxFrame.h"
static uint16_t readByte(uint8_t& data, Stream& serial, unsigned long startTime,
unsigned long timeoutMicros) {
do {
if (micros() - startTime > timeoutMicros) {
return ReadError | TimeoutError;
}
} while (!serial.available());
data = serial.read();
return NoError;
}
static uint16_t unstuffByte(uint8_t& data, Stream& serial,
unsigned long startTime,
unsigned long timeoutMicros) {
uint16_t error = readByte(data, serial, startTime, timeoutMicros);
if (error) {
return error;
}
if (data == 0x7d) {
error = readByte(data, serial, startTime, timeoutMicros);
if (error) {
return error;
}
data = data ^ (1 << 5);
}
return NoError;
}
uint16_t SensirionShdlcCommunication::sendFrame(SensirionShdlcTxFrame& frame,
Stream& serial) {
size_t writtenBytes = serial.write(&frame._buffer[0], frame._index);
if (writtenBytes != frame._index) {
return WriteError | SerialWriteError;
}
return NoError;
}
uint16_t SensirionShdlcCommunication::receiveFrame(
SensirionShdlcRxFrame& frame, Stream& serial, unsigned long timeoutMicros) {
unsigned long startTime = micros();
uint16_t error;
uint8_t dataLength;
uint8_t current = 0;
if (frame._numBytes) {
return ReadError | NonemptyFrameError;
}
// Wait for start byte and ignore all other bytes in case a partial frame
// is still in the receive buffer due to a previous error.
do {
error = readByte(current, serial, startTime, timeoutMicros);
if (error) {
return error;
}
} while (current != 0x7e);
// Handle a repeated start byte which may happen
do {
error = unstuffByte(current, serial, startTime, timeoutMicros);
if (error) {
return error;
}
} while (current == 0x7e);
frame._address = current;
error = unstuffByte(frame._command, serial, startTime, timeoutMicros);
if (error) {
return error;
}
error = unstuffByte(frame._state, serial, startTime, timeoutMicros);
if (error) {
return error;
}
error = unstuffByte(dataLength, serial, startTime, timeoutMicros);
if (error) {
return error;
}
uint8_t checksum =
frame._address + frame._command + frame._state + dataLength;
if (dataLength > frame._bufferSize) {
return RxFrameError | BufferSizeError;
}
size_t i = 0;
while (i < dataLength) {
error = unstuffByte(current, serial, startTime, timeoutMicros);
if (error) {
return error;
}
frame._buffer[i] = current;
checksum += current;
i++;
}
uint8_t expectedChecksum = ~checksum;
uint8_t actualChecksum;
error = unstuffByte(actualChecksum, serial, startTime, timeoutMicros);
if (error) {
return error;
}
if (expectedChecksum != actualChecksum) {
return ReadError | ChecksumError;
}
uint8_t stop;
error = readByte(stop, serial, startTime, timeoutMicros);
if (error) {
return error;
}
if (stop != 0x7e) {
return ReadError | StopByteError;
}
if (frame._state & 0x7F) {
return ExecutionError | frame._state;
}
frame._dataLength = dataLength;
frame._numBytes = dataLength;
return NoError;
}
uint16_t SensirionShdlcCommunication::sendAndReceiveFrame(
Stream& serial, SensirionShdlcTxFrame& txFrame,
SensirionShdlcRxFrame& rxFrame, unsigned long rxTimeoutMicros) {
uint16_t error;
error = SensirionShdlcCommunication::sendFrame(txFrame, serial);
if (error) {
return error;
}
error = SensirionShdlcCommunication::receiveFrame(rxFrame, serial,
rxTimeoutMicros);
if (error) {
return error;
}
if (rxFrame.getCommand() != txFrame.getCommand()) {
return RxFrameError | RxCommandError;
}
if (rxFrame.getAddress() != txFrame.getAddress()) {
return RxFrameError | RxAddressError;
}
return NoError;
}
@@ -1,95 +0,0 @@
/*
* Copyright (c) 2020, Sensirion AG
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Sensirion AG nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef SENSIRION_SHDLC_COMMUNICATION_H_
#define SENSIRION_SHDLC_COMMUNICATION_H_
#include <stdint.h>
#include <stdlib.h>
#include "Arduino.h"
#include "SensirionShdlcRxFrame.h"
#include "SensirionShdlcTxFrame.h"
class SensirionShdlcTxFrame;
class SensirionShdlcRxFrame;
/*
* SensirionShdlcCommunication - Class which is responsible for the
* communication via a UART (Serial) interface. It provides functionality to
* send and receive frames from a Sensirion sensor. The data is sent and
* received in a SensirionShdlcTxFrame or SensirionShdlcRxFrame respectively.
*/
class SensirionShdlcCommunication {
public:
/**
* sendFrame() - Sends frame to sensor
*
* @param frame Tx frame object containing a finished frame to send to the
* sensor.
* @param serial Stream object to communicate with the sensor.
*
* @return NoError on success, an error code otherwise
*/
static uint16_t sendFrame(SensirionShdlcTxFrame& frame, Stream& serial);
/**
* receiveFrame() - Receive Frame from sensor
*
* @param frame Rx frame to store the received data in.
* @param serial Stream object to communicate with the sensor.
* @param timeoutMicros Timeout in micro seconds for the receive operation.
*
* @return NoError on success, an error code otherwise
*/
static uint16_t receiveFrame(SensirionShdlcRxFrame& frame, Stream& serial,
unsigned long timeoutMicros);
/**
* sendAndReceiveFrame() - Send and receive a frame from sensor.
*
* @param serial Stream object to communicate with the sensor.
* @param txFrame Tx frame object containing a finished frame to
* send to the sensor.
* @param rxFrame Rx frame to store the received data in.
* @param rxTimeoutMicros Timeout in micro seconds for the receive
* operation.
*
* @return NoError on success, an error code otherwise
*/
static uint16_t sendAndReceiveFrame(Stream& serial,
SensirionShdlcTxFrame& txFrame,
SensirionShdlcRxFrame& rxFrame,
unsigned long rxTimeoutMicros);
};
#endif /* SENSIRION_SHDLC_COMMUNICATION_H_ */
@@ -1,86 +0,0 @@
/*
* Copyright (c) 2020, Sensirion AG
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Sensirion AG nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef SENSIRION_SHDLC_RX_FRAME_H_
#define SENSIRION_SHDLC_RX_FRAME_H_
#include <stdint.h>
#include <stdlib.h>
#include "SensirionRxFrame.h"
#include "SensirionShdlcCommunication.h"
/**
* SenirionShdlcRxFrame - Class which decodes the through UART received data
* into common data types. It contains a buffer which is filled by the
* SensirionShdlcCommunication class. By calling the different decode function
* inherited from the SensirionRxFrame base class the raw data can be decoded
* into different data types. In addition to that it also stores the four
* header bytes defined by the SHDLC protocol state, command, address,
* datalength. These bytes can be read out by the corresponding getter method.
*/
class SensirionShdlcRxFrame : public SensirionRxFrame {
friend class SensirionShdlcCommunication;
public:
/**
* Constructor
*
* @param buffer Buffer in which the receive frame will be stored.
* @param bufferSize Number of bytes in the buffer for the receive frame.
*/
SensirionShdlcRxFrame(uint8_t buffer[], size_t bufferSize)
: SensirionRxFrame(buffer, bufferSize){};
uint8_t getAddress(void) const {
return _address;
};
uint8_t getCommand(void) const {
return _command;
};
uint8_t getState(void) const {
return _state;
};
uint8_t getDataLength(void) const {
return _dataLength;
};
private:
uint8_t _address = 0;
uint8_t _command = 0;
uint8_t _state = 0;
uint8_t _dataLength = 0;
};
#endif /* SENSIRION_SHDLC_RX_FRAME_H_ */
@@ -1,130 +0,0 @@
/*
* Copyright (c) 2020, Sensirion AG
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Sensirion AG nor the names of its
* contributors may be used to endorse or promote products derived from
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "SensirionShdlcTxFrame.h"
#include <stdint.h>
#include <stdlib.h>
#include "SensirionErrors.h"
uint16_t SensirionShdlcTxFrame::begin(uint8_t command, uint8_t address,
uint8_t dataLength) {
_buffer[_index++] = 0x7e;
uint16_t error = addUInt8(address);
error |= addUInt8(command);
error |= addUInt8(dataLength);
_command = command;
_address = address;
return error;
}
uint16_t SensirionShdlcTxFrame::finish(void) {
uint16_t error = addUInt8(~_checksum);
if (error) {
return error;
}
if (_index + 1 > _bufferSize) {
return TxFrameError | BufferSizeError;
}
_buffer[_index++] = 0x7e;
_isFinished = true;
return NoError;
}
uint16_t SensirionShdlcTxFrame::addUInt32(uint32_t data) {
uint16_t error = addUInt8(static_cast<uint8_t>((data & 0xFF000000) >> 24));
error |= addUInt8(static_cast<uint8_t>((data & 0x00FF0000) >> 16));
error |= addUInt8(static_cast<uint8_t>((data & 0x0000FF00) >> 8));
error |= addUInt8(static_cast<uint8_t>((data & 0x000000FF) >> 0));
return error;
}
uint16_t SensirionShdlcTxFrame::addInt32(int32_t data) {
return addUInt32(static_cast<uint32_t>(data));
}
uint16_t SensirionShdlcTxFrame::addUInt16(uint16_t data) {
uint16_t error = addUInt8(static_cast<uint8_t>((data & 0xFF00) >> 8));
error |= addUInt8(static_cast<uint8_t>((data & 0x00FF) >> 0));
return error;
}
uint16_t SensirionShdlcTxFrame::addInt16(int16_t data) {
return addUInt16(static_cast<uint16_t>(data));
}
uint16_t SensirionShdlcTxFrame::addUInt8(uint8_t data) {
if (_index + 2 > _bufferSize) {
return TxFrameError | BufferSizeError;
}
switch (data) {
case 0x11:
case 0x13:
case 0x7d:
case 0x7e:
// byte stuffing is done by inserting 0x7d and inverting bit 5
_buffer[_index++] = 0x7d;
_buffer[_index++] = data ^ (1 << 5);
break;
default:
_buffer[_index++] = data;
}
_checksum += data;
return NoError;
}
uint16_t SensirionShdlcTxFrame::addInt8(int8_t data) {
return addUInt8(static_cast<uint8_t>(data));
}
uint16_t SensirionShdlcTxFrame::addBool(bool data) {
return addUInt8(static_cast<uint8_t>(data));
}
uint16_t SensirionShdlcTxFrame::addFloat(float data) {
union {
uint32_t uInt32Data;
float floatData;
} convert;
convert.floatData = data;
return addUInt32(convert.uInt32Data);
}
uint16_t SensirionShdlcTxFrame::addBytes(const uint8_t data[],
size_t dataLength) {
uint16_t error = 0;
for (size_t i = 0; i < dataLength; i++) {
error |= addUInt8(data[i]);
}
return error;
}
@@ -1,184 +0,0 @@
/*
* Copyright (c) 2020, Sensirion AG
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Sensirion AG nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef SENSIRION_SHDLC_TX_FRAME_H_
#define SENSIRION_SHDLC_TX_FRAME_H_
#include <stdint.h>
#include <stdlib.h>
#include "SensirionShdlcCommunication.h"
/*
* SensirionShdlcTxFrame - Class which helps to build a correct SHDLC frame.
* The begin() functions writes the header. The different addDatatype()
* functions add the frame data and the finish() function writes the tail.
* Using these functions one can easily construct a SHDLC frame.
*/
class SensirionShdlcTxFrame {
friend class SensirionShdlcCommunication;
public:
/**
* Constructor
*
* @param buffer Buffer in which the send frame will be stored.
* @param bufferSize Number of bytes in the buffer for the send frame.
*/
SensirionShdlcTxFrame(uint8_t buffer[], size_t bufferSize)
: _buffer(buffer), _bufferSize(bufferSize) {
}
/**
* begin() - Begin frame and write header.
*
* @note This function needs to be called before calling other
* data add functions to write the header at the beginning.
*
* @param command Command byte to add to the send frame.
* @param address Address byte to add to the send frame.
* @param dataLength Data length byte to add to the send frame.
*
* @return NoError on success, an error code otherwise
*/
uint16_t begin(uint8_t command, uint8_t address, uint8_t dataLength);
/**
* finish() - Finish frame and write tail.
*
* @note This function needs to be called last, after adding all
* data to frame and before sending the frame to the sensor.
*
* @return NoError on success, an error code otherwise
*/
uint16_t finish(void);
/**
* addUInt32() - Add unsigned 32bit integer to the send frame.
*
* @param data Unsigned 32bit integer to add to the send frame.
*
* @return NoError on success, an error code otherwise
*/
uint16_t addUInt32(uint32_t data);
/**
* addInt32() - Add signed 32bit integer to the send frame.
*
* @param data Signed 32bit integer to add to the send frame.
*
* @return NoError on success, an error code otherwise
*/
uint16_t addInt32(int32_t data);
/**
* addUInt16() - Add unsigned 16bit integer to the send frame.
*
* @param data Unsigned 16bit integer to add to the send frame.
*
* @return NoError on success, an error code otherwise
*/
uint16_t addUInt16(uint16_t data);
/**
* addInt16() - Add signed 16bit integer to the send frame.
*
* @param data Signed 16bit integer to add to the send frame.
*
* @return NoError on success, an error code otherwise
*/
uint16_t addInt16(int16_t data);
/**
* addUInt8() - Add unsigned 8bit integer to the send frame.
*
* @param data Unsigned 8bit integer to add to the send frame.
*
* @return NoError on success, an error code otherwise
*/
uint16_t addUInt8(uint8_t data);
/**
* addInt8() - Add signed 8bit integer to the send frame.
*
* @param data Signed 8bit integer to add to the send frame.
*
* @return NoError on success, an error code otherwise
*/
uint16_t addInt8(int8_t data);
/**
* addBool() - Add boolean to the send frame.
*
* @param data Boolean to add to the send frame.
*
* @return NoError on success, an error code otherwise
*/
uint16_t addBool(bool data);
/**
* addFloat() - Add float to the send frame.
*
* @param data Float to add to the send frame.
*
* @return NoError on success, an error code otherwise
*/
uint16_t addFloat(float data);
/**
* addBytes() - Add byte array to the send frame.
*
* @param data Byte array to add to the send frame.
* @param dataLength Number of bytes to add to the send frame.
*
* @return NoError on success, an error code otherwise
*/
uint16_t addBytes(const uint8_t data[], size_t dataLength);
uint8_t getCommand(void) const {
return _command;
};
uint8_t getAddress(void) const {
return _address;
}
private:
uint8_t* _buffer;
size_t _bufferSize;
size_t _index = 0;
uint8_t _checksum = 0;
bool _isFinished = false;
uint8_t _command = 0;
uint8_t _address = 0;
};
#endif /* SENSIRION_SHDLC_TX_FRAME_H_ */
@@ -1,45 +0,0 @@
#! /usr/bin/env python3
import subprocess
import json
import argparse
import sys
import logging
def main():
parser = argparse.ArgumentParser()
parser.description = u'Compile test a sketch for all available boards'
parser.add_argument(u'-s', u'--sketch', dest=u'sketch',
required=True, help=u'Path to sketch')
args = parser.parse_args()
test_all_boards(args.sketch)
def test_all_boards(sketch):
logging.basicConfig(level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s')
log = logging.getLogger('arduino-compile-test')
process = subprocess.run("arduino-cli board listall --format json".split(),
stdout=subprocess.PIPE)
board_list_json = process.stdout.decode('utf-8')
board_list = json.loads(board_list_json)
test_list = ["arduino:samd:mkrzero", "arduino:avr:mega",
"arduino:avr:nano", "arduino:avr:uno",
"esp32:esp32:esp32", "esp8266:esp8266:generic"]
for board in test_list:
if board in (b['fqbn'] for b in board_list['boards']):
log.info('Test compilation for board {}'.format(board))
command = 'arduino-cli compile --libraries="." --warnings all'\
' --fqbn {board} {sketch}'.format(board=board,
sketch=sketch)
process = subprocess.run(command.split(), stdout=subprocess.PIPE)
if process.returncode:
log.error(process.stdout.decode('utf-8'))
sys.exit(process.returncode)
else:
log.error('Board not installed: {}'.format(board))
sys.exit(-1)
if __name__ == '__main__':
main()
@@ -1,3 +0,0 @@
#!/bin/bash
set -euxo pipefail
cppcheck --std=c++11 --language=c++ --error-exitcode=1 --enable=warning,style,performance,portability src/*
@@ -1,5 +0,0 @@
#! /bin/bash
set -euxo pipefail
editorconfig-checker
flake8
find . -type f -iregex ".*\.\(c\|h\|cpp\|ino\)" -exec clang-format-6.0 -i -style=file {} \; && git diff --exit-code
@@ -41,7 +41,7 @@
#include <Wire.h>
#include "../../SensirionCore/src/SensirionCore.h"
#include <SensirionCore.h>
class SensirionI2CSgp41 {
+142
View File
@@ -0,0 +1,142 @@
#include "GoIaqs.h"
#include <math.h>
namespace {
struct Anchor {
float x;
int y;
};
/** White-paper PM2.5 anchors (ug/m^3, score). */
const Anchor PM25_ANCHORS[] = {
{0.0f, 10}, {10.0f, 8}, {11.0f, 7}, {25.0f, 4}, {26.0f, 3}, {100.0f, 0},
};
/** White-paper CO2 anchors (ppm, score). */
const Anchor CO2_ANCHORS[] = {
{400.0f, 10}, {800.0f, 8}, {801.0f, 7},
{1400.0f, 4}, {1401.0f, 3}, {5000.0f, 0},
};
const float PM25_MIN = 0.0f;
const float PM25_MAX = 100.0f;
const float CO2_MIN = 400.0f;
const float CO2_MAX = 5000.0f;
inline float clampF(float v, float lo, float hi) {
if (v < lo) {
return lo;
}
if (v > hi) {
return hi;
}
return v;
}
inline int clampI(int v, int lo, int hi) {
if (v < lo) {
return lo;
}
if (v > hi) {
return hi;
}
return v;
}
/** Round-half-up (matching the reference TS implementation). */
inline int roundHalfUp(float v) { return (int)floorf(v + 0.5f); }
int interpolate(float value, const Anchor *anchors, size_t count) {
if (value <= anchors[0].x) {
return anchors[0].y;
}
const Anchor &last = anchors[count - 1];
if (value >= last.x) {
return last.y;
}
for (size_t i = 0; i < count - 1; i++) {
const Anchor &left = anchors[i];
const Anchor &right = anchors[i + 1];
if (value <= right.x) {
float interp =
(float)left.y +
((value - left.x) * (float)(right.y - left.y)) / (right.x - left.x);
return clampI(roundHalfUp(interp), GoIaqs::SCORE_MIN, GoIaqs::SCORE_MAX);
}
}
return GoIaqs::SCORE_MIN;
}
int pollutantScore(float value, float min, float max, const Anchor *anchors,
size_t count) {
if (!isfinite(value) || value > max) {
return GoIaqs::SCORE_MIN;
}
return interpolate(clampF(value, min, max), anchors, count);
}
} // namespace
int GoIaqs::pm25Score(float pm25UgM3) {
return pollutantScore(pm25UgM3, PM25_MIN, PM25_MAX, PM25_ANCHORS,
sizeof(PM25_ANCHORS) / sizeof(PM25_ANCHORS[0]));
}
int GoIaqs::co2Score(float co2Ppm) {
return pollutantScore(co2Ppm, CO2_MIN, CO2_MAX, CO2_ANCHORS,
sizeof(CO2_ANCHORS) / sizeof(CO2_ANCHORS[0]));
}
int GoIaqs::totalScore(int pm25, int co2) {
if (pm25 == co2) {
if (pm25 <= 7) {
int reduced = pm25 - 1;
return reduced < SCORE_MIN ? SCORE_MIN : reduced;
}
return pm25;
}
return pm25 < co2 ? pm25 : co2;
}
GoIaqs::Category GoIaqs::categoryOf(int totalScore) {
if (totalScore >= 8) {
return CategoryGood;
}
if (totalScore >= 4) {
return CategoryModerate;
}
return CategoryUnhealthy;
}
GoIaqs::Rgb GoIaqs::colorOf(Category category) {
switch (category) {
case CategoryGood:
return Rgb{0x64, 0x8E, 0xFF};
case CategoryModerate:
return Rgb{255, 128, 0};
case CategoryUnhealthy:
default:
return Rgb{255, 0, 0};
}
}
GoIaqs::Dominant GoIaqs::dominantOf(int pm25Score, int co2Score) {
if (pm25Score == co2Score) {
return DominantBoth;
}
return (pm25Score < co2Score) ? DominantPm25 : DominantCo2;
}
char GoIaqs::letterOf(Category category) {
switch (category) {
case CategoryGood:
return 'A';
case CategoryModerate:
return 'B';
case CategoryUnhealthy:
default:
return 'Z';
}
}
+94
View File
@@ -0,0 +1,94 @@
#ifndef _GO_IAQS_H_
#define _GO_IAQS_H_
#include <stdint.h>
/**
* @brief GO IAQS Starter Score implementation.
*
* Reference:
* https://www.airgradient.com/blog/go-iaqs-starter-score-technical-implementation
* Attribution: Achim Haug (AirGradient) for GO AQS. License: CC BY-SA 4.0.
*
* Converts PM2.5 (ug/m^3) and CO2 (ppm) concentrations into an integer
* "0..10" total score plus category and category color, using piecewise
* linear anchors and the synergetic combination rule defined in the
* white paper.
*
* Pure computation; no Arduino / hardware dependencies.
*/
class GoIaqs {
public:
enum Category {
CategoryGood = 0, /** total score 8..10, color #648EFF */
CategoryModerate = 1, /** total score 4..7, color #FFB000 */
CategoryUnhealthy = 2, /** total score 0..3, color #FF190C */
};
enum Dominant {
DominantPm25 = 0, /** PM2.5 has the worse per-pollutant score */
DominantCo2 = 1, /** CO2 has the worse per-pollutant score */
DominantBoth = 2, /** PM2.5 and CO2 per-pollutant scores are equal */
};
struct Rgb {
uint8_t r;
uint8_t g;
uint8_t b;
};
/** Score bounds (inclusive). */
static const int SCORE_MIN = 0;
static const int SCORE_MAX = 10;
/**
* @brief Compute per-pollutant score for PM2.5.
*
* @param pm25UgM3 PM2.5 concentration in ug/m^3.
* @return int in [SCORE_MIN .. SCORE_MAX]
*/
static int pm25Score(float pm25UgM3);
/**
* @brief Compute per-pollutant score for CO2.
*
* @param co2Ppm CO2 concentration in ppm.
* @return int in [SCORE_MIN .. SCORE_MAX]
*/
static int co2Score(float co2Ppm);
/**
* @brief Combine two per-pollutant scores into a single total score
* applying the synergetic rule:
* - If scores differ: total = min(pm25, co2).
* - If equal and shared <= 7: total = max(shared - 1, 0).
* - If equal and shared >= 8: total = shared.
*/
static int totalScore(int pm25, int co2);
/**
* @brief Map a total score to a category (Good / Moderate / Unhealthy).
*/
static Category categoryOf(int totalScore);
/**
* @brief Get the RGB color associated with a category, tuned for the
* LED bar (may differ from the white-paper hex values to match the
* physical LED output).
*/
static Rgb colorOf(Category category);
/**
* @brief Identify the pollutant that drives the total score. The lower
* per-pollutant score wins; equal scores return DominantBoth.
*/
static Dominant dominantOf(int pm25Score, int co2Score);
/**
* @brief Get the single-character letter grade for a category, per the
* white paper: Good -> 'A', Moderate -> 'B', Unhealthy -> 'Z'.
*/
static char letterOf(Category category);
};
#endif /** _GO_IAQS_H_ */
+156
View File
@@ -0,0 +1,156 @@
#include "SPS30.h"
#if !defined(ESP8266)
// The Sensirion library defines NO_ERROR which can conflict with ESP-IDF.
// Re-define locally if needed.
#ifdef NO_ERROR
#undef NO_ERROR
#endif
#define NO_ERROR 0
SPS30::SPS30(BoardType def) : _boardDef(def) {}
bool SPS30::begin(HardwareSerial &serial) {
if (_isBegin) {
AgLog("Already initialized, call end() then try again");
return true;
}
_bsp = getBoardDef(_boardDef);
if (_bsp == nullptr) {
AgLog("Board [%d] not supported", _boardDef);
return false;
}
_serial = &serial;
// Fully reset the serial port — it may have been left at 9600 baud
// with stale buffer data from a failed PMS5003 detection attempt.
_serial->end();
delay(100);
// Serial0 (UART0) defaults map to the correct PM connector pins.
// Serial1 shares the S8 connector pins and needs explicit GPIO mapping,
// same as PMS5003T::begin() does.
#if ARDUINO_USB_CDC_ON_BOOT
if (_serial == &Serial0) {
#else
if (_serial == &Serial) {
#endif
_serial->begin(115200);
} else {
_serial->begin(115200, SERIAL_8N1, _bsp->SenseAirS8.uart_rx_pin,
_bsp->SenseAirS8.uart_tx_pin);
}
// Flush any garbage bytes left in the RX buffer
while (_serial->available()) {
_serial->read();
}
_driver.begin(serial);
// Reset sensor to a known state — handles the case where the sensor
// was previously running or in an unknown mode.
_driver.deviceReset();
delay(100);
// Ensure sensor is in idle state before detection
_driver.stopMeasurement();
// Try reading serial number as a detection mechanism
int8_t serialNumber[32] = {0};
int16_t error = _driver.readSerialNumber(serialNumber, sizeof(serialNumber));
if (error != NO_ERROR) {
AgLog("SPS30 not detected (readSerialNumber error: %d)", error);
_serial = nullptr;
return false;
}
AgLog("SPS30 detected, serial: %s", reinterpret_cast<char *>(serialNumber));
// Start continuous measurement in float output format
error = _driver.startMeasurement(SPS30_OUTPUT_FORMAT_OUTPUT_FORMAT_FLOAT);
if (error != NO_ERROR) {
AgLog("SPS30 startMeasurement failed (error: %d)", error);
_serial = nullptr;
return false;
}
_isBegin = true;
_connected = true;
_consecutiveErrors = 0;
return true;
}
void SPS30::end() {
if (!_isBegin) {
return;
}
_driver.stopMeasurement();
_isBegin = false;
_connected = false;
_serial = nullptr;
AgLog("De-initialized");
}
bool SPS30::readValues() {
if (!_isBegin) {
return false;
}
float mc1p0, mc2p5, mc4p0, mc10p0;
float nc0p5, nc1p0, nc2p5, nc4p0, nc10p0;
float typicalParticleSize;
int16_t error = _driver.readMeasurementValuesFloat(
mc1p0, mc2p5, mc4p0, mc10p0, nc0p5, nc1p0, nc2p5, nc4p0, nc10p0,
typicalParticleSize);
if (error != NO_ERROR) {
_consecutiveErrors++;
if (_consecutiveErrors >= kMaxConsecutiveErrors) {
_connected = false;
}
return false;
}
// Cache the successfully read values
_mc1p0 = mc1p0;
_mc2p5 = mc2p5;
_mc4p0 = mc4p0;
_mc10p0 = mc10p0;
_nc0p5 = nc0p5;
_nc1p0 = nc1p0;
_nc2p5 = nc2p5;
_nc4p0 = nc4p0;
_nc10p0 = nc10p0;
_typicalParticleSize = typicalParticleSize;
_connected = true;
_consecutiveErrors = 0;
return true;
}
bool SPS30::connected() { return _connected; }
// -- Mass concentration (µg/m³) → int, matching PMS5003 interface -----------
int SPS30::getPm01Ae() { return static_cast<int>(_mc1p0); }
int SPS30::getPm25Ae() { return static_cast<int>(_mc2p5); }
int SPS30::getPm10Ae() { return static_cast<int>(_mc10p0); }
// SPS30 has no Ae/SP distinction — return the same values
int SPS30::getPm01Sp() { return static_cast<int>(_mc1p0); }
int SPS30::getPm25Sp() { return static_cast<int>(_mc2p5); }
int SPS30::getPm10Sp() { return static_cast<int>(_mc10p0); }
// -- Number concentration: convert #/cm³ → #/0.1L (×100) -------------------
int SPS30::getPm05ParticleCount() { return static_cast<int>(_nc0p5 * 100.0f); }
int SPS30::getPm01ParticleCount() { return static_cast<int>(_nc1p0 * 100.0f); }
int SPS30::getPm25ParticleCount() { return static_cast<int>(_nc2p5 * 100.0f); }
int SPS30::getPm10ParticleCount() { return static_cast<int>(_nc10p0 * 100.0f); }
#endif // !ESP8266
+132
View File
@@ -0,0 +1,132 @@
#ifndef _AIR_GRADIENT_SPS30_H_
#define _AIR_GRADIENT_SPS30_H_
#include "../Main/BoardDef.h"
#if defined(ESP8266)
/**
* @brief Stub class for ESP8266 — SPS30 UART is not supported on this platform.
* Provides the type so AirGradient.h compiles, but begin() always returns false.
*/
class SPS30 {
public:
explicit SPS30(BoardType def) {}
bool begin(Stream &serial) { return false; }
void end() {}
bool readValues() { return false; }
bool connected() { return false; }
int getPm01Ae() { return 0; }
int getPm25Ae() { return 0; }
int getPm10Ae() { return 0; }
int getPm01Sp() { return 0; }
int getPm25Sp() { return 0; }
int getPm10Sp() { return 0; }
int getPm05ParticleCount() { return 0; }
int getPm01ParticleCount() { return 0; }
int getPm25ParticleCount() { return 0; }
int getPm10ParticleCount() { return 0; }
};
#else // ESP32
#include <SensirionUartSps30.h>
/**
* @brief Wrapper class for Sensirion SPS30 particulate matter sensor over UART.
*
* Follows the same interface pattern as PMS5003 so it can be used as a
* drop-in alternative PM sensor on the ONE_INDOOR board.
*
* Key differences from PMS5003:
* - SHDLC protocol at 115200 baud (vs PMS5003 streaming at 9600)
* - No continuous handle() needed; values are polled via readValues()
* - No PM0.3 or PM5.0 particle count bins
* - No distinction between atmospheric-environment and standard-particle
* mass concentrations (same values mapped to both)
*/
class SPS30 {
public:
explicit SPS30(BoardType def);
/**
* @brief Initialize sensor on the given serial port.
*
* Configures the serial port to 115200 baud, performs sensor detection
* (reads serial number), and starts continuous measurement in float mode.
*
* @param serial HardwareSerial instance (e.g. Serial0)
* @return true Sensor detected and measurement started
* @return false Sensor not found or initialization failed
*/
bool begin(HardwareSerial &serial);
/**
* @brief Stop measurement and release the serial port.
*/
void end();
/**
* @brief Poll the sensor for the latest measurement.
*
* Should be called periodically (e.g. every 2 s via pmsSchedule).
* Internally calls readMeasurementValuesFloat() over SHDLC.
*
* @return true New data read successfully
* @return false Read failed (sensor may be disconnected)
*/
bool readValues();
/**
* @brief Check whether the sensor is connected / responding.
*
* Based on the result of the most recent readValues() call.
*/
bool connected();
// -- Mass concentration (µg/m³), truncated to int like PMS5003 ----------
int getPm01Ae();
int getPm25Ae();
int getPm10Ae();
// SPS30 has no Ae/SP distinction — return the same values
int getPm01Sp();
int getPm25Sp();
int getPm10Sp();
// -- Number concentration (converted from #/cm³ to #/0.1 L) -------------
int getPm05ParticleCount();
int getPm01ParticleCount();
int getPm25ParticleCount();
int getPm10ParticleCount();
private:
bool _isBegin = false;
bool _connected = false;
int _consecutiveErrors = 0;
BoardType _boardDef;
const BoardDef *_bsp = nullptr;
SensirionUartSps30 _driver;
HardwareSerial *_serial = nullptr;
/// Maximum consecutive read errors before reporting disconnected
static constexpr int kMaxConsecutiveErrors = 3;
// Cached raw values from last successful read
float _mc1p0 = 0.0f;
float _mc2p5 = 0.0f;
float _mc4p0 = 0.0f;
float _mc10p0 = 0.0f;
float _nc0p5 = 0.0f;
float _nc1p0 = 0.0f;
float _nc2p5 = 0.0f;
float _nc4p0 = 0.0f;
float _nc10p0 = 0.0f;
float _typicalParticleSize = 0.0f;
};
#endif // ESP8266 / ESP32
#endif // _AIR_GRADIENT_SPS30_H_