As mentioned in section 1, the Raspberry Pi PH Circuit (https://undiluted.org/raspberry-pi-ph-circuit/) was selected, incorporating a PH sensor from AtlasScientific (https://www.atlas-scientific.com/product_pages/kits/ph-kit.html) and a Raspberry Pi Zero.

Summary - What PHreader is

In short, PHreader is a piece of software written in [C](https://en.wikipedia.org/wiki/C_(programming_language) to interface to the PH sensor.

PHreader communicates with the sensor to pull PH data over the I²C protocol via UART.

PHreader utilizes, the Wiring Pi Library version 2.50 (which was subsequently deprecated, but was taken over by GC2 in 2024) to communicate with the sensor/circuit.

The C program communicates with the sensor, continuously in a loop as a background daemon on the raspberry pi, retrieving data and storing the Ph value in a Postgresql database.

To store the sensor data in the PostgreSQL database, the software utilizes the libpq library.

Configuration

Since the software was written in C, using a Declarative, Block-Structured Configuration Format, made the mode sense.

This gave us the ability to have a configuration such as:

phreader = {
  debug = true;
  logfile = "/tmp/phreader.log";

  loop_interval = 3600;

  serial = {
    device = "/dev/ttyAMA0";
    baudrate = 9600;
  };

  database = {
    hostname = "";
    port = 5432;
    username = "";
    password = "";
    database = "";
    ssl_mode = false;
  };
};

Using libconfig we can easily parse our configuration into appropriate C structures, such as:

typedef struct pg_config {
  const char *username;
  const char *password;
  const char *hostname;
  int port;
  const char *database;
  int ssl_mode;
} pg_config;

typedef struct serial_config {
  const char *device;
  int baudrate;
} serial_config;

typedef struct phreader_config {
  int debug;
  int loop_interval;
  const char *logfile;
  const char *pidfile;
  int valid;
  pg_config pg_config;
  serial_config serial_config;
} phreader_config;

We can then utilize the configuration structures, in the main daemon loop to communicate with the sensor over UART (serial) to fetch our data, and to store the PH data in the Postgresql database.

Main Daemon Loop

The main daemon loop is simple it does a few things, in a continuous loop:

  • Open the serial port for reading

This is easily done thanks to the WiringPi library. To open the serial port, we simply do:

fd = serialOpen(cfg.serial_config.device, cfg.serial_config.baudrate))
  • Initialize the WiringPi Library

This is simply a call to make the GPIO pins to have sequential numbering that the WiringPi library uses vs the physical/Broadcom pin numbers.

wiringPiSetup()
  • Wake up Ph sensor circuit

For this piece, we wrote our own functions which are basically a wrapper around the Ph Circuits serial commands.

To wake up the Circuit, we simply send a 'Wake' command over serial.

serialPrintf(fd, "Wake\r");
  • Switch to continuous mode

Similar to the Wake command, the circuit allows for a continuous mode. The continous mode can be used when receiving data from the circuit.

serialPrintf(fd, "C,%d\r", seconds);
  • Fetch the Ph data

To fetch data, while the circuit is in continous mode, we simply send a read command and then listen for the data on the serial port.

serialPrintf(fd, "R\r"); // This starts our fetch
data = uartRecv(fd);
uartData * uartRecv(int fd)
{
  char *buf;
  char c;
  int count = 0;
  int iteration = 0;
  int index = 0;
  int line = 0;
  uartData *data = NULL;

  while((count = serialDataAvail(fd)) != 0)
  {
    c = serialGetchar(fd);

    if (!iteration) {
      buf = (char *)malloc((size_t)count + 1);
    }

    if (c == '\r') {
      buf[index++] = '\0';

      if (buf[0] == '*') {
        // cmd code response
        data = addUartData(data, line, "CMD_CODE", buf);
      }
      else
      {
        // actual data response
        data = addUartData(data, line, "DATA", buf);
      }

      line++;

      // realloc if we have more to read.
      if (count > 0) {
        // if realloc fails, break.
        if (realloc(buf, count) == NULL) {
          break;
        }

        index = 0;
      }
    }
    else
    {
      buf[index++] = c;
    }

    //fflush(stdout);
    iteration++;
  }

  free(buf);
  return data;
}
  • Set back to sleep mode

This is simply a call over the Serial port in order to put the circuit back into sleep mode to save power.

serialPrintf(fd, "sleep\r");
  • Close the serial port

Using the WiringPi library, we can close the serial port.

serialClose(fd);
  • Connect to the Postgresql DB

Using the libpgq library, we connect to the Postgresql DB.

db_uri = create_pg_db_uri(cfg.pg_config);
PGconn *conn = pg_connect(db_uri);
free(db_uri);   
  • Store the Ph data in the Postgresql DB

Using the libpgq library, we call our insert query to store our current Ph value and timestamp.

insert_ph(conn, ph);
  • Close the connection to the Postgesql DB

Assuming we are connected, we can then close our DB connection, and go to sleep until the next iteration of the loop.

pg_close(conn);
...

sleep(cfg.loop_interval);

The next step was to expose the data using a REST API.