This is a split flap display project which I didn't write, I built and upload the code on a esp32 board using platformio. whenever I power the ESP32 from PSU it will run the motors but it won't trigger the hall sensors. When I upload the ESP32 to the PC it works correctly.
Config.h
#pragma once
#define WIFI_SSID "xxxxxx"
#define WIFI_PWD "xxxxx"
#define MY_NTP_SERVER "au.pool.ntp.org" // Set the best fitting NTP server (pool) for your location
#define MY_TZ "AEST-10AEDT,M10.1.0,M4.1.0/3" // Set your time zone from https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
#define WORDNIKAPIKEY "" // Insert your private key from https://developer.wordnik.com/
#define MINWORDLEN 9 // Specify minimum word length to fetch from Wordnik
#define WORDUPDATESPERHOUR 0 // Set number of word updates from https://wordnik.com per hour. 0 will disable, else use an integer that results in an exact number of minutes btw updates eg. 1,2,3,4,5,6,10,12...
debug.h
#pragma once
// Set debug to 1 to allow debugging info on serial monitor
#define DEBUG 1
#if DEBUG == 1
#define debug(x) Serial.print(x)
#define debugln(x) Serial.println(x)
#define debugf(...) Serial.printf(__VA_ARGS__)
#define TXT_RST "\e[0m"
#define TXT_BLUE "\e[0;34m"
#define TXT_YELLOW "\e[0;33m"
#define TXT_RED "\e[1;31m"
#define TXT_GREEN "\e[0;32m"
#else
#define debug(x)
#define debugln(x)
#define debugf(...)
#endif
system.h
#pragma once
#include <Arduino.h>
#include <ArduinoJson.h>
#include <WebServer.h>
#include "debug.h"
#if __has_include(<config-private.h>)
#include "config-private.h"
#else
#include "config.h"
#endif
#include <WiFiClientSecure.h>
#include <ESPmDNS.h>
// Specify number of Units (characters) in the display (4 - 12)
#define UNITCOUNT 12
// Specify the network name of the display (useful if you have more than one display)
#define NETWORKNAME "splitflap"
#define STR_HELPER(x) #x
#define STR(x) STR_HELPER(x)
#define WORDNIKURL "https://api.wordnik.com/v4/words.json/randomWord?hasDictionaryDef=true&excludePartOfSpeech=family-name%2Cgiven-name%2Cproper-noun%2Cproper-noun-plural&minCorpusCount=100&maxCorpusCount=-1&minDictionaryCount=1&maxDictionaryCount=-1&minLength=" STR(MINWORDLEN) "&maxLength=" STR(UNITCOUNT) "&api_key=" WORDNIKAPIKEY
#define NTP_MIN_VALID_EPOCH 1577836800 //2020-1-1
#define RTC_MAGIC 0x76b78ec4
void disableCertificates();
boolean synchroniseWith_NTP_Time(time_t &now, tm &timeinfo);
boolean getNTP(time_t &now, tm &timeinfo);
String wordOfTheDay();
void setup_routing();
void sendwebpage();
void receiveAPI();
void receiveInput();
void randomWord ();
void handle_NotFound();
String padToFullWidth (const char* word);
extern void displayString(String display);
unit.h
#pragma once
#include <Arduino.h>
#include "FastAccelStepper.h"
#include "debug.h"
// Customise below for each unit for your build. (Units are numbered left to right 0 - 11)
const uint8_t calOffsetUnit[] = {87, 62, 77, 65, 89, 104, 107, 82, 95, 97, 90, 55};
const float FlapStep[] = {2038.0/45, 2038.0/45, 2038.0/45, 2050.0/45, 2049.0/45, 2049.0/45, 2049.0/45, 2038.0/45, 2051.0/45, 2051.2/45, 2049.0/45, 2038.0/45}; // stepper motor steps per rotation per flap, for each unit motor
// The following are hardware related and won't change unless PCB is changed. Note: Units are numbered from left to right 0 - 11.
const uint8_t unitStepPin[] = {14,13,5,4,18,17,16,15,26,25,23,19};
const uint8_t UnitEnablePin[] = {3,2,1,0,7,6,5,4,11,10,9,8};
const char sensorPort[] = {'A','A','A','A','B','B','A','A','B','B','B','B'}; //Maps unit sensors 0 - 11 to the MCP23017 ports A or B
const uint8_t sensorPortBit[] = {0b00001000,0b00000100,0b00000010,0b00000001,0b00000010,0b00000001,0b00100000,0b00010000,0b00100000,0b00010000,0b00001000,0b00000100}; //Maps unit sensors 0 - 11 to the bit of the MCP23017 port
const uint8_t interruptPin = 27;
const uint8_t button1Pin = 14;
const uint8_t button2Pin = 6;
const char letters[] = {' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '$', '&', '#', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', '.', '-', '?', '!'};
const uint16_t rotationSpeeduS = 2000;
////////////////////////////////////////
// For JTAG debugging, cannot use pins 13,14,15, so free these up when debugging
// const uint8_t UNITCOUNT = 9;
// const uint8_t unitStepPin[] = {5,4,18,17,16,26,25,23,19};
// const uint8_t UnitEnablePin[] = {1,0,7,6,5,11,10,9,8};
// const char sensorPort[] = {'A','A','B','B','A','B','B','B','B'}; //Maps unit sensors 0 - 11 to the MCP23017 ports A or B
// const uint8_t sensorPortBit[] = {0b00000010,0b00000001,0b00000010,0b00000001,0b00100000,0b00100000,0b00010000,0b00001000,0b00000100}; //Maps unit sensors 0 - 11 to the bit of the above MCP23017 port
// const uint8_t calOffsetUnit[] = {77, 65, 89, 104, 107, 95, 97, 90, 85};
////////////////////////////////////////
class Unit {
public:
boolean calibrationStarted;
boolean calibrationComplete;
char pendingLetter;
uint8_t destinationLetter;
// Constructor for each Unit object
Unit(FastAccelStepperEngine& engine, uint8_t unitNum);
void moveStepperbyStep(int16_t steps);
void moveSteppertoLetter(char toLetter);
void moveStepperbyFlap(uint16_t flaps);
void calibrateStart();
int8_t calibrate();
boolean checkIfRunning();
boolean updateHallValue(uint8_t updatedHallValue);
private:
FastAccelStepper* stepper;
float missedSteps;
uint8_t unitNum;
bool preInitialise;
uint8_t currentLetterPosition;
uint32_t calibrationStartTime;
uint8_t currentHallValue;
uint32_t lastHallUpdateTime;
int16_t stepsToRotate (float steps);
uint16_t stepsToRotateFlaps(uint16_t flaps);
uint8_t flapsToRotateToLetter(char letterchar, boolean *recalibrate);
uint8_t translateLettertoInt(char letterchar);
};
main.cpp
/* Firmware for Mechanical Split-Flap Display
*
* This is the Arduino firmware for esp32Core_board_v2 (ESP32 DevKitC)
*
* Malcolm Yeoman (2024)
*
* Blog post: https://tinkerwithtech.net/split-flap-display-brings-back-memories
* Github:
*
* Note1: Implemented so that on the MCP23017 GPA7 and GPB7 are not used as inputs
* https://microchipsupport.force.com/s/article/GPA7---GPB7-Cannot-Be-Used-as-Inputs-In-MCP23017
*
*/
#include <Arduino.h>
#include <Wire.h>
#include <MCP23017.h>
#include <time.h>
#include "system.h"
#include "unit.h"
#if __has_include(<config-private.h>)
#include "config-private.h"
#else
#include "config.h"
#endif
// Function headers
void print_test_menu ();
bool setExternalPin(uint8_t pin, uint8_t value);
// boolean calibrate_all_units();
void recalibrate_units();
void IRAM_ATTR sensor_ISR();
void updateHallSensors();
void displayString(String display);
boolean diplayStillMoving();
// void intToBinary(int num, char* binaryStr);
// void debugUnitFlags(String prefix);
// Global vars
uint32_t previousMillis = 0;
uint32_t displayLastStoppedMillis;
uint32_t nextWordAPIMillis = 0;
MCP23017 mcp_en_steppers = MCP23017(0x20);
MCP23017 mcp_sensor = MCP23017(0x21);
Unit *splitFlap[UNITCOUNT];
volatile bool sensortriggered = false;
uint16_t counter = 0;
uint8_t active_menu_unit = 0;
boolean getting_first_word = true;
const char* ssid = WIFI_SSID;
const char* password = WIFI_PWD;
time_t now; // this is the epoch
tm timeinfo; // structure tm holds time information in a more convenient way
String test_command_previous = "";
uint8_t charSeq = 40;
char save_display[13];
char previous_display[13];
uint8_t reboot_count;
String localIP;
FastAccelStepperEngine engine;
extern WebServer server;
uint8_t word_updates_per_hour = WORDUPDATESPERHOUR; //store config value in variable to prevent div by zero compiler warnings
// RTC memory structure - for persisting data between reboots
typedef struct {
uint32_t magic;
char previous_display[13];
uint8_t reboot_count;
} RTC;
RTC_NOINIT_ATTR RTC nvmem;
// SETUP
void setup() {
#if DEBUG == 1
Serial.begin(115200);
#endif
WiFi.begin(ssid, password);
debugln(TXT_BLUE "Starting" TXT_RST);
// Future expansion possibility: Set up external I2C to chain multiple controller boards
// Wire2.begin(32,33); //(SDA=32, SCL=33);
// Configure I2C for MCP23017 port expanders
Wire.begin(21, 22, 800000); // SDA=21, SCL=22, 800kHz
mcp_en_steppers.init();
mcp_en_steppers.portMode(MCP23017Port::A, 0); //Port A as output
mcp_en_steppers.portMode(MCP23017Port::B, 0); //Port B as output
mcp_en_steppers.writeRegister(MCP23017Register::GPIO_A, 0x00); //Reset port A
mcp_en_steppers.writeRegister(MCP23017Register::GPIO_B, 0x00); //Reset port B
mcp_sensor.init();
mcp_sensor.portMode(MCP23017Port::A, 0b01111111); //Port A 7 bits as input
mcp_sensor.portMode(MCP23017Port::B, 0b01111111); //Port B 7 bits as input
mcp_sensor.writeRegister(MCP23017Register::IPOL_A, 0x00);
mcp_sensor.writeRegister(MCP23017Register::IPOL_B, 0x00);
mcp_sensor.writeRegister(MCP23017Register::GPIO_A, 0xFF);
mcp_sensor.writeRegister(MCP23017Register::GPIO_B, 0xFF);
// Set up fast stepper engine
engine = FastAccelStepperEngine();
engine.init();
engine.setExternalCallForPin(setExternalPin);
// Initialise split-flap display units
for (uint8_t unit = 0; unit < UNITCOUNT; unit++) {
splitFlap[unit] = new Unit(engine, unit);
}
// Enable Sensor interrupts
mcp_sensor.interruptMode(MCP23017InterruptMode::Or); //Both ports logically ORed to same interrupt pin
mcp_sensor.interrupt(MCP23017Port::A, CHANGE);
mcp_sensor.interrupt(MCP23017Port::B, CHANGE);
mcp_sensor.clearInterrupts();
pinMode(interruptPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(interruptPin), sensor_ISR, FALLING);
// attempt to connect to Wifi network
debug("Attempting to connect to SSID: ");
debugln(ssid);
while (WiFi.status() != WL_CONNECTED) {
debug(".");
// wait 1 second between retries
delay(1000);
}
debug("\nConnected to " TXT_BLUE);
debugln(ssid);
localIP = WiFi.localIP().toString();
debug(localIP);
debugln(TXT_RST);
disableCertificates();
// Lookup NTP time
configTzTime(MY_TZ, MY_NTP_SERVER);
now = time(nullptr); //start time sync in background
// Set up REST API
setup_routing();
// Read hall sensors to get current values
updateHallSensors();
// Read any previous display before last reboot
if (nvmem.magic == RTC_MAGIC) {
strncpy(previous_display, nvmem.previous_display, 13);
previous_display[12]='\0';
reboot_count = nvmem.reboot_count;
debugf(TXT_YELLOW "previous_display: [%s], reboots: %d\n" TXT_RST, previous_display, reboot_count);
}
else {
previous_display[0] = '\0';
reboot_count = 0;
}
delay(101); // Delay to avoid initially false triggering the glitch detection
// Try up to 3 times to get NTP
uint8_t getntpcount = 0;
while (getntpcount++ < 3 && !getNTP(now, timeinfo)) {
delay(1000);
debugln("Retrying NTP...");
}
displayLastStoppedMillis = 0;
#if DEBUG == 1
print_test_menu();
#endif
}
void loop() {
String test_command;
uint16_t test_num;
////////////////////////
// HIGH LEVEL EVENT LOOP
////////////////////////
// Read hall sensors if change detected via interrupt
if (sensortriggered) {
sensortriggered = false;
updateHallSensors();
}
// Calibrate any units that require it
recalibrate_units();
// Move any units pending due to drum passing origin (after they have recalibrated)
for (uint8_t unit = 0; unit < UNITCOUNT; unit++) {
if (splitFlap[unit]->pendingLetter > 0 && splitFlap[unit]->calibrationComplete) {
splitFlap[unit]->moveSteppertoLetter(splitFlap[unit]->pendingLetter);
}
}
// Lower priority events below
if ((uint32_t)millis() - previousMillis >= 500) {
previousMillis = millis();
// Rest API server
server.handleClient();
//If display not moving, check if anything new to display
if (!diplayStillMoving()) {
displayLastStoppedMillis = millis();
// Check if need to redisplay after reboot
if (previous_display[0] != '\0') {
// don't allow more than 3 reboots per period - to avoid continous running in the event of fault
if (reboot_count++ > 3) {
debugln(TXT_RED "HALTING due to reboot count." TXT_RST);
while (true);
}
debugf("Display previous string: [%s], reboots: %d\n", previous_display, reboot_count);
displayString(String(previous_display));
previous_display[0] = '\0';
getting_first_word = false;
}
else if (getting_first_word) {
if (word_updates_per_hour > 0) {
String word = wordOfTheDay();
displayLastStoppedMillis = millis();
debugf("Word, %02d:%02d, [%s]\n", timeinfo.tm_hour, timeinfo.tm_min, word);
displayString(word);
nextWordAPIMillis = millis() + 60000; //dont check again until this minute passed
}
getting_first_word = false;
}
// Display random word according to WORDUPDATESPERHOUR
else if (word_updates_per_hour > 0 && ((uint32_t)millis() > nextWordAPIMillis)) {
// Only update during daytime hours
if (getNTP(now, timeinfo)) {
if ((timeinfo.tm_min % (60 / word_updates_per_hour) == 0) && (timeinfo.tm_hour >= 8 && timeinfo.tm_hour <= 19)) {
reboot_count = 0;
nextWordAPIMillis = (millis() + (3600 / word_updates_per_hour) * 1000) - 3000; //dont check again until nearly next word update time
String word = wordOfTheDay();
displayLastStoppedMillis = millis();
debugf("Word, %02d:%02d, [%s]\n", timeinfo.tm_hour, timeinfo.tm_min, word);
displayString(word);
// For testing only (changes all characters and requires a drum rotation + calibration each time)
// nextWordAPIMillis = (millis() + (3600 / word_updates_per_hour) * 1000) - 3000; //dont check again until nearly next word update time
// String thisSeq = "@@@@@@@@@@@@";
// charSeq--;
// if (charSeq == 0) {
// charSeq = 39;
// }
// thisSeq.replace("@",String(letters[charSeq]));
// displayString(thisSeq);
}
}
}
// Handle Button Press Code Here
// else if (digitalRead(button1Pin) == 0) {
// do something;
// }
}
//If display has been moving for more than 20 seconds, must be an error condition
else if (millis() - displayLastStoppedMillis > 20000) {
debugln(TXT_RED "Moving > 20 secs - RESTARTING!!!" TXT_RST);
nvmem.magic = RTC_MAGIC;
strncpy(nvmem.previous_display,save_display,13);
nvmem.reboot_count = reboot_count;
ESP.restart();
}
// Handle interactive serial commands over USB (used for debugging)
#if DEBUG == 1
if (Serial.available()) {
test_command = Serial.readStringUntil('\n');
test_command.replace("\r","");
test_command.toUpperCase();
// if only return was pressed then repeat last command
if (test_command.length() == 0) {
test_command = test_command_previous;
}
if (test_command.charAt(0) == char(92)) { //backslash
print_test_menu();
}
else if (test_command.charAt(0) == ']') {
test_num = test_command.substring(1,3).toInt();
if (test_num >= 0) {
active_menu_unit = test_num;
debugf("Active unit set to %d\n", active_menu_unit);
}
}
else if (test_command.charAt(0) == '>') {
test_num = test_command.substring(1,5).toInt();
if (test_num > 0) {
debugf("Move %d flaps\n", test_num);
splitFlap[active_menu_unit]->moveStepperbyFlap(test_num);
}
}
else if (test_command.charAt(0) == '}') {
test_num = test_command.substring(1,5).toInt();
if (test_num > 0) {
debugf("Move all flaps by %d\n", test_num);
for (uint8_t unit = 0; unit < UNITCOUNT; unit++) {
splitFlap[unit]->moveStepperbyFlap(test_num);
}
}
}
// Move stepper by a raw number of steps
else if (test_command.charAt(0) == '~') {
test_num = test_command.substring(1,5).toInt();
if (test_num > 0) {
debugf("Move %d steps\n", test_num);
splitFlap[active_menu_unit]->moveStepperbyStep(test_num);
}
}
else if (test_command.charAt(0) == '|') {
ESP.restart();
}
else if (test_command.charAt(0) == '%') {
String word = wordOfTheDay();
debugf("Word, %02d:%02d, [%s]\n", timeinfo.tm_hour, timeinfo.tm_min, word);
displayString(word);
}
else if (test_command.charAt(0) == '+') {
String thisSeq = "@@@@@@@@@@@@";
// String thisSeq = " @ ";
charSeq--;
if (charSeq == 0) {
charSeq = 39;
}
thisSeq.replace("@",String(letters[charSeq]));
displayString(thisSeq);
}
else if (test_command.charAt(0) == '<') {
debugln("Put ESP to sleep until power reset");
esp_deep_sleep_start();
}
else {
test_command.toUpperCase();
debugf("Display %s\n", test_command);
displayString(test_command);
}
test_command_previous = test_command;
}
#endif
}
}
void print_test_menu() {
getNTP(now, timeinfo);
debugln(TXT_GREEN "----------------------------------");
debugf (" %s", asctime(&timeinfo));
debugln("Enter a command followed by return");
debugln("----------------------------------");
debugln("\\ : Show this menu");
debugln("} : Move ALL forward number of flaps");
debugf ("] : Set active unit for this menu [%d]\n", active_menu_unit);
debugln("> : Move forward number of flaps");
debugln("~ : Move forward number steps");
debugln("| : Reset Display");
debugln("% : Display a random word");
debugln("+ : Test: countdown of all flaps");
debugln("< : Idle");
debugln("any : display given text");
debugln("----------------------------------" TXT_RST);
}
// Callback routine that actions the enable on or off triggered by setAutoEnable
bool setExternalPin(uint8_t pin, uint8_t value) {
pin = pin & ~PIN_EXTERNAL_FLAG;
// When using SLEEP instead of /ENABLE on the A4988 to save idle power, need to invert
// debugf("mcp en pin %d set to %d\n", pin, value ^ 0x01);
mcp_en_steppers.digitalWrite(pin, value ^ 0x01);
return value;
}
void recalibrate_units() {
uint8_t unitsCalibrating;
int8_t calibrationResult;
unitsCalibrating = 0;
for (uint8_t unit = 0; unit < UNITCOUNT; unit++) {
if (!splitFlap[unit]->calibrationComplete) {
if (!splitFlap[unit]->calibrationStarted) {
splitFlap[unit]->calibrateStart();
unitsCalibrating++;
}
else {
calibrationResult = splitFlap[unit]->calibrate();
if (calibrationResult == 0) {
unitsCalibrating++;
}
else if (calibrationResult < 0) {
debugf("Calibration failed for unit %d\n", unit);
debugln(TXT_RED "RESTARTING!!!" TXT_RST);
nvmem.magic = RTC_MAGIC;
strncpy(nvmem.previous_display,save_display,13);
nvmem.reboot_count = reboot_count;
ESP.restart();
}
}
}
}
}
void IRAM_ATTR sensor_ISR() {
sensortriggered = true;
}
void updateHallSensors() {
uint8_t newvalue;
mcp_sensor.clearInterrupts();
uint8_t sensor_port_current_a = mcp_sensor.readPort(MCP23017Port::A);
uint8_t sensor_port_current_b = mcp_sensor.readPort(MCP23017Port::B);
for (uint8_t unit = 0; unit < UNITCOUNT; unit++) {
if (sensorPort[unit] == 'A') {
newvalue = ((~sensor_port_current_a & sensorPortBit[unit]) == 0);
if (!splitFlap[unit]->updateHallValue(newvalue)) {
splitFlap[unit]->moveStepperbyFlap(1);
splitFlap[unit]->calibrationComplete = false;
splitFlap[unit]->calibrationStarted = false;
splitFlap[unit]->pendingLetter = splitFlap[unit]->destinationLetter;
}
}
else {
newvalue = ((~sensor_port_current_b & sensorPortBit[unit]) == 0);
if (!splitFlap[unit]->updateHallValue(newvalue)) {
splitFlap[unit]->moveStepperbyFlap(1);
splitFlap[unit]->calibrationComplete = false;
splitFlap[unit]->calibrationStarted = false;
splitFlap[unit]->pendingLetter = splitFlap[unit]->destinationLetter;
}
}
}
}
void displayString(String display) {
uint8_t test_length;
char display_char;
display.toUpperCase();
strncpy(save_display, display.c_str(), 13); // save display in case of reboot
test_length = display.length();
if (test_length > UNITCOUNT) {
test_length = UNITCOUNT;
}
for (uint8_t char_pos = 0; char_pos < test_length; char_pos++) {
display_char = display.charAt(char_pos);
splitFlap[char_pos]->moveSteppertoLetter(display_char);
}
}
boolean diplayStillMoving () {
boolean display_busy = true;
display_busy = false;
for (uint8_t unit = 0; unit < UNITCOUNT; unit++) {
if (splitFlap[unit]->checkIfRunning()) {
display_busy = true;
}
}
return display_busy;
}
// void intToBinary(int num, char* binaryStr) {
// for (int i = 7; i >= 0; i--) {
// int bit = (num >> i) & 1;
// binaryStr[7 - i] = bit + '0'; // Convert the bit to '0' or '1'
// }
// binaryStr[8] = '\0'; // Null-terminate the string
// }
// void debugUnitFlags(String prefix) {
// for (uint8_t unit = 0; unit < UNITCOUNT; unit++) {
// debugf("%s,%02d,%c,%d,%d\n", prefix, unit, (splitFlap[unit]->pendingLetter == 0) ? 95 : splitFlap[unit]->pendingLetter, splitFlap[unit]->calibrationStarted, splitFlap[unit]->calibrationComplete);
// }
// }
system.cpp
#include "system.h"
const char* word_server = "api.wordnik.com"; // word server
WiFiClientSecure client;
WebServer server(80);
// HTML web page to handle input of text to display
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html><head>
<title>Split-Flap Display</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head><body>
<form action="receiveInput" method="POST">
<p style="font-weight: bold; margin-bottom:12px;">Split-Flap Display</p>
<input type="text" id="displaytext" name="displaytext">
<input type="submit" value="Update" id="submitbtn" onclick="this.hidden=true; document.getElementById('submitrnd').hidden = true"><br/><br/>
</form>
<form action="randomWord" method="POST">
<input type="submit" value="Random Word" id="submitrnd" onclick="document.getElementById('displaytext').value = '***RANDOM***'; this.hidden=true; document.getElementById('submitbtn').hidden = true">
</form>
</body></html>)rawliteral";
void disableCertificates() {
client.setInsecure();
}
boolean synchroniseWith_NTP_Time(time_t &now, tm &timeinfo){
uint8_t timeout = 0;
// debug("Setting time using SNTP");
while (now < NTP_MIN_VALID_EPOCH && ++timeout < 50) {
delay(100);
// debug(".");
now = time(nullptr);
}
if (timeout >= 50) {
return false;
}
localtime_r(&now, &timeinfo); // update the structure tm (timeinfo) with the current time
return true;
}
boolean getNTP(time_t &now, tm &timeinfo) {
// If cannot get NTP time, return error
if (!synchroniseWith_NTP_Time(now, timeinfo)) {
debugln("Error getting time");
return false;
}
else {
time(&now); // read the current time
localtime_r(&now, &timeinfo); // update the structure tm with the local current time
}
return true;
}
String wordOfTheDay() {
JsonDocument jsonBufferData;
String json_word;
if (WORDNIKAPIKEY != "") {
// debugln("\nStarting connection to word server...");
if (!client.connect(word_server, 443)) {
debugln("Connection failed!");
return "";
}
else {
// debugln("Connected to word_server!");
// Make a HTTP request:
client.println("GET " WORDNIKURL " HTTP/1.0");
client.println("Host: api.wordnik.com");
client.println("Connection: close");
client.println();
while (client.connected()) {
String line = client.readStringUntil('\n');
if (line == "\r") {
// debugln("headers received");
break;
}
}
// if there are incoming bytes available
// from the word_server, read them and print them:
while (client.available()) {
char c = client.read();
json_word += c;
}
client.stop();
}
//Parse JSON to get word
DeserializationError jsonError = deserializeJson(jsonBufferData, json_word);
// Test if parsing succeeds
if (jsonError) {
debug(F("deserializeJson() failed: "));
debugln(jsonError.f_str());
return "";
}
const char* word = jsonBufferData["word"];
return padToFullWidth(word);
}
else {
debugln("WORDNIKAPIKEY not specified in config.h");
return("");
}
}
void setup_routing() {
// Send web page with input fields to client
server.on("/", HTTP_GET, sendwebpage);
server.on("/display", HTTP_POST, receiveAPI);
server.on("/receiveInput", HTTP_POST, receiveInput);
server.on("/randomWord", HTTP_POST, randomWord);
server.onNotFound(handle_NotFound);
server.begin();
// Setup mDNS to allow connection via hostname
if (!MDNS.begin(NETWORKNAME)) { // Set the hostname NETWORKNAME defined in system.h: by default "splitflap.local"
debugln("Error setting up MDNS responder!");
}
MDNS.addService("http", "tcp", 80);
MDNS.addServiceTxt("http", "tcp", NETWORKNAME, "1");
}
void sendwebpage() {
server.send(200, "text/html", index_html);
}
void receiveAPI() {
JsonDocument jsonBufferData;
const char* displaytext;
String body = server.arg("plain");
DeserializationError jsonError = deserializeJson(jsonBufferData, body);
// Test if parsing succeeds
if (jsonError) {
debug(F("deserializeJson() failed: "));
debugln(jsonError.f_str());
return;
}
displaytext = jsonBufferData["displaytext"];
debugf("Text to display from API: %s\n", padToFullWidth (displaytext));
displayString(padToFullWidth (displaytext));
server.send(200, "application/json", "{}");
}
void receiveInput() {
JsonDocument jsonBufferData;
const char* displaytext;
String inputText = server.arg("displaytext");
displaytext = inputText.c_str();
// server.send(200, "text/plain", "{}");
server.sendHeader("Location", "/",true);
server.send(302, "text/plain", "");
delay(500);
displayString(padToFullWidth (displaytext));
}
void randomWord () {
String word = wordOfTheDay();
debugf("Word, [%s]\n", word);
server.sendHeader("Location", "/",true);
server.send(302, "text/plain", "");
delay(500);
displayString(word);
}
void handle_NotFound() {
server.send(404, "text/plain", "Not found");
}
String padToFullWidth (const char* word) {
String word_fullwidth = word;
if (word_fullwidth.length() <= UNITCOUNT - 2) {
word_fullwidth = " " + word_fullwidth;
}
while (word_fullwidth.length() < UNITCOUNT) {
word_fullwidth += " ";
}
return word_fullwidth;
}
unit.cpp
#include "unit.h"
Unit::Unit(FastAccelStepperEngine& engine, uint8_t unit) {
unitNum = unit;
stepper = engine.stepperConnectToPin(unitStepPin[unitNum]);
stepper->setEnablePin(UnitEnablePin[unitNum] | PIN_EXTERNAL_FLAG);
stepper->setAutoEnable(true);
stepper->setDelayToEnable(1000); // microseconds
stepper->setDelayToDisable(2); // milliseconds
// debugf("Unit %d Step pin set to %d\n", unitNum, unitStepPin[unitNum]);
stepper->setSpeedInUs(rotationSpeeduS); // the parameter is us/step
stepper->setAcceleration(4000);
missedSteps = 0;
currentLetterPosition = 0;
destinationLetter = 0;
pendingLetter = 0;
calibrationStarted = false;
calibrationComplete = false;
currentHallValue = 1;
lastHallUpdateTime = 0;
}
// calc number of steps to rotate based on cumulative step error
int16_t Unit::stepsToRotate (float steps) {
int16_t roundedStep = (int16_t)steps;
missedSteps = missedSteps + ((float)steps - (float)roundedStep);
if (missedSteps > 1) {
roundedStep = roundedStep + 1;
missedSteps--;
}
return roundedStep;
}
// translates char to letter position
uint8_t Unit::translateLettertoInt(char letterchar) {
for (int i = 0; i < 45; i++) {
if (letterchar == letters[i]) {
return i;
}
}
return 0;
}
// calc flaps to rotate to get to a specified letter
uint8_t Unit::flapsToRotateToLetter(char letterchar, boolean *recalibrate) {
int8_t newLetterPosition;
int8_t deltaFlapPosition;
newLetterPosition = translateLettertoInt(letterchar);
deltaFlapPosition = newLetterPosition - currentLetterPosition;
if (deltaFlapPosition < 0) {
deltaFlapPosition = 45 + deltaFlapPosition;
*recalibrate = true;
}
else {
currentLetterPosition = newLetterPosition;
}
return deltaFlapPosition;
}
// calc steps to rotate forward a specified number of flaps
uint16_t Unit::stepsToRotateFlaps(uint16_t flaps) {
float preciseStep = (float)flaps * FlapStep[unitNum];
return stepsToRotate (preciseStep);
}
// only for testing: Move stepper by a raw number of steps
void Unit::moveStepperbyStep(int16_t steps) {
stepper->move(steps);
}
void Unit::moveStepperbyFlap(uint16_t flaps) {
stepper->move(stepsToRotateFlaps(flaps));
}
void Unit::moveSteppertoLetter(char toLetter) {
boolean recalibrate = false;
destinationLetter = toLetter;
uint8_t flapsToMove = flapsToRotateToLetter(toLetter, &recalibrate);
debugf("Unit %02d flapsToMove %d\n", unitNum, flapsToMove);
if (recalibrate) {
calibrationComplete = false;
pendingLetter = toLetter;
debugf("Unit %02d pendingLetter '%c'\n", unitNum, pendingLetter);
debugf("Pending,%02d,'%c'\n", unitNum, pendingLetter);
}
else {
debugf("Unit %02d move to '%c'\n", unitNum, toLetter);
moveStepperbyFlap(flapsToMove);
pendingLetter = 0;
}
}
// start calibration of the unit using the hall sensor
void Unit::calibrateStart() {
calibrationComplete = false;
calibrationStarted = true;
calibrationStartTime = millis();
stepper->runForward();
// if starting within range of the sensor, need to move outside range before doing calibration
if (currentHallValue == 0) {
debugf("preInitialise started for Unit %d\n", unitNum);
preInitialise = true;
}
else {
preInitialise = false;
}
debugf("Calibration started for Unit %d\n", unitNum);
stepper->runForward();
}
// continue calibration of the unit using the hall sensor
int8_t Unit::calibrate() {
if (!calibrationComplete) {
// If taking too long, there must be a problem
if (millis() - calibrationStartTime > 12000) {
currentLetterPosition = 0;
calibrationComplete = true;
calibrationStarted= false;
debugf("calibration for Unit %d failed\n", unitNum);
stepper->forceStop();
return -1;
}
// if reached end of preinitialisation phase
if (preInitialise == true && currentHallValue == 1) {
preInitialise = false;
debugf("preInitialise completed for Unit %d\n", unitNum);
}
//if still in preinitialising phase, keep moving
else if (preInitialise == true) {
return 0;
}
// if sensor reached, do calibration
else if (currentHallValue == 0) {
// reached marker, go to calibrated offset position
debugf("Calb,%02d\n", unitNum);
stepper->forceStopAndNewPosition(0);
delay(1); // attempt to fix rare hangup
stepper->move(calOffsetUnit[unitNum]);
currentLetterPosition = 0;
missedSteps = 0;
calibrationComplete = true;
calibrationStarted = false;
// Reset speed
// stepper->setSpeedInUs(rotationSpeeduS); // the parameter is us/step
debugf("Unit %d calibrated\n", unitNum);
return 1;
}
return 0;
}
// just return if calibration already completed
return 1;
}
boolean Unit::checkIfRunning() {
return stepper->isRunning();
}
boolean Unit::updateHallValue(uint8_t updatedHallValue) {
uint32_t timedelta;
if (updatedHallValue != currentHallValue) {
timedelta = millis() - lastHallUpdateTime;
currentHallValue = updatedHallValue;
lastHallUpdateTime = millis();
// debugf("Unit: %d, Hall: %d, Delta: %d\n", unitNum, currentHallValue, timedelta);
// debugf("Hall,%02d,%d,%lu,%d,%d,%d,'%c'\n", unitNum, currentHallValue, timedelta, preInitialise, calibrationStarted, calibrationComplete, pendingLetter);
// If occasional glitch occurrs, start calibration again
if (timedelta <= 100) {
debugf(TXT_RED "GLITCH,%02d,%d,%d,'%c'\n" TXT_RST, unitNum, updatedHallValue, timedelta, destinationLetter);
return false;
}
}
return true;
}
git hub link: split-flap/src/unit.cpp at main · tinkermax/split-flap · GitHub