diff --git a/firmware/Makefile b/firmware/Makefile index b45fa23..8620424 100644 --- a/firmware/Makefile +++ b/firmware/Makefile @@ -17,6 +17,7 @@ clean: esphome clean lolin_s2_mini.yaml esphome clean lolin_s3_mini.yaml esphome clean esp32-s3-devkitc-1.yaml + esphome clean esp32_s3_super_mini.yaml rm -rf .esphome/idedata/* rm -rf ~/.platformio/penv # build-esp32s2-mini: @@ -71,4 +72,7 @@ lolin_s3_mini: run lolin_s3_mini.yaml --device $(USB_ADDRESS) devkit: esphome \ - run esp32-s3-devkitc-1.yaml --device $(USB_ADDRESS) \ No newline at end of file + run esp32-s3-devkitc-1.yaml --device $(USB_ADDRESS) +s3_super_mini_manual_ams: + esphome \ + run esp32-s3-super-mini-manual-ams.yaml --device $(USB_ADDRESS) \ No newline at end of file diff --git a/firmware/common-manual-ams.yaml b/firmware/common-manual-ams.yaml new file mode 100644 index 0000000..a95f74a --- /dev/null +++ b/firmware/common-manual-ams.yaml @@ -0,0 +1,87 @@ +--- +preferences: + flash_write_interval: 3sec + +substitutions: + name: openspool + +esphome: + name: ${name} + name_add_mac_suffix: true + project: + name: spuder.openspool + version: ${version} + min_version: 2025.6.0 + platformio_options: + build_unflags: -std=gnu++11 + build_flags: + - -std=gnu++14 + - -DMBEDTLS_CONFIG_FILE=\"mbedtls/esp_config.h\" + on_boot: + then: + #TODO: breahting blue studders a little bit + - light.turn_on: + id: neopixel_light + effect: Breathing Blue + - wait_until: + condition: + wifi.connected: + - delay: 100ms + - light.turn_on: + id: neopixel_light + effect: none + - delay: 100ms + - light.turn_on: + id: neopixel_light + effect: Rainbow + - delay: 1s + - light.turn_off: + id: neopixel_light + - delay: 500ms + - script.execute: select_led + - if: + condition: + lambda: |- + return !id(bambu_lan_access_code).state.empty() && + !id(bambu_ip_address).state.empty() && + !id(bambu_serial_number).state.empty(); + then: + - logger.log: + level: info + format: "Connecting to Bambu printer" + - mqtt.enable: + id: bambu_mqtt + else: + - logger.log: + level: info + format: "Missing Bambu Credentials, skipping mqtt connect" + - mqtt.disable: + id: bambu_mqtt + on_shutdown: + then: + - light.turn_off: + id: neopixel_light + +esp32: + framework: + type: esp-idf + sdkconfig_options: + CONFIG_MBEDTLS_HKDF_C: y # Needed for bambu KDF + CONFIG_MBEDTLS_MD_C: y # Needed for bambu KDF + +packages: + version: !include conf.d/version.yaml + uptime: !include conf.d/uptime.yaml + wifi: !include conf.d/wifi.yaml + web_server: !include conf.d/web_server.yaml + debug: !include conf.d/debug.yaml + # time: !include conf.d/time.yaml + mqtt_bambu_lan: !include conf.d/mqtt_bambu_lan.yaml + filament: !include conf.d/filament.yaml + bambu_printer: !include conf.d/bambu_printer.yaml + automation: !include conf.d/automation.yaml + led-external: !include conf.d/led-external.yaml + ota: !include conf.d/ota.yaml + #update: !include conf.d/update.yaml + api: !include conf.d/api.yaml + logger: !include conf.d/logger.yaml diff --git a/firmware/conf.d/bambu_ams.yaml b/firmware/conf.d/bambu_ams.yaml new file mode 100644 index 0000000..9abdc06 --- /dev/null +++ b/firmware/conf.d/bambu_ams.yaml @@ -0,0 +1,105 @@ +globals: + - id: selected_slot + type: int + restore_value: true + initial_value: "0" + +script: + - id: update_selected_slot + then: + - lambda: |- + if (id(ams_number).state == 0) { + id(selected_slot) = 0; + } else { + id(selected_slot) = (id(ams_number).state - 1) * 4 + id(slot_number).state; + } + - script.execute: select_led + +number: + - platform: template + name: "Number of AMS units" + id: ams_units + optimistic: false + restore_value: true + icon: mdi:train-car-container + min_value: 0 + max_value: 4 + step: 1 + initial_value: 1 + set_action: + then: + - lambda: |- + if (id(bambu_model).state == "A1Mini" || id(bambu_model).state == "A1") { + ESP_LOGD("AMS Units", "A1Mini or A1 detected, setting AMS to 1"); + id(ams_units).publish_state(1); + } else { + ESP_LOGD("AMS Units", "AMS units: %d", (int)id(ams_units).state); + id(ams_units).publish_state(x); + } + ESP_LOGD("AMS", "Number of AMS units set: %d", (int)x); + + if (x < id(ams_number).state) { + ESP_LOGD("AMS Units", "AMS is higher than current units size, forcing AMS to %d", (int)x); + id(ams_number).publish_state(x); + } + if (x == 0) { + ESP_LOGD("AMS Units", "AMS units is 0, forcing slot to 1"); + id(slot_number).publish_state(1); + } + - script.execute: update_selected_slot + web_server: + sorting_group_id: sorting_group_printer_settings + sorting_weight: 100 + + - platform: template + name: "AMS Number" + id: ams_number + optimistic: false + restore_value: true + icon: mdi:train-car-container + min_value: 0 + max_value: 4 + step: 1 + initial_value: 1 + set_action: + then: + - lambda: |- + if (x > id(ams_units).state) { + ESP_LOGD("ams_number", "AMS number too high, setting to %d", (int)id(ams_units).state); + id(ams_number).publish_state(id(ams_units).state); + } else { + id(ams_number).publish_state(x); + } + ESP_LOGD("AMS", "AMS selected: %d", (int)x); + + if (x == 0 && id(slot_number).state != 1) { + ESP_LOGD("AMS", "AMS is 0, forcing slot to 1"); + id(slot_number).publish_state(1); + } + - script.execute: update_selected_slot + web_server: + sorting_group_id: sorting_group_ams + + - platform: template + name: "Slot Number" + id: slot_number + optimistic: false + restore_value: true + icon: mdi:record-circle-outline + min_value: 1 + max_value: 4 + step: 1 + initial_value: 1 + set_action: + then: + - lambda: |- + if (id(ams_number).state == 0) { + ESP_LOGD("slot_number", "AMS is 0, forcing slot to 1"); + id(slot_number).publish_state(1); + } else { + id(slot_number).publish_state(x); + } + ESP_LOGD("AMS", "Slot selected: %d", (int)x); + - script.execute: update_selected_slot + web_server: + sorting_group_id: sorting_group_ams diff --git a/firmware/conf.d/led-external.yaml b/firmware/conf.d/led-external.yaml index e2da575..cf98124 100644 --- a/firmware/conf.d/led-external.yaml +++ b/firmware/conf.d/led-external.yaml @@ -1,162 +1,199 @@ --- substitutions: - led_count: '9' + led_count: "17" # Match the max_value from filament_slots + +number: + - platform: template + name: "LED Brightness" + id: led_brightness + icon: mdi:led-on + entity_category: config + min_value: 1 + max_value: 100 + step: 1 + initial_value: 50 + unit_of_measurement: "%" + optimistic: true + restore_value: true + on_value: + then: + - lambda: |- + auto call = id(neopixel_light).make_call(); + call.set_brightness(x / 100.0); + call.set_color_brightness(x / 100.0); + call.perform(); + - delay: 500ms + - script.execute: select_led + light: -- platform: esp32_rmt_led_strip - name: LEDs - id: neopixel_light - state_topic: - pin: ${led_pin} - num_leds: ${led_count} - rgb_order: GRB - chipset: WS2812 - default_transition_length: 0.4s - restore_mode: RESTORE_DEFAULT_ON - entity_category: diagnostic - icon: mdi:led-strip - effects: - - addressable_rainbow: - name: Rainbow - speed: 25 - width: 15 - - addressable_lambda: - name: Breathing Blue - update_interval: 10ms - lambda: |- - static float b = 0; - b = (sin(millis() / 500.0) + 1.0) / 2.0 * 0.6 + 0.4; - auto color = esphome::light::ESPColor(0, 0, uint8_t(255 * b)); - it.all() = color; - - addressable_lambda: - name: Apple Breathing - update_interval: 10ms - lambda: |- - const float PI = 3.14159265359; - //TODO: 42.546 should be replaced by 83.333 - float t = millis() / 2000.0; - float brightness_float = (exp(sin(t * PI)) - 0.368) * 42.546; - uint8_t brightness = uint8_t(brightness_float); - auto color = esphome::light::ESPColor(brightness, 0, 0); - for (int i = 0; i < it.size(); ++i) { - it[i] = color; - } - # - strobe: - # name: "Data Upload" - # colors: - # - state: TRUE - # brightness: 100% - # duration: 50ms - # red: 0% - # green: 100% - # blue: 0% - # - state: FALSE - # duration: 50ms + - platform: esp32_rmt_led_strip + name: External LEDs + id: neopixel_light + pin: ${external_led_pin} + num_leds: ${led_count} + rmt_symbols: 144 + rgb_order: GRB + chipset: WS2812 + default_transition_length: 0.4s + restore_mode: RESTORE_DEFAULT_ON + entity_category: diagnostic + icon: mdi:led-strip + effects: + - addressable_rainbow: + name: Rainbow + speed: 25 + width: 15 + - addressable_lambda: + name: Breathing Blue + update_interval: 10ms + lambda: |- + static float b = 0; + b = (sin(millis() / 500.0) + 1.0) / 2.0 * 0.6 + 0.4; + auto color = esphome::light::ESPColor(0, 0, uint8_t(255 * b)); + it.all() = color; + - addressable_lambda: + name: Apple Breathing + update_interval: 10ms + lambda: |- + const float PI = 3.14159265359; + //TODO: 42.546 should be replaced by 83.333 + float t = millis() / 2000.0; + float brightness_float = (exp(sin(t * PI)) - 0.368) * 42.546; + uint8_t brightness = uint8_t(brightness_float); + auto color = esphome::light::ESPColor(brightness, 0, 0); + for (int i = 0; i < it.size(); ++i) { + it[i] = color; + } script: -- id: set_led_red - parameters: - led_number: int8_t - then: - - light.addressable_set: - id: neopixel_light - color_brightness: 100% - range_from: !lambda "return led_number < 0 ? 0 : led_number;" - range_to: !lambda "return led_number < 0 ? ${led_count} : led_number;" - red: 100% - green: 0% - blue: 0% -- id: set_led_green - parameters: - led_number: int8_t - then: - - light.addressable_set: - id: neopixel_light - color_brightness: 100% - range_from: !lambda "return led_number < 0 ? 0 : led_number;" - range_to: !lambda "return led_number < 0 ? ${led_count} : led_number;" - red: 0% - green: 100% - blue: 0% -- id: set_led_blue - parameters: - led_number: int8_t - then: - - light.addressable_set: - id: neopixel_light - color_brightness: 100% - range_from: !lambda "return led_number < 0 ? 0 : led_number;" - range_to: !lambda "return led_number < 0 ? ${led_count} : led_number;" - red: 0% - green: 0% - blue: 100% -- id: set_led_yellow - parameters: - led_number: int8_t - then: - - light.addressable_set: - id: neopixel_light - color_brightness: 100% - range_from: !lambda "return led_number < 0 ? 0 : led_number;" - range_to: !lambda "return led_number < 0 ? ${led_count} : led_number;" - red: 100% - green: 100% - blue: 0% -- id: set_led_white - parameters: - led_number: int8_t - then: - - light.addressable_set: - id: neopixel_light - color_brightness: 50% - range_from: !lambda "return led_number < 0 ? 0 : led_number;" - range_to: !lambda "return led_number < 0 ? ${led_count} : led_number;" - red: 100% - green: 100% - blue: 100% -- id: set_all_leds_white - then: - - light.turn_on: - id: neopixel_light - effect: none - brightness: 50% - red: 100% - green: 100% - blue: 100% -- id: set_led_off - then: - - light.turn_on: - id: neopixel_light - brightness: 0% - red: 0% - green: 0% - blue: 0% - effect: none - - delay: 50ms - - light.turn_off: - id: neopixel_light - transition_length: 0s -- id: set_led_rainbow - then: - - light.turn_on: - id: neopixel_light - effect: Rainbow -- id: set_led_breathing_blue - then: - - light.turn_on: - id: neopixel_light - effect: none - - light.turn_on: - id: neopixel_light - effect: Breathing Blue - brightness: 100% -- id: set_led_breathing_green - then: - - light.turn_on: - id: neopixel_light - effect: Breathing Green - brightness: 100% -- id: set_led_apple_breathing - then: - - light.turn_on: - id: neopixel_light - effect: Apple Breathing - brightness: 100% + - id: select_led + then: + - lambda: |- + for (int i = 0; i < 17; i++) { + id(set_led_off).execute(i); + } + id(set_led_blue).execute(id(selected_slot)); + ESP_LOGD("LED", "LED index: %d", (int)id(selected_slot)); + - id: set_led_red + parameters: + led_number: int8_t + then: + - light.addressable_set: + id: neopixel_light + range_from: !lambda "return led_number < 0 ? 0 : led_number;" + range_to: !lambda "return led_number < 0 ? ${led_count} : led_number;" + red: 100% + green: 0% + blue: 0% + - id: set_led_green + parameters: + led_number: int8_t + then: + - light.addressable_set: + id: neopixel_light + range_from: !lambda "return led_number < 0 ? 0 : led_number;" + range_to: !lambda "return led_number < 0 ? ${led_count} : led_number;" + red: 0% + green: 100% + blue: 0% + - id: set_led_blue + parameters: + led_number: int8_t + then: + - light.addressable_set: + id: neopixel_light + range_from: !lambda "return led_number < 0 ? 0 : led_number;" + range_to: !lambda "return led_number < 0 ? ${led_count} : led_number;" + red: 0% + green: 0% + blue: 100% + - id: set_led_cyan + parameters: + led_number: int8_t + then: + - light.addressable_set: + id: neopixel_light + range_from: !lambda "return led_number < 0 ? 0 : led_number;" + range_to: !lambda "return led_number < 0 ? ${led_count} : led_number;" + red: 0% + green: 100% + blue: 100% + - id: set_led_magenta + parameters: + led_number: int8_t + then: + - light.addressable_set: + id: neopixel_light + range_from: !lambda "return led_number < 0 ? 0 : led_number;" + range_to: !lambda "return led_number < 0 ? ${led_count} : led_number;" + red: 100% + green: 0% + blue: 100% + - id: set_led_yellow + parameters: + led_number: int8_t + then: + - light.addressable_set: + id: neopixel_light + range_from: !lambda "return led_number < 0 ? 0 : led_number;" + range_to: !lambda "return led_number < 0 ? ${led_count} : led_number;" + red: 100% + green: 100% + blue: 0% + - id: set_led_white + parameters: + led_number: int8_t + then: + - light.addressable_set: + id: neopixel_light + range_from: !lambda "return led_number < 0 ? 0 : led_number;" + range_to: !lambda "return led_number < 0 ? ${led_count} : led_number;" + red: 100% + green: 100% + blue: 100% + - id: set_led_off + parameters: + led_number: int8_t + then: + - light.addressable_set: + id: neopixel_light + range_from: !lambda "return led_number < 0 ? 0 : led_number;" + range_to: !lambda "return led_number < 0 ? ${led_count} : led_number;" + red: 0% + green: 0% + blue: 0% + - id: set_leds_white + then: + - light.turn_on: + id: neopixel_light + effect: none + red: 100% + green: 100% + blue: 100% + - id: set_leds_off + then: + - light.turn_off: + id: neopixel_light + transition_length: 0s + - id: set_led_rainbow + then: + - light.turn_on: + id: neopixel_light + effect: Rainbow + - id: set_led_breathing_blue + then: + - light.turn_on: + id: neopixel_light + effect: none + - light.turn_on: + id: neopixel_light + effect: Breathing Blue + - id: set_led_breathing_green + then: + - light.turn_on: + id: neopixel_light + effect: Breathing Green + - id: set_led_apple_breathing + then: + - light.turn_on: + id: neopixel_light + effect: Apple Breathing diff --git a/firmware/conf.d/led-internal.yaml b/firmware/conf.d/led-internal.yaml index 749cb3e..7204928 100644 --- a/firmware/conf.d/led-internal.yaml +++ b/firmware/conf.d/led-internal.yaml @@ -1,14 +1,14 @@ --- light: -- platform: esp32_rmt_led_strip - name: "Internal LED" - id: internal_led - state_topic: - pin: ${neopixel_pin} - chipset: WS2812 - rgb_order: GRB - num_leds: 1 - rmt_symbols: 48 - entity_category: diagnostic - restore_mode: ALWAYS_OFF - icon: mdi:led-strip + - platform: esp32_rmt_led_strip + name: "Internal LED" + id: internal_led + state_topic: + pin: ${neopixel_pin} + chipset: WS2812 + rgb_order: GRB + num_leds: 1 + rmt_symbols: 48 + entity_category: diagnostic + restore_mode: ALWAYS_OFF + icon: mdi:led-strip diff --git a/firmware/conf.d/pn532_rfid-manual-ams.yaml b/firmware/conf.d/pn532_rfid-manual-ams.yaml new file mode 100644 index 0000000..1bf42ba --- /dev/null +++ b/firmware/conf.d/pn532_rfid-manual-ams.yaml @@ -0,0 +1,301 @@ +--- +substitutions: + spi_data_rate: 200kHz + update_interval: "750ms" + +script: + - id: publish_filament_data_to_mqtt + parameters: + payload_data: string + then: + - if: + condition: + and: + - mqtt.connected + - lambda: 'return payload_data != "";' + - lambda: 'return payload_data != "{}";' + - binary_sensor.is_on: rfid_reader_spi_0_tag_is_openspool + - binary_sensor.is_on: rfid_reader_spi_0_tag_parsed + then: + - mqtt.publish: + topic: !lambda 'return "device/" + id(bambu_serial_number).state + "/request";' + payload: !lambda |- + // 255 = external ams, first ams is 0 + // 254 = external tray, first filament slot is tray 0 + uint16_t ams_id = 255; // Default to external AMS + uint16_t tray_id = 254; // Default to external tray + + if (id(ams_number).state > 0) { + ams_id = id(ams_number).state - 1; + tray_id = id(slot_number).state - 1; + } + + return bambulabs::generate_mqtt_payload(payload_data, ams_id, tray_id); + +binary_sensor: + - platform: template + name: "NFC Tag Present" + id: nfc_tag_present0 + state_topic: + icon: mdi:circle-double + web_server: + sorting_group_id: sorting_group_rfid + +spi: + # On esp32-s3 SPI2 a can support 6 devices, whereas only 3 on bus SPI3 + - id: SPI2 + clk_pin: ${spi2_clk_pin} # SCK + miso_pin: ${spi2_miso_pin} # MO/SDA/TX (MISO) + mosi_pin: ${spi2_mosi_pin} # M (MOSI) + interface: ${spi2_type} + +pn532_spi: + - id: rfid_reader_spi_0 + cs_pin: ${rfid0_ss_pin} # NSS/SCL/RX + spi_id: ${rfid0_spi_interface} + data_rate: ${spi_data_rate} + update_interval: ${update_interval} + on_tag_removed: + then: + - binary_sensor.template.publish: + id: nfc_tag_present0 + state: OFF + - lambda: |- + id(filament_raw_data0).publish_state(""); + id(set_led_blue).execute(id(selected_slot)); + on_tag: + then: + - binary_sensor.template.publish: + id: nfc_tag_present0 + state: ON + - lambda: |- + bool is_valid_openspool = false; + std::string payload; + + if (tag.has_ndef_message()) { + auto records = tag.get_ndef_message()->get_records(); + bool found_json = false; + + // Look for application/json record + for (const auto& record : records) { + if (record->get_type() == "application/json") { + if (found_json) { + // More than one JSON record found + ESP_LOGW("NFC", "Multiple JSON records found, using first one"); + break; + } + + payload = record->get_payload(); + ESP_LOGD("NFC", "Payload: %s", payload.c_str()); + id(filament_raw_data0).publish_state(payload); + + // Parse JSON and check for OpenSpool protocol + auto parse_result = json::parse_json(payload, [&](JsonObject root) { + is_valid_openspool = root["protocol"] == "openspool"; + return true; + }); + + if (!parse_result) { + ESP_LOGE("NFC", "Failed to parse JSON payload"); + } + + found_json = true; + } + } + + if (!found_json) { + ESP_LOGW("NFC", "No application/json record found"); + } + } else { + ESP_LOGI("NFC", "Tag found without NDEF message"); + } + + // Update states + id(rfid_reader_spi_0_tag_parsed).publish_state(true); + id(rfid_reader_spi_0_tag_is_openspool).publish_state(is_valid_openspool); + + // Set LED color + if (is_valid_openspool) { + id(set_led_green).execute(id(selected_slot)); + //TODO: Generate and send MQTT message here + if (!payload.empty()) { + id(publish_filament_data_to_mqtt).execute(payload); + } + } else { + id(set_led_red).execute(id(selected_slot)); + } + +text_sensor: + - platform: template + name: "NFC Raw Data" + id: filament_raw_data0 + state_topic: + internal: false # Always show raw data 0 + icon: mdi:nfc-variant + web_server: + sorting_group_id: sorting_group_rfid + filters: + - lambda: |- + auto pretty_json = [](const std::string &x) -> std::string { + if (x.empty()) { + ESP_LOGD("NFC", "Input string is empty"); + return x; + } + ESP_LOGD("NFC", "Input string: %s", x.c_str()); + + StaticJsonDocument<1024> doc; // Use StaticJsonDocument for memory efficiency + DeserializationError error = deserializeJson(doc, x); + if (error) { + ESP_LOGE("NFC", "JSON parsing failed: %s", error.c_str()); + return ""; + } + + if (!doc.is()) { + ESP_LOGE("NFC", "Invalid JSON: Not an object"); + return ""; + } + + const char* required_fields[] = {"protocol", "color_hex", "type", "min_temp", "max_temp", "brand"}; + for (const char* field : required_fields) { + if (!doc.containsKey(field)) { + ESP_LOGE("NFC", "Invalid JSON: Missing required field '%s'", field); + return ""; + } + } + + std::string output; + serializeJsonPretty(doc, output); + + if (output.length() > 1024) { + ESP_LOGE("NFC", "Prettified JSON exceeds 1024 bytes"); + return ""; + } + + return output; + }; + return pretty_json(x); + + - platform: template + name: "NFC Preview" + id: nfc_preview + state_topic: + icon: mdi:nfc-search-variant + web_server: + sorting_group_id: sorting_group_rfid + +# Create virtual button that can be pressed in the gui +button: + - platform: template + name: "Write NFC" + id: write_nfc + state_topic: + icon: mdi:nfc-tap + web_server: + sorting_group_id: sorting_group_filament_settings + sorting_weight: 210 + on_press: + then: + - if: + condition: + and: + - lambda: |- + if (id(filament_brand).state == "") { + ESP_LOGE("main", "Filament Brand cannot be empty"); + return false; + } else { + return true; + } + - lambda: |- + if (id(filament_brand_code).state == "") { + ESP_LOGE("main", "Filament Brand Code cannot be empty"); + return false; + } else { + return true; + } + - lambda: |- + if (id(filament_color_hex).state == "") { + ESP_LOGE("main", "Filament Color Hex cannot be empty"); + return false; + } else { + return true; + } + - lambda: |- + if (id(filament_max_temp).state > 300) { + ESP_LOGE("main", "Filament Max Temp must be less than 300"); + return false; + } else { + return true; + } + - lambda: |- + if (id(filament_min_temp).state < 150) { + ESP_LOGE("main", "Filament Min Temp must be greater than 150"); + return false; + } else { + return true; + } + - lambda: |- + if (id(filament_min_temp).state > id(filament_max_temp).state) { + ESP_LOGE("main", "Filament Min Temp must be less than Filament Max Temp"); + return false; + } else { + return true; + } + - lambda: |- + if (id(filament_type).state == "") { + ESP_LOGE("main", "Filament Type cannot be empty"); + return false; + } else { + return true; + } + then: + - lambda: |- + auto message = new nfc::NdefMessage(); + // Application/Json NDEF record + auto json_record = std::make_unique(); + json_record->set_tnf(nfc::TNF_MIME_MEDIA); + json_record->set_type("application/json"); + + DynamicJsonDocument doc(256); // Adjust size as needed + JsonObject root = doc.to(); + root["version"] = "1.0"; + root["protocol"] = "openspool"; + if (id(filament_include_alpha).state == true){ + root["color_hex"] = id(filament_color_hexaa).state; + } + else{ + root["color_hex"] = id(filament_color_hex).state; + } + root["type"] = id(filament_type).state; + root["min_temp"] = id(filament_min_temp).state; + root["max_temp"] = id(filament_max_temp).state; + root["brand"] = id(filament_brand).state; + + std::string json_string; + serializeJson(root, json_string); + if (json_string.empty()) { + ESP_LOGE("rfid", "Failed to serialize JSON"); + return; + } + ESP_LOGI("rfid", "JSON content to be written: %s", json_string.c_str()); + json_record->set_payload(json_string); + message->add_record(std::move(json_record)); + + std::vector encoded_message = message->encode(); + size_t message_size = encoded_message.size(); + ESP_LOGI("rfid", "NDEF message size: %zu bytes", message_size); + + id(rfid_reader_spi_0).write_mode(message); + ESP_LOGI("rfid", "Writing JSON NDEF message to tag"); + - wait_until: + not: + pn532.is_writing: + id: rfid_reader_spi_0 + - logger.log: + tag: "rfid" + format: "Finished writing tag" + level: INFO + - lambda: |- + id(set_led_green).execute(id(selected_slot)); + else: + - lambda: |- + ESP_LOGI("rfid", "NFC Tag not present"); diff --git a/firmware/conf.d/rotary-encoder-manual-ams.yaml b/firmware/conf.d/rotary-encoder-manual-ams.yaml new file mode 100644 index 0000000..12dd036 --- /dev/null +++ b/firmware/conf.d/rotary-encoder-manual-ams.yaml @@ -0,0 +1,73 @@ +script: + - id: navigate_slots + parameters: + direction: int # 1 for clockwise, -1 for anticlockwise + then: + - lambda: |- + // Calculate total slots (AMS 0 + all AMS units with 4 slots each) + int total_slots = 1 + id(ams_units).state * 4; + int current_slot; + + // Get current absolute slot position + if (id(ams_number).state == 0) { + current_slot = 0; + } else { + current_slot = (id(ams_number).state - 1) * 4 + id(slot_number).state; + } + + // Move to next/previous slot based on direction + if (direction > 0) { + current_slot = (current_slot + 1) % total_slots; + } else { + current_slot = (current_slot + total_slots - 1) % total_slots; + } + + // Convert back to AMS and slot + auto slot_call = id(slot_number).make_call(); + auto ams_call = id(ams_number).make_call(); + if (current_slot == 0) { + slot_call.set_value(1); + ams_call.set_value(0); + slot_call.perform(); + ams_call.perform(); + } else { + int ams = (current_slot - 1) / 4 + 1; + int slot = (current_slot - 1) % 4 + 1; + if (id(ams_number).state != ams) { + ams_call.set_value(ams); + ams_call.perform(); + } + if (id(slot_number).state != slot) { + slot_call.set_value(slot); + slot_call.perform(); + } + } + - script.execute: update_selected_slot + +sensor: + - platform: rotary_encoder + id: encoder_sensor + name: "Encoder Position" + resolution: 2 + restore_mode: ALWAYS_ZERO + pin_a: + number: ${encoder_pin_a} + mode: + input: true + pullup: true + pin_b: + number: ${encoder_pin_b} + mode: + input: true + pullup: true + on_clockwise: + then: + - script.execute: + id: navigate_slots + direction: 1 + on_anticlockwise: + then: + - script.execute: + id: navigate_slots + direction: -1 + entity_category: diagnostic diff --git a/firmware/conf.d/web_server.yaml b/firmware/conf.d/web_server.yaml index 33e4257..269a632 100644 --- a/firmware/conf.d/web_server.yaml +++ b/firmware/conf.d/web_server.yaml @@ -6,18 +6,21 @@ web_server: version: 3 local: true sorting_groups: - - id: sorting_group_filament_settings - name: "Filament Settings" - sorting_weight: -50 - - id: sorting_group_rfid - name: "RFID Programming" - sorting_weight: -40 - - id: sorting_group_printer_settings - name: "Printer Settings" - sorting_weight: -30 - - id: sorting_group_info - name: "Information" - sorting_weight: -20 - - id: sorting_group_extra - name: "Extra" - sorting_weight: -10 + - id: sorting_group_filament_settings + name: "Filament Settings" + sorting_weight: -50 + - id: sorting_group_rfid + name: "RFID Programming" + sorting_weight: -40 + - id: sorting_group_printer_settings + name: "Printer Settings" + sorting_weight: -30 + - id: sorting_group_ams + name: "AMS" + sorting_weight: -25 + - id: sorting_group_info + name: "Information" + sorting_weight: -20 + - id: sorting_group_extra + name: "Extra" + sorting_weight: -10 diff --git a/firmware/esp32-s3-super-mini-manual-ams.yaml b/firmware/esp32-s3-super-mini-manual-ams.yaml new file mode 100644 index 0000000..6268796 --- /dev/null +++ b/firmware/esp32-s3-super-mini-manual-ams.yaml @@ -0,0 +1,32 @@ +esp32: + board: esp32-s3-devkitc-1 + flash_size: 4MB + cpu_frequency: 80MHz # Lowest possible frequency to reduce power consumption + +psram: + mode: quad + speed: 80MHz + +substitutions: + spi2_type: SPI2 + rfid0_spi_interface: SPI2 + neopixel_pin: GPIO48 + encoder_pin_a: GPIO6 + encoder_pin_b: GPIO7 + external_led_pin: GPIO9 + spi2_clk_pin: GPIO10 + spi2_miso_pin: GPIO11 + spi2_mosi_pin: GPIO12 + rfid0_ss_pin: GPIO13 + +packages: + improv-serial: !include conf.d/improv-serial.yaml + bambu_ams: !include conf.d/bambu_ams.yaml + openspool-manual-ams: !include openspool-manual-ams.yaml + encoder: !include conf.d/rotary-encoder-manual-ams.yaml # Comment out if you don't use the rotary encoder + led-internal: !include conf.d/led-internal.yaml + extra: !include conf.d/extra.yaml + +dashboard_import: + package_import_url: github://spuder/openspool/firmware/esp32-s3-super-mini-manual-ams.yaml@main + import_full_config: false diff --git a/firmware/openspool-ams.yaml b/firmware/openspool-ams.yaml index 20b20ac..84e17e0 100644 --- a/firmware/openspool-ams.yaml +++ b/firmware/openspool-ams.yaml @@ -1,5 +1,5 @@ - packages: base: !include common.yaml psram: !include conf.d/psram-esp32s3.yaml - pn_532_rfid-ams: !include conf.d/pn532_rfid-ams.yaml \ No newline at end of file + pn_532_rfid-solo: !include conf.d/pn532_rfid-solo.yaml + pn_532_rfid-ams: !include conf.d/pn532_rfid-ams.yaml diff --git a/firmware/openspool-manual-ams.yaml b/firmware/openspool-manual-ams.yaml new file mode 100644 index 0000000..29c8085 --- /dev/null +++ b/firmware/openspool-manual-ams.yaml @@ -0,0 +1,4 @@ +packages: + base: !include common-manual-ams.yaml + psram: !include conf.d/psram-esp32s3.yaml + pn_532_rfid-manual-ams: !include conf.d/pn532_rfid-manual-ams.yaml diff --git a/firmware/openspool-mini.yaml b/firmware/openspool-mini.yaml index 8861db1..0ed44b4 100644 --- a/firmware/openspool-mini.yaml +++ b/firmware/openspool-mini.yaml @@ -1,3 +1,3 @@ - packages: - base: !include common.yaml \ No newline at end of file + base: !include common.yaml + pn532_rfid-solo: !include conf.d/pn532_rfid-solo.yaml