338 lines
14 KiB
C++
338 lines
14 KiB
C++
/**
|
|
* @file itender.ino
|
|
* @brief Official proxy code for iTender GPIO Communication via Serial.
|
|
*
|
|
* This code acts as a communication bridge between an Arduino and a Raspberry Pi
|
|
* using JSON over a serial connection (USB). It allows the Raspberry Pi to
|
|
* control Arduino GPIO pins (set digital/analog output, get digital/analog input)
|
|
* and read sensor data (specifically from multiple HX711 load cells dynamically).
|
|
*/
|
|
|
|
#include <ArduinoJson.h> // Include the ArduinoJson library for JSON parsing and serialization.
|
|
#include "HX711.h" // Include the HX711 library for interacting with the load cell amplifier.
|
|
|
|
// Define the size of the JSON buffer for incoming messages.
|
|
// Adjust this size based on the maximum expected size of incoming JSON messages.
|
|
#define JSON_BUFFER_SIZE 256
|
|
|
|
// Create a static JSON document for incoming messages.
|
|
// Static allocation is efficient but has a fixed size.
|
|
StaticJsonDocument<JSON_BUFFER_SIZE> incomingJson;
|
|
|
|
// Create a dynamic JSON document for outgoing messages.
|
|
// Dynamic allocation is more flexible but can lead to memory fragmentation on small microcontrollers.
|
|
DynamicJsonDocument outgoingJson(JSON_BUFFER_SIZE);
|
|
|
|
// --- Multiple HX711 Handling ---
|
|
|
|
// Define the maximum number of HX711 sensors we can manage tare offsets for.
|
|
// Adjust this based on available Arduino memory if needed.
|
|
#define MAX_SENSORS 5
|
|
|
|
// Structure to hold the pins and tare offset for a single sensor.
|
|
struct SensorTare {
|
|
int dataPin = -1;
|
|
int clockPin = -1;
|
|
long tareOffset = 0;
|
|
};
|
|
|
|
// Array to store the tare offsets for multiple sensors.
|
|
SensorTare sensorTareOffsets[MAX_SENSORS];
|
|
|
|
/**
|
|
* @brief Finds the index of a sensor in the sensorTareOffsets array based on its pins.
|
|
* @param dataPin The data pin of the sensor.
|
|
* @param clockPin The clock pin of the sensor.
|
|
* @return The index of the sensor, or -1 if not found.
|
|
*/
|
|
int findSensorTareIndex(int dataPin, int clockPin) {
|
|
for (int i = 0; i < MAX_SENSORS; i++) {
|
|
if (sensorTareOffsets[i].dataPin == dataPin && sensorTareOffsets[i].clockPin == clockPin) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1; // Sensor not found
|
|
}
|
|
|
|
/**
|
|
* @brief Adds or updates the tare offset for a sensor based on its pins.
|
|
* @param dataPin The data pin of the sensor.
|
|
* @param clockPin The clock pin of the sensor.
|
|
* @param offset The tare offset to store.
|
|
* @return The index where the offset was stored/updated, or -1 if the array is full.
|
|
*/
|
|
int addOrUpdateSensorTare(int dataPin, int clockPin, long offset) {
|
|
int index = findSensorTareIndex(dataPin, clockPin);
|
|
|
|
if (index != -1) {
|
|
// Sensor found, update the offset
|
|
sensorTareOffsets[index].tareOffset = offset;
|
|
return index;
|
|
} else {
|
|
// Sensor not found, find the first available slot
|
|
for (int i = 0; i < MAX_SENSORS; i++) {
|
|
if (sensorTareOffsets[i].dataPin == -1) { // Found an empty slot
|
|
sensorTareOffsets[i].dataPin = dataPin;
|
|
sensorTareOffsets[i].clockPin = clockPin;
|
|
sensorTareOffsets[i].tareOffset = offset;
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
return -1; // Array is full
|
|
}
|
|
|
|
/**
|
|
* @brief Gets the stored tare offset for a sensor based on its pins.
|
|
* @param dataPin The data pin of the sensor.
|
|
* @param clockPin The clock pin of the sensor.
|
|
* @return The stored tare offset, or 0 if the sensor is not found.
|
|
*/
|
|
long getSensorTareOffset(int dataPin, int clockPin) {
|
|
int index = findSensorTareIndex(dataPin, clockPin);
|
|
if (index != -1) {
|
|
return sensorTareOffsets[index].tareOffset;
|
|
}
|
|
return 0; // Return 0 offset if sensor not found (effectively untared)
|
|
}
|
|
|
|
// --- End Multiple HX711 Handling ---
|
|
|
|
|
|
void setup() {
|
|
// Initialize serial communication at 9600 baud rate.
|
|
// This speed should match the serial communication speed on the Raspberry Pi.
|
|
Serial.begin(9600);
|
|
|
|
// Initialize the sensorTareOffsets array.
|
|
for (int i = 0; i < MAX_SENSORS; i++) {
|
|
sensorTareOffsets[i].dataPin = -1; // Mark as empty
|
|
}
|
|
|
|
// Optional: Wait for the serial port to connect.
|
|
while (!Serial);
|
|
}
|
|
|
|
// Declare a function pointer to address 0.
|
|
// Calling this function effectively resets the Arduino.
|
|
void (*resetFunc)(void) = 0;
|
|
|
|
void loop() {
|
|
// Check if there is data available to read from the serial port.
|
|
if (Serial.available()) {
|
|
// Read the incoming JSON message from the serial buffer.
|
|
// deserializeJson reads from the Serial stream until it finds a complete JSON object.
|
|
DeserializationError error = deserializeJson(incomingJson, Serial);
|
|
|
|
// Check if deserialization was successful.
|
|
if (error) {
|
|
// Handle deserialization error.
|
|
// You might want to send an error message back to the Raspberry Pi.
|
|
Serial.print(F("Deserialization failed: "));
|
|
Serial.println(error.f_str());
|
|
// Clear the serial buffer to prevent processing incomplete messages.
|
|
while (Serial.available()) Serial.read();
|
|
} else {
|
|
// Deserialization was successful. Process the incoming message.
|
|
|
|
// Extract the "id", "type", and "data" fields from the JSON object.
|
|
// The "id" is used to correlate requests and responses.
|
|
String id = incomingJson["id"];
|
|
// The "type" determines the action to perform.
|
|
int type = incomingJson["type"];
|
|
// The "data" field contains parameters for the action.
|
|
JsonVariant data = incomingJson["data"];
|
|
|
|
// Create a nested object named "data" within the root outgoing JSON object.
|
|
// This will hold the response data.
|
|
JsonObject outgoingData = outgoingJson.to<JsonObject>().createNestedObject("data");
|
|
|
|
// Assume success by default, set to false in case of errors within cases.
|
|
outgoingData["success"] = true;
|
|
|
|
// Check if the 'data' field exists and is not null before accessing its members.
|
|
if (!data.isNull()) {
|
|
// Handle the message based on the "type" field using a switch statement.
|
|
switch (type) {
|
|
case 1: // Handle SET_PIN message: Set the state of a GPIO pin.
|
|
{ // Use a block to scope variables declared within the case.
|
|
int pin = data["pin"];
|
|
String mode = data["mode"];
|
|
int value = data["value"];
|
|
|
|
// Set the pin mode to OUTPUT.
|
|
pinMode(pin, OUTPUT);
|
|
|
|
// Check the mode (DIGITAL or ANALOG).
|
|
if (mode == "DIGITAL") {
|
|
// For digital mode, write HIGH or LOW.
|
|
digitalWrite(pin, value); // Assuming value is 0 for LOW, 1 for HIGH.
|
|
} else if (mode == "ANALOG") {
|
|
// For analog mode (PWM), write a value between 0 and 255.
|
|
// Ensure the pin supports PWM output.
|
|
analogWrite(pin, value); // Use the value directly for PWM.
|
|
} else {
|
|
outgoingData["success"] = false;
|
|
outgoingData["error"] = "Invalid mode for SET_PIN";
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 2: // Handle GET_VAL message: Get the current value of a GPIO pin.
|
|
{ // Use a block to scope variables declared within the case.
|
|
int pin = data["pin"];
|
|
String mode = data["mode"];
|
|
int val;
|
|
|
|
// Set the pin mode to INPUT.
|
|
pinMode(pin, INPUT);
|
|
|
|
// Check the mode (DIGITAL or ANALOG).
|
|
if (mode == "DIGITAL") {
|
|
// For digital mode, read HIGH or LOW.
|
|
val = digitalRead(pin);
|
|
} else if (mode == "ANALOG") {
|
|
// For analog mode, read the analog value (0-1023 for most Arduinos).
|
|
val = analogRead(pin);
|
|
} else {
|
|
outgoingData["success"] = false;
|
|
outgoingData["error"] = "Invalid mode for GET_VAL";
|
|
break; // Exit the case after setting the error.
|
|
}
|
|
// Add the read value to the outgoing JSON data.
|
|
outgoingData["value"] = val;
|
|
}
|
|
break;
|
|
|
|
case 3: // Handle GET_SENSOR message: Read data from a sensor (specifically HX711).
|
|
{ // Use a block to scope variables declared within the case.
|
|
// Get the data and clock pins for the HX711 from the incoming data.
|
|
const int LOADCELL_DATA_PIN = (int)data["pin_data"];
|
|
const int LOADCELL_CLOCK_PIN = (int)data["pin_clock"];
|
|
|
|
// Check if pins are valid.
|
|
if (LOADCELL_DATA_PIN < 0 || LOADCELL_CLOCK_PIN < 0) {
|
|
outgoingData["success"] = false;
|
|
outgoingData["error"] = "HX711 data and clock pins not provided or invalid";
|
|
break;
|
|
}
|
|
|
|
// Create a temporary HX711 object for this specific sensor.
|
|
HX711 currentScale;
|
|
currentScale.begin(LOADCELL_DATA_PIN, LOADCELL_CLOCK_PIN);
|
|
|
|
// Wait for the HX711 to be ready.
|
|
if (currentScale.wait_ready_timeout(1000)) { // Wait up to 1000ms
|
|
// Get the raw value from the scale.
|
|
long raw_value = currentScale.get_value(); // Use get_value() for raw reading.
|
|
|
|
// Get the stored tare offset for these pins.
|
|
long tare_offset = getSensorTareOffset(LOADCELL_DATA_PIN, LOADCELL_CLOCK_PIN);
|
|
|
|
// Calculate the taried value.
|
|
long taried_value = raw_value - tare_offset;
|
|
|
|
// Add the taried value to the outgoing JSON data.
|
|
outgoingData["value"] = taried_value;
|
|
|
|
/*
|
|
// Example calculation for weight in grams using a scale factor (requires calibration).
|
|
// You would need to store and retrieve the scale factor per sensor as well.
|
|
// currentScale.set_scale(calibration_factor);
|
|
// long weight_grams = currentScale.get_units(10); // Get average of 10 readings with scale factor
|
|
// outgoingData["grams"] = weight_grams;
|
|
*/
|
|
|
|
} else {
|
|
outgoingData["success"] = false;
|
|
outgoingData["error"] = "HX711 not ready or timeout";
|
|
}
|
|
} // Closing brace for case 3
|
|
break;
|
|
|
|
case 4: // Handle TARE message: Tare a specific HX711 sensor.
|
|
{ // Use a block to scope variables declared within the case.
|
|
// Get the data and clock pins for the HX711 from the incoming data.
|
|
const int LOADCELL_DATA_PIN = (int)data["pin_data"];
|
|
const int LOADCELL_CLOCK_PIN = (int)data["pin_clock"];
|
|
|
|
// Check if pins are valid.
|
|
if (LOADCELL_DATA_PIN < 0 || LOADCELL_CLOCK_PIN < 0) {
|
|
outgoingData["success"] = false;
|
|
outgoingData["error"] = "HX711 data and clock pins not provided or invalid";
|
|
break;
|
|
}
|
|
|
|
// Create a temporary HX711 object for this specific sensor.
|
|
HX711 currentScale;
|
|
currentScale.begin(LOADCELL_DATA_PIN, LOADCELL_CLOCK_PIN);
|
|
|
|
// Wait for the HX711 to be ready.
|
|
if (currentScale.wait_ready_timeout(1000)) { // Wait up to 1000ms
|
|
// Get the current raw value to use as the new tare offset.
|
|
long current_raw_value = currentScale.get_value();
|
|
|
|
// Store this value as the tare offset for these pins.
|
|
int index = addOrUpdateSensorTare(LOADCELL_DATA_PIN, LOADCELL_CLOCK_PIN, current_raw_value);
|
|
|
|
if (index != -1) {
|
|
outgoingData["message"] = "Sensor taried successfully";
|
|
// Optionally return the new tare offset or taried value (which should be close to 0)
|
|
// outgoingData["new_tare_offset"] = current_raw_value;
|
|
// outgoingData["taried_value"] = current_raw_value - current_raw_value; // Should be 0
|
|
} else {
|
|
outgoingData["success"] = false;
|
|
outgoingData["error"] = "Could not store tare offset, sensor array full";
|
|
}
|
|
|
|
} else {
|
|
outgoingData["success"] = false;
|
|
outgoingData["error"] = "HX711 not ready or timeout for tare";
|
|
}
|
|
} // Closing brace for case 4
|
|
break;
|
|
|
|
|
|
case 5: // Handle RESET message: Reset the Arduino.
|
|
outgoingData["message"] = "Resetting Arduino";
|
|
serializeJson(outgoingJson, Serial); // Send response before reset
|
|
Serial.println(); // Send newline
|
|
delay(100); // Small delay to ensure message is sent
|
|
resetFunc(); // Call the reset function.
|
|
break;
|
|
|
|
|
|
default: // Handle unknown message type.
|
|
outgoingData["success"] = false;
|
|
outgoingData["error"] = "Unknown message type";
|
|
break;
|
|
}
|
|
} else {
|
|
// Handle case where the 'data' field is missing or null.
|
|
outgoingData["success"] = false;
|
|
outgoingData["error"] = "Missing or null 'data' field in incoming JSON";
|
|
}
|
|
|
|
|
|
// Prepare the root of the outgoing JSON message.
|
|
// Set the 'id' to match the incoming message for correlation.
|
|
outgoingJson["id"] = id;
|
|
// Set the 'type' for the response (e.g., 0 for acknowledgment/response).
|
|
outgoingJson["type"] = 0;
|
|
// The 'data' field was already created as a nested object.
|
|
|
|
// Send the outgoing JSON message to the serial port.
|
|
serializeJson(outgoingJson, Serial);
|
|
// Send a newline character to indicate the end of the JSON message.
|
|
Serial.println();
|
|
|
|
// Clear the incoming JSON document to free up memory for the next message.
|
|
incomingJson.clear();
|
|
// Clear the outgoing JSON document.
|
|
outgoingJson.clear();
|
|
}
|
|
}
|
|
// Add a small delay to prevent the loop from running too fast when no data is available.
|
|
delay(1);
|
|
}
|