commit 208b6b51d96077d658058205ed18a7c85cf744cb
Author: Hunter David Halloran <hdh20267@uga.edu>
Date:   Tue Oct 1 11:09:39 2024 -0400

    jc mqtt

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..89cc49c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.pio
+.vscode/.browse.c_cpp.db*
+.vscode/c_cpp_properties.json
+.vscode/launch.json
+.vscode/ipch
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..080e70d
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,10 @@
+{
+    // See http://go.microsoft.com/fwlink/?LinkId=827846
+    // for the documentation about the extensions.json format
+    "recommendations": [
+        "platformio.platformio-ide"
+    ],
+    "unwantedRecommendations": [
+        "ms-vscode.cpptools-extension-pack"
+    ]
+}
diff --git a/include/README b/include/README
new file mode 100644
index 0000000..194dcd4
--- /dev/null
+++ b/include/README
@@ -0,0 +1,39 @@
+
+This directory is intended for project header files.
+
+A header file is a file containing C declarations and macro definitions
+to be shared between several project source files. You request the use of a
+header file in your project source file (C, C++, etc) located in `src` folder
+by including it, with the C preprocessing directive `#include'.
+
+```src/main.c
+
+#include "header.h"
+
+int main (void)
+{
+ ...
+}
+```
+
+Including a header file produces the same results as copying the header file
+into each source file that needs it. Such copying would be time-consuming
+and error-prone. With a header file, the related declarations appear
+in only one place. If they need to be changed, they can be changed in one
+place, and programs that include the header file will automatically use the
+new version when next recompiled. The header file eliminates the labor of
+finding and changing all the copies as well as the risk that a failure to
+find one copy will result in inconsistencies within a program.
+
+In C, the usual convention is to give header files names that end with `.h'.
+It is most portable to use only letters, digits, dashes, and underscores in
+header file names, and at most one dot.
+
+Read more about using header files in official GCC documentation:
+
+* Include Syntax
+* Include Operation
+* Once-Only Headers
+* Computed Includes
+
+https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
diff --git a/lib/README b/lib/README
new file mode 100644
index 0000000..6debab1
--- /dev/null
+++ b/lib/README
@@ -0,0 +1,46 @@
+
+This directory is intended for project specific (private) libraries.
+PlatformIO will compile them to static libraries and link into executable file.
+
+The source code of each library should be placed in a an own separate directory
+("lib/your_library_name/[here are source files]").
+
+For example, see a structure of the following two libraries `Foo` and `Bar`:
+
+|--lib
+|  |
+|  |--Bar
+|  |  |--docs
+|  |  |--examples
+|  |  |--src
+|  |     |- Bar.c
+|  |     |- Bar.h
+|  |  |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
+|  |
+|  |--Foo
+|  |  |- Foo.c
+|  |  |- Foo.h
+|  |
+|  |- README --> THIS FILE
+|
+|- platformio.ini
+|--src
+   |- main.c
+
+and a contents of `src/main.c`:
+```
+#include <Foo.h>
+#include <Bar.h>
+
+int main (void)
+{
+  ...
+}
+
+```
+
+PlatformIO Library Dependency Finder will find automatically dependent
+libraries scanning project source files.
+
+More information about PlatformIO Library Dependency Finder
+- https://docs.platformio.org/page/librarymanager/ldf.html
diff --git a/platformio.ini b/platformio.ini
new file mode 100644
index 0000000..21dd64b
--- /dev/null
+++ b/platformio.ini
@@ -0,0 +1,19 @@
+; PlatformIO Project Configuration File
+;
+;   Build options: build flags, source filter
+;   Upload options: custom upload port, speed and extra flags
+;   Library options: dependencies, extra library storages
+;   Advanced options: extra scripting
+;
+; Please visit documentation for the other options and examples
+; https://docs.platformio.org/page/projectconf.html
+
+[env:esp32]
+platform = espressif32
+board = esp32dev
+framework = arduino
+lib_deps = 
+    FastAccelStepper
+    PubSubClient
+    ArduinoJson
+
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..c2b1754
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,211 @@
+#include <Arduino.h>
+#include <FastAccelStepper.h>
+#include <WiFi.h>
+#include <PubSubClient.h>
+
+const uint8_t bottom_dir_pin = 5, top_dir_pin = 18;
+const uint8_t bottom_step_pin = 4, top_step_pin = 17;
+const uint8_t bottom_en_pin = 12, top_en_pin = 16;
+const uint8_t encoder_a = 19, encoder_b = 21;
+
+const char* ssid = "IOT-pecan";
+const char* password = "aaaaaaaa";
+const char* mqttServer = "192.168.1.110";
+const int mqttPort = 1883;
+const char* mqttHeightTopic = "/jc/height";
+const char* mqttAngleTopic = "/jc/angle";
+const char* mqttStopTopic = "/jc/stop";
+
+const char* mqttHeightLogTopic = "/jc/height/log";
+const char* mqttAngleLogTopic = "/jc/angle/log";
+const char* mqttRPMLogTopic = "/jc/rpm/log";
+
+// Thread pitch: 11 TPI (0.0909 in/rev)
+// Steps per revolution: 2000;
+const int steps_per_thou = 22;
+
+FastAccelStepperEngine engine = FastAccelStepperEngine();
+FastAccelStepper *bottom_stepper=nullptr;
+FastAccelStepper *top_stepper=nullptr;
+
+WiFiClient wifiClient;
+PubSubClient mqttClient(wifiClient);
+
+void callback(char* topic, byte* payload, unsigned int length);
+
+volatile int encoder_count_a = 0, encoder_count_b = 0;
+volatile int last_encoder_count_a = 0, last_encoder_count_b = 0;
+volatile double rotations_since_last = 0;
+volatile float encoder_rpm = 0;
+uint16_t ticks_per_rotation = 600;
+
+volatile long long bottom_step_goal = 0, top_step_goal = 0;
+volatile bool bottom_step_done = true, top_step_done = true;
+volatile bool bottom_step_sent = false, top_step_sent = false;
+volatile bool do_home = false;
+
+auto timer = timerBegin(0, 80, true);
+
+void setup() {
+  Serial.begin(9600);
+  engine.init();
+  bottom_stepper = engine.stepperConnectToPin(bottom_step_pin);
+  if (bottom_stepper) {
+    bottom_stepper->setDirectionPin(bottom_dir_pin);
+    bottom_stepper->setEnablePin(bottom_en_pin, false);
+    bottom_stepper->setSpeedInHz(1000);
+    bottom_stepper->setAcceleration(250);
+  }
+  top_stepper = engine.stepperConnectToPin(top_step_pin);
+  if (top_stepper) {
+    top_stepper->setDirectionPin(top_dir_pin);
+    top_stepper->setEnablePin(top_en_pin, false);
+    top_stepper->setSpeedInHz(1000);
+    top_stepper->setAcceleration(250);
+  }
+
+  pinMode(encoder_a, INPUT_PULLUP);
+  pinMode(encoder_b, INPUT_PULLUP);
+  attachInterrupt(encoder_a, []() {
+    encoder_count_a++;
+    double rotations = 0;
+    if(encoder_count_a > ticks_per_rotation / 100) {
+      rotations = (double)encoder_count_a / ticks_per_rotation;
+      rotations_since_last += rotations / 2;
+      encoder_count_a -= ticks_per_rotation * rotations;
+    }
+  }, FALLING);
+
+  attachInterrupt(encoder_b, []() {
+    encoder_count_b++;
+    double rotations = 0;
+    if(encoder_count_b > ticks_per_rotation / 100) {
+      rotations = (double)encoder_count_b / ticks_per_rotation;
+      rotations_since_last += rotations / 2;
+      encoder_count_b -= ticks_per_rotation * rotations;
+    }
+  }, FALLING);
+
+  WiFi.begin(ssid, password);
+  while (WiFi.status() != WL_CONNECTED) {
+    delay(1000);
+    Serial.println("Connecting to WiFi...");
+  }
+  Serial.println("Connected to WiFi");
+
+  mqttClient.setServer(mqttServer, mqttPort);
+  mqttClient.setCallback(callback);
+
+  Serial.println("Connected to MQTT");
+}
+
+void reconnect() {
+  while (!mqttClient.connected()) {
+    Serial.println("Attempting MQTT connection...");
+    if (mqttClient.connect("ESP32Client")) {
+      Serial.println("connected");
+      mqttClient.subscribe(mqttHeightTopic);
+      mqttClient.subscribe(mqttAngleTopic);
+    } else {
+      Serial.print("failed, rc=");
+      Serial.print(mqttClient.state());
+      Serial.println(" try again in 5 seconds");
+      delay(5000);
+    }
+  }
+}
+
+volatile int top_angle_difference_steps = steps_per_thou * 188; 
+volatile bool stop = false;
+
+void callback(char* topic, byte* payload, unsigned int length) {
+  String topic_str = String(topic);
+  String payload_str = "";
+  for (int i = 0; i < length; i++) {
+    payload_str += (char)payload[i];
+  }
+
+  int value = payload_str.toInt();
+  
+  if (topic_str == mqttHeightTopic) {
+    value -= 1875 - 210;
+    value *= steps_per_thou;
+    Serial.println(value);
+    stop = false;
+    bottom_step_goal = value;
+    bottom_step_done = false;
+    bottom_step_sent = false;
+    top_step_goal = value + top_angle_difference_steps;
+    top_step_done = false;
+    top_step_sent = false;
+  } else if (topic_str == mqttAngleTopic) {
+    stop = false;
+    top_angle_difference_steps = value * steps_per_thou;
+    top_step_goal = bottom_step_goal + top_angle_difference_steps;
+    top_step_done = false;
+    top_step_sent = false;
+  } else if (topic_str == mqttStopTopic) {
+    stop = true;
+  } 
+  if (top_step_goal < -1665 * steps_per_thou) top_step_goal = -1665 * steps_per_thou;
+  if (bottom_step_goal < -1665 * steps_per_thou) bottom_step_goal = -1665 * steps_per_thou;
+
+  if(top_step_goal > 0) top_step_goal = 0;
+  if(bottom_step_goal > 0) bottom_step_goal = 0;
+}
+
+long last_publish_time = 0;
+bool bot_homed = false, top_homed = false;
+void loop() {
+  if (!mqttClient.connected()) {
+    reconnect();
+  }
+  mqttClient.loop();
+
+  if (unsigned long elapsed = (millis() - last_publish_time) > 1000) {
+    encoder_rpm = (float)rotations_since_last * 60 / elapsed;
+    
+    // clamp encoder rpm to 0 if below 50
+    if (encoder_rpm < 50) {
+      encoder_rpm = 0;
+    }
+
+    rotations_since_last = 0;
+    last_publish_time = millis();
+    mqttClient.publish(mqttHeightLogTopic, String(1665 + bottom_stepper->getCurrentPosition() / steps_per_thou).c_str());
+    mqttClient.publish(mqttAngleLogTopic, String((top_stepper->getCurrentPosition() - bottom_stepper->getCurrentPosition()) / steps_per_thou).c_str());
+    mqttClient.publish(mqttRPMLogTopic, String(encoder_rpm).c_str());
+  }
+
+  if(stop) {
+    bottom_stepper->disableOutputs();
+    top_stepper->disableOutputs();
+  } else {
+    bottom_stepper->enableOutputs();
+    top_stepper->enableOutputs();
+  }
+
+  if(bottom_stepper->getCurrentPosition() != bottom_step_goal && !bottom_step_done && !bottom_step_sent) {
+    bottom_stepper->moveTo(bottom_step_goal);
+    bottom_step_sent = true;
+    Serial.println("Moving bottom stepper");
+  }
+
+  if(top_stepper->getCurrentPosition() != top_step_goal && !top_step_done && !top_step_sent) {
+    top_stepper->moveTo(top_step_goal);
+    top_step_sent = true;
+    Serial.println("Moving top stepper");
+  }
+
+  if(bottom_stepper->getCurrentPosition() == bottom_step_goal && !bottom_step_done && bottom_step_sent) {
+    bottom_step_done = true;
+    bottom_step_sent = false;
+    Serial.println("Bottom stepper done");
+  }
+
+  if(top_stepper->getCurrentPosition() == top_step_goal && !top_step_done && top_step_sent) {
+    top_step_done = true;
+    top_step_sent = false;
+    Serial.println("Top stepper done");
+  }
+}
diff --git a/test/README b/test/README
new file mode 100644
index 0000000..9b1e87b
--- /dev/null
+++ b/test/README
@@ -0,0 +1,11 @@
+
+This directory is intended for PlatformIO Test Runner and project tests.
+
+Unit Testing is a software testing method by which individual units of
+source code, sets of one or more MCU program modules together with associated
+control data, usage procedures, and operating procedures, are tested to
+determine whether they are fit for use. Unit testing finds problems early
+in the development cycle.
+
+More information about PlatformIO Unit Testing:
+- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html