From 2c258c20bf5b9bf13016bd84805ae3b8a85d11ac Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Mon, 19 Feb 2024 13:29:29 -0800 Subject: [PATCH 01/48] proof of concept --- Alipay ESP32 Code/.gitignore | 5 + Alipay ESP32 Code/.vscode/extensions.json | 10 ++ Alipay ESP32 Code/include/README | 39 +++++++ .../lib/LEDHandler/LEDHandler.cpp | 30 ++++++ Alipay ESP32 Code/lib/LEDHandler/LEDHandler.h | 7 ++ Alipay ESP32 Code/lib/README | 46 ++++++++ Alipay ESP32 Code/platformio.ini | 18 ++++ Alipay ESP32 Code/src/main.cpp | 101 ++++++++++++++++++ Alipay ESP32 Code/test/README | 11 ++ controller.py | 92 ++++++++++++++++ mycontroller.py | 52 +++++++++ mylistener.py | 11 ++ 12 files changed, 422 insertions(+) create mode 100644 Alipay ESP32 Code/.gitignore create mode 100644 Alipay ESP32 Code/.vscode/extensions.json create mode 100644 Alipay ESP32 Code/include/README create mode 100644 Alipay ESP32 Code/lib/LEDHandler/LEDHandler.cpp create mode 100644 Alipay ESP32 Code/lib/LEDHandler/LEDHandler.h create mode 100644 Alipay ESP32 Code/lib/README create mode 100644 Alipay ESP32 Code/platformio.ini create mode 100644 Alipay ESP32 Code/src/main.cpp create mode 100644 Alipay ESP32 Code/test/README create mode 100644 controller.py create mode 100644 mycontroller.py create mode 100644 mylistener.py diff --git a/Alipay ESP32 Code/.gitignore b/Alipay ESP32 Code/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/Alipay ESP32 Code/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/Alipay ESP32 Code/.vscode/extensions.json b/Alipay ESP32 Code/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/Alipay ESP32 Code/.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/Alipay ESP32 Code/include/README b/Alipay ESP32 Code/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/Alipay ESP32 Code/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/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.cpp b/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.cpp new file mode 100644 index 0000000..c8176c4 --- /dev/null +++ b/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.cpp @@ -0,0 +1,30 @@ +#include "LEDHandler.h" + +CRGB leds[1]; +bool ledToggleState = 0; +unsigned long lastdelayToggle = millis(); +unsigned long currentDelayToggle = millis(); + + +void init_led() { + FastLED.addLeds(leds, 1).setCorrection(TypicalLEDStrip); +} + +void setLeds(CRGB color) +{ + fill_solid(leds, 1, color); + FastLED.show(); +} + +void toggleLeds(CRGB color1, CRGB color2, int delayMS) { + currentDelayToggle = millis(); + if (currentDelayToggle - lastdelayToggle > delayMS) { + ledToggleState = !ledToggleState; + lastdelayToggle = currentDelayToggle; + } + + if (ledToggleState) + setLeds(color1); + else + setLeds(color2); +} \ No newline at end of file diff --git a/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.h b/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.h new file mode 100644 index 0000000..38afb4b --- /dev/null +++ b/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.h @@ -0,0 +1,7 @@ +#include + +void init_led(); + +void setLeds(CRGB color); + +void toggleLeds(CRGB color1, CRGB color2, int delayMS); \ No newline at end of file diff --git a/Alipay ESP32 Code/lib/README b/Alipay ESP32 Code/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/Alipay ESP32 Code/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 +#include + +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/Alipay ESP32 Code/platformio.ini b/Alipay ESP32 Code/platformio.ini new file mode 100644 index 0000000..55474a0 --- /dev/null +++ b/Alipay ESP32 Code/platformio.ini @@ -0,0 +1,18 @@ +; 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-s3-devkitc-1] +platform = espressif32 +board = esp32-s3-devkitc-1 +framework = arduino +upload_speed = 921600 +monitor_speed = 115200 +lib_deps = + fastled/FastLED@^3.6.0 diff --git a/Alipay ESP32 Code/src/main.cpp b/Alipay ESP32 Code/src/main.cpp new file mode 100644 index 0000000..9986c72 --- /dev/null +++ b/Alipay ESP32 Code/src/main.cpp @@ -0,0 +1,101 @@ +#include +#include +#include +#include +#include "USB.h" + +const char *ssid = "RESNET-BROTECTED"; +const char *pswrd = "marbry2025"; +const IPAddress baseStationIp = IPAddress(192, 168, 86, 25); +const int baseStationPort = 12346; +const int localUdpPort = 12345; + +WiFiUDP udp; +const int packSize = 3; +char packetBuffer[packSize]; +bool disconnectedFromConroller = 0; +unsigned long lastTransmission = millis(); + +void setup() +{ + delay(750); + init_led(); + USBSerial.begin(115200); + setLeds(CRGB::Green); + + WiFi.setMinSecurity(WIFI_AUTH_WEP); + WiFi.begin(ssid, pswrd); + WiFi.setTxPower(WIFI_POWER_8_5dBm); + wl_status_t wifistat = WiFi.status(); + while (wifistat != WL_CONNECTED) + { + wifistat = WiFi.status(); + switch (wifistat) + { + case WL_NO_SSID_AVAIL: + USBSerial.println("[WiFi] SSID not found"); + break; + case WL_CONNECT_FAILED: + USBSerial.print("[WiFi] Failed - WiFi not connected! Reason: "); + break; + case WL_CONNECTION_LOST: + USBSerial.println("[WiFi] Connection was lost"); + break; + case WL_SCAN_COMPLETED: + USBSerial.println("[WiFi] Scan is completed"); + break; + case WL_DISCONNECTED: + USBSerial.println("[WiFi] WiFi is disconnected"); + break; + case WL_CONNECTED: + USBSerial.println("[WiFi] WiFi is connected!"); + USBSerial.print("[WiFi] IP address: "); + USBSerial.println(WiFi.localIP()); + udp.begin(localUdpPort); + break; + default: + USBSerial.print("."); + } + delay(100); + } + setLeds(CRGB::Black); +} + +void loop() +{ + int packetSize = udp.parsePacket(); + if (packetSize) { // receive incoming UDP packets + // USBSerial.printf("Received %d bytes from %s, port %d\n", packetSize, udp.remoteIP().toString().c_str(), udp.remotePort()); + int len = udp.read(packetBuffer, packetSize); + if (len > 0) + packetBuffer[len] = 0; + lastTransmission = millis(); + // USBSerial.printf("[CONTROLLER]: %s\n", packetBuffer); + } + + disconnectedFromConroller = (millis() - lastTransmission > 500); + if (disconnectedFromConroller) { + toggleLeds(CRGB::Red, CRGB::Black, 250); + } else { + if (packetBuffer[1] == '1') { + if (packetBuffer[0] == '1') + setLeds(CRGB::Red); + else if (packetBuffer[0] == '2') + setLeds(CRGB::Orange); + else if (packetBuffer[0] == '3') + setLeds(CRGB::Blue); + else if (packetBuffer[0] == '4') + setLeds(CRGB::Green); + else + setLeds(CRGB::Black); + } else { + toggleLeds(CRGB::Red, CRGB::Green, 500); + } + } + + // udp.beginPacket(baseStationIp, baseStationPort); + // udp.print("hello from esp32!"); + // udp.endPacket(); + // delay(500); + // USBSerial.println("sent!"); +} diff --git a/Alipay ESP32 Code/test/README b/Alipay ESP32 Code/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/Alipay ESP32 Code/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 diff --git a/controller.py b/controller.py new file mode 100644 index 0000000..ce4d499 --- /dev/null +++ b/controller.py @@ -0,0 +1,92 @@ +import socket +import time +import keyboard + +def send_udp_packet(data, address): + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 8192) # Buffer size 8192 + sock.sendto(data.encode(), address) + +# IP address and port of the Arduino-ESP32 +arduino_address = ("192.168.43.71", 12345) + +last_throttle_command = "n" # Default to "n" for neutral +steering_value = 0 # Default steering value +power_value = 7 # Default power value (integer from 0 to 100) +radius_value = 3.21 # Default radius value +enabled = False # Default to enabled +rpm = 600 + +print_counter = 0 +print_interval = 10 # Print every 10 loops + +while True: + # Check for throttle command (up for forward, down for backward) + if keyboard.is_pressed("up"): + last_throttle_command = "f" + elif keyboard.is_pressed("down"): + last_throttle_command = "b" + else: + last_throttle_command = "n" + + # Check for steering command (left and right arrow keys) + if keyboard.is_pressed("r"): + steering_value -= 0.1 # No limit on steering_value adjustment + elif keyboard.is_pressed("l"): + steering_value += 0.1 + + # Check for power command (u for increase, d for decrease) + if keyboard.is_pressed("u"): + power_value = min(100, power_value + 1) + elif keyboard.is_pressed("d"): + power_value = max(0, power_value - 1) + + # Adjust radius using f and c keys + if keyboard.is_pressed("f"): + radius_value += 0.01 + elif keyboard.is_pressed("c"): + radius_value = max(0, radius_value - 0.01) + + # Toggle enabled field (e for enable, s for disable) + if keyboard.is_pressed("e"): + enabled = 'true' + elif keyboard.is_pressed("s"): + enabled = 'false' + + power_boost = 0 + if keyboard.is_pressed("p"): + power_boost = 25 + # Adjust rpm + if keyboard.is_pressed("3"): + rpm += 100 + elif keyboard.is_pressed("1"): + rpm = max(0, rpm - 100) + + # Construct the packet string with throttle, steering, power, radius, enabled, and timestamp + timestamp = int(time.time() * 100 - 170633619600) + s = steering_value + # Check for steering command (left and right arrow keys) + steering_power = 0.5 + if keyboard.is_pressed("q"): + steering_power = 1 + if keyboard.is_pressed("left"): + s += steering_power # No limit on steering_value adjustment + elif keyboard.is_pressed("right"): + s -= steering_power + power_command = max(power_boost, power_value) + packet = f"rpm={rpm}&.throttle={last_throttle_command}&steering={s:.2f}&power={power_command}&radius={radius_value:.2f}×tamp={timestamp}&enabled={enabled}&" + + # Send the packet to the Arduino-ESP32 + send_udp_packet(packet, arduino_address) + print("Sent Command:", packet) + + # Increment the print counter + print_counter += 1 + + # Print the sent command every 10 loops + if print_counter == print_interval: + # print("Sent Command:", packet) + print_counter = 0 # Reset the counter + + # Wait for a short interval to avoid sending too many packets + time.sleep(0.1) \ No newline at end of file diff --git a/mycontroller.py b/mycontroller.py new file mode 100644 index 0000000..5431428 --- /dev/null +++ b/mycontroller.py @@ -0,0 +1,52 @@ +import socket +import time +import threading +import keyboard + + +# IP address and port of the Arduino-ESP32 +alipay_address = ("192.168.86.35", 12345) + +def udp_receiver(): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(("0.0.0.0", 12346)) + + while True: + data, addr = sock.recvfrom(64) + print(f"[ALIPAY] {data}") + +def send_udp_packet(data, address): + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 128) + sock.sendto(data.encode(), address) + +receiver_thread = threading.Thread(target=udp_receiver) +receiver_thread.daemon = True # This allows the thread to exit when the main thread exits +receiver_thread.start() + +drivecmd = 0 +enabled = 0 +spacebarLast = 0 +spacebarCurrent = 0 +while (True): + if keyboard.is_pressed("up"): + drivecmd = 1 + elif keyboard.is_pressed("right"): + drivecmd = 2 + elif keyboard.is_pressed("down"): + drivecmd = 3 + elif keyboard.is_pressed("left"): + drivecmd = 4 + else: + drivecmd = 0 + + spacebarCurrent = keyboard.is_pressed('space') + if (spacebarCurrent and not spacebarLast): + enabled = not enabled + spacebarLast = spacebarCurrent + + cmd = f"{drivecmd}{int(enabled)}" + send_udp_packet(cmd, alipay_address) + # print(f"Sent Command: {cmd}") + time.sleep(0.05) + \ No newline at end of file diff --git a/mylistener.py b/mylistener.py new file mode 100644 index 0000000..c16adc6 --- /dev/null +++ b/mylistener.py @@ -0,0 +1,11 @@ +import socket + +UDP_IP = "0.0.0.0" +UDP_PORT = 12346 + +sock = socket.socket(socket.AF_INET, # Internet + socket.SOCK_DGRAM) # UDP +sock.bind((UDP_IP, UDP_PORT)) +while True: + data, addr = sock.recvfrom(128) # buffer size is 1024 bytes + print("received message: %s" % data) \ No newline at end of file From 45f117c3cfa74e9ffffe81968495432be8a4a1ab Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Wed, 21 Feb 2024 09:47:19 -0800 Subject: [PATCH 02/48] all the sensors work with code, no movement yet --- Alipay ESP32 Code/.vscode/settings.json | 12 ++ .../lib/Battery_Monitor/Battery_Monitor.h | 5 + .../lib/Drive_Motors/Drive_Motors.cpp | 25 ++++ .../lib/Drive_Motors/Drive_Motors.h | 16 +++ .../lib/LEDHandler/LEDHandler.cpp | 2 +- .../lib/LaptopTelemetry/LaptopTelemetry.cpp | 72 +++++++++++ .../lib/LaptopTelemetry/LaptopTelemetry.h | 20 +++ Alipay ESP32 Code/lib/Melty/melty.cpp | 45 +++++++ Alipay ESP32 Code/lib/Melty/melty.h | 22 ++++ Alipay ESP32 Code/lib/mpu6050/mpu6050.cpp | 35 ++++++ Alipay ESP32 Code/lib/mpu6050/mpu6050.h | 9 ++ Alipay ESP32 Code/platformio.ini | 6 +- Alipay ESP32 Code/src/main.cpp | 116 +++++++----------- Motor_Test/.gitignore | 5 + Motor_Test/.vscode/extensions.json | 10 ++ Motor_Test/include/README | 39 ++++++ Motor_Test/lib/README | 46 +++++++ Motor_Test/platformio.ini | 17 +++ Motor_Test/src/main.cpp | 43 +++++++ Motor_Test/test/README | 11 ++ mycontroller.py | 23 ++-- 21 files changed, 494 insertions(+), 85 deletions(-) create mode 100644 Alipay ESP32 Code/.vscode/settings.json create mode 100644 Alipay ESP32 Code/lib/Battery_Monitor/Battery_Monitor.h create mode 100644 Alipay ESP32 Code/lib/Drive_Motors/Drive_Motors.cpp create mode 100644 Alipay ESP32 Code/lib/Drive_Motors/Drive_Motors.h create mode 100644 Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp create mode 100644 Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.h create mode 100644 Alipay ESP32 Code/lib/Melty/melty.cpp create mode 100644 Alipay ESP32 Code/lib/Melty/melty.h create mode 100644 Alipay ESP32 Code/lib/mpu6050/mpu6050.cpp create mode 100644 Alipay ESP32 Code/lib/mpu6050/mpu6050.h create mode 100644 Motor_Test/.gitignore create mode 100644 Motor_Test/.vscode/extensions.json create mode 100644 Motor_Test/include/README create mode 100644 Motor_Test/lib/README create mode 100644 Motor_Test/platformio.ini create mode 100644 Motor_Test/src/main.cpp create mode 100644 Motor_Test/test/README diff --git a/Alipay ESP32 Code/.vscode/settings.json b/Alipay ESP32 Code/.vscode/settings.json new file mode 100644 index 0000000..de67d8b --- /dev/null +++ b/Alipay ESP32 Code/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "files.associations": { + "array": "cpp", + "deque": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "unordered_set": "cpp", + "vector": "cpp", + "string_view": "cpp", + "initializer_list": "cpp" + } +} \ No newline at end of file diff --git a/Alipay ESP32 Code/lib/Battery_Monitor/Battery_Monitor.h b/Alipay ESP32 Code/lib/Battery_Monitor/Battery_Monitor.h new file mode 100644 index 0000000..b6e3158 --- /dev/null +++ b/Alipay ESP32 Code/lib/Battery_Monitor/Battery_Monitor.h @@ -0,0 +1,5 @@ +#include + +float getVoltage() { + return (float(analogRead(18)) / 4096)*3.1 * 3.95; +} \ No newline at end of file diff --git a/Alipay ESP32 Code/lib/Drive_Motors/Drive_Motors.cpp b/Alipay ESP32 Code/lib/Drive_Motors/Drive_Motors.cpp new file mode 100644 index 0000000..1b247ab --- /dev/null +++ b/Alipay ESP32 Code/lib/Drive_Motors/Drive_Motors.cpp @@ -0,0 +1,25 @@ +#include "Drive_Motors.h" +#include + +void init_motors() { + ledcSetup(lMotChannel, motFreq, resolution); + ledcSetup(rMotChannel, motFreq, resolution); + ledcAttachPin(lMotPin, lMotChannel); + ledcAttachPin(rMotPin, rMotChannel); + + l_motor_write(neutralValue); + r_motor_write(neutralValue); +} + +void l_motor_write(int value) { // From 0 -> 100 + ledcWrite(lMotChannel, neutralValue + value); +} + +void r_motor_write(int value) { // From 0 -> 100 + ledcWrite(rMotChannel, neutralValue + value); +} + +void set_both_motors(int value) { + l_motor_write(value); + r_motor_write(value); +} \ No newline at end of file diff --git a/Alipay ESP32 Code/lib/Drive_Motors/Drive_Motors.h b/Alipay ESP32 Code/lib/Drive_Motors/Drive_Motors.h new file mode 100644 index 0000000..9a9508a --- /dev/null +++ b/Alipay ESP32 Code/lib/Drive_Motors/Drive_Motors.h @@ -0,0 +1,16 @@ +#define lMotPin 8 +#define rMotPin 7 +#define motFreq 10000 +#define resolution 8 // # of bits +#define lMotChannel 0 +#define rMotChannel 1 + +#define neutralValue 120 + +void init_motors(); + +void l_motor_write(int value); + +void r_motor_write(int value); + +void set_both_motors(int value); \ No newline at end of file diff --git a/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.cpp b/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.cpp index c8176c4..4a0b20a 100644 --- a/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.cpp +++ b/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.cpp @@ -7,7 +7,7 @@ unsigned long currentDelayToggle = millis(); void init_led() { - FastLED.addLeds(leds, 1).setCorrection(TypicalLEDStrip); + FastLED.addLeds(leds, 1).setCorrection(TypicalLEDStrip); } void setLeds(CRGB color) diff --git a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp new file mode 100644 index 0000000..522d002 --- /dev/null +++ b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp @@ -0,0 +1,72 @@ +#include "LaptopTelemetry.h" + +WiFiUDP udp; +IPAddress laptopIpAddress(192, 168, 86, 25); + +LaptopTelemetry::LaptopTelemetry(const char* _ssid, const char* _pswrd, char* _packetBuffer) { + ssid = _ssid; + pswrd = _pswrd; + packetBuffer = _packetBuffer; +} + +void LaptopTelemetry::init() { + WiFi.setMinSecurity(WIFI_AUTH_WEP); + WiFi.begin(ssid, pswrd); + WiFi.setTxPower(WIFI_POWER_8_5dBm); + wl_status_t wifistat = WiFi.status(); + while (wifistat != WL_CONNECTED) + { + wifistat = WiFi.status(); + switch (wifistat) { + case WL_NO_SSID_AVAIL: + USBSerial.println("[WiFi] SSID not found"); + break; + case WL_CONNECT_FAILED: + USBSerial.print("[WiFi] Failed - WiFi not connected! Reason: "); + break; + case WL_CONNECTION_LOST: + USBSerial.println("[WiFi] Connection was lost"); + break; + case WL_CONNECTED: + USBSerial.println("[WiFi] WiFi is connected!"); + USBSerial.print("[WiFi] IP address: "); + USBSerial.println(WiFi.localIP()); + udp.begin(12345); // Port that ESP32 will receive commands on + break; + default: + USBSerial.print("."); + } + delay(100); + } +} + +void LaptopTelemetry::send(const char* message) { + udp.beginPacket(laptopIpAddress, 12346); // Send to port 12346 + udp.print(message); + udp.endPacket(); +} + +void LaptopTelemetry::send(float value) { + char conversionbuffer[5]; + sprintf(conversionbuffer, "%.2f", value); + send(conversionbuffer); +} + +bool LaptopTelemetry::isDisconnected() { + return (millis() - lastTransmission > 500); +} + +void LaptopTelemetry::receive() { + size_t packetSize = udp.parsePacket(); + if (packetSize) { // receive incoming UDP packets + // USBSerial.printf("Received %d bytes from %s, port %d\n", packetSize, udp.remoteIP().toString().c_str(), udp.remotePort()); + int len = udp.read(packetBuffer, packetSize); + if (len > 0) + packetBuffer[len] = 0; + lastTransmission = millis(); + // USBSerial.printf("[CONTROLLER]: %s\n", packetBuffer); + } +} + + + diff --git a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.h b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.h new file mode 100644 index 0000000..7d3b226 --- /dev/null +++ b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.h @@ -0,0 +1,20 @@ +#include +#include + + + +class LaptopTelemetry { + public: + LaptopTelemetry(const char* _ssid, const char* _pswrd, char* _packetBuffer); + void receive(); + void init(); + void send(const char* message); + void send(float value); + bool isDisconnected(); + private: + const char* ssid; + const char* pswrd; + char* packetBuffer; + unsigned long lastTransmission = millis(); + +}; diff --git a/Alipay ESP32 Code/lib/Melty/melty.cpp b/Alipay ESP32 Code/lib/Melty/melty.cpp new file mode 100644 index 0000000..92201a7 --- /dev/null +++ b/Alipay ESP32 Code/lib/Melty/melty.cpp @@ -0,0 +1,45 @@ +#include +#include + +melty::melty() { + +} + +int melty::updateRPM() { + bool curSeenIRLed = isBeaconSensed(!digitalRead(TOP_IR_PIN)); + // USBSerial.println(curSeenIRLed); + // delay(10); + if (curSeenIRLed != lastSeenIRLed) + if (curSeenIRLed) { + setLeds(CRGB::Green); + currentPulse = millis(); + int millisPerRev = currentPulse - lastPulse; + RPM = 60000/millisPerRev; + lastPulse = millis(); + } + else + setLeds(CRGB::Red); + + lastSeenIRLed = curSeenIRLed; + + return RPM; +} + +bool melty::isBeaconSensed(bool currentReading) { + IRLedReadings[IRLedIndex++] = currentReading; // This is just code for a ring buffer + if (IRLedIndex == IRLedDataSize) + IRLedIndex = 0; + + if (lastIRLedReturnValue) { + for (int i = 0; i < IRLedDataSize; i++) + if (IRLedReadings[i] == lastIRLedReturnValue) + return true; + lastIRLedReturnValue = !lastIRLedReturnValue; + } else { + for (int i = 0; i < IRLedDataSize; i++) + if (IRLedReadings[i] == lastIRLedReturnValue) + return false; + lastIRLedReturnValue = !lastIRLedReturnValue; + } + return lastIRLedReturnValue; +} \ No newline at end of file diff --git a/Alipay ESP32 Code/lib/Melty/melty.h b/Alipay ESP32 Code/lib/Melty/melty.h new file mode 100644 index 0000000..8767aad --- /dev/null +++ b/Alipay ESP32 Code/lib/Melty/melty.h @@ -0,0 +1,22 @@ +#include +#include + +#define TOP_IR_PIN 10 +#define BOTTOM_IR_PIN 9 +#define IRLedDataSize 30 + +class melty { + public: + melty(); + int updateRPM(); + bool isBeaconSensed(bool currentReading); + int RPM = 0; + private: + bool lastSeenIRLed = 0; + unsigned long currentPulse = millis(); + unsigned long lastPulse = millis(); + + bool IRLedReadings[IRLedDataSize] = {0}; + int IRLedIndex = 0; + bool lastIRLedReturnValue = 0; +}; \ No newline at end of file diff --git a/Alipay ESP32 Code/lib/mpu6050/mpu6050.cpp b/Alipay ESP32 Code/lib/mpu6050/mpu6050.cpp new file mode 100644 index 0000000..d79fb2f --- /dev/null +++ b/Alipay ESP32 Code/lib/mpu6050/mpu6050.cpp @@ -0,0 +1,35 @@ +#include "mpu6050.h" + +Adafruit_MPU6050 mpu; + +void init_mpu6050() { + Wire.begin(5,6); + if (!mpu.begin()) { + USBSerial.println("Failed to find MPU6050 chip"); + while (1) { + delay(10); + } + } + USBSerial.println("MPU6050 Found!"); + + mpu.setHighPassFilter(MPU6050_HIGHPASS_0_63_HZ); + mpu.setAccelerometerRange(MPU6050_RANGE_16_G); +} + +float getAccelY() { + sensors_event_t a, g, temp; + mpu.getEvent(&a, &g, &temp); + return (a.acceleration.y); +} + +float getAccelZ() { + sensors_event_t a, g, temp; + mpu.getEvent(&a, &g, &temp); + return (a.acceleration.z); +} + +float getTemp() { + sensors_event_t a, g, temp; + mpu.getEvent(&a, &g, &temp); + return (temp.temperature); +} \ No newline at end of file diff --git a/Alipay ESP32 Code/lib/mpu6050/mpu6050.h b/Alipay ESP32 Code/lib/mpu6050/mpu6050.h new file mode 100644 index 0000000..004d02c --- /dev/null +++ b/Alipay ESP32 Code/lib/mpu6050/mpu6050.h @@ -0,0 +1,9 @@ +#include + +void init_mpu6050(); + +float getAccelY(); + +float getAccelZ(); + +float getTemp(); \ No newline at end of file diff --git a/Alipay ESP32 Code/platformio.ini b/Alipay ESP32 Code/platformio.ini index 55474a0..fe7bc23 100644 --- a/Alipay ESP32 Code/platformio.ini +++ b/Alipay ESP32 Code/platformio.ini @@ -15,4 +15,8 @@ framework = arduino upload_speed = 921600 monitor_speed = 115200 lib_deps = - fastled/FastLED@^3.6.0 + fastled/FastLED@^3.6.0 + adafruit/Adafruit MPU6050@^2.2.6 + Wire + adafruit/Adafruit BusIO@^1.15.0 + adafruit/Adafruit Unified Sensor@^1.1.14 diff --git a/Alipay ESP32 Code/src/main.cpp b/Alipay ESP32 Code/src/main.cpp index 9986c72..7986072 100644 --- a/Alipay ESP32 Code/src/main.cpp +++ b/Alipay ESP32 Code/src/main.cpp @@ -1,101 +1,71 @@ #include #include -#include -#include +#include #include "USB.h" +#include +#include +#include +#include const char *ssid = "RESNET-BROTECTED"; const char *pswrd = "marbry2025"; -const IPAddress baseStationIp = IPAddress(192, 168, 86, 25); -const int baseStationPort = 12346; -const int localUdpPort = 12345; -WiFiUDP udp; const int packSize = 3; char packetBuffer[packSize]; -bool disconnectedFromConroller = 0; -unsigned long lastTransmission = millis(); + +LaptopTelemetry myLaptop = LaptopTelemetry(ssid, pswrd, packetBuffer); +melty alipay = melty(); + + void setup() { - delay(750); + delay(750); // Some reason my aliexpress esp32s3 needs this delay to give it enough time to initialize init_led(); + init_mpu6050(); + init_motors(); + USBSerial.begin(115200); setLeds(CRGB::Green); - - WiFi.setMinSecurity(WIFI_AUTH_WEP); - WiFi.begin(ssid, pswrd); - WiFi.setTxPower(WIFI_POWER_8_5dBm); - wl_status_t wifistat = WiFi.status(); - while (wifistat != WL_CONNECTED) - { - wifistat = WiFi.status(); - switch (wifistat) - { - case WL_NO_SSID_AVAIL: - USBSerial.println("[WiFi] SSID not found"); - break; - case WL_CONNECT_FAILED: - USBSerial.print("[WiFi] Failed - WiFi not connected! Reason: "); - break; - case WL_CONNECTION_LOST: - USBSerial.println("[WiFi] Connection was lost"); - break; - case WL_SCAN_COMPLETED: - USBSerial.println("[WiFi] Scan is completed"); - break; - case WL_DISCONNECTED: - USBSerial.println("[WiFi] WiFi is disconnected"); - break; - case WL_CONNECTED: - USBSerial.println("[WiFi] WiFi is connected!"); - USBSerial.print("[WiFi] IP address: "); - USBSerial.println(WiFi.localIP()); - udp.begin(localUdpPort); - break; - default: - USBSerial.print("."); - } - delay(100); - } + myLaptop.init(); setLeds(CRGB::Black); } void loop() { - int packetSize = udp.parsePacket(); - if (packetSize) { // receive incoming UDP packets - // USBSerial.printf("Received %d bytes from %s, port %d\n", packetSize, udp.remoteIP().toString().c_str(), udp.remotePort()); - int len = udp.read(packetBuffer, packetSize); - if (len > 0) - packetBuffer[len] = 0; - lastTransmission = millis(); - // USBSerial.printf("[CONTROLLER]: %s\n", packetBuffer); - } - - disconnectedFromConroller = (millis() - lastTransmission > 500); - if (disconnectedFromConroller) { - toggleLeds(CRGB::Red, CRGB::Black, 250); + myLaptop.receive(); + + if (myLaptop.isDisconnected()) { + set_both_motors(0); + setLeds(CRGB::Orange); } else { if (packetBuffer[1] == '1') { - if (packetBuffer[0] == '1') - setLeds(CRGB::Red); + alipay.updateRPM(); + + EVERY_N_MILLIS(100) { + myLaptop.send(alipay.RPM); + } + + if (packetBuffer[0] == '1') { + set_both_motors(5); + } else if (packetBuffer[0] == '2') - setLeds(CRGB::Orange); + set_both_motors(7); else if (packetBuffer[0] == '3') - setLeds(CRGB::Blue); + set_both_motors(9); else if (packetBuffer[0] == '4') - setLeds(CRGB::Green); - else - setLeds(CRGB::Black); + set_both_motors(12); + // else + // setLeds(CRGB::Black); } else { - toggleLeds(CRGB::Red, CRGB::Green, 500); - } + toggleLeds(CRGB::Red, CRGB::Green, 500); + set_both_motors(0); + + EVERY_N_MILLIS(2000) { + myLaptop.send(getVoltage()); + } + } } - - // udp.beginPacket(baseStationIp, baseStationPort); - // udp.print("hello from esp32!"); - // udp.endPacket(); - // delay(500); - // USBSerial.println("sent!"); + + } diff --git a/Motor_Test/.gitignore b/Motor_Test/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/Motor_Test/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/Motor_Test/.vscode/extensions.json b/Motor_Test/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/Motor_Test/.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/Motor_Test/include/README b/Motor_Test/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/Motor_Test/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/Motor_Test/lib/README b/Motor_Test/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/Motor_Test/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 +#include + +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/Motor_Test/platformio.ini b/Motor_Test/platformio.ini new file mode 100644 index 0000000..599ca4d --- /dev/null +++ b/Motor_Test/platformio.ini @@ -0,0 +1,17 @@ +; 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-s3-devkitc-1] +platform = espressif32 +board = esp32-s3-devkitc-1 +framework = arduino +upload_speed = 921600 +monitor_speed = 115200 +monitor_filters = send_on_enter \ No newline at end of file diff --git a/Motor_Test/src/main.cpp b/Motor_Test/src/main.cpp new file mode 100644 index 0000000..d4c356a --- /dev/null +++ b/Motor_Test/src/main.cpp @@ -0,0 +1,43 @@ +#include +#include "/Users/mingweiyeoh/Documents/GitHub/Arduino-Projects/libraries/Custom/USBSerialHandler.h" + +SerialHandler myComputer = SerialHandler(); + +const int motpin = 8; +const int motchannel = 0; +const int neutralVal = 120; + +void autoMotorTune() { + delay(1000); + USBSerial.println("Start with motor off!!"); + ledcWrite(motchannel, 220); + delay(3000); + USBSerial.println("Turn motor on!!!"); + delay(5000); + USBSerial.println("Calibrating neutral"); + ledcWrite(motchannel, neutralVal); // Calibrate neutral stick + delay(10000); + USBSerial.println("Done!!!"); +} + + +void setup() { + USBSerial.begin(115200); + ledcSetup(motchannel, 10000, 8); + ledcAttachPin(motpin, motchannel); + ledcWrite(motchannel, neutralVal); + + + autoMotorTune(); + + while (myComputer.getInt(0) == 0) { + delay(10); + } +} + +void loop() { + // put your main code here, to run repeatedly: + ledcWrite(motchannel, myComputer.getInt(0)); + delay(10); +} + diff --git a/Motor_Test/test/README b/Motor_Test/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/Motor_Test/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 diff --git a/mycontroller.py b/mycontroller.py index 5431428..07b804e 100644 --- a/mycontroller.py +++ b/mycontroller.py @@ -5,15 +5,19 @@ # IP address and port of the Arduino-ESP32 -alipay_address = ("192.168.86.35", 12345) +alipay_address = ("192.168.86.22", 12345) def udp_receiver(): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(("0.0.0.0", 12346)) while True: - data, addr = sock.recvfrom(64) - print(f"[ALIPAY] {data}") + data, addr = sock.recvfrom(64) + try: + data_str = data.decode("utf-8") + print(f"[ALIPAY] {data_str}") + except: + pass def send_udp_packet(data, address): with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: @@ -26,8 +30,6 @@ def send_udp_packet(data, address): drivecmd = 0 enabled = 0 -spacebarLast = 0 -spacebarCurrent = 0 while (True): if keyboard.is_pressed("up"): drivecmd = 1 @@ -40,13 +42,14 @@ def send_udp_packet(data, address): else: drivecmd = 0 - spacebarCurrent = keyboard.is_pressed('space') - if (spacebarCurrent and not spacebarLast): - enabled = not enabled - spacebarLast = spacebarCurrent + if keyboard.is_pressed('space'): + if keyboard.is_pressed('tab'): + enabled = 1 + else: + enabled = 0 cmd = f"{drivecmd}{int(enabled)}" send_udp_packet(cmd, alipay_address) # print(f"Sent Command: {cmd}") time.sleep(0.05) - \ No newline at end of file + \ No newline at end of file From 35e227f9e73335db24e127be8a2e9ab335853429 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Wed, 21 Feb 2024 15:13:12 -0800 Subject: [PATCH 03/48] moved to pynput vs keyboard library --- .../lib/LaptopTelemetry/LaptopTelemetry.cpp | 3 +- Alipay ESP32 Code/src/main.cpp | 9 +- controller.py | 92 ------------------- controller/AlipayTelemetry.py | 0 mycontroller.py => controller/controller.py | 45 +++++++-- controller/pynput_test.py | 31 +++++++ mylistener.py | 11 --- 7 files changed, 75 insertions(+), 116 deletions(-) delete mode 100644 controller.py create mode 100644 controller/AlipayTelemetry.py rename mycontroller.py => controller/controller.py (50%) create mode 100644 controller/pynput_test.py delete mode 100644 mylistener.py diff --git a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp index 522d002..e29cbd0 100644 --- a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp +++ b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp @@ -1,7 +1,8 @@ #include "LaptopTelemetry.h" WiFiUDP udp; -IPAddress laptopIpAddress(192, 168, 86, 25); +// IPAddress laptopIpAddress(192, 168, 86, 25); +IPAddress laptopIpAddress(192, 168, 111, 171); LaptopTelemetry::LaptopTelemetry(const char* _ssid, const char* _pswrd, char* _packetBuffer) { ssid = _ssid; diff --git a/Alipay ESP32 Code/src/main.cpp b/Alipay ESP32 Code/src/main.cpp index 7986072..cf32b54 100644 --- a/Alipay ESP32 Code/src/main.cpp +++ b/Alipay ESP32 Code/src/main.cpp @@ -7,8 +7,11 @@ #include #include -const char *ssid = "RESNET-BROTECTED"; -const char *pswrd = "marbry2025"; +// const char *ssid = "RESNET-BROTECTED"; +// const char *pswrd = "marbry2025"; + +const char *ssid = "EnVision-Local"; +const char *pswrd = "thinkmakebreak"; const int packSize = 3; char packetBuffer[packSize]; @@ -20,7 +23,7 @@ melty alipay = melty(); void setup() { - delay(750); // Some reason my aliexpress esp32s3 needs this delay to give it enough time to initialize + // delay(750); // Some reason my aliexpress esp32s3 needs this delay to give it enough time to initialize init_led(); init_mpu6050(); init_motors(); diff --git a/controller.py b/controller.py deleted file mode 100644 index ce4d499..0000000 --- a/controller.py +++ /dev/null @@ -1,92 +0,0 @@ -import socket -import time -import keyboard - -def send_udp_packet(data, address): - with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: - sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 8192) # Buffer size 8192 - sock.sendto(data.encode(), address) - -# IP address and port of the Arduino-ESP32 -arduino_address = ("192.168.43.71", 12345) - -last_throttle_command = "n" # Default to "n" for neutral -steering_value = 0 # Default steering value -power_value = 7 # Default power value (integer from 0 to 100) -radius_value = 3.21 # Default radius value -enabled = False # Default to enabled -rpm = 600 - -print_counter = 0 -print_interval = 10 # Print every 10 loops - -while True: - # Check for throttle command (up for forward, down for backward) - if keyboard.is_pressed("up"): - last_throttle_command = "f" - elif keyboard.is_pressed("down"): - last_throttle_command = "b" - else: - last_throttle_command = "n" - - # Check for steering command (left and right arrow keys) - if keyboard.is_pressed("r"): - steering_value -= 0.1 # No limit on steering_value adjustment - elif keyboard.is_pressed("l"): - steering_value += 0.1 - - # Check for power command (u for increase, d for decrease) - if keyboard.is_pressed("u"): - power_value = min(100, power_value + 1) - elif keyboard.is_pressed("d"): - power_value = max(0, power_value - 1) - - # Adjust radius using f and c keys - if keyboard.is_pressed("f"): - radius_value += 0.01 - elif keyboard.is_pressed("c"): - radius_value = max(0, radius_value - 0.01) - - # Toggle enabled field (e for enable, s for disable) - if keyboard.is_pressed("e"): - enabled = 'true' - elif keyboard.is_pressed("s"): - enabled = 'false' - - power_boost = 0 - if keyboard.is_pressed("p"): - power_boost = 25 - # Adjust rpm - if keyboard.is_pressed("3"): - rpm += 100 - elif keyboard.is_pressed("1"): - rpm = max(0, rpm - 100) - - # Construct the packet string with throttle, steering, power, radius, enabled, and timestamp - timestamp = int(time.time() * 100 - 170633619600) - s = steering_value - # Check for steering command (left and right arrow keys) - steering_power = 0.5 - if keyboard.is_pressed("q"): - steering_power = 1 - if keyboard.is_pressed("left"): - s += steering_power # No limit on steering_value adjustment - elif keyboard.is_pressed("right"): - s -= steering_power - power_command = max(power_boost, power_value) - packet = f"rpm={rpm}&.throttle={last_throttle_command}&steering={s:.2f}&power={power_command}&radius={radius_value:.2f}×tamp={timestamp}&enabled={enabled}&" - - # Send the packet to the Arduino-ESP32 - send_udp_packet(packet, arduino_address) - print("Sent Command:", packet) - - # Increment the print counter - print_counter += 1 - - # Print the sent command every 10 loops - if print_counter == print_interval: - # print("Sent Command:", packet) - print_counter = 0 # Reset the counter - - # Wait for a short interval to avoid sending too many packets - time.sleep(0.1) \ No newline at end of file diff --git a/controller/AlipayTelemetry.py b/controller/AlipayTelemetry.py new file mode 100644 index 0000000..e69de29 diff --git a/mycontroller.py b/controller/controller.py similarity index 50% rename from mycontroller.py rename to controller/controller.py index 07b804e..42d3b25 100644 --- a/mycontroller.py +++ b/controller/controller.py @@ -1,11 +1,33 @@ import socket import time import threading -import keyboard +from pynput.keyboard import Key, Listener, KeyCode +key_state = {} + +# Function to be called when a key is pressed +def on_press(key): + key_state[key] = True # Update key state to pressed + +# Function to be called when a key is released +def on_release(key): + key_state[key] = False # Update key state to released + +def get_key_state(key): + if type(key) == str: + key = KeyCode.from_char(key) + if (key in key_state and key_state[key]): + return True + else: + return False + +# Start the keyboard listener in a separate thread +keyboard_thread = threading.Thread(target=lambda: Listener(on_press=on_press, on_release=on_release).start()) +keyboard_thread.start() # IP address and port of the Arduino-ESP32 -alipay_address = ("192.168.86.22", 12345) +# alipay_address = ("192.168.86.22", 12345) +alipay_address = ("192.168.111.177", 12345) def udp_receiver(): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -28,28 +50,33 @@ def send_udp_packet(data, address): receiver_thread.daemon = True # This allows the thread to exit when the main thread exits receiver_thread.start() + +print(f"IP Address: {socket.gethostbyname_ex(socket.gethostname())[-1][1]}") + + drivecmd = 0 enabled = 0 + while (True): - if keyboard.is_pressed("up"): + if get_key_state(Key.up): drivecmd = 1 - elif keyboard.is_pressed("right"): + elif get_key_state(Key.right): drivecmd = 2 - elif keyboard.is_pressed("down"): + elif get_key_state(Key.down): drivecmd = 3 - elif keyboard.is_pressed("left"): + elif get_key_state(Key.left): drivecmd = 4 else: drivecmd = 0 - if keyboard.is_pressed('space'): - if keyboard.is_pressed('tab'): + if get_key_state(Key.space): + if get_key_state(Key.tab): enabled = 1 else: enabled = 0 cmd = f"{drivecmd}{int(enabled)}" - send_udp_packet(cmd, alipay_address) + # send_udp_packet(cmd, alipay_address) # print(f"Sent Command: {cmd}") time.sleep(0.05) \ No newline at end of file diff --git a/controller/pynput_test.py b/controller/pynput_test.py new file mode 100644 index 0000000..a3f8729 --- /dev/null +++ b/controller/pynput_test.py @@ -0,0 +1,31 @@ +from pynput.keyboard import Key, Listener, KeyCode +import threading +import time + +# Initialize a variable to track the state of the key +key_state = {} + +# Function to be called when a key is pressed +def on_press(key): + key_state[key] = True # Update key state to pressed + +# Function to be called when a key is released +def on_release(key): + key_state[key] = False # Update key state to released + +def get_key_state(key): + if type(key) == str: + key = KeyCode.from_char(key) + if (key in key_state and key_state[key]): + return True + else: + return False + +# Start the keyboard listener in a separate thread +keyboard_thread = threading.Thread(target=lambda: Listener(on_press=on_press, on_release=on_release).start()) +keyboard_thread.start() + +while True: + print(get_key_state('a')) + print(get_key_state(Key.space)) + time.sleep(0.1) diff --git a/mylistener.py b/mylistener.py deleted file mode 100644 index c16adc6..0000000 --- a/mylistener.py +++ /dev/null @@ -1,11 +0,0 @@ -import socket - -UDP_IP = "0.0.0.0" -UDP_PORT = 12346 - -sock = socket.socket(socket.AF_INET, # Internet - socket.SOCK_DGRAM) # UDP -sock.bind((UDP_IP, UDP_PORT)) -while True: - data, addr = sock.recvfrom(128) # buffer size is 1024 bytes - print("received message: %s" % data) \ No newline at end of file From e6370c93c34ac233237c784371aa3a43a1f9cabd Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Wed, 21 Feb 2024 19:48:06 -0800 Subject: [PATCH 04/48] starting work on getting ir beacon connected and communicating with network --- .DS_Store | Bin 0 -> 6148 bytes Alipay ESP32 Code/.DS_Store | Bin 0 -> 6148 bytes Alipay ESP32 Code/lib/.DS_Store | Bin 0 -> 6148 bytes .../lib/LEDHandler/LEDHandler.cpp | 6 +- Alipay ESP32 Code/lib/LEDHandler/LEDHandler.h | 6 ++ .../lib/LaptopTelemetry/LaptopTelemetry.cpp | 2 +- .../lib/LaptopTelemetry/LaptopTelemetry.h | 10 ++- IR Beacon/.gitignore | 5 ++ IR Beacon/.vscode/extensions.json | 10 +++ IR Beacon/include/README | 39 +++++++++ .../lib/Battery_Monitor/Battery_Monitor.h | 5 ++ IR Beacon/lib/README | 46 +++++++++++ IR Beacon/platformio.ini | 20 +++++ IR Beacon/src/main.cpp | 39 +++++++++ IR Beacon/test/README | 11 +++ controller/AlipayTelemetry.py | 29 +++++++ .../{pynput_test.py => LaptopKeyboard.py} | 14 +--- .../AlipayTelemetry.cpython-311.pyc | Bin 0 -> 2464 bytes .../LaptopKeyboard.cpython-311.pyc | Bin 0 -> 958 bytes controller/controller.py | 77 +++++------------- 20 files changed, 244 insertions(+), 75 deletions(-) create mode 100644 .DS_Store create mode 100644 Alipay ESP32 Code/.DS_Store create mode 100644 Alipay ESP32 Code/lib/.DS_Store create mode 100644 IR Beacon/.gitignore create mode 100644 IR Beacon/.vscode/extensions.json create mode 100644 IR Beacon/include/README create mode 100644 IR Beacon/lib/Battery_Monitor/Battery_Monitor.h create mode 100644 IR Beacon/lib/README create mode 100644 IR Beacon/platformio.ini create mode 100644 IR Beacon/src/main.cpp create mode 100644 IR Beacon/test/README rename controller/{pynput_test.py => LaptopKeyboard.py} (55%) create mode 100644 controller/__pycache__/AlipayTelemetry.cpython-311.pyc create mode 100644 controller/__pycache__/LaptopKeyboard.cpython-311.pyc diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..9549f95f0a1bcf4cdc78c619b917b6ab403208ff GIT binary patch literal 6148 zcmeHKO>5gg5S?|LMj{kkXd%5V^ctuWr==kmRY?xLl_<1_rnpih5>ZuaEW4%{gYM}c z>L2Ov>6_h+NpX5?X~GQ5yzzWU+PARlAtDt|^Btld5d~1jP6%_0aGrHVIzF-nRQ4Gu zeIrfBl+fFiY6<+04DjAPqSsVWNh7+se*@aZULKO6oQk|TuQFpQY%_)QhE9-*sX)Uy z3hHdujIzpP$o|MiRoP7X{hz9{)?I(F5o`pT!N=s%%#ukunKp4c{=mOZ@wn{TPqTMr zF&p+CADViSmUS_<4P{=S3qI3*nhRH_xdju+j_pg z+lN1Rxmbk3lV{HlPKKYWFSWUH^ebKh%R>iC`yJjbOZ%FP>&ny@$X<6YTf`9tgaKjT zjxylSvtaX%e!qOSFdz*4JqGxE&``$6W9!f!9WZtT0Co|!0-t9I?$I72kF7&wAj-7@ zU90jVhH~wQYhM?6Y#q9GQhxYQ{>{oyD9XMa&)2q`ROC>sFdz*4&H>kRIRv$A6yx zZIKvZKp42645;o=auj22{%&1aCEv9g`U%Ryb*;l?3mkhDqn5AYEvOa5H9Nq_W9twR Qi2M<-G>8!f{wV{$06yhY=l}o! literal 0 HcmV?d00001 diff --git a/Alipay ESP32 Code/.DS_Store b/Alipay ESP32 Code/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f9f60d0a8f94f19a9f55d4bcbfeaf7866207ada5 GIT binary patch literal 6148 zcmeH~Jr2S!425mzfW*>~F$)La1`&c2Z~;_UkSZ}C_8gt(pM}DVD)cPbU+mOs`-Y|# z5#2wpJCR;QR&b+iElf<2FJ+XQ4A;kH9PangN^Xm!72v&0_H&z{0#twsPys4H1!kl` z9^|X_jGl>)LItS6JQT3+LxCG>vIYIqf#4$m*rDu(wa*e@u>x3=Er<$CqZN!+^)bZi z-VT<$t|nVB+C_8t(7dzS6a&*}7cEF&S{)2jfC@|$SVrF4`G16eoBt;*OsN1B_%j8x zJDd)Cyi}g8AFpTib5?EL;GkcQ@b(jc#E#+(+ztE17GO=bASy8a2)GOkRN$uyyZ{;| B5o7=W literal 0 HcmV?d00001 diff --git a/Alipay ESP32 Code/lib/.DS_Store b/Alipay ESP32 Code/lib/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..4929b633881ce76573c0e63fc89ea430d06e113f GIT binary patch literal 6148 zcmeHKyH3ME5S)b+5fr4PykFoCPLU%~q=OIOVLF3R2nC(H{5G=>kRuBz6-BZu?Tv45 z=T@G=>jfao&-Mmb0GQJivD0O0dagdPvxq2)-O=M6_jo}^Kg^VSg*6_@++xgs4g_eU7^7(32$hv&}N))Sk=r*1yX@jAQeajPFDf;Y_<8NW3H({Dv%2N zDxlwoLRYMTgQI;qxY!6loH1<1XPYI6MFYedI5;vw6Q>fLDluY+(-|+3R|5w}r$ekV z?}^G16N*@M#*3vxs$;IHKq|1Wz`1W1+W%MdKj!~^QtncLRN$x-kXdurEcr^&TPH84 wy|&OF=s(6>OXp&(m}sq-8*Rncqr9TeJg(leds, 1).setCorrection(TypicalLEDStrip); + FastLED.addLeds(leds, 1).setCorrection(TypicalLEDStrip); } void setLeds(CRGB color) { - fill_solid(leds, 1, color); + leds[0] = color; FastLED.show(); } diff --git a/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.h b/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.h index 38afb4b..2fb8ceb 100644 --- a/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.h +++ b/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.h @@ -1,5 +1,11 @@ #include +#ifdef IR_BEACON + #define LEDPIN 5 +#else + #define LEDPIN 4 +#endif + void init_led(); void setLeds(CRGB color); diff --git a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp index e29cbd0..57e3af6 100644 --- a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp +++ b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp @@ -42,7 +42,7 @@ void LaptopTelemetry::init() { } void LaptopTelemetry::send(const char* message) { - udp.beginPacket(laptopIpAddress, 12346); // Send to port 12346 + udp.beginPacket(laptopIpAddress, PORT); // Send to port 12346 udp.print(message); udp.endPacket(); } diff --git a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.h b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.h index 7d3b226..e365f58 100644 --- a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.h +++ b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.h @@ -1,8 +1,16 @@ +#ifdef IR_BEACON + #define PORT 12347 +#else + #define PORT 12346 +#endif + +// #ifdef WIFI_SETTINGS 0 + + #include #include - class LaptopTelemetry { public: LaptopTelemetry(const char* _ssid, const char* _pswrd, char* _packetBuffer); diff --git a/IR Beacon/.gitignore b/IR Beacon/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/IR Beacon/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/IR Beacon/.vscode/extensions.json b/IR Beacon/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/IR Beacon/.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/IR Beacon/include/README b/IR Beacon/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/IR Beacon/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/IR Beacon/lib/Battery_Monitor/Battery_Monitor.h b/IR Beacon/lib/Battery_Monitor/Battery_Monitor.h new file mode 100644 index 0000000..b10757d --- /dev/null +++ b/IR Beacon/lib/Battery_Monitor/Battery_Monitor.h @@ -0,0 +1,5 @@ +#include + +float getVoltage() { + return (float(analogRead(18)) / 4096); // <-- Not working properly?? +} \ No newline at end of file diff --git a/IR Beacon/lib/README b/IR Beacon/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/IR Beacon/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 +#include + +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/IR Beacon/platformio.ini b/IR Beacon/platformio.ini new file mode 100644 index 0000000..b699311 --- /dev/null +++ b/IR Beacon/platformio.ini @@ -0,0 +1,20 @@ +; 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-s3-devkitc-1] +platform = espressif32 +board = esp32-s3-devkitc-1 +framework = arduino +upload_speed = 921600 +monitor_speed = 115200 +build_flags = -D IR_BEACON=1 +lib_deps = fastled/FastLED@^3.6.0 +lib_extra_dirs = + /Users/mingweiyeoh/Documents/GitHub/Alipay/Alipay ESP32 Code/lib diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp new file mode 100644 index 0000000..c7a8cd7 --- /dev/null +++ b/IR Beacon/src/main.cpp @@ -0,0 +1,39 @@ +#include +#include +#include +#include "USB.h" +#include +#include + +const char *ssid = "RESNET-BROTECTED"; +const char *pswrd = "marbry2025"; + +const int packSize = 3; +char packetBuffer[packSize]; + +LaptopTelemetry myLaptop = LaptopTelemetry(ssid, pswrd, packetBuffer); + +const int IRLedPin = 4; +const int freq = 38000; +const int ledChannel = 0; +const int resolution = 8; + +void setup(){ + init_led(); + USBSerial.begin(115200); + ledcSetup(ledChannel, freq, resolution); + ledcAttachPin(IRLedPin, ledChannel); + + delay(500); + setLeds(CRGB::Green); + myLaptop.init(); + setLeds(CRGB::Black); + +} + +void loop(){ + setLeds(CRGB::Red); + delay(250); + // ledcWrite(ledChannel, 80); + // delay(100); +} \ No newline at end of file diff --git a/IR Beacon/test/README b/IR Beacon/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/IR Beacon/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 diff --git a/controller/AlipayTelemetry.py b/controller/AlipayTelemetry.py index e69de29..467d1a9 100644 --- a/controller/AlipayTelemetry.py +++ b/controller/AlipayTelemetry.py @@ -0,0 +1,29 @@ +import socket + +class Alipay: + def __init__(self, targetIP): + self.targetIP = targetIP + self.receiveBufferSize = 128 + + def get_laptop_ip(self): + laptop_ip = "" + while (laptop_ip == ""): + laptop_ip = socket.gethostbyname_ex(socket.gethostname())[-1][1] + return laptop_ip + + def udp_receiver(self): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(("0.0.0.0", 12346)) + + while True: + data, addr = sock.recvfrom(64) # This code is blocking!! Implement whether connected / disconnected from Alipay?? + try: + data_str = data.decode("utf-8") + print(f"[ALIPAY] {data_str}") + except: + pass + + def send_udp_packet(self, data): + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, self.receiveBufferSize) + sock.sendto(data.encode(), (self.targetIP, 12345)) \ No newline at end of file diff --git a/controller/pynput_test.py b/controller/LaptopKeyboard.py similarity index 55% rename from controller/pynput_test.py rename to controller/LaptopKeyboard.py index a3f8729..82d5b04 100644 --- a/controller/pynput_test.py +++ b/controller/LaptopKeyboard.py @@ -1,8 +1,5 @@ from pynput.keyboard import Key, Listener, KeyCode -import threading -import time -# Initialize a variable to track the state of the key key_state = {} # Function to be called when a key is pressed @@ -19,13 +16,4 @@ def get_key_state(key): if (key in key_state and key_state[key]): return True else: - return False - -# Start the keyboard listener in a separate thread -keyboard_thread = threading.Thread(target=lambda: Listener(on_press=on_press, on_release=on_release).start()) -keyboard_thread.start() - -while True: - print(get_key_state('a')) - print(get_key_state(Key.space)) - time.sleep(0.1) + return False \ No newline at end of file diff --git a/controller/__pycache__/AlipayTelemetry.cpython-311.pyc b/controller/__pycache__/AlipayTelemetry.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2c94ae62b6776e9cb97956fa831cd1b641614ea4 GIT binary patch literal 2464 zcmcIlO>7fK6rTODy-vKcfdp)&gz%GsX%nZ1LR%nU*ZicU4r+ixTDo$)8(45`cXmyP zm53ZTxKSjy@lzslN`KNwTzcz)a_Z5RR+Y69Ql*}9GYW@@OZ(oe~q5u(`?hdR=bNGD~nKi|{U?z-SSym+BrW zA6g%Ph2D@iXM1{hiY<7;CM$mdt)O>Uu-!&Bbwe}lz*Wt>mNm@L$xL!wQ?y%lYfXdw zFi*W;Z+p|poSDrjdahP@iiosjFjeY9g|=GZ8nozd#DPKe+O`+H6@TzSZ1#5X_KRSr z6^#7sw)$e_zPQyFF9%Or!IRHMt>D0KyRG10aiA)YK*+KF!yhb3LF$7*Z!Yy#nAm;N zP(-kG$XplTyQ$!twHpNoX=A@&DHm`ln@Hsv7lR{S0=BsHO$58JVd1`n9Mk-?03Q4T z!mEi?vC(5%H02E=)iou4OXm4T8;)c=dkIG_lZ8qM9_5wwP2^YD*|9hDi;aW*3qJ9baC} zDYZRL3)^bQ*{t+~gHZ%FLtgmX=ceZ`KK^R?qSe!1_8+(W$BSY`^4;~$dP`gPm8EV= z>Mlv$RnhHjuY`6!+4*Sa0xgFQTA_pAd{T_x+gIg9xxLb{>&cNvM;0c^9TBS|a!+~@ zXfJJlci{jqc*=nuE6`J7&svq|z#-6fPh4ADtI819K=Az<+8^V{bB>R>goZ=U4TEgY zpZQ;a_&xvz!16{YHo&_Mzy@s{WK-li{wbqHh_+OZ$RQZc?@Q^Z`h{rCRqWI5n&S@Fd%-U`(gMek1M_6ERrMoa%ColtxP=}@;T@-|3 zE{SJkVS5do|Il}!=P$qjpz^l4EAxjQ_b(r^-Z@&9k6H4uD&f7ypS4wdfxAPqL-VJs zw|bUuS`koL(a+1iOP23a$#)5d&(5BGAUqW!UTSdcExrG&v%Hi4$t#W4tOH$!5fj?35wmKDC1c* zpVkz`_9@EO`DEG|p{?Le+mY=-76$eMVW%|P<`mJ$7*^Og2W+O^sv_^|hQC{9lIRLm zNuBvxT~5MSIhYU|4|?b&m#%m&LW#7BB?zqtb1-bPW4m^LF*8vXBBW3u z7rjX?3Oy;se;%4Gf$LIcZd#!TLTqa% zu$a9PVz+_c4jJW~E#k%+e!PUxhjC@;^@Bm@pxMV~ibL?9$5>}NE@nA|i{@D#(|7@e z8Fi_J=a7ohoK(8@TbdQH@OSCASQ&!2bh|zHi0pGZBId^8%>5*1WsyZPi|5jbB>r6a z3cV1&x#yM3 zDC|kQ Date: Wed, 21 Feb 2024 23:48:47 -0800 Subject: [PATCH 05/48] got it translating! --- .../lib/LEDHandler/LEDHandler.cpp | 4 ++ Alipay ESP32 Code/lib/LEDHandler/LEDHandler.h | 4 +- .../lib/LaptopTelemetry/LaptopTelemetry.cpp | 4 +- Alipay ESP32 Code/lib/Melty/melty.cpp | 37 ++++++++++++---- Alipay ESP32 Code/lib/Melty/melty.h | 15 ++++++- Alipay ESP32 Code/src/main.cpp | 40 ++++++++++-------- IR Beacon/src/main.cpp | 26 ++++++++++-- controller/AlipayTelemetry.py | 15 ++++--- .../AlipayTelemetry.cpython-311.pyc | Bin 2464 -> 2614 bytes controller/controller.py | 18 +++++--- 10 files changed, 116 insertions(+), 47 deletions(-) diff --git a/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.cpp b/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.cpp index d9c292d..f6fba8e 100644 --- a/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.cpp +++ b/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.cpp @@ -16,6 +16,10 @@ void setLeds(CRGB color) FastLED.show(); } +void syncToggle() { + lastdelayToggle = millis(); +} + void toggleLeds(CRGB color1, CRGB color2, int delayMS) { currentDelayToggle = millis(); if (currentDelayToggle - lastdelayToggle > delayMS) { diff --git a/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.h b/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.h index 2fb8ceb..0e27d03 100644 --- a/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.h +++ b/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.h @@ -10,4 +10,6 @@ void init_led(); void setLeds(CRGB color); -void toggleLeds(CRGB color1, CRGB color2, int delayMS); \ No newline at end of file +void toggleLeds(CRGB color1, CRGB color2, int delayMS); + +void syncToggle(); \ No newline at end of file diff --git a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp index 57e3af6..e4fbe27 100644 --- a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp +++ b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp @@ -1,8 +1,8 @@ #include "LaptopTelemetry.h" WiFiUDP udp; -// IPAddress laptopIpAddress(192, 168, 86, 25); -IPAddress laptopIpAddress(192, 168, 111, 171); +IPAddress laptopIpAddress(192, 168, 86, 25); +// IPAddress laptopIpAddress(192, 168, 111, 171); LaptopTelemetry::LaptopTelemetry(const char* _ssid, const char* _pswrd, char* _packetBuffer) { ssid = _ssid; diff --git a/Alipay ESP32 Code/lib/Melty/melty.cpp b/Alipay ESP32 Code/lib/Melty/melty.cpp index 92201a7..ae80b7c 100644 --- a/Alipay ESP32 Code/lib/Melty/melty.cpp +++ b/Alipay ESP32 Code/lib/Melty/melty.cpp @@ -5,24 +5,43 @@ melty::melty() { } -int melty::updateRPM() { +void melty::update() { bool curSeenIRLed = isBeaconSensed(!digitalRead(TOP_IR_PIN)); - // USBSerial.println(curSeenIRLed); - // delay(10); if (curSeenIRLed != lastSeenIRLed) - if (curSeenIRLed) { + if (curSeenIRLed) { // Activates on the rising edge of seeing the IR LED setLeds(CRGB::Green); currentPulse = millis(); - int millisPerRev = currentPulse - lastPulse; - RPM = 60000/millisPerRev; - lastPulse = millis(); + unsigned long deltaPulse = currentPulse - lastPulse; // How long it takes to complete one revolution + RPM = 60000/(deltaPulse); + lastPulse = currentPulse; + + unsigned long midPulse = currentPulse + pulseWidth/2; // This should ideally be centered on the beacon + unsigned long centerOfDrivePulse = midPulse + (float(deg)/360)*deltaPulse; // Direction that we should be driving towards + unsigned long deltaDriveTiming = (percentageOfRotation * deltaPulse)/2; + if (timingToggle) { // If it is going to translate whilst computing the new timings we should use a different set of variables + endDrive = centerOfDrivePulse + deltaDriveTiming; + startDrive = centerOfDrivePulse - deltaDriveTiming; + } else { // Do the same but calculate with the other values + endDrive2 = centerOfDrivePulse + deltaDriveTiming; + startDrive2 = centerOfDrivePulse - deltaDriveTiming; + } + + timingToggle = !timingToggle; // Toggle it so that next iteration uses different variables } - else + else { setLeds(CRGB::Red); + pulseWidth = millis() - lastPulse; + } lastSeenIRLed = curSeenIRLed; +} - return RPM; +bool melty::translate() { + unsigned long currentTime = millis(); + if (percentageOfRotation != 0) + return (currentTime > startDrive && currentTime < endDrive || currentTime > startDrive2 && currentTime < endDrive2); + else + return 0; } bool melty::isBeaconSensed(bool currentReading) { diff --git a/Alipay ESP32 Code/lib/Melty/melty.h b/Alipay ESP32 Code/lib/Melty/melty.h index 8767aad..3476d6d 100644 --- a/Alipay ESP32 Code/lib/Melty/melty.h +++ b/Alipay ESP32 Code/lib/Melty/melty.h @@ -3,20 +3,31 @@ #define TOP_IR_PIN 10 #define BOTTOM_IR_PIN 9 -#define IRLedDataSize 30 +#define IRLedDataSize 50 class melty { public: melty(); - int updateRPM(); + void update(); bool isBeaconSensed(bool currentReading); + bool translate(); int RPM = 0; + int deg = 0; + float percentageOfRotation = 0; private: bool lastSeenIRLed = 0; unsigned long currentPulse = millis(); unsigned long lastPulse = millis(); + unsigned long pulseWidth = millis(); bool IRLedReadings[IRLedDataSize] = {0}; int IRLedIndex = 0; bool lastIRLedReturnValue = 0; + unsigned long startDrive = millis(); + unsigned long endDrive = millis(); + + unsigned long startDrive2 = millis(); + unsigned long endDrive2 = millis(); + + bool timingToggle = 0; }; \ No newline at end of file diff --git a/Alipay ESP32 Code/src/main.cpp b/Alipay ESP32 Code/src/main.cpp index cf32b54..2dbb2e4 100644 --- a/Alipay ESP32 Code/src/main.cpp +++ b/Alipay ESP32 Code/src/main.cpp @@ -7,11 +7,11 @@ #include #include -// const char *ssid = "RESNET-BROTECTED"; -// const char *pswrd = "marbry2025"; +const char *ssid = "RESNET-BROTECTED"; +const char *pswrd = "marbry2025"; -const char *ssid = "EnVision-Local"; -const char *pswrd = "thinkmakebreak"; +// const char *ssid = "EnVision-Local"; +// const char *pswrd = "thinkmakebreak"; const int packSize = 3; char packetBuffer[packSize]; @@ -42,24 +42,30 @@ void loop() set_both_motors(0); setLeds(CRGB::Orange); } else { - if (packetBuffer[1] == '1') { - alipay.updateRPM(); - - EVERY_N_MILLIS(100) { - myLaptop.send(alipay.RPM); + if (packetBuffer[1] == '1') { // Currently enabled + alipay.update(); + if (alipay.translate()) { + l_motor_write(8-5); + r_motor_write(8+5); + } else { + set_both_motors(8); } - if (packetBuffer[0] == '1') { - set_both_motors(5); - } + if (packetBuffer[0] == '1') // Check drive cmd + alipay.deg = 0; else if (packetBuffer[0] == '2') - set_both_motors(7); + alipay.deg = 90; else if (packetBuffer[0] == '3') - set_both_motors(9); + alipay.deg = 180; else if (packetBuffer[0] == '4') - set_both_motors(12); - // else - // setLeds(CRGB::Black); + alipay.deg = 270; + + if (packetBuffer[0] != '0') {// Check drive cmd + alipay.percentageOfRotation = 0.5; + } else { + alipay.percentageOfRotation = 0.00; + } + } else { toggleLeds(CRGB::Red, CRGB::Green, 500); set_both_motors(0); diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp index c7a8cd7..fe28faf 100644 --- a/IR Beacon/src/main.cpp +++ b/IR Beacon/src/main.cpp @@ -17,6 +17,8 @@ const int IRLedPin = 4; const int freq = 38000; const int ledChannel = 0; const int resolution = 8; + +char lastEnabledCmd = '0'; void setup(){ init_led(); @@ -32,8 +34,24 @@ void setup(){ } void loop(){ - setLeds(CRGB::Red); - delay(250); - // ledcWrite(ledChannel, 80); - // delay(100); + myLaptop.receive(); + + if (myLaptop.isDisconnected()) { + setLeds(CRGB::Orange); + } else { + if (packetBuffer[0] == '1' && lastEnabledCmd == '0') { // Enabled + toggleLeds(CRGB::Blue, CRGB::Purple, 500); + ledcWrite(ledChannel, 100); + } + if (packetBuffer[0] == '0') { // Disabled + toggleLeds(CRGB::Red, CRGB::Green, 500); + ledcWrite(ledChannel, 0); + } + + if (packetBuffer[2] == '1') // Sync led toggling + syncToggle(); + + } + + lastEnabledCmd = packetBuffer[0]; } \ No newline at end of file diff --git a/controller/AlipayTelemetry.py b/controller/AlipayTelemetry.py index 467d1a9..5109e67 100644 --- a/controller/AlipayTelemetry.py +++ b/controller/AlipayTelemetry.py @@ -1,29 +1,34 @@ import socket class Alipay: - def __init__(self, targetIP): + def __init__(self, targetIP, PORT, name): self.targetIP = targetIP self.receiveBufferSize = 128 + self.PORT = PORT + self.name = name def get_laptop_ip(self): laptop_ip = "" while (laptop_ip == ""): - laptop_ip = socket.gethostbyname_ex(socket.gethostname())[-1][1] + laptop_ip = socket.gethostbyname_ex(socket.gethostname())[-1] return laptop_ip def udp_receiver(self): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.bind(("0.0.0.0", 12346)) + sock.bind(("0.0.0.0", self.PORT)) while True: data, addr = sock.recvfrom(64) # This code is blocking!! Implement whether connected / disconnected from Alipay?? try: data_str = data.decode("utf-8") - print(f"[ALIPAY] {data_str}") + print(f"[{self.name}] {data_str}") except: pass def send_udp_packet(self, data): with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, self.receiveBufferSize) - sock.sendto(data.encode(), (self.targetIP, 12345)) \ No newline at end of file + try: + sock.sendto(data.encode(), (self.targetIP, 12345)) # Fixed port because each esp32 has different IP address + except: + pass \ No newline at end of file diff --git a/controller/__pycache__/AlipayTelemetry.cpython-311.pyc b/controller/__pycache__/AlipayTelemetry.cpython-311.pyc index 2c94ae62b6776e9cb97956fa831cd1b641614ea4..654dcab5fbf995a4696800a5b8a1d73ecacc817c 100644 GIT binary patch delta 1156 zcmZuvOK1~87@nD(eVIhpnucgYO*C59R?`Rt1@W;KR7$JXdQcIO?na`0$ZRW8R?=e- z59)wMt(ueXgC|k+(jW+4Or%K3Nsqln^iT?-|7=YZ_0Q}#|3CjD|Esv-c{ubS6k39S zQtv)G@2`dm5h|o#MBtX-`?}-mt3=Kt9y%$WeDWlagwiJ)9Y!YUo0|m5q*ZO~6g%yV z?LSyu&YVFfd)~3eGZ}{+&Q3U0dhkI1K$VW18g{^xy3SCBso=+S0U{e$BoPN%*q1uu zI?PQCXGeG&KH4D^?plCJG#icQ>E+ODBAKT%TI*~$3Znk&%IkTZ7jaAE0@&U_Hok~G zY~!Wcp&!Y)3daPp#rI|ispU*@qW^Q13X6}ILNP)4T_)EPX30ZQu+Ng2^lL3 z)}>rN_i0(QtVP*U_?tltP$vZ#zjdAbnS6wMQY`7AGmYkE$bGPW((u4YM;@wu;3cjFMLK~BU?oi*{Zw6)D|bX9}NoX@b);i_UggQK=nmCv#4$e2F?Q)U5vpKgTeD|EeL zKrpw!ztc5MSGE=GscgAvLq#)ZG_$Okyj|`(mM$cx%yJ|Rr+8uUt{7Wpj4fsV%-0AG zHP0I1D|CK-z7|sSW;X~yr{gKhKqZ4%VQH|jTcBw4?-=y&r2^d{Mb+=gq_UT9qy~Sh z>DGrg&*oCGC&jb delta 975 zcmZ`&O=uHA6rS0gO?HzkOMr){~ zf`?EFQs!XQ7JJhAOHW?(6c7YGY#>Sq2t5fsh$;0Dyy%;)RiX6F?8lq;-uGtSUtuY zNhTBOU@DtXxC%)p#*(TLIB7a4f)@_+%bsLe|3c{2{7xG+f`q6O|T(4YTJfzh~e55i~|jq?-yFuH+fVv3<(;j+e1o4!&< zLkJD6_CThrkhzu&9?42)B7L!e$nc0)F{(DYL_G8ZaS=sOCHjr*gL<68UPB!)(s_DZ zXtHSa%|+2)68)O!rxU{A#=v5$W)HwFT7xBPP}4X4D7Z|nmMBIA%tZzIU2xzNP}XUB z))1!!!#Nr9leJCV&Ax89J;n?hgk4Y};zJT5Ve&b2z%w_3+AP5pUGN7aSz&HWoZM>ZX#U>@tH#ZDi2I zKmmA)^ihB+`z4>1aDpGQY=TI0k|)Ot@UX40!DC~iaH`%*rmH~)Y`RqTll>GsFI|pj n#u8z*4UTMD(meoG9%Eca@|3=+#Es#Ot*{2RozR=D9{q~n!ph`0 diff --git a/controller/controller.py b/controller/controller.py index a2c9a27..fec1b93 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -3,11 +3,15 @@ from AlipayTelemetry import * from LaptopKeyboard import * -myAlipay = Alipay("192.168.111.177") - +myAlipay = Alipay("192.168.86.22", 12346, "ALIPAY") +myIRBeacon = Alipay("192.168.86.27", 12347, "IRBeac1") + keyboard_thread = threading.Thread(target=lambda: Listener(on_press=on_press, on_release=on_release).start()) -receiver_thread = threading.Thread(target=myAlipay.udp_receiver) -receiver_thread.start() +receiver_thread1 = threading.Thread(target=myAlipay.udp_receiver) +receiver_thread2 = threading.Thread(target=myIRBeacon.udp_receiver) + +receiver_thread1.start() +receiver_thread2.start() keyboard_thread.start() drivecmd = 0 @@ -19,11 +23,11 @@ while (True): if get_key_state(Key.up): drivecmd = 1 - elif get_key_state(Key.right): + elif get_key_state(Key.left): drivecmd = 2 elif get_key_state(Key.down): drivecmd = 3 - elif get_key_state(Key.left): + elif get_key_state(Key.right): drivecmd = 4 else: drivecmd = 0 @@ -40,6 +44,6 @@ cmd = f"{drivecmd}{enabled}" myAlipay.send_udp_packet(cmd) - # print(f"Command: {cmd}") + myIRBeacon.send_udp_packet(f"{enabled}") time.sleep(0.05) \ No newline at end of file From f526f7e9feaf3939c2752daf876ef3f4e2028a4c Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Thu, 22 Feb 2024 13:41:01 -0800 Subject: [PATCH 06/48] wifi issues, not sure why --- .../lib/LaptopTelemetry/LaptopTelemetry.cpp | 7 +-- .../lib/LaptopTelemetry/LaptopTelemetry.h | 3 -- Alipay ESP32 Code/lib/Melty/melty.cpp | 16 +++--- Alipay ESP32 Code/lib/Melty/melty.h | 2 +- Alipay ESP32 Code/src/main.cpp | 50 +++++++++++++------ IR Beacon/src/main.cpp | 14 ++++-- controller/controller.py | 48 ++++++++++++++---- 7 files changed, 95 insertions(+), 45 deletions(-) diff --git a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp index e4fbe27..ca0e3b5 100644 --- a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp +++ b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp @@ -1,8 +1,9 @@ #include "LaptopTelemetry.h" WiFiUDP udp; -IPAddress laptopIpAddress(192, 168, 86, 25); +// IPAddress laptopIpAddress(192, 168, 86, 25); // IPAddress laptopIpAddress(192, 168, 111, 171); +IPAddress laptopIpAddress(192, 168, 175, 127); LaptopTelemetry::LaptopTelemetry(const char* _ssid, const char* _pswrd, char* _packetBuffer) { ssid = _ssid; @@ -42,7 +43,7 @@ void LaptopTelemetry::init() { } void LaptopTelemetry::send(const char* message) { - udp.beginPacket(laptopIpAddress, PORT); // Send to port 12346 + udp.beginPacket(laptopIpAddress, PORT); // Send to port udp.print(message); udp.endPacket(); } @@ -60,7 +61,7 @@ bool LaptopTelemetry::isDisconnected() { void LaptopTelemetry::receive() { size_t packetSize = udp.parsePacket(); if (packetSize) { // receive incoming UDP packets - // USBSerial.printf("Received %d bytes from %s, port %d\n", packetSize, udp.remoteIP().toString().c_str(), udp.remotePort()); + USBSerial.printf("Received %d bytes from %s, port %d\n", packetSize, udp.remoteIP().toString().c_str(), udp.remotePort()); int len = udp.read(packetBuffer, packetSize); if (len > 0) packetBuffer[len] = 0; diff --git a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.h b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.h index e365f58..522da5c 100644 --- a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.h +++ b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.h @@ -4,9 +4,6 @@ #define PORT 12346 #endif -// #ifdef WIFI_SETTINGS 0 - - #include #include diff --git a/Alipay ESP32 Code/lib/Melty/melty.cpp b/Alipay ESP32 Code/lib/Melty/melty.cpp index ae80b7c..90a995d 100644 --- a/Alipay ESP32 Code/lib/Melty/melty.cpp +++ b/Alipay ESP32 Code/lib/Melty/melty.cpp @@ -11,13 +11,13 @@ void melty::update() { if (curSeenIRLed) { // Activates on the rising edge of seeing the IR LED setLeds(CRGB::Green); currentPulse = millis(); - unsigned long deltaPulse = currentPulse - lastPulse; // How long it takes to complete one revolution - RPM = 60000/(deltaPulse); + unsigned long period_MS = currentPulse - lastPulse; // How long it takes to complete one revolution + RPM = 60000/(period_MS); lastPulse = currentPulse; - unsigned long midPulse = currentPulse + pulseWidth/2; // This should ideally be centered on the beacon - unsigned long centerOfDrivePulse = midPulse + (float(deg)/360)*deltaPulse; // Direction that we should be driving towards - unsigned long deltaDriveTiming = (percentageOfRotation * deltaPulse)/2; + unsigned long center_of_beacon = currentPulse + time_seen_beacon/2; // This should ideally be centered on the beacon + unsigned long centerOfDrivePulse = center_of_beacon + (float(deg)/360)*period_MS; // Direction that we should be driving towards + unsigned long deltaDriveTiming = (percentageOfRotation * period_MS)/2; if (timingToggle) { // If it is going to translate whilst computing the new timings we should use a different set of variables endDrive = centerOfDrivePulse + deltaDriveTiming; startDrive = centerOfDrivePulse - deltaDriveTiming; @@ -28,15 +28,15 @@ void melty::update() { timingToggle = !timingToggle; // Toggle it so that next iteration uses different variables } - else { + else { // Activates on the falling edge of seeing the IR LED setLeds(CRGB::Red); - pulseWidth = millis() - lastPulse; + time_seen_beacon = millis() - lastPulse; } lastSeenIRLed = curSeenIRLed; } -bool melty::translate() { +bool melty::translate() { // Returns whether or not robot should translate now unsigned long currentTime = millis(); if (percentageOfRotation != 0) return (currentTime > startDrive && currentTime < endDrive || currentTime > startDrive2 && currentTime < endDrive2); diff --git a/Alipay ESP32 Code/lib/Melty/melty.h b/Alipay ESP32 Code/lib/Melty/melty.h index 3476d6d..3d87246 100644 --- a/Alipay ESP32 Code/lib/Melty/melty.h +++ b/Alipay ESP32 Code/lib/Melty/melty.h @@ -18,7 +18,7 @@ class melty { bool lastSeenIRLed = 0; unsigned long currentPulse = millis(); unsigned long lastPulse = millis(); - unsigned long pulseWidth = millis(); + unsigned long time_seen_beacon = millis(); bool IRLedReadings[IRLedDataSize] = {0}; int IRLedIndex = 0; diff --git a/Alipay ESP32 Code/src/main.cpp b/Alipay ESP32 Code/src/main.cpp index 2dbb2e4..78528b1 100644 --- a/Alipay ESP32 Code/src/main.cpp +++ b/Alipay ESP32 Code/src/main.cpp @@ -7,12 +7,15 @@ #include #include -const char *ssid = "RESNET-BROTECTED"; -const char *pswrd = "marbry2025"; +// const char *ssid = "RESNET-BROTECTED"; +// const char *pswrd = "marbry2025"; // const char *ssid = "EnVision-Local"; // const char *pswrd = "thinkmakebreak"; +const char *ssid = "AlipayDevices"; +const char *pswrd = "alipay123"; + const int packSize = 3; char packetBuffer[packSize]; @@ -23,7 +26,7 @@ melty alipay = melty(); void setup() { - // delay(750); // Some reason my aliexpress esp32s3 needs this delay to give it enough time to initialize + delay(750); // Some reason my aliexpress esp32s3 needs this delay to give it enough time to initialize init_led(); init_mpu6050(); init_motors(); @@ -37,10 +40,9 @@ void setup() void loop() { myLaptop.receive(); - if (myLaptop.isDisconnected()) { set_both_motors(0); - setLeds(CRGB::Orange); + toggleLeds(CRGB::Red, CRGB::Black, 500); } else { if (packetBuffer[1] == '1') { // Currently enabled alipay.update(); @@ -50,16 +52,34 @@ void loop() } else { set_both_motors(8); } - - if (packetBuffer[0] == '1') // Check drive cmd - alipay.deg = 0; - else if (packetBuffer[0] == '2') - alipay.deg = 90; - else if (packetBuffer[0] == '3') - alipay.deg = 180; - else if (packetBuffer[0] == '4') - alipay.deg = 270; - + + switch (packetBuffer[0]) { // Check the drive cmd + case '1': + alipay.deg = 0; + break; + case '2': + alipay.deg = 45; + break; + case '3': + alipay.deg = 90; + break; + case '4': + alipay.deg = 135; + break; + case '5': + alipay.deg = 180; + break; + case '6': + alipay.deg = 225; + break; + case '7': + alipay.deg = 270; + break; + case '8': + alipay.deg = 315; + break; + } + if (packetBuffer[0] != '0') {// Check drive cmd alipay.percentageOfRotation = 0.5; } else { diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp index fe28faf..ea9c93a 100644 --- a/IR Beacon/src/main.cpp +++ b/IR Beacon/src/main.cpp @@ -5,8 +5,14 @@ #include #include -const char *ssid = "RESNET-BROTECTED"; -const char *pswrd = "marbry2025"; +// const char *ssid = "RESNET-BROTECTED"; +// const char *pswrd = "marbry2025"; + +// const char *ssid = "EnVision-Local"; +// const char *pswrd = "thinkmakebreak"; + +const char *ssid = "AlipayDevices"; +const char *pswrd = "alipay123"; const int packSize = 3; char packetBuffer[packSize]; @@ -37,9 +43,9 @@ void loop(){ myLaptop.receive(); if (myLaptop.isDisconnected()) { - setLeds(CRGB::Orange); + toggleLeds(CRGB::Red, CRGB::Black, 500); } else { - if (packetBuffer[0] == '1' && lastEnabledCmd == '0') { // Enabled + if (packetBuffer[0] == '1') { // Enabled toggleLeds(CRGB::Blue, CRGB::Purple, 500); ledcWrite(ledChannel, 100); } diff --git a/controller/controller.py b/controller/controller.py index fec1b93..3e0d417 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -3,15 +3,21 @@ from AlipayTelemetry import * from LaptopKeyboard import * -myAlipay = Alipay("192.168.86.22", 12346, "ALIPAY") -myIRBeacon = Alipay("192.168.86.27", 12347, "IRBeac1") +# myAlipay = Alipay("192.168.86.22", 12346, "ALIPAY") +# myIRBeacon = Alipay("192.168.86.27", 12347, "IRBeac1") + +# myAlipay = Alipay("192.168.111.171", 12346, "ALIPAY") +# myIRBeacon = Alipay("192.168.111.178", 12347, "IRBeac1") + +myAlipay = Alipay("192.168.175.221", 12346, "ALIPAY") +# myIRBeacon = Alipay("192.168.175.195", 12347, "IRBeac1") keyboard_thread = threading.Thread(target=lambda: Listener(on_press=on_press, on_release=on_release).start()) receiver_thread1 = threading.Thread(target=myAlipay.udp_receiver) -receiver_thread2 = threading.Thread(target=myIRBeacon.udp_receiver) +# receiver_thread2 = threading.Thread(target=myIRBeacon.udp_receiver) receiver_thread1.start() -receiver_thread2.start() +# receiver_thread2.start() keyboard_thread.start() drivecmd = 0 @@ -21,17 +27,36 @@ print(f"IP : {myAlipay.get_laptop_ip()}") while (True): + y = 0 + x = 0 if get_key_state(Key.up): + y = y + 1 + if get_key_state(Key.down): + y = y - 1 + if get_key_state(Key.left): + x = x - 1 + if get_key_state(Key.right): + x = x + 1 + + if x == 0 and y == 0: + drivecmd = 0 + elif x == 0 and y == 1: drivecmd = 1 - elif get_key_state(Key.left): + elif x == 1 and y == 1: drivecmd = 2 - elif get_key_state(Key.down): + elif x == 1 and y == 0: drivecmd = 3 - elif get_key_state(Key.right): + elif x == 1 and y == -1: drivecmd = 4 - else: - drivecmd = 0 - + elif x == 0 and y == -1: + drivecmd = 5 + elif x == -1 and y == -1: + drivecmd = 6 + elif x == -1 and y == 0: + drivecmd = 7 + elif x == -1 and y == 1: + drivecmd = 8 + if get_key_state(Key.space): if get_key_state(Key.ctrl): enabled = 1 @@ -43,7 +68,8 @@ waitForRelease = 0 cmd = f"{drivecmd}{enabled}" + print(cmd) myAlipay.send_udp_packet(cmd) - myIRBeacon.send_udp_packet(f"{enabled}") + # myIRBeacon.send_udp_packet(f"{enabled}") time.sleep(0.05) \ No newline at end of file From cb24d2f571de1077bfad9e8ceff035ee2a5ad485 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Thu, 22 Feb 2024 15:24:25 -0800 Subject: [PATCH 07/48] beacon not connector to envisionlocal --- .../lib/LaptopTelemetry/LaptopTelemetry.cpp | 6 +- Alipay ESP32 Code/src/main.cpp | 8 +-- IR Beacon/platformio.ini | 2 +- IR Beacon/src/main.cpp | 15 ++--- .../AlipayTelemetry.cpython-311.pyc | Bin 2614 -> 2614 bytes .../LaptopKeyboard.cpython-311.pyc | Bin 958 -> 958 bytes controller/controller.py | 16 ++--- mycontroller.py | 55 ++++++++++++++++++ 8 files changed, 75 insertions(+), 27 deletions(-) create mode 100644 mycontroller.py diff --git a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp index ca0e3b5..13c470a 100644 --- a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp +++ b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp @@ -2,8 +2,8 @@ WiFiUDP udp; // IPAddress laptopIpAddress(192, 168, 86, 25); -// IPAddress laptopIpAddress(192, 168, 111, 171); -IPAddress laptopIpAddress(192, 168, 175, 127); +IPAddress laptopIpAddress(192, 168, 111, 177); +// IPAddress laptopIpAddress(192, 168, 175, 127); LaptopTelemetry::LaptopTelemetry(const char* _ssid, const char* _pswrd, char* _packetBuffer) { ssid = _ssid; @@ -61,7 +61,7 @@ bool LaptopTelemetry::isDisconnected() { void LaptopTelemetry::receive() { size_t packetSize = udp.parsePacket(); if (packetSize) { // receive incoming UDP packets - USBSerial.printf("Received %d bytes from %s, port %d\n", packetSize, udp.remoteIP().toString().c_str(), udp.remotePort()); + // USBSerial.printf("Received %d bytes from %s, port %d\n", packetSize, udp.remoteIP().toString().c_str(), udp.remotePort()); int len = udp.read(packetBuffer, packetSize); if (len > 0) packetBuffer[len] = 0; diff --git a/Alipay ESP32 Code/src/main.cpp b/Alipay ESP32 Code/src/main.cpp index 78528b1..3ee1f4f 100644 --- a/Alipay ESP32 Code/src/main.cpp +++ b/Alipay ESP32 Code/src/main.cpp @@ -10,11 +10,11 @@ // const char *ssid = "RESNET-BROTECTED"; // const char *pswrd = "marbry2025"; -// const char *ssid = "EnVision-Local"; -// const char *pswrd = "thinkmakebreak"; +const char *ssid = "EnVision-Local"; +const char *pswrd = "thinkmakebreak"; -const char *ssid = "AlipayDevices"; -const char *pswrd = "alipay123"; +// const char *ssid = "AlipayDevices"; +// const char *pswrd = "alipay123"; const int packSize = 3; char packetBuffer[packSize]; diff --git a/IR Beacon/platformio.ini b/IR Beacon/platformio.ini index b699311..a21eec3 100644 --- a/IR Beacon/platformio.ini +++ b/IR Beacon/platformio.ini @@ -14,7 +14,7 @@ board = esp32-s3-devkitc-1 framework = arduino upload_speed = 921600 monitor_speed = 115200 -build_flags = -D IR_BEACON=1 +build_flags = -D IR_BEACON=1 lib_deps = fastled/FastLED@^3.6.0 lib_extra_dirs = /Users/mingweiyeoh/Documents/GitHub/Alipay/Alipay ESP32 Code/lib diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp index ea9c93a..d224dd2 100644 --- a/IR Beacon/src/main.cpp +++ b/IR Beacon/src/main.cpp @@ -8,11 +8,11 @@ // const char *ssid = "RESNET-BROTECTED"; // const char *pswrd = "marbry2025"; -// const char *ssid = "EnVision-Local"; -// const char *pswrd = "thinkmakebreak"; +const char *ssid = "EnVision-Local"; +const char *pswrd = "thinkmakebreak"; -const char *ssid = "AlipayDevices"; -const char *pswrd = "alipay123"; +// const char *ssid = "AlipayDevices"; +// const char *pswrd = "alipay123"; const int packSize = 3; char packetBuffer[packSize]; @@ -24,8 +24,6 @@ const int freq = 38000; const int ledChannel = 0; const int resolution = 8; -char lastEnabledCmd = '0'; - void setup(){ init_led(); USBSerial.begin(115200); @@ -53,11 +51,6 @@ void loop(){ toggleLeds(CRGB::Red, CRGB::Green, 500); ledcWrite(ledChannel, 0); } - - if (packetBuffer[2] == '1') // Sync led toggling - syncToggle(); - } - lastEnabledCmd = packetBuffer[0]; } \ No newline at end of file diff --git a/controller/__pycache__/AlipayTelemetry.cpython-311.pyc b/controller/__pycache__/AlipayTelemetry.cpython-311.pyc index 654dcab5fbf995a4696800a5b8a1d73ecacc817c..b7317a9b2be722489feb4a6ce1885ffb2e40e56f 100644 GIT binary patch delta 20 acmdlcvQ310IWI340}#ADdwnCfHWvUq`33C& delta 20 acmdlcvQ310IWI340}$w+y0(#9n+pIpumvjs diff --git a/controller/__pycache__/LaptopKeyboard.cpython-311.pyc b/controller/__pycache__/LaptopKeyboard.cpython-311.pyc index 6acf17919a25abf02ccd85bd8f8219e9b8ec4b73..c0cf76116ac85aea9dab3169c2517263b33fa6b7 100644 GIT binary patch delta 20 acmdnTzK@-IIWI340}#ADdwnDKW@Z3AUIt_U delta 20 acmdnTzK@-IIWI340}upEzqXNkGcy1;$_0=B diff --git a/controller/controller.py b/controller/controller.py index 3e0d417..ba5b663 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -6,18 +6,18 @@ # myAlipay = Alipay("192.168.86.22", 12346, "ALIPAY") # myIRBeacon = Alipay("192.168.86.27", 12347, "IRBeac1") -# myAlipay = Alipay("192.168.111.171", 12346, "ALIPAY") -# myIRBeacon = Alipay("192.168.111.178", 12347, "IRBeac1") +myAlipay = Alipay("192.168.111.171", 12346, "ALIPAY") +myIRBeacon = Alipay("192.168.111.178", 12347, "IRBeac1") -myAlipay = Alipay("192.168.175.221", 12346, "ALIPAY") +# myAlipay = Alipay("192.168.175.221", 12346, "ALIPAY") # myIRBeacon = Alipay("192.168.175.195", 12347, "IRBeac1") keyboard_thread = threading.Thread(target=lambda: Listener(on_press=on_press, on_release=on_release).start()) receiver_thread1 = threading.Thread(target=myAlipay.udp_receiver) -# receiver_thread2 = threading.Thread(target=myIRBeacon.udp_receiver) +receiver_thread2 = threading.Thread(target=myIRBeacon.udp_receiver) receiver_thread1.start() -# receiver_thread2.start() +receiver_thread2.start() keyboard_thread.start() drivecmd = 0 @@ -68,8 +68,8 @@ waitForRelease = 0 cmd = f"{drivecmd}{enabled}" - print(cmd) + # print(cmd) + myIRBeacon.send_udp_packet(f"{enabled}") myAlipay.send_udp_packet(cmd) - # myIRBeacon.send_udp_packet(f"{enabled}") time.sleep(0.05) - \ No newline at end of file + \ No newline at end of file diff --git a/mycontroller.py b/mycontroller.py new file mode 100644 index 0000000..8e76dea --- /dev/null +++ b/mycontroller.py @@ -0,0 +1,55 @@ +import socket +import time +import threading +import keyboard + + +# IP address and port of the Arduino-ESP32 +alipay_address = ("192.168.111.171", 12345) + +def udp_receiver(): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(("0.0.0.0", 12346)) + + while True: + data, addr = sock.recvfrom(64) + try: + data_str = data.decode("utf-8") + print(f"[ALIPAY] {data_str}") + except: + pass + +def send_udp_packet(data, address): + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 128) + sock.sendto(data.encode(), address) + +receiver_thread = threading.Thread(target=udp_receiver) +receiver_thread.daemon = True # This allows the thread to exit when the main thread exits +receiver_thread.start() + +drivecmd = 0 +enabled = 0 +while (True): + if keyboard.is_pressed("up"): + drivecmd = 1 + elif keyboard.is_pressed("right"): + drivecmd = 2 + elif keyboard.is_pressed("down"): + drivecmd = 3 + elif keyboard.is_pressed("left"): + drivecmd = 4 + else: + drivecmd = 0 + + if keyboard.is_pressed('space'): + if keyboard.is_pressed('tab'): + enabled = 1 + else: + enabled = 0 + + cmd = f"{drivecmd}{int(enabled)}" + send_udp_packet(cmd, alipay_address) + # print(f"Sent Command: {cmd}") + time.sleep(0.05) + \ No newline at end of file From 15a87ab780f769e2f30c0806f9699e258e7494da Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Thu, 22 Feb 2024 17:25:27 -0800 Subject: [PATCH 08/48] works at home, turns out its just envision's wifi --- .../lib/LaptopTelemetry/LaptopTelemetry.cpp | 18 +++++++++--------- IR Beacon/src/main.cpp | 8 ++++---- controller/controller.py | 8 ++++---- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp index 13c470a..772776b 100644 --- a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp +++ b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp @@ -1,8 +1,8 @@ #include "LaptopTelemetry.h" WiFiUDP udp; -// IPAddress laptopIpAddress(192, 168, 86, 25); -IPAddress laptopIpAddress(192, 168, 111, 177); +IPAddress laptopIpAddress(192, 168, 86, 25); +// IPAddress laptopIpAddress(192, 168, 111, 177); // IPAddress laptopIpAddress(192, 168, 175, 127); LaptopTelemetry::LaptopTelemetry(const char* _ssid, const char* _pswrd, char* _packetBuffer) { @@ -16,27 +16,27 @@ void LaptopTelemetry::init() { WiFi.begin(ssid, pswrd); WiFi.setTxPower(WIFI_POWER_8_5dBm); wl_status_t wifistat = WiFi.status(); - while (wifistat != WL_CONNECTED) - { + while (wifistat != WL_CONNECTED) { wifistat = WiFi.status(); switch (wifistat) { - case WL_NO_SSID_AVAIL: + case WL_NO_SSID_AVAIL: USBSerial.println("[WiFi] SSID not found"); break; - case WL_CONNECT_FAILED: + case WL_CONNECT_FAILED: USBSerial.print("[WiFi] Failed - WiFi not connected! Reason: "); break; - case WL_CONNECTION_LOST: + case WL_CONNECTION_LOST: USBSerial.println("[WiFi] Connection was lost"); break; - case WL_CONNECTED: + case WL_CONNECTED: USBSerial.println("[WiFi] WiFi is connected!"); USBSerial.print("[WiFi] IP address: "); USBSerial.println(WiFi.localIP()); udp.begin(12345); // Port that ESP32 will receive commands on break; - default: + default: USBSerial.print("."); + break; } delay(100); } diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp index d224dd2..0030bab 100644 --- a/IR Beacon/src/main.cpp +++ b/IR Beacon/src/main.cpp @@ -5,11 +5,11 @@ #include #include -// const char *ssid = "RESNET-BROTECTED"; -// const char *pswrd = "marbry2025"; +const char *ssid = "RESNET-BROTECTED"; +const char *pswrd = "marbry2025"; -const char *ssid = "EnVision-Local"; -const char *pswrd = "thinkmakebreak"; +// const char *ssid = "EnVision-Local"; +// const char *pswrd = "thinkmakebreak"; // const char *ssid = "AlipayDevices"; // const char *pswrd = "alipay123"; diff --git a/controller/controller.py b/controller/controller.py index ba5b663..7763daa 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -3,11 +3,11 @@ from AlipayTelemetry import * from LaptopKeyboard import * -# myAlipay = Alipay("192.168.86.22", 12346, "ALIPAY") -# myIRBeacon = Alipay("192.168.86.27", 12347, "IRBeac1") +myAlipay = Alipay("192.168.86.22", 12346, "ALIPAY") +myIRBeacon = Alipay("192.168.86.27", 12347, "IRBeac1") -myAlipay = Alipay("192.168.111.171", 12346, "ALIPAY") -myIRBeacon = Alipay("192.168.111.178", 12347, "IRBeac1") +# myAlipay = Alipay("192.168.111.171", 12346, "ALIPAY") +# myIRBeacon = Alipay("192.168.111.178", 12347, "IRBeac1") # myAlipay = Alipay("192.168.175.221", 12346, "ALIPAY") # myIRBeacon = Alipay("192.168.175.195", 12347, "IRBeac1") From c5c9fc104dfee1e422b09ce38bfd63d2df5fca06 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Sat, 24 Feb 2024 14:30:04 -0800 Subject: [PATCH 09/48] before implementing BLE to IR beacon --- .../lib/LaptopTelemetry/LaptopTelemetry.cpp | 4 +- Alipay ESP32 Code/src/main.cpp | 8 +- ESP32 Mac Address/.gitignore | 5 ++ ESP32 Mac Address/.vscode/extensions.json | 10 +++ ESP32 Mac Address/include/README | 39 ++++++++++ ESP32 Mac Address/lib/README | 46 +++++++++++ ESP32 Mac Address/platformio.ini | 16 ++++ ESP32 Mac Address/src/main.cpp | 15 ++++ ESP32 Mac Address/test/README | 11 +++ IR Beacon/src/main.cpp | 10 +-- bluetooth scanner | 9 +++ bluetooth testing.py | 76 +++++++++++++++++++ controller/controller.py | 8 +- mycontroller.py | 55 -------------- 14 files changed, 242 insertions(+), 70 deletions(-) create mode 100644 ESP32 Mac Address/.gitignore create mode 100644 ESP32 Mac Address/.vscode/extensions.json create mode 100644 ESP32 Mac Address/include/README create mode 100644 ESP32 Mac Address/lib/README create mode 100644 ESP32 Mac Address/platformio.ini create mode 100644 ESP32 Mac Address/src/main.cpp create mode 100644 ESP32 Mac Address/test/README create mode 100644 bluetooth scanner create mode 100644 bluetooth testing.py delete mode 100644 mycontroller.py diff --git a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp index 772776b..c7df133 100644 --- a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp +++ b/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp @@ -1,9 +1,9 @@ #include "LaptopTelemetry.h" WiFiUDP udp; -IPAddress laptopIpAddress(192, 168, 86, 25); +// IPAddress laptopIpAddress(192, 168, 86, 25); // IPAddress laptopIpAddress(192, 168, 111, 177); -// IPAddress laptopIpAddress(192, 168, 175, 127); +IPAddress laptopIpAddress(192, 168, 175, 127); LaptopTelemetry::LaptopTelemetry(const char* _ssid, const char* _pswrd, char* _packetBuffer) { ssid = _ssid; diff --git a/Alipay ESP32 Code/src/main.cpp b/Alipay ESP32 Code/src/main.cpp index 3ee1f4f..78528b1 100644 --- a/Alipay ESP32 Code/src/main.cpp +++ b/Alipay ESP32 Code/src/main.cpp @@ -10,11 +10,11 @@ // const char *ssid = "RESNET-BROTECTED"; // const char *pswrd = "marbry2025"; -const char *ssid = "EnVision-Local"; -const char *pswrd = "thinkmakebreak"; +// const char *ssid = "EnVision-Local"; +// const char *pswrd = "thinkmakebreak"; -// const char *ssid = "AlipayDevices"; -// const char *pswrd = "alipay123"; +const char *ssid = "AlipayDevices"; +const char *pswrd = "alipay123"; const int packSize = 3; char packetBuffer[packSize]; diff --git a/ESP32 Mac Address/.gitignore b/ESP32 Mac Address/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/ESP32 Mac Address/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/ESP32 Mac Address/.vscode/extensions.json b/ESP32 Mac Address/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/ESP32 Mac Address/.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/ESP32 Mac Address/include/README b/ESP32 Mac Address/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/ESP32 Mac Address/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/ESP32 Mac Address/lib/README b/ESP32 Mac Address/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/ESP32 Mac Address/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 +#include + +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/ESP32 Mac Address/platformio.ini b/ESP32 Mac Address/platformio.ini new file mode 100644 index 0000000..f028691 --- /dev/null +++ b/ESP32 Mac Address/platformio.ini @@ -0,0 +1,16 @@ +; 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-s3-devkitc-1] +platform = espressif32 +board = esp32-s3-devkitc-1 +framework = arduino +upload_speed = 921600 +monitor_speed = 115200 diff --git a/ESP32 Mac Address/src/main.cpp b/ESP32 Mac Address/src/main.cpp new file mode 100644 index 0000000..c840377 --- /dev/null +++ b/ESP32 Mac Address/src/main.cpp @@ -0,0 +1,15 @@ +#include + +void setup() { + // put your setup code here, to run once: + USBSerial.begin(115200); + USBSerial.println(); + USBSerial.print("ESP Board MAC Address: "); + USBSerial.println(WiFi.macAddress()); +} + +void loop() { + delay(1000); + USBSerial.println(WiFi.macAddress()); + // put your main code here, to run repeatedly: +} \ No newline at end of file diff --git a/ESP32 Mac Address/test/README b/ESP32 Mac Address/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/ESP32 Mac Address/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 diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp index 0030bab..45355a1 100644 --- a/IR Beacon/src/main.cpp +++ b/IR Beacon/src/main.cpp @@ -5,14 +5,14 @@ #include #include -const char *ssid = "RESNET-BROTECTED"; -const char *pswrd = "marbry2025"; +// const char *ssid = "RESNET-BROTECTED"; +// const char *pswrd = "marbry2025"; // const char *ssid = "EnVision-Local"; // const char *pswrd = "thinkmakebreak"; -// const char *ssid = "AlipayDevices"; -// const char *pswrd = "alipay123"; +const char *ssid = "AlipayDevices"; +const char *pswrd = "alipay123"; const int packSize = 3; char packetBuffer[packSize]; @@ -45,7 +45,7 @@ void loop(){ } else { if (packetBuffer[0] == '1') { // Enabled toggleLeds(CRGB::Blue, CRGB::Purple, 500); - ledcWrite(ledChannel, 100); + ledcWrite(ledChannel, 80); } if (packetBuffer[0] == '0') { // Disabled toggleLeds(CRGB::Red, CRGB::Green, 500); diff --git a/bluetooth scanner b/bluetooth scanner new file mode 100644 index 0000000..c110f51 --- /dev/null +++ b/bluetooth scanner @@ -0,0 +1,9 @@ +import asyncio +from bleak import discover + +async def scan(): + devices = await discover() + for device in devices: + print(device) + +asyncio.run(scan()) \ No newline at end of file diff --git a/bluetooth testing.py b/bluetooth testing.py new file mode 100644 index 0000000..6226892 --- /dev/null +++ b/bluetooth testing.py @@ -0,0 +1,76 @@ +import asyncio +import sys + +from bleak import BleakScanner, BleakClient +from bleak.backends.scanner import AdvertisementData +from bleak.backends.device import BLEDevice + +# https://github.com/hbldh/bleak/blob/develop/examples/uart_service.py + +class BLE_UART: + + UART_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" + UART_RX_CHAR_UUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" + UART_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" + + # All BLE devices have MTU of at least 23. Subtracting 3 bytes overhead, we can + # safely send 20 bytes at a time to any device supporting this service. + UART_SAFE_SIZE = 20 + + def __init__(self, peripheral_name='ESP32_BLE'): + self._peripheral_name = peripheral_name + self._rx_queue = asyncio.Queue() + + async def read(self): + msg = await self._rx_queue.get() + return msg + + async def write(self, msg): + if isinstance(msg, str): + msg = msg.encode() + await self._client.write_gatt_char(self.UART_RX_CHAR_UUID, msg) + + async def connect(self): + self._discovery_queue = asyncio.Queue() + device = None + print(f"scanning for {self._peripheral_name}") + async with BleakScanner(detection_callback=self._find_uart_device): + device: BLEDevice = await self._discovery_queue.get() + print(f"connecting to {self._peripheral_name} ...", end="") + client = self._client = BleakClient(device, disconnected_callback=self._handle_disconnect) + await client.connect() + await client.start_notify(self.UART_TX_CHAR_UUID, self._rx_handler) + print(f" connected") + + async def disconnect(self): + await self._client.disconnect() + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + await self.disconnect() + + def _rx_handler(self, _: int, data: bytearray): + self._rx_queue.put_nowait(data) + + def _find_uart_device(self, device: BLEDevice, adv: AdvertisementData): + # called whenever a device is detected during discovery + # ignore all but target device + if device.name == self._peripheral_name: + self._discovery_queue.put_nowait(device) + + def _handle_disconnect(self, _: BleakClient): + self._rx_queue.put_nowait(None) + +async def main(): + async with BLE_UART() as uart: + await uart.connect() + + for i in range(3): + msg = await uart.read() + # msg = msg.decode() + print("received", msg) + await uart.write(f"echo {msg}") + +asyncio.run(main()) \ No newline at end of file diff --git a/controller/controller.py b/controller/controller.py index 7763daa..7fc2937 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -3,14 +3,14 @@ from AlipayTelemetry import * from LaptopKeyboard import * -myAlipay = Alipay("192.168.86.22", 12346, "ALIPAY") -myIRBeacon = Alipay("192.168.86.27", 12347, "IRBeac1") +# myAlipay = Alipay("192.168.86.22", 12346, "ALIPAY") +# myIRBeacon = Alipay("192.168.86.27", 12347, "IRBeac1") # myAlipay = Alipay("192.168.111.171", 12346, "ALIPAY") # myIRBeacon = Alipay("192.168.111.178", 12347, "IRBeac1") -# myAlipay = Alipay("192.168.175.221", 12346, "ALIPAY") -# myIRBeacon = Alipay("192.168.175.195", 12347, "IRBeac1") +myAlipay = Alipay("192.168.175.221", 12346, "ALIPAY") +myIRBeacon = Alipay("192.168.175.195", 12347, "IRBeac1") keyboard_thread = threading.Thread(target=lambda: Listener(on_press=on_press, on_release=on_release).start()) receiver_thread1 = threading.Thread(target=myAlipay.udp_receiver) diff --git a/mycontroller.py b/mycontroller.py deleted file mode 100644 index 8e76dea..0000000 --- a/mycontroller.py +++ /dev/null @@ -1,55 +0,0 @@ -import socket -import time -import threading -import keyboard - - -# IP address and port of the Arduino-ESP32 -alipay_address = ("192.168.111.171", 12345) - -def udp_receiver(): - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.bind(("0.0.0.0", 12346)) - - while True: - data, addr = sock.recvfrom(64) - try: - data_str = data.decode("utf-8") - print(f"[ALIPAY] {data_str}") - except: - pass - -def send_udp_packet(data, address): - with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: - sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 128) - sock.sendto(data.encode(), address) - -receiver_thread = threading.Thread(target=udp_receiver) -receiver_thread.daemon = True # This allows the thread to exit when the main thread exits -receiver_thread.start() - -drivecmd = 0 -enabled = 0 -while (True): - if keyboard.is_pressed("up"): - drivecmd = 1 - elif keyboard.is_pressed("right"): - drivecmd = 2 - elif keyboard.is_pressed("down"): - drivecmd = 3 - elif keyboard.is_pressed("left"): - drivecmd = 4 - else: - drivecmd = 0 - - if keyboard.is_pressed('space'): - if keyboard.is_pressed('tab'): - enabled = 1 - else: - enabled = 0 - - cmd = f"{drivecmd}{int(enabled)}" - send_udp_packet(cmd, alipay_address) - # print(f"Sent Command: {cmd}") - time.sleep(0.05) - \ No newline at end of file From 57500164537816019e0a7e1f0b018951203cbcf7 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Sat, 24 Feb 2024 20:25:53 -0800 Subject: [PATCH 10/48] finished espnow class --- IR Beacon/lib/BLE_Uart/BLE_Uart.cpp | 65 +++++++++++++++++++ IR Beacon/lib/BLE_Uart/BLE_Uart.h | 15 +++++ IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp | 47 ++++++++++++++ IR Beacon/lib/esp_now_txrx/esp_now_txrx.h | 16 +++++ IR Beacon/platformio.ini | 10 ++- IR Beacon/src/main.cpp | 69 ++++++++++++--------- bluetooth scanner | 7 ++- bluetooth testing.py | 4 +- 8 files changed, 194 insertions(+), 39 deletions(-) create mode 100644 IR Beacon/lib/BLE_Uart/BLE_Uart.cpp create mode 100644 IR Beacon/lib/BLE_Uart/BLE_Uart.h create mode 100644 IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp create mode 100644 IR Beacon/lib/esp_now_txrx/esp_now_txrx.h diff --git a/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp b/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp new file mode 100644 index 0000000..577f80c --- /dev/null +++ b/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp @@ -0,0 +1,65 @@ +#include + +BLEServer *pServer = NULL; +BLECharacteristic * pTxCharacteristic; +bool deviceConnected = false; +bool oldDeviceConnected = false; + +char* packetBuffer; +int packSize = 6; + +class MyServerCallbacks: public BLEServerCallbacks { + void onConnect(BLEServer* pServer) { + deviceConnected = true; + } + + void onDisconnect(BLEServer* pServer) { + deviceConnected = false; + } +}; + +class MyCallbacks: public BLECharacteristicCallbacks { + void onWrite(BLECharacteristic *pCharacteristic) { + std::string rxValue = pCharacteristic->getValue(); + + if (rxValue.length() > 0) { + for (int i = 0; i < packSize; i++) { + packetBuffer[i] = rxValue[i]; + // USBSerial.print(rxValue[i]); + } + // USBSerial.println(); + } + } +}; + +BLE_Uart::BLE_Uart(char* _packetBuffer, int _packSize) { + packetBuffer = _packetBuffer; + packSize = _packSize; +} + +void BLE_Uart::init_ble() { + BLEDevice::setDeviceName("ESP32_BLE"); // Name wont change for one of the S3s for some reason. + BLEDevice::init("ESP32_BLE"); + + pServer = BLEDevice::createServer(); // Create the BLE Server + pServer->setCallbacks(new MyServerCallbacks()); + + BLEService *pService = pServer->createService(SERVICE_UUID); // Create the BLE Service= + + pTxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_TX,NIMBLE_PROPERTY::NOTIFY); // Create a BLE Characteristic + + BLECharacteristic * pRxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_RX,NIMBLE_PROPERTY::WRITE); + + pRxCharacteristic->setCallbacks(new MyCallbacks()); + + // Start the service + pService->start(); + + // Start advertising + pServer->getAdvertising()->start(); +} + +void BLE_Uart::send(String message) { + pTxCharacteristic->setValue(message); + pTxCharacteristic->notify(); +} diff --git a/IR Beacon/lib/BLE_Uart/BLE_Uart.h b/IR Beacon/lib/BLE_Uart/BLE_Uart.h new file mode 100644 index 0000000..ceda8d4 --- /dev/null +++ b/IR Beacon/lib/BLE_Uart/BLE_Uart.h @@ -0,0 +1,15 @@ +#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" // UART service UUID +#define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" +#define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" + +#include +#include + +class BLE_Uart { + public: + BLE_Uart(char* _packetBuffer, int _packSize); + void init_ble(); + void send(String message); + private: + +}; \ No newline at end of file diff --git a/IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp b/IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp new file mode 100644 index 0000000..7b49d05 --- /dev/null +++ b/IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp @@ -0,0 +1,47 @@ +#include + +esp_now_peer_info_t peerInfo; + +ESP_NOW_TXRX::ESP_NOW_TXRX(uint8_t* _receiver_address, int _packetSize) { + memcpy(peerInfo.peer_addr, _receiver_address, 6); + receiver_address = _receiver_address; + // packetBuffer = _packetBuffer; + packetSize = _packetSize; +} + +void ESP_NOW_TXRX::getMacAddress() { + USBSerial.println(WiFi.macAddress()); +} + +void ESP_NOW_TXRX::send(const uint8_t *bytes) { + esp_now_send(receiver_address, bytes, packetSize); +} + +void ESP_NOW_TXRX::init(esp_now_recv_cb_t receiv_cb) { + WiFi.mode(WIFI_STA); + if (esp_now_init() != ESP_OK) { + USBSerial.println("Error initializing ESP-NOW"); + return; + } + int x = 10; + esp_now_register_recv_cb(receiv_cb); //(const uint8_t *mac, const uint8_t *incomingData, int len); + // memcpy(this->packetBuffer, incomingData, len); + // x = 3; + // USBSerial.print("Bytes received: "); + // USBSerial.println(len); + // USBSerial.print("Message: "); + + // for (int i = 0; i < len; i++) + // USBSerial.print(incomingData[i]); + // USBSerial.println(); + + + // memcpy(peerInfo.peer_addr, receiver_address, 6); + peerInfo.channel = 0; + peerInfo.encrypt = false; + + if (esp_now_add_peer(&peerInfo) != ESP_OK){ + USBSerial.println("Failed to add peer"); + return; + } +} diff --git a/IR Beacon/lib/esp_now_txrx/esp_now_txrx.h b/IR Beacon/lib/esp_now_txrx/esp_now_txrx.h new file mode 100644 index 0000000..7f70ecd --- /dev/null +++ b/IR Beacon/lib/esp_now_txrx/esp_now_txrx.h @@ -0,0 +1,16 @@ +#include +#include + +class ESP_NOW_TXRX { + public: + ESP_NOW_TXRX(uint8_t *_receiver_address, int _packetSize); + void init(esp_now_recv_cb_t receiv_cb); + void getMacAddress(); + void send(const uint8_t *bytes); + + private: + // uint8_t* packetBuffer; + int packetSize; + uint8_t *receiver_address; + // void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len); +}; \ No newline at end of file diff --git a/IR Beacon/platformio.ini b/IR Beacon/platformio.ini index a21eec3..1d45095 100644 --- a/IR Beacon/platformio.ini +++ b/IR Beacon/platformio.ini @@ -14,7 +14,11 @@ board = esp32-s3-devkitc-1 framework = arduino upload_speed = 921600 monitor_speed = 115200 -build_flags = -D IR_BEACON=1 -lib_deps = fastled/FastLED@^3.6.0 +upload_port = /dev/tty.usbmodem1101 +monitor_port = /dev/tty.usbmodem1101 +build_flags = -D IR_BEACON=1 +lib_deps = + fastled/FastLED@^3.6.0 + h2zero/NimBLE-Arduino@^1.4.1 lib_extra_dirs = - /Users/mingweiyeoh/Documents/GitHub/Alipay/Alipay ESP32 Code/lib + /Users/mingweiyeoh/Documents/GitHub/Alipay/Alipay ESP32 Code/lib diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp index 45355a1..27b0fa3 100644 --- a/IR Beacon/src/main.cpp +++ b/IR Beacon/src/main.cpp @@ -3,54 +3,61 @@ #include #include "USB.h" #include -#include +#include +#include -// const char *ssid = "RESNET-BROTECTED"; -// const char *pswrd = "marbry2025"; - -// const char *ssid = "EnVision-Local"; -// const char *pswrd = "thinkmakebreak"; - -const char *ssid = "AlipayDevices"; -const char *pswrd = "alipay123"; - -const int packSize = 3; -char packetBuffer[packSize]; - -LaptopTelemetry myLaptop = LaptopTelemetry(ssid, pswrd, packetBuffer); +const int packSize = 6; +uint8_t mypacketBuffer[packSize]; const int IRLedPin = 4; const int freq = 38000; const int ledChannel = 0; const int resolution = 8; +// Callback when data is received +void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) { + // struct_message myData; + // memcpy(&myData, incomingData, sizeof(myData)); + USBSerial.print("Bytes received: "); + USBSerial.println(len); + USBSerial.print("Message: "); + // USBSerial.println(myData.a); + + for (int i = 0; i < len; i++) + USBSerial.print(incomingData[i]); + USBSerial.println(); +} + +// BLE_Uart myBLEUart = BLE_Uart(mypacketBuffer, packSize); + +uint8_t mac_addy[] = {0x48, 0x27, 0xE2, 0xD2, 0xC3, 0x94}; +ESP_NOW_TXRX myESP32 = ESP_NOW_TXRX(mac_addy,packSize); + void setup(){ init_led(); USBSerial.begin(115200); + delay(1000); ledcSetup(ledChannel, freq, resolution); ledcAttachPin(IRLedPin, ledChannel); - delay(500); - setLeds(CRGB::Green); - myLaptop.init(); + setLeds(CRGB::Green); + myESP32.init(OnDataRecv); + // myBLEUart.init_ble(); setLeds(CRGB::Black); } void loop(){ - myLaptop.receive(); - - if (myLaptop.isDisconnected()) { - toggleLeds(CRGB::Red, CRGB::Black, 500); - } else { - if (packetBuffer[0] == '1') { // Enabled - toggleLeds(CRGB::Blue, CRGB::Purple, 500); - ledcWrite(ledChannel, 80); - } - if (packetBuffer[0] == '0') { // Disabled - toggleLeds(CRGB::Red, CRGB::Green, 500); - ledcWrite(ledChannel, 0); - } - } + delay(1000); + uint8_t mydata[] = {0x03, 0x02}; + myESP32.send(mydata); + + + // myBLEUart.send("hello"); + // for (int i = 0; i < 6; i ++) { + // USBSerial.print(mypacketBuffer[i]); + // } + // USBSerial.println(); + } \ No newline at end of file diff --git a/bluetooth scanner b/bluetooth scanner index c110f51..653ba3a 100644 --- a/bluetooth scanner +++ b/bluetooth scanner @@ -1,9 +1,10 @@ import asyncio -from bleak import discover +from bleak import BleakScanner async def scan(): - devices = await discover() + devices = await BleakScanner.discover(timeout=1) for device in devices: - print(device) + if device.name is not None: + print(device) asyncio.run(scan()) \ No newline at end of file diff --git a/bluetooth testing.py b/bluetooth testing.py index 6226892..3af0366 100644 --- a/bluetooth testing.py +++ b/bluetooth testing.py @@ -69,8 +69,8 @@ async def main(): for i in range(3): msg = await uart.read() - # msg = msg.decode() + msg = msg.decode('utf-8') print("received", msg) - await uart.write(f"echo {msg}") + await uart.write(f"{msg}") asyncio.run(main()) \ No newline at end of file From bb92e9c1a1f84a385ac0844a6f9145d2e2d4dfae Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Sat, 24 Feb 2024 20:45:48 -0800 Subject: [PATCH 11/48] before fixing python code --- IR Beacon/.vscode/settings.json | 5 ++++ IR Beacon/lib/BLE_Uart/BLE_Uart.cpp | 8 +++--- IR Beacon/lib/BLE_Uart/BLE_Uart.h | 4 +-- IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp | 14 +---------- IR Beacon/lib/esp_now_txrx/esp_now_txrx.h | 2 -- IR Beacon/src/main.cpp | 28 ++++++++++----------- bluetooth testing.py | 7 +----- 7 files changed, 27 insertions(+), 41 deletions(-) create mode 100644 IR Beacon/.vscode/settings.json diff --git a/IR Beacon/.vscode/settings.json b/IR Beacon/.vscode/settings.json new file mode 100644 index 0000000..2018d0a --- /dev/null +++ b/IR Beacon/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "system_error": "cpp" + } +} \ No newline at end of file diff --git a/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp b/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp index 577f80c..a692c91 100644 --- a/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp +++ b/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp @@ -5,7 +5,7 @@ BLECharacteristic * pTxCharacteristic; bool deviceConnected = false; bool oldDeviceConnected = false; -char* packetBuffer; +uint8_t* packetBuffer; int packSize = 6; class MyServerCallbacks: public BLEServerCallbacks { @@ -32,7 +32,7 @@ class MyCallbacks: public BLECharacteristicCallbacks { } }; -BLE_Uart::BLE_Uart(char* _packetBuffer, int _packSize) { +BLE_Uart::BLE_Uart(uint8_t* _packetBuffer, int _packSize) { packetBuffer = _packetBuffer; packSize = _packSize; } @@ -59,7 +59,7 @@ void BLE_Uart::init_ble() { pServer->getAdvertising()->start(); } -void BLE_Uart::send(String message) { - pTxCharacteristic->setValue(message); +void BLE_Uart::send(uint8_t *msg) { + pTxCharacteristic->setValue(msg, packSize); pTxCharacteristic->notify(); } diff --git a/IR Beacon/lib/BLE_Uart/BLE_Uart.h b/IR Beacon/lib/BLE_Uart/BLE_Uart.h index ceda8d4..a48d901 100644 --- a/IR Beacon/lib/BLE_Uart/BLE_Uart.h +++ b/IR Beacon/lib/BLE_Uart/BLE_Uart.h @@ -7,9 +7,9 @@ class BLE_Uart { public: - BLE_Uart(char* _packetBuffer, int _packSize); + BLE_Uart(uint8_t* _packetBuffer, int _packSize); void init_ble(); - void send(String message); + void send(uint8_t *msg); private: }; \ No newline at end of file diff --git a/IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp b/IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp index 7b49d05..73d66ee 100644 --- a/IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp +++ b/IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp @@ -23,20 +23,8 @@ void ESP_NOW_TXRX::init(esp_now_recv_cb_t receiv_cb) { USBSerial.println("Error initializing ESP-NOW"); return; } - int x = 10; - esp_now_register_recv_cb(receiv_cb); //(const uint8_t *mac, const uint8_t *incomingData, int len); - // memcpy(this->packetBuffer, incomingData, len); - // x = 3; - // USBSerial.print("Bytes received: "); - // USBSerial.println(len); - // USBSerial.print("Message: "); - - // for (int i = 0; i < len; i++) - // USBSerial.print(incomingData[i]); - // USBSerial.println(); - - // memcpy(peerInfo.peer_addr, receiver_address, 6); + esp_now_register_recv_cb(receiv_cb); peerInfo.channel = 0; peerInfo.encrypt = false; diff --git a/IR Beacon/lib/esp_now_txrx/esp_now_txrx.h b/IR Beacon/lib/esp_now_txrx/esp_now_txrx.h index 7f70ecd..f9c8505 100644 --- a/IR Beacon/lib/esp_now_txrx/esp_now_txrx.h +++ b/IR Beacon/lib/esp_now_txrx/esp_now_txrx.h @@ -9,8 +9,6 @@ class ESP_NOW_TXRX { void send(const uint8_t *bytes); private: - // uint8_t* packetBuffer; int packetSize; uint8_t *receiver_address; - // void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len); }; \ No newline at end of file diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp index 27b0fa3..e6cae97 100644 --- a/IR Beacon/src/main.cpp +++ b/IR Beacon/src/main.cpp @@ -7,7 +7,9 @@ #include const int packSize = 6; -uint8_t mypacketBuffer[packSize]; +uint8_t laptop_packetBuffer[packSize]; + +uint8_t esp_now_packetBuffer[packSize]; const int IRLedPin = 4; const int freq = 38000; @@ -16,19 +18,17 @@ const int resolution = 8; // Callback when data is received void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) { - // struct_message myData; - // memcpy(&myData, incomingData, sizeof(myData)); + memcpy(esp_now_packetBuffer, incomingData, packSize); USBSerial.print("Bytes received: "); USBSerial.println(len); USBSerial.print("Message: "); - // USBSerial.println(myData.a); for (int i = 0; i < len; i++) USBSerial.print(incomingData[i]); USBSerial.println(); } -// BLE_Uart myBLEUart = BLE_Uart(mypacketBuffer, packSize); +BLE_Uart myBLEUart = BLE_Uart(laptop_packetBuffer, packSize); uint8_t mac_addy[] = {0x48, 0x27, 0xE2, 0xD2, 0xC3, 0x94}; ESP_NOW_TXRX myESP32 = ESP_NOW_TXRX(mac_addy,packSize); @@ -41,8 +41,8 @@ void setup(){ ledcAttachPin(IRLedPin, ledChannel); setLeds(CRGB::Green); - myESP32.init(OnDataRecv); - // myBLEUart.init_ble(); + // myESP32.init(OnDataRecv); + myBLEUart.init_ble(); setLeds(CRGB::Black); } @@ -50,14 +50,14 @@ void setup(){ void loop(){ delay(1000); - uint8_t mydata[] = {0x03, 0x02}; - myESP32.send(mydata); + uint8_t mydata[] = {0x03, 0x02, 0x00, 0x00, 0x00, 0x00}; + // myESP32.send(mydata); - // myBLEUart.send("hello"); - // for (int i = 0; i < 6; i ++) { - // USBSerial.print(mypacketBuffer[i]); - // } - // USBSerial.println(); + myBLEUart.send(mydata); + for (int i = 0; i < 6; i ++) { + USBSerial.print(laptop_packetBuffer[i]); + } + USBSerial.println(); } \ No newline at end of file diff --git a/bluetooth testing.py b/bluetooth testing.py index 3af0366..4246fa3 100644 --- a/bluetooth testing.py +++ b/bluetooth testing.py @@ -12,10 +12,6 @@ class BLE_UART: UART_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" UART_RX_CHAR_UUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" UART_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" - - # All BLE devices have MTU of at least 23. Subtracting 3 bytes overhead, we can - # safely send 20 bytes at a time to any device supporting this service. - UART_SAFE_SIZE = 20 def __init__(self, peripheral_name='ESP32_BLE'): self._peripheral_name = peripheral_name @@ -69,8 +65,7 @@ async def main(): for i in range(3): msg = await uart.read() - msg = msg.decode('utf-8') print("received", msg) - await uart.write(f"{msg}") + await uart.write(msg) asyncio.run(main()) \ No newline at end of file From 53e4e1e876001dee57df65f6149227f025b8268f Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Sat, 24 Feb 2024 23:42:57 -0800 Subject: [PATCH 12/48] established computer -> ir beacon -> alipay communication --- IR Beacon/lib/BLE_Uart/BLE_Uart.cpp | 13 +- IR Beacon/lib/BLE_Uart/BLE_Uart.h | 5 +- IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp | 5 +- IR Beacon/lib/esp_now_txrx/esp_now_txrx.h | 7 +- IR Beacon/src/main.cpp | 43 ++++-- bluetooth testing.py | 24 ++-- .../LaptopKeyboard.cpython-311.pyc | Bin 958 -> 958 bytes .../__pycache__/bluetooth.cpython-311.pyc | Bin 0 -> 4860 bytes controller/bluetooth.py | 57 ++++++++ controller/controller.py | 123 +++++++++--------- 10 files changed, 178 insertions(+), 99 deletions(-) create mode 100644 controller/__pycache__/bluetooth.cpython-311.pyc create mode 100644 controller/bluetooth.py diff --git a/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp b/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp index a692c91..bc4f9cc 100644 --- a/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp +++ b/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp @@ -5,7 +5,7 @@ BLECharacteristic * pTxCharacteristic; bool deviceConnected = false; bool oldDeviceConnected = false; -uint8_t* packetBuffer; +char* packetBuffer; int packSize = 6; class MyServerCallbacks: public BLEServerCallbacks { @@ -21,7 +21,6 @@ class MyServerCallbacks: public BLEServerCallbacks { class MyCallbacks: public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { std::string rxValue = pCharacteristic->getValue(); - if (rxValue.length() > 0) { for (int i = 0; i < packSize; i++) { packetBuffer[i] = rxValue[i]; @@ -32,11 +31,15 @@ class MyCallbacks: public BLECharacteristicCallbacks { } }; -BLE_Uart::BLE_Uart(uint8_t* _packetBuffer, int _packSize) { +BLE_Uart::BLE_Uart(char* _packetBuffer, int _packSize) { packetBuffer = _packetBuffer; packSize = _packSize; } +bool BLE_Uart::isConnected() { + return deviceConnected; +} + void BLE_Uart::init_ble() { BLEDevice::setDeviceName("ESP32_BLE"); // Name wont change for one of the S3s for some reason. BLEDevice::init("ESP32_BLE"); @@ -59,7 +62,7 @@ void BLE_Uart::init_ble() { pServer->getAdvertising()->start(); } -void BLE_Uart::send(uint8_t *msg) { - pTxCharacteristic->setValue(msg, packSize); +void BLE_Uart::send(char *msg) { + pTxCharacteristic->setValue(msg); pTxCharacteristic->notify(); } diff --git a/IR Beacon/lib/BLE_Uart/BLE_Uart.h b/IR Beacon/lib/BLE_Uart/BLE_Uart.h index a48d901..6ae30e9 100644 --- a/IR Beacon/lib/BLE_Uart/BLE_Uart.h +++ b/IR Beacon/lib/BLE_Uart/BLE_Uart.h @@ -7,9 +7,10 @@ class BLE_Uart { public: - BLE_Uart(uint8_t* _packetBuffer, int _packSize); + BLE_Uart(char* _packetBuffer, int _packSize); void init_ble(); - void send(uint8_t *msg); + void send(char* _msg); + bool isConnected(); private: }; \ No newline at end of file diff --git a/IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp b/IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp index 73d66ee..58a256d 100644 --- a/IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp +++ b/IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp @@ -5,7 +5,6 @@ esp_now_peer_info_t peerInfo; ESP_NOW_TXRX::ESP_NOW_TXRX(uint8_t* _receiver_address, int _packetSize) { memcpy(peerInfo.peer_addr, _receiver_address, 6); receiver_address = _receiver_address; - // packetBuffer = _packetBuffer; packetSize = _packetSize; } @@ -13,8 +12,8 @@ void ESP_NOW_TXRX::getMacAddress() { USBSerial.println(WiFi.macAddress()); } -void ESP_NOW_TXRX::send(const uint8_t *bytes) { - esp_now_send(receiver_address, bytes, packetSize); +void ESP_NOW_TXRX::send(struct_message dat) { + esp_now_send(receiver_address, (uint8_t *) &dat, packetSize); } void ESP_NOW_TXRX::init(esp_now_recv_cb_t receiv_cb) { diff --git a/IR Beacon/lib/esp_now_txrx/esp_now_txrx.h b/IR Beacon/lib/esp_now_txrx/esp_now_txrx.h index f9c8505..d74022d 100644 --- a/IR Beacon/lib/esp_now_txrx/esp_now_txrx.h +++ b/IR Beacon/lib/esp_now_txrx/esp_now_txrx.h @@ -1,12 +1,17 @@ #include #include + +typedef struct struct_message { + char a[6]; +} struct_message; + class ESP_NOW_TXRX { public: ESP_NOW_TXRX(uint8_t *_receiver_address, int _packetSize); void init(esp_now_recv_cb_t receiv_cb); void getMacAddress(); - void send(const uint8_t *bytes); + void send(struct_message dat); private: int packetSize; diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp index e6cae97..786a80a 100644 --- a/IR Beacon/src/main.cpp +++ b/IR Beacon/src/main.cpp @@ -7,9 +7,9 @@ #include const int packSize = 6; -uint8_t laptop_packetBuffer[packSize]; +char laptop_packetBuffer[packSize] = {}; -uint8_t esp_now_packetBuffer[packSize]; +char esp_now_packetBuffer[packSize] = {}; const int IRLedPin = 4; const int freq = 38000; @@ -31,33 +31,50 @@ void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) { BLE_Uart myBLEUart = BLE_Uart(laptop_packetBuffer, packSize); uint8_t mac_addy[] = {0x48, 0x27, 0xE2, 0xD2, 0xC3, 0x94}; -ESP_NOW_TXRX myESP32 = ESP_NOW_TXRX(mac_addy,packSize); +ESP_NOW_TXRX Alipay = ESP_NOW_TXRX(mac_addy,packSize); +struct_message myData; void setup(){ init_led(); USBSerial.begin(115200); - delay(1000); + // delay(1000); ledcSetup(ledChannel, freq, resolution); ledcAttachPin(IRLedPin, ledChannel); setLeds(CRGB::Green); - // myESP32.init(OnDataRecv); + Alipay.init(OnDataRecv); myBLEUart.init_ble(); setLeds(CRGB::Black); } + +uint8_t charToByte(char *letter) { + return byte(atoi(letter)); +} void loop(){ + if (myBLEUart.isConnected()) { + strcpy(myData.a, laptop_packetBuffer); + Alipay.send(myData); - delay(1000); - uint8_t mydata[] = {0x03, 0x02, 0x00, 0x00, 0x00, 0x00}; - // myESP32.send(mydata); - + if (laptop_packetBuffer[0] == '0') {// Robot is disabled + toggleLeds(CRGB::Red, CRGB::Green, 500); + ledcWrite(ledChannel, 0); + } else { + toggleLeds(CRGB::Blue, CRGB::Purple, 500); + ledcWrite(ledChannel, 100); + } - myBLEUart.send(mydata); - for (int i = 0; i < 6; i ++) { - USBSerial.print(laptop_packetBuffer[i]); + } else { + toggleLeds(CRGB::Red, CRGB::Black, 500); + ledcWrite(ledChannel, 0); } - USBSerial.println(); + // delay(100); + // strcpy(myData.a, "123456"); + // // myData.a = "123456"; + // // char* ptr = laptop_packetBuffer[0]; + // // USBSerial.println(byte('0')); + // // mydata[0] = + // Alipay.send(myData); } \ No newline at end of file diff --git a/bluetooth testing.py b/bluetooth testing.py index 4246fa3..1e5cda1 100644 --- a/bluetooth testing.py +++ b/bluetooth testing.py @@ -5,10 +5,7 @@ from bleak.backends.scanner import AdvertisementData from bleak.backends.device import BLEDevice -# https://github.com/hbldh/bleak/blob/develop/examples/uart_service.py - class BLE_UART: - UART_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" UART_RX_CHAR_UUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" UART_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" @@ -21,10 +18,13 @@ async def read(self): msg = await self._rx_queue.get() return msg + def getBytes(self, letter): + return int(letter).to_bytes(1) + async def write(self, msg): - if isinstance(msg, str): - msg = msg.encode() - await self._client.write_gatt_char(self.UART_RX_CHAR_UUID, msg) + # bytes = self.getBytes(msg[0]) + self.getBytes(msg[1]) + self.getBytes(msg[2]) + self.getBytes(msg[3]) + self.getBytes(msg[4]) + self.getBytes(msg[5]) + # print(bytes) + await self._client.write_gatt_char(self.UART_RX_CHAR_UUID, msg.encode()) async def connect(self): self._discovery_queue = asyncio.Queue() @@ -51,21 +51,21 @@ def _rx_handler(self, _: int, data: bytearray): self._rx_queue.put_nowait(data) def _find_uart_device(self, device: BLEDevice, adv: AdvertisementData): - # called whenever a device is detected during discovery - # ignore all but target device if device.name == self._peripheral_name: self._discovery_queue.put_nowait(device) def _handle_disconnect(self, _: BleakClient): self._rx_queue.put_nowait(None) + async def main(): async with BLE_UART() as uart: await uart.connect() for i in range(3): - msg = await uart.read() - print("received", msg) - await uart.write(msg) - + # msg = 123456 + await uart.write("123456") + await asyncio.sleep(1) +# num = "123456" +# print(int(num[0]).to_bytes(1)) asyncio.run(main()) \ No newline at end of file diff --git a/controller/__pycache__/LaptopKeyboard.cpython-311.pyc b/controller/__pycache__/LaptopKeyboard.cpython-311.pyc index c0cf76116ac85aea9dab3169c2517263b33fa6b7..84d5614aa4258ddfb19714008a5b858816e353c0 100644 GIT binary patch delta 20 acmdnTzK@-IIWI340}y;Zd21v0W@Z3Ae+Fp) delta 20 acmdnTzK@-IIWI340}#ADdwnDKW@Z3AUIt_U diff --git a/controller/__pycache__/bluetooth.cpython-311.pyc b/controller/__pycache__/bluetooth.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7be4ca87e28b626028b423a90877cc023394f9e8 GIT binary patch literal 4860 zcmb6dTWl29_0D70vk#B$^_mcz;B`xk*MJwWp*$k!`oXiZQVb~BkB-KB$FQ*8b?=M; zyGB9+wW|mzX@vw;gXBsP1;Ix@`cyBzPGYVExaJtg;mcgu?Y0SaEgl_3D7#jQ489ru^j}_!e{@6N6*~ zN-`o9G7=RtGL5gGv)rBQ(GwB1mtYIMxX zwBv!C>7<=T7ws})(2qgC8|LG)BJGBD4~+v`I~)ZLr(H>3y1caK=;Y`@D2EP=9Z8QI zI5Tp5e$bOOI#Fcu6aq_I2NZ?Op%Et^Ju*6CV>VZeCwZWs3!i74vz6rDpOAhE*(D*N5j99Yxb}g`A~nOI@{F z7_JSq0~xayz&%n;q)M`_y#M;$#24@Xx$ld zHVYg~)xQ=>?g~~L?Bf-Rm*HQ*U&vGO8SL`X-$@`As-{-}7tg@Us}{^ravz*)O|1nq z3ZLpq1I5~UP#pKwGV~n@`XvvlecZ)_bHOI4k-WUeIik;i;u|6Qe9R*u-p+B_NW(3z!C9@*IroJ3u9V%l}RK=X#X z4L=*(jT3F&DV!*{0a#fmTDjRp&&n@1Sh*9yIOOgEDMrrpkD8QR&l_~9IziWUn0Ts0 zBEh{d+XtPcPTwJTRCmFs`7PLv=MoZozij_&$Cn*Ev{bu#O2OAkds#j5gxb4D;0?8+ z9JQ6BW##DW?!Lc|+9T)7-4`m|7wql}=tIVYw};h%A$#ED+Q8}6fzy?Nv^|h6Ct7Ri z%(~RN>vx>a_dk)^+n;MY%r|e|Y&Z2El+uYOmfbfBZ?}FWmX+~}GHxs5(0HBL_x#Mi z_ShHna$=^En6VQxWo4!m{5<~Xy{a1jtng`}qVBfU-DPEq3K(l@|Ek)*vd8|}v48Bb zPh6;|7j5-oS=pk(ot5$xrPo$^%Svyx7jgqz*;(e*#F+fgj>MQCeJzB?B5hx*0>G^k zhO2<hpD+xNF`6F{4-o?Egwbq4e{1q56>2-gkrg~0XML=v9%jzDv%+t=PY z?p~NMPXO=|iQWX~)eBx9tg0VW)V;R4x2$YiH%Bcb%%c`M%38wY1??AvjUvtZ;heCh z>5$+V405*m8S8^+dM5f=?~EPYrO0NY7<&Ey&&DYLHKHrf-dwg1Lyg&mz))(CqVcp$ z*4Z_)DM<+73GeVsGv-Uy9~*|XCIau!YV5P(r^SleXRCc>W!uX6asw23Mbh#y7H!uguCQm%W^9$%QZ|gbB^MdRo?9M&}(Cq(^m8E!{&$=%?1T!1x ziy;qHq7T(IrGHfcL#rrz?FOykoH5K<9Z9FRwr1@&Fb%8;_3XOQRBrP0g1rZ0^+cml zM6lW|kU@fiH6}G^H1*6;c4Yikaib5~06Pt!CIUPpO9N;YK@vumcGX4D8m_or{{oOe z5RtBjC!g%GccoS%sd6M$jdt46{`ag(NDf| zNT9ahn}2ICvx zX@aqL?p<7$#XxeMGy_EpbmN!@6e(~HB6b4|M*@j;;)7jcU=Z;=pa_9D#K0N|3W)3j z=ng)a#(}}-A^vIwK{0Ryr{GmScAmdm0gxD6x!Af8_pFb(f_Q`^sC@Dj6*|*Xky;b@{LW{A~u`;n??EOmGqWAJiQnN&o-= literal 0 HcmV?d00001 diff --git a/controller/bluetooth.py b/controller/bluetooth.py new file mode 100644 index 0000000..1086625 --- /dev/null +++ b/controller/bluetooth.py @@ -0,0 +1,57 @@ +import asyncio + +from bleak import BleakScanner, BleakClient +from bleak.backends.scanner import AdvertisementData +from bleak.backends.device import BLEDevice + +class BLE_UART: + UART_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" + UART_RX_CHAR_UUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" + UART_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" + isConnected = False + + def __init__(self, peripheral_name='ESP32_BLE'): + self._peripheral_name = peripheral_name + self._rx_queue = asyncio.Queue() + + async def read(self): + msg = await self._rx_queue.get() + return msg + + async def write(self, msg): + if isinstance(msg, str): + msg = msg.encode() + await self._client.write_gatt_char(self.UART_RX_CHAR_UUID, msg) + + async def connect(self): + self._discovery_queue = asyncio.Queue() + device = None + print(f"scanning for {self._peripheral_name}") + async with BleakScanner(detection_callback=self._find_uart_device): + device: BLEDevice = await self._discovery_queue.get() + print(f"connecting to {self._peripheral_name} ...", end="") + client = self._client = BleakClient(device, disconnected_callback=self._handle_disconnect) + await client.connect() + await client.start_notify(self.UART_TX_CHAR_UUID, self._rx_handler) + print("Connected!") + self.isConnected = True + + async def disconnect(self): + self.isConnected = False + await self._client.disconnect() + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + await self.disconnect() + + def _rx_handler(self, _: int, data: bytearray): + self._rx_queue.put_nowait(data) + + def _find_uart_device(self, device: BLEDevice, adv: AdvertisementData): + if device.name == self._peripheral_name: + self._discovery_queue.put_nowait(device) + + def _handle_disconnect(self, _: BleakClient): + self._rx_queue.put_nowait(None) diff --git a/controller/controller.py b/controller/controller.py index 7fc2937..6fbf1d5 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -1,75 +1,72 @@ import time import threading -from AlipayTelemetry import * from LaptopKeyboard import * +from bluetooth import * -# myAlipay = Alipay("192.168.86.22", 12346, "ALIPAY") -# myIRBeacon = Alipay("192.168.86.27", 12347, "IRBeac1") +uart = BLE_UART(peripheral_name='ESP32_BLE') -# myAlipay = Alipay("192.168.111.171", 12346, "ALIPAY") -# myIRBeacon = Alipay("192.168.111.178", 12347, "IRBeac1") - -myAlipay = Alipay("192.168.175.221", 12346, "ALIPAY") -myIRBeacon = Alipay("192.168.175.195", 12347, "IRBeac1") keyboard_thread = threading.Thread(target=lambda: Listener(on_press=on_press, on_release=on_release).start()) -receiver_thread1 = threading.Thread(target=myAlipay.udp_receiver) -receiver_thread2 = threading.Thread(target=myIRBeacon.udp_receiver) - -receiver_thread1.start() -receiver_thread2.start() +keyboard_thread.daemon = True keyboard_thread.start() -drivecmd = 0 -enabled = "0" -waitForRelease = 0 - -print(f"IP : {myAlipay.get_laptop_ip()}") - -while (True): - y = 0 - x = 0 - if get_key_state(Key.up): - y = y + 1 - if get_key_state(Key.down): - y = y - 1 - if get_key_state(Key.left): - x = x - 1 - if get_key_state(Key.right): - x = x + 1 - - if x == 0 and y == 0: - drivecmd = 0 - elif x == 0 and y == 1: - drivecmd = 1 - elif x == 1 and y == 1: - drivecmd = 2 - elif x == 1 and y == 0: - drivecmd = 3 - elif x == 1 and y == -1: - drivecmd = 4 - elif x == 0 and y == -1: - drivecmd = 5 - elif x == -1 and y == -1: - drivecmd = 6 - elif x == -1 and y == 0: - drivecmd = 7 - elif x == -1 and y == 1: - drivecmd = 8 +async def main(): + await uart.connect() + drivecmd = 0 + enabled = 0 + waitForRelease = 0 + while (True): + y = 0 + x = 0 + if get_key_state(Key.up): + y = y + 1 + if get_key_state(Key.down): + y = y - 1 + if get_key_state(Key.left): + x = x - 1 + if get_key_state(Key.right): + x = x + 1 + + if x == 0 and y == 0: + drivecmd = 0 + elif x == 0 and y == 1: + drivecmd = 1 + elif x == 1 and y == 1: + drivecmd = 2 + elif x == 1 and y == 0: + drivecmd = 3 + elif x == 1 and y == -1: + drivecmd = 4 + elif x == 0 and y == -1: + drivecmd = 5 + elif x == -1 and y == -1: + drivecmd = 6 + elif x == -1 and y == 0: + drivecmd = 7 + elif x == -1 and y == 1: + drivecmd = 8 + + if get_key_state(Key.space): + if get_key_state(Key.ctrl): + enabled = 1 + waitForRelease = 1 + else: + if not waitForRelease: + enabled = 0 + if not (get_key_state(Key.space)) and not (get_key_state(Key.ctrl)): + waitForRelease = 0 - if get_key_state(Key.space): - if get_key_state(Key.ctrl): - enabled = 1 - waitForRelease = 1 - else: - if not waitForRelease: - enabled = 0 - if not (get_key_state(Key.space)) and not (get_key_state(Key.ctrl)): - waitForRelease = 0 + cmd = f"{enabled}{drivecmd}0000" + if (uart.isConnected): + await uart.write(cmd) - cmd = f"{drivecmd}{enabled}" - # print(cmd) - myIRBeacon.send_udp_packet(f"{enabled}") - myAlipay.send_udp_packet(cmd) - time.sleep(0.05) + +asyncio.run(main()) + + + + + +# while (True): + \ No newline at end of file From e399833e4ee55600aaca5e7cbbbbccf2877ab929 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Sun, 25 Feb 2024 11:17:19 -0800 Subject: [PATCH 13/48] before switching to only bluetooth --- Alipay ESP32 Code/lib/Melty/melty.cpp | 6 +++ Alipay ESP32 Code/lib/Melty/melty.h | 2 +- Alipay ESP32 Code/platformio.ini | 4 ++ Alipay ESP32 Code/src/main.cpp | 46 ++++++++++++--------- IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp | 10 ++++- IR Beacon/lib/esp_now_txrx/esp_now_txrx.h | 6 ++- IR Beacon/src/main.cpp | 19 +++------ controller/controller.py | 6 ++- 8 files changed, 59 insertions(+), 40 deletions(-) diff --git a/Alipay ESP32 Code/lib/Melty/melty.cpp b/Alipay ESP32 Code/lib/Melty/melty.cpp index 90a995d..5a567a4 100644 --- a/Alipay ESP32 Code/lib/Melty/melty.cpp +++ b/Alipay ESP32 Code/lib/Melty/melty.cpp @@ -7,6 +7,7 @@ melty::melty() { void melty::update() { bool curSeenIRLed = isBeaconSensed(!digitalRead(TOP_IR_PIN)); + // USBSerial.println(digitalRead(TOP_IR_PIN)); if (curSeenIRLed != lastSeenIRLed) if (curSeenIRLed) { // Activates on the rising edge of seeing the IR LED setLeds(CRGB::Green); @@ -45,6 +46,11 @@ bool melty::translate() { // Returns whether or not robot should translate now } bool melty::isBeaconSensed(bool currentReading) { + + for (int i = 0; i < IRLedDataSize; i++) + USBSerial.print(IRLedReadings[i]); + USBSerial.println(); + IRLedReadings[IRLedIndex++] = currentReading; // This is just code for a ring buffer if (IRLedIndex == IRLedDataSize) IRLedIndex = 0; diff --git a/Alipay ESP32 Code/lib/Melty/melty.h b/Alipay ESP32 Code/lib/Melty/melty.h index 3d87246..f4f08f5 100644 --- a/Alipay ESP32 Code/lib/Melty/melty.h +++ b/Alipay ESP32 Code/lib/Melty/melty.h @@ -3,7 +3,7 @@ #define TOP_IR_PIN 10 #define BOTTOM_IR_PIN 9 -#define IRLedDataSize 50 +#define IRLedDataSize 10 class melty { public: diff --git a/Alipay ESP32 Code/platformio.ini b/Alipay ESP32 Code/platformio.ini index fe7bc23..a10afa8 100644 --- a/Alipay ESP32 Code/platformio.ini +++ b/Alipay ESP32 Code/platformio.ini @@ -14,9 +14,13 @@ board = esp32-s3-devkitc-1 framework = arduino upload_speed = 921600 monitor_speed = 115200 +upload_port = /dev/tty.usbmodem2101 +monitor_port = /dev/tty.usbmodem2101 lib_deps = fastled/FastLED@^3.6.0 adafruit/Adafruit MPU6050@^2.2.6 Wire adafruit/Adafruit BusIO@^1.15.0 adafruit/Adafruit Unified Sensor@^1.1.14 +lib_extra_dirs = + /Users/mingweiyeoh/Documents/GitHub/Alipay/IR Beacon/lib \ No newline at end of file diff --git a/Alipay ESP32 Code/src/main.cpp b/Alipay ESP32 Code/src/main.cpp index 78528b1..4bb0803 100644 --- a/Alipay ESP32 Code/src/main.cpp +++ b/Alipay ESP32 Code/src/main.cpp @@ -1,50 +1,56 @@ #include #include -#include #include "USB.h" #include #include #include #include +#include -// const char *ssid = "RESNET-BROTECTED"; -// const char *pswrd = "marbry2025"; +const int packSize = 6; +// char esp_now_packetBuffer[packSize]; -// const char *ssid = "EnVision-Local"; -// const char *pswrd = "thinkmakebreak"; - -const char *ssid = "AlipayDevices"; -const char *pswrd = "alipay123"; +melty alipay = melty(); -const int packSize = 3; -char packetBuffer[packSize]; +uint8_t mac_addy[] = {0xF4, 0x12, 0xFA, 0x6A, 0xA5, 0xB4}; +ESP_NOW_TXRX IRBeacon = ESP_NOW_TXRX(mac_addy,packSize); +struct_message myData; -LaptopTelemetry myLaptop = LaptopTelemetry(ssid, pswrd, packetBuffer); -melty alipay = melty(); +// Callback when data is received +void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) { + IRBeacon.keepAlive(); + memcpy(&myData, incomingData, packSize); +} void setup() { - delay(750); // Some reason my aliexpress esp32s3 needs this delay to give it enough time to initialize + delay(750); init_led(); init_mpu6050(); init_motors(); USBSerial.begin(115200); + delay(1000); setLeds(CRGB::Green); - myLaptop.init(); + IRBeacon.init(OnDataRecv); setLeds(CRGB::Black); } void loop() { - myLaptop.receive(); - if (myLaptop.isDisconnected()) { + // delay(100); + // USBSerial.println(digitalRead(10)); + // IRBeacon.keepAlive(); + // myData.a = + // USBSerial.println(IRBeacon.isDisconnected()); + + if (IRBeacon.isDisconnected()) { set_both_motors(0); toggleLeds(CRGB::Red, CRGB::Black, 500); } else { - if (packetBuffer[1] == '1') { // Currently enabled + if (myData.a[0] == '1') { // Currently enabled alipay.update(); if (alipay.translate()) { l_motor_write(8-5); @@ -53,7 +59,7 @@ void loop() set_both_motors(8); } - switch (packetBuffer[0]) { // Check the drive cmd + switch (myData.a[1]) { // Check the drive cmd case '1': alipay.deg = 0; break; @@ -80,7 +86,7 @@ void loop() break; } - if (packetBuffer[0] != '0') {// Check drive cmd + if (myData.a[1] != '0') {// Check drive cmd alipay.percentageOfRotation = 0.5; } else { alipay.percentageOfRotation = 0.00; @@ -91,7 +97,7 @@ void loop() set_both_motors(0); EVERY_N_MILLIS(2000) { - myLaptop.send(getVoltage()); + // myLaptop.send(getVoltage()); } } } diff --git a/IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp b/IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp index 58a256d..8f72366 100644 --- a/IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp +++ b/IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp @@ -8,7 +8,7 @@ ESP_NOW_TXRX::ESP_NOW_TXRX(uint8_t* _receiver_address, int _packetSize) { packetSize = _packetSize; } -void ESP_NOW_TXRX::getMacAddress() { +void ESP_NOW_TXRX::getMyMacAddress() { USBSerial.println(WiFi.macAddress()); } @@ -32,3 +32,11 @@ void ESP_NOW_TXRX::init(esp_now_recv_cb_t receiv_cb) { return; } } + +bool ESP_NOW_TXRX::isDisconnected() { + return millis() - lastMessage > 500; +} + +void ESP_NOW_TXRX::keepAlive() { + lastMessage = millis(); +} \ No newline at end of file diff --git a/IR Beacon/lib/esp_now_txrx/esp_now_txrx.h b/IR Beacon/lib/esp_now_txrx/esp_now_txrx.h index d74022d..5a30694 100644 --- a/IR Beacon/lib/esp_now_txrx/esp_now_txrx.h +++ b/IR Beacon/lib/esp_now_txrx/esp_now_txrx.h @@ -10,10 +10,12 @@ class ESP_NOW_TXRX { public: ESP_NOW_TXRX(uint8_t *_receiver_address, int _packetSize); void init(esp_now_recv_cb_t receiv_cb); - void getMacAddress(); + void getMyMacAddress(); void send(struct_message dat); - + bool isDisconnected(); + void keepAlive(); private: int packetSize; uint8_t *receiver_address; + unsigned long lastMessage = millis(); }; \ No newline at end of file diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp index 786a80a..87562b1 100644 --- a/IR Beacon/src/main.cpp +++ b/IR Beacon/src/main.cpp @@ -30,7 +30,7 @@ void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) { BLE_Uart myBLEUart = BLE_Uart(laptop_packetBuffer, packSize); -uint8_t mac_addy[] = {0x48, 0x27, 0xE2, 0xD2, 0xC3, 0x94}; +uint8_t mac_addy[] = {0xF4, 0x12, 0xFA, 0x6A, 0x0F, 0x64}; ESP_NOW_TXRX Alipay = ESP_NOW_TXRX(mac_addy,packSize); struct_message myData; @@ -45,19 +45,18 @@ void setup(){ Alipay.init(OnDataRecv); myBLEUart.init_ble(); setLeds(CRGB::Black); + // delay(1000); + // Alipay.getMyMacAddress(); } - -uint8_t charToByte(char *letter) { - return byte(atoi(letter)); -} void loop(){ if (myBLEUart.isConnected()) { strcpy(myData.a, laptop_packetBuffer); + delay(100); Alipay.send(myData); - if (laptop_packetBuffer[0] == '0') {// Robot is disabled + if (myData.a[0] == '0') {// Robot is disabled toggleLeds(CRGB::Red, CRGB::Green, 500); ledcWrite(ledChannel, 0); } else { @@ -69,12 +68,4 @@ void loop(){ toggleLeds(CRGB::Red, CRGB::Black, 500); ledcWrite(ledChannel, 0); } - // delay(100); - // strcpy(myData.a, "123456"); - // // myData.a = "123456"; - // // char* ptr = laptop_packetBuffer[0]; - // // USBSerial.println(byte('0')); - // // mydata[0] = - // Alipay.send(myData); - } \ No newline at end of file diff --git a/controller/controller.py b/controller/controller.py index 6fbf1d5..1425fa5 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -4,6 +4,7 @@ from bluetooth import * uart = BLE_UART(peripheral_name='ESP32_BLE') +alipay = BLE_UART(peripheral_name='Alipay') keyboard_thread = threading.Thread(target=lambda: Listener(on_press=on_press, on_release=on_release).start()) @@ -12,13 +13,14 @@ async def main(): await uart.connect() + await alipay.connect() drivecmd = 0 enabled = 0 waitForRelease = 0 while (True): - y = 0 + y = 0 x = 0 - if get_key_state(Key.up): + if get_key_state(Key.up): y = y + 1 if get_key_state(Key.down): y = y - 1 From dfc54cf14cbd9f3a0962cd4854d25e2c632c3d0c Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Sun, 25 Feb 2024 11:36:25 -0800 Subject: [PATCH 14/48] now using only BLE --- Alipay ESP32 Code/.vscode/settings.json | 3 +- Alipay ESP32 Code/platformio.ini | 3 +- Alipay ESP32 Code/src/main.cpp | 46 +++++++++++------------ IR Beacon/lib/BLE_Uart/BLE_Uart.cpp | 6 +-- IR Beacon/lib/BLE_Uart/BLE_Uart.h | 2 +- IR Beacon/src/main.cpp | 38 +++++++++---------- bluetooth scanner => bluetooth scanner.py | 0 controller/controller.py | 15 +++----- 8 files changed, 56 insertions(+), 57 deletions(-) rename bluetooth scanner => bluetooth scanner.py (100%) diff --git a/Alipay ESP32 Code/.vscode/settings.json b/Alipay ESP32 Code/.vscode/settings.json index de67d8b..ab87840 100644 --- a/Alipay ESP32 Code/.vscode/settings.json +++ b/Alipay ESP32 Code/.vscode/settings.json @@ -7,6 +7,7 @@ "unordered_set": "cpp", "vector": "cpp", "string_view": "cpp", - "initializer_list": "cpp" + "initializer_list": "cpp", + "random": "cpp" } } \ No newline at end of file diff --git a/Alipay ESP32 Code/platformio.ini b/Alipay ESP32 Code/platformio.ini index a10afa8..112ea8b 100644 --- a/Alipay ESP32 Code/platformio.ini +++ b/Alipay ESP32 Code/platformio.ini @@ -22,5 +22,6 @@ lib_deps = Wire adafruit/Adafruit BusIO@^1.15.0 adafruit/Adafruit Unified Sensor@^1.1.14 + h2zero/NimBLE-Arduino@^1.4.1 lib_extra_dirs = - /Users/mingweiyeoh/Documents/GitHub/Alipay/IR Beacon/lib \ No newline at end of file + /Users/mingweiyeoh/Documents/GitHub/Alipay/IR Beacon/lib diff --git a/Alipay ESP32 Code/src/main.cpp b/Alipay ESP32 Code/src/main.cpp index 4bb0803..80a6ada 100644 --- a/Alipay ESP32 Code/src/main.cpp +++ b/Alipay ESP32 Code/src/main.cpp @@ -5,23 +5,24 @@ #include #include #include -#include +#include const int packSize = 6; -// char esp_now_packetBuffer[packSize]; +char laptop_packetBuffer[packSize] = {}; + +BLE_Uart laptop = BLE_Uart(laptop_packetBuffer, packSize); melty alipay = melty(); -uint8_t mac_addy[] = {0xF4, 0x12, 0xFA, 0x6A, 0xA5, 0xB4}; -ESP_NOW_TXRX IRBeacon = ESP_NOW_TXRX(mac_addy,packSize); -struct_message myData; +// uint8_t mac_addy[] = {0xF4, 0x12, 0xFA, 0x6A, 0xA5, 0xB4}; +// ESP_NOW_TXRX IRBeacon = ESP_NOW_TXRX(mac_addy,packSize); +// struct_message myData; // Callback when data is received -void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) { - IRBeacon.keepAlive(); - memcpy(&myData, incomingData, packSize); - -} +// void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) { +// IRBeacon.keepAlive(); +// memcpy(&myData, incomingData, packSize); +// } void setup() @@ -34,8 +35,9 @@ void setup() USBSerial.begin(115200); delay(1000); setLeds(CRGB::Green); - IRBeacon.init(OnDataRecv); - setLeds(CRGB::Black); + laptop.init_ble("Alipay"); + // IRBeacon.init(OnDataRecv); + setLeds(CRGB::Black); } void loop() @@ -46,11 +48,8 @@ void loop() // myData.a = // USBSerial.println(IRBeacon.isDisconnected()); - if (IRBeacon.isDisconnected()) { - set_both_motors(0); - toggleLeds(CRGB::Red, CRGB::Black, 500); - } else { - if (myData.a[0] == '1') { // Currently enabled + if (laptop.isConnected()) { + if (laptop_packetBuffer[0] == '1') { // Currently enabled alipay.update(); if (alipay.translate()) { l_motor_write(8-5); @@ -59,7 +58,7 @@ void loop() set_both_motors(8); } - switch (myData.a[1]) { // Check the drive cmd + switch (laptop_packetBuffer[1]) { // Check the drive cmd case '1': alipay.deg = 0; break; @@ -86,21 +85,22 @@ void loop() break; } - if (myData.a[1] != '0') {// Check drive cmd + if (laptop_packetBuffer[1] != '0') {// Check drive cmd alipay.percentageOfRotation = 0.5; } else { alipay.percentageOfRotation = 0.00; } - } else { + } else { // Currently disabled toggleLeds(CRGB::Red, CRGB::Green, 500); set_both_motors(0); EVERY_N_MILLIS(2000) { // myLaptop.send(getVoltage()); } - } + } + } else { + set_both_motors(0); + toggleLeds(CRGB::Red, CRGB::Black, 500); } - - } diff --git a/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp b/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp index bc4f9cc..6aff16c 100644 --- a/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp +++ b/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp @@ -40,9 +40,9 @@ bool BLE_Uart::isConnected() { return deviceConnected; } -void BLE_Uart::init_ble() { - BLEDevice::setDeviceName("ESP32_BLE"); // Name wont change for one of the S3s for some reason. - BLEDevice::init("ESP32_BLE"); +void BLE_Uart::init_ble(const std::string &name) { + BLEDevice::setDeviceName(name); // Name wont change for one of the S3s for some reason. + BLEDevice::init(name); pServer = BLEDevice::createServer(); // Create the BLE Server pServer->setCallbacks(new MyServerCallbacks()); diff --git a/IR Beacon/lib/BLE_Uart/BLE_Uart.h b/IR Beacon/lib/BLE_Uart/BLE_Uart.h index 6ae30e9..1a20f6b 100644 --- a/IR Beacon/lib/BLE_Uart/BLE_Uart.h +++ b/IR Beacon/lib/BLE_Uart/BLE_Uart.h @@ -8,7 +8,7 @@ class BLE_Uart { public: BLE_Uart(char* _packetBuffer, int _packSize); - void init_ble(); + void init_ble(const std::string &name); void send(char* _msg); bool isConnected(); private: diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp index 87562b1..ad3419d 100644 --- a/IR Beacon/src/main.cpp +++ b/IR Beacon/src/main.cpp @@ -9,7 +9,7 @@ const int packSize = 6; char laptop_packetBuffer[packSize] = {}; -char esp_now_packetBuffer[packSize] = {}; +// char esp_now_packetBuffer[packSize] = {}; const int IRLedPin = 4; const int freq = 38000; @@ -17,22 +17,22 @@ const int ledChannel = 0; const int resolution = 8; // Callback when data is received -void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) { - memcpy(esp_now_packetBuffer, incomingData, packSize); - USBSerial.print("Bytes received: "); - USBSerial.println(len); - USBSerial.print("Message: "); +// void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) { +// memcpy(esp_now_packetBuffer, incomingData, packSize); +// USBSerial.print("Bytes received: "); +// USBSerial.println(len); +// USBSerial.print("Message: "); - for (int i = 0; i < len; i++) - USBSerial.print(incomingData[i]); - USBSerial.println(); -} +// for (int i = 0; i < len; i++) +// USBSerial.print(incomingData[i]); +// USBSerial.println(); +// } BLE_Uart myBLEUart = BLE_Uart(laptop_packetBuffer, packSize); -uint8_t mac_addy[] = {0xF4, 0x12, 0xFA, 0x6A, 0x0F, 0x64}; -ESP_NOW_TXRX Alipay = ESP_NOW_TXRX(mac_addy,packSize); -struct_message myData; +// uint8_t mac_addy[] = {0xF4, 0x12, 0xFA, 0x6A, 0x0F, 0x64}; +// ESP_NOW_TXRX Alipay = ESP_NOW_TXRX(mac_addy,packSize); +// struct_message myData; void setup(){ init_led(); @@ -42,8 +42,8 @@ void setup(){ ledcAttachPin(IRLedPin, ledChannel); setLeds(CRGB::Green); - Alipay.init(OnDataRecv); - myBLEUart.init_ble(); + // Alipay.init(OnDataRecv); + myBLEUart.init_ble("IR Beacon"); setLeds(CRGB::Black); // delay(1000); // Alipay.getMyMacAddress(); @@ -52,11 +52,11 @@ void setup(){ void loop(){ if (myBLEUart.isConnected()) { - strcpy(myData.a, laptop_packetBuffer); - delay(100); - Alipay.send(myData); + // strcpy(myData.a, laptop_packetBuffer); + // delay(100); + // Alipay.send(myData); - if (myData.a[0] == '0') {// Robot is disabled + if (laptop_packetBuffer[0] == '0') {// Robot is disabled toggleLeds(CRGB::Red, CRGB::Green, 500); ledcWrite(ledChannel, 0); } else { diff --git a/bluetooth scanner b/bluetooth scanner.py similarity index 100% rename from bluetooth scanner rename to bluetooth scanner.py diff --git a/controller/controller.py b/controller/controller.py index 1425fa5..915304e 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -3,7 +3,7 @@ from LaptopKeyboard import * from bluetooth import * -uart = BLE_UART(peripheral_name='ESP32_BLE') +ir_beacon = BLE_UART(peripheral_name='IR Beacon') alipay = BLE_UART(peripheral_name='Alipay') @@ -12,7 +12,7 @@ keyboard_thread.start() async def main(): - await uart.connect() + await ir_beacon.connect() await alipay.connect() drivecmd = 0 enabled = 0 @@ -59,16 +59,13 @@ async def main(): waitForRelease = 0 cmd = f"{enabled}{drivecmd}0000" - if (uart.isConnected): - await uart.write(cmd) - + if (ir_beacon.isConnected): + await ir_beacon.write(cmd) + if (alipay.isConnected): + await alipay.write(cmd) asyncio.run(main()) - - - -# while (True): \ No newline at end of file From e3d95be6851d7fb7df9ac22fccd5cefb9a87674b Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Sun, 25 Feb 2024 21:28:02 -0800 Subject: [PATCH 15/48] all basic things work, just need to work on melty algo --- .../lib/Battery_Monitor/Battery_Monitor.h | 8 +- .../lib/LEDHandler/LEDHandler.cpp | 11 +++ Alipay ESP32 Code/lib/Melty/melty.h | 2 +- Alipay ESP32 Code/platformio.ini | 4 +- Alipay ESP32 Code/src/main.cpp | 92 ++++++++++-------- IR Beacon/lib/BLE_Uart/BLE_Uart.cpp | 13 ++- IR Beacon/lib/BLE_Uart/BLE_Uart.h | 3 +- .../lib/Battery_Monitor/Battery_Monitor.h | 5 - IR Beacon/platformio.ini | 4 +- IR Beacon/src/main.cpp | 41 ++------ bluetooth testing.py | 2 - .../__pycache__/bluetooth.cpython-311.pyc | Bin 4860 -> 4904 bytes controller/bluetooth.py | 34 +++---- controller/controller.py | 70 ++++++++++--- 14 files changed, 169 insertions(+), 120 deletions(-) delete mode 100644 IR Beacon/lib/Battery_Monitor/Battery_Monitor.h diff --git a/Alipay ESP32 Code/lib/Battery_Monitor/Battery_Monitor.h b/Alipay ESP32 Code/lib/Battery_Monitor/Battery_Monitor.h index b6e3158..4efaee4 100644 --- a/Alipay ESP32 Code/lib/Battery_Monitor/Battery_Monitor.h +++ b/Alipay ESP32 Code/lib/Battery_Monitor/Battery_Monitor.h @@ -1,5 +1,9 @@ #include -float getVoltage() { - return (float(analogRead(18)) / 4096)*3.1 * 3.95; +float get3sVoltage() { + return (float(analogRead(18)) / 4096)*3.1 * 3.95 * 1.0175; +} + +float get1sVoltage() { + return (float(analogRead(18)) / 4096)*3.1 * 2.00 * 1.0625; } \ No newline at end of file diff --git a/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.cpp b/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.cpp index f6fba8e..4411a90 100644 --- a/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.cpp +++ b/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.cpp @@ -4,6 +4,9 @@ bool ledToggleState = 0; unsigned long lastdelayToggle = millis(); unsigned long currentDelayToggle = millis(); +CRGB lastColor1; +CRGB lastColor2; + CRGB leds[1]; void init_led() { @@ -21,7 +24,12 @@ void syncToggle() { } void toggleLeds(CRGB color1, CRGB color2, int delayMS) { + if (color1 != lastColor1 || color2 != lastColor2 || millis() - lastdelayToggle > delayMS + 500) { + lastdelayToggle = millis(); + ledToggleState = 0; + } currentDelayToggle = millis(); + if (currentDelayToggle - lastdelayToggle > delayMS) { ledToggleState = !ledToggleState; lastdelayToggle = currentDelayToggle; @@ -31,4 +39,7 @@ void toggleLeds(CRGB color1, CRGB color2, int delayMS) { setLeds(color1); else setLeds(color2); + + lastColor1 = color1; + lastColor2 = color2; } \ No newline at end of file diff --git a/Alipay ESP32 Code/lib/Melty/melty.h b/Alipay ESP32 Code/lib/Melty/melty.h index f4f08f5..cb33b1a 100644 --- a/Alipay ESP32 Code/lib/Melty/melty.h +++ b/Alipay ESP32 Code/lib/Melty/melty.h @@ -3,7 +3,7 @@ #define TOP_IR_PIN 10 #define BOTTOM_IR_PIN 9 -#define IRLedDataSize 10 +#define IRLedDataSize 15 class melty { public: diff --git a/Alipay ESP32 Code/platformio.ini b/Alipay ESP32 Code/platformio.ini index 112ea8b..837a382 100644 --- a/Alipay ESP32 Code/platformio.ini +++ b/Alipay ESP32 Code/platformio.ini @@ -14,8 +14,8 @@ board = esp32-s3-devkitc-1 framework = arduino upload_speed = 921600 monitor_speed = 115200 -upload_port = /dev/tty.usbmodem2101 -monitor_port = /dev/tty.usbmodem2101 +; upload_port = /dev/tty.usbmodem2101 +; monitor_port = /dev/tty.usbmodem2101 lib_deps = fastled/FastLED@^3.6.0 adafruit/Adafruit MPU6050@^2.2.6 diff --git a/Alipay ESP32 Code/src/main.cpp b/Alipay ESP32 Code/src/main.cpp index 80a6ada..59cf20d 100644 --- a/Alipay ESP32 Code/src/main.cpp +++ b/Alipay ESP32 Code/src/main.cpp @@ -13,91 +13,103 @@ char laptop_packetBuffer[packSize] = {}; BLE_Uart laptop = BLE_Uart(laptop_packetBuffer, packSize); melty alipay = melty(); - -// uint8_t mac_addy[] = {0xF4, 0x12, 0xFA, 0x6A, 0xA5, 0xB4}; -// ESP_NOW_TXRX IRBeacon = ESP_NOW_TXRX(mac_addy,packSize); -// struct_message myData; - -// Callback when data is received -// void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) { -// IRBeacon.keepAlive(); -// memcpy(&myData, incomingData, packSize); -// } - +struct melty_parameters { + int rot = 15; + int tra = 5; + float per = 0.5; +} melty_parameters; void setup() { - delay(750); init_led(); + setLeds(CRGB::Green); + USBSerial.begin(115200); init_mpu6050(); init_motors(); - - USBSerial.begin(115200); - delay(1000); - setLeds(CRGB::Green); laptop.init_ble("Alipay"); - // IRBeacon.init(OnDataRecv); setLeds(CRGB::Black); } void loop() { - // delay(100); - // USBSerial.println(digitalRead(10)); - // IRBeacon.keepAlive(); - // myData.a = - // USBSerial.println(IRBeacon.isDisconnected()); - if (laptop.isConnected()) { - if (laptop_packetBuffer[0] == '1') { // Currently enabled + if (laptop_packetBuffer[0] == '1') { // Currently enabled and translating!! alipay.update(); if (alipay.translate()) { - l_motor_write(8-5); - r_motor_write(8+5); + l_motor_write(melty_parameters.rot-melty_parameters.tra); + r_motor_write(melty_parameters.rot+melty_parameters.tra); } else { - set_both_motors(8); + set_both_motors(melty_parameters.rot); } switch (laptop_packetBuffer[1]) { // Check the drive cmd - case '1': + case '8': alipay.deg = 0; break; - case '2': + case '7': alipay.deg = 45; break; - case '3': + case '6': alipay.deg = 90; break; - case '4': + case '5': alipay.deg = 135; break; - case '5': + case '4': alipay.deg = 180; break; - case '6': + case '3': alipay.deg = 225; break; - case '7': + case '2': alipay.deg = 270; break; - case '8': + case '1': alipay.deg = 315; break; } if (laptop_packetBuffer[1] != '0') {// Check drive cmd - alipay.percentageOfRotation = 0.5; + alipay.percentageOfRotation = melty_parameters.per; } else { - alipay.percentageOfRotation = 0.00; + alipay.percentageOfRotation = 0; + } + + EVERY_N_MILLIS(50) { // Print out the settings + String msg = "rotpwr : " + String(melty_parameters.rot) + " tranpwr : " + String(melty_parameters.tra) + " perc : " + String(melty_parameters.per); + laptop.send(msg); + } + + EVERY_N_MILLIS(100) { + switch (laptop_packetBuffer[2]) { + case '1': + melty_parameters.rot++; + break; + case '2': + melty_parameters.rot--; + break; + case '3': + melty_parameters.tra++; + break; + case '4': + melty_parameters.tra--; + break; + case '5': + melty_parameters.per = melty_parameters.per + .03; + break; + case '6': + melty_parameters.per = melty_parameters.per - .03; + break; + } } } else { // Currently disabled toggleLeds(CRGB::Red, CRGB::Green, 500); set_both_motors(0); + EVERY_N_SECONDS(10) { + laptop.send(get3sVoltage()); + } - EVERY_N_MILLIS(2000) { - // myLaptop.send(getVoltage()); - } } } else { set_both_motors(0); diff --git a/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp b/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp index 6aff16c..f473577 100644 --- a/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp +++ b/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp @@ -41,7 +41,7 @@ bool BLE_Uart::isConnected() { } void BLE_Uart::init_ble(const std::string &name) { - BLEDevice::setDeviceName(name); // Name wont change for one of the S3s for some reason. + BLEDevice::setDeviceName(name); BLEDevice::init(name); pServer = BLEDevice::createServer(); // Create the BLE Server @@ -62,7 +62,14 @@ void BLE_Uart::init_ble(const std::string &name) { pServer->getAdvertising()->start(); } -void BLE_Uart::send(char *msg) { - pTxCharacteristic->setValue(msg); +void BLE_Uart::send(String message) { + pTxCharacteristic->setValue(message); pTxCharacteristic->notify(); } + +void BLE_Uart::send(float value) + { + char conversionbuffer[5]; + sprintf(conversionbuffer, "%.2f", value); + send(conversionbuffer); + } diff --git a/IR Beacon/lib/BLE_Uart/BLE_Uart.h b/IR Beacon/lib/BLE_Uart/BLE_Uart.h index 1a20f6b..2b1083e 100644 --- a/IR Beacon/lib/BLE_Uart/BLE_Uart.h +++ b/IR Beacon/lib/BLE_Uart/BLE_Uart.h @@ -9,7 +9,8 @@ class BLE_Uart { public: BLE_Uart(char* _packetBuffer, int _packSize); void init_ble(const std::string &name); - void send(char* _msg); + void send(String message); + void send(float value); bool isConnected(); private: diff --git a/IR Beacon/lib/Battery_Monitor/Battery_Monitor.h b/IR Beacon/lib/Battery_Monitor/Battery_Monitor.h deleted file mode 100644 index b10757d..0000000 --- a/IR Beacon/lib/Battery_Monitor/Battery_Monitor.h +++ /dev/null @@ -1,5 +0,0 @@ -#include - -float getVoltage() { - return (float(analogRead(18)) / 4096); // <-- Not working properly?? -} \ No newline at end of file diff --git a/IR Beacon/platformio.ini b/IR Beacon/platformio.ini index 1d45095..e9af7cd 100644 --- a/IR Beacon/platformio.ini +++ b/IR Beacon/platformio.ini @@ -14,8 +14,8 @@ board = esp32-s3-devkitc-1 framework = arduino upload_speed = 921600 monitor_speed = 115200 -upload_port = /dev/tty.usbmodem1101 -monitor_port = /dev/tty.usbmodem1101 +; upload_port = /dev/tty.usbmodem1101 +; monitor_port = /dev/tty.usbmodem1101 build_flags = -D IR_BEACON=1 lib_deps = fastled/FastLED@^3.6.0 diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp index ad3419d..6f4385b 100644 --- a/IR Beacon/src/main.cpp +++ b/IR Beacon/src/main.cpp @@ -9,53 +9,28 @@ const int packSize = 6; char laptop_packetBuffer[packSize] = {}; -// char esp_now_packetBuffer[packSize] = {}; - const int IRLedPin = 4; const int freq = 38000; const int ledChannel = 0; const int resolution = 8; -// Callback when data is received -// void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) { -// memcpy(esp_now_packetBuffer, incomingData, packSize); -// USBSerial.print("Bytes received: "); -// USBSerial.println(len); -// USBSerial.print("Message: "); - -// for (int i = 0; i < len; i++) -// USBSerial.print(incomingData[i]); -// USBSerial.println(); -// } - -BLE_Uart myBLEUart = BLE_Uart(laptop_packetBuffer, packSize); -// uint8_t mac_addy[] = {0xF4, 0x12, 0xFA, 0x6A, 0x0F, 0x64}; -// ESP_NOW_TXRX Alipay = ESP_NOW_TXRX(mac_addy,packSize); -// struct_message myData; +BLE_Uart laptop = BLE_Uart(laptop_packetBuffer, packSize); void setup(){ init_led(); + setLeds(CRGB::Green); USBSerial.begin(115200); - // delay(1000); ledcSetup(ledChannel, freq, resolution); ledcAttachPin(IRLedPin, ledChannel); - - setLeds(CRGB::Green); - // Alipay.init(OnDataRecv); - myBLEUart.init_ble("IR Beacon"); + + laptop.init_ble("IR Beacon"); setLeds(CRGB::Black); - // delay(1000); - // Alipay.getMyMacAddress(); - } void loop(){ - if (myBLEUart.isConnected()) { - // strcpy(myData.a, laptop_packetBuffer); - // delay(100); - // Alipay.send(myData); - + + if (laptop.isConnected()) { if (laptop_packetBuffer[0] == '0') {// Robot is disabled toggleLeds(CRGB::Red, CRGB::Green, 500); ledcWrite(ledChannel, 0); @@ -64,6 +39,10 @@ void loop(){ ledcWrite(ledChannel, 100); } + EVERY_N_SECONDS(10) { + laptop.send(get1sVoltage()); + } + } else { toggleLeds(CRGB::Red, CRGB::Black, 500); ledcWrite(ledChannel, 0); diff --git a/bluetooth testing.py b/bluetooth testing.py index 1e5cda1..a9b9052 100644 --- a/bluetooth testing.py +++ b/bluetooth testing.py @@ -22,8 +22,6 @@ def getBytes(self, letter): return int(letter).to_bytes(1) async def write(self, msg): - # bytes = self.getBytes(msg[0]) + self.getBytes(msg[1]) + self.getBytes(msg[2]) + self.getBytes(msg[3]) + self.getBytes(msg[4]) + self.getBytes(msg[5]) - # print(bytes) await self._client.write_gatt_char(self.UART_RX_CHAR_UUID, msg.encode()) async def connect(self): diff --git a/controller/__pycache__/bluetooth.cpython-311.pyc b/controller/__pycache__/bluetooth.cpython-311.pyc index 7be4ca87e28b626028b423a90877cc023394f9e8..fb1205634b6ab8de627a6fd67f90fe6af69f37ff 100644 GIT binary patch delta 1673 zcmb7EO>7%Q6rS1j+UuXSopqWT%XREJscgrM)6mwb6N=n46saf(9Dd5ha=n|9;@H*L zD5>Q%mB@hu2hyn&s-vR(gV6nky9w#OH=p?Mw>hM4DK~R$amoVc?;ky!e|Yd>Bo*B%^AI1 z1=Fk6f&D2IL?t8mJ2HUC;1XA%mkj%1nkS3VIi#Q^?h2=%R7YFD3uuu)hZZog!m_YH z9MCoS3u>upRMc!iQJdze#jJX%G*`$sEy`RmTgd0tZ0RmfoZ8g+;)Nzwi#!bc^adYT zUIpz3c&?%rXD|_$MSLp)j+v`J;7ZScQvZ=~NE z4j*V5)jXq}xFH~Uih&29!>>*7Ei*^)20)i&9!Qq^u16op!&~z3MyU~-`n>p6`HOP3 z@@1ti&otzjn(c}0|C0U86Ug67_Si}EnI(CcuZo^zh_8kO(CPa^ANFtF5ki>qCJ*MR z&vb9x0PM07*W@NrgxmZ_7@>Rcwgn@_SV3u4oSHDw!=~x7l%hYG4o1Sbxo@{LRVAIx z8d#a}A;A7lEOn8p3{Q!(d_LZq_u)C*JTSCTK!40r0<2iD?uo0H~A zOgAifPtAV(1Et}68v1sti}8jSuZeLQu#Uc{AIue4*RGe6{?!O67&6nxH_>fwPhK44 zrB@bJp{n4p()(7^GCy6GPwNK`GMFWWdH-{8NW-uhu`w87(9eLCN*HU0vdDJy6S-?H zyYwt~S&D$SI0MiwqlcnYvnSS%HiBc&L#vD8ctaeoiQ`nVouE`&8a>Ja9%JjY)9&wV z5d%xPSKlH{7SnHL@=8u6F?z}##YbqtPQxL#cnl!izB$MdW=H`1HCluYIr{M^J?r?; z7iZHsEhoY@FEXOK+YV8a)9*eG7IK0?FTgY~V1^!U&u5)uuLhWXn1RNVi)jZK1R1ab z&}eJ&bNN!!eExDt%@C5gOjt&QU8RKG2ZT{Y*qA^enS7%~E5a5o|!ERtf z9c&iC-$FXLc_Gw7I#}p?JzslHv#9LLwZHXPoR}{tZ|BsL7%Q6rS0&o%PzgcIu=t? zdGr3}y;<&m_^k4+qR0fs`p>S}y`=m^-(T8D)6~-6`otx2I%-4#6+m=-Jvx1QXI%Q}xB)f}G(F=0;=*+8QqlF_Uj;{Skr)WX|J0HRS z!7+g2gp#YgSDbuihOh`QvI2|27MRB068nV@ zDgQm%r_Pgig4;6ugP^YaeuiARY?m$DDOx4d6>W#PA=4^V4U-F@y)xSkr+lYRgwhKs zT%7{&J-;7GT`pZ8YeX{5NM@7JaAqTsx|;hUw={TVa52B3CO)g&Q*-NTZh5@9XY$Ti zL!D}>Q+4TypC93lfJg|ed3in}khQ}d{dBkkIIUrb5LjSqb&VK->!K0-E_4ghyo#yD z!HZIwcJPbRYF4@hs#n|UDKWzHq<}Z&4THdS%hNT*=Ck3xd`&G?Ez2x9Wou4%s(LM; z4-E~uBuibgs_non~enX8*e*)TMkw$~UDv5cl~n5i7D-X-GXysi!XW z@N#stt6hmFL6=_=28FQrrkWCN3VbA}@u<>!QkZ@vE3)T7h@}BsdE5CF>XoRCnbBDS zMaBSX$+o-Ovd_VWeE`6(0sg(Rf1%V)De#f@ZVY?T_h%jw>($!|VdKEvVaSyWg`#OW zCMy(n*xP1e-fZltR=32Gi;>65qLGj-{SA3DqbP%exfgrjT% zm52a(%P2#QHEr7*J6CwSXc-lgjqtD4K|0LusyvHnXNLjc4$*st4+xtCcvM@Ke;muu zasEy0MjBJm3YMYqow2eUFUIw*eW>MS1bkbjvy_ElhBxo4_`U@l`Fjz(s!1Y+xn{ix zcrji+*-J=aNw{L!a@_El^Nv|$ta#pwjbRHje12FK0sEEZ5MD$;yMD<@NFiW{+YbrP zckX{v2qrg48zeD!jBb)0C`W?&CTT;r5F9}H7DzOx!Nar#A&SCn2=N~}?{+`x#Xr)- f*{bng#XQ8O`L)EU7e*=Q`Y-&6yubcu!wcwN-fx6M diff --git a/controller/bluetooth.py b/controller/bluetooth.py index 1086625..443ede8 100644 --- a/controller/bluetooth.py +++ b/controller/bluetooth.py @@ -10,41 +10,39 @@ class BLE_UART: UART_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" isConnected = False - def __init__(self, peripheral_name='ESP32_BLE'): + def __init__(self, peripheral_name=""): self._peripheral_name = peripheral_name self._rx_queue = asyncio.Queue() async def read(self): - msg = await self._rx_queue.get() - return msg - - async def write(self, msg): - if isinstance(msg, str): - msg = msg.encode() - await self._client.write_gatt_char(self.UART_RX_CHAR_UUID, msg) - + try: + msg = await self._rx_queue.get() + return msg.decode() + except Exception as e: + await self.disconnect() + + async def write(self, msg): # Where msg is a String + try: + await self._client.write_gatt_char(self.UART_RX_CHAR_UUID, msg.encode()) + except Exception as e: + await self.disconnect() + async def connect(self): self._discovery_queue = asyncio.Queue() device = None - print(f"scanning for {self._peripheral_name}") + print(f"Scanning for {self._peripheral_name}") async with BleakScanner(detection_callback=self._find_uart_device): device: BLEDevice = await self._discovery_queue.get() - print(f"connecting to {self._peripheral_name} ...", end="") client = self._client = BleakClient(device, disconnected_callback=self._handle_disconnect) await client.connect() await client.start_notify(self.UART_TX_CHAR_UUID, self._rx_handler) - print("Connected!") + print(f"Connected to {self._peripheral_name}!") self.isConnected = True async def disconnect(self): + print(f"Disconnected from {self._peripheral_name}") self.isConnected = False await self._client.disconnect() - - async def __aenter__(self): - return self - - async def __aexit__(self, *args): - await self.disconnect() def _rx_handler(self, _: int, data: bytearray): self._rx_queue.put_nowait(data) diff --git a/controller/controller.py b/controller/controller.py index 915304e..b285d85 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -1,7 +1,8 @@ -import time +# import time import threading from LaptopKeyboard import * from bluetooth import * +import asyncio ir_beacon = BLE_UART(peripheral_name='IR Beacon') alipay = BLE_UART(peripheral_name='Alipay') @@ -11,15 +12,44 @@ keyboard_thread.daemon = True keyboard_thread.start() -async def main(): - await ir_beacon.connect() +cmd = "" + +async def ir_beacon_receive(): + while True: + await asyncio.sleep(0.1) + if (ir_beacon.isConnected): + print(f"[IR BEACON] {await ir_beacon.read()}") +async def alipay_receive(): + while True: + await asyncio.sleep(0.1) + if (alipay.isConnected): + print(f"[ALIPAY] {await alipay.read()}") + +async def alipay_comm(): await alipay.connect() - drivecmd = 0 + while True: + await asyncio.sleep(0.05) + if (alipay.isConnected): + await alipay.write(cmd) + else: + await alipay.connect() + +async def ir_beacon_comm(): + await ir_beacon.connect() + while True: + await asyncio.sleep(0.05) + if (ir_beacon.isConnected): + await ir_beacon.write(cmd) + else: + await ir_beacon.connect() + +async def cmd_handler(): + global cmd enabled = 0 waitForRelease = 0 - while (True): - y = 0 - x = 0 + while True: + x,y,drivecmd,tuning = (0,)*4 + if get_key_state(Key.up): y = y + 1 if get_key_state(Key.down): @@ -47,7 +77,20 @@ async def main(): drivecmd = 7 elif x == -1 and y == 1: drivecmd = 8 - + + if get_key_state('u'): + tuning = 1 + elif get_key_state('j'): + tuning = 2 + elif get_key_state('i'): + tuning = 3 + elif get_key_state('k'): + tuning = 4 + elif get_key_state('o'): + tuning = 5 + elif get_key_state('l'): + tuning = 6 + if get_key_state(Key.space): if get_key_state(Key.ctrl): enabled = 1 @@ -58,11 +101,12 @@ async def main(): if not (get_key_state(Key.space)) and not (get_key_state(Key.ctrl)): waitForRelease = 0 - cmd = f"{enabled}{drivecmd}0000" - if (ir_beacon.isConnected): - await ir_beacon.write(cmd) - if (alipay.isConnected): - await alipay.write(cmd) + cmd = f"{enabled}{drivecmd}{tuning}000" + # print(cmd) + await asyncio.sleep(0.05) + +async def main(): + await asyncio.gather(cmd_handler(), ir_beacon_comm(), alipay_comm(), ir_beacon_receive(), alipay_receive()) asyncio.run(main()) From 1acb128af7b148b604bbf7b86d77d36b49a4a660 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Mon, 26 Feb 2024 23:42:39 -0800 Subject: [PATCH 16/48] file management, increase controller.py readability, ability to change ir beam strength on the fly --- .DS_Store | Bin 6148 -> 6148 bytes .../.DS_Store | Bin .../.gitignore | 0 .../.vscode/extensions.json | 0 .../.vscode/settings.json | 0 .../include/README | 0 .../lib/.DS_Store | Bin .../lib/Battery_Monitor/Battery_Monitor.h | 0 .../lib/Drive_Motors/Drive_Motors.cpp | 0 .../lib/Drive_Motors/Drive_Motors.h | 0 .../lib/LEDHandler/LEDHandler.cpp | 2 +- .../lib/LEDHandler/LEDHandler.h | 0 .../lib/LaptopTelemetry/LaptopTelemetry.cpp | 0 .../lib/LaptopTelemetry/LaptopTelemetry.h | 0 .../lib/Melty/melty.cpp | 0 .../lib/Melty/melty.h | 0 .../lib/README | 0 .../lib/mpu6050/mpu6050.cpp | 0 .../lib/mpu6050/mpu6050.h | 0 .../platformio.ini | 0 .../src/main.cpp | 16 ++-- .../test/README | 0 {controller => Archive}/AlipayTelemetry.py | 0 ESP32 Mac Address/.gitignore | 5 - ESP32 Mac Address/.vscode/extensions.json | 10 -- ESP32 Mac Address/include/README | 39 -------- ESP32 Mac Address/lib/README | 46 --------- ESP32 Mac Address/platformio.ini | 16 ---- ESP32 Mac Address/src/main.cpp | 15 --- ESP32 Mac Address/test/README | 11 --- IR Beacon/lib/BLE_Uart/BLE_Uart.cpp | 4 +- IR Beacon/platformio.ini | 2 +- IR Beacon/src/main.cpp | 50 +++++++--- bluetooth scanner.py | 4 +- .../__pycache__/bluetooth.cpython-311.pyc | Bin 4904 -> 4948 bytes controller/bluetooth.py | 5 +- controller/controller.py | 89 ++++++++++++------ 37 files changed, 113 insertions(+), 201 deletions(-) rename {Alipay ESP32 Code => Alipay Robot Code}/.DS_Store (100%) rename {Alipay ESP32 Code => Alipay Robot Code}/.gitignore (100%) rename {Alipay ESP32 Code => Alipay Robot Code}/.vscode/extensions.json (100%) rename {Alipay ESP32 Code => Alipay Robot Code}/.vscode/settings.json (100%) rename {Alipay ESP32 Code => Alipay Robot Code}/include/README (100%) rename {Alipay ESP32 Code => Alipay Robot Code}/lib/.DS_Store (100%) rename {Alipay ESP32 Code => Alipay Robot Code}/lib/Battery_Monitor/Battery_Monitor.h (100%) rename {Alipay ESP32 Code => Alipay Robot Code}/lib/Drive_Motors/Drive_Motors.cpp (100%) rename {Alipay ESP32 Code => Alipay Robot Code}/lib/Drive_Motors/Drive_Motors.h (100%) rename {Alipay ESP32 Code => Alipay Robot Code}/lib/LEDHandler/LEDHandler.cpp (90%) rename {Alipay ESP32 Code => Alipay Robot Code}/lib/LEDHandler/LEDHandler.h (100%) rename {Alipay ESP32 Code => Alipay Robot Code}/lib/LaptopTelemetry/LaptopTelemetry.cpp (100%) rename {Alipay ESP32 Code => Alipay Robot Code}/lib/LaptopTelemetry/LaptopTelemetry.h (100%) rename {Alipay ESP32 Code => Alipay Robot Code}/lib/Melty/melty.cpp (100%) rename {Alipay ESP32 Code => Alipay Robot Code}/lib/Melty/melty.h (100%) rename {Alipay ESP32 Code => Alipay Robot Code}/lib/README (100%) rename {Alipay ESP32 Code => Alipay Robot Code}/lib/mpu6050/mpu6050.cpp (100%) rename {Alipay ESP32 Code => Alipay Robot Code}/lib/mpu6050/mpu6050.h (100%) rename {Alipay ESP32 Code => Alipay Robot Code}/platformio.ini (100%) rename {Alipay ESP32 Code => Alipay Robot Code}/src/main.cpp (83%) rename {Alipay ESP32 Code => Alipay Robot Code}/test/README (100%) rename {controller => Archive}/AlipayTelemetry.py (100%) delete mode 100644 ESP32 Mac Address/.gitignore delete mode 100644 ESP32 Mac Address/.vscode/extensions.json delete mode 100644 ESP32 Mac Address/include/README delete mode 100644 ESP32 Mac Address/lib/README delete mode 100644 ESP32 Mac Address/platformio.ini delete mode 100644 ESP32 Mac Address/src/main.cpp delete mode 100644 ESP32 Mac Address/test/README diff --git a/.DS_Store b/.DS_Store index 9549f95f0a1bcf4cdc78c619b917b6ab403208ff..e312e14cd6bc2d4bd406cd59ea040cff93f48ff9 100644 GIT binary patch delta 50 zcmZoMXfc@JFUrKgz`)4BAi%(o%8 delayMS + 500) { + if (color1 != lastColor1 || color2 != lastColor2 || millis() - lastdelayToggle > delayMS + 50) { // For color syncing purposes between different devices lastdelayToggle = millis(); ledToggleState = 0; } diff --git a/Alipay ESP32 Code/lib/LEDHandler/LEDHandler.h b/Alipay Robot Code/lib/LEDHandler/LEDHandler.h similarity index 100% rename from Alipay ESP32 Code/lib/LEDHandler/LEDHandler.h rename to Alipay Robot Code/lib/LEDHandler/LEDHandler.h diff --git a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp b/Alipay Robot Code/lib/LaptopTelemetry/LaptopTelemetry.cpp similarity index 100% rename from Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.cpp rename to Alipay Robot Code/lib/LaptopTelemetry/LaptopTelemetry.cpp diff --git a/Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.h b/Alipay Robot Code/lib/LaptopTelemetry/LaptopTelemetry.h similarity index 100% rename from Alipay ESP32 Code/lib/LaptopTelemetry/LaptopTelemetry.h rename to Alipay Robot Code/lib/LaptopTelemetry/LaptopTelemetry.h diff --git a/Alipay ESP32 Code/lib/Melty/melty.cpp b/Alipay Robot Code/lib/Melty/melty.cpp similarity index 100% rename from Alipay ESP32 Code/lib/Melty/melty.cpp rename to Alipay Robot Code/lib/Melty/melty.cpp diff --git a/Alipay ESP32 Code/lib/Melty/melty.h b/Alipay Robot Code/lib/Melty/melty.h similarity index 100% rename from Alipay ESP32 Code/lib/Melty/melty.h rename to Alipay Robot Code/lib/Melty/melty.h diff --git a/Alipay ESP32 Code/lib/README b/Alipay Robot Code/lib/README similarity index 100% rename from Alipay ESP32 Code/lib/README rename to Alipay Robot Code/lib/README diff --git a/Alipay ESP32 Code/lib/mpu6050/mpu6050.cpp b/Alipay Robot Code/lib/mpu6050/mpu6050.cpp similarity index 100% rename from Alipay ESP32 Code/lib/mpu6050/mpu6050.cpp rename to Alipay Robot Code/lib/mpu6050/mpu6050.cpp diff --git a/Alipay ESP32 Code/lib/mpu6050/mpu6050.h b/Alipay Robot Code/lib/mpu6050/mpu6050.h similarity index 100% rename from Alipay ESP32 Code/lib/mpu6050/mpu6050.h rename to Alipay Robot Code/lib/mpu6050/mpu6050.h diff --git a/Alipay ESP32 Code/platformio.ini b/Alipay Robot Code/platformio.ini similarity index 100% rename from Alipay ESP32 Code/platformio.ini rename to Alipay Robot Code/platformio.ini diff --git a/Alipay ESP32 Code/src/main.cpp b/Alipay Robot Code/src/main.cpp similarity index 83% rename from Alipay ESP32 Code/src/main.cpp rename to Alipay Robot Code/src/main.cpp index 59cf20d..e76f780 100644 --- a/Alipay ESP32 Code/src/main.cpp +++ b/Alipay Robot Code/src/main.cpp @@ -8,7 +8,7 @@ #include const int packSize = 6; -char laptop_packetBuffer[packSize] = {}; +char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; BLE_Uart laptop = BLE_Uart(laptop_packetBuffer, packSize); @@ -33,13 +33,13 @@ void setup() void loop() { if (laptop.isConnected()) { - if (laptop_packetBuffer[0] == '1') { // Currently enabled and translating!! + if (laptop_packetBuffer[0] == '1' || laptop_packetBuffer[0] == '2') { // Currently enabled and translating!! alipay.update(); if (alipay.translate()) { l_motor_write(melty_parameters.rot-melty_parameters.tra); r_motor_write(melty_parameters.rot+melty_parameters.tra); } else { - set_both_motors(melty_parameters.rot); + set_both_motors(melty_parameters.rot); } switch (laptop_packetBuffer[1]) { // Check the drive cmd @@ -75,11 +75,6 @@ void loop() alipay.percentageOfRotation = 0; } - EVERY_N_MILLIS(50) { // Print out the settings - String msg = "rotpwr : " + String(melty_parameters.rot) + " tranpwr : " + String(melty_parameters.tra) + " perc : " + String(melty_parameters.per); - laptop.send(msg); - } - EVERY_N_MILLIS(100) { switch (laptop_packetBuffer[2]) { case '1': @@ -101,6 +96,11 @@ void loop() melty_parameters.per = melty_parameters.per - .03; break; } + + if (laptop_packetBuffer[2] != '0') { + String msg = "rotpwr : " + String(melty_parameters.rot) + " tranpwr : " + String(melty_parameters.tra) + " perc : " + String(melty_parameters.per); + laptop.send(msg); + } } } else { // Currently disabled diff --git a/Alipay ESP32 Code/test/README b/Alipay Robot Code/test/README similarity index 100% rename from Alipay ESP32 Code/test/README rename to Alipay Robot Code/test/README diff --git a/controller/AlipayTelemetry.py b/Archive/AlipayTelemetry.py similarity index 100% rename from controller/AlipayTelemetry.py rename to Archive/AlipayTelemetry.py diff --git a/ESP32 Mac Address/.gitignore b/ESP32 Mac Address/.gitignore deleted file mode 100644 index 89cc49c..0000000 --- a/ESP32 Mac Address/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.pio -.vscode/.browse.c_cpp.db* -.vscode/c_cpp_properties.json -.vscode/launch.json -.vscode/ipch diff --git a/ESP32 Mac Address/.vscode/extensions.json b/ESP32 Mac Address/.vscode/extensions.json deleted file mode 100644 index 080e70d..0000000 --- a/ESP32 Mac Address/.vscode/extensions.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - // 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/ESP32 Mac Address/include/README b/ESP32 Mac Address/include/README deleted file mode 100644 index 194dcd4..0000000 --- a/ESP32 Mac Address/include/README +++ /dev/null @@ -1,39 +0,0 @@ - -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/ESP32 Mac Address/lib/README b/ESP32 Mac Address/lib/README deleted file mode 100644 index 6debab1..0000000 --- a/ESP32 Mac Address/lib/README +++ /dev/null @@ -1,46 +0,0 @@ - -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 -#include - -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/ESP32 Mac Address/platformio.ini b/ESP32 Mac Address/platformio.ini deleted file mode 100644 index f028691..0000000 --- a/ESP32 Mac Address/platformio.ini +++ /dev/null @@ -1,16 +0,0 @@ -; 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-s3-devkitc-1] -platform = espressif32 -board = esp32-s3-devkitc-1 -framework = arduino -upload_speed = 921600 -monitor_speed = 115200 diff --git a/ESP32 Mac Address/src/main.cpp b/ESP32 Mac Address/src/main.cpp deleted file mode 100644 index c840377..0000000 --- a/ESP32 Mac Address/src/main.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include - -void setup() { - // put your setup code here, to run once: - USBSerial.begin(115200); - USBSerial.println(); - USBSerial.print("ESP Board MAC Address: "); - USBSerial.println(WiFi.macAddress()); -} - -void loop() { - delay(1000); - USBSerial.println(WiFi.macAddress()); - // put your main code here, to run repeatedly: -} \ No newline at end of file diff --git a/ESP32 Mac Address/test/README b/ESP32 Mac Address/test/README deleted file mode 100644 index 9b1e87b..0000000 --- a/ESP32 Mac Address/test/README +++ /dev/null @@ -1,11 +0,0 @@ - -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 diff --git a/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp b/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp index f473577..718ebb3 100644 --- a/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp +++ b/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp @@ -41,9 +41,9 @@ bool BLE_Uart::isConnected() { } void BLE_Uart::init_ble(const std::string &name) { - BLEDevice::setDeviceName(name); BLEDevice::init(name); - + BLEDevice::setDeviceName(name); + pServer = BLEDevice::createServer(); // Create the BLE Server pServer->setCallbacks(new MyServerCallbacks()); diff --git a/IR Beacon/platformio.ini b/IR Beacon/platformio.ini index e9af7cd..5825065 100644 --- a/IR Beacon/platformio.ini +++ b/IR Beacon/platformio.ini @@ -21,4 +21,4 @@ lib_deps = fastled/FastLED@^3.6.0 h2zero/NimBLE-Arduino@^1.4.1 lib_extra_dirs = - /Users/mingweiyeoh/Documents/GitHub/Alipay/Alipay ESP32 Code/lib + /Users/mingweiyeoh/Documents/GitHub/Alipay/Alipay Robot Code/lib diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp index 6f4385b..6c17618 100644 --- a/IR Beacon/src/main.cpp +++ b/IR Beacon/src/main.cpp @@ -6,14 +6,18 @@ #include #include +#define TARGETCMD '2' // Change based on which IR_Beacon working on + const int packSize = 6; -char laptop_packetBuffer[packSize] = {}; +char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; const int IRLedPin = 4; const int freq = 38000; const int ledChannel = 0; const int resolution = 8; +int dutycycle = 100; +const int maxdutycycle = 180; BLE_Uart laptop = BLE_Uart(laptop_packetBuffer, packSize); @@ -23,26 +27,46 @@ void setup(){ USBSerial.begin(115200); ledcSetup(ledChannel, freq, resolution); ledcAttachPin(IRLedPin, ledChannel); - laptop.init_ble("IR Beacon"); setLeds(CRGB::Black); } void loop(){ - if (laptop.isConnected()) { - if (laptop_packetBuffer[0] == '0') {// Robot is disabled - toggleLeds(CRGB::Red, CRGB::Green, 500); - ledcWrite(ledChannel, 0); - } else { - toggleLeds(CRGB::Blue, CRGB::Purple, 500); - ledcWrite(ledChannel, 100); + switch (laptop_packetBuffer[0]) { + case '0': // Disabled + toggleLeds(CRGB::Red, CRGB::Green, 500); + ledcWrite(ledChannel, 0); + EVERY_N_SECONDS(10) + laptop.send(get1sVoltage()); + break; + case TARGETCMD: // Enable IR Beacon + toggleLeds(CRGB::Blue, CRGB::Purple, 500); + ledcWrite(ledChannel, dutycycle); + break; + default: // Don't enable current IR Beacon + setLeds(CRGB::Black); + ledcWrite(ledChannel, 0); + break; } - - EVERY_N_SECONDS(10) { - laptop.send(get1sVoltage()); + EVERY_N_MILLIS(100) { + if (laptop_packetBuffer[0] == TARGETCMD) { + switch (laptop_packetBuffer[2]) { + case '7': + dutycycle++; + if (dutycycle > maxdutycycle) + dutycycle = maxdutycycle; + break; + case '8': + dutycycle--; + break; + } + if (laptop_packetBuffer[2] == '7' || laptop_packetBuffer[2] == '8') { + String msg = "dutycycle : " + String(dutycycle); + laptop.send(msg); + } + } } - } else { toggleLeds(CRGB::Red, CRGB::Black, 500); ledcWrite(ledChannel, 0); diff --git a/bluetooth scanner.py b/bluetooth scanner.py index 653ba3a..c08a78d 100644 --- a/bluetooth scanner.py +++ b/bluetooth scanner.py @@ -2,9 +2,9 @@ from bleak import BleakScanner async def scan(): - devices = await BleakScanner.discover(timeout=1) + devices = await BleakScanner.discover(timeout=2) for device in devices: if device.name is not None: print(device) -asyncio.run(scan()) \ No newline at end of file +asyncio.run(scan()) \ No newline at end of file diff --git a/controller/__pycache__/bluetooth.cpython-311.pyc b/controller/__pycache__/bluetooth.cpython-311.pyc index fb1205634b6ab8de627a6fd67f90fe6af69f37ff..33b856979feb3dcde5b907b12806a6b14798dcde 100644 GIT binary patch delta 922 zcmZvaO=uHA6vuaVH!<0``Dz+sOiiNLnxst|>j(XyqL7271Zg99iObquNi=tdv z!Hd{KPR>EaUPTKcNN$37_9oIpS@7y1r-};V$@jKNO4T|1=6&&hGjHbA=+3D8QkEqS z*6a2cz4%ssNAd!?y~V?h{`Y0T6Rt&CoYG5iT#E;f^j*`@w>a*Oz*I*#2Whb%)VLLH zn-o$G%da$aT4|JZs#cYnx~5w$Mb$K_8wQ)>V`G9P8hZ7%<@>wrNUzGk6D63*mxoV+ z^?=(=Om2!hu94kHd{b=8oh~mC;%7gFGzqYT7>b6V%o3@tYEAjdQeL@!dAXQHH`pnN zT@r~xo*qZU0Q>rh1P!rk&L}z7x$9gKoL)@(*n9UZiLhVp5t3wKX%QNzQY?lIXco}) z5A;{-wVJ+a#yiiXH+(*W6-NTA$9}juG|w8!`P4iBy6caqAJCk delta 855 zcmZvaO=uHQ5XblJZqj7qZg!IpleB3PEU}xEZv2R~RV)<+ky6kqh*Fo3EGE)?_>vec z6+9O4HliN9iY=lb!HWkEf_N3lLDqW@o+5hktMZqm&jsn}>ITD7H6ekIom(CLv9!O(j0Srzttf{-n z#d@t~6iE2*mRRHuvQ&6Jd-0N18?OO5`v&o&iR?!mKyDr zS*v#nm6k3zN%Sbn^9)1ZK@E7RM2P_(m4iwMC5JFyD3oqPLJ5QFujFkJhrZL$Bub7m z{4I-eI>#_vHUqExiP6L8pJ8Ad@M#ZEJe}ypj4wXpv(JESf6AR>1`UC%u8gBf=l5E5 zHi!#hewu|1HK`p#a~6S(F6ji`tMd~nbW9_hVFl5OBMu;N&NK=sEtEFUbR%4d3_?U? z8Mbzcvi)DvW<$J7?{mZOz^3+Gb|&OudfiK9xEffF;cxBq<8=DlbFx}5wO6fE^c;K& UT*>FiWS{#NKhXE Date: Tue, 27 Feb 2024 17:09:25 -0800 Subject: [PATCH 17/48] before rewriting melty algo --- .DS_Store | Bin 6148 -> 6148 bytes Alipay Robot Code/lib/Melty/melty.cpp | 23 +++++++++++++++----- Alipay Robot Code/src/main.cpp | 7 ++++-- IR Beacon/lib/BLE_Uart/BLE_Uart.h | 1 - IR Beacon/src/main.cpp | 1 - controller/controller.py | 30 +------------------------- 6 files changed, 24 insertions(+), 38 deletions(-) diff --git a/.DS_Store b/.DS_Store index e312e14cd6bc2d4bd406cd59ea040cff93f48ff9..eebe13f7cdcc9b0f7e74683cab35d2ac78cd8158 100644 GIT binary patch delta 159 zcmZoMXfc@JFUrcmz`)4BAi%(o$WY0kz!1cc&yciPka;;{AV^A(!I2?{A(Np1SsJLM z1gO**NT)EQrW7aVB<1Jl08L@wVn_xG<^iRPfOHNJrZN=46fi92VPcf!V(uDFIsC$FiB7<1aq|px+|g delta 54 zcmZoMXfc@JFUrKgz`)4BAi%(o%8 #include -#include "USB.h" #include #include #include @@ -75,6 +74,10 @@ void loop() alipay.percentageOfRotation = 0; } + EVERY_N_SECONDS(1) { + // String msg = "RPM: " + } + EVERY_N_MILLIS(100) { switch (laptop_packetBuffer[2]) { case '1': @@ -97,7 +100,7 @@ void loop() break; } - if (laptop_packetBuffer[2] != '0') { + if (!(laptop_packetBuffer[2] == '0' || laptop_packetBuffer[2] == '7' || laptop_packetBuffer[2] == '8')) { String msg = "rotpwr : " + String(melty_parameters.rot) + " tranpwr : " + String(melty_parameters.tra) + " perc : " + String(melty_parameters.per); laptop.send(msg); } diff --git a/IR Beacon/lib/BLE_Uart/BLE_Uart.h b/IR Beacon/lib/BLE_Uart/BLE_Uart.h index 2b1083e..21d8a57 100644 --- a/IR Beacon/lib/BLE_Uart/BLE_Uart.h +++ b/IR Beacon/lib/BLE_Uart/BLE_Uart.h @@ -3,7 +3,6 @@ #define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" #include -#include class BLE_Uart { public: diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp index 6c17618..bf3dbde 100644 --- a/IR Beacon/src/main.cpp +++ b/IR Beacon/src/main.cpp @@ -1,7 +1,6 @@ #include #include #include -#include "USB.h" #include #include #include diff --git a/controller/controller.py b/controller/controller.py index 9621f92..81800f0 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -1,5 +1,4 @@ - -# sudo pkill bluetoothd if bluetooth not working on mac +# `sudo pkill bluetoothd` if bluetooth not working on ming's mac m3 pro import threading from LaptopKeyboard import * @@ -31,33 +30,6 @@ async def bluetooth_comm_handler(BLE_DEVICE): await BLE_DEVICE.write(cmd) else: await BLE_DEVICE.connect() - -# async def alipay_comm(): -# await alipay.connect() -# while True: -# await asyncio.sleep(0.05) -# if (alipay.isConnected): -# await alipay.write(cmd) -# else: -# await alipay.connect() - -# async def ir_beacon_1_comm(): -# await ir_beacon_1.connect() -# while True: -# await asyncio.sleep(0.05) -# if (ir_beacon_1.isConnected): -# await ir_beacon_1.write(cmd) -# else: -# await ir_beacon_1.connect() - -# async def ir_beacon_2_comm(): -# await ir_beacon_2.connect() -# while True: -# await asyncio.sleep(0.05) -# if (ir_beacon_2.isConnected): -# await ir_beacon_2.write(cmd) -# else: -# await ir_beacon_2.connect() async def cmd_handler(): global cmd From a186496ea96dd8627b8edea557c1dce9698d7305 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Wed, 28 Feb 2024 16:16:51 -0800 Subject: [PATCH 18/48] working well with one filter --- Alipay Robot Code/lib/Melty/melty.cpp | 67 +++++++++++---------- Alipay Robot Code/lib/Melty/melty.h | 87 ++++++++++++++++++++++++--- Alipay Robot Code/src/main.cpp | 9 ++- IR Beacon/src/main.cpp | 6 +- 4 files changed, 122 insertions(+), 47 deletions(-) diff --git a/Alipay Robot Code/lib/Melty/melty.cpp b/Alipay Robot Code/lib/Melty/melty.cpp index f364dbe..d6fb3ff 100644 --- a/Alipay Robot Code/lib/Melty/melty.cpp +++ b/Alipay Robot Code/lib/Melty/melty.cpp @@ -2,7 +2,8 @@ #include melty::melty() { - + period_micros_calc = ringBuffer(1); + time_seen_beacon_calc = ringBuffer(0.5); } void melty::update() { @@ -10,49 +11,51 @@ void melty::update() { if (curSeenIRLed != lastSeenIRLed) if (curSeenIRLed) { // Activates on the rising edge of seeing the IR LED setLeds(CRGB::Green); - currentPulse = millis(); - unsigned long period_MS = currentPulse - lastPulse; // How long it takes to complete one revolution - RPM = 60000/(period_MS); + currentPulse = micros(); + period_micros = currentPulse - lastPulse; // How long it takes to complete one revolution + period_micros_calc.update(period_micros); lastPulse = currentPulse; - - unsigned long center_of_beacon = currentPulse + time_seen_beacon/2; // This should ideally be centered on the beacon - unsigned long centerOfDrivePulse = center_of_beacon + (float(deg)/360)*period_MS; // Direction that we should be driving towards - unsigned long deltaDriveTiming = (percentageOfRotation * period_MS)/2; - if (timingToggle) { // If it is going to translate whilst computing the new timings we should use a different set of variables - endDrive = centerOfDrivePulse + deltaDriveTiming; - startDrive = centerOfDrivePulse - deltaDriveTiming; - } else { // Do the same but calculate with the other values - endDrive2 = centerOfDrivePulse + deltaDriveTiming; - startDrive2 = centerOfDrivePulse - deltaDriveTiming; - } - - timingToggle = !timingToggle; // Toggle it so that next iteration uses different variables } else { // Activates on the falling edge of seeing the IR LED setLeds(CRGB::Red); - time_seen_beacon = millis() - lastPulse; + time_seen_beacon = micros() - currentPulse; + time_seen_beacon_calc.update(time_seen_beacon); + + if (time_seen_beacon_calc.isLegit(time_seen_beacon)) {// && period_ms_calc.isLegit(period_MS)) + computeTimings(); + } } - /* A better IR Beam algorithm planning: - I think that we want to use the falling edge of when the time_seen_beacon is the biggest per revolution. This is to minimize - Interference from other objects that are producing very annoying reflections. + + lastSeenIRLed = curSeenIRLed; +} - Everytime we have a new time_seen_beacon value: - If the new value is within some amount of the max value from our ring buffer: - calculate period_ms from that - if calculated period_ms is within some margin from another ring buffer: - that is the value we should based our calculations off of - - store value in a ring buffer - +void melty::computeTimings() { + RPM = (60000*1000)/(period_micros_calc.getMaxVal()); + unsigned long center_of_beacon = currentPulse + time_seen_beacon/2; // This should ideally be centered on the beacon + unsigned long centerOfDrivePulse = center_of_beacon + (float(deg)/360)*period_micros_calc.getMaxVal(); // Direction that we should be driving towards + unsigned long deltaDriveTiming = (percentageOfRotation * period_micros_calc.getMaxVal())/2; - */ + unsigned long offset = 0; + if (centerOfDrivePulse - deltaDriveTiming < micros()) { // If we're supposed to start translating before we have stored those values, offset by one rotation + // setLeds(CRGB::White); + offset = period_micros_calc.getMaxVal(); + } + + if (timingToggle) { // It is going to translate whilst computing the new timings we should use a different set of variables + startDrive = centerOfDrivePulse - deltaDriveTiming + offset; + endDrive = centerOfDrivePulse + deltaDriveTiming + offset; + } else { // Do the same but calculate with the other values + startDrive2 = centerOfDrivePulse - deltaDriveTiming + offset; + endDrive2 = centerOfDrivePulse + deltaDriveTiming + offset; + } + + timingToggle = !timingToggle; // Toggle it so that next iteration uses different variables - lastSeenIRLed = curSeenIRLed; } bool melty::translate() { // Returns whether or not robot should translate now - unsigned long currentTime = millis(); + unsigned long currentTime = micros(); if (percentageOfRotation != 0) return (currentTime > startDrive && currentTime < endDrive || currentTime > startDrive2 && currentTime < endDrive2); else diff --git a/Alipay Robot Code/lib/Melty/melty.h b/Alipay Robot Code/lib/Melty/melty.h index cb33b1a..99cce71 100644 --- a/Alipay Robot Code/lib/Melty/melty.h +++ b/Alipay Robot Code/lib/Melty/melty.h @@ -1,33 +1,100 @@ #include #include +/* + +two uses for ringbuffer is calculating +period_ms +time_seen_beacon +*/ + + +#define RINGBUFSIZE 5 +class ringBuffer{ + public: + /** + * @brief Construct a ring buffer object. + * + * @param _criteria How close should the new value match the Max value from the ring buffer to be considered legit? + */ + ringBuffer(float _criteria) { + criteria = _criteria; + } + + ringBuffer() {} + + void update(unsigned long val) { + ringBuf[curIndex++] = val; + if (curIndex > RINGBUFSIZE) + curIndex = 0; + } + + bool isLegit(unsigned long newVal) { + unsigned long maxVal = getMaxVal(); + if (newVal > maxVal * criteria) + return true; + else + return false; + } + + // private: + unsigned long ringBuf[RINGBUFSIZE]; + int curIndex = 0; + float criteria = 0; + + unsigned long getMaxVal() { + unsigned long maxVal = 0; + for (int i = 0; i < RINGBUFSIZE; i++) + if (maxVal < ringBuf[i]) + maxVal = ringBuf[i]; + return maxVal; + } + + String returnArray() { + String msg = ""; + for (int i = 0; i < RINGBUFSIZE; i++) { + msg = msg + String(ringBuf[i]) + " "; + } + return msg; + } +}; + + + #define TOP_IR_PIN 10 #define BOTTOM_IR_PIN 9 -#define IRLedDataSize 15 +#define IRLedDataSize 25 class melty { public: melty(); void update(); + // bool isLegit(); bool isBeaconSensed(bool currentReading); + void computeTimings(); bool translate(); int RPM = 0; int deg = 0; float percentageOfRotation = 0; - private: + // private: bool lastSeenIRLed = 0; - unsigned long currentPulse = millis(); - unsigned long lastPulse = millis(); - unsigned long time_seen_beacon = millis(); + unsigned long period_micros = micros(); + unsigned long currentPulse = micros(); + unsigned long lastPulse = micros(); + unsigned long time_seen_beacon = micros(); bool IRLedReadings[IRLedDataSize] = {0}; int IRLedIndex = 0; bool lastIRLedReturnValue = 0; - unsigned long startDrive = millis(); - unsigned long endDrive = millis(); + unsigned long startDrive = micros(); + unsigned long endDrive = micros(); - unsigned long startDrive2 = millis(); - unsigned long endDrive2 = millis(); + unsigned long startDrive2 = micros(); + unsigned long endDrive2 = micros(); bool timingToggle = 0; -}; \ No newline at end of file + + ringBuffer period_micros_calc; + ringBuffer time_seen_beacon_calc; +}; + diff --git a/Alipay Robot Code/src/main.cpp b/Alipay Robot Code/src/main.cpp index 69ab487..37bb0d5 100644 --- a/Alipay Robot Code/src/main.cpp +++ b/Alipay Robot Code/src/main.cpp @@ -13,7 +13,7 @@ BLE_Uart laptop = BLE_Uart(laptop_packetBuffer, packSize); melty alipay = melty(); struct melty_parameters { - int rot = 15; + int rot = 10; int tra = 5; float per = 0.5; } melty_parameters; @@ -74,8 +74,11 @@ void loop() alipay.percentageOfRotation = 0; } - EVERY_N_SECONDS(1) { - // String msg = "RPM: " + EVERY_N_SECONDS(1) { // DEBUGGIN!!!! + // String msg = " time_seen beacon: " + alipay.time_seen_beacon_calc.returnArray(); + // laptop.send(msg); + String msg = "rpm : " + String(alipay.RPM); + laptop.send(msg); } EVERY_N_MILLIS(100) { diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp index bf3dbde..a1fd2dc 100644 --- a/IR Beacon/src/main.cpp +++ b/IR Beacon/src/main.cpp @@ -52,12 +52,14 @@ void loop(){ if (laptop_packetBuffer[0] == TARGETCMD) { switch (laptop_packetBuffer[2]) { case '7': - dutycycle++; + dutycycle+=3; if (dutycycle > maxdutycycle) dutycycle = maxdutycycle; break; case '8': - dutycycle--; + dutycycle-=3; + if (dutycycle < 0) + dutycycle = 0; break; } if (laptop_packetBuffer[2] == '7' || laptop_packetBuffer[2] == '8') { From 5aa920b4229852c1b2fe26e92834880c92346066 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Thu, 29 Feb 2024 13:13:50 -0800 Subject: [PATCH 19/48] motors are now bidrectional, about to fix up alipay code --- Alipay Robot Code/lib/Melty/melty.cpp | 7 +++---- Alipay Robot Code/lib/Melty/melty.h | 1 - Motor_Test/src/main.cpp | 15 +++++++++------ 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Alipay Robot Code/lib/Melty/melty.cpp b/Alipay Robot Code/lib/Melty/melty.cpp index d6fb3ff..5e24b44 100644 --- a/Alipay Robot Code/lib/Melty/melty.cpp +++ b/Alipay Robot Code/lib/Melty/melty.cpp @@ -37,15 +37,14 @@ void melty::computeTimings() { unsigned long deltaDriveTiming = (percentageOfRotation * period_micros_calc.getMaxVal())/2; unsigned long offset = 0; - if (centerOfDrivePulse - deltaDriveTiming < micros()) { // If we're supposed to start translating before we have stored those values, offset by one rotation - // setLeds(CRGB::White); + if (centerOfDrivePulse - deltaDriveTiming < micros()) { // If we're supposed to start translating before we have calculated those values, offset by one rotation offset = period_micros_calc.getMaxVal(); } - if (timingToggle) { // It is going to translate whilst computing the new timings we should use a different set of variables + if (timingToggle) { // Swap variables that we use so we don't interfere (in the case that we're currently translating whilst computing) startDrive = centerOfDrivePulse - deltaDriveTiming + offset; endDrive = centerOfDrivePulse + deltaDriveTiming + offset; - } else { // Do the same but calculate with the other values + } else { startDrive2 = centerOfDrivePulse - deltaDriveTiming + offset; endDrive2 = centerOfDrivePulse + deltaDriveTiming + offset; } diff --git a/Alipay Robot Code/lib/Melty/melty.h b/Alipay Robot Code/lib/Melty/melty.h index 99cce71..58d930b 100644 --- a/Alipay Robot Code/lib/Melty/melty.h +++ b/Alipay Robot Code/lib/Melty/melty.h @@ -69,7 +69,6 @@ class melty { public: melty(); void update(); - // bool isLegit(); bool isBeaconSensed(bool currentReading); void computeTimings(); bool translate(); diff --git a/Motor_Test/src/main.cpp b/Motor_Test/src/main.cpp index d4c356a..674e13a 100644 --- a/Motor_Test/src/main.cpp +++ b/Motor_Test/src/main.cpp @@ -3,9 +3,9 @@ SerialHandler myComputer = SerialHandler(); -const int motpin = 8; +const int motpin = 8; // 7 is right and 8 is left const int motchannel = 0; -const int neutralVal = 120; +const int neutralVal = 189; void autoMotorTune() { delay(1000); @@ -14,16 +14,19 @@ void autoMotorTune() { delay(3000); USBSerial.println("Turn motor on!!!"); delay(5000); - USBSerial.println("Calibrating neutral"); - ledcWrite(motchannel, neutralVal); // Calibrate neutral stick - delay(10000); + ledcWrite(motchannel, 40); + USBSerial.println("Tuning low value!!!"); + delay(2500); + ledcWrite(motchannel, neutralVal); + USBSerial.println("Tuning neutral value!!!"); + delay(2500); USBSerial.println("Done!!!"); } void setup() { USBSerial.begin(115200); - ledcSetup(motchannel, 10000, 8); + ledcSetup(motchannel, 500, 8); ledcAttachPin(motpin, motchannel); ledcWrite(motchannel, neutralVal); From d4beb0e7ccab29cbc08a62b741c92cccbd2086c2 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Thu, 29 Feb 2024 16:52:17 -0800 Subject: [PATCH 20/48] motors bidrectional and robot controls are inverteable --- .../lib/Drive_Motors/Drive_Motors.cpp | 40 ++++++- .../lib/Drive_Motors/Drive_Motors.h | 22 ++-- Alipay Robot Code/lib/Melty/melty.cpp | 7 +- Alipay Robot Code/lib/Melty/melty.h | 1 + Alipay Robot Code/src/main.cpp | 109 +++++++++++++++--- IR Beacon/src/main.cpp | 23 ++-- controller/controller.py | 60 ++++++---- 7 files changed, 204 insertions(+), 58 deletions(-) diff --git a/Alipay Robot Code/lib/Drive_Motors/Drive_Motors.cpp b/Alipay Robot Code/lib/Drive_Motors/Drive_Motors.cpp index 1b247ab..92dbc44 100644 --- a/Alipay Robot Code/lib/Drive_Motors/Drive_Motors.cpp +++ b/Alipay Robot Code/lib/Drive_Motors/Drive_Motors.cpp @@ -1,7 +1,9 @@ #include "Drive_Motors.h" #include -void init_motors() { + + +void Drive_Motors::init_motors() { ledcSetup(lMotChannel, motFreq, resolution); ledcSetup(rMotChannel, motFreq, resolution); ledcAttachPin(lMotPin, lMotChannel); @@ -11,15 +13,41 @@ void init_motors() { r_motor_write(neutralValue); } -void l_motor_write(int value) { // From 0 -> 100 - ledcWrite(lMotChannel, neutralValue + value); +void Drive_Motors::l_motor_write(int value) { + l_motor_value = value; + if (value != 0) + if (value > 0) + value += 2; + else + value -= 2; + if (!flip_motors) + ledcWrite(lMotChannel, neutralValue + value); + else + ledcWrite(rMotChannel, neutralValue + value); } -void r_motor_write(int value) { // From 0 -> 100 - ledcWrite(rMotChannel, neutralValue + value); +void Drive_Motors::r_motor_write(int value) { + r_motor_value = 0; + if (value != 0) + if (value > 0) + value += 2; + else + value -= 2; + + if (!flip_motors) + ledcWrite(rMotChannel, neutralValue + value); + else + ledcWrite(lMotChannel, neutralValue + value); } -void set_both_motors(int value) { +void Drive_Motors::set_both_motors(int value) { l_motor_write(value); r_motor_write(value); +} + +bool Drive_Motors::isNeutral() { + if (l_motor_value == 0 && r_motor_value == 0) + return true; + else + return false; } \ No newline at end of file diff --git a/Alipay Robot Code/lib/Drive_Motors/Drive_Motors.h b/Alipay Robot Code/lib/Drive_Motors/Drive_Motors.h index 9a9508a..bd02117 100644 --- a/Alipay Robot Code/lib/Drive_Motors/Drive_Motors.h +++ b/Alipay Robot Code/lib/Drive_Motors/Drive_Motors.h @@ -1,16 +1,24 @@ #define lMotPin 8 #define rMotPin 7 -#define motFreq 10000 +#define motFreq 500 #define resolution 8 // # of bits #define lMotChannel 0 #define rMotChannel 1 -#define neutralValue 120 +#define neutralValue 189 -void init_motors(); -void l_motor_write(int value); +class Drive_Motors { + public: + Drive_Motors() {} + void init_motors(); + void l_motor_write(int value); + void r_motor_write(int value); + void set_both_motors(int value); + bool isNeutral(); + bool flip_motors = 0; + private: + int l_motor_value = 0; + int r_motor_value = 0; +}; -void r_motor_write(int value); - -void set_both_motors(int value); \ No newline at end of file diff --git a/Alipay Robot Code/lib/Melty/melty.cpp b/Alipay Robot Code/lib/Melty/melty.cpp index 5e24b44..25c5933 100644 --- a/Alipay Robot Code/lib/Melty/melty.cpp +++ b/Alipay Robot Code/lib/Melty/melty.cpp @@ -7,7 +7,12 @@ melty::melty() { } void melty::update() { - bool curSeenIRLed = isBeaconSensed(!digitalRead(TOP_IR_PIN)); + bool curSeenIRLed; + if (useTopIr) + curSeenIRLed = isBeaconSensed(!digitalRead(TOP_IR_PIN)); + else + curSeenIRLed = isBeaconSensed(!digitalRead(BOTTOM_IR_PIN)); + if (curSeenIRLed != lastSeenIRLed) if (curSeenIRLed) { // Activates on the rising edge of seeing the IR LED setLeds(CRGB::Green); diff --git a/Alipay Robot Code/lib/Melty/melty.h b/Alipay Robot Code/lib/Melty/melty.h index 58d930b..9ad4f84 100644 --- a/Alipay Robot Code/lib/Melty/melty.h +++ b/Alipay Robot Code/lib/Melty/melty.h @@ -75,6 +75,7 @@ class melty { int RPM = 0; int deg = 0; float percentageOfRotation = 0; + bool useTopIr = 1; // private: bool lastSeenIRLed = 0; unsigned long period_micros = micros(); diff --git a/Alipay Robot Code/src/main.cpp b/Alipay Robot Code/src/main.cpp index 37bb0d5..7e00c5d 100644 --- a/Alipay Robot Code/src/main.cpp +++ b/Alipay Robot Code/src/main.cpp @@ -10,21 +10,27 @@ const int packSize = 6; char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; BLE_Uart laptop = BLE_Uart(laptop_packetBuffer, packSize); +Drive_Motors driveMotors = Drive_Motors(); melty alipay = melty(); struct melty_parameters { int rot = 10; - int tra = 5; + int tra = 6; float per = 0.5; } melty_parameters; +struct tank_drive_parameters { + int drive = 3; + int turn = 2; +} tank_drive_parameters; + void setup() { init_led(); setLeds(CRGB::Green); USBSerial.begin(115200); init_mpu6050(); - init_motors(); + driveMotors.init_motors(); laptop.init_ble("Alipay"); setLeds(CRGB::Black); } @@ -32,13 +38,24 @@ void setup() void loop() { if (laptop.isConnected()) { - if (laptop_packetBuffer[0] == '1' || laptop_packetBuffer[0] == '2') { // Currently enabled and translating!! + if (driveMotors.isNeutral()) { + if (getAccelZ() > 7) { + alipay.useTopIr = 1; + driveMotors.flip_motors = 0; + } + if (getAccelZ() < -7) { + alipay.useTopIr = 0; + driveMotors.flip_motors = 1; + } + } + + if (laptop_packetBuffer[0] == '1') { // Currently enabled and meltybraining!!! alipay.update(); if (alipay.translate()) { - l_motor_write(melty_parameters.rot-melty_parameters.tra); - r_motor_write(melty_parameters.rot+melty_parameters.tra); + driveMotors.l_motor_write((melty_parameters.rot-melty_parameters.tra)); + driveMotors.r_motor_write((melty_parameters.rot+melty_parameters.tra)); } else { - set_both_motors(melty_parameters.rot); + driveMotors.set_both_motors(melty_parameters.rot); } switch (laptop_packetBuffer[1]) { // Check the drive cmd @@ -68,15 +85,13 @@ void loop() break; } - if (laptop_packetBuffer[1] != '0') {// Check drive cmd + if (laptop_packetBuffer[1] != '0') {// Check drive cmd for setting the "neutral" state alipay.percentageOfRotation = melty_parameters.per; } else { alipay.percentageOfRotation = 0; } EVERY_N_SECONDS(1) { // DEBUGGIN!!!! - // String msg = " time_seen beacon: " + alipay.time_seen_beacon_calc.returnArray(); - // laptop.send(msg); String msg = "rpm : " + String(alipay.RPM); laptop.send(msg); } @@ -103,22 +118,90 @@ void loop() break; } - if (!(laptop_packetBuffer[2] == '0' || laptop_packetBuffer[2] == '7' || laptop_packetBuffer[2] == '8')) { + if (laptop_packetBuffer[2] != '0') { String msg = "rotpwr : " + String(melty_parameters.rot) + " tranpwr : " + String(melty_parameters.tra) + " perc : " + String(melty_parameters.per); laptop.send(msg); } } + } else if (laptop_packetBuffer[0] == '2') { // Tank driving mode! + int lmotorpwr = 0; + int rmotorpwr = 0; + switch (laptop_packetBuffer[1]) { // Check the drive cmd + case '0': + lmotorpwr = 0; + rmotorpwr = 0; + break; + case '1': + lmotorpwr = tank_drive_parameters.drive; + rmotorpwr = tank_drive_parameters.drive; + break; + case '2': + lmotorpwr = tank_drive_parameters.drive + tank_drive_parameters.turn; + rmotorpwr = tank_drive_parameters.drive; + break; + case '3': + lmotorpwr = tank_drive_parameters.turn; + rmotorpwr = -tank_drive_parameters.turn; + break; + case '4': + lmotorpwr = -tank_drive_parameters.drive - tank_drive_parameters.turn; + rmotorpwr = -tank_drive_parameters.drive; + break; + case '5': + lmotorpwr = -tank_drive_parameters.drive; + rmotorpwr = -tank_drive_parameters.drive; + break; + case '6': + lmotorpwr = -tank_drive_parameters.drive; + rmotorpwr = -tank_drive_parameters.drive - tank_drive_parameters.turn; + break; + case '7': + lmotorpwr = -tank_drive_parameters.turn; + rmotorpwr = tank_drive_parameters.turn; + break; + case '8': + lmotorpwr = tank_drive_parameters.drive; + rmotorpwr = tank_drive_parameters.drive + tank_drive_parameters.turn; + break; + } + + EVERY_N_MILLIS(100) { + switch (laptop_packetBuffer[2]) { + case '1': + tank_drive_parameters.drive++; + break; + case '2': + tank_drive_parameters.drive--; + break; + case '3': + tank_drive_parameters.turn++; + break; + case '4': + tank_drive_parameters.turn--; + break; + } + + if (laptop_packetBuffer[2] != '0') { + String msg = "drivpwr : " + String(tank_drive_parameters.drive) + " turnpwr : " + String(tank_drive_parameters.turn); + laptop.send(msg); + } + } + + driveMotors.l_motor_write(-lmotorpwr); + driveMotors.r_motor_write(rmotorpwr); + toggleLeds(CRGB::White, CRGB::Black, 500); + } else { // Currently disabled toggleLeds(CRGB::Red, CRGB::Green, 500); - set_both_motors(0); + driveMotors.set_both_motors(0); EVERY_N_SECONDS(10) { - laptop.send(get3sVoltage()); + laptop.send(get3sVoltage()); } } } else { - set_both_motors(0); + driveMotors.set_both_motors(0); toggleLeds(CRGB::Red, CRGB::Black, 500); } } diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp index a1fd2dc..c929b81 100644 --- a/IR Beacon/src/main.cpp +++ b/IR Beacon/src/main.cpp @@ -5,7 +5,7 @@ #include #include -#define TARGETCMD '2' // Change based on which IR_Beacon working on +#define TARGETCMD '1' // Change based on which IR_Beacon working on const int packSize = 6; char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; @@ -39,30 +39,35 @@ void loop(){ EVERY_N_SECONDS(10) laptop.send(get1sVoltage()); break; - case TARGETCMD: // Enable IR Beacon - toggleLeds(CRGB::Blue, CRGB::Purple, 500); - ledcWrite(ledChannel, dutycycle); + case '1': // Enable one of the IR Beacons + if (laptop_packetBuffer[1] == TARGETCMD) { + toggleLeds(CRGB::Blue, CRGB::Purple, 500); + ledcWrite(ledChannel, dutycycle); + } else { + setLeds(CRGB::Black); + ledcWrite(ledChannel, 0); + } break; - default: // Don't enable current IR Beacon + default: // Don't enable current IR Beacon if in another mode setLeds(CRGB::Black); ledcWrite(ledChannel, 0); break; } EVERY_N_MILLIS(100) { - if (laptop_packetBuffer[0] == TARGETCMD) { + if (laptop_packetBuffer[1] == TARGETCMD) { switch (laptop_packetBuffer[2]) { - case '7': + case '1': dutycycle+=3; if (dutycycle > maxdutycycle) dutycycle = maxdutycycle; break; - case '8': + case '2': dutycycle-=3; if (dutycycle < 0) dutycycle = 0; break; } - if (laptop_packetBuffer[2] == '7' || laptop_packetBuffer[2] == '8') { + if (laptop_packetBuffer[2] == '1' || laptop_packetBuffer[2] == '2') { String msg = "dutycycle : " + String(dutycycle); laptop.send(msg); } diff --git a/controller/controller.py b/controller/controller.py index 81800f0..1e232fa 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -14,7 +14,9 @@ keyboard_thread.daemon = True keyboard_thread.start() -cmd = "" +alipaycmd = "" +irbeaconcmd = "" + async def bluetooth_receive_handler(BLE_DEVICE): while True: @@ -22,22 +24,28 @@ async def bluetooth_receive_handler(BLE_DEVICE): if (BLE_DEVICE.isConnected): print(f"[{BLE_DEVICE._peripheral_name}] {await BLE_DEVICE.read()}") -async def bluetooth_comm_handler(BLE_DEVICE): +async def bluetooth_comm_handler(BLE_DEVICE, isMainRobot): await BLE_DEVICE.connect() while True: await asyncio.sleep(0.05) if (BLE_DEVICE.isConnected): - await BLE_DEVICE.write(cmd) + if (isMainRobot): + await BLE_DEVICE.write(alipaycmd) + else: + await BLE_DEVICE.write(irbeaconcmd) else: await BLE_DEVICE.connect() async def cmd_handler(): - global cmd + global alipaycmd + global irbeaconcmd enabled = 0 waitForEnableReleased = 0 lastState = 0 + drivestate = 1 + activeBeacon = 1 while True: - x,y,drivecmd,tuning = (0,)*4 + x,y,drivecmd,alipaytuning,irbeacontuning = (0,)*5 if get_key_state(Key.up): y = y + 1 @@ -68,25 +76,26 @@ async def cmd_handler(): drivecmd = 8 if get_key_state('u'): - tuning = 1 + alipaytuning = 1 elif get_key_state('j'): - tuning = 2 + alipaytuning = 2 elif get_key_state('i'): - tuning = 3 + alipaytuning = 3 elif get_key_state('k'): - tuning = 4 + alipaytuning = 4 elif get_key_state('o'): - tuning = 5 + alipaytuning = 5 elif get_key_state('l'): - tuning = 6 - elif get_key_state('t'): - tuning = 7 + alipaytuning = 6 + + if get_key_state('t'): + irbeacontuning = 1 elif get_key_state('g'): - tuning = 8 + irbeacontuning = 2 if get_key_state(Key.space): if get_key_state(Key.ctrl): - enabled = 1 + enabled = drivestate waitForEnableReleased = 1 else: if not waitForEnableReleased: @@ -94,20 +103,27 @@ async def cmd_handler(): if not (get_key_state(Key.space)) and not (get_key_state(Key.ctrl)): waitForEnableReleased = 0 + if (enabled != 0): + if (get_key_state('1')): + enabled = drivestate = 1 + if (get_key_state('2')): + enabled = drivestate = 2 + curState = get_key_state('0') if curState and not lastState: - if enabled == 1: - enabled = 2 - elif enabled == 2: - enabled = 1 + if activeBeacon == 1: + activeBeacon = 2 + elif activeBeacon == 2: + activeBeacon = 1 lastState = curState - cmd = f"{enabled}{drivecmd}{tuning}000" - # print(cmd) + alipaycmd = f"{enabled}{drivecmd}{alipaytuning}000" + irbeaconcmd = f"{enabled}{activeBeacon}{irbeacontuning}000" + # print(alipaycmd) await asyncio.sleep(0.05) async def main(): - await asyncio.gather(cmd_handler(), bluetooth_comm_handler(ir_beacon_1), bluetooth_comm_handler(alipay), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(alipay), bluetooth_comm_handler(ir_beacon_2), bluetooth_receive_handler(ir_beacon_2)) + await asyncio.gather(cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(alipay, True), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(alipay), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) asyncio.run(main()) From 5e4f389dafead808b22d7575de52b636fc9d5fe3 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Thu, 29 Feb 2024 23:36:49 -0800 Subject: [PATCH 21/48] Fixed weird bug with IR Receiver, Hardware issue, ESD or something killed ir receivers?? --- Alipay Robot Code/src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Alipay Robot Code/src/main.cpp b/Alipay Robot Code/src/main.cpp index 7e00c5d..4bfaace 100644 --- a/Alipay Robot Code/src/main.cpp +++ b/Alipay Robot Code/src/main.cpp @@ -34,7 +34,7 @@ void setup() laptop.init_ble("Alipay"); setLeds(CRGB::Black); } - + void loop() { if (laptop.isConnected()) { From 8b2bfed615dcfa5be923c7b165e60c1c58db2d8b Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Fri, 1 Mar 2024 17:42:30 -0800 Subject: [PATCH 22/48] added decceleration slowdown code --- .../lib/Drive_Motors/Drive_Motors.cpp | 2 +- Alipay Robot Code/lib/Melty/melty.cpp | 2 +- Alipay Robot Code/lib/Melty/melty.h | 13 +++++----- Alipay Robot Code/src/main.cpp | 25 +++++++++++++++---- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/Alipay Robot Code/lib/Drive_Motors/Drive_Motors.cpp b/Alipay Robot Code/lib/Drive_Motors/Drive_Motors.cpp index 92dbc44..a787402 100644 --- a/Alipay Robot Code/lib/Drive_Motors/Drive_Motors.cpp +++ b/Alipay Robot Code/lib/Drive_Motors/Drive_Motors.cpp @@ -27,7 +27,7 @@ void Drive_Motors::l_motor_write(int value) { } void Drive_Motors::r_motor_write(int value) { - r_motor_value = 0; + r_motor_value = value; if (value != 0) if (value > 0) value += 2; diff --git a/Alipay Robot Code/lib/Melty/melty.cpp b/Alipay Robot Code/lib/Melty/melty.cpp index 25c5933..76cdf9f 100644 --- a/Alipay Robot Code/lib/Melty/melty.cpp +++ b/Alipay Robot Code/lib/Melty/melty.cpp @@ -26,7 +26,7 @@ void melty::update() { time_seen_beacon = micros() - currentPulse; time_seen_beacon_calc.update(time_seen_beacon); - if (time_seen_beacon_calc.isLegit(time_seen_beacon)) {// && period_ms_calc.isLegit(period_MS)) + if (time_seen_beacon_calc.isLegit(time_seen_beacon)) { // && period_micros_calc.isLegit(period_micros) computeTimings(); } } diff --git a/Alipay Robot Code/lib/Melty/melty.h b/Alipay Robot Code/lib/Melty/melty.h index 9ad4f84..1711f47 100644 --- a/Alipay Robot Code/lib/Melty/melty.h +++ b/Alipay Robot Code/lib/Melty/melty.h @@ -37,11 +37,6 @@ class ringBuffer{ return false; } - // private: - unsigned long ringBuf[RINGBUFSIZE]; - int curIndex = 0; - float criteria = 0; - unsigned long getMaxVal() { unsigned long maxVal = 0; for (int i = 0; i < RINGBUFSIZE; i++) @@ -49,7 +44,11 @@ class ringBuffer{ maxVal = ringBuf[i]; return maxVal; } - + + private: + unsigned long ringBuf[RINGBUFSIZE]; + int curIndex = 0; + float criteria = 0; String returnArray() { String msg = ""; for (int i = 0; i < RINGBUFSIZE; i++) { @@ -76,7 +75,7 @@ class melty { int deg = 0; float percentageOfRotation = 0; bool useTopIr = 1; - // private: + private: bool lastSeenIRLed = 0; unsigned long period_micros = micros(); unsigned long currentPulse = micros(); diff --git a/Alipay Robot Code/src/main.cpp b/Alipay Robot Code/src/main.cpp index 4bfaace..5ac76df 100644 --- a/Alipay Robot Code/src/main.cpp +++ b/Alipay Robot Code/src/main.cpp @@ -8,6 +8,8 @@ const int packSize = 6; char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; +bool wasMeltying = false; +int slowDownSpeed = 5; BLE_Uart laptop = BLE_Uart(laptop_packetBuffer, packSize); Drive_Motors driveMotors = Drive_Motors(); @@ -17,10 +19,11 @@ struct melty_parameters { int rot = 10; int tra = 6; float per = 0.5; + int invert = 1; } melty_parameters; struct tank_drive_parameters { - int drive = 3; + int drive = 2; int turn = 2; } tank_drive_parameters; @@ -42,20 +45,22 @@ void loop() if (getAccelZ() > 7) { alipay.useTopIr = 1; driveMotors.flip_motors = 0; + melty_parameters.invert = 1; } if (getAccelZ() < -7) { alipay.useTopIr = 0; driveMotors.flip_motors = 1; + melty_parameters.invert = -1; } } if (laptop_packetBuffer[0] == '1') { // Currently enabled and meltybraining!!! alipay.update(); if (alipay.translate()) { - driveMotors.l_motor_write((melty_parameters.rot-melty_parameters.tra)); - driveMotors.r_motor_write((melty_parameters.rot+melty_parameters.tra)); + driveMotors.l_motor_write(melty_parameters.invert * (melty_parameters.rot-melty_parameters.tra)); + driveMotors.r_motor_write(melty_parameters.invert *(melty_parameters.rot+melty_parameters.tra)); } else { - driveMotors.set_both_motors(melty_parameters.rot); + driveMotors.set_both_motors(melty_parameters.invert * melty_parameters.rot); } switch (laptop_packetBuffer[1]) { // Check the drive cmd @@ -124,7 +129,18 @@ void loop() } } + wasMeltying = 1; + } else if (laptop_packetBuffer[0] == '2') { // Tank driving mode! + if (wasMeltying) { // Was previously meltybraining, we need to slowdown + unsigned long timeout = millis(); + while (abs(getAccelY()) > .3 && millis() - timeout < 2000) { + driveMotors.set_both_motors(-slowDownSpeed * melty_parameters.invert); + toggleLeds(CRGB::White, CRGB::Red, 150); + } + wasMeltying = 0; + } + int lmotorpwr = 0; int rmotorpwr = 0; switch (laptop_packetBuffer[1]) { // Check the drive cmd @@ -198,7 +214,6 @@ void loop() EVERY_N_SECONDS(10) { laptop.send(get3sVoltage()); } - } } else { driveMotors.set_both_motors(0); From aba5a58170677bac601665a6f526ff14e671ba28 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Mon, 4 Mar 2024 17:54:38 -0800 Subject: [PATCH 23/48] controls no longer reversed when flipped over --- Alipay Robot Code/.vscode/settings.json | 8 ++- .../lib/Drive_Motors/Drive_Motors.h | 1 - Alipay Robot Code/lib/Melty/melty.cpp | 33 +++++----- Alipay Robot Code/lib/Melty/melty.h | 49 ++++++++------ Alipay Robot Code/lib/mpu6050/mpu6050.cpp | 2 +- Alipay Robot Code/src/main.cpp | 66 ++++++++----------- controller/controller.py | 12 ++-- 7 files changed, 88 insertions(+), 83 deletions(-) diff --git a/Alipay Robot Code/.vscode/settings.json b/Alipay Robot Code/.vscode/settings.json index ab87840..8c93b85 100644 --- a/Alipay Robot Code/.vscode/settings.json +++ b/Alipay Robot Code/.vscode/settings.json @@ -8,6 +8,12 @@ "vector": "cpp", "string_view": "cpp", "initializer_list": "cpp", - "random": "cpp" + "random": "cpp", + "*.tcc": "cpp", + "memory": "cpp", + "istream": "cpp", + "functional": "cpp", + "tuple": "cpp", + "utility": "cpp" } } \ No newline at end of file diff --git a/Alipay Robot Code/lib/Drive_Motors/Drive_Motors.h b/Alipay Robot Code/lib/Drive_Motors/Drive_Motors.h index bd02117..95e0a8a 100644 --- a/Alipay Robot Code/lib/Drive_Motors/Drive_Motors.h +++ b/Alipay Robot Code/lib/Drive_Motors/Drive_Motors.h @@ -21,4 +21,3 @@ class Drive_Motors { int l_motor_value = 0; int r_motor_value = 0; }; - diff --git a/Alipay Robot Code/lib/Melty/melty.cpp b/Alipay Robot Code/lib/Melty/melty.cpp index 76cdf9f..fa048ca 100644 --- a/Alipay Robot Code/lib/Melty/melty.cpp +++ b/Alipay Robot Code/lib/Melty/melty.cpp @@ -36,14 +36,15 @@ void melty::update() { } void melty::computeTimings() { - RPM = (60000*1000)/(period_micros_calc.getMaxVal()); + unsigned long max_period = period_micros_calc.getMaxVal(); + RPM = (us_per_min)/(max_period); unsigned long center_of_beacon = currentPulse + time_seen_beacon/2; // This should ideally be centered on the beacon - unsigned long centerOfDrivePulse = center_of_beacon + (float(deg)/360)*period_micros_calc.getMaxVal(); // Direction that we should be driving towards - unsigned long deltaDriveTiming = (percentageOfRotation * period_micros_calc.getMaxVal())/2; + unsigned long centerOfDrivePulse = center_of_beacon + (float(deg)/360)*max_period; // Direction that we should be driving towards + unsigned long deltaDriveTiming = (percentageOfRotation * max_period)/2; unsigned long offset = 0; if (centerOfDrivePulse - deltaDriveTiming < micros()) { // If we're supposed to start translating before we have calculated those values, offset by one rotation - offset = period_micros_calc.getMaxVal(); + offset = max_period; } if (timingToggle) { // Swap variables that we use so we don't interfere (in the case that we're currently translating whilst computing) @@ -70,21 +71,23 @@ bool melty::isBeaconSensed(bool currentReading) { // for (int i = 0; i < IRLedDataSize; i++) // USBSerial.print(IRLedReadings[i]); // USBSerial.println(); - - IRLedReadings[IRLedIndex++] = currentReading; // This is just code for a ring buffer + enum states {seen, not_seen}; + + IRLedReadings[IRLedIndex++] = currentReading; // Add to ring buffer if (IRLedIndex == IRLedDataSize) IRLedIndex = 0; - if (lastIRLedReturnValue) { + if (lastIRLedReturnValue == seen) { for (int i = 0; i < IRLedDataSize; i++) - if (IRLedReadings[i] == lastIRLedReturnValue) - return true; - lastIRLedReturnValue = !lastIRLedReturnValue; - } else { + if (IRLedReadings[i] == lastIRLedReturnValue) // If any of our readings in the ring buffer is the last value, keep the previous state + return seen; + lastIRLedReturnValue = not_seen; + return not_seen; + } else { // This is to cover the not_seen case for (int i = 0; i < IRLedDataSize; i++) if (IRLedReadings[i] == lastIRLedReturnValue) - return false; - lastIRLedReturnValue = !lastIRLedReturnValue; + return not_seen; + lastIRLedReturnValue = seen; + return seen; } - return lastIRLedReturnValue; -} \ No newline at end of file +} diff --git a/Alipay Robot Code/lib/Melty/melty.h b/Alipay Robot Code/lib/Melty/melty.h index 1711f47..707bc15 100644 --- a/Alipay Robot Code/lib/Melty/melty.h +++ b/Alipay Robot Code/lib/Melty/melty.h @@ -1,21 +1,14 @@ #include #include -/* - -two uses for ringbuffer is calculating -period_ms -time_seen_beacon -*/ - - #define RINGBUFSIZE 5 + class ringBuffer{ public: /** * @brief Construct a ring buffer object. * - * @param _criteria How close should the new value match the Max value from the ring buffer to be considered legit? + * @param _criteria How close should the new value match the max value from the ring buffer to be considered legit? */ ringBuffer(float _criteria) { criteria = _criteria; @@ -23,12 +16,24 @@ class ringBuffer{ ringBuffer() {} + /** + * @brief Adds a value to the ring buffer. + * + * @param val Value to be added to the ring buffer. + */ void update(unsigned long val) { ringBuf[curIndex++] = val; - if (curIndex > RINGBUFSIZE) + if (curIndex > RINGBUFSIZE-1) curIndex = 0; } + /** + * @brief Compares the input value to the max value. + * + * @param newVal New value to check against current max value. + * + * @return Whether new value is some percentage of max value. Based on the criteria variable. + */ bool isLegit(unsigned long newVal) { unsigned long maxVal = getMaxVal(); if (newVal > maxVal * criteria) @@ -36,7 +41,10 @@ class ringBuffer{ else return false; } - + + /** + * @brief Returns maximum value from our ring buffer. + */ unsigned long getMaxVal() { unsigned long maxVal = 0; for (int i = 0; i < RINGBUFSIZE; i++) @@ -49,7 +57,7 @@ class ringBuffer{ unsigned long ringBuf[RINGBUFSIZE]; int curIndex = 0; float criteria = 0; - String returnArray() { + String returnArray() { // For debug purposes String msg = ""; for (int i = 0; i < RINGBUFSIZE; i++) { msg = msg + String(ringBuf[i]) + " "; @@ -60,9 +68,9 @@ class ringBuffer{ -#define TOP_IR_PIN 10 +#define TOP_IR_PIN 10 #define BOTTOM_IR_PIN 9 -#define IRLedDataSize 25 +#define IRLedDataSize 25 // Size of our Ring Buffer that will hold the IR Led data class melty { public: @@ -78,22 +86,21 @@ class melty { private: bool lastSeenIRLed = 0; unsigned long period_micros = micros(); - unsigned long currentPulse = micros(); - unsigned long lastPulse = micros(); unsigned long time_seen_beacon = micros(); - + unsigned long currentPulse = micros(), lastPulse = micros(); + bool IRLedReadings[IRLedDataSize] = {0}; int IRLedIndex = 0; + bool lastIRLedReturnValue = 0; - unsigned long startDrive = micros(); - unsigned long endDrive = micros(); - unsigned long startDrive2 = micros(); - unsigned long endDrive2 = micros(); + unsigned long startDrive = micros(), endDrive = micros(), startDrive2 = micros(), endDrive2 = micros(); bool timingToggle = 0; ringBuffer period_micros_calc; ringBuffer time_seen_beacon_calc; + + const int us_per_min = 60000000; }; diff --git a/Alipay Robot Code/lib/mpu6050/mpu6050.cpp b/Alipay Robot Code/lib/mpu6050/mpu6050.cpp index d79fb2f..10a0197 100644 --- a/Alipay Robot Code/lib/mpu6050/mpu6050.cpp +++ b/Alipay Robot Code/lib/mpu6050/mpu6050.cpp @@ -19,7 +19,7 @@ void init_mpu6050() { float getAccelY() { sensors_event_t a, g, temp; mpu.getEvent(&a, &g, &temp); - return (a.acceleration.y); + return (abs(a.acceleration.y)); } float getAccelZ() { diff --git a/Alipay Robot Code/src/main.cpp b/Alipay Robot Code/src/main.cpp index 5ac76df..2950001 100644 --- a/Alipay Robot Code/src/main.cpp +++ b/Alipay Robot Code/src/main.cpp @@ -8,23 +8,26 @@ const int packSize = 6; char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; +const int headings[] = {0, 45, 90, 135, 180, 225, 270, 315}; bool wasMeltying = false; -int slowDownSpeed = 5; +int slowDownSpeed = 8; BLE_Uart laptop = BLE_Uart(laptop_packetBuffer, packSize); Drive_Motors driveMotors = Drive_Motors(); melty alipay = melty(); struct melty_parameters { - int rot = 10; + int rot = 6; int tra = 6; float per = 0.5; int invert = 1; + int boost = 5; } melty_parameters; struct tank_drive_parameters { int drive = 2; int turn = 2; + int boost = 2; } tank_drive_parameters; void setup() @@ -56,41 +59,26 @@ void loop() if (laptop_packetBuffer[0] == '1') { // Currently enabled and meltybraining!!! alipay.update(); + int boostVal = 0; + if (laptop_packetBuffer[3] == '1') + boostVal = melty_parameters.boost; if (alipay.translate()) { - driveMotors.l_motor_write(melty_parameters.invert * (melty_parameters.rot-melty_parameters.tra)); - driveMotors.r_motor_write(melty_parameters.invert *(melty_parameters.rot+melty_parameters.tra)); + driveMotors.l_motor_write(melty_parameters.invert * (melty_parameters.rot - (melty_parameters.tra + boostVal))); + driveMotors.r_motor_write(melty_parameters.invert * (melty_parameters.rot + (melty_parameters.tra + boostVal))); } else { - driveMotors.set_both_motors(melty_parameters.invert * melty_parameters.rot); + driveMotors.set_both_motors(melty_parameters.invert * (melty_parameters.rot + boostVal)); } - switch (laptop_packetBuffer[1]) { // Check the drive cmd - case '8': - alipay.deg = 0; - break; - case '7': - alipay.deg = 45; - break; - case '6': - alipay.deg = 90; - break; - case '5': - alipay.deg = 135; - break; - case '4': - alipay.deg = 180; - break; - case '3': - alipay.deg = 225; - break; - case '2': - alipay.deg = 270; - break; - case '1': - alipay.deg = 315; - break; + int drivecmd = laptop_packetBuffer[1] - '0'; + if (drivecmd > 0 && drivecmd < 9) { // 1,2,3,4,5,6,7,8 + if (melty_parameters.invert == 1) + drivecmd = 8 - drivecmd; + else + drivecmd = drivecmd - 1; + alipay.deg = headings[drivecmd]; } - if (laptop_packetBuffer[1] != '0') {// Check drive cmd for setting the "neutral" state + if (laptop_packetBuffer[1] != '0') { // Check drive cmd for setting the "neutral" state alipay.percentageOfRotation = melty_parameters.per; } else { alipay.percentageOfRotation = 0; @@ -134,7 +122,7 @@ void loop() } else if (laptop_packetBuffer[0] == '2') { // Tank driving mode! if (wasMeltying) { // Was previously meltybraining, we need to slowdown unsigned long timeout = millis(); - while (abs(getAccelY()) > .3 && millis() - timeout < 2000) { + while (getAccelY() > .2 && millis() - timeout < 2000) { driveMotors.set_both_motors(-slowDownSpeed * melty_parameters.invert); toggleLeds(CRGB::White, CRGB::Red, 150); } @@ -158,7 +146,7 @@ void loop() break; case '3': lmotorpwr = tank_drive_parameters.turn; - rmotorpwr = -tank_drive_parameters.turn; + // rmotorpwr = -tank_drive_parameters.turn; break; case '4': lmotorpwr = -tank_drive_parameters.drive - tank_drive_parameters.turn; @@ -173,7 +161,7 @@ void loop() rmotorpwr = -tank_drive_parameters.drive - tank_drive_parameters.turn; break; case '7': - lmotorpwr = -tank_drive_parameters.turn; + // lmotorpwr = -tank_drive_parameters.turn; rmotorpwr = tank_drive_parameters.turn; break; case '8': @@ -204,8 +192,12 @@ void loop() } } - driveMotors.l_motor_write(-lmotorpwr); - driveMotors.r_motor_write(rmotorpwr); + int boostVal = 0; + if (laptop_packetBuffer[3] == '1') + boostVal = tank_drive_parameters.boost; + + driveMotors.l_motor_write(-lmotorpwr - boostVal); + driveMotors.r_motor_write(rmotorpwr + boostVal); toggleLeds(CRGB::White, CRGB::Black, 500); } else { // Currently disabled @@ -215,7 +207,7 @@ void loop() laptop.send(get3sVoltage()); } } - } else { + } else { // Currently DISCONNECTED driveMotors.set_both_motors(0); toggleLeds(CRGB::Red, CRGB::Black, 500); } diff --git a/controller/controller.py b/controller/controller.py index 1e232fa..3718b49 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -17,7 +17,6 @@ alipaycmd = "" irbeaconcmd = "" - async def bluetooth_receive_handler(BLE_DEVICE): while True: await asyncio.sleep(0.1) @@ -45,7 +44,7 @@ async def cmd_handler(): drivestate = 1 activeBeacon = 1 while True: - x,y,drivecmd,alipaytuning,irbeacontuning = (0,)*5 + x,y,drivecmd,alipaytuning,irbeacontuning,boost = (0,)*6 if get_key_state(Key.up): y = y + 1 @@ -116,8 +115,11 @@ async def cmd_handler(): elif activeBeacon == 2: activeBeacon = 1 lastState = curState + + if (get_key_state(Key.shift)): + boost = 1 - alipaycmd = f"{enabled}{drivecmd}{alipaytuning}000" + alipaycmd = f"{enabled}{drivecmd}{alipaytuning}{boost}00" irbeaconcmd = f"{enabled}{activeBeacon}{irbeacontuning}000" # print(alipaycmd) await asyncio.sleep(0.05) @@ -126,7 +128,3 @@ async def main(): await asyncio.gather(cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(alipay, True), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(alipay), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) asyncio.run(main()) - - - - \ No newline at end of file From 145d7e86136e0349845db8b9ea95d09cf7217f35 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Thu, 7 Mar 2024 00:13:13 -0800 Subject: [PATCH 24/48] added automatic switching code --- Alipay Robot Code/lib/Melty/melty.cpp | 3 +- Alipay Robot Code/lib/Melty/melty.h | 2 +- Alipay Robot Code/src/main.cpp | 12 +++-- IR Beacon/src/main.cpp | 45 +++++++++-------- controller/controller.py | 69 ++++++++++++++++++++------- 5 files changed, 87 insertions(+), 44 deletions(-) diff --git a/Alipay Robot Code/lib/Melty/melty.cpp b/Alipay Robot Code/lib/Melty/melty.cpp index fa048ca..cbfb876 100644 --- a/Alipay Robot Code/lib/Melty/melty.cpp +++ b/Alipay Robot Code/lib/Melty/melty.cpp @@ -6,7 +6,7 @@ melty::melty() { time_seen_beacon_calc = ringBuffer(0.5); } -void melty::update() { +bool melty::update() { bool curSeenIRLed; if (useTopIr) curSeenIRLed = isBeaconSensed(!digitalRead(TOP_IR_PIN)); @@ -33,6 +33,7 @@ void melty::update() { lastSeenIRLed = curSeenIRLed; + return curSeenIRLed; } void melty::computeTimings() { diff --git a/Alipay Robot Code/lib/Melty/melty.h b/Alipay Robot Code/lib/Melty/melty.h index 707bc15..64d6f5c 100644 --- a/Alipay Robot Code/lib/Melty/melty.h +++ b/Alipay Robot Code/lib/Melty/melty.h @@ -75,7 +75,7 @@ class ringBuffer{ class melty { public: melty(); - void update(); + bool update(); bool isBeaconSensed(bool currentReading); void computeTimings(); bool translate(); diff --git a/Alipay Robot Code/src/main.cpp b/Alipay Robot Code/src/main.cpp index 2950001..62de7a0 100644 --- a/Alipay Robot Code/src/main.cpp +++ b/Alipay Robot Code/src/main.cpp @@ -58,7 +58,13 @@ void loop() } if (laptop_packetBuffer[0] == '1') { // Currently enabled and meltybraining!!! - alipay.update(); + if (alipay.update()) { // If seen the LED + EVERY_N_MILLIS(250) { + laptop.send("seen"); + } + } + + int boostVal = 0; if (laptop_packetBuffer[3] == '1') boostVal = melty_parameters.boost; @@ -85,7 +91,7 @@ void loop() } EVERY_N_SECONDS(1) { // DEBUGGIN!!!! - String msg = "rpm : " + String(alipay.RPM); + String msg = "RPM : " + String(alipay.RPM); laptop.send(msg); } @@ -122,7 +128,7 @@ void loop() } else if (laptop_packetBuffer[0] == '2') { // Tank driving mode! if (wasMeltying) { // Was previously meltybraining, we need to slowdown unsigned long timeout = millis(); - while (getAccelY() > .2 && millis() - timeout < 2000) { + while (getAccelY() > .3 && millis() - timeout < 2000) { driveMotors.set_both_motors(-slowDownSpeed * melty_parameters.invert); toggleLeds(CRGB::White, CRGB::Red, 150); } diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp index c929b81..87eab7d 100644 --- a/IR Beacon/src/main.cpp +++ b/IR Beacon/src/main.cpp @@ -5,7 +5,7 @@ #include #include -#define TARGETCMD '1' // Change based on which IR_Beacon working on +#define TARGETCMD '2' // Change based on which IR_Beacon working on const int packSize = 6; char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; @@ -43,6 +43,26 @@ void loop(){ if (laptop_packetBuffer[1] == TARGETCMD) { toggleLeds(CRGB::Blue, CRGB::Purple, 500); ledcWrite(ledChannel, dutycycle); + + EVERY_N_MILLIS(100) { + switch (laptop_packetBuffer[2]) { + case '1': + dutycycle+=3; + if (dutycycle > maxdutycycle) + dutycycle = maxdutycycle; + break; + case '2': + dutycycle-=3; + if (dutycycle < 0) + dutycycle = 0; + break; + } + if (laptop_packetBuffer[2] == '1' || laptop_packetBuffer[2] == '2') { + String msg = "dutycycle : " + String(dutycycle); + laptop.send(msg); + } + } + } else { setLeds(CRGB::Black); ledcWrite(ledChannel, 0); @@ -53,26 +73,9 @@ void loop(){ ledcWrite(ledChannel, 0); break; } - EVERY_N_MILLIS(100) { - if (laptop_packetBuffer[1] == TARGETCMD) { - switch (laptop_packetBuffer[2]) { - case '1': - dutycycle+=3; - if (dutycycle > maxdutycycle) - dutycycle = maxdutycycle; - break; - case '2': - dutycycle-=3; - if (dutycycle < 0) - dutycycle = 0; - break; - } - if (laptop_packetBuffer[2] == '1' || laptop_packetBuffer[2] == '2') { - String msg = "dutycycle : " + String(dutycycle); - laptop.send(msg); - } - } - } + + + } else { toggleLeds(CRGB::Red, CRGB::Black, 500); ledcWrite(ledChannel, 0); diff --git a/controller/controller.py b/controller/controller.py index 3718b49..6913b86 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -4,6 +4,12 @@ from LaptopKeyboard import * from bluetooth import * import asyncio +import time + +startTime = time.time() + +def millis(): + return round((time.time()-startTime) * 1000) ir_beacon_2 = BLE_UART(peripheral_name='IR Beacon 2', address = 'CBB4A195-F34C-E6C5-21CD-ACBB7D44352A') ir_beacon_1 = BLE_UART(peripheral_name='IR Beacon 1', address = 'CC80B9F1-FE04-64E9-2CE8-014A24EEE1BF') @@ -16,12 +22,21 @@ alipaycmd = "" irbeaconcmd = "" +enabled = 0 +activeBeacon = 1 +lastBeaconRead = millis() async def bluetooth_receive_handler(BLE_DEVICE): + global lastBeaconRead while True: await asyncio.sleep(0.1) if (BLE_DEVICE.isConnected): - print(f"[{BLE_DEVICE._peripheral_name}] {await BLE_DEVICE.read()}") + msg = await BLE_DEVICE.read() + if (msg == "seen"): + if (lastBeaconRead < millis()): # Some reason it thinks its "seen" right after enabling + lastBeaconRead = millis() + else: + print(f"[{BLE_DEVICE._peripheral_name}] {msg}") async def bluetooth_comm_handler(BLE_DEVICE, isMainRobot): await BLE_DEVICE.connect() @@ -34,17 +49,38 @@ async def bluetooth_comm_handler(BLE_DEVICE, isMainRobot): await BLE_DEVICE.write(irbeaconcmd) else: await BLE_DEVICE.connect() + + +async def ir_beacon_switcher(): + global enabled + global lastBeaconRead + while True: + await asyncio.sleep(0.1) + if (enabled != 1): + lastBeaconRead = millis() + 2000 # Add some time so the beacon doesnt switch right after enabling melty brain mode + if (enabled == 1 and millis() - lastBeaconRead > 1000 and ir_beacon_2.isConnected == True): + toggleBeacon() + # await asyncio.sleep(1) + +def toggleBeacon(): + global activeBeacon + global lastBeaconRead + lastBeaconRead = millis() + if activeBeacon == 1: + activeBeacon = 2 + elif activeBeacon == 2: + activeBeacon = 1 async def cmd_handler(): global alipaycmd global irbeaconcmd - enabled = 0 + global enabled + global activeBeacon waitForEnableReleased = 0 lastState = 0 drivestate = 1 - activeBeacon = 1 while True: - x,y,drivecmd,alipaytuning,irbeacontuning,boost = (0,)*6 + x,y,drivecmd,alipaytuning,irbeacontuning,boost = (0,)*6 if get_key_state(Key.up): y = y + 1 @@ -74,17 +110,17 @@ async def cmd_handler(): elif x == -1 and y == 1: drivecmd = 8 - if get_key_state('u'): + if get_key_state('q'): alipaytuning = 1 - elif get_key_state('j'): + elif get_key_state('a'): alipaytuning = 2 - elif get_key_state('i'): + elif get_key_state('w'): alipaytuning = 3 - elif get_key_state('k'): + elif get_key_state('s'): alipaytuning = 4 - elif get_key_state('o'): + elif get_key_state('e'): alipaytuning = 5 - elif get_key_state('l'): + elif get_key_state('d'): alipaytuning = 6 if get_key_state('t'): @@ -103,17 +139,14 @@ async def cmd_handler(): waitForEnableReleased = 0 if (enabled != 0): - if (get_key_state('1')): + if (get_key_state('z')): enabled = drivestate = 1 - if (get_key_state('2')): + if (get_key_state('x')): enabled = drivestate = 2 - curState = get_key_state('0') + curState = get_key_state('1') if curState and not lastState: - if activeBeacon == 1: - activeBeacon = 2 - elif activeBeacon == 2: - activeBeacon = 1 + toggleBeacon() lastState = curState if (get_key_state(Key.shift)): @@ -125,6 +158,6 @@ async def cmd_handler(): await asyncio.sleep(0.05) async def main(): - await asyncio.gather(cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(alipay, True), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(alipay), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) + await asyncio.gather(ir_beacon_switcher(), cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(alipay, True), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(alipay), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) asyncio.run(main()) From b56d16c0c2b4a8f8055c0213cc24abdd68e3279f Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Fri, 8 Mar 2024 21:27:29 -0800 Subject: [PATCH 25/48] fixed boostval to work bidirectionally for tank drive, before trying to implement pid controller --- .DS_Store | Bin 6148 -> 6148 bytes Alipay Robot Code/src/main.cpp | 42 +++++++++++++++++---------------- controller/controller.py | 1 - 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/.DS_Store b/.DS_Store index eebe13f7cdcc9b0f7e74683cab35d2ac78cd8158..0c0e9cd90b83c3e8748aabe524548898b26fa9b2 100644 GIT binary patch delta 20 bcmZoMXffDe#l*-k*_ug#k#Tbj(+W`lITHmI delta 20 bcmZoMXffDe#l*-s*_ug#kzsQT(+W`lIT-~O diff --git a/Alipay Robot Code/src/main.cpp b/Alipay Robot Code/src/main.cpp index 62de7a0..d6d91e8 100644 --- a/Alipay Robot Code/src/main.cpp +++ b/Alipay Robot Code/src/main.cpp @@ -137,42 +137,48 @@ void loop() int lmotorpwr = 0; int rmotorpwr = 0; + + int boostVal = 0; + if (laptop_packetBuffer[3] == '1') + boostVal = tank_drive_parameters.boost; + + switch (laptop_packetBuffer[1]) { // Check the drive cmd case '0': lmotorpwr = 0; rmotorpwr = 0; break; case '1': - lmotorpwr = tank_drive_parameters.drive; - rmotorpwr = tank_drive_parameters.drive; + lmotorpwr = tank_drive_parameters.drive + boostVal; + rmotorpwr = tank_drive_parameters.drive + boostVal; break; case '2': - lmotorpwr = tank_drive_parameters.drive + tank_drive_parameters.turn; - rmotorpwr = tank_drive_parameters.drive; + lmotorpwr = tank_drive_parameters.drive + tank_drive_parameters.turn + boostVal; + rmotorpwr = tank_drive_parameters.drive + boostVal; break; case '3': - lmotorpwr = tank_drive_parameters.turn; + lmotorpwr = tank_drive_parameters.turn + boostVal; // rmotorpwr = -tank_drive_parameters.turn; break; case '4': - lmotorpwr = -tank_drive_parameters.drive - tank_drive_parameters.turn; - rmotorpwr = -tank_drive_parameters.drive; + lmotorpwr = -tank_drive_parameters.drive - tank_drive_parameters.turn - boostVal; + rmotorpwr = -tank_drive_parameters.drive - boostVal; break; case '5': - lmotorpwr = -tank_drive_parameters.drive; - rmotorpwr = -tank_drive_parameters.drive; + lmotorpwr = -tank_drive_parameters.drive - boostVal; + rmotorpwr = -tank_drive_parameters.drive - boostVal; break; case '6': - lmotorpwr = -tank_drive_parameters.drive; - rmotorpwr = -tank_drive_parameters.drive - tank_drive_parameters.turn; + lmotorpwr = -tank_drive_parameters.drive - boostVal; + rmotorpwr = -tank_drive_parameters.drive - tank_drive_parameters.turn - boostVal; break; case '7': // lmotorpwr = -tank_drive_parameters.turn; - rmotorpwr = tank_drive_parameters.turn; + rmotorpwr = tank_drive_parameters.turn + boostVal; break; case '8': - lmotorpwr = tank_drive_parameters.drive; - rmotorpwr = tank_drive_parameters.drive + tank_drive_parameters.turn; + lmotorpwr = tank_drive_parameters.drive + boostVal; + rmotorpwr = tank_drive_parameters.drive + tank_drive_parameters.turn + boostVal; break; } @@ -198,12 +204,8 @@ void loop() } } - int boostVal = 0; - if (laptop_packetBuffer[3] == '1') - boostVal = tank_drive_parameters.boost; - - driveMotors.l_motor_write(-lmotorpwr - boostVal); - driveMotors.r_motor_write(rmotorpwr + boostVal); + driveMotors.l_motor_write(-lmotorpwr); + driveMotors.r_motor_write(rmotorpwr); toggleLeds(CRGB::White, CRGB::Black, 500); } else { // Currently disabled diff --git a/controller/controller.py b/controller/controller.py index 6913b86..e648301 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -60,7 +60,6 @@ async def ir_beacon_switcher(): lastBeaconRead = millis() + 2000 # Add some time so the beacon doesnt switch right after enabling melty brain mode if (enabled == 1 and millis() - lastBeaconRead > 1000 and ir_beacon_2.isConnected == True): toggleBeacon() - # await asyncio.sleep(1) def toggleBeacon(): global activeBeacon From 4451fe982ba469ea58fcdf873d1fff83b6c3035a Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Sat, 9 Mar 2024 06:28:13 -0800 Subject: [PATCH 26/48] added pid tank drive, commit right before comp --- Alipay Robot Code/lib/PID/PID.h | 45 +++++++++++ Alipay Robot Code/lib/mpu6050/mpu6050.cpp | 8 ++ Alipay Robot Code/lib/mpu6050/mpu6050.h | 4 +- Alipay Robot Code/src/main.cpp | 97 +++++++++++++++++++++++ controller/controller.py | 2 + 5 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 Alipay Robot Code/lib/PID/PID.h diff --git a/Alipay Robot Code/lib/PID/PID.h b/Alipay Robot Code/lib/PID/PID.h new file mode 100644 index 0000000..cf8d9d6 --- /dev/null +++ b/Alipay Robot Code/lib/PID/PID.h @@ -0,0 +1,45 @@ +#include + +struct PID { + float kP = .01; + float kI = 0; + float kD = .01; + float maxError = 100; + float maxI = 2; + float maxOut = 2; +} PID; + +int calcTurnPower(float headingError) { + if (headingError > PID.maxError) + headingError = PID.maxError; + if (headingError < -PID.maxError) + headingError = -PID.maxError; + // Static variables to store the previous error and the integral of errors + static float prevHeadingError = 0; + static float integral = 0; + static unsigned long lastCalc = millis(); + float derivative; + float output; + + // PID calculations + integral += headingError; + if (integral > PID.maxI) + integral = PID.maxI; + if ((headingError > 0 && prevHeadingError < 0) || (headingError < 0 && prevHeadingError > 0)) + integral = 0; + + derivative = headingError - prevHeadingError / (millis() - lastCalc); + lastCalc = millis(); + // PID output calculation + output = PID.kP * headingError + PID.kI * integral + PID.kD * derivative; + + if (output > PID.maxOut) + output = PID.maxOut; + else if(output < -PID.maxOut) + output = -PID.maxOut; + + // Update previous error + prevHeadingError = headingError; + + return (int)output; +} \ No newline at end of file diff --git a/Alipay Robot Code/lib/mpu6050/mpu6050.cpp b/Alipay Robot Code/lib/mpu6050/mpu6050.cpp index 10a0197..d6ba0e2 100644 --- a/Alipay Robot Code/lib/mpu6050/mpu6050.cpp +++ b/Alipay Robot Code/lib/mpu6050/mpu6050.cpp @@ -32,4 +32,12 @@ float getTemp() { sensors_event_t a, g, temp; mpu.getEvent(&a, &g, &temp); return (temp.temperature); +} + +float getGyroZ() { + static float heading = 0; + sensors_event_t a, g, temp; + mpu.getEvent(&a, &g, &temp); + heading += g.orientation.z; + return (heading); } \ No newline at end of file diff --git a/Alipay Robot Code/lib/mpu6050/mpu6050.h b/Alipay Robot Code/lib/mpu6050/mpu6050.h index 004d02c..523aff7 100644 --- a/Alipay Robot Code/lib/mpu6050/mpu6050.h +++ b/Alipay Robot Code/lib/mpu6050/mpu6050.h @@ -6,4 +6,6 @@ float getAccelY(); float getAccelZ(); -float getTemp(); \ No newline at end of file +float getTemp(); + +float getGyroZ(); \ No newline at end of file diff --git a/Alipay Robot Code/src/main.cpp b/Alipay Robot Code/src/main.cpp index d6d91e8..a6d8ef6 100644 --- a/Alipay Robot Code/src/main.cpp +++ b/Alipay Robot Code/src/main.cpp @@ -5,6 +5,7 @@ #include #include #include +#include const int packSize = 6; char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; @@ -30,6 +31,12 @@ struct tank_drive_parameters { int boost = 2; } tank_drive_parameters; +struct pid_tank_drive_parameters { + int drive = 2; + int headingTurn = 2.3; + int boost = 2; +} pid_tank_drive_parameters; + void setup() { init_led(); @@ -208,6 +215,92 @@ void loop() driveMotors.r_motor_write(rmotorpwr); toggleLeds(CRGB::White, CRGB::Black, 500); + } else if (laptop_packetBuffer[0] == '3'){ // Tank driving mode + PID! + int lmotorpwr; + int rmotorpwr; + static float desiredHeading; + int pidOutput = 0; + + int boostVal = 0; + if (laptop_packetBuffer[3] == '1') + boostVal = pid_tank_drive_parameters.boost; + + switch (laptop_packetBuffer[1]) { // Check the drive cmd + case '0': + lmotorpwr = 0; + rmotorpwr = 0; + desiredHeading = getGyroZ(); + break; + case '8': + lmotorpwr = pid_tank_drive_parameters.drive +boostVal; + rmotorpwr = pid_tank_drive_parameters.drive +boostVal; + desiredHeading+=pid_tank_drive_parameters.headingTurn; + break; + case '1': + lmotorpwr = pid_tank_drive_parameters.drive +boostVal; + rmotorpwr = pid_tank_drive_parameters.drive +boostVal; + break; + case '2': + lmotorpwr = pid_tank_drive_parameters.drive +boostVal; + rmotorpwr = pid_tank_drive_parameters.drive +boostVal; + desiredHeading-=pid_tank_drive_parameters.headingTurn; + break; + + case '4': + lmotorpwr = -pid_tank_drive_parameters.drive -boostVal; + rmotorpwr = -pid_tank_drive_parameters.drive -boostVal; + desiredHeading+=pid_tank_drive_parameters.headingTurn; + break; + case '5': + lmotorpwr = -pid_tank_drive_parameters.drive -boostVal; + rmotorpwr = -pid_tank_drive_parameters.drive -boostVal; + break; + case '6': + lmotorpwr = -pid_tank_drive_parameters.drive -boostVal; + rmotorpwr = -pid_tank_drive_parameters.drive -boostVal; + desiredHeading-=pid_tank_drive_parameters.headingTurn; + break; + } + + if (laptop_packetBuffer[1] != '0') // We want to drive, therefore start calculating pidout + pidOutput = calcTurnPower(desiredHeading - getGyroZ()); + + EVERY_N_MILLIS(10) { + driveMotors.l_motor_write(- lmotorpwr + pidOutput); + driveMotors.r_motor_write(rmotorpwr + pidOutput); + } + + toggleLeds(CRGB::Green, CRGB::Blue, 500); + + EVERY_N_MILLIS(100) { + switch (laptop_packetBuffer[2]) { + case '1': + PID.kP+=.001; + break; + case '2': + PID.kP-=.001; + break; + case '3': + PID.kI+=.01; + break; + case '4': + PID.kI-=.01; + break; + case '5': + PID.kD+=.001; + break; + case '6': + PID.kD-=.001; + break; + } + + if (laptop_packetBuffer[2] != '0') { + String msg = "kP : " + String(PID.kP, 3) + " kI : " + String(PID.kI ,3) + " kD : " + String(PID.kD, 3); + laptop.send(msg); + } + } + + } else { // Currently disabled toggleLeds(CRGB::Red, CRGB::Green, 500); driveMotors.set_both_motors(0); @@ -219,4 +312,8 @@ void loop() driveMotors.set_both_motors(0); toggleLeds(CRGB::Red, CRGB::Black, 500); } + + + + } diff --git a/controller/controller.py b/controller/controller.py index e648301..233fd3d 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -142,6 +142,8 @@ async def cmd_handler(): enabled = drivestate = 1 if (get_key_state('x')): enabled = drivestate = 2 + if (get_key_state('c')): + enabled = drivestate = 3 curState = get_key_state('1') if curState and not lastState: From d131848e94af67d5790adb8cfb83fc43dcc90073 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Mon, 25 Mar 2024 20:57:36 -0700 Subject: [PATCH 27/48] DSHOT + Led working --- .DS_Store | Bin 6148 -> 6148 bytes Alipay Robot Code/lib/Melty/melty.cpp | 4 + Alipay Robot Code/lib/Melty/melty.h | 5 + Alipay Robot Code/src/main.cpp | 48 +-- {Motor_Test => Motor_Test_DSDHOT}/.gitignore | 0 .../.vscode/extensions.json | 0 .../include/README | 0 Motor_Test_DSDHOT/lib/DShotRMT/DShotRMT.cpp | 277 ++++++++++++++++++ Motor_Test_DSDHOT/lib/DShotRMT/DShotRMT.h | 201 +++++++++++++ {Motor_Test => Motor_Test_DSDHOT}/lib/README | 0 Motor_Test_DSDHOT/platformio.ini | 21 ++ Motor_Test_DSDHOT/src/main.cpp | 67 +++++ {Motor_Test => Motor_Test_DSDHOT}/test/README | 0 Motor_Test_PWM/.gitignore | 5 + Motor_Test_PWM/.vscode/extensions.json | 10 + Motor_Test_PWM/include/README | 39 +++ Motor_Test_PWM/lib/README | 46 +++ {Motor_Test => Motor_Test_PWM}/platformio.ini | 0 {Motor_Test => Motor_Test_PWM}/src/main.cpp | 2 +- Motor_Test_PWM/test/README | 11 + controller/controller.py | 2 +- 21 files changed, 715 insertions(+), 23 deletions(-) rename {Motor_Test => Motor_Test_DSDHOT}/.gitignore (100%) rename {Motor_Test => Motor_Test_DSDHOT}/.vscode/extensions.json (100%) rename {Motor_Test => Motor_Test_DSDHOT}/include/README (100%) create mode 100644 Motor_Test_DSDHOT/lib/DShotRMT/DShotRMT.cpp create mode 100644 Motor_Test_DSDHOT/lib/DShotRMT/DShotRMT.h rename {Motor_Test => Motor_Test_DSDHOT}/lib/README (100%) create mode 100644 Motor_Test_DSDHOT/platformio.ini create mode 100644 Motor_Test_DSDHOT/src/main.cpp rename {Motor_Test => Motor_Test_DSDHOT}/test/README (100%) create mode 100644 Motor_Test_PWM/.gitignore create mode 100644 Motor_Test_PWM/.vscode/extensions.json create mode 100644 Motor_Test_PWM/include/README create mode 100644 Motor_Test_PWM/lib/README rename {Motor_Test => Motor_Test_PWM}/platformio.ini (100%) rename {Motor_Test => Motor_Test_PWM}/src/main.cpp (95%) create mode 100644 Motor_Test_PWM/test/README diff --git a/.DS_Store b/.DS_Store index 0c0e9cd90b83c3e8748aabe524548898b26fa9b2..2d36adc5f2cd5439207dd359524f74e980ffc9b6 100644 GIT binary patch delta 259 zcmZoMXfc=|#>B!ku~2NHo+2aX#(>?7izhHMF>+4kVKU-nXK-XFVn}AlV8~=Bn{2?O z&B!o0hUp_`N^x>dQht68!(?-2RZTtyUxs{!5+E*Oh-V04NM$Gn^5Pi+7{VERlgf(= zl5+Bs7#J9KOs-=pB)qu~2NHo+2ab#(>?7jI5J+Sd1n|vdA(rPA+5>o7~HCdvZG~zbqGn zFGD^<2@n@C#5062q%ssUl%y0V=OpFl=WG__5M!CxP_dbvgP#LvB9Q-`c{0Cp6F!wdj@-5Y}d diff --git a/Alipay Robot Code/lib/Melty/melty.cpp b/Alipay Robot Code/lib/Melty/melty.cpp index cbfb876..1ed3af4 100644 --- a/Alipay Robot Code/lib/Melty/melty.cpp +++ b/Alipay Robot Code/lib/Melty/melty.cpp @@ -4,6 +4,7 @@ melty::melty() { period_micros_calc = ringBuffer(1); time_seen_beacon_calc = ringBuffer(0.5); + photo_resistor_vals = ringBuffer(1); } bool melty::update() { @@ -20,6 +21,7 @@ bool melty::update() { period_micros = currentPulse - lastPulse; // How long it takes to complete one revolution period_micros_calc.update(period_micros); lastPulse = currentPulse; + } else { // Activates on the falling edge of seeing the IR LED setLeds(CRGB::Red); @@ -31,6 +33,8 @@ bool melty::update() { } } + if (curSeenIRLed) + photo_resistor_vals.update(analogRead(BOTTOM_IR_PIN)); lastSeenIRLed = curSeenIRLed; return curSeenIRLed; diff --git a/Alipay Robot Code/lib/Melty/melty.h b/Alipay Robot Code/lib/Melty/melty.h index 64d6f5c..5a7ce89 100644 --- a/Alipay Robot Code/lib/Melty/melty.h +++ b/Alipay Robot Code/lib/Melty/melty.h @@ -53,6 +53,8 @@ class ringBuffer{ return maxVal; } + + private: unsigned long ringBuf[RINGBUFSIZE]; int curIndex = 0; @@ -83,6 +85,9 @@ class melty { int deg = 0; float percentageOfRotation = 0; bool useTopIr = 1; + + ringBuffer photo_resistor_vals = ringBuffer(1); + private: bool lastSeenIRLed = 0; unsigned long period_micros = micros(); diff --git a/Alipay Robot Code/src/main.cpp b/Alipay Robot Code/src/main.cpp index a6d8ef6..e7d8cee 100644 --- a/Alipay Robot Code/src/main.cpp +++ b/Alipay Robot Code/src/main.cpp @@ -40,11 +40,17 @@ struct pid_tank_drive_parameters { void setup() { init_led(); - setLeds(CRGB::Green); + for (int i = 0; i < 10; i++) { + setLeds(CRGB::Orange); + delay(50); + } USBSerial.begin(115200); - init_mpu6050(); + // init_mpu6050(); + USBSerial.println("init mpu6050"); driveMotors.init_motors(); + USBSerial.println("init motors"); laptop.init_ble("Alipay"); + USBSerial.println("init laptop"); setLeds(CRGB::Black); } @@ -52,16 +58,16 @@ void loop() { if (laptop.isConnected()) { if (driveMotors.isNeutral()) { - if (getAccelZ() > 7) { + // if (getAccelZ() > 7) { alipay.useTopIr = 1; driveMotors.flip_motors = 0; melty_parameters.invert = 1; - } - if (getAccelZ() < -7) { - alipay.useTopIr = 0; - driveMotors.flip_motors = 1; - melty_parameters.invert = -1; - } + // } + // if (getAccelZ() < -7) { + // alipay.useTopIr = 0; + // driveMotors.flip_motors = 1; + // melty_parameters.invert = -1; + // } } if (laptop_packetBuffer[0] == '1') { // Currently enabled and meltybraining!!! @@ -98,7 +104,7 @@ void loop() } EVERY_N_SECONDS(1) { // DEBUGGIN!!!! - String msg = "RPM : " + String(alipay.RPM); + String msg = "PR IR_LED: " + String(alipay.photo_resistor_vals.getMaxVal()); laptop.send(msg); } @@ -133,14 +139,14 @@ void loop() wasMeltying = 1; } else if (laptop_packetBuffer[0] == '2') { // Tank driving mode! - if (wasMeltying) { // Was previously meltybraining, we need to slowdown - unsigned long timeout = millis(); - while (getAccelY() > .3 && millis() - timeout < 2000) { - driveMotors.set_both_motors(-slowDownSpeed * melty_parameters.invert); - toggleLeds(CRGB::White, CRGB::Red, 150); - } - wasMeltying = 0; - } + // if (wasMeltying) { // Was previously meltybraining, we need to slowdown + // unsigned long timeout = millis(); + // while (getAccelY() > .3 && millis() - timeout < 2000) { + // driveMotors.set_both_motors(-slowDownSpeed * melty_parameters.invert); + // toggleLeds(CRGB::White, CRGB::Red, 150); + // } + // wasMeltying = 0; + // } int lmotorpwr = 0; int rmotorpwr = 0; @@ -229,7 +235,7 @@ void loop() case '0': lmotorpwr = 0; rmotorpwr = 0; - desiredHeading = getGyroZ(); + // desiredHeading = getGyroZ(); break; case '8': lmotorpwr = pid_tank_drive_parameters.drive +boostVal; @@ -262,8 +268,8 @@ void loop() break; } - if (laptop_packetBuffer[1] != '0') // We want to drive, therefore start calculating pidout - pidOutput = calcTurnPower(desiredHeading - getGyroZ()); + // if (laptop_packetBuffer[1] != '0') // We want to drive, therefore start calculating pidout + // pidOutput = calcTurnPower(desiredHeading - getGyroZ()); EVERY_N_MILLIS(10) { driveMotors.l_motor_write(- lmotorpwr + pidOutput); diff --git a/Motor_Test/.gitignore b/Motor_Test_DSDHOT/.gitignore similarity index 100% rename from Motor_Test/.gitignore rename to Motor_Test_DSDHOT/.gitignore diff --git a/Motor_Test/.vscode/extensions.json b/Motor_Test_DSDHOT/.vscode/extensions.json similarity index 100% rename from Motor_Test/.vscode/extensions.json rename to Motor_Test_DSDHOT/.vscode/extensions.json diff --git a/Motor_Test/include/README b/Motor_Test_DSDHOT/include/README similarity index 100% rename from Motor_Test/include/README rename to Motor_Test_DSDHOT/include/README diff --git a/Motor_Test_DSDHOT/lib/DShotRMT/DShotRMT.cpp b/Motor_Test_DSDHOT/lib/DShotRMT/DShotRMT.cpp new file mode 100644 index 0000000..ac703ef --- /dev/null +++ b/Motor_Test_DSDHOT/lib/DShotRMT/DShotRMT.cpp @@ -0,0 +1,277 @@ +// +// Name: DShotRMT.cpp +// Created: 20.03.2021 00:49:15 +// Author: derdoktor667 +// + +#include + +// Constructor that takes gpio and rmtChannel as arguments +DShotRMT::DShotRMT(gpio_num_t gpio, rmt_channel_t rmtChannel) +{ + // Initialize the dshot_config structure with the arguments passed to the constructor + dshot_config.gpio_num = gpio; + dshot_config.pin_num = static_cast(gpio); + dshot_config.rmt_channel = rmtChannel; + dshot_config.mem_block_num = static_cast(RMT_CHANNEL_MAX - static_cast(rmtChannel)); + + // Create an empty packet using the DSHOT_NULL_PACKET and the buildTxRmtItem function + buildTxRmtItem(DSHOT_NULL_PACKET); +} + +// Constructor that takes pin and channel as arguments +DShotRMT::DShotRMT(uint8_t pin, uint8_t channel) +{ + // Initialize the dshot_config structure with the arguments passed to the constructor + dshot_config.gpio_num = static_cast(pin); + dshot_config.pin_num = pin; + dshot_config.rmt_channel = static_cast(channel); + dshot_config.mem_block_num = RMT_CHANNEL_MAX - channel; + + // Create an empty packet using the DSHOT_NULL_PACKET and the buildTxRmtItem function + buildTxRmtItem(DSHOT_NULL_PACKET); +} + +// ...simplest but only for testing +DShotRMT::DShotRMT(uint8_t pin) +{ + // Initialize the dshot_config structure with the arguments passed to the constructor + dshot_config.gpio_num = static_cast(pin); + dshot_config.pin_num = pin; + dshot_config.rmt_channel = static_cast(RMT_CHANNEL_MAX -1); + dshot_config.mem_block_num = RMT_CHANNEL_MAX - 1; + + // Create an empty packet using the DSHOT_NULL_PACKET and the buildTxRmtItem function + buildTxRmtItem(DSHOT_NULL_PACKET); +} + +DShotRMT::~DShotRMT() +{ + // Uninstall the RMT driver + rmt_driver_uninstall(dshot_config.rmt_channel); +} + +DShotRMT::DShotRMT(DShotRMT const &) +{ + // ...write me +} + +bool DShotRMT::begin(dshot_mode_t dshot_mode, bool is_bidirectional) +{ + // Set DShot configuration parameters based on input parameters + dshot_config.mode = dshot_mode; + dshot_config.clk_div = DSHOT_CLK_DIVIDER; + dshot_config.name_str = dshot_mode_name[dshot_mode]; + dshot_config.is_bidirectional = is_bidirectional; + + // Set timing parameters based on selected DShot mode + switch (dshot_config.mode) + { + case DSHOT150: + dshot_config.ticks_per_bit = 64; + dshot_config.ticks_zero_high = 24; + dshot_config.ticks_one_high = 48; + break; + + case DSHOT300: + dshot_config.ticks_per_bit = 32; + dshot_config.ticks_zero_high = 12; + dshot_config.ticks_one_high = 24; + break; + + case DSHOT600: + dshot_config.ticks_per_bit = 16; + dshot_config.ticks_zero_high = 6; + dshot_config.ticks_one_high = 12; + break; + + case DSHOT1200: + dshot_config.ticks_per_bit = 8; + dshot_config.ticks_zero_high = 3; + dshot_config.ticks_one_high = 6; + break; + + // Default case to handle invalid input + default: + dshot_config.ticks_per_bit = 0; + dshot_config.ticks_zero_high = 0; + dshot_config.ticks_one_high = 0; + break; + } + + // Calculate low signal timing + dshot_config.ticks_zero_low = (dshot_config.ticks_per_bit - dshot_config.ticks_zero_high); + dshot_config.ticks_one_low = (dshot_config.ticks_per_bit - dshot_config.ticks_one_high); + + // Set up RMT configuration for DShot transmission + dshot_tx_rmt_config.rmt_mode = RMT_MODE_TX; + dshot_tx_rmt_config.channel = dshot_config.rmt_channel; + dshot_tx_rmt_config.gpio_num = dshot_config.gpio_num; + dshot_tx_rmt_config.mem_block_num = dshot_config.mem_block_num; + dshot_tx_rmt_config.clk_div = dshot_config.clk_div; + dshot_tx_rmt_config.tx_config.loop_en = false; + dshot_tx_rmt_config.tx_config.carrier_en = false; + dshot_tx_rmt_config.tx_config.idle_output_en = true; + + // Set idle level for RMT transmission based on input parameter + if (dshot_config.is_bidirectional) + { + dshot_tx_rmt_config.tx_config.idle_level = RMT_IDLE_LEVEL_HIGH; + } + else + { + dshot_tx_rmt_config.tx_config.idle_level = RMT_IDLE_LEVEL_LOW; + } + + // Set up selected DShot mode + rmt_config(&dshot_tx_rmt_config); + + // Install RMT driver and return result + return rmt_driver_install(dshot_tx_rmt_config.channel, 0, 0); +} + +// Define a function to send a DShot command over an RMT interface to control a brushless motor's speed. +void DShotRMT::sendThrottleValue(uint16_t throttle_value) +{ + dshot_packet_t dshot_rmt_packet = {}; + + // Check if the throttle value is less than the minimum allowed value for the DShot protocol. + if (throttle_value < DSHOT_THROTTLE_MIN) + { + throttle_value = DSHOT_THROTTLE_MIN; + } + + // Check if the throttle value is greater than the maximum allowed value for the DShot protocol. + if (throttle_value > DSHOT_THROTTLE_MAX) + { + throttle_value = DSHOT_THROTTLE_MAX; + } + + dshot_rmt_packet.throttle_value = throttle_value; + + // Telemetric using additional pin on the ESC is not supported. + dshot_rmt_packet.telemetric_request = NO_TELEMETRIC; + + // Calculate the checksum for the DShot packet using the calculateCRC function. + dshot_rmt_packet.checksum = calculateCRC(dshot_rmt_packet); + + // Send the DShot packet over the RMT interface to control the motor's speed. + sendRmtPaket(dshot_rmt_packet); +} + +// This method builds the RMT data transmission sequence for the DShot protocol +rmt_item32_t *DShotRMT::buildTxRmtItem(uint16_t parsed_packet) +{ + // Check if DShot is set to bidirectional mode + if (dshot_config.is_bidirectional) + { + // If bidirectional, invert the high/low bits + for (int i = 0; i < DSHOT_PAUSE_BIT; i++, parsed_packet <<= 1) + { + if (parsed_packet & 0b1000000000000000) + { + // Set RMT item for a logic high signal + dshot_tx_rmt_item[i].duration0 = dshot_config.ticks_one_low; + dshot_tx_rmt_item[i].duration1 = dshot_config.ticks_one_high; + } + else + { + // Set RMT item for a logic low signal + dshot_tx_rmt_item[i].duration0 = dshot_config.ticks_zero_low; + dshot_tx_rmt_item[i].duration1 = dshot_config.ticks_zero_high; + } + + // Set level of RMT item + dshot_tx_rmt_item[i].level0 = 0; + dshot_tx_rmt_item[i].level1 = 1; + } + } + else + { + // If not bidirectional, set the RMT items as usual + for (int i = 0; i < DSHOT_PAUSE_BIT; i++, parsed_packet <<= 1) + { + if (parsed_packet & 0b1000000000000000) + { + // Set RMT item for a logic high signal + dshot_tx_rmt_item[i].duration0 = dshot_config.ticks_one_high; + dshot_tx_rmt_item[i].duration1 = dshot_config.ticks_one_low; + } + else + { + // Set RMT item for a logic low signal + dshot_tx_rmt_item[i].duration0 = dshot_config.ticks_zero_high; + dshot_tx_rmt_item[i].duration1 = dshot_config.ticks_zero_low; + } + + // Set level of RMT item + dshot_tx_rmt_item[i].level0 = 1; + dshot_tx_rmt_item[i].level1 = 0; + } + } + + // Set end marker for each frame + if (dshot_config.is_bidirectional) + { + dshot_tx_rmt_item[DSHOT_PAUSE_BIT].level0 = 1; + dshot_tx_rmt_item[DSHOT_PAUSE_BIT].level1 = 0; + } + else + { + dshot_tx_rmt_item[DSHOT_PAUSE_BIT].level0 = 0; + dshot_tx_rmt_item[DSHOT_PAUSE_BIT].level1 = 1; + } + + // Add packet seperator aka DShot Pause. + dshot_tx_rmt_item[DSHOT_PAUSE_BIT].duration1 = DSHOT_PAUSE; + + // Return the rmt_item + return dshot_tx_rmt_item; +} + +// Calculates a CRC value for a DShot digital control signal packet +uint16_t DShotRMT::calculateCRC(const dshot_packet_t &dshot_packet) +{ + uint16_t crc; + + // Combine the throttle value and telemetric request flag into a 16-bit packet value + const uint16_t packet = (dshot_packet.throttle_value << 1) | dshot_packet.telemetric_request; + + // Calculate the CRC value using different bitwise operations depending on the DShot configuration + if (dshot_config.is_bidirectional) + { + // Bidirectional configuration: perform a bitwise negation of the result of XORing the packet with its right-shifted values by 4 and 8 bits, + // and then bitwise AND the result with 0x0F + const uint16_t intermediate_result = packet ^ (packet >> 4) ^ (packet >> 8); + crc = (~intermediate_result) & 0x0F; + } + else + { + // Unidirectional configuration: XOR the packet with its right-shifted values by 4 and 8 bits, + // and then bitwise AND the result with 0x0F + crc = (packet ^ (packet >> 4) ^ (packet >> 8)) & 0x0F; + } + + // Return the calculated CRC value as a 16-bit unsigned integer + return crc; +} + +uint16_t DShotRMT::parseRmtPaket(const dshot_packet_t &dshot_packet) +{ + uint16_t parsedRmtPaket = DSHOT_NULL_PACKET; + uint16_t crc = calculateCRC(dshot_packet); + + // Complete the paket + parsedRmtPaket = (dshot_packet.throttle_value << 1) | dshot_packet.telemetric_request; + parsedRmtPaket = (parsedRmtPaket << 4) | crc; + + return parsedRmtPaket; +} + +// Output using ESP32 RMT +void DShotRMT::sendRmtPaket(const dshot_packet_t &dshot_packet) +{ + buildTxRmtItem(parseRmtPaket(dshot_packet)); + + rmt_write_items(dshot_tx_rmt_config.channel, dshot_tx_rmt_item, DSHOT_PACKET_LENGTH, false); +} diff --git a/Motor_Test_DSDHOT/lib/DShotRMT/DShotRMT.h b/Motor_Test_DSDHOT/lib/DShotRMT/DShotRMT.h new file mode 100644 index 0000000..7713be5 --- /dev/null +++ b/Motor_Test_DSDHOT/lib/DShotRMT/DShotRMT.h @@ -0,0 +1,201 @@ +// +// Name: DShotRMT.h +// Created: 20.03.2021 00:49:15 +// Author: derdoktor667 +// + +#ifndef _DSHOTRMT_h +#define _DSHOTRMT_h + +#include + +// The RMT (Remote Control) module library is used for generating the DShot signal. +#include + +// Defines the library version +constexpr auto DSHOT_LIB_VERSION = "0.2.4"; + +// Constants related to the DShot protocol +constexpr auto DSHOT_CLK_DIVIDER = 8; // Slow down RMT clock to 0.1 microseconds / 100 nanoseconds per cycle +constexpr auto DSHOT_PACKET_LENGTH = 17; // Last pack is the pause +constexpr auto DSHOT_THROTTLE_MIN = 48; +constexpr auto DSHOT_THROTTLE_MAX = 2047; +constexpr auto DSHOT_NULL_PACKET = 0b0000000000000000; +constexpr auto DSHOT_PAUSE = 21; // 21-bit is recommended +constexpr auto DSHOT_PAUSE_BIT = 16; +constexpr auto F_CPU_RMT = APB_CLK_FREQ; +constexpr auto RMT_CYCLES_PER_SEC = (F_CPU_RMT / DSHOT_CLK_DIVIDER); +constexpr auto RMT_CYCLES_PER_ESP_CYCLE = (F_CPU / RMT_CYCLES_PER_SEC); + +// Enumeration for the DShot mode +typedef enum dshot_mode_e +{ + DSHOT_OFF, + DSHOT150, + DSHOT300, + DSHOT600, + DSHOT1200 +} dshot_mode_t; + +// Array of human-readable DShot mode names +static const char *const dshot_mode_name[] = { + "DSHOT_OFF", + "DSHOT150", + "DSHOT300", + "DSHOT600", + "DSHOT1200"}; + +// Enumeration for telemetric request +typedef enum telemetric_request_e +{ + NO_TELEMETRIC, + ENABLE_TELEMETRIC, +} telemetric_request_t; + +// Structure for DShot packets +typedef struct dshot_packet_s +{ + uint16_t throttle_value : 11; + telemetric_request_t telemetric_request : 1; + uint16_t checksum : 4; +} dshot_packet_t; + +// Structure for eRPM packets +typedef struct eRPM_packet_s +{ + uint16_t eRPM_data : 12; + uint8_t checksum : 4; +} eRPM_packet_t; + +// return states of the RPM getting function +typedef enum dshot_erpm_exit_mode_e +{ + DECODE_SUCCESS = 0, + ERR_EMPTY_QUEUE, + ERR_NO_PACKETS, + ERR_CHECKSUM_FAIL, + ERR_BIDIRECTION_DISABLED, + +} dshot_erpm_exit_mode_t; + +// Structure for all settings for the DShot mode +typedef struct dshot_config_s +{ + dshot_mode_t mode; + String name_str; + bool is_bidirectional; + gpio_num_t gpio_num; + uint8_t pin_num; + rmt_channel_t rmt_channel; + uint8_t mem_block_num; + uint16_t ticks_per_bit; + uint8_t clk_div; + uint16_t ticks_zero_high; + uint16_t ticks_zero_low; + uint16_t ticks_one_high; + uint16_t ticks_one_low; +} dshot_config_t; + +// The official DShot Commands +typedef enum dshot_cmd_e +{ + DSHOT_CMD_MOTOR_STOP = 0, // Currently not implemented - STOP Motors + DSHOT_CMD_BEEP1, // Wait at least length of beep (380ms) before next command + DSHOT_CMD_BEEP2, // Wait at least length of beep (380ms) before next command + DSHOT_CMD_BEEP3, // Wait at least length of beep (400ms) before next command + DSHOT_CMD_BEEP4, // Wait at least length of beep (400ms) before next command + DSHOT_CMD_BEEP5, // Wait at least length of beep (400ms) before next command + DSHOT_CMD_ESC_INFO, // Currently not implemented + DSHOT_CMD_SPIN_DIRECTION_1, // Need 6x, no wait required + DSHOT_CMD_SPIN_DIRECTION_2, // Need 6x, no wait required + DSHOT_CMD_3D_MODE_OFF, // Need 6x, no wait required + DSHOT_CMD_3D_MODE_ON, // Need 6x, no wait required + DSHOT_CMD_SETTINGS_REQUEST, // Currently not implemented + DSHOT_CMD_SAVE_SETTINGS, // Need 6x, wait at least 12ms before next command + DSHOT_CMD_SPIN_DIRECTION_NORMAL, // Need 6x, no wait required + DSHOT_CMD_SPIN_DIRECTION_REVERSED, // Need 6x, no wait required + DSHOT_CMD_LED0_ON, // Currently not implemented + DSHOT_CMD_LED1_ON, // Currently not implemented + DSHOT_CMD_LED2_ON, // Currently not implemented + DSHOT_CMD_LED3_ON, // Currently not implemented + DSHOT_CMD_LED0_OFF, // Currently not implemented + DSHOT_CMD_LED1_OFF, // Currently not implemented + DSHOT_CMD_LED2_OFF, // Currently not implemented + DSHOT_CMD_LED3_OFF, // Currently not implemented + DSHOT_CMD_36, // Not yet assigned + DSHOT_CMD_37, // Not yet assigned + DSHOT_CMD_38, // Not yet assigned + DSHOT_CMD_39, // Not yet assigned + DSHOT_CMD_40, // Not yet assigned + DSHOT_CMD_41, // Not yet assigned + DSHOT_CMD_SIGNAL_LINE_TEMPERATURE_TELEMETRY, // No wait required + DSHOT_CMD_SIGNAL_LINE_VOLTAGE_TELEMETRY, // No wait required + DSHOT_CMD_SIGNAL_LINE_CURRENT_TELEMETRY, // No wait required + DSHOT_CMD_SIGNAL_LINE_CONSUMPTION_TELEMETRY, // No wait required + DSHOT_CMD_SIGNAL_LINE_ERPM_TELEMETRY, // No wait required + DSHOT_CMD_SIGNAL_LINE_ERPM_PERIOD_TELEMETRY, // No wait required (also command 47) + DSHOT_CMD_MAX = 47 +} dshot_cmd_t; + +// ...Mapping for GCR +static const unsigned char GCR_encode[16] = + { + 0x19, 0x1B, 0x12, 0x13, + 0x1D, 0x15, 0x16, 0x17, + 0x1A, 0x09, 0x0A, 0x0B, + 0x1E, 0x0D, 0x0E, 0x0F}; + +// ...shifting 5 bits > 4 bits (0xff => invalid) +static const unsigned char GCR_decode[32] = + { + 0xFF, 0xFF, 0xFF, 0xFF, // 0 - 3 + 0xFF, 0xFF, 0xFF, 0xFF, // 4 - 7 + 0xFF, 9, 10, 11, // 8 - 11 + 0xFF, 13, 14, 15, // 12 - 15 + + 0xFF, 0xFF, 2, 3, // 16 - 19 + 0xFF, 5, 6, 7, // 20 - 23 + 0xFF, 0, 8, 1, // 24 - 27 + 0xFF, 4, 12, 0xFF, // 28 - 31 +}; + +// The main DShotRMT class +class DShotRMT +{ +public: + // Constructor for the DShotRMT class + DShotRMT(gpio_num_t gpio, rmt_channel_t rmtChannel); + DShotRMT(uint8_t pin, uint8_t channel); + DShotRMT(uint8_t pin); + + // Destructor for the DShotRMT class + ~DShotRMT(); + + // Copy constructor for the DShotRMT class + DShotRMT(DShotRMT const &); + + // The begin() function initializes the DShotRMT class with + // a given DShot mode (DSHOT_OFF, DSHOT150, DSHOT300, DSHOT600, DSHOT1200) + // and a bidirectional flag. It returns a boolean value + // indicating whether or not the initialization was successful. + bool begin(dshot_mode_t dshot_mode = DSHOT_OFF, bool is_bidirectional = false); + + // The sendThrottleValue() function sends a DShot packet with a given + // throttle value (between 49 and 2047) and an optional telemetry + // request flag. + // void sendThrottleValue(uint16_t throttle_value, telemetric_request_t telemetric_request = NO_TELEMETRIC); + void sendThrottleValue(uint16_t throttle_value); + +private: + rmt_item32_t dshot_tx_rmt_item[DSHOT_PACKET_LENGTH]; // An array of RMT items used to send a DShot packet. + rmt_config_t dshot_tx_rmt_config; // The RMT configuration used for sending DShot packets. + dshot_config_t dshot_config; // The configuration for the DShot mode. + + rmt_item32_t *buildTxRmtItem(uint16_t parsed_packet); // Constructs an RMT item from a parsed DShot packet. + uint16_t calculateCRC(const dshot_packet_t &dshot_packet); // Calculates the CRC checksum for a DShot packet. + uint16_t parseRmtPaket(const dshot_packet_t &dshot_packet); // Parses an RMT packet to obtain a DShot packet. + + void sendRmtPaket(const dshot_packet_t &dshot_packet); // Sends a DShot packet via RMT. +}; + +#endif diff --git a/Motor_Test/lib/README b/Motor_Test_DSDHOT/lib/README similarity index 100% rename from Motor_Test/lib/README rename to Motor_Test_DSDHOT/lib/README diff --git a/Motor_Test_DSDHOT/platformio.ini b/Motor_Test_DSDHOT/platformio.ini new file mode 100644 index 0000000..7720063 --- /dev/null +++ b/Motor_Test_DSDHOT/platformio.ini @@ -0,0 +1,21 @@ +; 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-s3-devkitc-1] +platform = espressif32 +board = esp32-s3-devkitc-1 +framework = arduino +lib_deps = + moddingear/ESP32 ESC@^1.0.0 + fastled/FastLED@^3.6.0 + freenove/Freenove WS2812 Lib for ESP32@^1.0.6 +upload_speed = 921600 +monitor_speed = 115200 +monitor_filters = send_on_enter diff --git a/Motor_Test_DSDHOT/src/main.cpp b/Motor_Test_DSDHOT/src/main.cpp new file mode 100644 index 0000000..b389ea8 --- /dev/null +++ b/Motor_Test_DSDHOT/src/main.cpp @@ -0,0 +1,67 @@ +#define LEDS_COUNT 1 +#define LEDS_PIN 4 +#define CHANNEL 0 +#include "Freenove_WS2812_Lib_for_ESP32.h" +#include "DShotESC.h" +#include "/Users/mingweiyeoh/Documents/GitHub/Arduino-Projects/libraries/Custom/USBSerialHandler.h" + +SerialHandler myComputer = SerialHandler(); + +Freenove_ESP32_WS2812 strip = Freenove_ESP32_WS2812(LEDS_COUNT, LEDS_PIN, 0, TYPE_GRB); // Channel is always 0 for some reason..... + +DShotESC rmot; +DShotESC lmot; + +void setup() +{ + USBSerial.begin(115200); + + rmot.install(GPIO_NUM_7, RMT_CHANNEL_2); //<-- This is the problem line. Apparently this line has to go before initializing the strip + rmot.init(); + rmot.setReversed(false); + rmot.set3DMode(true); + rmot.throttleArm(); // <--- Super important!!!; + + lmot.install(GPIO_NUM_8, RMT_CHANNEL_1); //<-- This is the problem line. Apparently this line has to go before initializing the strip + lmot.init(); + lmot.setReversed(false); + lmot.set3DMode(true); + lmot.throttleArm(); // <--- Super important!!!; + + + strip.begin(); + strip.setBrightness(10); + + for (int i = 0; i < 3 ; i++) { + strip.setLedColorData(0, 255, 0 ,0); + strip.show(); + delay(1); + } + + int mydelay = 10; + for (int i = 0; i < 3000/mydelay; i++) + { + rmot.sendThrottle3D(0); + lmot.sendThrottle(0); + delay(mydelay); + } + USBSerial.println("Armed!!!"); +} + +void loop() { + int throttle = myComputer.getInt(0); + rmot.sendThrottle3D(throttle); // Throttle value from -999 to 999 + lmot.sendThrottle3D(throttle); + delay(10); + + if (throttle != 0) { + strip.setLedColorData(0, 255, 0 ,0); + strip.show(); + } else { + strip.setLedColorData(0, 0, 255 ,0); + strip.show(); + } + +} + + diff --git a/Motor_Test/test/README b/Motor_Test_DSDHOT/test/README similarity index 100% rename from Motor_Test/test/README rename to Motor_Test_DSDHOT/test/README diff --git a/Motor_Test_PWM/.gitignore b/Motor_Test_PWM/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/Motor_Test_PWM/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/Motor_Test_PWM/.vscode/extensions.json b/Motor_Test_PWM/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/Motor_Test_PWM/.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/Motor_Test_PWM/include/README b/Motor_Test_PWM/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/Motor_Test_PWM/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/Motor_Test_PWM/lib/README b/Motor_Test_PWM/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/Motor_Test_PWM/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 +#include + +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/Motor_Test/platformio.ini b/Motor_Test_PWM/platformio.ini similarity index 100% rename from Motor_Test/platformio.ini rename to Motor_Test_PWM/platformio.ini diff --git a/Motor_Test/src/main.cpp b/Motor_Test_PWM/src/main.cpp similarity index 95% rename from Motor_Test/src/main.cpp rename to Motor_Test_PWM/src/main.cpp index 674e13a..758fdbc 100644 --- a/Motor_Test/src/main.cpp +++ b/Motor_Test_PWM/src/main.cpp @@ -3,7 +3,7 @@ SerialHandler myComputer = SerialHandler(); -const int motpin = 8; // 7 is right and 8 is left +const int motpin = 7; // 7 is right and 8 is left const int motchannel = 0; const int neutralVal = 189; diff --git a/Motor_Test_PWM/test/README b/Motor_Test_PWM/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/Motor_Test_PWM/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 diff --git a/controller/controller.py b/controller/controller.py index 233fd3d..13ec139 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -88,7 +88,7 @@ async def cmd_handler(): if get_key_state(Key.left): x = x - 1 if get_key_state(Key.right): - x = x + 1 + x = x + 1 if x == 0 and y == 0: drivecmd = 0 From ac8f54afa5b1017c413ce28b1a758ca0a66a02cb Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Mon, 1 Apr 2024 18:48:25 -0700 Subject: [PATCH 28/48] renamed, starting moving to dshot and different type of led driver --- .DS_Store | Bin 6148 -> 6148 bytes Alipay Robot Code/lib/LEDHandler/LEDHandler.h | 15 --- Motor_Test_DSDHOT/src/main.cpp | 36 ++++--- {Alipay Robot Code => Robot Code}/.DS_Store | Bin {Alipay Robot Code => Robot Code}/.gitignore | 0 .../.vscode/extensions.json | 0 .../.vscode/settings.json | 0 .../include/README | 0 .../lib/.DS_Store | Bin .../lib/Battery_Monitor/Battery_Monitor.h | 0 .../lib/Drive_Motors/Drive_Motors.cpp | 0 .../lib/Drive_Motors/Drive_Motors.h | 0 .../lib/LEDHandler/LEDHandler.cpp | 38 +++---- Robot Code/lib/LEDHandler/LEDHandler.h | 29 ++++++ .../lib/LaptopTelemetry/LaptopTelemetry.cpp | 0 .../lib/LaptopTelemetry/LaptopTelemetry.h | 0 .../lib/Melty/melty.cpp | 4 +- .../lib/Melty/melty.h | 0 .../lib/PID/PID.h | 0 {Alipay Robot Code => Robot Code}/lib/README | 0 .../lib/mpu6050/mpu6050.cpp | 0 .../lib/mpu6050/mpu6050.h | 0 .../platformio.ini | 4 +- .../src/main.cpp | 17 ++-- {Alipay Robot Code => Robot Code}/test/README | 0 controller/drawing.svg | 50 ++++++++++ .../drawing.svg.2024_03_26_20_10_56.0.svg | 93 ++++++++++++++++++ controller/rpmgauge.jpg | Bin 0 -> 140830 bytes controller/tkinter_testing.py | 48 +++++++++ 29 files changed, 279 insertions(+), 55 deletions(-) delete mode 100644 Alipay Robot Code/lib/LEDHandler/LEDHandler.h rename {Alipay Robot Code => Robot Code}/.DS_Store (100%) rename {Alipay Robot Code => Robot Code}/.gitignore (100%) rename {Alipay Robot Code => Robot Code}/.vscode/extensions.json (100%) rename {Alipay Robot Code => Robot Code}/.vscode/settings.json (100%) rename {Alipay Robot Code => Robot Code}/include/README (100%) rename {Alipay Robot Code => Robot Code}/lib/.DS_Store (100%) rename {Alipay Robot Code => Robot Code}/lib/Battery_Monitor/Battery_Monitor.h (100%) rename {Alipay Robot Code => Robot Code}/lib/Drive_Motors/Drive_Motors.cpp (100%) rename {Alipay Robot Code => Robot Code}/lib/Drive_Motors/Drive_Motors.h (100%) rename {Alipay Robot Code => Robot Code}/lib/LEDHandler/LEDHandler.cpp (51%) create mode 100644 Robot Code/lib/LEDHandler/LEDHandler.h rename {Alipay Robot Code => Robot Code}/lib/LaptopTelemetry/LaptopTelemetry.cpp (100%) rename {Alipay Robot Code => Robot Code}/lib/LaptopTelemetry/LaptopTelemetry.h (100%) rename {Alipay Robot Code => Robot Code}/lib/Melty/melty.cpp (98%) rename {Alipay Robot Code => Robot Code}/lib/Melty/melty.h (100%) rename {Alipay Robot Code => Robot Code}/lib/PID/PID.h (100%) rename {Alipay Robot Code => Robot Code}/lib/README (100%) rename {Alipay Robot Code => Robot Code}/lib/mpu6050/mpu6050.cpp (100%) rename {Alipay Robot Code => Robot Code}/lib/mpu6050/mpu6050.h (100%) rename {Alipay Robot Code => Robot Code}/platformio.ini (90%) rename {Alipay Robot Code => Robot Code}/src/main.cpp (97%) rename {Alipay Robot Code => Robot Code}/test/README (100%) create mode 100644 controller/drawing.svg create mode 100644 controller/drawing.svg.2024_03_26_20_10_56.0.svg create mode 100644 controller/rpmgauge.jpg create mode 100644 controller/tkinter_testing.py diff --git a/.DS_Store b/.DS_Store index 2d36adc5f2cd5439207dd359524f74e980ffc9b6..ab2150a1ff19f5ec004f34720267f4a6b9ff5cb5 100644 GIT binary patch delta 45 zcmZoMXfc=&$(_ki#*ms)oSc)CzgduDIWr^YW^0xyOpIKU - -#ifdef IR_BEACON - #define LEDPIN 5 -#else - #define LEDPIN 4 -#endif - -void init_led(); - -void setLeds(CRGB color); - -void toggleLeds(CRGB color1, CRGB color2, int delayMS); - -void syncToggle(); \ No newline at end of file diff --git a/Motor_Test_DSDHOT/src/main.cpp b/Motor_Test_DSDHOT/src/main.cpp index b389ea8..807b78a 100644 --- a/Motor_Test_DSDHOT/src/main.cpp +++ b/Motor_Test_DSDHOT/src/main.cpp @@ -4,7 +4,6 @@ #include "Freenove_WS2812_Lib_for_ESP32.h" #include "DShotESC.h" #include "/Users/mingweiyeoh/Documents/GitHub/Arduino-Projects/libraries/Custom/USBSerialHandler.h" - SerialHandler myComputer = SerialHandler(); Freenove_ESP32_WS2812 strip = Freenove_ESP32_WS2812(LEDS_COUNT, LEDS_PIN, 0, TYPE_GRB); // Channel is always 0 for some reason..... @@ -12,29 +11,47 @@ Freenove_ESP32_WS2812 strip = Freenove_ESP32_WS2812(LEDS_COUNT, LEDS_PIN, 0, TYP DShotESC rmot; DShotESC lmot; +enum Colors { + RED, + ORANGE, + YELLOW, + LIME, + GREEN, + AQUA, + CYAN, + SKY, + BLUE, + PURPLE, + RED_PURPLE, +} Colors; + +void setLed(int colorWheel) { + strip.setLedColorData(0, strip.hsv2rgb(colorWheel*30, 100, 100)); + strip.show(); +} + + void setup() { USBSerial.begin(115200); - rmot.install(GPIO_NUM_7, RMT_CHANNEL_2); //<-- This is the problem line. Apparently this line has to go before initializing the strip + rmot.install(GPIO_NUM_7, RMT_CHANNEL_1); //<-- This is the problem line. Apparently this line has to go before initializing the strip rmot.init(); rmot.setReversed(false); rmot.set3DMode(true); rmot.throttleArm(); // <--- Super important!!!; - lmot.install(GPIO_NUM_8, RMT_CHANNEL_1); //<-- This is the problem line. Apparently this line has to go before initializing the strip + lmot.install(GPIO_NUM_8, RMT_CHANNEL_2); //<-- This is the problem line. Apparently this line has to go before initializing the strip lmot.init(); lmot.setReversed(false); lmot.set3DMode(true); lmot.throttleArm(); // <--- Super important!!!; - strip.begin(); strip.setBrightness(10); for (int i = 0; i < 3 ; i++) { - strip.setLedColorData(0, 255, 0 ,0); - strip.show(); + setLed(RED); delay(1); } @@ -55,13 +72,10 @@ void loop() { delay(10); if (throttle != 0) { - strip.setLedColorData(0, 255, 0 ,0); - strip.show(); + setLed(RED); } else { - strip.setLedColorData(0, 0, 255 ,0); - strip.show(); + setLed(GREEN); } } - diff --git a/Alipay Robot Code/.DS_Store b/Robot Code/.DS_Store similarity index 100% rename from Alipay Robot Code/.DS_Store rename to Robot Code/.DS_Store diff --git a/Alipay Robot Code/.gitignore b/Robot Code/.gitignore similarity index 100% rename from Alipay Robot Code/.gitignore rename to Robot Code/.gitignore diff --git a/Alipay Robot Code/.vscode/extensions.json b/Robot Code/.vscode/extensions.json similarity index 100% rename from Alipay Robot Code/.vscode/extensions.json rename to Robot Code/.vscode/extensions.json diff --git a/Alipay Robot Code/.vscode/settings.json b/Robot Code/.vscode/settings.json similarity index 100% rename from Alipay Robot Code/.vscode/settings.json rename to Robot Code/.vscode/settings.json diff --git a/Alipay Robot Code/include/README b/Robot Code/include/README similarity index 100% rename from Alipay Robot Code/include/README rename to Robot Code/include/README diff --git a/Alipay Robot Code/lib/.DS_Store b/Robot Code/lib/.DS_Store similarity index 100% rename from Alipay Robot Code/lib/.DS_Store rename to Robot Code/lib/.DS_Store diff --git a/Alipay Robot Code/lib/Battery_Monitor/Battery_Monitor.h b/Robot Code/lib/Battery_Monitor/Battery_Monitor.h similarity index 100% rename from Alipay Robot Code/lib/Battery_Monitor/Battery_Monitor.h rename to Robot Code/lib/Battery_Monitor/Battery_Monitor.h diff --git a/Alipay Robot Code/lib/Drive_Motors/Drive_Motors.cpp b/Robot Code/lib/Drive_Motors/Drive_Motors.cpp similarity index 100% rename from Alipay Robot Code/lib/Drive_Motors/Drive_Motors.cpp rename to Robot Code/lib/Drive_Motors/Drive_Motors.cpp diff --git a/Alipay Robot Code/lib/Drive_Motors/Drive_Motors.h b/Robot Code/lib/Drive_Motors/Drive_Motors.h similarity index 100% rename from Alipay Robot Code/lib/Drive_Motors/Drive_Motors.h rename to Robot Code/lib/Drive_Motors/Drive_Motors.h diff --git a/Alipay Robot Code/lib/LEDHandler/LEDHandler.cpp b/Robot Code/lib/LEDHandler/LEDHandler.cpp similarity index 51% rename from Alipay Robot Code/lib/LEDHandler/LEDHandler.cpp rename to Robot Code/lib/LEDHandler/LEDHandler.cpp index 6f26789..f2d084f 100644 --- a/Alipay Robot Code/lib/LEDHandler/LEDHandler.cpp +++ b/Robot Code/lib/LEDHandler/LEDHandler.cpp @@ -4,26 +4,28 @@ bool ledToggleState = 0; unsigned long lastdelayToggle = millis(); unsigned long currentDelayToggle = millis(); -CRGB lastColor1; -CRGB lastColor2; +Colors lastColor1; +Colors lastColor2; -CRGB leds[1]; +Freenove_ESP32_WS2812 strip = Freenove_ESP32_WS2812(1, LEDPIN, 0, TYPE_GRB); void init_led() { - FastLED.addLeds(leds, 1).setCorrection(TypicalLEDStrip); + strip.begin(); + strip.setBrightness(100); } -void setLeds(CRGB color) -{ - leds[0] = color; - FastLED.show(); +void setLeds(Colors color) { + if (color == BLACK) + strip.setLedColorData(0, 0, 0, 0); + else if (color == WHITE) + strip.setLedColorData(0, 255, 255, 255); + else + strip.setLedColorData(0, strip.hsv2rgb(color*30, 100, 100)); + // delay(1); + strip.show(); } -void syncToggle() { - lastdelayToggle = millis(); -} - -void toggleLeds(CRGB color1, CRGB color2, int delayMS) { +void toggleLeds(Colors color1, Colors color2, int delayMS) { if (color1 != lastColor1 || color2 != lastColor2 || millis() - lastdelayToggle > delayMS + 50) { // For color syncing purposes between different devices lastdelayToggle = millis(); ledToggleState = 0; @@ -33,13 +35,11 @@ void toggleLeds(CRGB color1, CRGB color2, int delayMS) { if (currentDelayToggle - lastdelayToggle > delayMS) { ledToggleState = !ledToggleState; lastdelayToggle = currentDelayToggle; + if (ledToggleState) + setLeds(color1); + else + setLeds(color2); } - - if (ledToggleState) - setLeds(color1); - else - setLeds(color2); - lastColor1 = color1; lastColor2 = color2; } \ No newline at end of file diff --git a/Robot Code/lib/LEDHandler/LEDHandler.h b/Robot Code/lib/LEDHandler/LEDHandler.h new file mode 100644 index 0000000..60415c8 --- /dev/null +++ b/Robot Code/lib/LEDHandler/LEDHandler.h @@ -0,0 +1,29 @@ +#include + +#ifdef IR_BEACON + #define LEDPIN 5 +#else + #define LEDPIN 4 +#endif + +enum Colors { + RED, + ORANGE, + YELLOW, + LIME, + GREEN, + AQUA, + CYAN, + SKY, + BLUE, + PURPLE, + RED_PURPLE, + BLACK, + WHITE +}; + +void init_led(); + +void setLeds(Colors color); + +void toggleLeds(Colors color1, Colors color2, int delayMS); \ No newline at end of file diff --git a/Alipay Robot Code/lib/LaptopTelemetry/LaptopTelemetry.cpp b/Robot Code/lib/LaptopTelemetry/LaptopTelemetry.cpp similarity index 100% rename from Alipay Robot Code/lib/LaptopTelemetry/LaptopTelemetry.cpp rename to Robot Code/lib/LaptopTelemetry/LaptopTelemetry.cpp diff --git a/Alipay Robot Code/lib/LaptopTelemetry/LaptopTelemetry.h b/Robot Code/lib/LaptopTelemetry/LaptopTelemetry.h similarity index 100% rename from Alipay Robot Code/lib/LaptopTelemetry/LaptopTelemetry.h rename to Robot Code/lib/LaptopTelemetry/LaptopTelemetry.h diff --git a/Alipay Robot Code/lib/Melty/melty.cpp b/Robot Code/lib/Melty/melty.cpp similarity index 98% rename from Alipay Robot Code/lib/Melty/melty.cpp rename to Robot Code/lib/Melty/melty.cpp index 1ed3af4..0e8ec1b 100644 --- a/Alipay Robot Code/lib/Melty/melty.cpp +++ b/Robot Code/lib/Melty/melty.cpp @@ -16,7 +16,7 @@ bool melty::update() { if (curSeenIRLed != lastSeenIRLed) if (curSeenIRLed) { // Activates on the rising edge of seeing the IR LED - setLeds(CRGB::Green); + setLeds(GREEN); currentPulse = micros(); period_micros = currentPulse - lastPulse; // How long it takes to complete one revolution period_micros_calc.update(period_micros); @@ -24,7 +24,7 @@ bool melty::update() { } else { // Activates on the falling edge of seeing the IR LED - setLeds(CRGB::Red); + setLeds(RED); time_seen_beacon = micros() - currentPulse; time_seen_beacon_calc.update(time_seen_beacon); diff --git a/Alipay Robot Code/lib/Melty/melty.h b/Robot Code/lib/Melty/melty.h similarity index 100% rename from Alipay Robot Code/lib/Melty/melty.h rename to Robot Code/lib/Melty/melty.h diff --git a/Alipay Robot Code/lib/PID/PID.h b/Robot Code/lib/PID/PID.h similarity index 100% rename from Alipay Robot Code/lib/PID/PID.h rename to Robot Code/lib/PID/PID.h diff --git a/Alipay Robot Code/lib/README b/Robot Code/lib/README similarity index 100% rename from Alipay Robot Code/lib/README rename to Robot Code/lib/README diff --git a/Alipay Robot Code/lib/mpu6050/mpu6050.cpp b/Robot Code/lib/mpu6050/mpu6050.cpp similarity index 100% rename from Alipay Robot Code/lib/mpu6050/mpu6050.cpp rename to Robot Code/lib/mpu6050/mpu6050.cpp diff --git a/Alipay Robot Code/lib/mpu6050/mpu6050.h b/Robot Code/lib/mpu6050/mpu6050.h similarity index 100% rename from Alipay Robot Code/lib/mpu6050/mpu6050.h rename to Robot Code/lib/mpu6050/mpu6050.h diff --git a/Alipay Robot Code/platformio.ini b/Robot Code/platformio.ini similarity index 90% rename from Alipay Robot Code/platformio.ini rename to Robot Code/platformio.ini index 837a382..d193668 100644 --- a/Alipay Robot Code/platformio.ini +++ b/Robot Code/platformio.ini @@ -14,8 +14,6 @@ board = esp32-s3-devkitc-1 framework = arduino upload_speed = 921600 monitor_speed = 115200 -; upload_port = /dev/tty.usbmodem2101 -; monitor_port = /dev/tty.usbmodem2101 lib_deps = fastled/FastLED@^3.6.0 adafruit/Adafruit MPU6050@^2.2.6 @@ -23,5 +21,7 @@ lib_deps = adafruit/Adafruit BusIO@^1.15.0 adafruit/Adafruit Unified Sensor@^1.1.14 h2zero/NimBLE-Arduino@^1.4.1 + moddingear/ESP32 ESC@^1.0.0 + freenove/Freenove WS2812 Lib for ESP32@^1.0.6 lib_extra_dirs = /Users/mingweiyeoh/Documents/GitHub/Alipay/IR Beacon/lib diff --git a/Alipay Robot Code/src/main.cpp b/Robot Code/src/main.cpp similarity index 97% rename from Alipay Robot Code/src/main.cpp rename to Robot Code/src/main.cpp index e7d8cee..0ff0e78 100644 --- a/Alipay Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include const int packSize = 6; char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; @@ -41,7 +42,7 @@ void setup() { init_led(); for (int i = 0; i < 10; i++) { - setLeds(CRGB::Orange); + setLeds(ORANGE); delay(50); } USBSerial.begin(115200); @@ -51,7 +52,7 @@ void setup() USBSerial.println("init motors"); laptop.init_ble("Alipay"); USBSerial.println("init laptop"); - setLeds(CRGB::Black); + setLeds(BLACK); } void loop() @@ -219,7 +220,7 @@ void loop() driveMotors.l_motor_write(-lmotorpwr); driveMotors.r_motor_write(rmotorpwr); - toggleLeds(CRGB::White, CRGB::Black, 500); + toggleLeds(WHITE, BLACK, 500); } else if (laptop_packetBuffer[0] == '3'){ // Tank driving mode + PID! int lmotorpwr; @@ -276,7 +277,7 @@ void loop() driveMotors.r_motor_write(rmotorpwr + pidOutput); } - toggleLeds(CRGB::Green, CRGB::Blue, 500); + toggleLeds(GREEN, BLUE, 500); EVERY_N_MILLIS(100) { switch (laptop_packetBuffer[2]) { @@ -308,7 +309,7 @@ void loop() } else { // Currently disabled - toggleLeds(CRGB::Red, CRGB::Green, 500); + toggleLeds(RED, GREEN, 500); driveMotors.set_both_motors(0); EVERY_N_SECONDS(10) { laptop.send(get3sVoltage()); @@ -316,7 +317,11 @@ void loop() } } else { // Currently DISCONNECTED driveMotors.set_both_motors(0); - toggleLeds(CRGB::Red, CRGB::Black, 500); + toggleLeds(RED, BLACK, 500); + // setLeds(RED); + // delay(500); + // setLeds(BLACK); + // delay(500); } diff --git a/Alipay Robot Code/test/README b/Robot Code/test/README similarity index 100% rename from Alipay Robot Code/test/README rename to Robot Code/test/README diff --git a/controller/drawing.svg b/controller/drawing.svg new file mode 100644 index 0000000..356732e --- /dev/null +++ b/controller/drawing.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + diff --git a/controller/drawing.svg.2024_03_26_20_10_56.0.svg b/controller/drawing.svg.2024_03_26_20_10_56.0.svg new file mode 100644 index 0000000..fda99fa --- /dev/null +++ b/controller/drawing.svg.2024_03_26_20_10_56.0.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + diff --git a/controller/rpmgauge.jpg b/controller/rpmgauge.jpg new file mode 100644 index 0000000000000000000000000000000000000000..43bb295f1b60992b45c4a58c38c98fb3ba020d19 GIT binary patch literal 140830 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 literal 0 HcmV?d00001 diff --git a/controller/tkinter_testing.py b/controller/tkinter_testing.py new file mode 100644 index 0000000..e3f44fe --- /dev/null +++ b/controller/tkinter_testing.py @@ -0,0 +1,48 @@ +import tkinter as tk +import math +from PIL import Image, ImageTk + +# Initialize Tkinter and create main window +root = tk.Tk() +root.title("RPM Gauge") + +# Constants for gauge drawing +canvas_width, canvas_height = 800, 800 +center_x, center_y = canvas_width // 2, canvas_height // 2 +needle_length = 300 + +# Create canvas widget +canvas = tk.Canvas(root, width=canvas_width, height=canvas_height, bg='black') +canvas.pack() + +# Open the image with Pillow +image = Image.open("rpmgauge.jpg") +image = image.resize((canvas_width, canvas_height), Image.LANCZOS) + +# Convert the Image object into a TkPhoto object +photo = ImageTk.PhotoImage(image) + +canvas.create_image(0, 0, anchor=tk.NW, image=photo) + +# Initial needle (at 0 RPM) +needle = canvas.create_line(center_x, center_y, center_x, center_y - needle_length, fill='red', width=4) + +# Function to update the needle based on RPM +def update_needle(rpm): + max_rpm = 7000 # Maximum RPM for scale + angle_degrees = 90 + (rpm / max_rpm) * 180 # Convert RPM to angle (simplified) + angle_radians = math.radians(angle_degrees) + + # Calculate new needle end position + end_x = center_x + needle_length * math.cos(angle_radians) + end_y = center_y - needle_length * math.sin(angle_radians) + + # Update the needle + canvas.coords(needle, center_x, center_y, end_x, end_y) + +# Slider to simulate changing RPM +rpm_slider = tk.Scale(root, from_=0, to=7000, orient='horizontal', command=lambda value: update_needle(int(value))) +rpm_slider.pack() + +# Start the Tkinter event loop +root.mainloop() From a0b27799958e6aeb90209c059c8d2f593fd4dd41 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Mon, 1 Apr 2024 23:07:58 -0700 Subject: [PATCH 29/48] DSHOT working with main code --- Motor_Test_DSDHOT/src/main.cpp | 7 +-- Robot Code/lib/Drive_Motors/Drive_Motors.cpp | 54 ++++++++++++-------- Robot Code/lib/Drive_Motors/Drive_Motors.h | 16 +++--- Robot Code/lib/LEDHandler/LEDHandler.h | 4 +- Robot Code/platformio.ini | 2 +- Robot Code/src/main.cpp | 17 ++---- controller/controller.py | 1 - 7 files changed, 51 insertions(+), 50 deletions(-) diff --git a/Motor_Test_DSDHOT/src/main.cpp b/Motor_Test_DSDHOT/src/main.cpp index 807b78a..853ba5e 100644 --- a/Motor_Test_DSDHOT/src/main.cpp +++ b/Motor_Test_DSDHOT/src/main.cpp @@ -23,7 +23,7 @@ enum Colors { BLUE, PURPLE, RED_PURPLE, -} Colors; +}; void setLed(int colorWheel) { strip.setLedColorData(0, strip.hsv2rgb(colorWheel*30, 100, 100)); @@ -56,12 +56,13 @@ void setup() } int mydelay = 10; - for (int i = 0; i < 3000/mydelay; i++) + for (int i = 0; i < 100/mydelay; i++) { rmot.sendThrottle3D(0); - lmot.sendThrottle(0); + lmot.sendThrottle3D(0); delay(mydelay); } + // delay(3000); USBSerial.println("Armed!!!"); } diff --git a/Robot Code/lib/Drive_Motors/Drive_Motors.cpp b/Robot Code/lib/Drive_Motors/Drive_Motors.cpp index a787402..9d4291d 100644 --- a/Robot Code/lib/Drive_Motors/Drive_Motors.cpp +++ b/Robot Code/lib/Drive_Motors/Drive_Motors.cpp @@ -1,43 +1,55 @@ #include "Drive_Motors.h" #include - - void Drive_Motors::init_motors() { - ledcSetup(lMotChannel, motFreq, resolution); - ledcSetup(rMotChannel, motFreq, resolution); - ledcAttachPin(lMotPin, lMotChannel); - ledcAttachPin(rMotPin, rMotChannel); + rmot.install(GPIO_NUM_7, RMT_CHANNEL_1); + rmot.init(); + rmot.setReversed(false); + rmot.set3DMode(true); + rmot.throttleArm(); // <--- Super important!!!; - l_motor_write(neutralValue); - r_motor_write(neutralValue); + lmot.install(GPIO_NUM_8, RMT_CHANNEL_2); + lmot.init(); + lmot.setReversed(false); + lmot.set3DMode(true); + lmot.throttleArm(); // <--- Super important!!!; +} + +void Drive_Motors::arm_motors() { + for (int i = 0; i < 10; i++) + { + rmot.sendThrottle3D(0); + lmot.sendThrottle3D(0); + delay(10); + } } void Drive_Motors::l_motor_write(int value) { l_motor_value = value; if (value != 0) if (value > 0) - value += 2; + value += kickstart_value; else - value -= 2; - if (!flip_motors) - ledcWrite(lMotChannel, neutralValue + value); - else - ledcWrite(rMotChannel, neutralValue + value); + value -= kickstart_value; + EVERY_N_MILLIS(1) // Important or else DSHOT data is sent too fast. + if (!flip_motors) + lmot.sendThrottle3D(value); + else + rmot.sendThrottle3D(value); } void Drive_Motors::r_motor_write(int value) { r_motor_value = value; if (value != 0) if (value > 0) - value += 2; + value += kickstart_value; else - value -= 2; - - if (!flip_motors) - ledcWrite(rMotChannel, neutralValue + value); - else - ledcWrite(lMotChannel, neutralValue + value); + value -= kickstart_value; + EVERY_N_MILLIS(1) // Important or else DSHOT data is sent too fast. + if (!flip_motors) + rmot.sendThrottle3D(value); + else + lmot.sendThrottle3D(value); } void Drive_Motors::set_both_motors(int value) { diff --git a/Robot Code/lib/Drive_Motors/Drive_Motors.h b/Robot Code/lib/Drive_Motors/Drive_Motors.h index 95e0a8a..0f8c097 100644 --- a/Robot Code/lib/Drive_Motors/Drive_Motors.h +++ b/Robot Code/lib/Drive_Motors/Drive_Motors.h @@ -1,23 +1,21 @@ -#define lMotPin 8 -#define rMotPin 7 -#define motFreq 500 -#define resolution 8 // # of bits -#define lMotChannel 0 -#define rMotChannel 1 - -#define neutralValue 189 - +#include "DShotESC.h" +#include "FastLED.h" class Drive_Motors { public: Drive_Motors() {} void init_motors(); + void arm_motors(); void l_motor_write(int value); void r_motor_write(int value); void set_both_motors(int value); bool isNeutral(); bool flip_motors = 0; private: + DShotESC rmot; + DShotESC lmot; + + int kickstart_value = 8; int l_motor_value = 0; int r_motor_value = 0; }; diff --git a/Robot Code/lib/LEDHandler/LEDHandler.h b/Robot Code/lib/LEDHandler/LEDHandler.h index 60415c8..9c50fca 100644 --- a/Robot Code/lib/LEDHandler/LEDHandler.h +++ b/Robot Code/lib/LEDHandler/LEDHandler.h @@ -18,8 +18,8 @@ enum Colors { BLUE, PURPLE, RED_PURPLE, - BLACK, - WHITE + BLACK, + WHITE }; void init_led(); diff --git a/Robot Code/platformio.ini b/Robot Code/platformio.ini index d193668..11b14b5 100644 --- a/Robot Code/platformio.ini +++ b/Robot Code/platformio.ini @@ -21,7 +21,7 @@ lib_deps = adafruit/Adafruit BusIO@^1.15.0 adafruit/Adafruit Unified Sensor@^1.1.14 h2zero/NimBLE-Arduino@^1.4.1 - moddingear/ESP32 ESC@^1.0.0 freenove/Freenove WS2812 Lib for ESP32@^1.0.6 + moddingear/ESP32 ESC@^1.0.0 lib_extra_dirs = /Users/mingweiyeoh/Documents/GitHub/Alipay/IR Beacon/lib diff --git a/Robot Code/src/main.cpp b/Robot Code/src/main.cpp index 0ff0e78..ebde499 100644 --- a/Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -40,18 +40,13 @@ struct pid_tank_drive_parameters { void setup() { - init_led(); - for (int i = 0; i < 10; i++) { - setLeds(ORANGE); - delay(50); - } USBSerial.begin(115200); // init_mpu6050(); - USBSerial.println("init mpu6050"); - driveMotors.init_motors(); - USBSerial.println("init motors"); + driveMotors.init_motors(); // <- This needs to be init first or else something with RMT doesnt work.... + init_led(); + setLeds(ORANGE); + driveMotors.arm_motors(); laptop.init_ble("Alipay"); - USBSerial.println("init laptop"); setLeds(BLACK); } @@ -318,10 +313,6 @@ void loop() } else { // Currently DISCONNECTED driveMotors.set_both_motors(0); toggleLeds(RED, BLACK, 500); - // setLeds(RED); - // delay(500); - // setLeds(BLACK); - // delay(500); } diff --git a/controller/controller.py b/controller/controller.py index 13ec139..ec8e8d5 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -1,5 +1,4 @@ # `sudo pkill bluetoothd` if bluetooth not working on ming's mac m3 pro - import threading from LaptopKeyboard import * from bluetooth import * From 5fe9ddf2936e28fd1ada251ff0176b6c98bda2e8 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Wed, 3 Apr 2024 00:14:36 -0700 Subject: [PATCH 30/48] dshot + improved movement code. Still need to add in accelerometer and not have to fully rely on seeing the IR beacon every rotation --- ADXL375 Test/.gitignore | 5 + ADXL375 Test/.vscode/extensions.json | 10 ++ ADXL375 Test/include/README | 39 +++++ ADXL375 Test/lib/README | 46 ++++++ ADXL375 Test/platformio.ini | 15 ++ ADXL375 Test/src/main.cpp | 114 +++++++++++++++ ADXL375 Test/test/README | 11 ++ Robot Code/.vscode/settings.json | 3 +- .../lib/Battery_Monitor/Battery_Monitor.h | 2 +- Robot Code/lib/LEDHandler/LEDHandler.cpp | 8 +- .../lib/LaptopTelemetry/LaptopTelemetry.cpp | 74 ---------- .../lib/LaptopTelemetry/LaptopTelemetry.h | 25 ---- Robot Code/lib/Melty/melty.cpp | 23 ++- Robot Code/lib/Melty/melty.h | 54 ++++--- Robot Code/lib/adxl375/adxl375.cpp | 33 +++++ Robot Code/lib/adxl375/adxl375.h | 9 ++ Robot Code/lib/mpu6050/mpu6050.cpp | 43 ------ Robot Code/lib/mpu6050/mpu6050.h | 11 -- Robot Code/platformio.ini | 6 +- Robot Code/src/main.cpp | 135 +++--------------- .../LaptopKeyboard.cpython-311.pyc | Bin 958 -> 956 bytes .../__pycache__/bluetooth.cpython-311.pyc | Bin 4948 -> 4946 bytes controller/controller.py | 28 ++-- 23 files changed, 372 insertions(+), 322 deletions(-) create mode 100644 ADXL375 Test/.gitignore create mode 100644 ADXL375 Test/.vscode/extensions.json create mode 100644 ADXL375 Test/include/README create mode 100644 ADXL375 Test/lib/README create mode 100644 ADXL375 Test/platformio.ini create mode 100644 ADXL375 Test/src/main.cpp create mode 100644 ADXL375 Test/test/README delete mode 100644 Robot Code/lib/LaptopTelemetry/LaptopTelemetry.cpp delete mode 100644 Robot Code/lib/LaptopTelemetry/LaptopTelemetry.h create mode 100644 Robot Code/lib/adxl375/adxl375.cpp create mode 100644 Robot Code/lib/adxl375/adxl375.h delete mode 100644 Robot Code/lib/mpu6050/mpu6050.cpp delete mode 100644 Robot Code/lib/mpu6050/mpu6050.h diff --git a/ADXL375 Test/.gitignore b/ADXL375 Test/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/ADXL375 Test/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/ADXL375 Test/.vscode/extensions.json b/ADXL375 Test/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/ADXL375 Test/.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/ADXL375 Test/include/README b/ADXL375 Test/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/ADXL375 Test/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/ADXL375 Test/lib/README b/ADXL375 Test/lib/README new file mode 100644 index 0000000..2593a33 --- /dev/null +++ b/ADXL375 Test/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 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 +#include + +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/ADXL375 Test/platformio.ini b/ADXL375 Test/platformio.ini new file mode 100644 index 0000000..c784fac --- /dev/null +++ b/ADXL375 Test/platformio.ini @@ -0,0 +1,15 @@ +; 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-s3-devkitc-1] +platform = espressif32 +board = esp32-s3-devkitc-1 +framework = arduino +lib_deps = adafruit/Adafruit ADXL375@^1.1.2 diff --git a/ADXL375 Test/src/main.cpp b/ADXL375 Test/src/main.cpp new file mode 100644 index 0000000..ec485c4 --- /dev/null +++ b/ADXL375 Test/src/main.cpp @@ -0,0 +1,114 @@ +#include +#include +#include + +#define ADXL375_SCK 13 +#define ADXL375_MISO 12 +#define ADXL375_MOSI 11 +#define ADXL375_CS 10 + +/* Assign a unique ID to this sensor at the same time */ +/* Uncomment following line for default Wire bus */ +Adafruit_ADXL375 accel = Adafruit_ADXL375(12345); + +/* Uncomment for software SPI */ +//Adafruit_ADXL375 accel = Adafruit_ADXL375(ADXL375_SCK, ADXL375_MISO, ADXL375_MOSI, ADXL375_CS, 12345); + +/* Uncomment for hardware SPI */ +//Adafruit_ADXL375 accel = Adafruit_ADXL375(ADXL375_CS, &SPI, 12345); + +void displayDataRate(void) +{ + USBSerial.print ("Data Rate: "); + + switch(accel.getDataRate()) + { + case ADXL343_DATARATE_3200_HZ: + USBSerial.print ("3200 "); + break; + case ADXL343_DATARATE_1600_HZ: + USBSerial.print ("1600 "); + break; + case ADXL343_DATARATE_800_HZ: + USBSerial.print ("800 "); + break; + case ADXL343_DATARATE_400_HZ: + USBSerial.print ("400 "); + break; + case ADXL343_DATARATE_200_HZ: + USBSerial.print ("200 "); + break; + case ADXL343_DATARATE_100_HZ: + USBSerial.print ("100 "); + break; + case ADXL343_DATARATE_50_HZ: + USBSerial.print ("50 "); + break; + case ADXL343_DATARATE_25_HZ: + USBSerial.print ("25 "); + break; + case ADXL343_DATARATE_12_5_HZ: + USBSerial.print ("12.5 "); + break; + case ADXL343_DATARATE_6_25HZ: + USBSerial.print ("6.25 "); + break; + case ADXL343_DATARATE_3_13_HZ: + USBSerial.print ("3.13 "); + break; + case ADXL343_DATARATE_1_56_HZ: + USBSerial.print ("1.56 "); + break; + case ADXL343_DATARATE_0_78_HZ: + USBSerial.print ("0.78 "); + break; + case ADXL343_DATARATE_0_39_HZ: + USBSerial.print ("0.39 "); + break; + case ADXL343_DATARATE_0_20_HZ: + USBSerial.print ("0.20 "); + break; + case ADXL343_DATARATE_0_10_HZ: + USBSerial.print ("0.10 "); + break; + default: + USBSerial.print ("???? "); + break; + } + USBSerial.println(" Hz"); +} + +void setup(void) +{ + USBSerial.begin(115200); + while (!USBSerial); + USBSerial.println("ADXL375 Accelerometer Test"); USBSerial.println(""); + Wire.begin(5,6); + /* Initialise the sensor */ + if(!accel.begin()) + { + /* There was a problem detecting the ADXL375 ... check your connections */ + USBSerial.println("Ooops, no ADXL375 detected ... Check your wiring!"); + while(1); + } + + // Range is fixed at +-200g + + /* Display some basic information on this sensor */ + accel.printSensorDetails(); + displayDataRate(); + USBSerial.println(""); +} + +void loop(void) +{ + /* Get a new sensor event */ + sensors_event_t event; + accel.getEvent(&event); + + /* Display the results (acceleration is measured in m/s^2) */ + USBSerial.print("X: "); USBSerial.print(event.acceleration.x); USBSerial.print(" "); + USBSerial.print("Y: "); USBSerial.print(event.acceleration.y); USBSerial.print(" "); + USBSerial.print("Z: "); USBSerial.print(event.acceleration.z); USBSerial.print(" ");USBSerial.println("m/s^2 "); + delay(100); +} \ No newline at end of file diff --git a/ADXL375 Test/test/README b/ADXL375 Test/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/ADXL375 Test/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 diff --git a/Robot Code/.vscode/settings.json b/Robot Code/.vscode/settings.json index 8c93b85..936cb56 100644 --- a/Robot Code/.vscode/settings.json +++ b/Robot Code/.vscode/settings.json @@ -14,6 +14,7 @@ "istream": "cpp", "functional": "cpp", "tuple": "cpp", - "utility": "cpp" + "utility": "cpp", + "list": "cpp" } } \ No newline at end of file diff --git a/Robot Code/lib/Battery_Monitor/Battery_Monitor.h b/Robot Code/lib/Battery_Monitor/Battery_Monitor.h index 4efaee4..a8ec9ae 100644 --- a/Robot Code/lib/Battery_Monitor/Battery_Monitor.h +++ b/Robot Code/lib/Battery_Monitor/Battery_Monitor.h @@ -1,7 +1,7 @@ #include float get3sVoltage() { - return (float(analogRead(18)) / 4096)*3.1 * 3.95 * 1.0175; + return (float(analogRead(18)) / 4096)*3.1 * 4.1 * 1.01; } float get1sVoltage() { diff --git a/Robot Code/lib/LEDHandler/LEDHandler.cpp b/Robot Code/lib/LEDHandler/LEDHandler.cpp index f2d084f..28a638c 100644 --- a/Robot Code/lib/LEDHandler/LEDHandler.cpp +++ b/Robot Code/lib/LEDHandler/LEDHandler.cpp @@ -7,7 +7,7 @@ unsigned long currentDelayToggle = millis(); Colors lastColor1; Colors lastColor2; -Freenove_ESP32_WS2812 strip = Freenove_ESP32_WS2812(1, LEDPIN, 0, TYPE_GRB); +Freenove_ESP32_WS2812 strip = Freenove_ESP32_WS2812(2, LEDPIN, 0, TYPE_GRB); void init_led() { strip.begin(); @@ -16,11 +16,11 @@ void init_led() { void setLeds(Colors color) { if (color == BLACK) - strip.setLedColorData(0, 0, 0, 0); + strip.setAllLedsColorData(0, 0, 0); else if (color == WHITE) - strip.setLedColorData(0, 255, 255, 255); + strip.setAllLedsColorData(255, 255, 255); else - strip.setLedColorData(0, strip.hsv2rgb(color*30, 100, 100)); + strip.setAllLedsColorData(strip.hsv2rgb(color*30, 100, 100)); // delay(1); strip.show(); } diff --git a/Robot Code/lib/LaptopTelemetry/LaptopTelemetry.cpp b/Robot Code/lib/LaptopTelemetry/LaptopTelemetry.cpp deleted file mode 100644 index c7df133..0000000 --- a/Robot Code/lib/LaptopTelemetry/LaptopTelemetry.cpp +++ /dev/null @@ -1,74 +0,0 @@ -#include "LaptopTelemetry.h" - -WiFiUDP udp; -// IPAddress laptopIpAddress(192, 168, 86, 25); -// IPAddress laptopIpAddress(192, 168, 111, 177); -IPAddress laptopIpAddress(192, 168, 175, 127); - -LaptopTelemetry::LaptopTelemetry(const char* _ssid, const char* _pswrd, char* _packetBuffer) { - ssid = _ssid; - pswrd = _pswrd; - packetBuffer = _packetBuffer; -} - -void LaptopTelemetry::init() { - WiFi.setMinSecurity(WIFI_AUTH_WEP); - WiFi.begin(ssid, pswrd); - WiFi.setTxPower(WIFI_POWER_8_5dBm); - wl_status_t wifistat = WiFi.status(); - while (wifistat != WL_CONNECTED) { - wifistat = WiFi.status(); - switch (wifistat) { - case WL_NO_SSID_AVAIL: - USBSerial.println("[WiFi] SSID not found"); - break; - case WL_CONNECT_FAILED: - USBSerial.print("[WiFi] Failed - WiFi not connected! Reason: "); - break; - case WL_CONNECTION_LOST: - USBSerial.println("[WiFi] Connection was lost"); - break; - case WL_CONNECTED: - USBSerial.println("[WiFi] WiFi is connected!"); - USBSerial.print("[WiFi] IP address: "); - USBSerial.println(WiFi.localIP()); - udp.begin(12345); // Port that ESP32 will receive commands on - break; - default: - USBSerial.print("."); - break; - } - delay(100); - } -} - -void LaptopTelemetry::send(const char* message) { - udp.beginPacket(laptopIpAddress, PORT); // Send to port - udp.print(message); - udp.endPacket(); -} - -void LaptopTelemetry::send(float value) { - char conversionbuffer[5]; - sprintf(conversionbuffer, "%.2f", value); - send(conversionbuffer); -} - -bool LaptopTelemetry::isDisconnected() { - return (millis() - lastTransmission > 500); -} - -void LaptopTelemetry::receive() { - size_t packetSize = udp.parsePacket(); - if (packetSize) { // receive incoming UDP packets - // USBSerial.printf("Received %d bytes from %s, port %d\n", packetSize, udp.remoteIP().toString().c_str(), udp.remotePort()); - int len = udp.read(packetBuffer, packetSize); - if (len > 0) - packetBuffer[len] = 0; - lastTransmission = millis(); - // USBSerial.printf("[CONTROLLER]: %s\n", packetBuffer); - } -} - - - diff --git a/Robot Code/lib/LaptopTelemetry/LaptopTelemetry.h b/Robot Code/lib/LaptopTelemetry/LaptopTelemetry.h deleted file mode 100644 index 522da5c..0000000 --- a/Robot Code/lib/LaptopTelemetry/LaptopTelemetry.h +++ /dev/null @@ -1,25 +0,0 @@ -#ifdef IR_BEACON - #define PORT 12347 -#else - #define PORT 12346 -#endif - -#include -#include - - -class LaptopTelemetry { - public: - LaptopTelemetry(const char* _ssid, const char* _pswrd, char* _packetBuffer); - void receive(); - void init(); - void send(const char* message); - void send(float value); - bool isDisconnected(); - private: - const char* ssid; - const char* pswrd; - char* packetBuffer; - unsigned long lastTransmission = millis(); - -}; diff --git a/Robot Code/lib/Melty/melty.cpp b/Robot Code/lib/Melty/melty.cpp index 0e8ec1b..5f63d4b 100644 --- a/Robot Code/lib/Melty/melty.cpp +++ b/Robot Code/lib/Melty/melty.cpp @@ -2,9 +2,8 @@ #include melty::melty() { - period_micros_calc = ringBuffer(1); - time_seen_beacon_calc = ringBuffer(0.5); - photo_resistor_vals = ringBuffer(1); + period_micros_calc = ringBuffer(period_micros_calc_array, 5, 1); + time_seen_beacon_calc = ringBuffer(time_seen_beacon_calc_array, 10, 0.75); } bool melty::update() { @@ -16,32 +15,28 @@ bool melty::update() { if (curSeenIRLed != lastSeenIRLed) if (curSeenIRLed) { // Activates on the rising edge of seeing the IR LED - setLeds(GREEN); + setLeds(BLUE); currentPulse = micros(); - period_micros = currentPulse - lastPulse; // How long it takes to complete one revolution - period_micros_calc.update(period_micros); - lastPulse = currentPulse; - } else { // Activates on the falling edge of seeing the IR LED - setLeds(RED); + setLeds(YELLOW); time_seen_beacon = micros() - currentPulse; time_seen_beacon_calc.update(time_seen_beacon); - if (time_seen_beacon_calc.isLegit(time_seen_beacon)) { // && period_micros_calc.isLegit(period_micros) + unsigned long curMicros = micros(); + period_micros = curMicros - lastPulse; // How long it takes to complete one revolution + period_micros_calc.update(period_micros); + lastPulse = curMicros; computeTimings(); } } - - if (curSeenIRLed) - photo_resistor_vals.update(analogRead(BOTTOM_IR_PIN)); lastSeenIRLed = curSeenIRLed; return curSeenIRLed; } void melty::computeTimings() { - unsigned long max_period = period_micros_calc.getMaxVal(); + unsigned long max_period = period_micros_calc.getMinVal(); RPM = (us_per_min)/(max_period); unsigned long center_of_beacon = currentPulse + time_seen_beacon/2; // This should ideally be centered on the beacon unsigned long centerOfDrivePulse = center_of_beacon + (float(deg)/360)*max_period; // Direction that we should be driving towards diff --git a/Robot Code/lib/Melty/melty.h b/Robot Code/lib/Melty/melty.h index 5a7ce89..a3cdf97 100644 --- a/Robot Code/lib/Melty/melty.h +++ b/Robot Code/lib/Melty/melty.h @@ -1,7 +1,5 @@ #include -#include - -#define RINGBUFSIZE 5 +#include class ringBuffer{ public: @@ -10,7 +8,9 @@ class ringBuffer{ * * @param _criteria How close should the new value match the max value from the ring buffer to be considered legit? */ - ringBuffer(float _criteria) { + ringBuffer(unsigned long *_ringBuf, int _arraysize, float _criteria) { + ringBuf = _ringBuf; + arraySize = _arraysize; criteria = _criteria; } @@ -23,7 +23,7 @@ class ringBuffer{ */ void update(unsigned long val) { ringBuf[curIndex++] = val; - if (curIndex > RINGBUFSIZE-1) + if (curIndex > arraySize-1) curIndex = 0; } @@ -47,32 +47,46 @@ class ringBuffer{ */ unsigned long getMaxVal() { unsigned long maxVal = 0; - for (int i = 0; i < RINGBUFSIZE; i++) + for (int i = 0; i < arraySize; i++) if (maxVal < ringBuf[i]) maxVal = ringBuf[i]; return maxVal; } - + unsigned long getMinVal() { + unsigned long minVal = ringBuf[0]; + for (int i = 1; i < arraySize; i++) + if (minVal > ringBuf[i]) + minVal = ringBuf[i]; + if (minVal == 0) + minVal = 1; + return minVal; + } - private: - unsigned long ringBuf[RINGBUFSIZE]; - int curIndex = 0; - float criteria = 0; String returnArray() { // For debug purposes String msg = ""; - for (int i = 0; i < RINGBUFSIZE; i++) { + for (int i = 0; i < arraySize; i++) { msg = msg + String(ringBuf[i]) + " "; } return msg; } + + + + private: + unsigned long *ringBuf; + int arraySize; + + int curIndex = 0; + float criteria = 0; + }; -#define TOP_IR_PIN 10 -#define BOTTOM_IR_PIN 9 -#define IRLedDataSize 25 // Size of our Ring Buffer that will hold the IR Led data +#define TOP_IR_PIN 9 +#define BOTTOM_IR_PIN 10 +#define IRLedDataSize 40 // Size of our Ring Buffer that will hold the IR Led data class melty { public: @@ -86,7 +100,11 @@ class melty { float percentageOfRotation = 0; bool useTopIr = 1; - ringBuffer photo_resistor_vals = ringBuffer(1); + unsigned long period_micros_calc_array[20] = {0}; + ringBuffer period_micros_calc; + + unsigned long time_seen_beacon_calc_array[10] = {0}; + ringBuffer time_seen_beacon_calc; private: bool lastSeenIRLed = 0; @@ -103,9 +121,9 @@ class melty { bool timingToggle = 0; - ringBuffer period_micros_calc; - ringBuffer time_seen_beacon_calc; + const int us_per_min = 60000000; }; + \ No newline at end of file diff --git a/Robot Code/lib/adxl375/adxl375.cpp b/Robot Code/lib/adxl375/adxl375.cpp new file mode 100644 index 0000000..555f72b --- /dev/null +++ b/Robot Code/lib/adxl375/adxl375.cpp @@ -0,0 +1,33 @@ +#include "adxl375.h" + +Adafruit_ADXL375 accel = Adafruit_ADXL375(12345); + +void init_adxl375() { + Wire.begin(5,6); + if (!accel.begin()) { + while (1) { + delay(100); + USBSerial.println("Failed to find ADXL375 chip"); + } + } +} + +float getAccelY() { + sensors_event_t event; + accel.getEvent(&event); + return (abs(event.acceleration.y)); +} + +float getAccelZ() { + sensors_event_t event; + accel.getEvent(&event); + return (event.acceleration.z); +} + +float getGyroZ() { + static float heading = 0; + sensors_event_t event; + accel.getEvent(&event); + heading += event.orientation.z; + return (heading); +} \ No newline at end of file diff --git a/Robot Code/lib/adxl375/adxl375.h b/Robot Code/lib/adxl375/adxl375.h new file mode 100644 index 0000000..a0ddf8d --- /dev/null +++ b/Robot Code/lib/adxl375/adxl375.h @@ -0,0 +1,9 @@ +#include + +void init_adxl375(); + +float getAccelY(); + +float getAccelZ(); + +float getGyroZ(); \ No newline at end of file diff --git a/Robot Code/lib/mpu6050/mpu6050.cpp b/Robot Code/lib/mpu6050/mpu6050.cpp deleted file mode 100644 index d6ba0e2..0000000 --- a/Robot Code/lib/mpu6050/mpu6050.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include "mpu6050.h" - -Adafruit_MPU6050 mpu; - -void init_mpu6050() { - Wire.begin(5,6); - if (!mpu.begin()) { - USBSerial.println("Failed to find MPU6050 chip"); - while (1) { - delay(10); - } - } - USBSerial.println("MPU6050 Found!"); - - mpu.setHighPassFilter(MPU6050_HIGHPASS_0_63_HZ); - mpu.setAccelerometerRange(MPU6050_RANGE_16_G); -} - -float getAccelY() { - sensors_event_t a, g, temp; - mpu.getEvent(&a, &g, &temp); - return (abs(a.acceleration.y)); -} - -float getAccelZ() { - sensors_event_t a, g, temp; - mpu.getEvent(&a, &g, &temp); - return (a.acceleration.z); -} - -float getTemp() { - sensors_event_t a, g, temp; - mpu.getEvent(&a, &g, &temp); - return (temp.temperature); -} - -float getGyroZ() { - static float heading = 0; - sensors_event_t a, g, temp; - mpu.getEvent(&a, &g, &temp); - heading += g.orientation.z; - return (heading); -} \ No newline at end of file diff --git a/Robot Code/lib/mpu6050/mpu6050.h b/Robot Code/lib/mpu6050/mpu6050.h deleted file mode 100644 index 523aff7..0000000 --- a/Robot Code/lib/mpu6050/mpu6050.h +++ /dev/null @@ -1,11 +0,0 @@ -#include - -void init_mpu6050(); - -float getAccelY(); - -float getAccelZ(); - -float getTemp(); - -float getGyroZ(); \ No newline at end of file diff --git a/Robot Code/platformio.ini b/Robot Code/platformio.ini index 11b14b5..ddf1d4d 100644 --- a/Robot Code/platformio.ini +++ b/Robot Code/platformio.ini @@ -16,12 +16,10 @@ upload_speed = 921600 monitor_speed = 115200 lib_deps = fastled/FastLED@^3.6.0 - adafruit/Adafruit MPU6050@^2.2.6 Wire - adafruit/Adafruit BusIO@^1.15.0 - adafruit/Adafruit Unified Sensor@^1.1.14 h2zero/NimBLE-Arduino@^1.4.1 freenove/Freenove WS2812 Lib for ESP32@^1.0.6 moddingear/ESP32 ESC@^1.0.0 + adafruit/Adafruit ADXL375@^1.1.2 lib_extra_dirs = - /Users/mingweiyeoh/Documents/GitHub/Alipay/IR Beacon/lib + /Users/mingweiyeoh/Documents/GitHub/Oreo/IR Beacon/lib diff --git a/Robot Code/src/main.cpp b/Robot Code/src/main.cpp index ebde499..92cbc2d 100644 --- a/Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -1,6 +1,6 @@ #include #include -#include +#include #include #include #include @@ -17,36 +17,30 @@ int slowDownSpeed = 8; BLE_Uart laptop = BLE_Uart(laptop_packetBuffer, packSize); Drive_Motors driveMotors = Drive_Motors(); -melty alipay = melty(); +melty oreo = melty(); struct melty_parameters { - int rot = 6; - int tra = 6; + int rot = 80; + int tra = 60; float per = 0.5; int invert = 1; int boost = 5; } melty_parameters; struct tank_drive_parameters { - int drive = 2; - int turn = 2; - int boost = 2; + int drive = 40; + int turn = 20; + int boost = 40; } tank_drive_parameters; -struct pid_tank_drive_parameters { - int drive = 2; - int headingTurn = 2.3; - int boost = 2; -} pid_tank_drive_parameters; - void setup() { USBSerial.begin(115200); - // init_mpu6050(); driveMotors.init_motors(); // <- This needs to be init first or else something with RMT doesnt work.... init_led(); setLeds(ORANGE); + init_adxl375(); driveMotors.arm_motors(); - laptop.init_ble("Alipay"); + laptop.init_ble("oreo"); setLeds(BLACK); } @@ -55,19 +49,19 @@ void loop() if (laptop.isConnected()) { if (driveMotors.isNeutral()) { // if (getAccelZ() > 7) { - alipay.useTopIr = 1; + oreo.useTopIr = 1; driveMotors.flip_motors = 0; melty_parameters.invert = 1; // } // if (getAccelZ() < -7) { - // alipay.useTopIr = 0; + // oreo.useTopIr = 0; // driveMotors.flip_motors = 1; // melty_parameters.invert = -1; // } } if (laptop_packetBuffer[0] == '1') { // Currently enabled and meltybraining!!! - if (alipay.update()) { // If seen the LED + if (oreo.update()) { // If seen the LED EVERY_N_MILLIS(250) { laptop.send("seen"); } @@ -77,7 +71,7 @@ void loop() int boostVal = 0; if (laptop_packetBuffer[3] == '1') boostVal = melty_parameters.boost; - if (alipay.translate()) { + if (oreo.translate()) { driveMotors.l_motor_write(melty_parameters.invert * (melty_parameters.rot - (melty_parameters.tra + boostVal))); driveMotors.r_motor_write(melty_parameters.invert * (melty_parameters.rot + (melty_parameters.tra + boostVal))); } else { @@ -90,33 +84,34 @@ void loop() drivecmd = 8 - drivecmd; else drivecmd = drivecmd - 1; - alipay.deg = headings[drivecmd]; + oreo.deg = headings[drivecmd]; } if (laptop_packetBuffer[1] != '0') { // Check drive cmd for setting the "neutral" state - alipay.percentageOfRotation = melty_parameters.per; + oreo.percentageOfRotation = melty_parameters.per; } else { - alipay.percentageOfRotation = 0; + oreo.percentageOfRotation = 0; } EVERY_N_SECONDS(1) { // DEBUGGIN!!!! - String msg = "PR IR_LED: " + String(alipay.photo_resistor_vals.getMaxVal()); + String msg = "Beacon RPM: " + String(oreo.RPM) + " time seen beacon: " + oreo.time_seen_beacon_calc.returnArray();//" Accel: " + String(getAccelY()); laptop.send(msg); } EVERY_N_MILLIS(100) { + int tuningValue = 5; switch (laptop_packetBuffer[2]) { case '1': - melty_parameters.rot++; + melty_parameters.rot+=tuningValue; break; case '2': - melty_parameters.rot--; + melty_parameters.rot-=tuningValue; break; case '3': - melty_parameters.tra++; + melty_parameters.tra+=tuningValue; break; case '4': - melty_parameters.tra--; + melty_parameters.tra-=tuningValue; break; case '5': melty_parameters.per = melty_parameters.per + .03; @@ -217,92 +212,6 @@ void loop() driveMotors.r_motor_write(rmotorpwr); toggleLeds(WHITE, BLACK, 500); - } else if (laptop_packetBuffer[0] == '3'){ // Tank driving mode + PID! - int lmotorpwr; - int rmotorpwr; - static float desiredHeading; - int pidOutput = 0; - - int boostVal = 0; - if (laptop_packetBuffer[3] == '1') - boostVal = pid_tank_drive_parameters.boost; - - switch (laptop_packetBuffer[1]) { // Check the drive cmd - case '0': - lmotorpwr = 0; - rmotorpwr = 0; - // desiredHeading = getGyroZ(); - break; - case '8': - lmotorpwr = pid_tank_drive_parameters.drive +boostVal; - rmotorpwr = pid_tank_drive_parameters.drive +boostVal; - desiredHeading+=pid_tank_drive_parameters.headingTurn; - break; - case '1': - lmotorpwr = pid_tank_drive_parameters.drive +boostVal; - rmotorpwr = pid_tank_drive_parameters.drive +boostVal; - break; - case '2': - lmotorpwr = pid_tank_drive_parameters.drive +boostVal; - rmotorpwr = pid_tank_drive_parameters.drive +boostVal; - desiredHeading-=pid_tank_drive_parameters.headingTurn; - break; - - case '4': - lmotorpwr = -pid_tank_drive_parameters.drive -boostVal; - rmotorpwr = -pid_tank_drive_parameters.drive -boostVal; - desiredHeading+=pid_tank_drive_parameters.headingTurn; - break; - case '5': - lmotorpwr = -pid_tank_drive_parameters.drive -boostVal; - rmotorpwr = -pid_tank_drive_parameters.drive -boostVal; - break; - case '6': - lmotorpwr = -pid_tank_drive_parameters.drive -boostVal; - rmotorpwr = -pid_tank_drive_parameters.drive -boostVal; - desiredHeading-=pid_tank_drive_parameters.headingTurn; - break; - } - - // if (laptop_packetBuffer[1] != '0') // We want to drive, therefore start calculating pidout - // pidOutput = calcTurnPower(desiredHeading - getGyroZ()); - - EVERY_N_MILLIS(10) { - driveMotors.l_motor_write(- lmotorpwr + pidOutput); - driveMotors.r_motor_write(rmotorpwr + pidOutput); - } - - toggleLeds(GREEN, BLUE, 500); - - EVERY_N_MILLIS(100) { - switch (laptop_packetBuffer[2]) { - case '1': - PID.kP+=.001; - break; - case '2': - PID.kP-=.001; - break; - case '3': - PID.kI+=.01; - break; - case '4': - PID.kI-=.01; - break; - case '5': - PID.kD+=.001; - break; - case '6': - PID.kD-=.001; - break; - } - - if (laptop_packetBuffer[2] != '0') { - String msg = "kP : " + String(PID.kP, 3) + " kI : " + String(PID.kI ,3) + " kD : " + String(PID.kD, 3); - laptop.send(msg); - } - } - - } else { // Currently disabled toggleLeds(RED, GREEN, 500); driveMotors.set_both_motors(0); diff --git a/controller/__pycache__/LaptopKeyboard.cpython-311.pyc b/controller/__pycache__/LaptopKeyboard.cpython-311.pyc index 84d5614aa4258ddfb19714008a5b858816e353c0..f8c43c14120a77e02370006f21dc366098bf7744 100644 GIT binary patch delta 34 pcmdnTzK5M>IWI340}$L>z@0Xc=Mbao#PcdF{za+z8-E^U0syU83xWUu delta 36 rcmdnPzK@+}IWI340}y;Zc`J1y&ml(liRV?=9CI=Y5-T@;Kgt9E+58P# diff --git a/controller/__pycache__/bluetooth.cpython-311.pyc b/controller/__pycache__/bluetooth.cpython-311.pyc index 33b856979feb3dcde5b907b12806a6b14798dcde..8fb9b34a20d65a301e0d1695fd40df461128a2d1 100644 GIT binary patch delta 37 scmcbjc1ewAIWI340}$L>z@4^{r delta 39 ucmcblc14Y6IWI340}!m~xtqF?r Date: Thu, 4 Apr 2024 15:40:18 -0700 Subject: [PATCH 31/48] back to meltying, flipover code works, about to integrate accelrometer to melty algo --- IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp | 42 -------- IR Beacon/lib/esp_now_txrx/esp_now_txrx.h | 21 ---- IR Beacon/platformio.ini | 5 +- IR Beacon/src/main.cpp | 29 +++--- Robot Code/lib/.DS_Store | Bin 6148 -> 6148 bytes .../lib/Battery_Monitor/Battery_Monitor.h | 4 +- Robot Code/lib/LEDHandler/LEDHandler.cpp | 33 +++--- Robot Code/lib/LEDHandler/LEDHandler.h | 1 + Robot Code/lib/Melty/melty.cpp | 7 +- Robot Code/lib/Melty/melty.h | 96 ++---------------- Robot Code/lib/adxl375/adxl375.cpp | 38 ++++--- Robot Code/lib/adxl375/adxl375.h | 5 +- Robot Code/lib/ringBuffer/ringBuffer.h | 85 ++++++++++++++++ Robot Code/src/main.cpp | 36 ++++--- controller/controller.py | 2 +- 15 files changed, 184 insertions(+), 220 deletions(-) delete mode 100644 IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp delete mode 100644 IR Beacon/lib/esp_now_txrx/esp_now_txrx.h create mode 100644 Robot Code/lib/ringBuffer/ringBuffer.h diff --git a/IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp b/IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp deleted file mode 100644 index 8f72366..0000000 --- a/IR Beacon/lib/esp_now_txrx/esp_now_txrx.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include - -esp_now_peer_info_t peerInfo; - -ESP_NOW_TXRX::ESP_NOW_TXRX(uint8_t* _receiver_address, int _packetSize) { - memcpy(peerInfo.peer_addr, _receiver_address, 6); - receiver_address = _receiver_address; - packetSize = _packetSize; -} - -void ESP_NOW_TXRX::getMyMacAddress() { - USBSerial.println(WiFi.macAddress()); -} - -void ESP_NOW_TXRX::send(struct_message dat) { - esp_now_send(receiver_address, (uint8_t *) &dat, packetSize); -} - -void ESP_NOW_TXRX::init(esp_now_recv_cb_t receiv_cb) { - WiFi.mode(WIFI_STA); - if (esp_now_init() != ESP_OK) { - USBSerial.println("Error initializing ESP-NOW"); - return; - } - - esp_now_register_recv_cb(receiv_cb); - peerInfo.channel = 0; - peerInfo.encrypt = false; - - if (esp_now_add_peer(&peerInfo) != ESP_OK){ - USBSerial.println("Failed to add peer"); - return; - } -} - -bool ESP_NOW_TXRX::isDisconnected() { - return millis() - lastMessage > 500; -} - -void ESP_NOW_TXRX::keepAlive() { - lastMessage = millis(); -} \ No newline at end of file diff --git a/IR Beacon/lib/esp_now_txrx/esp_now_txrx.h b/IR Beacon/lib/esp_now_txrx/esp_now_txrx.h deleted file mode 100644 index 5a30694..0000000 --- a/IR Beacon/lib/esp_now_txrx/esp_now_txrx.h +++ /dev/null @@ -1,21 +0,0 @@ -#include -#include - - -typedef struct struct_message { - char a[6]; -} struct_message; - -class ESP_NOW_TXRX { - public: - ESP_NOW_TXRX(uint8_t *_receiver_address, int _packetSize); - void init(esp_now_recv_cb_t receiv_cb); - void getMyMacAddress(); - void send(struct_message dat); - bool isDisconnected(); - void keepAlive(); - private: - int packetSize; - uint8_t *receiver_address; - unsigned long lastMessage = millis(); -}; \ No newline at end of file diff --git a/IR Beacon/platformio.ini b/IR Beacon/platformio.ini index 5825065..2cda21d 100644 --- a/IR Beacon/platformio.ini +++ b/IR Beacon/platformio.ini @@ -14,11 +14,10 @@ board = esp32-s3-devkitc-1 framework = arduino upload_speed = 921600 monitor_speed = 115200 -; upload_port = /dev/tty.usbmodem1101 -; monitor_port = /dev/tty.usbmodem1101 build_flags = -D IR_BEACON=1 lib_deps = fastled/FastLED@^3.6.0 h2zero/NimBLE-Arduino@^1.4.1 + freenove/Freenove WS2812 Lib for ESP32@^1.0.6 lib_extra_dirs = - /Users/mingweiyeoh/Documents/GitHub/Alipay/Alipay Robot Code/lib + /Users/mingweiyeoh/Documents/GitHub/Oreo/Robot Code/lib diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp index 87eab7d..a7dba4a 100644 --- a/IR Beacon/src/main.cpp +++ b/IR Beacon/src/main.cpp @@ -1,58 +1,57 @@ #include -#include #include #include #include -#include +#include -#define TARGETCMD '2' // Change based on which IR_Beacon working on +#define TARGETCMD '1' // Change based on which IR_Beacon working on const int packSize = 6; char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; const int IRLedPin = 4; const int freq = 38000; -const int ledChannel = 0; +const int ledChannel = 1; const int resolution = 8; -int dutycycle = 100; -const int maxdutycycle = 180; +int dutycycle = 30; +const int maxdutycycle = 100; BLE_Uart laptop = BLE_Uart(laptop_packetBuffer, packSize); void setup(){ init_led(); - setLeds(CRGB::Green); + setLeds(ORANGE); USBSerial.begin(115200); ledcSetup(ledChannel, freq, resolution); ledcAttachPin(IRLedPin, ledChannel); laptop.init_ble("IR Beacon"); - setLeds(CRGB::Black); + setLeds(BLACK); } void loop(){ if (laptop.isConnected()) { switch (laptop_packetBuffer[0]) { case '0': // Disabled - toggleLeds(CRGB::Red, CRGB::Green, 500); + toggleLeds(RED, GREEN, 500); ledcWrite(ledChannel, 0); EVERY_N_SECONDS(10) laptop.send(get1sVoltage()); break; case '1': // Enable one of the IR Beacons if (laptop_packetBuffer[1] == TARGETCMD) { - toggleLeds(CRGB::Blue, CRGB::Purple, 500); + toggleLeds(BLUE, PURPLE, 500); ledcWrite(ledChannel, dutycycle); EVERY_N_MILLIS(100) { switch (laptop_packetBuffer[2]) { case '1': - dutycycle+=3; + dutycycle+=5; if (dutycycle > maxdutycycle) dutycycle = maxdutycycle; break; case '2': - dutycycle-=3; + dutycycle-=5; if (dutycycle < 0) dutycycle = 0; break; @@ -64,12 +63,12 @@ void loop(){ } } else { - setLeds(CRGB::Black); + setLeds(BLACK); ledcWrite(ledChannel, 0); } break; default: // Don't enable current IR Beacon if in another mode - setLeds(CRGB::Black); + setLeds(BLACK); ledcWrite(ledChannel, 0); break; } @@ -77,7 +76,7 @@ void loop(){ } else { - toggleLeds(CRGB::Red, CRGB::Black, 500); + toggleLeds(RED, BLACK, 500); ledcWrite(ledChannel, 0); } } \ No newline at end of file diff --git a/Robot Code/lib/.DS_Store b/Robot Code/lib/.DS_Store index 4929b633881ce76573c0e63fc89ea430d06e113f..d554410ea1e76a3a0518728b4955f411e4cfd04e 100644 GIT binary patch delta 74 zcmZoMXfc=|&e%4wP;8=}q9`K+0|O8XFfb%C(MI{VH43#Ox$vH{+`8f<5dji-uvvUY=Ft%^p_?>w&zlbg)!)8a3!^{&K FSOC1a95Mg^ diff --git a/Robot Code/lib/Battery_Monitor/Battery_Monitor.h b/Robot Code/lib/Battery_Monitor/Battery_Monitor.h index a8ec9ae..9f18f8a 100644 --- a/Robot Code/lib/Battery_Monitor/Battery_Monitor.h +++ b/Robot Code/lib/Battery_Monitor/Battery_Monitor.h @@ -1,9 +1,9 @@ #include float get3sVoltage() { - return (float(analogRead(18)) / 4096)*3.1 * 4.1 * 1.01; + return (float(analogRead(18)) / 4096)*3.1 * 4.1; } float get1sVoltage() { - return (float(analogRead(18)) / 4096)*3.1 * 2.00 * 1.0625; + return (float(analogRead(18)) / 4096)*3.1 * 2.00 + .35; } \ No newline at end of file diff --git a/Robot Code/lib/LEDHandler/LEDHandler.cpp b/Robot Code/lib/LEDHandler/LEDHandler.cpp index 28a638c..9d2ab53 100644 --- a/Robot Code/lib/LEDHandler/LEDHandler.cpp +++ b/Robot Code/lib/LEDHandler/LEDHandler.cpp @@ -15,31 +15,38 @@ void init_led() { } void setLeds(Colors color) { - if (color == BLACK) - strip.setAllLedsColorData(0, 0, 0); - else if (color == WHITE) - strip.setAllLedsColorData(255, 255, 255); - else - strip.setAllLedsColorData(strip.hsv2rgb(color*30, 100, 100)); - // delay(1); - strip.show(); + static unsigned long lastLedShowing = 0; + if (millis() - lastLedShowing > 5) { + if (color == BLACK) + strip.setAllLedsColorData(0, 0, 0); + else if (color == WHITE) + strip.setAllLedsColorData(255, 255, 255); + else + strip.setAllLedsColorData(strip.hsv2rgb(color*30, 100, 100)); + strip.show(); + lastLedShowing = millis(); + } + } void toggleLeds(Colors color1, Colors color2, int delayMS) { - if (color1 != lastColor1 || color2 != lastColor2 || millis() - lastdelayToggle > delayMS + 50) { // For color syncing purposes between different devices + if (color1 != lastColor1 || color2 != lastColor2 || millis() - lastdelayToggle > delayMS + 50) { lastdelayToggle = millis(); ledToggleState = 0; + setLeds(color1); } currentDelayToggle = millis(); if (currentDelayToggle - lastdelayToggle > delayMS) { ledToggleState = !ledToggleState; lastdelayToggle = currentDelayToggle; - if (ledToggleState) - setLeds(color1); - else - setLeds(color2); } + + if (ledToggleState) + setLeds(color1); + else + setLeds(color2); + lastColor1 = color1; lastColor2 = color2; } \ No newline at end of file diff --git a/Robot Code/lib/LEDHandler/LEDHandler.h b/Robot Code/lib/LEDHandler/LEDHandler.h index 9c50fca..2697011 100644 --- a/Robot Code/lib/LEDHandler/LEDHandler.h +++ b/Robot Code/lib/LEDHandler/LEDHandler.h @@ -1,4 +1,5 @@ #include +#include #ifdef IR_BEACON #define LEDPIN 5 diff --git a/Robot Code/lib/Melty/melty.cpp b/Robot Code/lib/Melty/melty.cpp index 5f63d4b..1b1f349 100644 --- a/Robot Code/lib/Melty/melty.cpp +++ b/Robot Code/lib/Melty/melty.cpp @@ -22,7 +22,7 @@ bool melty::update() { setLeds(YELLOW); time_seen_beacon = micros() - currentPulse; time_seen_beacon_calc.update(time_seen_beacon); - if (time_seen_beacon_calc.isLegit(time_seen_beacon)) { // && period_micros_calc.isLegit(period_micros) + if (time_seen_beacon_calc.isLegit(time_seen_beacon)) { unsigned long curMicros = micros(); period_micros = curMicros - lastPulse; // How long it takes to complete one revolution period_micros_calc.update(period_micros); @@ -37,7 +37,10 @@ bool melty::update() { void melty::computeTimings() { unsigned long max_period = period_micros_calc.getMinVal(); - RPM = (us_per_min)/(max_period); + int ledRPM = (us_per_min)/(max_period); + // int accelRPM = getAccelY() * ; // calculate the accelrometer rpm + // Compare acclerometer rpm to the rpm calculated based on light, if its off but a certain margin + RPM = ledRPM; unsigned long center_of_beacon = currentPulse + time_seen_beacon/2; // This should ideally be centered on the beacon unsigned long centerOfDrivePulse = center_of_beacon + (float(deg)/360)*max_period; // Direction that we should be driving towards unsigned long deltaDriveTiming = (percentageOfRotation * max_period)/2; diff --git a/Robot Code/lib/Melty/melty.h b/Robot Code/lib/Melty/melty.h index a3cdf97..78479e8 100644 --- a/Robot Code/lib/Melty/melty.h +++ b/Robot Code/lib/Melty/melty.h @@ -1,88 +1,6 @@ #include #include - -class ringBuffer{ - public: - /** - * @brief Construct a ring buffer object. - * - * @param _criteria How close should the new value match the max value from the ring buffer to be considered legit? - */ - ringBuffer(unsigned long *_ringBuf, int _arraysize, float _criteria) { - ringBuf = _ringBuf; - arraySize = _arraysize; - criteria = _criteria; - } - - ringBuffer() {} - - /** - * @brief Adds a value to the ring buffer. - * - * @param val Value to be added to the ring buffer. - */ - void update(unsigned long val) { - ringBuf[curIndex++] = val; - if (curIndex > arraySize-1) - curIndex = 0; - } - - /** - * @brief Compares the input value to the max value. - * - * @param newVal New value to check against current max value. - * - * @return Whether new value is some percentage of max value. Based on the criteria variable. - */ - bool isLegit(unsigned long newVal) { - unsigned long maxVal = getMaxVal(); - if (newVal > maxVal * criteria) - return true; - else - return false; - } - - /** - * @brief Returns maximum value from our ring buffer. - */ - unsigned long getMaxVal() { - unsigned long maxVal = 0; - for (int i = 0; i < arraySize; i++) - if (maxVal < ringBuf[i]) - maxVal = ringBuf[i]; - return maxVal; - } - - unsigned long getMinVal() { - unsigned long minVal = ringBuf[0]; - for (int i = 1; i < arraySize; i++) - if (minVal > ringBuf[i]) - minVal = ringBuf[i]; - if (minVal == 0) - minVal = 1; - return minVal; - } - - String returnArray() { // For debug purposes - String msg = ""; - for (int i = 0; i < arraySize; i++) { - msg = msg + String(ringBuf[i]) + " "; - } - return msg; - } - - - - private: - unsigned long *ringBuf; - int arraySize; - - int curIndex = 0; - float criteria = 0; - -}; - - +#include #define TOP_IR_PIN 9 #define BOTTOM_IR_PIN 10 @@ -100,24 +18,24 @@ class melty { float percentageOfRotation = 0; bool useTopIr = 1; - unsigned long period_micros_calc_array[20] = {0}; + long period_micros_calc_array[20] = {0}; ringBuffer period_micros_calc; - unsigned long time_seen_beacon_calc_array[10] = {0}; + long time_seen_beacon_calc_array[10] = {0}; ringBuffer time_seen_beacon_calc; private: bool lastSeenIRLed = 0; - unsigned long period_micros = micros(); - unsigned long time_seen_beacon = micros(); - unsigned long currentPulse = micros(), lastPulse = micros(); + long period_micros = micros(); + long time_seen_beacon = micros(); + long currentPulse = micros(), lastPulse = micros(); bool IRLedReadings[IRLedDataSize] = {0}; int IRLedIndex = 0; bool lastIRLedReturnValue = 0; - unsigned long startDrive = micros(), endDrive = micros(), startDrive2 = micros(), endDrive2 = micros(); + long startDrive = micros(), endDrive = micros(), startDrive2 = micros(), endDrive2 = micros(); bool timingToggle = 0; diff --git a/Robot Code/lib/adxl375/adxl375.cpp b/Robot Code/lib/adxl375/adxl375.cpp index 555f72b..cf256d3 100644 --- a/Robot Code/lib/adxl375/adxl375.cpp +++ b/Robot Code/lib/adxl375/adxl375.cpp @@ -1,14 +1,16 @@ -#include "adxl375.h" +#include Adafruit_ADXL375 accel = Adafruit_ADXL375(12345); +long zAccelValues[zAccelValuesSize] = {0}; + +ringBuffer zRingBuffer = ringBuffer(zAccelValues, zAccelValuesSize, 1); + void init_adxl375() { Wire.begin(5,6); - if (!accel.begin()) { - while (1) { + while (!accel.begin()) { delay(100); USBSerial.println("Failed to find ADXL375 chip"); - } } } @@ -21,13 +23,25 @@ float getAccelY() { float getAccelZ() { sensors_event_t event; accel.getEvent(&event); - return (event.acceleration.z); + return (event.acceleration.z-9.8); +} + +bool isFlipped() { + static bool isFlipped = 0; + long accelValueThreshold = 4; + zRingBuffer.update(getAccelZ()); + + if (!isFlipped) { + for (int i = 0; i < zAccelValuesSize; i++) + if (zAccelValues[i] < -accelValueThreshold) + return 0; + } else { + for (int i = 0; i < zAccelValuesSize; i++) + if (zAccelValues[i] > accelValueThreshold) + return 1; + } + + isFlipped = !isFlipped; + return isFlipped; } -float getGyroZ() { - static float heading = 0; - sensors_event_t event; - accel.getEvent(&event); - heading += event.orientation.z; - return (heading); -} \ No newline at end of file diff --git a/Robot Code/lib/adxl375/adxl375.h b/Robot Code/lib/adxl375/adxl375.h index a0ddf8d..0a62996 100644 --- a/Robot Code/lib/adxl375/adxl375.h +++ b/Robot Code/lib/adxl375/adxl375.h @@ -1,4 +1,7 @@ #include +#include + +#define zAccelValuesSize 10 void init_adxl375(); @@ -6,4 +9,4 @@ float getAccelY(); float getAccelZ(); -float getGyroZ(); \ No newline at end of file +bool isFlipped(); \ No newline at end of file diff --git a/Robot Code/lib/ringBuffer/ringBuffer.h b/Robot Code/lib/ringBuffer/ringBuffer.h new file mode 100644 index 0000000..9f33859 --- /dev/null +++ b/Robot Code/lib/ringBuffer/ringBuffer.h @@ -0,0 +1,85 @@ +#include +#ifndef RINGBUFFER_H +#define RINGBUFFER_H +class ringBuffer{ + public: + /** + * @brief Construct a ring buffer object. + * + * @param _criteria How close should the new value match the max value from the ring buffer to be considered legit? + */ + ringBuffer(long *_ringBuf, int _arraysize, float _criteria) { + ringBuf = _ringBuf; + arraySize = _arraysize; + criteria = _criteria; + } + + ringBuffer() {} + + /** + * @brief Adds a value to the ring buffer. + * + * @param val Value to be added to the ring buffer. + */ + void update(long val) { + ringBuf[curIndex++] = val; + if (curIndex > arraySize-1) + curIndex = 0; + } + + /** + * @brief Compares the input value to the max value. + * + * @param newVal New value to check against current max value. + * + * @return Whether new value is some percentage of max value. Based on the criteria variable. + */ + bool isLegit(long newVal) { + long maxVal = getMaxVal(); + if (newVal > maxVal * criteria) + return true; + else + return false; + } + + /** + * @brief Returns maximum value from our ring buffer. + */ + long getMaxVal() { + long maxVal = 0; + for (int i = 0; i < arraySize; i++) + if (maxVal < ringBuf[i]) + maxVal = ringBuf[i]; + return maxVal; + } + + long getMinVal() { + long minVal = ringBuf[0]; + for (int i = 1; i < arraySize; i++) + if (minVal > ringBuf[i]) + minVal = ringBuf[i]; + if (minVal == 0) + minVal = 1; + return minVal; + } + + String returnArray() { // For debug purposes + String msg = ""; + for (int i = 0; i < arraySize; i++) { + msg = msg + String(ringBuf[i]) + " "; + } + return msg; + } + + + + private: + long *ringBuf; + int arraySize; + + int curIndex = 0; + float criteria = 0; + +}; + +#endif \ No newline at end of file diff --git a/Robot Code/src/main.cpp b/Robot Code/src/main.cpp index 92cbc2d..fc5fb6f 100644 --- a/Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -1,11 +1,9 @@ -#include #include #include #include #include #include #include -#include #include const int packSize = 6; @@ -20,7 +18,7 @@ Drive_Motors driveMotors = Drive_Motors(); melty oreo = melty(); struct melty_parameters { int rot = 80; - int tra = 60; + int tra = 80; float per = 0.5; int invert = 1; int boost = 5; @@ -32,6 +30,8 @@ struct tank_drive_parameters { int boost = 40; } tank_drive_parameters; +int tuningValue = 10; + void setup() { USBSerial.begin(115200); @@ -40,26 +40,24 @@ void setup() setLeds(ORANGE); init_adxl375(); driveMotors.arm_motors(); - laptop.init_ble("oreo"); + laptop.init_ble("Oreo"); setLeds(BLACK); } void loop() { if (laptop.isConnected()) { - if (driveMotors.isNeutral()) { - // if (getAccelZ() > 7) { + EVERY_N_MILLIS(50) { + if (!isFlipped()) { oreo.useTopIr = 1; driveMotors.flip_motors = 0; melty_parameters.invert = 1; - // } - // if (getAccelZ() < -7) { - // oreo.useTopIr = 0; - // driveMotors.flip_motors = 1; - // melty_parameters.invert = -1; - // } + } else { + oreo.useTopIr = 0; + driveMotors.flip_motors = 1; + melty_parameters.invert = -1; + } } - if (laptop_packetBuffer[0] == '1') { // Currently enabled and meltybraining!!! if (oreo.update()) { // If seen the LED EVERY_N_MILLIS(250) { @@ -94,12 +92,12 @@ void loop() } EVERY_N_SECONDS(1) { // DEBUGGIN!!!! - String msg = "Beacon RPM: " + String(oreo.RPM) + " time seen beacon: " + oreo.time_seen_beacon_calc.returnArray();//" Accel: " + String(getAccelY()); + String msg = "RPM : " + String(oreo.RPM) + "AccelY : " + String(getAccelY()); laptop.send(msg); } EVERY_N_MILLIS(100) { - int tuningValue = 5; + switch (laptop_packetBuffer[2]) { case '1': melty_parameters.rot+=tuningValue; @@ -189,16 +187,16 @@ void loop() EVERY_N_MILLIS(100) { switch (laptop_packetBuffer[2]) { case '1': - tank_drive_parameters.drive++; + tank_drive_parameters.drive+=tuningValue; break; case '2': - tank_drive_parameters.drive--; + tank_drive_parameters.drive-=tuningValue; break; case '3': - tank_drive_parameters.turn++; + tank_drive_parameters.turn+=tuningValue; break; case '4': - tank_drive_parameters.turn--; + tank_drive_parameters.turn-=tuningValue; break; } diff --git a/controller/controller.py b/controller/controller.py index 8e9e3b3..3257382 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -11,7 +11,7 @@ def millis(): return round((time.time()-startTime) * 1000) ir_beacon_2 = BLE_UART(peripheral_name='IR Beacon 2', address = 'CBB4A195-F34C-E6C5-21CD-ACBB7D44352A') -ir_beacon_1 = BLE_UART(peripheral_name='IR Beacon 1', address = 'CC80B9F1-FE04-64E9-2CE8-014A24EEE1BF') +ir_beacon_1 = BLE_UART(peripheral_name='IR Beacon 1', address = '37E54CED-FA64-96E8-C84C-8528ADB5AC13') oreo = BLE_UART(peripheral_name='Oreo', address = '599CA2EF-37D8-78BE-C3A8-C8DC5CEE9838') From 2a28b502089c7d9106167457fae57781da027443 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Thu, 11 Apr 2024 09:37:15 -0700 Subject: [PATCH 32/48] auto slowdown working, tracking well, accelrpm should update regardless of IR led tracking --- IR Beacon/src/main.cpp | 4 +- .../lib/Battery_Monitor/Battery_Monitor.h | 21 ++++++-- Robot Code/lib/Melty/melty.cpp | 50 ++++++++++++------- Robot Code/lib/Melty/melty.h | 17 ++++--- Robot Code/lib/adxl375/adxl375.cpp | 13 +++-- Robot Code/lib/adxl375/adxl375.h | 2 + Robot Code/src/main.cpp | 45 ++++++++--------- controller/controller.py | 17 +++---- 8 files changed, 102 insertions(+), 67 deletions(-) diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp index a7dba4a..1625836 100644 --- a/IR Beacon/src/main.cpp +++ b/IR Beacon/src/main.cpp @@ -4,7 +4,7 @@ #include #include -#define TARGETCMD '1' // Change based on which IR_Beacon working on +#define TARGETCMD '2' // Change based on which IR_Beacon working on const int packSize = 6; char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; @@ -68,7 +68,7 @@ void loop(){ } break; default: // Don't enable current IR Beacon if in another mode - setLeds(BLACK); + toggleLeds(WHITE, BLACK, 500); ledcWrite(ledChannel, 0); break; } diff --git a/Robot Code/lib/Battery_Monitor/Battery_Monitor.h b/Robot Code/lib/Battery_Monitor/Battery_Monitor.h index 9f18f8a..0c8ac7c 100644 --- a/Robot Code/lib/Battery_Monitor/Battery_Monitor.h +++ b/Robot Code/lib/Battery_Monitor/Battery_Monitor.h @@ -1,9 +1,24 @@ #include -float get3sVoltage() { - return (float(analogRead(18)) / 4096)*3.1 * 4.1; +int getPerc(int min, int max, int val) { + if (val == 0) // Switch not on + return -1; + int perc = 100 * (val - min) / double(max - min); + if (perc > 100) + perc = 100; + if (perc < 0) + perc = 0; + return perc; + +} +int get3sSOC() { + int max = 4096-20; + int min = 3300; + // return analogRead(18); + return getPerc(min, max ,analogRead(18)); } float get1sVoltage() { return (float(analogRead(18)) / 4096)*3.1 * 2.00 + .35; -} \ No newline at end of file +} + diff --git a/Robot Code/lib/Melty/melty.cpp b/Robot Code/lib/Melty/melty.cpp index 1b1f349..4e58892 100644 --- a/Robot Code/lib/Melty/melty.cpp +++ b/Robot Code/lib/Melty/melty.cpp @@ -3,7 +3,7 @@ melty::melty() { period_micros_calc = ringBuffer(period_micros_calc_array, 5, 1); - time_seen_beacon_calc = ringBuffer(time_seen_beacon_calc_array, 10, 0.75); + time_seen_beacon_calc = ringBuffer(time_seen_beacon_calc_array, TIME_SEEN_BEACON_ARRAY_SIZE, 0.7); } bool melty::update() { @@ -21,7 +21,9 @@ bool melty::update() { else { // Activates on the falling edge of seeing the IR LED setLeds(YELLOW); time_seen_beacon = micros() - currentPulse; + time_seen_beacon_calc.update(time_seen_beacon); + if (time_seen_beacon_calc.isLegit(time_seen_beacon)) { unsigned long curMicros = micros(); period_micros = curMicros - lastPulse; // How long it takes to complete one revolution @@ -36,27 +38,33 @@ bool melty::update() { } void melty::computeTimings() { - unsigned long max_period = period_micros_calc.getMinVal(); - int ledRPM = (us_per_min)/(max_period); - // int accelRPM = getAccelY() * ; // calculate the accelrometer rpm - // Compare acclerometer rpm to the rpm calculated based on light, if its off but a certain margin - RPM = ledRPM; + unsigned long rotation_period; + ledRPM = (us_per_min)/(period_micros_calc.getMinVal()); + if (abs(ledRPM - accelRPM) < 50) + rotation_period = period_micros_calc.getMinVal(); + else + rotation_period = acccel_period; + + unsigned long center_of_beacon = currentPulse + time_seen_beacon/2; // This should ideally be centered on the beacon - unsigned long centerOfDrivePulse = center_of_beacon + (float(deg)/360)*max_period; // Direction that we should be driving towards - unsigned long deltaDriveTiming = (percentageOfRotation * max_period)/2; + unsigned long centerOfDrivePulse = center_of_beacon + (float(deg)/360)*rotation_period; // Direction that we should be driving towards + unsigned long deltaDriveTiming = (percentageOfRotation * rotation_period)/2; unsigned long offset = 0; if (centerOfDrivePulse - deltaDriveTiming < micros()) { // If we're supposed to start translating before we have calculated those values, offset by one rotation - offset = max_period; + offset = rotation_period; } - if (timingToggle) { // Swap variables that we use so we don't interfere (in the case that we're currently translating whilst computing) - startDrive = centerOfDrivePulse - deltaDriveTiming + offset; - endDrive = centerOfDrivePulse + deltaDriveTiming + offset; - } else { - startDrive2 = centerOfDrivePulse - deltaDriveTiming + offset; - endDrive2 = centerOfDrivePulse + deltaDriveTiming + offset; - } + for (int i = 0; i < TRANSLATE_TIMINGS_SIZE/2; i++) { + int index = 0; + if (timingToggle) + index = i*2; + else + index = i*2+1; + + startDrive[index] = centerOfDrivePulse - deltaDriveTiming + offset + rotation_period*i; + endDrive[index] = centerOfDrivePulse + deltaDriveTiming + offset + rotation_period*i; + } timingToggle = !timingToggle; // Toggle it so that next iteration uses different variables @@ -64,8 +72,14 @@ void melty::computeTimings() { bool melty::translate() { // Returns whether or not robot should translate now unsigned long currentTime = micros(); - if (percentageOfRotation != 0) - return (currentTime > startDrive && currentTime < endDrive || currentTime > startDrive2 && currentTime < endDrive2); + + if (percentageOfRotation != 0) { + for (int i = 0; i < TRANSLATE_TIMINGS_SIZE; i++) { + if (currentTime > startDrive[i] && currentTime < endDrive[i]) + return 1; + } + return 0; + } else return 0; } diff --git a/Robot Code/lib/Melty/melty.h b/Robot Code/lib/Melty/melty.h index 78479e8..8f158df 100644 --- a/Robot Code/lib/Melty/melty.h +++ b/Robot Code/lib/Melty/melty.h @@ -6,6 +6,8 @@ #define BOTTOM_IR_PIN 10 #define IRLedDataSize 40 // Size of our Ring Buffer that will hold the IR Led data +#define TRANSLATE_TIMINGS_SIZE 6 +#define TIME_SEEN_BEACON_ARRAY_SIZE 5 class melty { public: melty(); @@ -13,17 +15,21 @@ class melty { bool isBeaconSensed(bool currentReading); void computeTimings(); bool translate(); - int RPM = 0; + int ledRPM = 0; + int accelRPM = 0; + unsigned long acccel_period = 1; int deg = 0; float percentageOfRotation = 0; bool useTopIr = 1; - long period_micros_calc_array[20] = {0}; + long period_micros_calc_array[5] = {0}; ringBuffer period_micros_calc; - long time_seen_beacon_calc_array[10] = {0}; + long time_seen_beacon_calc_array[TIME_SEEN_BEACON_ARRAY_SIZE] = {0}; ringBuffer time_seen_beacon_calc; + const int us_per_min = 60000000; + private: bool lastSeenIRLed = 0; long period_micros = micros(); @@ -35,13 +41,12 @@ class melty { bool lastIRLedReturnValue = 0; - long startDrive = micros(), endDrive = micros(), startDrive2 = micros(), endDrive2 = micros(); + long startDrive[TRANSLATE_TIMINGS_SIZE] = {0}; + long endDrive[TRANSLATE_TIMINGS_SIZE] = {0}; bool timingToggle = 0; - - const int us_per_min = 60000000; }; \ No newline at end of file diff --git a/Robot Code/lib/adxl375/adxl375.cpp b/Robot Code/lib/adxl375/adxl375.cpp index cf256d3..69b4db1 100644 --- a/Robot Code/lib/adxl375/adxl375.cpp +++ b/Robot Code/lib/adxl375/adxl375.cpp @@ -1,6 +1,7 @@ #include Adafruit_ADXL375 accel = Adafruit_ADXL375(12345); +sensors_event_t event; long zAccelValues[zAccelValuesSize] = {0}; @@ -14,15 +15,19 @@ void init_adxl375() { } } -float getAccelY() { - sensors_event_t event; +void updateAccel() { accel.getEvent(&event); +} + +float getAccelY() { + // sensors_event_t event; + // accel.getEvent(&event); return (abs(event.acceleration.y)); } float getAccelZ() { - sensors_event_t event; - accel.getEvent(&event); + // sensors_event_t event; + // accel.getEvent(&event); return (event.acceleration.z-9.8); } diff --git a/Robot Code/lib/adxl375/adxl375.h b/Robot Code/lib/adxl375/adxl375.h index 0a62996..157a49e 100644 --- a/Robot Code/lib/adxl375/adxl375.h +++ b/Robot Code/lib/adxl375/adxl375.h @@ -5,6 +5,8 @@ void init_adxl375(); +void updateAccel(); + float getAccelY(); float getAccelZ(); diff --git a/Robot Code/src/main.cpp b/Robot Code/src/main.cpp index fc5fb6f..9ae2a20 100644 --- a/Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -10,7 +10,7 @@ const int packSize = 6; char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; const int headings[] = {0, 45, 90, 135, 180, 225, 270, 315}; bool wasMeltying = false; -int slowDownSpeed = 8; +int slowDownSpeed = 120; BLE_Uart laptop = BLE_Uart(laptop_packetBuffer, packSize); Drive_Motors driveMotors = Drive_Motors(); @@ -21,11 +21,10 @@ struct melty_parameters { int tra = 80; float per = 0.5; int invert = 1; - int boost = 5; } melty_parameters; struct tank_drive_parameters { - int drive = 40; + int drive = 10; int turn = 20; int boost = 40; } tank_drive_parameters; @@ -47,6 +46,11 @@ void setup() void loop() { if (laptop.isConnected()) { + EVERY_N_MILLIS(25) { + updateAccel(); + oreo.acccel_period =(2 * 3.14 * 20.97 * 1000000) / (sqrt(getAccelY() * 1000 * 20.97)); + oreo.accelRPM = (oreo.us_per_min)/(oreo.acccel_period); + } EVERY_N_MILLIS(50) { if (!isFlipped()) { oreo.useTopIr = 1; @@ -65,15 +69,11 @@ void loop() } } - - int boostVal = 0; - if (laptop_packetBuffer[3] == '1') - boostVal = melty_parameters.boost; if (oreo.translate()) { - driveMotors.l_motor_write(melty_parameters.invert * (melty_parameters.rot - (melty_parameters.tra + boostVal))); - driveMotors.r_motor_write(melty_parameters.invert * (melty_parameters.rot + (melty_parameters.tra + boostVal))); + driveMotors.l_motor_write(melty_parameters.invert * (melty_parameters.rot - melty_parameters.invert * melty_parameters.tra)); + driveMotors.r_motor_write(melty_parameters.invert * (melty_parameters.rot + melty_parameters.invert * melty_parameters.tra)); } else { - driveMotors.set_both_motors(melty_parameters.invert * (melty_parameters.rot + boostVal)); + driveMotors.set_both_motors(melty_parameters.invert * (melty_parameters.rot)); } int drivecmd = laptop_packetBuffer[1] - '0'; @@ -92,12 +92,11 @@ void loop() } EVERY_N_SECONDS(1) { // DEBUGGIN!!!! - String msg = "RPM : " + String(oreo.RPM) + "AccelY : " + String(getAccelY()); + String msg = "RPM (IR): " + String(oreo.ledRPM) + " RPM (Accel) : " + String(oreo.accelRPM); laptop.send(msg); } EVERY_N_MILLIS(100) { - switch (laptop_packetBuffer[2]) { case '1': melty_parameters.rot+=tuningValue; @@ -128,14 +127,15 @@ void loop() wasMeltying = 1; } else if (laptop_packetBuffer[0] == '2') { // Tank driving mode! - // if (wasMeltying) { // Was previously meltybraining, we need to slowdown - // unsigned long timeout = millis(); - // while (getAccelY() > .3 && millis() - timeout < 2000) { - // driveMotors.set_both_motors(-slowDownSpeed * melty_parameters.invert); - // toggleLeds(CRGB::White, CRGB::Red, 150); - // } - // wasMeltying = 0; - // } + if (wasMeltying) { // Was previously meltybraining, we need to slowdown + unsigned long timeout = millis(); + while (getAccelY() > 2 && millis() - timeout < 2000) { + updateAccel(); + driveMotors.set_both_motors(-slowDownSpeed * melty_parameters.invert); + toggleLeds(WHITE, RED, 150); + } + wasMeltying = 0; + } int lmotorpwr = 0; int rmotorpwr = 0; @@ -214,7 +214,7 @@ void loop() toggleLeds(RED, GREEN, 500); driveMotors.set_both_motors(0); EVERY_N_SECONDS(10) { - laptop.send(get3sVoltage()); + laptop.send("SOC: " + String(get3sSOC()) + " %"); } } } else { // Currently DISCONNECTED @@ -222,7 +222,4 @@ void loop() toggleLeds(RED, BLACK, 500); } - - - } diff --git a/controller/controller.py b/controller/controller.py index 3257382..d6ef2f9 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -10,8 +10,8 @@ def millis(): return round((time.time()-startTime) * 1000) -ir_beacon_2 = BLE_UART(peripheral_name='IR Beacon 2', address = 'CBB4A195-F34C-E6C5-21CD-ACBB7D44352A') -ir_beacon_1 = BLE_UART(peripheral_name='IR Beacon 1', address = '37E54CED-FA64-96E8-C84C-8528ADB5AC13') +ir_beacon_2 = BLE_UART(peripheral_name='Beac 2', address = '642D48B0-0DA1-AB00-2DDD-B639F5353E80') +ir_beacon_1 = BLE_UART(peripheral_name='Beac 1', address = '37E54CED-FA64-96E8-C84C-8528ADB5AC13') oreo = BLE_UART(peripheral_name='Oreo', address = '599CA2EF-37D8-78BE-C3A8-C8DC5CEE9838') @@ -49,7 +49,7 @@ async def bluetooth_comm_handler(BLE_DEVICE, isMainRobot): else: await BLE_DEVICE.connect() - + async def ir_beacon_switcher(): global enabled global lastBeaconRead @@ -57,7 +57,7 @@ async def ir_beacon_switcher(): await asyncio.sleep(0.1) if (enabled != 1): lastBeaconRead = millis() + 2000 # Add some time so the beacon doesnt switch right after enabling melty brain mode - if (enabled == 1 and millis() - lastBeaconRead > 1000 and ir_beacon_2.isConnected == True): + if (enabled == 1 and millis() - lastBeaconRead > 1000 and ir_beacon_2.isConnected == True and ir_beacon_1.isConnected): toggleBeacon() def toggleBeacon(): @@ -137,12 +137,10 @@ async def cmd_handler(): waitForEnableReleased = 0 if (enabled != 0): - if (get_key_state('z')): + if (get_key_state('z') or get_key_state('Z')): enabled = drivestate = 1 - if (get_key_state('x')): + if (get_key_state('x') or get_key_state('X')): enabled = drivestate = 2 - if (get_key_state('c')): - enabled = drivestate = 3 curState = get_key_state('1') if curState and not lastState: @@ -150,11 +148,10 @@ async def cmd_handler(): lastState = curState if (get_key_state(Key.shift)): - boost = 1 + boost = 1 oreocmd = f"{enabled}{drivecmd}{oreotuning}{boost}00" irbeaconcmd = f"{enabled}{activeBeacon}{irbeacontuning}000" - # print(oreocmd) await asyncio.sleep(0.05) async def main(): From 4ab49099b9572129e9fd11b794e9fa0255d4bfe2 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Mon, 15 Apr 2024 23:05:36 -0700 Subject: [PATCH 33/48] Stable release, everything works. Brownout at ~1300 rpm but pretty sure its because of low speced battery --- .DS_Store | Bin 6148 -> 6148 bytes IR Beacon/.DS_Store | Bin 0 -> 6148 bytes IR Beacon/lib/.DS_Store | Bin 0 -> 6148 bytes IR Beacon/src/main.cpp | 7 ++++--- .../lib/BLE_Uart/BLE_Uart.cpp | 0 .../lib/BLE_Uart/BLE_Uart.h | 0 .../lib/Battery_Monitor/Battery_Monitor.h | 8 ++++---- Robot Code/platformio.ini | 2 -- Robot Code/src/main.cpp | 2 ++ .../__pycache__/bluetooth.cpython-311.pyc | Bin 4946 -> 4982 bytes controller/bluetooth.py | 3 ++- controller/controller.py | 4 ++-- 12 files changed, 14 insertions(+), 12 deletions(-) create mode 100644 IR Beacon/.DS_Store create mode 100644 IR Beacon/lib/.DS_Store rename {IR Beacon => Robot Code}/lib/BLE_Uart/BLE_Uart.cpp (100%) rename {IR Beacon => Robot Code}/lib/BLE_Uart/BLE_Uart.h (100%) diff --git a/.DS_Store b/.DS_Store index ab2150a1ff19f5ec004f34720267f4a6b9ff5cb5..44d37b0de32debf78f4a44839dd5134e08dd84a6 100644 GIT binary patch delta 86 zcmZoMXfc@J&&a(oU^g=(_hcTH*vb8@r5xs_<~j-h1zy@&)8GCy%GdVacAT%&FlRO6-0gIDZ d2Oj~9lR*U*vp5BO0h5pi46$`a0ka1P{SV%q6kGrR diff --git a/IR Beacon/.DS_Store b/IR Beacon/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..e147f99608f9d407090f9424f6809caa8db593de GIT binary patch literal 6148 zcmeHKF=_)r43uIQhBPiy?ic)n#W*kU2ZHU!aA2@;e^uUj_*V+}a<|`Y@k-TOCojid+u$E?)_lQfSO*0m+A%QNF&@~C d-=Zk%8rRtGg=1pSkqH1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 #include -#define TARGETCMD '2' // Change based on which IR_Beacon working on +#define TARGETCMD '1' // Change based on which IR_Beacon working on const int packSize = 6; char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; @@ -35,8 +35,9 @@ void loop(){ case '0': // Disabled toggleLeds(RED, GREEN, 500); ledcWrite(ledChannel, 0); - EVERY_N_SECONDS(10) - laptop.send(get1sVoltage()); + EVERY_N_SECONDS(10) { + laptop.send("SOC: " + String(get1sSOC()) + " %"); + } break; case '1': // Enable one of the IR Beacons if (laptop_packetBuffer[1] == TARGETCMD) { diff --git a/IR Beacon/lib/BLE_Uart/BLE_Uart.cpp b/Robot Code/lib/BLE_Uart/BLE_Uart.cpp similarity index 100% rename from IR Beacon/lib/BLE_Uart/BLE_Uart.cpp rename to Robot Code/lib/BLE_Uart/BLE_Uart.cpp diff --git a/IR Beacon/lib/BLE_Uart/BLE_Uart.h b/Robot Code/lib/BLE_Uart/BLE_Uart.h similarity index 100% rename from IR Beacon/lib/BLE_Uart/BLE_Uart.h rename to Robot Code/lib/BLE_Uart/BLE_Uart.h diff --git a/Robot Code/lib/Battery_Monitor/Battery_Monitor.h b/Robot Code/lib/Battery_Monitor/Battery_Monitor.h index 0c8ac7c..e86d7d6 100644 --- a/Robot Code/lib/Battery_Monitor/Battery_Monitor.h +++ b/Robot Code/lib/Battery_Monitor/Battery_Monitor.h @@ -14,11 +14,11 @@ int getPerc(int min, int max, int val) { int get3sSOC() { int max = 4096-20; int min = 3300; - // return analogRead(18); return getPerc(min, max ,analogRead(18)); } -float get1sVoltage() { - return (float(analogRead(18)) / 4096)*3.1 * 2.00 + .35; +int get1sSOC() { + int max = 2585-50; + int min = 2048; + return getPerc(min, max ,analogRead(18)); } - diff --git a/Robot Code/platformio.ini b/Robot Code/platformio.ini index ddf1d4d..45e92ab 100644 --- a/Robot Code/platformio.ini +++ b/Robot Code/platformio.ini @@ -21,5 +21,3 @@ lib_deps = freenove/Freenove WS2812 Lib for ESP32@^1.0.6 moddingear/ESP32 ESC@^1.0.0 adafruit/Adafruit ADXL375@^1.1.2 -lib_extra_dirs = - /Users/mingweiyeoh/Documents/GitHub/Oreo/IR Beacon/lib diff --git a/Robot Code/src/main.cpp b/Robot Code/src/main.cpp index 9ae2a20..e4ea19d 100644 --- a/Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -133,6 +133,8 @@ void loop() updateAccel(); driveMotors.set_both_motors(-slowDownSpeed * melty_parameters.invert); toggleLeds(WHITE, RED, 150); + if (laptop_packetBuffer[0] != '2') + break; } wasMeltying = 0; } diff --git a/controller/__pycache__/bluetooth.cpython-311.pyc b/controller/__pycache__/bluetooth.cpython-311.pyc index 8fb9b34a20d65a301e0d1695fd40df461128a2d1..35c87ab79fa9adc40d0e266a13fbffe308bc0fdb 100644 GIT binary patch delta 293 zcmcbl_DzjUn*5wcmX)!FA)a9}C$F!vTa}PYW^rh~SxA z$!lkn1Z2Jd!Ul#LJOWpEWEQYq;!(Z8qk5M|@Pd%tf|4uhHb5GPF7nu4;jzEKV?X&e z?{h}O&4>Be85vC`pXR^KXt8;@fDWTH3#-Nl1~|dP$f_|T_yYrwTERKlP)Kw$vyc>{ VsSzVk5hn2wB=!Z1M3ETKfdK7!N$~&x delta 241 zcmeySc1ewQIWI340}$L>z?~Mdk#`@DObd|b&A`YoouPygBm)5{j5Q2djFbQJ$g(ok zFvK%Wmgn_Vt`c&|EKbhP%S%lzNlj5mE6UGRaMNTh(gx}&;sFsxllyt?R6>Bv7eLs+ zaF>Jk0-x0alPk&=KpKcHa#&yCu)e@yJ^4THb4G*B&-vIH8I32u=fBNpzWKC(4kJG^ htHuWgIKj1 Date: Wed, 17 Apr 2024 16:31:28 -0700 Subject: [PATCH 34/48] Fixed keyboard controller bug . uppercase keys now treated as lowercase keys --- controller/LaptopKeyboard.py | 13 ++++-- .../LaptopKeyboard.cpython-311.pyc | Bin 956 -> 1409 bytes controller/controller.py | 43 +++++++++--------- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/controller/LaptopKeyboard.py b/controller/LaptopKeyboard.py index 82d5b04..5c39849 100644 --- a/controller/LaptopKeyboard.py +++ b/controller/LaptopKeyboard.py @@ -1,18 +1,23 @@ from pynput.keyboard import Key, Listener, KeyCode - +from time import sleep key_state = {} # Function to be called when a key is pressed -def on_press(key): +def on_press(key): + key = str(key).strip("'") + # print(len(key)) + if len(key) == 1 and key.isupper(): + key = key.lower() key_state[key] = True # Update key state to pressed # Function to be called when a key is released def on_release(key): + key = str(key).strip("'") + if len(key) == 1 and key.isupper(): + key = key.lower() key_state[key] = False # Update key state to released def get_key_state(key): - if type(key) == str: - key = KeyCode.from_char(key) if (key in key_state and key_state[key]): return True else: diff --git a/controller/__pycache__/LaptopKeyboard.cpython-311.pyc b/controller/__pycache__/LaptopKeyboard.cpython-311.pyc index f8c43c14120a77e02370006f21dc366098bf7744..9987fd75847ae9e28132c1a985d5a863d22a02c4 100644 GIT binary patch literal 1409 zcmd^8&rcIU6n@j~_Q!@&NI?bCgm{r&+Ng;q;sr&Gq#nQnJxEG-#7%d*>&#SW(k6?+ zq!0|=K!cGJhVai|CJ8;y70&VKXWyf<%VzIpRG7SjOb>-_lh z-5|hsX|yG9p|mDZ*#;eSVuD3-#0umBmXcG591EC1OU5i&_P}@F$-*f1JqJLsGQH4FZc8~1i$YE!c4PhK|)+^m{HP98|vwR*| z1<9ul$V)JLMsA$Sz0VzlJcSE}JP2kuo?_CH7dCj=agZm~w3o4q7HMUHi@cx*#8c4V za_acm{mfHN8P8ZoX=#}n6>7i8+_MX1iJTSz=@?z!*qjsiXmjts-ld{ZA-VwHg z7qDpOnVxp=UhL9>!zkx$2-Ak}=UD_7PPACPus*Rlv6bFP?yGgg!=pWoNHjMS+X0fg`hDY8!suxzXE`BY{2>;zRvk0D= zMG>Z%ECn`*aDIC%fvTq=_l%k}j~96r2l}P|uk4@vGy9{z*k{9-w~KvQ^0L@lcM!Jm zQ3&kY@(Nh|vR#FdwhLH5C$sqcv5b}M+;obt0rd48EVe|&LZ@z-oG+`3Uq^7E8G&fu z+saYo8n%z~dwlm9DvfxeF22Qw>Scew*_7tRol40mi!^5P>y^npGx_?Me9~S}7#3x+ zX3w`F*YJt-S3Ceekx%tmWYV(rvPtirocWdQ$vNHLFtYn^Sn5@d8R?m8irKGYT^%iq+Nl_6<<1Nm#qWs+Wl{XB$lKWi2&7r+*IrcBpMj*vU7JhH&(V(PIbP-E`EVs{4Ss96+Za|%oq7o zuJEaJFyG+d?ybDQApt~lBNs>%&xr%FZtx3tlsw=U=_m#J&rg$M@(LCOQ-}jV!3qu& zuw5$|z)HZO_KU+NH$SB`C)KV<9H Date: Fri, 19 Apr 2024 09:11:59 -0700 Subject: [PATCH 35/48] fixed "brownout" problem. Needed to run adxl375 calibration code --- ADXL375 Test/src/main.cpp | 35 +++++++++++++++--- Robot Code/lib/adxl375/adxl375.cpp | 10 ++--- Robot Code/lib/adxl375/adxl375.h | 2 +- Robot Code/src/main.cpp | 2 +- .../LaptopKeyboard.cpython-311.pyc | Bin 1409 -> 1407 bytes controller/controller.py | 4 +- 6 files changed, 36 insertions(+), 17 deletions(-) diff --git a/ADXL375 Test/src/main.cpp b/ADXL375 Test/src/main.cpp index ec485c4..b8d2dd7 100644 --- a/ADXL375 Test/src/main.cpp +++ b/ADXL375 Test/src/main.cpp @@ -82,22 +82,45 @@ void setup(void) { USBSerial.begin(115200); while (!USBSerial); + delay(1000); USBSerial.println("ADXL375 Accelerometer Test"); USBSerial.println(""); Wire.begin(5,6); /* Initialise the sensor */ - if(!accel.begin()) + while(!accel.begin()) { /* There was a problem detecting the ADXL375 ... check your connections */ USBSerial.println("Ooops, no ADXL375 detected ... Check your wiring!"); - while(1); + delay(100); } // Range is fixed at +-200g /* Display some basic information on this sensor */ - accel.printSensorDetails(); - displayDataRate(); - USBSerial.println(""); + accel.setTrimOffsets(0, 0, 0); + + USBSerial.println("Hold accelerometer flat to set offsets to 0, 0, and -1g..."); + delay(1000); + int16_t x, y, z; + x = accel.getX(); + y = accel.getY(); + z = accel.getZ(); + USBSerial.print("Raw X: "); USBSerial.print(x); USBSerial.print(" "); + USBSerial.print("Y: "); USBSerial.print(y); USBSerial.print(" "); + USBSerial.print("Z: "); USBSerial.print(z); USBSerial.print(" ");USBSerial.println(" counts"); + + // the trim offsets are in 'multiples' of 4, we want to round, so we add 2 + accel.setTrimOffsets(-(x+2)/4, + -(y+2)/4, + -(z-20+2)/4); // Z should be '20' at 1g (49mg per bit) + + int8_t x_offset, y_offset, z_offset; + accel.getTrimOffsets(&x_offset, &y_offset, &z_offset); + USBSerial.print("Current trim offsets: "); + USBSerial.print(x_offset); USBSerial.print(", "); + USBSerial.print(y_offset); USBSerial.print(", "); + USBSerial.println(z_offset); + + USBSerial.println(); } void loop(void) @@ -110,5 +133,5 @@ void loop(void) USBSerial.print("X: "); USBSerial.print(event.acceleration.x); USBSerial.print(" "); USBSerial.print("Y: "); USBSerial.print(event.acceleration.y); USBSerial.print(" "); USBSerial.print("Z: "); USBSerial.print(event.acceleration.z); USBSerial.print(" ");USBSerial.println("m/s^2 "); - delay(100); + delay(250); } \ No newline at end of file diff --git a/Robot Code/lib/adxl375/adxl375.cpp b/Robot Code/lib/adxl375/adxl375.cpp index 69b4db1..d05c4ca 100644 --- a/Robot Code/lib/adxl375/adxl375.cpp +++ b/Robot Code/lib/adxl375/adxl375.cpp @@ -20,24 +20,20 @@ void updateAccel() { } float getAccelY() { - // sensors_event_t event; - // accel.getEvent(&event); return (abs(event.acceleration.y)); } float getAccelZ() { - // sensors_event_t event; - // accel.getEvent(&event); - return (event.acceleration.z-9.8); + return (event.acceleration.z); } bool isFlipped() { static bool isFlipped = 0; - long accelValueThreshold = 4; + long accelValueThreshold = 6; zRingBuffer.update(getAccelZ()); if (!isFlipped) { - for (int i = 0; i < zAccelValuesSize; i++) + for (int i = 0; i < zAccelValuesSize; i++) // Negative values mean robot is NOT flipped if (zAccelValues[i] < -accelValueThreshold) return 0; } else { diff --git a/Robot Code/lib/adxl375/adxl375.h b/Robot Code/lib/adxl375/adxl375.h index 157a49e..11b148e 100644 --- a/Robot Code/lib/adxl375/adxl375.h +++ b/Robot Code/lib/adxl375/adxl375.h @@ -1,7 +1,7 @@ #include #include -#define zAccelValuesSize 10 +#define zAccelValuesSize 20 void init_adxl375(); diff --git a/Robot Code/src/main.cpp b/Robot Code/src/main.cpp index e4ea19d..8e41cca 100644 --- a/Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -10,7 +10,7 @@ const int packSize = 6; char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; const int headings[] = {0, 45, 90, 135, 180, 225, 270, 315}; bool wasMeltying = false; -int slowDownSpeed = 120; +int slowDownSpeed = 200; BLE_Uart laptop = BLE_Uart(laptop_packetBuffer, packSize); Drive_Motors driveMotors = Drive_Motors(); diff --git a/controller/__pycache__/LaptopKeyboard.cpython-311.pyc b/controller/__pycache__/LaptopKeyboard.cpython-311.pyc index 9987fd75847ae9e28132c1a985d5a863d22a02c4..83ff40925a76e30cba7fcf452fee39167179f3aa 100644 GIT binary patch delta 66 zcmZqV{?Em?oR^o20SMkjDx|Gv+Q^r~!l?k{HZa`a;GW#d@<4`#QS<`?CQ&2`R0RMS C{0@-- delta 68 zcmey*)yU1aoR^o20SGQfDx}R}*~pi}!mR}4HZXkPVBqAL+|2SohMiIL0|O>eBnngq E04945m;e9( diff --git a/controller/controller.py b/controller/controller.py index 268560c..1b6ea8a 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -155,7 +155,7 @@ async def cmd_handler(): await asyncio.sleep(0.05) async def main(): - await asyncio.gather(ir_beacon_switcher(), cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(oreo, True), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(oreo), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) - # await asyncio.gather(ir_beacon_switcher(), cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(oreo, True), bluetooth_comm_handler(hockey_puck, True), bluetooth_receive_handler(hockey_puck), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(oreo), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) + # await asyncio.gather(ir_beacon_switcher(), cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(oreo, True), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(oreo), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) + await asyncio.gather(ir_beacon_switcher(), cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(oreo, True), bluetooth_comm_handler(hockey_puck, True), bluetooth_receive_handler(hockey_puck), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(oreo), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) asyncio.run(main()) From 9e594ea5e0bb3d52aa26ca5cb8407c5a9a958bf8 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Fri, 19 Apr 2024 17:50:42 -0700 Subject: [PATCH 36/48] added accelerometer calibration while disabled --- Robot Code/lib/adxl375/adxl375.cpp | 21 +++++++++++++++++++++ Robot Code/lib/adxl375/adxl375.h | 2 ++ Robot Code/src/main.cpp | 15 +++++++++++++-- controller/controller.py | 15 ++++++++++++--- 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/Robot Code/lib/adxl375/adxl375.cpp b/Robot Code/lib/adxl375/adxl375.cpp index d05c4ca..bc94038 100644 --- a/Robot Code/lib/adxl375/adxl375.cpp +++ b/Robot Code/lib/adxl375/adxl375.cpp @@ -27,6 +27,27 @@ float getAccelZ() { return (event.acceleration.z); } +void calibrateAccel(bool topside) { + accel.setTrimOffsets(0, 0, 0); + delay(1000); + + int16_t x, y, z; + x = accel.getX(); + y = accel.getY(); + z = accel.getZ(); + + // the trim offsets are in 'multiples' of 4, we want to round, so we add 2 + if (topside) { + accel.setTrimOffsets(-(x+2)/4, + -(y+2)/4, + -(z+20+2)/4); + } else { + accel.setTrimOffsets(-(x+2)/4, + -(y+2)/4, + -(z-20+2)/4); // Z should be '20' at 1g (49mg per bit) + } +} + bool isFlipped() { static bool isFlipped = 0; long accelValueThreshold = 6; diff --git a/Robot Code/lib/adxl375/adxl375.h b/Robot Code/lib/adxl375/adxl375.h index 11b148e..e3400ef 100644 --- a/Robot Code/lib/adxl375/adxl375.h +++ b/Robot Code/lib/adxl375/adxl375.h @@ -7,6 +7,8 @@ void init_adxl375(); void updateAccel(); +void calibrateAccel(bool topside); + float getAccelY(); float getAccelZ(); diff --git a/Robot Code/src/main.cpp b/Robot Code/src/main.cpp index 8e41cca..8c0a003 100644 --- a/Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -215,9 +215,20 @@ void loop() } else { // Currently disabled toggleLeds(RED, GREEN, 500); driveMotors.set_both_motors(0); - EVERY_N_SECONDS(10) { - laptop.send("SOC: " + String(get3sSOC()) + " %"); + + EVERY_N_MILLIS(100) { + if (laptop_packetBuffer[4] == '1') { + laptop.send("Calibrating AccelZ with Robot Upwards (~ -9.8)"); + calibrateAccel(1); + } else if (laptop_packetBuffer[4] == '2') { + laptop.send("Calibrating AccelZ with Robot Downwards (~ 9.8)"); + calibrateAccel(0); } + } + + EVERY_N_SECONDS(1) { + laptop.send("SOC: " + String(get3sSOC()) + " % " + String(getAccelZ())); + } } } else { // Currently DISCONNECTED driveMotors.set_both_motors(0); diff --git a/controller/controller.py b/controller/controller.py index 1b6ea8a..df0dc50 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -24,6 +24,7 @@ def millis(): enabled = 0 activeBeacon = 1 lastBeaconRead = millis() +calibrate_accel = 0 async def bluetooth_receive_handler(BLE_DEVICE): global lastBeaconRead @@ -149,13 +150,21 @@ async def cmd_handler(): if (get_key_state("Key.shift")): boost = 1 + + if get_key_state("u"): + calibrate_accel = 1 + elif get_key_state("j"): + calibrate_accel = 2 + else: + calibrate_accel = 0 - robotcmd = f"{enabled}{drivecmd}{robottuning}{boost}00" + robotcmd = f"{enabled}{drivecmd}{robottuning}{boost}{calibrate_accel}0" + # print(robotcmd) irbeaconcmd = f"{enabled}{activeBeacon}{irbeacontuning}000" await asyncio.sleep(0.05) async def main(): - # await asyncio.gather(ir_beacon_switcher(), cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(oreo, True), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(oreo), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) - await asyncio.gather(ir_beacon_switcher(), cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(oreo, True), bluetooth_comm_handler(hockey_puck, True), bluetooth_receive_handler(hockey_puck), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(oreo), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) + await asyncio.gather(ir_beacon_switcher(), cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(oreo, True), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(oreo), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) + # await asyncio.gather(ir_beacon_switcher(), cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(oreo, True), bluetooth_comm_handler(hockey_puck, True), bluetooth_receive_handler(hockey_puck), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(oreo), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) asyncio.run(main()) From 22e5ee8a305b16334afe1887d654c7803b9ac723 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Fri, 19 Apr 2024 17:59:40 -0700 Subject: [PATCH 37/48] redid controls, too easy to accidentally calibrate while disabled --- Robot Code/src/main.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Robot Code/src/main.cpp b/Robot Code/src/main.cpp index 8c0a003..6396b22 100644 --- a/Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -206,17 +206,7 @@ void loop() String msg = "drivpwr : " + String(tank_drive_parameters.drive) + " turnpwr : " + String(tank_drive_parameters.turn); laptop.send(msg); } - } - - driveMotors.l_motor_write(-lmotorpwr); - driveMotors.r_motor_write(rmotorpwr); - toggleLeds(WHITE, BLACK, 500); - - } else { // Currently disabled - toggleLeds(RED, GREEN, 500); - driveMotors.set_both_motors(0); - EVERY_N_MILLIS(100) { if (laptop_packetBuffer[4] == '1') { laptop.send("Calibrating AccelZ with Robot Upwards (~ -9.8)"); calibrateAccel(1); @@ -226,6 +216,14 @@ void loop() } } + driveMotors.l_motor_write(-lmotorpwr); + driveMotors.r_motor_write(rmotorpwr); + toggleLeds(WHITE, BLACK, 500); + + } else { // Currently disabled + toggleLeds(RED, GREEN, 500); + driveMotors.set_both_motors(0); + EVERY_N_SECONDS(1) { laptop.send("SOC: " + String(get3sSOC()) + " % " + String(getAccelZ())); } From 822524319b8975ed8bf1b923541301cfad062562 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Fri, 19 Apr 2024 19:44:52 -0700 Subject: [PATCH 38/48] updated power settings for oreo --- Robot Code/src/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Robot Code/src/main.cpp b/Robot Code/src/main.cpp index 6396b22..5474bee 100644 --- a/Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -17,8 +17,8 @@ Drive_Motors driveMotors = Drive_Motors(); melty oreo = melty(); struct melty_parameters { - int rot = 80; - int tra = 80; + int rot = 210; + int tra = 270; float per = 0.5; int invert = 1; } melty_parameters; From 646bc219896ebd6e20290ce8cd123c895be2d658 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Tue, 4 Jun 2024 12:45:53 -0700 Subject: [PATCH 39/48] starting to revise the code, somewhat working code for oreo v2 --- .DS_Store | Bin 6148 -> 10244 bytes Robot Code/include/pin_definitions.h | 8 ++ Robot Code/lib/.DS_Store | Bin 6148 -> 8196 bytes .../lib/Battery_Monitor/Battery_Monitor.h | 2 +- Robot Code/lib/Drive_Motors/Drive_Motors.cpp | 4 +- Robot Code/lib/LEDHandler/LEDHandler.h | 2 +- Robot Code/lib/Melty/melty.cpp | 23 ++++- Robot Code/lib/Melty/melty.h | 10 ++- .../lib/Photo_Transistors/Photo_Transistors.h | 40 +++++++++ Robot Code/lib/adxl375/adxl375.cpp | 69 --------------- Robot Code/lib/adxl375/adxl375.h | 16 ---- Robot Code/platformio.ini | 1 - Robot Code/src/main.cpp | 83 +++++++++--------- .../__pycache__/bluetooth.cpython-311.pyc | Bin 4982 -> 4982 bytes controller/controller.py | 5 +- 15 files changed, 121 insertions(+), 142 deletions(-) create mode 100644 Robot Code/include/pin_definitions.h create mode 100644 Robot Code/lib/Photo_Transistors/Photo_Transistors.h delete mode 100644 Robot Code/lib/adxl375/adxl375.cpp delete mode 100644 Robot Code/lib/adxl375/adxl375.h diff --git a/.DS_Store b/.DS_Store index 44d37b0de32debf78f4a44839dd5134e08dd84a6..2df912dd545ea3a48bc3ee4dad9152d63690606a 100644 GIT binary patch literal 10244 zcmeHMTWl0n7(V~B&@=7OPNBeX-NCA8S)pt%P=T=BwqTLcrn_ZZK$hK|kxrP-l$qHr zTG6UcN{F{;j8PNggAy+f#tTuS4+cdae9(Hq7!!>TUZVJF;)DO0GfPS@_&^L^<}~O0 z^Z)1n&+Pf;|IYTGC4@ktplu{XC4^AyOv(}tvoto(`iv$7mIWvQ;|VcHk?5M6H*@w3 z@6Zq;5F!vF5F!vF5F+qTKmfn9S))~4hGU38h(L(I1q9gnpkik-9>@tU{i6d1zXU*8 zj%L5%HP!)sj2g&zASbx=Lvc-BJwOl&L5Tr89OqT$jxrv|2`+g!0Uk~uxHAM53ix-& zIMsJ25a%)+Lj*zuW+K39_X^TZ9Fix)Q|ouzWX_dnwuwbNbg+que>!n^fzKU8(oOo2 zs|lRQa_8KbJDM}?9EP9Hjhcq*H8y?@LRtB|`4zN+R?%CM!}eIx%X$Sjk#+VnEoIwg zu5_Kv?KSkVj9k}YTVB?%bjJ@cG#yD=aT;)ym{B&# z`QWLf1EogvD-Bq4ODkqltm!fVn{{tfftuC9bc-FFQe_P#N_|zLlvr6MEl^Sesl90_ zBQGo>cX~%X!?xVLy5pkE$TcoRjoWp! z)wp@bHqAcUHP|vkgPwlOV{f+-V=HRhf3jNoQmZriT3Q~4}C$Zotl>w3MTx>fo}+m$X#_qoGaNADfO^R5)p zO{xFz(9S}-G{dul$<;~^r9*77c1^=|7=XQfijt9K!zvnhy5ghbhphYwrL?~E2OMPj6r>?Vis#2P0j$>Zc1@+vt)-Y1`tZ^)12Joy~}=7R{8Pz80c z7?y$pYoGzv!40q;wm>Iz!4Bw${cr#dLIyM#0SiXqC=}r?n1FlWUbqh)hDYF0cnqF~ z=iqrb1+Tzc@HV^y@50CM1$+tT;CuK5eueV_2$e#WAPGx^h!7K+g%+Vz*d)Y-Zefp* z<{w-r9pXc!9R~J6lyk8u_<|Ciot;gQoj2|5-u3Tc)46KSpT$d-s*F^dx3yo$KAxPM;OEG9L zoypDwZlqL6P2UrH*REnegq=0!H^HSJf`k7$9;N>}9u@XF{yOZ;H>fcE>v&+B;L;CA aA3q!=f1Cfe{}>R~{|J9r|C9f5{r@L?D@d9E delta 117 zcmZn(XfcprU|?W$DortDU=RQ@Ie-{Mvv5r;6q~50$jH4hU^g=(_hude4Q37_19Ke( z6BEP9Ho~Qok4c6tX6N7#WCkh+0s(Fy;R@2ZvG6(JnZpbK DiLMgp diff --git a/Robot Code/include/pin_definitions.h b/Robot Code/include/pin_definitions.h new file mode 100644 index 0000000..f828276 --- /dev/null +++ b/Robot Code/include/pin_definitions.h @@ -0,0 +1,8 @@ +#define TOP_PHOTO_TRANSISTOR 11 +#define BOTTOM_PHOTO_TRANSISTOR 12 + +#define LEFT_MOTOR GPIO_NUM_45 +#define RIGHT_MOTOR GPIO_NUM_1 + +#define LEFT_MOTOR_CHANNEL RMT_CHANNEL_2 +#define RIGHT_MOTOR_CHANNEL RMT_CHANNEL_1 \ No newline at end of file diff --git a/Robot Code/lib/.DS_Store b/Robot Code/lib/.DS_Store index d554410ea1e76a3a0518728b4955f411e4cfd04e..1d1363ef8d642642e0be5139f728fa7ca01b8e5e 100644 GIT binary patch literal 8196 zcmeHMU2GIp6u#fIz@2uWQz$U}cCjk4tWXM6DiF8Z7AO|l(3ZA3 z(W*~Mh=0)-qb9}&CH_1Ze~21=Fev)qgVrC6G12(oPZVEGeDK`4vlS?PP!nV0+|9k` z-gECcbI+OY+`DI&F@|KpT+5ij7~@n0)hnsFLJ{+#PAW>snL!jJ&sdtd%w!I;SZ>nl zh!J@p@<8N)$ODlFA`ko*JV1B0D13^0U(QBtu^C3o6&_qbbBn^%ZYJwDi zXgRTiLVd~u0-0Dy6CoXwG!P}7GJ8NMiZI216sP%^SWYw%(lJS;ID-^t2%n5FLqYI# zl8Xh)84{94ZRCN-1Cu=$=X>P+kSEWZu(g#<9Ut{si1V;rqup0OR@qxL?24< zGb8?R#_=*Ve@!^o!ciyy`(Tx>Zdb% zY-=Q?*R*^aI%*Il)z~rgUR;w5ho{9&2pccp@Gf zZEkFc$JVcFI&nhf^Xt}Z+MPP;47uKYV)Jwsgl7huEyok>N$OqEHpefvO54inOSXMk zvQ4e1)MgvKeZ717wUj=$NJexG=WWmR_gFcfXiBg0$=Hsp=XU2TG2fr{oWhXnr}XM< z+Q}B2blz%loF4l&i+ELjHs?7`x98hJ&EP=ZI-VDElcomt`d%*I8YmmENxxjB7kq2C zMM~|W)kdoQnh6tS2kJ|67A{?JP5qkn&0CUNckCLk(yQmrtI@Q*DeL0A@l(1u9X4`B(@8D^QPBxE`yq32kV{R_w+;?8gD5U}6X^hH(r<+=(&VjeBq} z9>T+T1drk=Jd5XW8ZYBbyoI;%4nD%?_yXtg9e&0yxS&9(P%0HoS)kM^jmmmugVLnj zpd^$IWtY+~H?EYTX|QBr$R8j&RWyYsh4Rdmqp5b=_H7+IrcX4TpCjXE{(^-u5mXzu zv`nB+g0+Oti*X-7{gMC)pdR4ja+n&67B5-4OsnmqSg?*y6`J}i5tn>itX^@IR;$mT zXpzHfSJH4MMT#7*ir3ca3I(VfHmt4H^fC%RHIUs9ry;^Y^NpIOms23B<4YDTHne40 zLREDZEKIhN8WC3i6yWFCx9ofNEBl=QJ_qv%)+^C~CTu1+??N|v(2G8T^g(2h4PfmN zsB`dfJMJK0-$l?qiBq^A58y#OjwkSB0PoXy9xvcUyoT3t25;b9oW=V%hY#=}KEYS` z20vZ~&P{$ODlF{(=Wk z-qzEWB!Q`pHIZw_s2-#$idZ)$X&^#Pkd6}#(s7~_e;86fMoWcEBBW!I5{Jsa{zJf@ UU$p=Jx9RBqkM92fz0 int getPerc(int min, int max, int val) { - if (val == 0) // Switch not on + if (val < 100) // Switch not on return -1; int perc = 100 * (val - min) / double(max - min); if (perc > 100) diff --git a/Robot Code/lib/Drive_Motors/Drive_Motors.cpp b/Robot Code/lib/Drive_Motors/Drive_Motors.cpp index 9d4291d..76a8941 100644 --- a/Robot Code/lib/Drive_Motors/Drive_Motors.cpp +++ b/Robot Code/lib/Drive_Motors/Drive_Motors.cpp @@ -2,13 +2,13 @@ #include void Drive_Motors::init_motors() { - rmot.install(GPIO_NUM_7, RMT_CHANNEL_1); + rmot.install(GPIO_NUM_1, RMT_CHANNEL_1); rmot.init(); rmot.setReversed(false); rmot.set3DMode(true); rmot.throttleArm(); // <--- Super important!!!; - lmot.install(GPIO_NUM_8, RMT_CHANNEL_2); + lmot.install(GPIO_NUM_45, RMT_CHANNEL_2); lmot.init(); lmot.setReversed(false); lmot.set3DMode(true); diff --git a/Robot Code/lib/LEDHandler/LEDHandler.h b/Robot Code/lib/LEDHandler/LEDHandler.h index 2697011..0256911 100644 --- a/Robot Code/lib/LEDHandler/LEDHandler.h +++ b/Robot Code/lib/LEDHandler/LEDHandler.h @@ -4,7 +4,7 @@ #ifdef IR_BEACON #define LEDPIN 5 #else - #define LEDPIN 4 + #define LEDPIN 46 #endif enum Colors { diff --git a/Robot Code/lib/Melty/melty.cpp b/Robot Code/lib/Melty/melty.cpp index 4e58892..4cee4e3 100644 --- a/Robot Code/lib/Melty/melty.cpp +++ b/Robot Code/lib/Melty/melty.cpp @@ -40,10 +40,8 @@ bool melty::update() { void melty::computeTimings() { unsigned long rotation_period; ledRPM = (us_per_min)/(period_micros_calc.getMinVal()); - if (abs(ledRPM - accelRPM) < 50) - rotation_period = period_micros_calc.getMinVal(); - else - rotation_period = acccel_period; + rotation_period = period_micros_calc.getMinVal(); + unsigned long center_of_beacon = currentPulse + time_seen_beacon/2; // This should ideally be centered on the beacon @@ -64,6 +62,9 @@ void melty::computeTimings() { startDrive[index] = centerOfDrivePulse - deltaDriveTiming + offset + rotation_period*i; endDrive[index] = centerOfDrivePulse + deltaDriveTiming + offset + rotation_period*i; + + startDriveInverse[index] = startDrive[index] + rotation_period/2; + endDriveInverse[index] = endDrive[index] + rotation_period/2; } timingToggle = !timingToggle; // Toggle it so that next iteration uses different variables @@ -84,6 +85,20 @@ bool melty::translate() { // Returns whether or not robot should translate now return 0; } +bool melty::translateInverse() { // Returns whether or not robot should translate now + unsigned long currentTime = micros(); + + if (percentageOfRotation != 0) { + for (int i = 0; i < TRANSLATE_TIMINGS_SIZE; i++) { + if (currentTime > startDriveInverse[i] && currentTime < endDriveInverse[i]) + return 1; + } + return 0; + } + else + return 0; +} + bool melty::isBeaconSensed(bool currentReading) { // for (int i = 0; i < IRLedDataSize; i++) // USBSerial.print(IRLedReadings[i]); diff --git a/Robot Code/lib/Melty/melty.h b/Robot Code/lib/Melty/melty.h index 8f158df..10a7666 100644 --- a/Robot Code/lib/Melty/melty.h +++ b/Robot Code/lib/Melty/melty.h @@ -1,9 +1,8 @@ #include -#include #include -#define TOP_IR_PIN 9 -#define BOTTOM_IR_PIN 10 +#define TOP_IR_PIN 41 +#define BOTTOM_IR_PIN 42 #define IRLedDataSize 40 // Size of our Ring Buffer that will hold the IR Led data #define TRANSLATE_TIMINGS_SIZE 6 @@ -15,8 +14,8 @@ class melty { bool isBeaconSensed(bool currentReading); void computeTimings(); bool translate(); + bool translateInverse(); int ledRPM = 0; - int accelRPM = 0; unsigned long acccel_period = 1; int deg = 0; float percentageOfRotation = 0; @@ -44,6 +43,9 @@ class melty { long startDrive[TRANSLATE_TIMINGS_SIZE] = {0}; long endDrive[TRANSLATE_TIMINGS_SIZE] = {0}; + long startDriveInverse[TRANSLATE_TIMINGS_SIZE] = {0}; + long endDriveInverse[TRANSLATE_TIMINGS_SIZE] = {0}; + bool timingToggle = 0; diff --git a/Robot Code/lib/Photo_Transistors/Photo_Transistors.h b/Robot Code/lib/Photo_Transistors/Photo_Transistors.h new file mode 100644 index 0000000..97ccb1e --- /dev/null +++ b/Robot Code/lib/Photo_Transistors/Photo_Transistors.h @@ -0,0 +1,40 @@ +#include + +class robotOrientation{ + + private: + bool curIsFlipped = 0; + + + bool lastIsFlipped = 0; + unsigned long lastTransition = millis(); + int topPin; + int bottomPin; + + const int delayTimeMS = 500; + + public: + bool isFlippedResult = 0; + + robotOrientation(int topPin, int bottomPin) : topPin(topPin), bottomPin(bottomPin) {} + + + bool checkIsFlipped() { + curIsFlipped = analogRead(bottomPin) - analogRead(topPin) > 0 ? 1 : 0; + + if (curIsFlipped == 1 && lastIsFlipped == 0) + lastTransition = millis(); + if (curIsFlipped == 1 && millis() - lastTransition > delayTimeMS) + isFlippedResult = 1; + + if (curIsFlipped == 0 && lastIsFlipped == 1) + lastTransition = millis(); + if (curIsFlipped == 0 && millis() - lastTransition > delayTimeMS) + isFlippedResult = 0; + + lastIsFlipped = curIsFlipped; + return isFlippedResult; + } +}; + + diff --git a/Robot Code/lib/adxl375/adxl375.cpp b/Robot Code/lib/adxl375/adxl375.cpp deleted file mode 100644 index bc94038..0000000 --- a/Robot Code/lib/adxl375/adxl375.cpp +++ /dev/null @@ -1,69 +0,0 @@ -#include - -Adafruit_ADXL375 accel = Adafruit_ADXL375(12345); -sensors_event_t event; - -long zAccelValues[zAccelValuesSize] = {0}; - -ringBuffer zRingBuffer = ringBuffer(zAccelValues, zAccelValuesSize, 1); - -void init_adxl375() { - Wire.begin(5,6); - while (!accel.begin()) { - delay(100); - USBSerial.println("Failed to find ADXL375 chip"); - } -} - -void updateAccel() { - accel.getEvent(&event); -} - -float getAccelY() { - return (abs(event.acceleration.y)); -} - -float getAccelZ() { - return (event.acceleration.z); -} - -void calibrateAccel(bool topside) { - accel.setTrimOffsets(0, 0, 0); - delay(1000); - - int16_t x, y, z; - x = accel.getX(); - y = accel.getY(); - z = accel.getZ(); - - // the trim offsets are in 'multiples' of 4, we want to round, so we add 2 - if (topside) { - accel.setTrimOffsets(-(x+2)/4, - -(y+2)/4, - -(z+20+2)/4); - } else { - accel.setTrimOffsets(-(x+2)/4, - -(y+2)/4, - -(z-20+2)/4); // Z should be '20' at 1g (49mg per bit) - } -} - -bool isFlipped() { - static bool isFlipped = 0; - long accelValueThreshold = 6; - zRingBuffer.update(getAccelZ()); - - if (!isFlipped) { - for (int i = 0; i < zAccelValuesSize; i++) // Negative values mean robot is NOT flipped - if (zAccelValues[i] < -accelValueThreshold) - return 0; - } else { - for (int i = 0; i < zAccelValuesSize; i++) - if (zAccelValues[i] > accelValueThreshold) - return 1; - } - - isFlipped = !isFlipped; - return isFlipped; -} - diff --git a/Robot Code/lib/adxl375/adxl375.h b/Robot Code/lib/adxl375/adxl375.h deleted file mode 100644 index e3400ef..0000000 --- a/Robot Code/lib/adxl375/adxl375.h +++ /dev/null @@ -1,16 +0,0 @@ -#include -#include - -#define zAccelValuesSize 20 - -void init_adxl375(); - -void updateAccel(); - -void calibrateAccel(bool topside); - -float getAccelY(); - -float getAccelZ(); - -bool isFlipped(); \ No newline at end of file diff --git a/Robot Code/platformio.ini b/Robot Code/platformio.ini index 45e92ab..fa9dd57 100644 --- a/Robot Code/platformio.ini +++ b/Robot Code/platformio.ini @@ -20,4 +20,3 @@ lib_deps = h2zero/NimBLE-Arduino@^1.4.1 freenove/Freenove WS2812 Lib for ESP32@^1.0.6 moddingear/ESP32 ESC@^1.0.0 - adafruit/Adafruit ADXL375@^1.1.2 diff --git a/Robot Code/src/main.cpp b/Robot Code/src/main.cpp index 5474bee..2231ade 100644 --- a/Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -1,26 +1,27 @@ #include -#include #include #include #include #include #include +#include +#include const int packSize = 6; char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; const int headings[] = {0, 45, 90, 135, 180, 225, 270, 315}; bool wasMeltying = false; int slowDownSpeed = 200; - BLE_Uart laptop = BLE_Uart(laptop_packetBuffer, packSize); Drive_Motors driveMotors = Drive_Motors(); +robotOrientation myPTs = robotOrientation(TOP_PHOTO_TRANSISTOR, BOTTOM_PHOTO_TRANSISTOR); melty oreo = melty(); struct melty_parameters { - int rot = 210; - int tra = 270; + int rot = 80; + int tra = 80; float per = 0.5; - int invert = 1; + int boost = 50; } melty_parameters; struct tank_drive_parameters { @@ -37,7 +38,6 @@ void setup() driveMotors.init_motors(); // <- This needs to be init first or else something with RMT doesnt work.... init_led(); setLeds(ORANGE); - init_adxl375(); driveMotors.arm_motors(); laptop.init_ble("Oreo"); setLeds(BLACK); @@ -46,20 +46,14 @@ void setup() void loop() { if (laptop.isConnected()) { - EVERY_N_MILLIS(25) { - updateAccel(); - oreo.acccel_period =(2 * 3.14 * 20.97 * 1000000) / (sqrt(getAccelY() * 1000 * 20.97)); - oreo.accelRPM = (oreo.us_per_min)/(oreo.acccel_period); - } EVERY_N_MILLIS(50) { - if (!isFlipped()) { + myPTs.checkIsFlipped(); + if (!myPTs.isFlippedResult) { // Not flipped oreo.useTopIr = 1; driveMotors.flip_motors = 0; - melty_parameters.invert = 1; } else { oreo.useTopIr = 0; driveMotors.flip_motors = 1; - melty_parameters.invert = -1; } } if (laptop_packetBuffer[0] == '1') { // Currently enabled and meltybraining!!! @@ -68,17 +62,32 @@ void loop() laptop.send("seen"); } } + + int boostVal = 0; + if (laptop_packetBuffer[3] == '1') + boostVal = melty_parameters.boost; + + int adjRotValue = melty_parameters.rot + boostVal; + int adjTransValue = melty_parameters.tra + boostVal; + + if (myPTs.isFlippedResult) { // Since we need to spin the opposite way for tooth engagement + adjRotValue *= -1; + // adjTransValue *= -1; + } if (oreo.translate()) { - driveMotors.l_motor_write(melty_parameters.invert * (melty_parameters.rot - melty_parameters.invert * melty_parameters.tra)); - driveMotors.r_motor_write(melty_parameters.invert * (melty_parameters.rot + melty_parameters.invert * melty_parameters.tra)); + driveMotors.l_motor_write(adjRotValue - adjTransValue); + driveMotors.r_motor_write(adjRotValue + adjTransValue); + } else if (oreo.translateInverse()) { + driveMotors.l_motor_write(adjRotValue + adjTransValue); + driveMotors.r_motor_write(adjRotValue - adjTransValue); } else { - driveMotors.set_both_motors(melty_parameters.invert * (melty_parameters.rot)); - } + driveMotors.set_both_motors((adjRotValue)); + } int drivecmd = laptop_packetBuffer[1] - '0'; if (drivecmd > 0 && drivecmd < 9) { // 1,2,3,4,5,6,7,8 - if (melty_parameters.invert == 1) + if (myPTs.isFlippedResult == false) drivecmd = 8 - drivecmd; else drivecmd = drivecmd - 1; @@ -92,7 +101,7 @@ void loop() } EVERY_N_SECONDS(1) { // DEBUGGIN!!!! - String msg = "RPM (IR): " + String(oreo.ledRPM) + " RPM (Accel) : " + String(oreo.accelRPM); + String msg = "RPM : " + String(oreo.ledRPM); laptop.send(msg); } @@ -127,17 +136,17 @@ void loop() wasMeltying = 1; } else if (laptop_packetBuffer[0] == '2') { // Tank driving mode! - if (wasMeltying) { // Was previously meltybraining, we need to slowdown - unsigned long timeout = millis(); - while (getAccelY() > 2 && millis() - timeout < 2000) { - updateAccel(); - driveMotors.set_both_motors(-slowDownSpeed * melty_parameters.invert); - toggleLeds(WHITE, RED, 150); - if (laptop_packetBuffer[0] != '2') - break; - } - wasMeltying = 0; - } + // if (wasMeltying) { // Was previously meltybraining, we need to slowdown + // unsigned long timeout = millis(); + // while (getAccelY() > 2 && millis() - timeout < 2000) { + // updateAccel(); + // driveMotors.set_both_motors(-slowDownSpeed * melty_parameters.invert); + // toggleLeds(WHITE, RED, 150); + // if (laptop_packetBuffer[0] != '2') + // break; + // } + // wasMeltying = 0; + // } int lmotorpwr = 0; int rmotorpwr = 0; @@ -206,26 +215,18 @@ void loop() String msg = "drivpwr : " + String(tank_drive_parameters.drive) + " turnpwr : " + String(tank_drive_parameters.turn); laptop.send(msg); } - - if (laptop_packetBuffer[4] == '1') { - laptop.send("Calibrating AccelZ with Robot Upwards (~ -9.8)"); - calibrateAccel(1); - } else if (laptop_packetBuffer[4] == '2') { - laptop.send("Calibrating AccelZ with Robot Downwards (~ 9.8)"); - calibrateAccel(0); - } } driveMotors.l_motor_write(-lmotorpwr); driveMotors.r_motor_write(rmotorpwr); toggleLeds(WHITE, BLACK, 500); - } else { // Currently disabled + } else { // Currently DISABLED toggleLeds(RED, GREEN, 500); driveMotors.set_both_motors(0); EVERY_N_SECONDS(1) { - laptop.send("SOC: " + String(get3sSOC()) + " % " + String(getAccelZ())); + laptop.send("SOC: " + String(get3sSOC()) + " %"); } } } else { // Currently DISCONNECTED diff --git a/controller/__pycache__/bluetooth.cpython-311.pyc b/controller/__pycache__/bluetooth.cpython-311.pyc index 35c87ab79fa9adc40d0e266a13fbffe308bc0fdb..177c445d3a20a4373a63e7f9d9c41f9a5d252574 100644 GIT binary patch delta 20 acmeyS_Dzj@IWI340}xo%2XExg76t%7Jq1+& delta 20 acmeyS_Dzj@IWI340}#wTBe9V?TNnUCnFY!K diff --git a/controller/controller.py b/controller/controller.py index df0dc50..e718216 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -12,7 +12,7 @@ def millis(): ir_beacon_2 = BLE_UART(peripheral_name='Beac 2', address = '642D48B0-0DA1-AB00-2DDD-B639F5353E80') ir_beacon_1 = BLE_UART(peripheral_name='Beac 1', address = '37E54CED-FA64-96E8-C84C-8528ADB5AC13') -oreo = BLE_UART(peripheral_name='Oreo', address = '599CA2EF-37D8-78BE-C3A8-C8DC5CEE9838') +oreo = BLE_UART(peripheral_name='Oreo', address = '168B3E4A-21A9-918B-F28C-8D26D656012C') hockey_puck = BLE_UART(peripheral_name='Hockey Puck', address = 'DB644862-A8E2-33B5-1D6E-794F2EEA94E8') keyboard_thread = threading.Thread(target=lambda: Listener(on_press=on_press, on_release=on_release).start()) @@ -58,7 +58,7 @@ async def ir_beacon_switcher(): await asyncio.sleep(0.1) if (enabled != 1): lastBeaconRead = millis() + 2000 # Add some time so the beacon doesnt switch right after enabling melty brain mode - if (enabled == 1 and millis() - lastBeaconRead > 1000 and ir_beacon_2.isConnected == True and ir_beacon_1.isConnected): + if (enabled == 1 and millis() - lastBeaconRead > 1000 and ir_beacon_2.isConnected and ir_beacon_1.isConnected): toggleBeacon() def toggleBeacon(): @@ -159,7 +159,6 @@ async def cmd_handler(): calibrate_accel = 0 robotcmd = f"{enabled}{drivecmd}{robottuning}{boost}{calibrate_accel}0" - # print(robotcmd) irbeaconcmd = f"{enabled}{activeBeacon}{irbeacontuning}000" await asyncio.sleep(0.05) From 6fff39573d3644f53ac91cabff59748a9d4aa0f4 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Sat, 22 Jun 2024 21:25:22 -0700 Subject: [PATCH 40/48] got spiffs file working, trying to get led indication working --- Robot Code/data/motor_settings.txt | 2 + Robot Code/include/pin_definitions.h | 11 ++- .../lib/Battery_Monitor/Battery_Monitor.h | 4 +- Robot Code/lib/Drive_Motors/Drive_Motors.cpp | 6 +- Robot Code/lib/Drive_Motors/Drive_Motors.h | 8 +- Robot Code/lib/LEDHandler/LEDHandler.cpp | 76 +++++++++++-------- Robot Code/lib/LEDHandler/LEDHandler.h | 6 ++ Robot Code/lib/Melty/melty.cpp | 6 +- Robot Code/lib/Melty/melty.h | 8 +- .../lib/databasehandler/databasehandler.cpp | 50 ++++++++++++ .../lib/databasehandler/databasehandler.h | 25 ++++++ Robot Code/platformio.ini | 1 + Robot Code/src/main.cpp | 65 ++++++++++++---- 13 files changed, 207 insertions(+), 61 deletions(-) create mode 100644 Robot Code/data/motor_settings.txt create mode 100644 Robot Code/lib/databasehandler/databasehandler.cpp create mode 100644 Robot Code/lib/databasehandler/databasehandler.h diff --git a/Robot Code/data/motor_settings.txt b/Robot Code/data/motor_settings.txt new file mode 100644 index 0000000..195b657 --- /dev/null +++ b/Robot Code/data/motor_settings.txt @@ -0,0 +1,2 @@ +{"rot" : "80", "tra" : "80", "per" : "0.5", "boost" : "50"} +{"drive" : "80", "turn" : "80", "boost" : "40"} \ No newline at end of file diff --git a/Robot Code/include/pin_definitions.h b/Robot Code/include/pin_definitions.h index f828276..258d9c9 100644 --- a/Robot Code/include/pin_definitions.h +++ b/Robot Code/include/pin_definitions.h @@ -1,8 +1,13 @@ #define TOP_PHOTO_TRANSISTOR 11 #define BOTTOM_PHOTO_TRANSISTOR 12 -#define LEFT_MOTOR GPIO_NUM_45 -#define RIGHT_MOTOR GPIO_NUM_1 +#define TOP_IR_PIN 41 +#define BOTTOM_IR_PIN 42 + +#define LEFT_MOTOR_PIN GPIO_NUM_45 +#define RIGHT_MOTOR_PIN GPIO_NUM_1 #define LEFT_MOTOR_CHANNEL RMT_CHANNEL_2 -#define RIGHT_MOTOR_CHANNEL RMT_CHANNEL_1 \ No newline at end of file +#define RIGHT_MOTOR_CHANNEL RMT_CHANNEL_1 + +#define BAT_PIN 18 diff --git a/Robot Code/lib/Battery_Monitor/Battery_Monitor.h b/Robot Code/lib/Battery_Monitor/Battery_Monitor.h index 4bbac92..1481ef6 100644 --- a/Robot Code/lib/Battery_Monitor/Battery_Monitor.h +++ b/Robot Code/lib/Battery_Monitor/Battery_Monitor.h @@ -12,8 +12,8 @@ int getPerc(int min, int max, int val) { } int get3sSOC() { - int max = 4096-20; - int min = 3300; + int max = 4096-50; + int min = 3000; return getPerc(min, max ,analogRead(18)); } diff --git a/Robot Code/lib/Drive_Motors/Drive_Motors.cpp b/Robot Code/lib/Drive_Motors/Drive_Motors.cpp index 76a8941..a3bb3b4 100644 --- a/Robot Code/lib/Drive_Motors/Drive_Motors.cpp +++ b/Robot Code/lib/Drive_Motors/Drive_Motors.cpp @@ -1,14 +1,16 @@ #include "Drive_Motors.h" #include +Drive_Motors::Drive_Motors(gpio_num_t l_motor_pin, rmt_channel_t l_motor_channel, gpio_num_t r_motor_pin, rmt_channel_t r_motor_channel) : l_motor_pin(l_motor_pin), l_motor_channel(l_motor_channel), r_motor_pin(r_motor_pin), r_motor_channel(r_motor_channel) {} + void Drive_Motors::init_motors() { - rmot.install(GPIO_NUM_1, RMT_CHANNEL_1); + rmot.install(r_motor_pin, r_motor_channel); rmot.init(); rmot.setReversed(false); rmot.set3DMode(true); rmot.throttleArm(); // <--- Super important!!!; - lmot.install(GPIO_NUM_45, RMT_CHANNEL_2); + lmot.install(l_motor_pin, l_motor_channel); lmot.init(); lmot.setReversed(false); lmot.set3DMode(true); diff --git a/Robot Code/lib/Drive_Motors/Drive_Motors.h b/Robot Code/lib/Drive_Motors/Drive_Motors.h index 0f8c097..4aead87 100644 --- a/Robot Code/lib/Drive_Motors/Drive_Motors.h +++ b/Robot Code/lib/Drive_Motors/Drive_Motors.h @@ -3,7 +3,7 @@ class Drive_Motors { public: - Drive_Motors() {} + Drive_Motors(gpio_num_t l_motor_pin, rmt_channel_t l_motor_channel, gpio_num_t r_motor_pin, rmt_channel_t r_motor_channel); void init_motors(); void arm_motors(); void l_motor_write(int value); @@ -15,6 +15,12 @@ class Drive_Motors { DShotESC rmot; DShotESC lmot; + gpio_num_t l_motor_pin = GPIO_NUM_NC; + gpio_num_t r_motor_pin = GPIO_NUM_NC; + + rmt_channel_t l_motor_channel; + rmt_channel_t r_motor_channel; + int kickstart_value = 8; int l_motor_value = 0; int r_motor_value = 0; diff --git a/Robot Code/lib/LEDHandler/LEDHandler.cpp b/Robot Code/lib/LEDHandler/LEDHandler.cpp index 9d2ab53..b5740f9 100644 --- a/Robot Code/lib/LEDHandler/LEDHandler.cpp +++ b/Robot Code/lib/LEDHandler/LEDHandler.cpp @@ -10,43 +10,53 @@ Colors lastColor2; Freenove_ESP32_WS2812 strip = Freenove_ESP32_WS2812(2, LEDPIN, 0, TYPE_GRB); void init_led() { - strip.begin(); - strip.setBrightness(100); + strip.begin(); + strip.setBrightness(100); } void setLeds(Colors color) { - static unsigned long lastLedShowing = 0; - if (millis() - lastLedShowing > 5) { - if (color == BLACK) - strip.setAllLedsColorData(0, 0, 0); - else if (color == WHITE) - strip.setAllLedsColorData(255, 255, 255); - else - strip.setAllLedsColorData(strip.hsv2rgb(color*30, 100, 100)); - strip.show(); - lastLedShowing = millis(); - } - + static unsigned long lastLedShowing = 0; + if (millis() - lastLedShowing > 5) { + + if (ledmode123 == BOTH) { + if (color == BLACK) + strip.setAllLedsColorData(0, 0, 0); + else if (color == WHITE) + strip.setAllLedsColorData(255, 255, 255); + else + strip.setAllLedsColorData(strip.hsv2rgb(color*30, 100, 100)); + } else { + if (color == BLACK) + strip.setLedColorData(ledmode123-1, 0, 0, 0); + else if (color == WHITE) + strip.setLedColorData(ledmode123-1, 255, 255, 255); + else + strip.setLedColorData(ledmode123-1, strip.hsv2rgb(color*30, 100, 100)); + } + strip.show(); + lastLedShowing = millis(); + } + } void toggleLeds(Colors color1, Colors color2, int delayMS) { - if (color1 != lastColor1 || color2 != lastColor2 || millis() - lastdelayToggle > delayMS + 50) { - lastdelayToggle = millis(); - ledToggleState = 0; - setLeds(color1); - } - currentDelayToggle = millis(); - - if (currentDelayToggle - lastdelayToggle > delayMS) { - ledToggleState = !ledToggleState; - lastdelayToggle = currentDelayToggle; - } - - if (ledToggleState) - setLeds(color1); - else - setLeds(color2); - - lastColor1 = color1; - lastColor2 = color2; + if (color1 != lastColor1 || color2 != lastColor2 || millis() - lastdelayToggle > delayMS + 50) { + lastdelayToggle = millis(); + ledToggleState = 0; + setLeds(color1); + } + currentDelayToggle = millis(); + + if (currentDelayToggle - lastdelayToggle > delayMS) { + ledToggleState = !ledToggleState; + lastdelayToggle = currentDelayToggle; + } + + if (ledToggleState) + setLeds(color1); + else + setLeds(color2); + + lastColor1 = color1; + lastColor2 = color2; } \ No newline at end of file diff --git a/Robot Code/lib/LEDHandler/LEDHandler.h b/Robot Code/lib/LEDHandler/LEDHandler.h index 0256911..3fa7df9 100644 --- a/Robot Code/lib/LEDHandler/LEDHandler.h +++ b/Robot Code/lib/LEDHandler/LEDHandler.h @@ -23,6 +23,12 @@ enum Colors { WHITE }; +enum ledmodes{ + BOTH, TOP, BOTTOM +}; + +int ledmode123 = BOTH; + void init_led(); void setLeds(Colors color); diff --git a/Robot Code/lib/Melty/melty.cpp b/Robot Code/lib/Melty/melty.cpp index 4cee4e3..d450f15 100644 --- a/Robot Code/lib/Melty/melty.cpp +++ b/Robot Code/lib/Melty/melty.cpp @@ -1,7 +1,7 @@ #include #include -melty::melty() { +melty::melty(int top_ir_pin, int bottom_ir_pin) : top_ir_pin(top_ir_pin), bottom_ir_pin(bottom_ir_pin) { period_micros_calc = ringBuffer(period_micros_calc_array, 5, 1); time_seen_beacon_calc = ringBuffer(time_seen_beacon_calc_array, TIME_SEEN_BEACON_ARRAY_SIZE, 0.7); } @@ -9,9 +9,9 @@ melty::melty() { bool melty::update() { bool curSeenIRLed; if (useTopIr) - curSeenIRLed = isBeaconSensed(!digitalRead(TOP_IR_PIN)); + curSeenIRLed = isBeaconSensed(!digitalRead(top_ir_pin)); else - curSeenIRLed = isBeaconSensed(!digitalRead(BOTTOM_IR_PIN)); + curSeenIRLed = isBeaconSensed(!digitalRead(bottom_ir_pin)); if (curSeenIRLed != lastSeenIRLed) if (curSeenIRLed) { // Activates on the rising edge of seeing the IR LED diff --git a/Robot Code/lib/Melty/melty.h b/Robot Code/lib/Melty/melty.h index 10a7666..e742bf9 100644 --- a/Robot Code/lib/Melty/melty.h +++ b/Robot Code/lib/Melty/melty.h @@ -1,15 +1,13 @@ #include #include -#define TOP_IR_PIN 41 -#define BOTTOM_IR_PIN 42 #define IRLedDataSize 40 // Size of our Ring Buffer that will hold the IR Led data #define TRANSLATE_TIMINGS_SIZE 6 #define TIME_SEEN_BEACON_ARRAY_SIZE 5 class melty { public: - melty(); + melty (int top_ir_pin, int bottom_ir_pin); bool update(); bool isBeaconSensed(bool currentReading); void computeTimings(); @@ -30,6 +28,10 @@ class melty { const int us_per_min = 60000000; private: + + int top_ir_pin = 0; + int bottom_ir_pin = 0; + bool lastSeenIRLed = 0; long period_micros = micros(); long time_seen_beacon = micros(); diff --git a/Robot Code/lib/databasehandler/databasehandler.cpp b/Robot Code/lib/databasehandler/databasehandler.cpp new file mode 100644 index 0000000..150af9b --- /dev/null +++ b/Robot Code/lib/databasehandler/databasehandler.cpp @@ -0,0 +1,50 @@ +#include + +database_handler::database_handler() {} + +void database_handler::updateFromDatabase() { + File databaseFile = SPIFFS.open("/motor_settings.txt", "r"); + + melty_param_string = databaseFile.readStringUntil('\n'); + tankdrive_param_string = databaseFile.readStringUntil('\n'); + + databaseFile.close(); +} + +void database_handler::storeMeltyParameters(int rot, int tra, float per, int boost) { + JsonDocument newSettings; + + newSettings["rot"] = rot; + newSettings["tra"] = tra; + newSettings["per"] = per; + newSettings["boost"] = boost; + + File newFile = SPIFFS.open("/motor_settings.txt", "w"); + + String newSettingsString; + serializeJson(newSettings, newSettingsString); + newFile.println(newSettingsString); + newFile.println(tankdrive_param_string); + + newFile.close(); + newSettings = 1; + updateFromDatabase(); +} + +void database_handler::storeTankParameters(int drive, int turn, int boost) { + JsonDocument newSettings; + + newSettings["drive"] = drive; + newSettings["turn"] = turn; + newSettings["boost"] = boost; + + File newFile = SPIFFS.open("/motor_settings.txt", "w"); + String newSettingsString; + serializeJson(newSettings, newSettingsString); + newFile.println(melty_param_string); + newFile.println(newSettingsString); + + newFile.close(); + newSettings = 1; + updateFromDatabase(); +} \ No newline at end of file diff --git a/Robot Code/lib/databasehandler/databasehandler.h b/Robot Code/lib/databasehandler/databasehandler.h new file mode 100644 index 0000000..9394003 --- /dev/null +++ b/Robot Code/lib/databasehandler/databasehandler.h @@ -0,0 +1,25 @@ +#include +#include +#include +#include + +using namespace std; + +class database_handler { + public: + String melty_param_string; + String tankdrive_param_string; + + bool newSettings = 1; + + database_handler(); + + void updateFromDatabase(); + + void storeMeltyParameters(int rot, int tra, float per, int boost); + + void storeTankParameters(int drive, int turn, int boost); + + private: + +}; \ No newline at end of file diff --git a/Robot Code/platformio.ini b/Robot Code/platformio.ini index fa9dd57..5782b16 100644 --- a/Robot Code/platformio.ini +++ b/Robot Code/platformio.ini @@ -20,3 +20,4 @@ lib_deps = h2zero/NimBLE-Arduino@^1.4.1 freenove/Freenove WS2812 Lib for ESP32@^1.0.6 moddingear/ESP32 ESC@^1.0.0 + bblanchon/ArduinoJson@^7.0.4 diff --git a/Robot Code/src/main.cpp b/Robot Code/src/main.cpp index 2231ade..6f6d5dc 100644 --- a/Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -6,6 +6,8 @@ #include #include #include +#include "SPIFFS.h" +#include const int packSize = 6; char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; @@ -13,21 +15,22 @@ const int headings[] = {0, 45, 90, 135, 180, 225, 270, 315}; bool wasMeltying = false; int slowDownSpeed = 200; BLE_Uart laptop = BLE_Uart(laptop_packetBuffer, packSize); -Drive_Motors driveMotors = Drive_Motors(); -robotOrientation myPTs = robotOrientation(TOP_PHOTO_TRANSISTOR, BOTTOM_PHOTO_TRANSISTOR); +Drive_Motors driveMotors = Drive_Motors(LEFT_MOTOR_PIN, LEFT_MOTOR_CHANNEL, RIGHT_MOTOR_PIN, RIGHT_MOTOR_CHANNEL); +robotOrientation myPTs = robotOrientation(TOP_PHOTO_TRANSISTOR, BOTTOM_PHOTO_TRANSISTOR);\ +database_handler motor_settings = database_handler(); -melty oreo = melty(); +melty oreo = melty(TOP_IR_PIN, BOTTOM_IR_PIN); struct melty_parameters { - int rot = 80; - int tra = 80; - float per = 0.5; - int boost = 50; + int rot; + int tra; + float per; + int boost; } melty_parameters; struct tank_drive_parameters { - int drive = 10; - int turn = 20; - int boost = 40; + int drive; + int turn; + int boost; } tank_drive_parameters; int tuningValue = 10; @@ -35,12 +38,17 @@ int tuningValue = 10; void setup() { USBSerial.begin(115200); + SPIFFS.begin(true); + + motor_settings.updateFromDatabase(); + driveMotors.init_motors(); // <- This needs to be init first or else something with RMT doesnt work.... init_led(); setLeds(ORANGE); driveMotors.arm_motors(); laptop.init_ble("Oreo"); setLeds(BLACK); + } void loop() @@ -51,11 +59,35 @@ void loop() if (!myPTs.isFlippedResult) { // Not flipped oreo.useTopIr = 1; driveMotors.flip_motors = 0; + // ledmode = TOP; } else { oreo.useTopIr = 0; driveMotors.flip_motors = 1; + // ledmode = BOTTOM; + } + + if (motor_settings.newSettings == true) { + JsonDocument melty_params_json; + deserializeJson(melty_params_json, motor_settings.melty_param_string); + + melty_parameters.rot = melty_params_json["rot"].as(); + melty_parameters.tra = melty_params_json["tra"].as(); + melty_parameters.per = melty_params_json["per"].as(); + melty_parameters.boost = melty_params_json["boost"].as(); + + JsonDocument tank_params_json; + deserializeJson(tank_params_json, motor_settings.tankdrive_param_string); + + tank_drive_parameters.drive = tank_params_json["drive"].as(); + tank_drive_parameters.turn = tank_params_json["turn"].as(); + tank_drive_parameters.boost = tank_params_json["boost"].as(); + + motor_settings.newSettings = false; + } } + + if (laptop_packetBuffer[0] == '1') { // Currently enabled and meltybraining!!! if (oreo.update()) { // If seen the LED EVERY_N_MILLIS(250) { @@ -71,8 +103,7 @@ void loop() int adjTransValue = melty_parameters.tra + boostVal; if (myPTs.isFlippedResult) { // Since we need to spin the opposite way for tooth engagement - adjRotValue *= -1; - // adjTransValue *= -1; + // adjRotValue *= -1; } if (oreo.translate()) { @@ -80,7 +111,7 @@ void loop() driveMotors.r_motor_write(adjRotValue + adjTransValue); } else if (oreo.translateInverse()) { driveMotors.l_motor_write(adjRotValue + adjTransValue); - driveMotors.r_motor_write(adjRotValue - adjTransValue); + driveMotors.r_motor_write(adjRotValue - adjTransValue); } else { driveMotors.set_both_motors((adjRotValue)); } @@ -130,6 +161,8 @@ void loop() if (laptop_packetBuffer[2] != '0') { String msg = "rotpwr : " + String(melty_parameters.rot) + " tranpwr : " + String(melty_parameters.tra) + " perc : " + String(melty_parameters.per); laptop.send(msg); + + motor_settings.storeMeltyParameters(melty_parameters.rot, melty_parameters.tra, melty_parameters.per, melty_parameters.boost); } } @@ -214,6 +247,8 @@ void loop() if (laptop_packetBuffer[2] != '0') { String msg = "drivpwr : " + String(tank_drive_parameters.drive) + " turnpwr : " + String(tank_drive_parameters.turn); laptop.send(msg); + + motor_settings.storeTankParameters(tank_drive_parameters.drive, tank_drive_parameters.turn, tank_drive_parameters.boost); } } @@ -226,10 +261,12 @@ void loop() driveMotors.set_both_motors(0); EVERY_N_SECONDS(1) { - laptop.send("SOC: " + String(get3sSOC()) + " %"); + // laptop.send("SOC: " + String(get3sSOC()) + " %"); + // laptop.send(motor_settings.tankdrive_param_string); } } } else { // Currently DISCONNECTED + // ledmode = BOTH; driveMotors.set_both_motors(0); toggleLeds(RED, BLACK, 500); } From d9bf3336a6dac035c28344c898025016c2e97f52 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Sat, 22 Jun 2024 22:01:22 -0700 Subject: [PATCH 41/48] individually addressing each led and save profiles working --- Robot Code/lib/LEDHandler/LEDHandler.cpp | 41 +++++++++++-------- Robot Code/lib/LEDHandler/LEDHandler.h | 4 +- .../lib/Photo_Transistors/Photo_Transistors.h | 15 ++++++- Robot Code/src/main.cpp | 9 ++-- 4 files changed, 43 insertions(+), 26 deletions(-) diff --git a/Robot Code/lib/LEDHandler/LEDHandler.cpp b/Robot Code/lib/LEDHandler/LEDHandler.cpp index b5740f9..35cf955 100644 --- a/Robot Code/lib/LEDHandler/LEDHandler.cpp +++ b/Robot Code/lib/LEDHandler/LEDHandler.cpp @@ -9,32 +9,39 @@ Colors lastColor2; Freenove_ESP32_WS2812 strip = Freenove_ESP32_WS2812(2, LEDPIN, 0, TYPE_GRB); +int ledmode = BOTH; + void init_led() { strip.begin(); strip.setBrightness(100); } +void setLedMode(ledmodes newledmode) { + ledmode = newledmode; +} + void setLeds(Colors color) { static unsigned long lastLedShowing = 0; if (millis() - lastLedShowing > 5) { - - if (ledmode123 == BOTH) { - if (color == BLACK) + if (ledmode == BOTH) { + if (color == BLACK) + strip.setAllLedsColorData(0, 0, 0); + else if (color == WHITE) + strip.setAllLedsColorData(255, 255, 255); + else + strip.setAllLedsColorData(strip.hsv2rgb(color*30, 100, 100)); + } else { // We are only using one of the two leds, not both anymore strip.setAllLedsColorData(0, 0, 0); - else if (color == WHITE) - strip.setAllLedsColorData(255, 255, 255); - else - strip.setAllLedsColorData(strip.hsv2rgb(color*30, 100, 100)); - } else { - if (color == BLACK) - strip.setLedColorData(ledmode123-1, 0, 0, 0); - else if (color == WHITE) - strip.setLedColorData(ledmode123-1, 255, 255, 255); - else - strip.setLedColorData(ledmode123-1, strip.hsv2rgb(color*30, 100, 100)); - } - strip.show(); - lastLedShowing = millis(); + + if (color == BLACK) + strip.setLedColorData(ledmode-1, 0, 0, 0); + else if (color == WHITE) + strip.setLedColorData(ledmode-1, 255, 255, 255); + else + strip.setLedColorData(ledmode-1, strip.hsv2rgb(color*30, 100, 100)); + } + strip.show(); + lastLedShowing = millis(); } } diff --git a/Robot Code/lib/LEDHandler/LEDHandler.h b/Robot Code/lib/LEDHandler/LEDHandler.h index 3fa7df9..326b273 100644 --- a/Robot Code/lib/LEDHandler/LEDHandler.h +++ b/Robot Code/lib/LEDHandler/LEDHandler.h @@ -24,10 +24,10 @@ enum Colors { }; enum ledmodes{ - BOTH, TOP, BOTTOM + BOTH, BOTTOM, TOP }; -int ledmode123 = BOTH; +void setLedMode(ledmodes newledmode); void init_led(); diff --git a/Robot Code/lib/Photo_Transistors/Photo_Transistors.h b/Robot Code/lib/Photo_Transistors/Photo_Transistors.h index 97ccb1e..cd02d98 100644 --- a/Robot Code/lib/Photo_Transistors/Photo_Transistors.h +++ b/Robot Code/lib/Photo_Transistors/Photo_Transistors.h @@ -1,4 +1,6 @@ #include +#include +#define RING_BUF_PHOTOTRANS_SIZE 10 class robotOrientation{ @@ -11,7 +13,13 @@ class robotOrientation{ int topPin; int bottomPin; - const int delayTimeMS = 500; + const int delayTimeMS = 100; + + long topPhotoTransVals[RING_BUF_PHOTOTRANS_SIZE] = {0}; + ringBuffer topPhotoRingBuf = ringBuffer(topPhotoTransVals, RING_BUF_PHOTOTRANS_SIZE, 0); + + long bottomPhotoTransVals[RING_BUF_PHOTOTRANS_SIZE] = {0}; + ringBuffer bottomPhotoRingBuf = ringBuffer(bottomPhotoTransVals, RING_BUF_PHOTOTRANS_SIZE, 0); public: bool isFlippedResult = 0; @@ -20,7 +28,10 @@ class robotOrientation{ bool checkIsFlipped() { - curIsFlipped = analogRead(bottomPin) - analogRead(topPin) > 0 ? 1 : 0; + topPhotoRingBuf.update(analogRead(topPin)); + bottomPhotoRingBuf.update(analogRead(bottomPin)); + + curIsFlipped = bottomPhotoRingBuf.getMaxVal() - topPhotoRingBuf.getMaxVal() > 0 ? 1 : 0; if (curIsFlipped == 1 && lastIsFlipped == 0) lastTransition = millis(); diff --git a/Robot Code/src/main.cpp b/Robot Code/src/main.cpp index 6f6d5dc..727d5b0 100644 --- a/Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -59,11 +59,11 @@ void loop() if (!myPTs.isFlippedResult) { // Not flipped oreo.useTopIr = 1; driveMotors.flip_motors = 0; - // ledmode = TOP; + setLedMode(TOP); } else { oreo.useTopIr = 0; driveMotors.flip_motors = 1; - // ledmode = BOTTOM; + setLedMode(BOTTOM); } if (motor_settings.newSettings == true) { @@ -261,12 +261,11 @@ void loop() driveMotors.set_both_motors(0); EVERY_N_SECONDS(1) { - // laptop.send("SOC: " + String(get3sSOC()) + " %"); - // laptop.send(motor_settings.tankdrive_param_string); + laptop.send("SOC: " + String(get3sSOC()) + " %"); } } } else { // Currently DISCONNECTED - // ledmode = BOTH; + setLedMode(BOTH); driveMotors.set_both_motors(0); toggleLeds(RED, BLACK, 500); } From 12a255523bd8fcd62e2bc9f98a58286dddd1c626 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Wed, 24 Jul 2024 21:51:21 -0700 Subject: [PATCH 42/48] added macro for changing if we hockey puck since hockey puck has to spin oppposite direction --- Robot Code/src/main.cpp | 21 +++++++++++++++------ controller/controller.py | 8 ++++---- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Robot Code/src/main.cpp b/Robot Code/src/main.cpp index 727d5b0..0ffaad8 100644 --- a/Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -9,6 +9,8 @@ #include "SPIFFS.h" #include +#define IS_HOCKEY_PUCK 1 + const int packSize = 6; char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; const int headings[] = {0, 45, 90, 135, 180, 225, 270, 315}; @@ -16,7 +18,7 @@ bool wasMeltying = false; int slowDownSpeed = 200; BLE_Uart laptop = BLE_Uart(laptop_packetBuffer, packSize); Drive_Motors driveMotors = Drive_Motors(LEFT_MOTOR_PIN, LEFT_MOTOR_CHANNEL, RIGHT_MOTOR_PIN, RIGHT_MOTOR_CHANNEL); -robotOrientation myPTs = robotOrientation(TOP_PHOTO_TRANSISTOR, BOTTOM_PHOTO_TRANSISTOR);\ +robotOrientation myPTs = robotOrientation(TOP_PHOTO_TRANSISTOR, BOTTOM_PHOTO_TRANSISTOR); database_handler motor_settings = database_handler(); melty oreo = melty(TOP_IR_PIN, BOTTOM_IR_PIN); @@ -102,8 +104,9 @@ void loop() int adjRotValue = melty_parameters.rot + boostVal; int adjTransValue = melty_parameters.tra + boostVal; - if (myPTs.isFlippedResult) { // Since we need to spin the opposite way for tooth engagement - // adjRotValue *= -1; + if (myPTs.isFlippedResult && IS_HOCKEY_PUCK) { // Since we need to spin the opposite way for tooth engagement if we are hockey puck + adjRotValue *= -1; + adjTransValue *= -1; } if (oreo.translate()) { @@ -118,10 +121,16 @@ void loop() int drivecmd = laptop_packetBuffer[1] - '0'; if (drivecmd > 0 && drivecmd < 9) { // 1,2,3,4,5,6,7,8 - if (myPTs.isFlippedResult == false) - drivecmd = 8 - drivecmd; + drivecmd -= 1; // Make it 0 indexed, so now it goes 0 -> 7 + if (myPTs.isFlippedResult == true && IS_HOCKEY_PUCK) // Perform manipulation on the orientation offsets 1 + drivecmd = drivecmd + 3; else - drivecmd = drivecmd - 1; + drivecmd = 7 - drivecmd; + + if (drivecmd < 0 || drivecmd > 7) { + drivecmd = (drivecmd % 8 + 8) % 8; // Make sure we always in the bounds of the array + } + oreo.deg = headings[drivecmd]; } diff --git a/controller/controller.py b/controller/controller.py index e718216..1ad7ecf 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -13,7 +13,7 @@ def millis(): ir_beacon_2 = BLE_UART(peripheral_name='Beac 2', address = '642D48B0-0DA1-AB00-2DDD-B639F5353E80') ir_beacon_1 = BLE_UART(peripheral_name='Beac 1', address = '37E54CED-FA64-96E8-C84C-8528ADB5AC13') oreo = BLE_UART(peripheral_name='Oreo', address = '168B3E4A-21A9-918B-F28C-8D26D656012C') -hockey_puck = BLE_UART(peripheral_name='Hockey Puck', address = 'DB644862-A8E2-33B5-1D6E-794F2EEA94E8') +hockey_puck = BLE_UART(peripheral_name='Hockey Puck', address = '07C80925-7C0F-1236-FB54-CFC3912A3B9D') keyboard_thread = threading.Thread(target=lambda: Listener(on_press=on_press, on_release=on_release).start()) keyboard_thread.daemon = True @@ -161,9 +161,9 @@ async def cmd_handler(): robotcmd = f"{enabled}{drivecmd}{robottuning}{boost}{calibrate_accel}0" irbeaconcmd = f"{enabled}{activeBeacon}{irbeacontuning}000" await asyncio.sleep(0.05) - + async def main(): - await asyncio.gather(ir_beacon_switcher(), cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(oreo, True), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(oreo), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) - # await asyncio.gather(ir_beacon_switcher(), cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(oreo, True), bluetooth_comm_handler(hockey_puck, True), bluetooth_receive_handler(hockey_puck), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(oreo), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) + # await asyncio.gather(ir_beacon_switcher(), cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(oreo, True), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(oreo), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) + await asyncio.gather(ir_beacon_switcher(), cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(oreo, True), bluetooth_comm_handler(hockey_puck, True), bluetooth_receive_handler(hockey_puck), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(oreo), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) asyncio.run(main()) From 9a51e3d51286d6106aea5cb48d7fa2f0487e1cc8 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Sat, 9 Nov 2024 11:11:37 -0800 Subject: [PATCH 43/48] trying to add gui to oreo --- .DS_Store | Bin 10244 -> 10244 bytes .../ADXL375 Test}/.gitignore | 0 .../ADXL375 Test}/.vscode/extensions.json | 0 .../ADXL375 Test}/include/README | 0 .../ADXL375 Test}/lib/README | 0 .../ADXL375 Test}/platformio.ini | 0 .../ADXL375 Test}/src/main.cpp | 0 .../ADXL375 Test}/test/README | 0 IR Beacon/platformio.ini | 9 +- IR Beacon/src/main.cpp | 12 ++- Motor_Test_DSDHOT/src/main.cpp | 45 ++++----- Motor_Test_PWM/src/main.cpp | 2 +- Robot Code/include/pin_definitions.h | 13 ++- .../lib/Battery_Monitor/Battery_Monitor.h | 10 +- Robot Code/lib/LEDHandler/LEDHandler.h | 4 +- Robot Code/lib/Melty/melty.cpp | 12 ++- Robot Code/lib/Melty/melty.h | 4 +- Robot Code/src/main.cpp | 17 +++- .../__pycache__/streamlit.cpython-311.pyc | Bin 0 -> 1452 bytes controller/app.py | 43 ++++++++ controller/controller.py | 47 +++++++-- controller/drawing.svg | 50 ---------- .../drawing.svg.2024_03_26_20_10_56.0.svg | 93 ------------------ controller/rpmgauge.jpg | Bin 140830 -> 0 bytes controller/tkinter_testing.py | 48 --------- 25 files changed, 154 insertions(+), 255 deletions(-) rename {ADXL375 Test => Archive/ADXL375 Test}/.gitignore (100%) rename {ADXL375 Test => Archive/ADXL375 Test}/.vscode/extensions.json (100%) rename {ADXL375 Test => Archive/ADXL375 Test}/include/README (100%) rename {ADXL375 Test => Archive/ADXL375 Test}/lib/README (100%) rename {ADXL375 Test => Archive/ADXL375 Test}/platformio.ini (100%) rename {ADXL375 Test => Archive/ADXL375 Test}/src/main.cpp (100%) rename {ADXL375 Test => Archive/ADXL375 Test}/test/README (100%) create mode 100644 controller/__pycache__/streamlit.cpython-311.pyc create mode 100644 controller/app.py delete mode 100644 controller/drawing.svg delete mode 100644 controller/drawing.svg.2024_03_26_20_10_56.0.svg delete mode 100644 controller/rpmgauge.jpg delete mode 100644 controller/tkinter_testing.py diff --git a/.DS_Store b/.DS_Store index 2df912dd545ea3a48bc3ee4dad9152d63690606a..22c633f7e8e848e1be74fcf697df27a27dff8396 100644 GIT binary patch delta 21 ccmZn(XbIS0BFtfIVxXg7WNNtCM)-sX07Z5N0RR91 delta 21 ccmZn(XbIS0BFtf8YNn%LVr0D8M)-sX07cja3IG5A diff --git a/ADXL375 Test/.gitignore b/Archive/ADXL375 Test/.gitignore similarity index 100% rename from ADXL375 Test/.gitignore rename to Archive/ADXL375 Test/.gitignore diff --git a/ADXL375 Test/.vscode/extensions.json b/Archive/ADXL375 Test/.vscode/extensions.json similarity index 100% rename from ADXL375 Test/.vscode/extensions.json rename to Archive/ADXL375 Test/.vscode/extensions.json diff --git a/ADXL375 Test/include/README b/Archive/ADXL375 Test/include/README similarity index 100% rename from ADXL375 Test/include/README rename to Archive/ADXL375 Test/include/README diff --git a/ADXL375 Test/lib/README b/Archive/ADXL375 Test/lib/README similarity index 100% rename from ADXL375 Test/lib/README rename to Archive/ADXL375 Test/lib/README diff --git a/ADXL375 Test/platformio.ini b/Archive/ADXL375 Test/platformio.ini similarity index 100% rename from ADXL375 Test/platformio.ini rename to Archive/ADXL375 Test/platformio.ini diff --git a/ADXL375 Test/src/main.cpp b/Archive/ADXL375 Test/src/main.cpp similarity index 100% rename from ADXL375 Test/src/main.cpp rename to Archive/ADXL375 Test/src/main.cpp diff --git a/ADXL375 Test/test/README b/Archive/ADXL375 Test/test/README similarity index 100% rename from ADXL375 Test/test/README rename to Archive/ADXL375 Test/test/README diff --git a/IR Beacon/platformio.ini b/IR Beacon/platformio.ini index 2cda21d..2a790f4 100644 --- a/IR Beacon/platformio.ini +++ b/IR Beacon/platformio.ini @@ -8,13 +8,16 @@ ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html -[env:esp32-s3-devkitc-1] +[env:esp32-c3-devkitc-1] platform = espressif32 -board = esp32-s3-devkitc-1 +board = esp32-c3-devkitm-1 framework = arduino upload_speed = 921600 monitor_speed = 115200 -build_flags = -D IR_BEACON=1 +build_flags = + -D IR_BEACON=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D ARDUINO_USB_MODE=1 lib_deps = fastled/FastLED@^3.6.0 h2zero/NimBLE-Arduino@^1.4.1 diff --git a/IR Beacon/src/main.cpp b/IR Beacon/src/main.cpp index 305b6ce..64d3735 100644 --- a/IR Beacon/src/main.cpp +++ b/IR Beacon/src/main.cpp @@ -4,12 +4,14 @@ #include #include -#define TARGETCMD '1' // Change based on which IR_Beacon working on +#define TARGETCMD '2' // Change based on which IR_Beacon working on + +#define IRLEDPIN 3 +#define BAT_PIN 1 const int packSize = 6; char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; -const int IRLedPin = 4; const int freq = 38000; const int ledChannel = 1; const int resolution = 8; @@ -22,9 +24,9 @@ BLE_Uart laptop = BLE_Uart(laptop_packetBuffer, packSize); void setup(){ init_led(); setLeds(ORANGE); - USBSerial.begin(115200); + Serial.begin(115200); ledcSetup(ledChannel, freq, resolution); - ledcAttachPin(IRLedPin, ledChannel); + ledcAttachPin(IRLEDPIN, ledChannel); laptop.init_ble("IR Beacon"); setLeds(BLACK); } @@ -36,7 +38,7 @@ void loop(){ toggleLeds(RED, GREEN, 500); ledcWrite(ledChannel, 0); EVERY_N_SECONDS(10) { - laptop.send("SOC: " + String(get1sSOC()) + " %"); + laptop.send("SOC: " + String(get1sSOC(BAT_PIN)) + " %"); } break; case '1': // Enable one of the IR Beacons diff --git a/Motor_Test_DSDHOT/src/main.cpp b/Motor_Test_DSDHOT/src/main.cpp index 853ba5e..e7b129d 100644 --- a/Motor_Test_DSDHOT/src/main.cpp +++ b/Motor_Test_DSDHOT/src/main.cpp @@ -1,5 +1,5 @@ #define LEDS_COUNT 1 -#define LEDS_PIN 4 +#define LEDS_PIN 48 #define CHANNEL 0 #include "Freenove_WS2812_Lib_for_ESP32.h" #include "DShotESC.h" @@ -9,7 +9,6 @@ SerialHandler myComputer = SerialHandler(); Freenove_ESP32_WS2812 strip = Freenove_ESP32_WS2812(LEDS_COUNT, LEDS_PIN, 0, TYPE_GRB); // Channel is always 0 for some reason..... DShotESC rmot; -DShotESC lmot; enum Colors { RED, @@ -34,43 +33,35 @@ void setLed(int colorWheel) { void setup() { USBSerial.begin(115200); + rmot.install(GPIO_NUM_7, RMT_CHANNEL_1); //<-- This is the problem line. Apparently this line has to go before initializing the strip + delay(250); rmot.init(); - rmot.setReversed(false); - rmot.set3DMode(true); + delay(250); + rmot.sendMotorStop(); + delay(250); + // rmot.setReversed(false); + // rmot.set3DMode(true); rmot.throttleArm(); // <--- Super important!!!; - - lmot.install(GPIO_NUM_8, RMT_CHANNEL_2); //<-- This is the problem line. Apparently this line has to go before initializing the strip - lmot.init(); - lmot.setReversed(false); - lmot.set3DMode(true); - lmot.throttleArm(); // <--- Super important!!!; + delay(250); + // rmot.blueJayArm(); + // delay(1000); + // for (int i = 0; i < 10; i++) { + // rmot.sendThrottle3D(0); + // delay(10); + // } strip.begin(); strip.setBrightness(10); - - for (int i = 0; i < 3 ; i++) { - setLed(RED); - delay(1); - } - - int mydelay = 10; - for (int i = 0; i < 100/mydelay; i++) - { - rmot.sendThrottle3D(0); - lmot.sendThrottle3D(0); - delay(mydelay); - } - // delay(3000); - USBSerial.println("Armed!!!"); + USBSerial.println("Done initializing!!!"); } void loop() { int throttle = myComputer.getInt(0); rmot.sendThrottle3D(throttle); // Throttle value from -999 to 999 - lmot.sendThrottle3D(throttle); - delay(10); + // lmot.sendThrottle3D(throttle); + delay(1); // Maybe need to send update faster than 10ms?? if (throttle != 0) { setLed(RED); diff --git a/Motor_Test_PWM/src/main.cpp b/Motor_Test_PWM/src/main.cpp index 758fdbc..3fa3403 100644 --- a/Motor_Test_PWM/src/main.cpp +++ b/Motor_Test_PWM/src/main.cpp @@ -31,7 +31,7 @@ void setup() { ledcWrite(motchannel, neutralVal); - autoMotorTune(); + // autoMotorTune(); while (myComputer.getInt(0) == 0) { delay(10); diff --git a/Robot Code/include/pin_definitions.h b/Robot Code/include/pin_definitions.h index 258d9c9..39549b2 100644 --- a/Robot Code/include/pin_definitions.h +++ b/Robot Code/include/pin_definitions.h @@ -1,13 +1,16 @@ -#define TOP_PHOTO_TRANSISTOR 11 -#define BOTTOM_PHOTO_TRANSISTOR 12 +#define TOP_PHOTO_TRANSISTOR 6 +#define BOTTOM_PHOTO_TRANSISTOR 5 -#define TOP_IR_PIN 41 -#define BOTTOM_IR_PIN 42 +#define TOP_IR_PIN 42 +#define BOTTOM_IR_PIN 41 -#define LEFT_MOTOR_PIN GPIO_NUM_45 +#define LEFT_MOTOR_PIN GPIO_NUM_37 #define RIGHT_MOTOR_PIN GPIO_NUM_1 #define LEFT_MOTOR_CHANNEL RMT_CHANNEL_2 #define RIGHT_MOTOR_CHANNEL RMT_CHANNEL_1 +#define RED_LED_TOP 39 +#define RED_LED_BOT 40 + #define BAT_PIN 18 diff --git a/Robot Code/lib/Battery_Monitor/Battery_Monitor.h b/Robot Code/lib/Battery_Monitor/Battery_Monitor.h index 1481ef6..b1dd416 100644 --- a/Robot Code/lib/Battery_Monitor/Battery_Monitor.h +++ b/Robot Code/lib/Battery_Monitor/Battery_Monitor.h @@ -11,14 +11,14 @@ int getPerc(int min, int max, int val) { return perc; } -int get3sSOC() { +int get3sSOC(int pin) { int max = 4096-50; int min = 3000; - return getPerc(min, max ,analogRead(18)); + return getPerc(min, max ,analogRead(pin)); } -int get1sSOC() { - int max = 2585-50; +int get1sSOC(int pin) { + int max = 2900-50; int min = 2048; - return getPerc(min, max ,analogRead(18)); + return getPerc(min, max ,analogRead(pin)); } diff --git a/Robot Code/lib/LEDHandler/LEDHandler.h b/Robot Code/lib/LEDHandler/LEDHandler.h index 326b273..da48795 100644 --- a/Robot Code/lib/LEDHandler/LEDHandler.h +++ b/Robot Code/lib/LEDHandler/LEDHandler.h @@ -2,9 +2,9 @@ #include #ifdef IR_BEACON - #define LEDPIN 5 + #define LEDPIN 4 #else - #define LEDPIN 46 + #define LEDPIN 45 #endif enum Colors { diff --git a/Robot Code/lib/Melty/melty.cpp b/Robot Code/lib/Melty/melty.cpp index d450f15..fc51148 100644 --- a/Robot Code/lib/Melty/melty.cpp +++ b/Robot Code/lib/Melty/melty.cpp @@ -1,7 +1,9 @@ #include #include -melty::melty(int top_ir_pin, int bottom_ir_pin) : top_ir_pin(top_ir_pin), bottom_ir_pin(bottom_ir_pin) { +melty::melty(int top_ir_pin, int bottom_ir_pin, int top_led_pin, int bottom_led_pin) : top_ir_pin(top_ir_pin), bottom_ir_pin(bottom_ir_pin), top_led_pin(top_led_pin), bottom_led_pin(bottom_led_pin) { + pinMode(top_led_pin, OUTPUT); + pinMode(bottom_led_pin, OUTPUT); period_micros_calc = ringBuffer(period_micros_calc_array, 5, 1); time_seen_beacon_calc = ringBuffer(time_seen_beacon_calc_array, TIME_SEEN_BEACON_ARRAY_SIZE, 0.7); } @@ -15,11 +17,15 @@ bool melty::update() { if (curSeenIRLed != lastSeenIRLed) if (curSeenIRLed) { // Activates on the rising edge of seeing the IR LED - setLeds(BLUE); + if (useTopIr) + digitalWrite(top_led_pin, LOW); + else + digitalWrite(bottom_led_pin, LOW); currentPulse = micros(); } else { // Activates on the falling edge of seeing the IR LED - setLeds(YELLOW); + digitalWrite(top_led_pin, HIGH); + digitalWrite(bottom_led_pin, HIGH); time_seen_beacon = micros() - currentPulse; time_seen_beacon_calc.update(time_seen_beacon); diff --git a/Robot Code/lib/Melty/melty.h b/Robot Code/lib/Melty/melty.h index e742bf9..eedf4b4 100644 --- a/Robot Code/lib/Melty/melty.h +++ b/Robot Code/lib/Melty/melty.h @@ -7,7 +7,7 @@ #define TIME_SEEN_BEACON_ARRAY_SIZE 5 class melty { public: - melty (int top_ir_pin, int bottom_ir_pin); + melty (int top_ir_pin, int bottom_ir_pin, int top_led_pin, int bottom_led_pin); bool update(); bool isBeaconSensed(bool currentReading); void computeTimings(); @@ -31,6 +31,8 @@ class melty { int top_ir_pin = 0; int bottom_ir_pin = 0; + int top_led_pin = 0; + int bottom_led_pin = 0; bool lastSeenIRLed = 0; long period_micros = micros(); diff --git a/Robot Code/src/main.cpp b/Robot Code/src/main.cpp index 0ffaad8..9f49559 100644 --- a/Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -9,7 +9,7 @@ #include "SPIFFS.h" #include -#define IS_HOCKEY_PUCK 1 +#define IS_HOCKEY_PUCK 0 const int packSize = 6; char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; @@ -21,7 +21,7 @@ Drive_Motors driveMotors = Drive_Motors(LEFT_MOTOR_PIN, LEFT_MOTOR_CHANNEL, RIGH robotOrientation myPTs = robotOrientation(TOP_PHOTO_TRANSISTOR, BOTTOM_PHOTO_TRANSISTOR); database_handler motor_settings = database_handler(); -melty oreo = melty(TOP_IR_PIN, BOTTOM_IR_PIN); +melty oreo = melty(TOP_IR_PIN, BOTTOM_IR_PIN, RED_LED_TOP, RED_LED_BOT); struct melty_parameters { int rot; int tra; @@ -46,6 +46,11 @@ void setup() driveMotors.init_motors(); // <- This needs to be init first or else something with RMT doesnt work.... init_led(); + pinMode(RED_LED_BOT, OUTPUT); + pinMode(RED_LED_TOP, OUTPUT); + digitalWrite(RED_LED_BOT, HIGH); + digitalWrite(RED_LED_TOP, HIGH); + setLeds(ORANGE); driveMotors.arm_motors(); laptop.init_ble("Oreo"); @@ -89,8 +94,10 @@ void loop() } } - + if (laptop_packetBuffer[0] == '1') { // Currently enabled and meltybraining!!! + setLedMode(BOTH); + setLeds(BLACK); if (oreo.update()) { // If seen the LED EVERY_N_MILLIS(250) { laptop.send("seen"); @@ -270,7 +277,7 @@ void loop() driveMotors.set_both_motors(0); EVERY_N_SECONDS(1) { - laptop.send("SOC: " + String(get3sSOC()) + " %"); + laptop.send("SOC: " + String(get3sSOC(BAT_PIN)) + " %"); } } } else { // Currently DISCONNECTED @@ -280,3 +287,5 @@ void loop() } } + +// this is katie hello :)) \ No newline at end of file diff --git a/controller/__pycache__/streamlit.cpython-311.pyc b/controller/__pycache__/streamlit.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d184218ba8a94849f475228b8b5101194b65f33e GIT binary patch literal 1452 zcmZ`&&uiOO9DlN9*;1?|b%G&uFf}w~Hq08`X`z(W^}6g(n=KjJ3?s;XFR_AtAw8|N z*};cEedwW#K?h?HcE~o;L;r*wca%Vf@J@xnz_3H0x1RQuY-!2xp1*$Y`@WyAKi}`? zEBO~$mJqDle>D!DPT+$o@E@SQCVB{*=+ z7S1ok`3&b5;)M(^T*bRW!e7jA0s1Q9(hKnvIL|WPZYjwVvpnf4k9#Y@vwXZngdfpQ zi`D#_RVJlHS;=gq?ox7=yE1Su9WAG|iF~w@`X{TYclpFJUOt7Ho@H}AM0n+A?#1fW zX!qKGy1sT*)qMJul$$KZ*Zv>J+%BqBrg1`hRDC#99_X$|m0ju*%`|kX+;g1KTVD{P zZ<|D+wqg!1ftuso7&Z3>x~G6?il>{7K@H)hKe8xs6)!159@Fj7%Kd?Dd%&ewzS$%06hn*C_`R#;nP}?P{x_Q86L7W1bC>rh zahR~{_e`CR685nA3>rt$w_8o$0k%iUvKlX3TJ94q#1 z!#6E&w6OyOvAU!;SF^BfDp;f1c5R)Bpl&uNmh&hpP@Nit2?L^G;cSRpY)DG2RanTrzZv?Dq$nOMgSJK_Pncq53)0_f#0);Ul6Ci9JwTcHcK)PM-R)PIvsC z+Hr}k_HB#0wqX!g%``Tg;hWMO)5g9*?z*>tFn%0f8}^Gl$8oPvHAwA3K%6+v9hU;} zMu;{ev>BkyI4D6-45W8LREtn8K(%vJn&mwVQ9DBI0JR}+`TFFur(Xxx-wowPBsazq z#H5W$<>}kOM*XGs`}==<8i<`x>_lQ`EC9&Mw6Gdo9>AhlP;wXu8r?X%|hpFHn; z*Ewna%7yFoXubZj6MuqYD=fC6Vr$H2nr?<@D?(cV+B!#NkXfGWg{TrGjB%l5h{+Qi PqO~Zk2zA|?FxP(oqg;BD literal 0 HcmV?d00001 diff --git a/controller/app.py b/controller/app.py new file mode 100644 index 0000000..7fa5a46 --- /dev/null +++ b/controller/app.py @@ -0,0 +1,43 @@ +import streamlit as st +import time +import random + +# Function to simulate checking connection status for each device +def check_connection(device_id): + time.sleep(1) # Simulate a delay for checking each device + return random.choice([True, False]) # Randomly return connected or disconnected status + +# Set up the Streamlit app +st.title("Multi-Device Connection Status Checker") + +# Define the device names +devices = ["Device 1", "Device 2", "Device 3"] + +# Layout the devices in columns +status_columns = st.columns(len(devices)) + +# Function to update the status with a loading bar +def update_status(): + for i, device in enumerate(devices): + with status_columns[i]: + st.subheader(device) + st.spinner("Connecting...") + # Show a progress bar while checking connection + # with st.spinner("Checking..."): + # connection_status = check_connection(device) + # time.sleep(1) # Simulate loading time + + # Display the connection status + # if connection_status: + # st.success("Connected") + # else: + # st.error("Disconnected") + +# Button to manually refresh connection statuses +if st.button("Refresh Connection Status"): + update_status() + +# Auto-refresh every 10 seconds (optional) +while True: + update_status() + time.sleep(10) # Adjust the refresh interval as needed diff --git a/controller/controller.py b/controller/controller.py index 1ad7ecf..ba29f94 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -4,16 +4,18 @@ from bluetooth import * import asyncio import time +import streamlit as st startTime = time.time() def millis(): return round((time.time()-startTime) * 1000) -ir_beacon_2 = BLE_UART(peripheral_name='Beac 2', address = '642D48B0-0DA1-AB00-2DDD-B639F5353E80') -ir_beacon_1 = BLE_UART(peripheral_name='Beac 1', address = '37E54CED-FA64-96E8-C84C-8528ADB5AC13') -oreo = BLE_UART(peripheral_name='Oreo', address = '168B3E4A-21A9-918B-F28C-8D26D656012C') -hockey_puck = BLE_UART(peripheral_name='Hockey Puck', address = '07C80925-7C0F-1236-FB54-CFC3912A3B9D') +ir_beacon_2 = BLE_UART(peripheral_name='Beac 2', address = '9AC0FFF3-446C-1A64-DA89-1376064B2BA1') +ir_beacon_1 = BLE_UART(peripheral_name='Beac 1', address = 'DBA047A7-4143-45D5-E469-FEEA2E354502') +oreo = BLE_UART(peripheral_name='Oreo', address = '1932D032-A476-F238-07F0-A39D5208BC73') +bt_devices = {ir_beacon_2, ir_beacon_1, oreo} +# hockey_puck = BLE_UART(peripheral_name='Hockey Puck', address = '07C80925-7C0F-1236-FB54-CFC3912A3B9D') keyboard_thread = threading.Thread(target=lambda: Listener(on_press=on_press, on_release=on_release).start()) keyboard_thread.daemon = True @@ -162,8 +164,37 @@ async def cmd_handler(): irbeaconcmd = f"{enabled}{activeBeacon}{irbeacontuning}000" await asyncio.sleep(0.05) -async def main(): - # await asyncio.gather(ir_beacon_switcher(), cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(oreo, True), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(oreo), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) - await asyncio.gather(ir_beacon_switcher(), cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(oreo, True), bluetooth_comm_handler(hockey_puck, True), bluetooth_receive_handler(hockey_puck), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(oreo), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) +async def main_async_tasks(): + await asyncio.gather(ir_beacon_switcher(), cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(oreo, True), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(oreo), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) + # await asyncio.gather(ir_beacon_switcher(), cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(oreo, True), bluetooth_comm_handler(hockey_puck, True), bluetooth_receive_handler(hockey_puck), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(oreo), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) -asyncio.run(main()) +def start_async_tasks(): + asyncio.run(main_async_tasks()) + + +if "Running" not in st.session_state: + st.session_state["Running"] = True + async_thread = threading.Thread(target=start_async_tasks) + async_thread.start() + +# Display UI in Streamlit, updating based on session state +st.title("Oreo Control Panel") +st.header("Device Status") + +# Use an empty container to update just the status UI +status_container = st.empty() + +# Update only the device status UI +# while True: +with status_container.container(): + status_cols = st.columns(len(bt_devices)) + for i, bt_device in enumerate(bt_devices): + with status_cols[i]: + st.subheader(bt_device._peripheral_name) + status = bt_device.isConnected + if status: + st.success("Connected") + else: + st.error("Disconnected") +time.sleep(2) # Update interval in seconds +st.rerun() \ No newline at end of file diff --git a/controller/drawing.svg b/controller/drawing.svg deleted file mode 100644 index 356732e..0000000 --- a/controller/drawing.svg +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - diff --git a/controller/drawing.svg.2024_03_26_20_10_56.0.svg b/controller/drawing.svg.2024_03_26_20_10_56.0.svg deleted file mode 100644 index fda99fa..0000000 --- a/controller/drawing.svg.2024_03_26_20_10_56.0.svg +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/controller/rpmgauge.jpg b/controller/rpmgauge.jpg deleted file mode 100644 index 43bb295f1b60992b45c4a58c38c98fb3ba020d19..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 140830 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 diff --git a/controller/tkinter_testing.py b/controller/tkinter_testing.py deleted file mode 100644 index e3f44fe..0000000 --- a/controller/tkinter_testing.py +++ /dev/null @@ -1,48 +0,0 @@ -import tkinter as tk -import math -from PIL import Image, ImageTk - -# Initialize Tkinter and create main window -root = tk.Tk() -root.title("RPM Gauge") - -# Constants for gauge drawing -canvas_width, canvas_height = 800, 800 -center_x, center_y = canvas_width // 2, canvas_height // 2 -needle_length = 300 - -# Create canvas widget -canvas = tk.Canvas(root, width=canvas_width, height=canvas_height, bg='black') -canvas.pack() - -# Open the image with Pillow -image = Image.open("rpmgauge.jpg") -image = image.resize((canvas_width, canvas_height), Image.LANCZOS) - -# Convert the Image object into a TkPhoto object -photo = ImageTk.PhotoImage(image) - -canvas.create_image(0, 0, anchor=tk.NW, image=photo) - -# Initial needle (at 0 RPM) -needle = canvas.create_line(center_x, center_y, center_x, center_y - needle_length, fill='red', width=4) - -# Function to update the needle based on RPM -def update_needle(rpm): - max_rpm = 7000 # Maximum RPM for scale - angle_degrees = 90 + (rpm / max_rpm) * 180 # Convert RPM to angle (simplified) - angle_radians = math.radians(angle_degrees) - - # Calculate new needle end position - end_x = center_x + needle_length * math.cos(angle_radians) - end_y = center_y - needle_length * math.sin(angle_radians) - - # Update the needle - canvas.coords(needle, center_x, center_y, end_x, end_y) - -# Slider to simulate changing RPM -rpm_slider = tk.Scale(root, from_=0, to=7000, orient='horizontal', command=lambda value: update_needle(int(value))) -rpm_slider.pack() - -# Start the Tkinter event loop -root.mainloop() From cd325dbc2828446cedc7b4777de707c03d01f92d Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Wed, 18 Dec 2024 02:56:22 -0800 Subject: [PATCH 44/48] after ucsd competition, planning on adding ir beacon interrupt and sin wave translation --- .DS_Store | Bin 10244 -> 14340 bytes Motor_Test_PWM/.vscode/settings.json | 11 + Motor_Test_PWM/platformio.ini | 10 +- Motor_Test_PWM/src/main.cpp | 18 +- Robot Code/.DS_Store | Bin 6148 -> 10244 bytes Robot Code/lib/.DS_Store | Bin 8196 -> 10244 bytes Robot Code/lib/Melty/melty.cpp | 13 +- Robot Code/lib/Melty/melty.h | 6 +- .../lib/Photo_Transistors/Photo_Transistors.h | 3 + Robot Code/src/main.cpp | 44 ++-- .../__pycache__/bluetooth.cpython-311.pyc | Bin 4982 -> 5187 bytes controller/bluetooth.py | 4 + controller/controller.py | 35 +--- controller/controller_gui.py | 195 ++++++++++++++++++ 14 files changed, 258 insertions(+), 81 deletions(-) create mode 100644 Motor_Test_PWM/.vscode/settings.json create mode 100644 controller/controller_gui.py diff --git a/.DS_Store b/.DS_Store index 22c633f7e8e848e1be74fcf697df27a27dff8396..ba034ff108dfb014ede5c31134ab0d03e60b962e 100644 GIT binary patch delta 609 zcmZn(Xem%&U|?W$DortDU@!nOIe-{M3-ADmHUdQht6814AA|2}2P>K0^)=rZN;Il@}Kz zZ7g(V-^|Xz!okS7*-`K_BfF8MnT~?7#b#^aGdvp`EEyTuC-VpxPmU94fJsfZ7IvDf zBOpInOF(q;Mqza(o`}h}gsWxZ1q4cqQ-e}-5=%16Qe6^DQi~%plk@X}GpkZJ7mCbc zRNz#00C@)lI2dFZj2K)Pd>9gdZmMSJWthRRda}0IY)1dd$Hi2IfnGs(@8thtK)bvN z+qGX@mk7IfBpVqYP41IamB(UNS#VKaPJUiG(4~x^Pz3t_$mSf$*=!pdOc^Cv!Cqi6 z;0DsJ$kDU0@ICWneidC#Q1BlBshVuVBRx4mg`L#`%qvitI9Wx+5Pw+F#{ClkY8;QX delta 179 zcmZoEXbF&DU|?W$DortDU{C-uIe-{M3-C-V6q~3gIoZI3MH0wo;00nvpg03VCPNuR zYD#f(PE!8ng%Vlp6CYS^X6InxVC0?5qhLH)T9IesWZ}uh>PIF{P?$JTbhDDcT}BRL z69XLuBU8i4ztwFvI}7#m2r`3A1zN@pBwRrjZoK%Nc{0C=E+fNa6CUNs2`22E8Xy%w QTMZ`Xm`vP!Mwpoy00v7Z8vp -#include "/Users/mingweiyeoh/Documents/GitHub/Arduino-Projects/libraries/Custom/USBSerialHandler.h" +#include "/Users/mingweiyeoh/Documents/GitHub/Arduino-Projects/libraries/Custom/SerialHandler.h" SerialHandler myComputer = SerialHandler(); -const int motpin = 7; // 7 is right and 8 is left +const int motpin = 1; const int motchannel = 0; const int neutralVal = 189; void autoMotorTune() { delay(1000); - USBSerial.println("Start with motor off!!"); + Serial.println("Start with motor off!!"); ledcWrite(motchannel, 220); delay(3000); - USBSerial.println("Turn motor on!!!"); + Serial.println("Turn motor on!!!"); delay(5000); ledcWrite(motchannel, 40); - USBSerial.println("Tuning low value!!!"); + Serial.println("Tuning low value!!!"); delay(2500); ledcWrite(motchannel, neutralVal); - USBSerial.println("Tuning neutral value!!!"); + Serial.println("Tuning neutral value!!!"); delay(2500); - USBSerial.println("Done!!!"); + Serial.println("Done!!!"); } void setup() { - USBSerial.begin(115200); + Serial.begin(115200); ledcSetup(motchannel, 500, 8); ledcAttachPin(motpin, motchannel); ledcWrite(motchannel, neutralVal); - // autoMotorTune(); + autoMotorTune(); while (myComputer.getInt(0) == 0) { delay(10); diff --git a/Robot Code/.DS_Store b/Robot Code/.DS_Store index f9f60d0a8f94f19a9f55d4bcbfeaf7866207ada5..cd5968c3598759d2e51867bbb8c190d0ed49910e 100644 GIT binary patch literal 10244 zcmeHMTWl0n7(U;$&@*(PQz$T8cd;s3DzpVE6$slKSh=*JTiOD$?Cy*(VLDTGcDHCn zt3D|q-l8!^eeppF>Vxq@)aZjj(FY&2UTTbq#0QO0d^PdG|IC@?(!z@-26Rp`=b!VR z^PidjJKsP5p0kWGv=z)o#^Q`IPM46nM%87C=oinz0yCMF8S#fR&Xm!S zB0?ZSAVMHQAVMHQ;A%jCa<(YEQp%`~5Qq?n5IB#3_&&tx5;76WF)4#j2UWoqfMhwT zgF^ST4+w1Hp-hBwOv=EN=9Ie!gr*3$7?9@F9~Z+(CPFzTr8H-d<_zJJ5$;eBJe};~ z!ElCzlu;ca5Fs!X0kL3L43bf3=bvaOMnUeoTm`Lyj?xxm0SEgG`7-*!!Jq|Gb1rXSe$ z$_}n_HKiM)qfIT%_3_o~8jse;M_Za2>f>uxHy=H!^0~FE*6&Oma)w;*5$OoRj{%!G z$+2Toy>k3wd&Uu7mDQEJ`nKeiI-^pXX(aoSyR?CnKD$V--#whSJ=fo5<$RJUy~-yy zyRx3!le0vBf7WvfL$06FtFvh*TX52OtJQIO?RzZJRr%SR=Qur{Z;NRL2lCdDyy%ajhHnwfPW5;-v zUOjtGji&7;Jl(u~mu=Ga4_v>E!VW&1N}wYw=<5FbkjptHR@G$e70U)Wh^ij zalMug6f*YVf}OuR&}vJKxT?R&_nPz~+dWgvXv*wdQtgYkvR<*wXZSwx;B9F?-#u)(rB~WybaKAiKbX#0-6KS~l0_Hi!Qzo#m~@(U-U~aI8rwM^ z6qB`@Ce0%N?CvoPS_a!K+C*?#I>~eWbIy5fX$fi*b@DdAQLRlhggu-sVNI-qZDadc zj*YV8>=|~Fy}?eikJy*&d-gLs$Nm5?9V%v^5;d5M`7p2yby$I0uoCOhflh43PVB*6 z>_ZABhTvithf&1+7{h~j2oK{4Jc+09G+w|!!bly3RnN-2&8OCE;$0g{ux4irapx+mgDdGYdb z6x(w9mM(4UH4{e@vt;DVoi{%&!fD;c*7LlRG%YdnV#Ws~za%sQk_U{KOi*LN!bOXh zXt6$u0_(_?f>Ny{8J=w;VwF?|}viEQ4uoSM@qMr3nEeJrLc6q>Tx&=}M7G73C3 zuw7eEO@xh>+cZrtrw~-f7cE$5XiKz&s_HBl*w#+dh=BU1)Sh5x*-z|u_9sz$7UmG4 zm!kpA*g!=QVN zSMVy{!rM57ckm%j<71q`C-@X!;5+<)vy%wC?NS2cm#G9U_U2s2a}Q9DpnVyoJ)33Z zq4hPN^g?e;KkH;DB?~k{Rh0jt{1@fFzmfkg#7~s>=$BrU_b&XTySnpUNZd*JPNamW zLwIP~Oqo(MArIjsYh7s*oa1*M7Q05#z?B3{0pJ xLxes!j;Dq=j=w3?l}<-QeQi_vvlJfI&Ktf=^ z1f@X&KrFxjmETx6jeTN+&|-ED4nby!8Ubz~?Fz)I8wn>$YEEKFW?8CBVo7RoL}qe+UT|hr>gI=nvltaPl^qO# z4gdiT23ZCp1{Ve&h6JEfs~LJ3W-zRtJX?4+qyJ=c5oH3l=86Dq^(JhqzNjt{w$_L> zGCrF8PfS@GuU{F8%7TmXa`N-if$nGA%p>p{7_Nr}nZf>HP~ZmAuAs=+SoocJGQUiq u2oofVG(ZY~(E|4Rn delta 124 zcmZn(XmOBWU|?W$DortDU;r^WfEYvza8E20o2aKK$_kPP@);Ns8S)rX7;+d=8HzR* zPGg_gz`L29gN1{Ub+f1VZ+1auph6&!;06+|AOkiQerKM{uM)@s)DAM1VRAgr)X8ro LTsGIq9A*Ll>R1;G diff --git a/Robot Code/lib/Melty/melty.cpp b/Robot Code/lib/Melty/melty.cpp index fc51148..557c564 100644 --- a/Robot Code/lib/Melty/melty.cpp +++ b/Robot Code/lib/Melty/melty.cpp @@ -1,9 +1,7 @@ #include #include -melty::melty(int top_ir_pin, int bottom_ir_pin, int top_led_pin, int bottom_led_pin) : top_ir_pin(top_ir_pin), bottom_ir_pin(bottom_ir_pin), top_led_pin(top_led_pin), bottom_led_pin(bottom_led_pin) { - pinMode(top_led_pin, OUTPUT); - pinMode(bottom_led_pin, OUTPUT); +melty::melty(int top_ir_pin, int bottom_ir_pin) : top_ir_pin(top_ir_pin), bottom_ir_pin(bottom_ir_pin) { period_micros_calc = ringBuffer(period_micros_calc_array, 5, 1); time_seen_beacon_calc = ringBuffer(time_seen_beacon_calc_array, TIME_SEEN_BEACON_ARRAY_SIZE, 0.7); } @@ -15,17 +13,11 @@ bool melty::update() { else curSeenIRLed = isBeaconSensed(!digitalRead(bottom_ir_pin)); - if (curSeenIRLed != lastSeenIRLed) + if (curSeenIRLed != lastSeenIRLed) { if (curSeenIRLed) { // Activates on the rising edge of seeing the IR LED - if (useTopIr) - digitalWrite(top_led_pin, LOW); - else - digitalWrite(bottom_led_pin, LOW); currentPulse = micros(); } else { // Activates on the falling edge of seeing the IR LED - digitalWrite(top_led_pin, HIGH); - digitalWrite(bottom_led_pin, HIGH); time_seen_beacon = micros() - currentPulse; time_seen_beacon_calc.update(time_seen_beacon); @@ -38,6 +30,7 @@ bool melty::update() { computeTimings(); } } + } lastSeenIRLed = curSeenIRLed; return curSeenIRLed; diff --git a/Robot Code/lib/Melty/melty.h b/Robot Code/lib/Melty/melty.h index eedf4b4..746071c 100644 --- a/Robot Code/lib/Melty/melty.h +++ b/Robot Code/lib/Melty/melty.h @@ -7,7 +7,7 @@ #define TIME_SEEN_BEACON_ARRAY_SIZE 5 class melty { public: - melty (int top_ir_pin, int bottom_ir_pin, int top_led_pin, int bottom_led_pin); + melty(int top_ir_pin, int bottom_ir_pin); bool update(); bool isBeaconSensed(bool currentReading); void computeTimings(); @@ -31,9 +31,7 @@ class melty { int top_ir_pin = 0; int bottom_ir_pin = 0; - int top_led_pin = 0; - int bottom_led_pin = 0; - + bool lastSeenIRLed = 0; long period_micros = micros(); long time_seen_beacon = micros(); diff --git a/Robot Code/lib/Photo_Transistors/Photo_Transistors.h b/Robot Code/lib/Photo_Transistors/Photo_Transistors.h index cd02d98..a851e14 100644 --- a/Robot Code/lib/Photo_Transistors/Photo_Transistors.h +++ b/Robot Code/lib/Photo_Transistors/Photo_Transistors.h @@ -26,6 +26,9 @@ class robotOrientation{ robotOrientation(int topPin, int bottomPin) : topPin(topPin), bottomPin(bottomPin) {} + void printDebugInfo() { + USBSerial.printf("Top : %d Bottom: %d \n", analogRead(topPin), analogRead(bottomPin) ); + } bool checkIsFlipped() { topPhotoRingBuf.update(analogRead(topPin)); diff --git a/Robot Code/src/main.cpp b/Robot Code/src/main.cpp index 9f49559..25b868e 100644 --- a/Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -21,7 +21,7 @@ Drive_Motors driveMotors = Drive_Motors(LEFT_MOTOR_PIN, LEFT_MOTOR_CHANNEL, RIGH robotOrientation myPTs = robotOrientation(TOP_PHOTO_TRANSISTOR, BOTTOM_PHOTO_TRANSISTOR); database_handler motor_settings = database_handler(); -melty oreo = melty(TOP_IR_PIN, BOTTOM_IR_PIN, RED_LED_TOP, RED_LED_BOT); +melty oreo = melty(TOP_IR_PIN, BOTTOM_IR_PIN); struct melty_parameters { int rot; int tra; @@ -39,19 +39,23 @@ int tuningValue = 10; void setup() { + init_led(); + setLeds(ORANGE); + + delay(250); // Power up delay for the Serial USBSerial.begin(115200); SPIFFS.begin(true); motor_settings.updateFromDatabase(); driveMotors.init_motors(); // <- This needs to be init first or else something with RMT doesnt work.... - init_led(); + pinMode(RED_LED_BOT, OUTPUT); pinMode(RED_LED_TOP, OUTPUT); digitalWrite(RED_LED_BOT, HIGH); digitalWrite(RED_LED_TOP, HIGH); - setLeds(ORANGE); + driveMotors.arm_motors(); laptop.init_ble("Oreo"); setLeds(BLACK); @@ -60,6 +64,11 @@ void setup() void loop() { + if (!laptop.isConnected() || laptop_packetBuffer[0] == '1') { // Turn off high powered leds if not meltying + digitalWrite(RED_LED_TOP, HIGH); + digitalWrite(RED_LED_BOT, HIGH); + } + if (laptop.isConnected()) { EVERY_N_MILLIS(50) { myPTs.checkIsFlipped(); @@ -99,9 +108,17 @@ void loop() setLedMode(BOTH); setLeds(BLACK); if (oreo.update()) { // If seen the LED + if (oreo.useTopIr) + digitalWrite(RED_LED_TOP, LOW); + else + digitalWrite(RED_LED_BOT, LOW); EVERY_N_MILLIS(250) { laptop.send("seen"); } + + } else { + digitalWrite(RED_LED_TOP, HIGH); + digitalWrite(RED_LED_BOT, HIGH); } int boostVal = 0; @@ -110,11 +127,6 @@ void loop() int adjRotValue = melty_parameters.rot + boostVal; int adjTransValue = melty_parameters.tra + boostVal; - - if (myPTs.isFlippedResult && IS_HOCKEY_PUCK) { // Since we need to spin the opposite way for tooth engagement if we are hockey puck - adjRotValue *= -1; - adjTransValue *= -1; - } if (oreo.translate()) { driveMotors.l_motor_write(adjRotValue - adjTransValue); @@ -129,10 +141,7 @@ void loop() int drivecmd = laptop_packetBuffer[1] - '0'; if (drivecmd > 0 && drivecmd < 9) { // 1,2,3,4,5,6,7,8 drivecmd -= 1; // Make it 0 indexed, so now it goes 0 -> 7 - if (myPTs.isFlippedResult == true && IS_HOCKEY_PUCK) // Perform manipulation on the orientation offsets 1 - drivecmd = drivecmd + 3; - else - drivecmd = 7 - drivecmd; + drivecmd = 7 - drivecmd; if (drivecmd < 0 || drivecmd > 7) { drivecmd = (drivecmd % 8 + 8) % 8; // Make sure we always in the bounds of the array @@ -185,17 +194,6 @@ void loop() wasMeltying = 1; } else if (laptop_packetBuffer[0] == '2') { // Tank driving mode! - // if (wasMeltying) { // Was previously meltybraining, we need to slowdown - // unsigned long timeout = millis(); - // while (getAccelY() > 2 && millis() - timeout < 2000) { - // updateAccel(); - // driveMotors.set_both_motors(-slowDownSpeed * melty_parameters.invert); - // toggleLeds(WHITE, RED, 150); - // if (laptop_packetBuffer[0] != '2') - // break; - // } - // wasMeltying = 0; - // } int lmotorpwr = 0; int rmotorpwr = 0; diff --git a/controller/__pycache__/bluetooth.cpython-311.pyc b/controller/__pycache__/bluetooth.cpython-311.pyc index 177c445d3a20a4373a63e7f9d9c41f9a5d252574..db5e1948046eb4acb9f641f357389d0906c8223e 100644 GIT binary patch delta 785 zcmZvZOKTHR6vyw$JZdJHR~}=0?rWS40Dk4(FVicqZCCyBWB2jNfA*Eu$ zEL^y--i0gIf-7OZfM7RP#4KEde1ZCcMig=BJ&7$Ay)*OUo^$^9oO{mv?0-F|K37$R z@YQ&mTl})Bu7kJQOa-Oc9w+Yf)PVt56Lm5hcEX&hqq>@-x%z_W=yh?{a17UUOgF;q z2)Cm=9$N@FQNE8mu_Cc8b_VWAG{7tPYj6-iW4{9V`)F@1+&|#o_XNi@0rHeA!!nuM ziW7$v2@P@6hE{04dOT%49lq3nMqU0lgR!DPS_M!M;SEA8*spazn*IBcPl zoT7xD;MkcZ&)(~47-yUMv(zNYyMv@y^zRAM5%$8!$P?U6kF$nhfyFkA@v8&qIE3hQ zhyRVn5qlB62pfTe=``CCu2sBNaK7fbB}z**TEK{mkPvx}3VRE~#Q$OjsXQq>sk~_{ zDZDK#QG6+Usr)JYsRBU00FW=3!ksFV#*`umWDBPV1v6-hOb%qM-F%tRn30ipGCQ-u z=IhKpjEq8?Rajp!a!Ua%E0P8gGLt3Q)fpu>JF@pMGfGT8$0f_CIQb1%j<_CBwo1Us z$2C6GF(^bYIX^EiHMvB7b3XSGCLJS?R&5Z$10obb1jsl|#v(x=Rm=z^6pBQ^!jt3q zBp9_PSMa@&FarsggA_1>6evKI7*4L?mt-`YJdIz6(- 1000 and ir_beacon_2.isConnected and ir_beacon_1.isConnected): + toggleBeacon() + +def toggleBeacon(): + global activeBeacon + global lastBeaconRead + lastBeaconRead = millis() + if activeBeacon == 1: + activeBeacon = 2 + elif activeBeacon == 2: + activeBeacon = 1 + +async def cmd_handler(): + global robotcmd + global irbeaconcmd + global enabled + global activeBeacon + waitForEnableReleased = 0 + lastState = 0 + drivestate = 1 + while True: + x,y,drivecmd,robottuning,irbeacontuning,boost = (0,)*6 + + if get_key_state("Key.up"): + y = y + 1 + if get_key_state("Key.down"): + y = y - 1 + if get_key_state("Key.left"): + x = x - 1 + if get_key_state("Key.right"): + x = x + 1 + + if x == 0 and y == 0: + drivecmd = 0 + elif x == 0 and y == 1: + drivecmd = 1 + elif x == 1 and y == 1: + drivecmd = 2 + elif x == 1 and y == 0: + drivecmd = 3 + elif x == 1 and y == -1: + drivecmd = 4 + elif x == 0 and y == -1: + drivecmd = 5 + elif x == -1 and y == -1: + drivecmd = 6 + elif x == -1 and y == 0: + drivecmd = 7 + elif x == -1 and y == 1: + drivecmd = 8 + + if get_key_state('q'): + robottuning = 1 + elif get_key_state('a'): + robottuning = 2 + elif get_key_state('w'): + robottuning = 3 + elif get_key_state('s'): + robottuning = 4 + elif get_key_state('e'): + robottuning = 5 + elif get_key_state('d'): + robottuning = 6 + + if get_key_state('t'): + irbeacontuning = 1 + elif get_key_state('g'): + irbeacontuning = 2 + + if get_key_state("Key.space"): + if get_key_state("Key.ctrl"): + enabled = drivestate + waitForEnableReleased = 1 + else: + if not waitForEnableReleased: + enabled = 0 + if not (get_key_state("Key.space")) and not (get_key_state("Key.ctrl")): + waitForEnableReleased = 0 + + if (enabled != 0): + if (get_key_state('z') or get_key_state('Z')): + enabled = drivestate = 1 + if (get_key_state('x') or get_key_state('X')): + enabled = drivestate = 2 + + curState = get_key_state('1') + if curState and not lastState: + toggleBeacon() + lastState = curState + + if (get_key_state("Key.shift")): + boost = 1 + + if get_key_state("u"): + calibrate_accel = 1 + elif get_key_state("j"): + calibrate_accel = 2 + else: + calibrate_accel = 0 + + robotcmd = f"{enabled}{drivecmd}{robottuning}{boost}{calibrate_accel}0" + irbeaconcmd = f"{enabled}{activeBeacon}{irbeacontuning}000" + await asyncio.sleep(0.05) + +async def main_async_tasks(): + await asyncio.gather(ir_beacon_switcher(), cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(oreo, True), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(oreo), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) + # await asyncio.gather(ir_beacon_switcher(), cmd_handler(), bluetooth_comm_handler(ir_beacon_1, False), bluetooth_comm_handler(oreo, True), bluetooth_comm_handler(hockey_puck, True), bluetooth_receive_handler(hockey_puck), bluetooth_receive_handler(ir_beacon_1), bluetooth_receive_handler(oreo), bluetooth_comm_handler(ir_beacon_2, False), bluetooth_receive_handler(ir_beacon_2)) + +def start_async_tasks(): + asyncio.run(main_async_tasks()) + + +async_thread = threading.Thread(target=start_async_tasks) +async_thread.start() + +# Use an empty container to update just the status UI +status_container = st.empty() + +while True: + with status_container.container(): # Using st.container() instead of st.empty() + status_cols = st.columns(len(bt_devices)) + for i, bt_device in enumerate(bt_devices): + with status_cols[i]: + st.subheader(bt_device._peripheral_name) + if (bt_device.isConnected): + st.progress(bt_device.batteryPerc / 100) # Normalize battery level (0 to 1 for progress bar) + st.caption(f"Battery: {bt_device.batteryPerc}%") + else: + st.error("Disconnected") + + time.sleep(0.1) # Update interval in seconds \ No newline at end of file From 26d08c72a73f182f8b374692574ebf5bf95300f5 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Sun, 16 Feb 2025 12:18:55 -0800 Subject: [PATCH 45/48] update --- Robot Code/lib/Melty/melty.cpp | 50 ++++++++++++++++++++++------------ Robot Code/lib/Melty/melty.h | 4 +-- Robot Code/src/main.cpp | 23 +++++++--------- controller/controller.py | 1 + 4 files changed, 46 insertions(+), 32 deletions(-) diff --git a/Robot Code/lib/Melty/melty.cpp b/Robot Code/lib/Melty/melty.cpp index 557c564..002cd67 100644 --- a/Robot Code/lib/Melty/melty.cpp +++ b/Robot Code/lib/Melty/melty.cpp @@ -70,34 +70,50 @@ void melty::computeTimings() { } -bool melty::translate() { // Returns whether or not robot should translate now +int melty::translate() { // Returns percentage that robot should translate unsigned long currentTime = micros(); if (percentageOfRotation != 0) { for (int i = 0; i < TRANSLATE_TIMINGS_SIZE; i++) { - if (currentTime > startDrive[i] && currentTime < endDrive[i]) - return 1; + if (currentTime > startDrive[i] && currentTime < endDrive[i]) { + int delta = endDrive[i] - startDrive[i]; + int timeSinceStart = currentTime - startDrive[i]; + int maxPowerAt = delta/2; + int distanceFromMax = abs(timeSinceStart - maxPowerAt); + + return 100 - (100 * distanceFromMax/maxPowerAt); + } } - return 0; - } - else - return 0; -} -bool melty::translateInverse() { // Returns whether or not robot should translate now - unsigned long currentTime = micros(); - - if (percentageOfRotation != 0) { for (int i = 0; i < TRANSLATE_TIMINGS_SIZE; i++) { - if (currentTime > startDriveInverse[i] && currentTime < endDriveInverse[i]) - return 1; + if (currentTime > startDriveInverse[i] && currentTime < endDriveInverse[i]) { + int delta = endDriveInverse[i] - startDriveInverse[i]; + int timeSinceStart = currentTime - startDriveInverse[i]; + int maxPowerAt = delta/2; + int distanceFromMax = abs(timeSinceStart - maxPowerAt); + + return -(100 - (100 * distanceFromMax/maxPowerAt)); + } } - return 0; + } - else - return 0; + return 0; } +// bool melty::translateInverse() { // Returns whether or not robot should translate now +// unsigned long currentTime = micros(); + +// if (percentageOfRotation != 0) { +// for (int i = 0; i < TRANSLATE_TIMINGS_SIZE; i++) { +// if (currentTime > startDriveInverse[i] && currentTime < endDriveInverse[i]) +// return 1; +// } +// return 0; +// } +// else +// return 0; +// } + bool melty::isBeaconSensed(bool currentReading) { // for (int i = 0; i < IRLedDataSize; i++) // USBSerial.print(IRLedReadings[i]); diff --git a/Robot Code/lib/Melty/melty.h b/Robot Code/lib/Melty/melty.h index 746071c..d0506b8 100644 --- a/Robot Code/lib/Melty/melty.h +++ b/Robot Code/lib/Melty/melty.h @@ -11,8 +11,8 @@ class melty { bool update(); bool isBeaconSensed(bool currentReading); void computeTimings(); - bool translate(); - bool translateInverse(); + int translate(); + // bool translateInverse(); int ledRPM = 0; unsigned long acccel_period = 1; int deg = 0; diff --git a/Robot Code/src/main.cpp b/Robot Code/src/main.cpp index 25b868e..f338c05 100644 --- a/Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -42,24 +42,23 @@ void setup() init_led(); setLeds(ORANGE); - delay(250); // Power up delay for the Serial - USBSerial.begin(115200); SPIFFS.begin(true); - + motor_settings.updateFromDatabase(); - + driveMotors.init_motors(); // <- This needs to be init first or else something with RMT doesnt work.... pinMode(RED_LED_BOT, OUTPUT); pinMode(RED_LED_TOP, OUTPUT); digitalWrite(RED_LED_BOT, HIGH); digitalWrite(RED_LED_TOP, HIGH); - driveMotors.arm_motors(); laptop.init_ble("Oreo"); setLeds(BLACK); - + + delay(250); // Power up delay for the Serial + USBSerial.begin(115200); } void loop() @@ -127,13 +126,11 @@ void loop() int adjRotValue = melty_parameters.rot + boostVal; int adjTransValue = melty_parameters.tra + boostVal; - - if (oreo.translate()) { - driveMotors.l_motor_write(adjRotValue - adjTransValue); - driveMotors.r_motor_write(adjRotValue + adjTransValue); - } else if (oreo.translateInverse()) { - driveMotors.l_motor_write(adjRotValue + adjTransValue); - driveMotors.r_motor_write(adjRotValue - adjTransValue); + float transMultiplier = oreo.translate() / 100.0; + + if (transMultiplier != 0) { + driveMotors.l_motor_write(adjRotValue - adjTransValue * transMultiplier); + driveMotors.r_motor_write(adjRotValue + adjTransValue * transMultiplier); } else { driveMotors.set_both_motors((adjRotValue)); } diff --git a/controller/controller.py b/controller/controller.py index 1ffc8f5..a01d240 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -169,3 +169,4 @@ async def main(): asyncio.run(main()) + \ No newline at end of file From b56a3a5f53d5555bc7c099603f2adb67dea3ea67 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Sun, 13 Apr 2025 23:20:44 -0700 Subject: [PATCH 46/48] before cleaning up code, adding enums to ble class --- Robot Code/lib/BLE_Uart/BLE_Uart.h | 13 +++++++++++-- Robot Code/src/main.cpp | 6 +++--- controller/controller.py | 9 +-------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Robot Code/lib/BLE_Uart/BLE_Uart.h b/Robot Code/lib/BLE_Uart/BLE_Uart.h index 21d8a57..4ca4a09 100644 --- a/Robot Code/lib/BLE_Uart/BLE_Uart.h +++ b/Robot Code/lib/BLE_Uart/BLE_Uart.h @@ -12,5 +12,14 @@ class BLE_Uart { void send(float value); bool isConnected(); private: - -}; \ No newline at end of file +}; + +// enum class ROBOT_MODES { +// IDLE = '0', +// MELTY = '1', +// TANK = '2' +// }; + +// class Robot_BLE_Uart : BLE_Uart { + +// } \ No newline at end of file diff --git a/Robot Code/src/main.cpp b/Robot Code/src/main.cpp index f338c05..cfce417 100644 --- a/Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -63,15 +63,15 @@ void setup() void loop() { - if (!laptop.isConnected() || laptop_packetBuffer[0] == '1') { // Turn off high powered leds if not meltying + if (!laptop.isConnected() || laptop_packetBuffer[0] != '1') { // Turn off high powered leds if not meltying digitalWrite(RED_LED_TOP, HIGH); digitalWrite(RED_LED_BOT, HIGH); } if (laptop.isConnected()) { EVERY_N_MILLIS(50) { - myPTs.checkIsFlipped(); - if (!myPTs.isFlippedResult) { // Not flipped + // myPTs.printDebugInfo(); + if (!myPTs.checkIsFlipped()) { // Not flipped oreo.useTopIr = 1; driveMotors.flip_motors = 0; setLedMode(TOP); diff --git a/controller/controller.py b/controller/controller.py index a01d240..13c8890 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -151,15 +151,8 @@ async def cmd_handler(): if (get_key_state("Key.shift")): boost = 1 - - if get_key_state("u"): - calibrate_accel = 1 - elif get_key_state("j"): - calibrate_accel = 2 - else: - calibrate_accel = 0 - robotcmd = f"{enabled}{drivecmd}{robottuning}{boost}{calibrate_accel}0" + robotcmd = f"{enabled}{drivecmd}{robottuning}{boost}00" irbeaconcmd = f"{enabled}{activeBeacon}{irbeacontuning}000" await asyncio.sleep(0.05) From 259bd9020fa0b455bfcbd72ee0ac2f07a5510662 Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Sat, 17 May 2025 09:26:24 -0700 Subject: [PATCH 47/48] right before scar competition --- Robot Code/.vscode/settings.json | 38 +++++++++++++++++++++++++++- Robot Code/data/motor_settings.txt | 4 +-- Robot Code/lib/BLE_Uart/BLE_Uart.cpp | 9 +++++++ Robot Code/lib/BLE_Uart/BLE_Uart.h | 24 ++++++++++++------ Robot Code/src/main.cpp | 25 +++++++++--------- controller/controller.py | 1 - 6 files changed, 77 insertions(+), 24 deletions(-) diff --git a/Robot Code/.vscode/settings.json b/Robot Code/.vscode/settings.json index 936cb56..151a080 100644 --- a/Robot Code/.vscode/settings.json +++ b/Robot Code/.vscode/settings.json @@ -1,5 +1,8 @@ { "files.associations": { + "*.sqlbook": "sql", + "*.ndjson": "jsonl", + "*.dbclient-js": "javascript", "array": "cpp", "deque": "cpp", "string": "cpp", @@ -15,6 +18,39 @@ "functional": "cpp", "tuple": "cpp", "utility": "cpp", - "list": "cpp" + "list": "cpp", + "atomic": "cpp", + "cctype": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "iterator": "cpp", + "map": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "system_error": "cpp", + "type_traits": "cpp", + "fstream": "cpp", + "iomanip": "cpp", + "iosfwd": "cpp", + "limits": "cpp", + "new": "cpp", + "ostream": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "cinttypes": "cpp", + "typeinfo": "cpp" } } \ No newline at end of file diff --git a/Robot Code/data/motor_settings.txt b/Robot Code/data/motor_settings.txt index 195b657..c866e35 100644 --- a/Robot Code/data/motor_settings.txt +++ b/Robot Code/data/motor_settings.txt @@ -1,2 +1,2 @@ -{"rot" : "80", "tra" : "80", "per" : "0.5", "boost" : "50"} -{"drive" : "80", "turn" : "80", "boost" : "40"} \ No newline at end of file +{"rot" : "80", "tra" : "80", "per" : "0.5", "boost" : "100"} +{"drive" : "10", "turn" : "10", "boost" : "40"} \ No newline at end of file diff --git a/Robot Code/lib/BLE_Uart/BLE_Uart.cpp b/Robot Code/lib/BLE_Uart/BLE_Uart.cpp index 718ebb3..25a9c29 100644 --- a/Robot Code/lib/BLE_Uart/BLE_Uart.cpp +++ b/Robot Code/lib/BLE_Uart/BLE_Uart.cpp @@ -73,3 +73,12 @@ void BLE_Uart::send(float value) sprintf(conversionbuffer, "%.2f", value); send(conversionbuffer); } + +ROBOT_MODES Robot_BLE_Uart::getMode() { + auto map = RobotModeMap.find(packetBuffer[0]); + if (map != RobotModeMap.end()) { + return map->second; + } else { + return ROBOT_MODES::IDLE; + } +} diff --git a/Robot Code/lib/BLE_Uart/BLE_Uart.h b/Robot Code/lib/BLE_Uart/BLE_Uart.h index 4ca4a09..ff13c7f 100644 --- a/Robot Code/lib/BLE_Uart/BLE_Uart.h +++ b/Robot Code/lib/BLE_Uart/BLE_Uart.h @@ -3,7 +3,7 @@ #define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" #include - +#include class BLE_Uart { public: BLE_Uart(char* _packetBuffer, int _packSize); @@ -14,12 +14,20 @@ class BLE_Uart { private: }; -// enum class ROBOT_MODES { -// IDLE = '0', -// MELTY = '1', -// TANK = '2' -// }; +enum class ROBOT_MODES { + IDLE, + MELTY, + TANK +}; -// class Robot_BLE_Uart : BLE_Uart { +static std::unordered_map RobotModeMap{ + {'0', ROBOT_MODES::IDLE}, + {'1', ROBOT_MODES::MELTY}, + {'2', ROBOT_MODES::TANK} +}; -// } \ No newline at end of file +class Robot_BLE_Uart : public BLE_Uart { + public: + Robot_BLE_Uart(char* _packetBuffer, int _packSize) : BLE_Uart(_packetBuffer, _packSize) {}; + ROBOT_MODES getMode(); +}; \ No newline at end of file diff --git a/Robot Code/src/main.cpp b/Robot Code/src/main.cpp index cfce417..6bde14a 100644 --- a/Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -16,12 +16,13 @@ char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; const int headings[] = {0, 45, 90, 135, 180, 225, 270, 315}; bool wasMeltying = false; int slowDownSpeed = 200; -BLE_Uart laptop = BLE_Uart(laptop_packetBuffer, packSize); +Robot_BLE_Uart transmitter = Robot_BLE_Uart(laptop_packetBuffer, packSize); Drive_Motors driveMotors = Drive_Motors(LEFT_MOTOR_PIN, LEFT_MOTOR_CHANNEL, RIGHT_MOTOR_PIN, RIGHT_MOTOR_CHANNEL); robotOrientation myPTs = robotOrientation(TOP_PHOTO_TRANSISTOR, BOTTOM_PHOTO_TRANSISTOR); database_handler motor_settings = database_handler(); melty oreo = melty(TOP_IR_PIN, BOTTOM_IR_PIN); + struct melty_parameters { int rot; int tra; @@ -54,7 +55,7 @@ void setup() digitalWrite(RED_LED_TOP, HIGH); driveMotors.arm_motors(); - laptop.init_ble("Oreo"); + transmitter.init_ble("Oreo"); setLeds(BLACK); delay(250); // Power up delay for the Serial @@ -63,12 +64,12 @@ void setup() void loop() { - if (!laptop.isConnected() || laptop_packetBuffer[0] != '1') { // Turn off high powered leds if not meltying + if (!transmitter.isConnected() || laptop_packetBuffer[0] != '1') { // Turn off high powered leds if not meltying digitalWrite(RED_LED_TOP, HIGH); digitalWrite(RED_LED_BOT, HIGH); } - if (laptop.isConnected()) { + if (transmitter.isConnected()) { EVERY_N_MILLIS(50) { // myPTs.printDebugInfo(); if (!myPTs.checkIsFlipped()) { // Not flipped @@ -103,7 +104,7 @@ void loop() } - if (laptop_packetBuffer[0] == '1') { // Currently enabled and meltybraining!!! + if (transmitter.getMode() == ROBOT_MODES::MELTY) { // Currently enabled and meltybraining!!! setLedMode(BOTH); setLeds(BLACK); if (oreo.update()) { // If seen the LED @@ -112,7 +113,7 @@ void loop() else digitalWrite(RED_LED_BOT, LOW); EVERY_N_MILLIS(250) { - laptop.send("seen"); + transmitter.send("seen"); } } else { @@ -155,7 +156,7 @@ void loop() EVERY_N_SECONDS(1) { // DEBUGGIN!!!! String msg = "RPM : " + String(oreo.ledRPM); - laptop.send(msg); + transmitter.send(msg); } EVERY_N_MILLIS(100) { @@ -182,7 +183,7 @@ void loop() if (laptop_packetBuffer[2] != '0') { String msg = "rotpwr : " + String(melty_parameters.rot) + " tranpwr : " + String(melty_parameters.tra) + " perc : " + String(melty_parameters.per); - laptop.send(msg); + transmitter.send(msg); motor_settings.storeMeltyParameters(melty_parameters.rot, melty_parameters.tra, melty_parameters.per, melty_parameters.boost); } @@ -190,7 +191,7 @@ void loop() wasMeltying = 1; - } else if (laptop_packetBuffer[0] == '2') { // Tank driving mode! + } else if (transmitter.getMode() == ROBOT_MODES::TANK) { // Tank driving mode! int lmotorpwr = 0; int rmotorpwr = 0; @@ -257,7 +258,7 @@ void loop() if (laptop_packetBuffer[2] != '0') { String msg = "drivpwr : " + String(tank_drive_parameters.drive) + " turnpwr : " + String(tank_drive_parameters.turn); - laptop.send(msg); + transmitter.send(msg); motor_settings.storeTankParameters(tank_drive_parameters.drive, tank_drive_parameters.turn, tank_drive_parameters.boost); } @@ -267,12 +268,12 @@ void loop() driveMotors.r_motor_write(rmotorpwr); toggleLeds(WHITE, BLACK, 500); - } else { // Currently DISABLED + } else if (transmitter.getMode() == ROBOT_MODES::IDLE ){ // Currently DISABLED toggleLeds(RED, GREEN, 500); driveMotors.set_both_motors(0); EVERY_N_SECONDS(1) { - laptop.send("SOC: " + String(get3sSOC(BAT_PIN)) + " %"); + transmitter.send("SOC: " + String(get3sSOC(BAT_PIN)) + " %"); } } } else { // Currently DISCONNECTED diff --git a/controller/controller.py b/controller/controller.py index 13c8890..fbc9711 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -25,7 +25,6 @@ def millis(): enabled = 0 activeBeacon = 1 lastBeaconRead = millis() -calibrate_accel = 0 async def bluetooth_receive_handler(BLE_DEVICE): global lastBeaconRead From fff232c423e8b3225261daab77ff7cfec03477ce Mon Sep 17 00:00:00 2001 From: Mew463 <72902803+Mew463@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:55:04 -0800 Subject: [PATCH 48/48] before modifying code for oreov6 headware --- Robot Code/include/pin_definitions.h | 4 ++-- .../lib/Photo_Transistors/Photo_Transistors.h | 12 +++++----- Robot Code/src/main.cpp | 22 ++++++++----------- controller/controller.py | 2 +- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/Robot Code/include/pin_definitions.h b/Robot Code/include/pin_definitions.h index 39549b2..5e84f0d 100644 --- a/Robot Code/include/pin_definitions.h +++ b/Robot Code/include/pin_definitions.h @@ -10,7 +10,7 @@ #define LEFT_MOTOR_CHANNEL RMT_CHANNEL_2 #define RIGHT_MOTOR_CHANNEL RMT_CHANNEL_1 -#define RED_LED_TOP 39 -#define RED_LED_BOT 40 +#define RED_LED_TOP 40 +#define RED_LED_BOT 35 #define BAT_PIN 18 diff --git a/Robot Code/lib/Photo_Transistors/Photo_Transistors.h b/Robot Code/lib/Photo_Transistors/Photo_Transistors.h index a851e14..6bd857d 100644 --- a/Robot Code/lib/Photo_Transistors/Photo_Transistors.h +++ b/Robot Code/lib/Photo_Transistors/Photo_Transistors.h @@ -33,16 +33,18 @@ class robotOrientation{ bool checkIsFlipped() { topPhotoRingBuf.update(analogRead(topPin)); bottomPhotoRingBuf.update(analogRead(bottomPin)); + const int deltaVal = 1; + if (bottomPhotoRingBuf.getMaxVal() - topPhotoRingBuf.getMaxVal() > deltaVal) + curIsFlipped = 1; + if (topPhotoRingBuf.getMaxVal() - bottomPhotoRingBuf.getMaxVal() > deltaVal) + curIsFlipped = 0; - curIsFlipped = bottomPhotoRingBuf.getMaxVal() - topPhotoRingBuf.getMaxVal() > 0 ? 1 : 0; - - if (curIsFlipped == 1 && lastIsFlipped == 0) + if (curIsFlipped != lastIsFlipped) lastTransition = millis(); + if (curIsFlipped == 1 && millis() - lastTransition > delayTimeMS) isFlippedResult = 1; - if (curIsFlipped == 0 && lastIsFlipped == 1) - lastTransition = millis(); if (curIsFlipped == 0 && millis() - lastTransition > delayTimeMS) isFlippedResult = 0; diff --git a/Robot Code/src/main.cpp b/Robot Code/src/main.cpp index 6bde14a..1c552ea 100644 --- a/Robot Code/src/main.cpp +++ b/Robot Code/src/main.cpp @@ -14,8 +14,6 @@ const int packSize = 6; char laptop_packetBuffer[packSize] = {'0', '0', '0', '0', '0', '0'}; const int headings[] = {0, 45, 90, 135, 180, 225, 270, 315}; -bool wasMeltying = false; -int slowDownSpeed = 200; Robot_BLE_Uart transmitter = Robot_BLE_Uart(laptop_packetBuffer, packSize); Drive_Motors driveMotors = Drive_Motors(LEFT_MOTOR_PIN, LEFT_MOTOR_CHANNEL, RIGHT_MOTOR_PIN, RIGHT_MOTOR_CHANNEL); robotOrientation myPTs = robotOrientation(TOP_PHOTO_TRANSISTOR, BOTTOM_PHOTO_TRANSISTOR); @@ -51,8 +49,8 @@ void setup() pinMode(RED_LED_BOT, OUTPUT); pinMode(RED_LED_TOP, OUTPUT); - digitalWrite(RED_LED_BOT, HIGH); - digitalWrite(RED_LED_TOP, HIGH); + digitalWrite(RED_LED_BOT, LOW); + digitalWrite(RED_LED_TOP, LOW); driveMotors.arm_motors(); transmitter.init_ble("Oreo"); @@ -65,13 +63,13 @@ void setup() void loop() { if (!transmitter.isConnected() || laptop_packetBuffer[0] != '1') { // Turn off high powered leds if not meltying - digitalWrite(RED_LED_TOP, HIGH); - digitalWrite(RED_LED_BOT, HIGH); + digitalWrite(RED_LED_TOP, LOW); + digitalWrite(RED_LED_BOT, LOW); } if (transmitter.isConnected()) { EVERY_N_MILLIS(50) { - // myPTs.printDebugInfo(); + myPTs.printDebugInfo(); if (!myPTs.checkIsFlipped()) { // Not flipped oreo.useTopIr = 1; driveMotors.flip_motors = 0; @@ -109,16 +107,16 @@ void loop() setLeds(BLACK); if (oreo.update()) { // If seen the LED if (oreo.useTopIr) - digitalWrite(RED_LED_TOP, LOW); + digitalWrite(RED_LED_TOP, HIGH); else - digitalWrite(RED_LED_BOT, LOW); + digitalWrite(RED_LED_BOT, HIGH); EVERY_N_MILLIS(250) { transmitter.send("seen"); } } else { - digitalWrite(RED_LED_TOP, HIGH); - digitalWrite(RED_LED_BOT, HIGH); + digitalWrite(RED_LED_TOP, LOW); + digitalWrite(RED_LED_BOT, LOW); } int boostVal = 0; @@ -189,8 +187,6 @@ void loop() } } - wasMeltying = 1; - } else if (transmitter.getMode() == ROBOT_MODES::TANK) { // Tank driving mode! int lmotorpwr = 0; diff --git a/controller/controller.py b/controller/controller.py index fbc9711..ff544ab 100644 --- a/controller/controller.py +++ b/controller/controller.py @@ -12,7 +12,7 @@ def millis(): ir_beacon_2 = BLE_UART(peripheral_name='Beac 2', address = '9AC0FFF3-446C-1A64-DA89-1376064B2BA1') ir_beacon_1 = BLE_UART(peripheral_name='Beac 1', address = 'DBA047A7-4143-45D5-E469-FEEA2E354502') -oreo = BLE_UART(peripheral_name='Oreo', address = '1932D032-A476-F238-07F0-A39D5208BC73') +oreo = BLE_UART(peripheral_name='Oreo', address = '07C80925-7C0F-1236-FB54-CFC3912A3B9D') bt_devices = {ir_beacon_2, ir_beacon_1, oreo} # hockey_puck = BLE_UART(peripheral_name='Hockey Puck', address = '07C80925-7C0F-1236-FB54-CFC3912A3B9D')