Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat ph #1

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .clang-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Language: Cpp
BasedOnStyle: WebKit
BreakBeforeBraces: Attach
NamespaceIndentation: None
30 changes: 30 additions & 0 deletions API_ru.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# TroykaPH API (RUS)

## `class TroykaPH`

Создайте объект типа `TroykaPH` для управления [модулем изменения pH жидкостей TroykaPH](https://amperka.ru/product/troyka-ph-sensor).
yurybotov marked this conversation as resolved.
Show resolved Hide resolved

### `TroykaPH(uint8_t pin)`

Создает новый объект TroykaPH.

- `pin`: аналоговый пин к которому подключен модуль.

### `void begin(float correction = 1.0, float zeroShift = 2.0)`

- `correction` - коррекция с учетом сдвига напряжения внутреннего источника опорного напряжения AVR.
- `zeroShift` - коррекция с учетом реального сдвига `0` при измерениях.

Инициализирует библиотеку. Вызовите этот метод до начала взаимодействия с TroykaPH. Например в функции `setup()`.

- `correctionMultiplyer`: корректировочный множитель полученный в результате процедуры калибровки. При отсутствии принимается равным `1`.
yurybotov marked this conversation as resolved.
Show resolved Hide resolved

### `void update(long periodMilliseconds = 1000)`

Производит периодическое чтение показаний датчика и корректировочных коэфициентов. Разместите его вызов в `loop()`.

- `periodMilliseconds`: период опроса датчик.

### `float read() const;`

Возвращает последнее замеренное значение pH.
102 changes: 102 additions & 0 deletions examples/calibrate/calibrate.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*

Берем плату Arduino или совместимую, подключаем по USB, прошиваем этим скетчем и
запускаем скетч. Выплняем указания в Serial Monitor.
yurybotov marked this conversation as resolved.
Show resolved Hide resolved

Важно: надо ввести в Serial Monitor значение в миллвольтах - 4 цифры, незначащие
yurybotov marked this conversation as resolved.
Show resolved Hide resolved
правые дополнить нулями.

*/

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest using only one language in comments.

/*
This example don't use library. It demonstrates TroykaPH calibration procedure
*/

float measureVop() {
#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#elif defined(__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
ADMUX = _BV(MUX5) | _BV(MUX0);
#elif defined(__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
ADMUX = _BV(MUX3) | _BV(MUX2);
#else
ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#endif
delay(75);
float Vop = 0;
for (uint8_t i = 0; i < 100; i++) {
ADCSRA |= _BV(ADSC);
while (bit_is_set(ADCSRA, ADSC))
;
uint8_t low = ADCL;
uint8_t high = ADCH;

Vop += (float)((high << 8) | low) * 5. / 1024.;
delay(10);
}
return Vop / 100.;
}

float measureZeroLevel() {
(void)analogRead(A4);
float Vzero = 0;
for (uint8_t i = 0; i < 100; i++) {
Vzero += (float)analogRead(A4) * 5. / 1024.;
delay(10);
}
return Vzero / 100.;
}

float factor;

void setup() {
Serial.begin(9600);
while (!Serial)
;

Serial.print("\n\n\nTroyka pH module zero-shift calibration procedure\n\n");
Serial.print("- Connect TroykaPH module to Arduino board.\n");
Serial.print("- Shortcut TroykaPH module BNC input to ground.\n");
Serial.print("- Measure (using good multimeter) Arduino board feed voltage between 5V and GND pins\n");
Serial.print("- Input this value in millivolts to Serial Monitor input line, press 'Send' after it.\n");
Serial.print("For example: if measured value is '4.93' volts - punch '4930'\n\n");
}

void loop() {
if (Serial.available() > 0) {
float Vmeasured = (float)Serial.parseInt() / 1000;
if (Vmeasured == 0)
return;
Serial.print("Voltage measured by multimeter (V) = ");
Serial.println(Vmeasured, 3);
float Vop = measureVop();
Serial.print("Voltage of internal reference (V) = ");
Serial.println(Vop, 3);
float VccCalculated = Vop * 5. / 1.1;
Serial.print("Calculated Vcc (V) = ");
Serial.println(VccCalculated, 3);

factor = (float)(VccCalculated) / (float)(Vmeasured);

float VccReal = 5. / factor;
Serial.print("Real Vcc (V) = ");
Serial.println(VccReal, 3);

float Vzero = measureZeroLevel();
Serial.print("Calculated zero-shift voltage (V) = ");
Serial.println(Vzero, 3);
float VzeroReal = Vzero / factor;
Serial.print("Real zero-shift voltage (V) = ");
Serial.println(VzeroReal, 3);

Serial.print("Calibration complete.");
Serial.print("\n\nCall in your 'setup()' method {your-pH-sensor-name}.begin(");
Serial.print(factor, 3);
Serial.print(", ");
Serial.print(VzeroReal, 3);
Serial.print("); \n\n");

Serial.println("Recalibration is needed if you change Arduino board to another.\n\n");
delay(5000);
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you use microcontroller registers? After all, the calibration example won't work with the Arduino Due and ESP8226 boards?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the standard Arduino API, there is no way to read the voltage at the internal Vref. There is only a way to use it instead of Aref. Now, as you can see, there is a way only for different AVRs. The options for other controllers can be worked on in the next version.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose to write from which boards support the calibration process. And there are no other options easier than using Vref?

30 changes: 30 additions & 0 deletions examples/using/using.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think an example should be called simpleReadPH

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. Done.

This example demonstrate pH value reading
*/
#include "Arduino.h"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#include "Arduino.h"
#include "Arduino.h"

Why are you include Arduino.h in the example?

Copy link
Collaborator Author

@yurybotov yurybotov Nov 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My compiler (under linux) swore at millis


// Include library
#include "troykaph.h"
yurybotov marked this conversation as resolved.
Show resolved Hide resolved

TroykaPH phMeter(A4); // set used analog pin

uint32_t showingTime;

void setup() {
phMeter.begin(); // if module not calibrated or...
// phMeter.begin(correction,zeroShift); // if you have it (use calibrate.ino) for it

Serial.begin(9600);
showingTime = millis() + 3000; // show result once per 3 seconds
}

void loop() {
phMeter.update(1000); // real read from sensor once per second
// (you can increase this period, in practice pH value changing too slowly)

if (showingTime <= millis()) {
yurybotov marked this conversation as resolved.
Show resolved Hide resolved
Serial.print("\nCurrent pH: ");
Serial.print(phMeter.read(), 1);
showingTime += 3000;
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How the example works? Why not do it in our standard way?

void loop() {
    // Read the sensor data values PH
    phMeter.read();
    // Receive the stored PH
    float valuePH = phMeter.getPH();
    // Print results
    Serial.print("PH Value = ");
    Serial.println(valuePH);
    // Wait 1000 ms
    delay(1000);
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes no sense to constantly poll the pH sensor. First, its meaning changes very slowly. Secondly, it's a long time, there are a lot of delays when reading. Therefore, it makes sense to read from time to time, and return the last read value upon request. As here.

9 changes: 9 additions & 0 deletions library.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name=TroykaPH
version=1.0.0
author=Yury Botov <[email protected]>
maintainer=Amperka <amperka.ru>
sentence=Library for TroykaPH sensor.
paragraph=Allows calibrate and read sensor value
category=Sensor
url=https://github.com/amperka/TroykaPH
architectures=*
89 changes: 89 additions & 0 deletions src/troykaph.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* This file is a part of TroykaPH library.
*
* Product page: https://amperka.ru/product/zelo-folow-line-sensor
yurybotov marked this conversation as resolved.
Show resolved Hide resolved
* © Amperka LLC (https://amperka.com, [email protected])
*
* Author: Yury Botov <[email protected]>
* License: GPLv3, all text here must be included in any redistribution.
*/

#include "troykaph.h"
yurybotov marked this conversation as resolved.
Show resolved Hide resolved

TroykaPH::TroykaPH(uint8_t pin) {
_pin = pin;
_lastValue = 7;
_correction = 1.0;
_nextMeasureTime = 0;
}

void TroykaPH::begin(float correction, float zeroLevel) {
_correction = correction;
_zeroLevel = zeroLevel;
}

static float fmap(float value, float minVal, float maxVal, float minOut, float maxOut) {
return (value - minVal) / (maxVal - minVal) * (maxOut - minOut) + minOut;
}

void TroykaPH::update(long periodMilliseconds) {
constexpr float idealVcc = 5.0;
constexpr float minPh = 0.0;
constexpr float maxPh = 14.0;
constexpr float phHalfRangeInVolts = 0.82;

float value = 0;
if (_nextMeasureTime <= millis()) {
yurybotov marked this conversation as resolved.
Show resolved Hide resolved
_nextMeasureTime += periodMilliseconds;
// read value
(void)analogRead(_pin);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(void)analogRead(_pin);
(void)analogRead(_pin);

Why?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Googling showed that in cases where greater accuracy is needed, you must first make an empty read so that the internal analog multiplexer switches to the desired channel, and only after a pause, when the internal capacitor voltage value stabilizes, read real data.

delay(75);
for (uint8_t i = 0; i < 10; i++) {
value += (float)analogRead(_pin) * 5.0 / 1024.;
}
value = value / 10;
// read real Vcc value
float realVcc = (float)(_readVcc()) / 1000.;

float result = value * idealVcc / realVcc;
result /= _correction; // internal reference source correction

_lastValue = fmap(result, _zeroLevel - phHalfRangeInVolts, _zeroLevel + phHalfRangeInVolts, minPh, maxPh);
}
}

float TroykaPH::read() const { return _lastValue; }

long TroykaPH::_readVcc() {
constexpr float adcResolution = 1024.;
constexpr float meanReferenceVoltage = 1.1;
constexpr long toMillivolts = 1000L;

long result = 0;
#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#elif defined(__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
ADMUX = _BV(MUX5) | _BV(MUX0);
#elif defined(__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
ADMUX = _BV(MUX3) | _BV(MUX2);
#else
ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#endif

delay(75);
// internal reference reading
for (uint8_t i = 0; i < 10; i++) {
ADCSRA |= _BV(ADSC);
while (bit_is_set(ADCSRA, ADSC))
;

uint8_t low = ADCL;
uint8_t high = ADCH;

result += (high << 8) | low;
}
result /= 10;

result = (long)(adcResolution * meanReferenceVoltage) * toMillivolts / result;
return result;
}
34 changes: 34 additions & 0 deletions src/troykaph.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* This file is a part of TroykaPH library.
*
* Product page: https://amperka.ru/product/zelo-folow-line-sensor
* © Amperka LLC (https://amperka.com, [email protected])
*
* Author: Yury Botov <[email protected]>
* License: GPLv3, all text here must be included in any redistribution.
*/

#ifndef __TROYKA_PH_H__
#define __TROYKA_PH_H__

#include "Arduino.h"

class TroykaPH {
public:
TroykaPH(uint8_t pin);
void begin(float correction = 1.0, float zeroLevel = 2.0);

void update(long periodMilliseconds = 1000);

float read() const;

private:
uint8_t _pin;
float _lastValue;
float _correction;
float _zeroLevel;
uint32_t _nextMeasureTime;
long _readVcc();
};

#endif //__TROYKA_PH_H__
yurybotov marked this conversation as resolved.
Show resolved Hide resolved