1
0
Эх сурвалжийг харах

Added Firmware updater with versioning in ino files, endpoint to get current firmware version and a way to flash the firmware

Thokoop 1 жил өмнө
parent
commit
b10c852555

+ 4 - 0
CHANGELOG.md

@@ -2,6 +2,10 @@
 
 All notable changes to this project will be documented in this file.
 
+## [1.4.0] Firmware updater
+
+- Added Firmware updater to make it possible to flash the Arduino Firmware from the web app.
+
 ## [1.3.0] Revamped UI
 
 Massive thanks to Thokoop for helping us redesigning the UI just within a few days! The new design looks gorgeous on both PC and mobile. 

+ 19 - 0
Dockerfile

@@ -7,6 +7,25 @@ WORKDIR /app
 # Copy the current directory contents into the container at /app
 COPY . /app
 
+# Install required system packages
+RUN apt-get update && apt-get install -y --no-install-recommends \
+    gcc \
+    avrdude \
+    wget \
+    unzip \
+    && rm -rf /var/lib/apt/lists/*
+
+# Install arduino-cli
+RUN wget https://downloads.arduino.cc/arduino-cli/arduino-cli_latest_Linux_64bit.tar.gz && \
+    tar -xvf arduino-cli_latest_Linux_64bit.tar.gz && \
+    mv arduino-cli /usr/local/bin/ && \
+    rm arduino-cli_latest_Linux_64bit.tar.gz
+
+# Initialize arduino-cli
+RUN arduino-cli config init && \
+    arduino-cli core update-index && \
+    arduino-cli core install arduino:avr
+
 # Install any needed packages specified in requirements.txt
 RUN pip install --no-cache-dir -r requirements.txt
 

+ 217 - 0
app.py

@@ -7,6 +7,7 @@ import threading
 import serial.tools.list_ports
 import math
 import json
+import subprocess
 
 app = Flask(__name__)
 
@@ -27,12 +28,69 @@ stop_requested = False
 serial_lock = threading.Lock()
 
 PLAYLISTS_FILE = os.path.join(os.getcwd(), "playlists.json")
+OPTIONS_FILE = os.path.join(os.getcwd(), "options.json")
+MOTOR_TYPE_MAPPING = {
+    "TMC2209": "./arduino_code_TMC2209/arduino_code_TMC2209.ino",
+    "DRV8825": "./arduino_code/arduino_code.ino",
+    "esp32": "./esp32/esp32.ino"
+}
 
 # Ensure the file exists and contains at least an empty JSON object
 if not os.path.exists(PLAYLISTS_FILE):
     with open(PLAYLISTS_FILE, "w") as f:
         json.dump({}, f, indent=2)
 
+# Ensure the file exists and contains at least an empty JSON object
+if not os.path.exists(OPTIONS_FILE):
+    with open(PLAYLISTS_FILE, "w") as f:
+        json.dump({}, f, indent=2)
+
+def get_ino_firmware_details(ino_file_path):
+    """
+    Extract firmware details, including version and motor type, from the given .ino file.
+
+    Args:
+        ino_file_path (str): Path to the .ino file.
+
+    Returns:
+        dict: Dictionary containing firmware details such as version and motor type, or None if not found.
+    """
+    try:
+        if not ino_file_path:
+            raise ValueError("Invalid path: ino_file_path is None or empty.")
+
+        firmware_details = {"version": None, "motorType": None}
+
+        with open(ino_file_path, "r") as file:
+            for line in file:
+                # Extract firmware version
+                if "firmwareVersion" in line:
+                    start = line.find('"') + 1
+                    end = line.rfind('"')
+                    if start != -1 and end != -1 and start < end:
+                        firmware_details["version"] = line[start:end]
+
+                # Extract motor type
+                if "motorType" in line:
+                    start = line.find('"') + 1
+                    end = line.rfind('"')
+                    if start != -1 and end != -1 and start < end:
+                        firmware_details["motorType"] = line[start:end]
+
+        if not firmware_details["version"]:
+            print(f"Firmware version not found in file: {ino_file_path}")
+        if not firmware_details["motorType"]:
+            print(f"Motor type not found in file: {ino_file_path}")
+
+        return firmware_details if any(firmware_details.values()) else None
+
+    except FileNotFoundError:
+        print(f"File not found: {ino_file_path}")
+        return None
+    except Exception as e:
+        print(f"Error reading .ino file: {str(e)}")
+        return None
+
 def list_serial_ports():
     """Return a list of available serial ports."""
     ports = serial.tools.list_ports.comports()
@@ -481,6 +539,7 @@ def send_coordinate():
 
         # Send the coordinate to the Arduino
         send_coordinate_batch(ser, [(theta, rho)])
+        reset_theta()
         return jsonify({"success": True})
     except Exception as e:
         return jsonify({"success": False, "error": str(e)}), 500
@@ -750,6 +809,164 @@ def set_speed():
     except Exception as e:
         return jsonify({"success": False, "error": str(e)}), 500
 
+@app.route('/get_firmware_info', methods=['GET', 'POST'])
+def get_firmware_info():
+    """
+    Compare the installed firmware version and motor type with the one in the .ino file.
+    """
+    global ser
+    if ser is None or not ser.is_open:
+        return jsonify({"success": False, "error": "Arduino not connected or serial port not open"}), 400
+
+    try:
+        if request.method == "GET":
+            # Attempt to retrieve installed firmware details from the Arduino
+            ser.reset_input_buffer()
+            ser.reset_output_buffer()
+            ser.write(b"GET_VERSION\n")
+            time.sleep(0.5)
+
+            installed_version = 'Unknown'
+            installed_type = 'Unknown'
+            if ser.in_waiting > 0:
+                response = ser.readline().decode().strip()
+                if " | " in response:
+                    installed_version, installed_type = response.split(" | ", 1)
+
+            # If Arduino provides valid details, proceed with comparison
+            if installed_version != 'Unknown' and installed_type != 'Unknown':
+                ino_path = MOTOR_TYPE_MAPPING.get(installed_type)
+                firmware_details = get_ino_firmware_details(ino_path)
+
+                if not firmware_details or not firmware_details.get("version") or not firmware_details.get("motorType"):
+                    return jsonify({"success": False, "error": "Failed to retrieve .ino firmware details"}), 500
+
+                update_available = (
+                    installed_version != firmware_details["version"] or
+                    installed_type != firmware_details["motorType"]
+                )
+
+                return jsonify({
+                    "success": True,
+                    "installedVersion": installed_version,
+                    "installedType": installed_type,
+                    "inoVersion": firmware_details["version"],
+                    "inoType": firmware_details["motorType"],
+                    "updateAvailable": update_available
+                })
+
+            # If Arduino details are unknown, indicate the need for POST
+            return jsonify({
+                "success": True,
+                "installedVersion": installed_version,
+                "installedType": installed_type,
+                "updateAvailable": False
+            })
+
+        if request.method == "POST":
+            motor_type = request.json.get("motorType", None)
+            if not motor_type or motor_type not in MOTOR_TYPE_MAPPING:
+                return jsonify({"success": False, "error": "Invalid or missing motor type"}), 400
+
+            ino_path = MOTOR_TYPE_MAPPING.get(motor_type)
+            firmware_details = get_ino_firmware_details(ino_path)
+
+            if not firmware_details or not firmware_details.get("version") or not firmware_details.get("motorType"):
+                return jsonify({"success": False, "error": "Failed to retrieve .ino firmware details"}), 500
+
+            return jsonify({
+                "success": True,
+                "installedVersion": 'Unknown',
+                "installedType": motor_type,
+                "inoVersion": firmware_details["version"],
+                "inoType": firmware_details["motorType"],
+                "updateAvailable": True
+            })
+    except Exception as e:
+        return jsonify({"success": False, "error": str(e)}), 500
+
+@app.route('/flash_firmware', methods=['POST'])
+def flash_firmware():
+    """
+    Compile and flash the firmware to the connected Arduino.
+    """
+    global ser_port
+
+    # Ensure the Arduino is connected
+    if ser_port is None or ser is None or not ser.is_open:
+        return jsonify({"success": False, "error": "No Arduino connected or connection lost"}), 400
+
+    build_dir = "/tmp/arduino_build"  # Temporary build directory
+
+    try:
+        data = request.json
+        motor_type = data.get("motorType", None)
+
+        # Validate motor type
+        if not motor_type or motor_type not in MOTOR_TYPE_MAPPING:
+            return jsonify({"success": False, "error": "Invalid or missing motor type"}), 400
+
+        # Get the .ino file path based on the motor type
+        ino_file_path = MOTOR_TYPE_MAPPING[motor_type]
+        ino_file_name = os.path.basename(ino_file_path)
+
+        # Install required libraries
+        required_libraries = ["AccelStepper"]  # AccelStepper includes MultiStepper
+        for library in required_libraries:
+            library_install_command = ["arduino-cli", "lib", "install", library]
+            install_process = subprocess.run(library_install_command, capture_output=True, text=True)
+            if install_process.returncode != 0:
+                return jsonify({
+                    "success": False,
+                    "error": f"Library installation failed for {library}: {install_process.stderr}"
+                }), 500
+
+        # Step 1: Compile the .ino file to a .hex file
+        compile_command = [
+            "arduino-cli",
+            "compile",
+            "--fqbn", "arduino:avr:uno",  # Use the detected FQBN
+            "--output-dir", build_dir,
+            ino_file_path
+        ]
+
+        compile_process = subprocess.run(compile_command, capture_output=True, text=True)
+        if compile_process.returncode != 0:
+            return jsonify({
+                "success": False,
+                "error": compile_process.stderr
+            }), 500
+
+        # Step 2: Flash the .hex file to the Arduino
+        hex_file_path = os.path.join(build_dir, ino_file_name+".hex")
+        flash_command = [
+            "avrdude",
+            "-v",
+            "-c", "arduino",  # Programmer type
+            "-p", "atmega328p",  # Microcontroller type
+            "-P", ser_port,  # Use the dynamic serial port
+            "-b", "115200",  # Baud rate
+            "-D",
+            "-U", f"flash:w:{hex_file_path}:i"  # Flash memory write command
+        ]
+
+        flash_process = subprocess.run(flash_command, capture_output=True, text=True)
+        if flash_process.returncode != 0:
+            return jsonify({
+                "success": False,
+                "error": flash_process.stderr
+            }), 500
+
+        return jsonify({"success": True, "message": "Firmware flashed successfully"})
+    except Exception as e:
+        return jsonify({"success": False, "error": str(e)}), 500
+    finally:
+        # Clean up temporary files
+        if os.path.exists(build_dir):
+            for file in os.listdir(build_dir):
+                os.remove(os.path.join(build_dir, file))
+            os.rmdir(build_dir)
+
 if __name__ == '__main__':
     # Auto-connect to serial
     connect_to_serial()

+ 14 - 1
arduino_code/arduino_code.ino

@@ -48,6 +48,10 @@ float userDefinedSpeed = maxSpeed; // Store user-defined speed
 // Running Mode
 int currentMode = MODE_APP; // Default mode is app mode.
 
+// FIRMWARE VERSION
+const char* firmwareVersion = "1.4.1";
+const char* motorType = "DRV8825";
+
 void setup()
 {
     // Set maximum speed and acceleration
@@ -180,12 +184,21 @@ void appMode()
         String input = Serial.readStringUntil('\n');
 
         // Ignore invalid messages
-        if (input != "HOME" && input != "RESET_THETA" && !input.startsWith("SET_SPEED") && !input.endsWith(";"))
+        if (input != "HOME" && input != "RESET_THETA" && input != "GET_VERSION" && !input.startsWith("SET_SPEED") && !input.endsWith(";"))
         {
             Serial.print("IGNORED: ");
             Serial.println(input);
             return;
         }
+
+        if (input == "GET_VERSION")
+        {
+            Serial.print(firmwareVersion);
+            Serial.print(" | ");
+            Serial.println(motorType);
+            return;
+        }
+
         if (input == "RESET_THETA")
         {
             resetTheta(); // Reset currentTheta

+ 15 - 2
arduino_code_TMC2209/arduino_code_TMC2209.ino

@@ -48,6 +48,10 @@ float userDefinedSpeed = maxSpeed; // Store user-defined speed
 // Running Mode
 int currentMode = MODE_APP; // Default mode is app mode.
 
+// FIRMWARE VERSION
+const char* firmwareVersion = "1.5.0";
+const char* motorType = "TMC2209";
+
 void setup()
 {
     // Set maximum speed and acceleration
@@ -72,7 +76,6 @@ void setup()
     homing();
 }
 
-
 void resetTheta()
 {
     isFirstCoordinates = true; // Set flag to skip interpolation for the next movement
@@ -181,12 +184,21 @@ void appMode()
         String input = Serial.readStringUntil('\n');
 
         // Ignore invalid messages
-        if (input != "HOME" && input != "RESET_THETA" && !input.startsWith("SET_SPEED") && !input.endsWith(";"))
+        if (input != "HOME" && input != "RESET_THETA" && input != "GET_VERSION" && !input.startsWith("SET_SPEED") && !input.endsWith(";"))
         {
             Serial.print("IGNORED: ");
             Serial.println(input);
             return;
         }
+
+        if (input == "GET_VERSION")
+        {
+            Serial.print(firmwareVersion);
+            Serial.print(" | ");
+            Serial.println(motorType);
+            return;
+        }
+
         if (input == "RESET_THETA")
         {
             resetTheta(); // Reset currentTheta
@@ -194,6 +206,7 @@ void appMode()
             Serial.println("READY");
             return;
         }
+
         if (input == "HOME")
         {
             homing();

+ 14 - 1
esp32/esp32.ino

@@ -44,6 +44,10 @@ double maxSpeed = 500;
 double maxAcceleration = 5000;
 double subSteps = 1;
 
+// FIRMWARE VERSION
+const char* firmwareVersion = "1.4.0";
+const char* motorType = "esp32";
+
 int modulus(int x, int y) {
   return x < 0 ? ((x + 1) % y) + y - 1 : x % y;
 }
@@ -75,12 +79,21 @@ void loop()
         String input = Serial.readStringUntil('\n');
 
         // Ignore invalid messages
-        if (input != "HOME" && input != "RESET_THETA" && !input.startsWith("SET_SPEED") && !input.endsWith(";"))
+        if (input != "HOME" && input != "RESET_THETA"  && input != "GET_VERSION" && !input.startsWith("SET_SPEED") && !input.endsWith(";"))
         {
             Serial.println("IGNORED");
             return;
         }
 
+        if (input == "GET_VERSION")
+        {
+            Serial.print(firmwareVersion);
+            Serial.print(" | ");
+            Serial.println(motorType);
+            return;
+        }
+
+
         // Example: The user calls "SET_SPEED 60" => 60% of maxSpeed
         if (input.startsWith("SET_SPEED"))
         {

+ 138 - 0
static/main.js

@@ -555,6 +555,144 @@ async function restartSerial() {
     }
 }
 
+// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+//  Firmware / Software Updater
+// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+async function fetchFirmwareInfo(motorType = null) {
+    const checkButton = document.getElementById("check_updates_button");
+    const motorTypeElement = document.getElementById("motor_type");
+    const currentVersionElement = document.getElementById("current_firmware_version");
+    const newVersionElement = document.getElementById("new_firmware_version");
+    const motorSelectionDiv = document.getElementById("motor_selection");
+    const updateButtonElement = document.getElementById("update_firmware_button");
+
+    try {
+        // Disable the button while fetching
+        checkButton.disabled = true;
+        checkButton.textContent = "Checking...";
+
+        // Prepare fetch options
+        const options = motorType
+            ? {
+                method: "POST",
+                headers: { "Content-Type": "application/json" },
+                body: JSON.stringify({ motorType }),
+            }
+            : { method: "GET" };
+
+        const response = await fetch("/get_firmware_info", options);
+        if (!response.ok) {
+            throw new Error(`Server responded with status ${response.status}`);
+        }
+
+        const data = await response.json();
+        if (data.success) {
+            const { installedVersion, installedType, inoVersion, inoType, updateAvailable } = data;
+
+            // Handle unknown motor type
+            if (!installedType || installedType === "Unknown") {
+                motorSelectionDiv.style.display = "flex"; // Show the dropdown
+                updateButtonElement.style.display = "none"; // Hide update button
+                checkButton.style.display = "none";
+            } else {
+                // Display motor type
+                motorTypeElement.textContent = `Type: ${installedType || "Unknown"}`;
+                // Pre-select the correct motor type in the dropdown
+                const motorSelect = document.getElementById("manual_motor_type");
+                if (motorSelect) {
+                    Array.from(motorSelect.options).forEach(option => {
+                        option.selected = option.value === installedType;
+                    });
+                }
+
+                // Display firmware versions
+                currentVersionElement.textContent = `Current version: ${installedVersion || "Unknown"}`;
+
+                if (updateAvailable) {
+                    newVersionElement.textContent = `New version: ${inoVersion}`;
+                    updateButtonElement.style.display = "block";
+                    checkButton.style.display = "none";
+                } else {
+                    newVersionElement.textContent = "You are up to date!";
+                    updateButtonElement.style.display = "none";
+                    checkButton.style.display = "none";
+                }
+            }
+        } else {
+            logMessage("Error fetching firmware info.", LOG_TYPE.ERROR);
+            logMessage(data.error, LOG_TYPE.DEBUG);
+        }
+    } catch (error) {
+        logMessage("Error fetching firmware info.", LOG_TYPE.ERROR);
+        logMessage(error.message, LOG_TYPE.DEBUG);
+    } finally {
+        // Re-enable the button after fetching
+        checkButton.disabled = false;
+        checkButton.textContent = "Check for Updates";
+    }
+}
+
+function setMotorType() {
+    const selectElement = document.getElementById("manual_motor_type");
+    const selectedMotorType = selectElement.value;
+
+    if (!selectedMotorType) {
+        logMessage("Please select a motor type before proceeding.", LOG_TYPE.WARNING);
+        return;
+    }
+
+    const motorSelectionDiv = document.getElementById("motor_selection");
+    motorSelectionDiv.style.display = "none";
+
+    // Call fetchFirmwareInfo with the selected motor type
+    fetchFirmwareInfo(selectedMotorType);
+}
+
+async function updateFirmware() {
+    const button = document.getElementById("update_firmware_button");
+    const motorTypeDropdown = document.getElementById("manual_motor_type");
+    const motorType = motorTypeDropdown ? motorTypeDropdown.value : null;
+
+    if (!motorType) {
+        logMessage("Motor type is not set. Please select a motor type.", LOG_TYPE.WARNING);
+        return;
+    }
+
+    button.disabled = true;
+    button.textContent = "Updating...";
+
+    try {
+        logMessage("Firmware update started...", LOG_TYPE.INFO);
+
+        const response = await fetch("/flash_firmware", {
+            method: "POST",
+            headers: { "Content-Type": "application/json" },
+            body: JSON.stringify({ motorType }),
+        });
+
+        const data = await response.json();
+        if (data.success) {
+            logMessage("Firmware updated successfully!", LOG_TYPE.SUCCESS);
+            // Refresh the firmware info to update current version
+            logMessage("Refreshing firmware info...");
+            await fetchFirmwareInfo();
+
+            // Display "You're up to date" message if versions match
+            const newVersionElement = document.getElementById("new_firmware_version");
+            newVersionElement.textContent = "You're up to date!";
+            const motorSelectionDiv = document.getElementById("motor_selection");
+            motorSelectionDiv.style.display = "none";
+        } else {
+            logMessage(`Firmware update failed: ${data.error}`, LOG_TYPE.ERROR);
+        }
+    } catch (error) {
+        logMessage(`Error during firmware update: ${error.message}`, LOG_TYPE.ERROR);
+    } finally {
+        button.disabled = false; // Re-enable button
+        button.textContent = "Update Firmware";
+    }
+}
+
 
 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 //  PART A: Loading / listing playlists from the server

+ 27 - 0
static/style.css

@@ -248,6 +248,33 @@ section.debug {
     justify-content: space-between;
 }
 
+section.version {
+    flex-direction: row;
+    justify-content: space-between;
+    flex-wrap: wrap;
+}
+
+.version .header {
+    width: 100%;
+}
+
+.version #motor_selection h3 {
+    width: 100%;
+    flex-grow: 1;
+}
+
+.version #motor_selection {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    width: 100%;
+}
+
+.version select#manual_motor_type {
+    margin: 0 20px 0 0;
+    flex: 1;
+}
+
 section.sticky {
     position: fixed;
     background-color: rgba(255, 255, 255, 0.5);

+ 39 - 16
templates/index.html

@@ -155,22 +155,6 @@
 
     <!-- Device / Settings Tab -->
     <main class="tab-content" id="settings-tab">
-        <section>
-            <div class="header">
-                <h2>Serial Connection</h2>
-            </div>
-            <div id="serial_status_container">Status: <span id="serial_status" class="status"> Not connected</span></div>
-            <div id="serial_ports_container">
-                <label for="serial_ports">Available Ports:</label>
-                <select id="serial_ports"></select>
-                <button onclick="connectSerial()" class="cta">Connect</button>
-            </div>
-            <div id="serial_ports_buttons" class="button-group">
-                <button onclick="disconnectSerial()" class="cancel">Disconnect</button>
-                <button onclick="restartSerial()" class="warn">Restart</button>
-            </div>
-        </section>
-
         <section class="main">
             <div class="header">
                 <h2>Device Controls</h2>
@@ -226,6 +210,45 @@
             </div>
         </section>
 
+        <section>
+            <div class="header">
+                <h2>Serial Connection</h2>
+            </div>
+            <div id="serial_status_container">Status: <span id="serial_status" class="status"> Not connected</span></div>
+            <div id="serial_ports_container">
+                <label for="serial_ports">Available Ports:</label>
+                <select id="serial_ports"></select>
+                <button onclick="connectSerial()" class="cta">Connect</button>
+            </div>
+            <div id="serial_ports_buttons" class="button-group">
+                <button onclick="disconnectSerial()" class="cancel">Disconnect</button>
+                <button onclick="restartSerial()" class="warn">Restart</button>
+            </div>
+        </section>
+
+        <section class="version">
+            <div class="header">
+                <h2>Firmware Update</h2>
+            </div>
+            <div id="firmware_info">
+                <div id="motor_type"></div>
+                <div id="current_firmware_version"></div>
+                <div id="new_firmware_version"></div>
+            </div>
+            <div class="button-group">
+                <div id="motor_selection" style="display: none;">
+                    <h3>Select installed motor driver</h3>
+                    <select id="manual_motor_type">
+                        <option value="TMC2209">Arduino TMC2209</option>
+                        <option value="DRV8825">Arduino DRV8825</option>
+                        <option value="esp32">ESP32</option>
+                    </select>
+                    <button id="set_motor_type_button" onclick="setMotorType()">Set Motor Type</button>
+                </div>
+                <button id="check_updates_button" onclick="fetchFirmwareInfo()">Check for Updates</button>
+                <button id="update_firmware_button" class="cta" style="display: none;" onclick="updateFirmware()">Update Firmware</button>
+            </div>
+        </section>
         <section class="debug">
             <div id="github">
                 <span>Help us improve! <a href="https://github.com/tuanchris/dune-weaver/pulls" target="_blank">Submit a Pull Request</a> or <a