Skip to content

Commit 914da89

Browse files
author
thingsapart
committed
New UI + DWC implementation
1 parent bf71ff9 commit 914da89

34 files changed

+3811
-2562
lines changed

firmware/cnc_interface/.vscode/settings.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@
115115
"feed_rate_view.h": "c",
116116
"task.h": "c",
117117
"lvgl_ui.h": "c",
118-
"data_binding.h": "c"
118+
"data_binding.h": "c",
119+
"esp_http_client.h": "c",
120+
"machine_rrf.h": "c",
121+
"math.h": "c",
122+
"cjson.h": "c",
123+
"dwc_http_client_wrapper.h": "c"
119124
}
120125
}

firmware/cnc_interface/data/files_default.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ src/main/main.cpp
55
src/tasks/*.c
66
src/ui/*.c
77
src/ui/*.h
8+
src/debug.h
89
platformio.ini
910
README.md
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Data Binding: `action` and `observes`
2+
3+
The data binding system provides a mechanism to connect the state of your application (the machine interface) to the UI, and to receive events from the UI back into your application logic. It is based on a "Model-View-ViewModel" (MVVM) pattern that creates a clean separation of concerns:
4+
5+
* **View:** The UI definition in your YAML file.
6+
* **Model:** The `machine_interface_t` struct, representing the CNC machine's state.
7+
* **ViewModel:** The `data_binding` library, which mediates between the Model and the View.
8+
9+
This system is comprised of two main features, configured by the `action` and `observes` keys in your UI definition.
10+
11+
## `action`: UI to Machine Communication
12+
13+
The `action` key allows widgets to send commands to the machine. When a user interacts with a widget (e.g., clicks a button), a named action is dispatched to a handler that calls the corresponding function on the `machine_interface_t`.
14+
15+
### Action Reference
16+
17+
| Action Name | Description | Value Type | YAML Example |
18+
| :--- | :--- | :--- | :--- |
19+
| **Homing & Control** |
20+
| `home_machine` | Homes all axes (`G28`). | `trigger` | `{ home_machine: trigger }` |
21+
| `home_machine_x` | Homes the X axis (`G28 X`). | `trigger` | `{ home_machine_x: trigger }` |
22+
| `home_machine_y` | Homes the Y axis (`G28 Y`). | `trigger` | `{ home_machine_y: trigger }` |
23+
| `home_machine_z` | Homes the Z axis (`G28 Z`). | `trigger` | `{ home_machine_z: trigger }` |
24+
| `feed_hold` | Toggles feed hold (`!`) and resume (`~`). | `trigger` | `{ feed_hold: trigger }` |
25+
| `program_run` | Starts or resumes the current job (`M24`). | `trigger` | `{ program_run: trigger }` |
26+
| `program_stop` | Issues a soft reset (`Ctrl+X`). | `trigger` | `{ program_stop: trigger }` |
27+
| **Overrides** |
28+
| `set_feed_override` | Sets the feed rate override percentage. | `float` | `{ set_feed_override: 110.0 }` |
29+
| `set_speed_override` | Sets the spindle speed override percentage. | `float` | `{ set_speed_override: 95.0 }` |
30+
| **Movement & Jogging** |
31+
| `jog_continuous_start_x_plus` | Starts a continuous positive jog on the X axis. | `trigger` | `{ jog_continuous_start_x_plus: trigger }` |
32+
| `jog_continuous_start_x_minus` | Starts a continuous negative jog on the X axis. | `trigger` | `{ jog_continuous_start_x_minus: trigger }` |
33+
| ..._y_plus/minus | (Same for Y axis) | `trigger` | ... |
34+
| ..._z_plus/minus | (Same for Z axis) | `trigger` | ... |
35+
| `jog_continuous_stop` | Stops any continuous jog. | `trigger` | `{ jog_continuous_stop: trigger }` |
36+
| `jog_step_x_plus` | Steps the X axis by `jog_step` distance. | `trigger` | `{ jog_step_x_plus: trigger }` |
37+
| `jog_step_x_minus` | Steps the X axis by `-jog_step` distance. | `trigger` | `{ jog_step_x_minus: trigger }` |
38+
| ..._y_plus/minus | (Same for Y axis) | `trigger` | ... |
39+
| ..._z_plus/minus | (Same for Z axis) | `trigger` | ... |
40+
| `cycle_jog_step` | Cycles through predefined jog step distances. | `cycle` | `{ cycle_jog_step: [0.01, 0.1, 1.0, 10.0] }` |
41+
| `set_move_axis_x` | Sets the currently active axis for jogging to X. | `trigger` | `{ set_move_axis_x: trigger }` |
42+
| `set_move_axis_y` | Sets the currently active axis for jogging to Y. | `trigger` | `{ set_move_axis_y: trigger }` |
43+
| `set_move_axis_z` | Sets the currently active axis for jogging to Z. | `trigger` | `{ set_move_axis_z: trigger }` |
44+
| `set_move_axis_off` | Deactivates axis jogging selection. | `trigger` | `{ set_move_axis_off: trigger }` |
45+
| `cycle_move_axis` | Cycles the active jog axis (X -> Y -> Z -> OFF). | `trigger` | `{ cycle_move_axis: trigger }` |
46+
| **Coordinate Systems (WCS)** |
47+
| `set_wcs` | Sets the current Work Coordinate System. | `float` | `{ set_wcs: 2.0 }` (for G55) |
48+
| `cycle_wcs` | Cycles to the next WCS (e.g., G54 -> G55). | `trigger` | `{ cycle_wcs: trigger }` |
49+
| `zero_wcs_x` | Sets the current X position as the origin for the current WCS. | `trigger` | `{ zero_wcs_x: trigger }` |
50+
| `zero_wcs_y` | Sets the current Y position as the origin for the current WCS. | `trigger` | `{ zero_wcs_y: trigger }` |
51+
| `zero_wcs_z` | Sets the current Z position as the origin for the current WCS. | `trigger` | `{ zero_wcs_z: trigger }` |
52+
| **Files & Macros** |
53+
| `list_files` | Requests a file listing for a given path. | `string` | `{ list_files: "/gcodes" }` |
54+
| `run_macro` | Runs a macro file by name. | `string` | `{ run_macro: "probe.g" }` |
55+
| `start_job` | Starts a job file by name. | `string` | `{ start_job: "my_part.gcode" }` |
56+
| **Probing** |
57+
| `probe` | Executes a G-code probing routine. | `string` | `{ probe: "G38.2 Z-20 F100" }` |
58+
| **Dialogs / Modals** |
59+
| `dialog_ok` | Responds "OK" to the current modal dialog. | `trigger` | `{ dialog_ok: trigger }` |
60+
| `dialog_cancel` | Responds "Cancel" to the current modal dialog. | `trigger` | `{ dialog_cancel: trigger }` |
61+
| `dialog_choice` | Responds with a choice to a multi-choice dialog. | `float` | `{ dialog_choice: 1.0 }` (for 2nd choice) |
62+
| `dialog_input_int` | Submits an integer value from an input dialog. | `float` | `{ dialog_input_int: 123.0 }` |
63+
| `dialog_input_float`| Submits a float value from an input dialog. | `float` | `{ dialog_input_float: 45.6 }` |
64+
| `dialog_input_str` | Submits a string value from an input dialog. | `string` | `{ dialog_input_str: "my_value" }` |
65+
| **G-Code** |
66+
| `send_gcode` | Sends a raw G-code string to the machine. | `string` | `{ send_gcode: "M3 S10000" }` |
67+
68+
## `observes`: Machine to UI Communication
69+
70+
The `observes` key allows widgets to automatically update their appearance in response to changes in the machine's state. You simply notify the data binding system when a piece of your data changes, and it handles updating the UI.
71+
72+
### Observable State Reference
73+
74+
| State Name | Description | Value Type | YAML Example |
75+
| :--- | :--- | :--- | :--- |
76+
| **Machine Status** |
77+
| `is_connected` | `true` if a connection to the machine is active. | `bool` | `{ is_connected: { visible: { true: true, false: false } } }` |
78+
| `machine_mode`| The current high-level status (e.g., "IDLE", "AUTO", "PAUSED").| `string` | `{ machine_mode: { text: "%s" } }` |
79+
| `program_running` | `true` if a job is currently running. | `bool` | `{ program_running: { style: { true: '@style_active' } } }` |
80+
| `program_paused` | `true` if a job is currently paused. | `bool` | `{ program_paused: { visible: true } }` |
81+
| **Position** |
82+
| `pos_x` / `_y` / `_z` | Machine-space X, Y, or Z position. | `float` | `{ pos_x: { text: "X: %.3f" } }` |
83+
| `wcs_pos_x` / `_y` / `_z` | Work-space (WCS) X, Y, or Z position. | `float` | `{ wcs_pos_x: { text: "%.3f" } }` |
84+
| `target_pos_x` / `_y` / `_z` | The final target position for the current move. | `float` | `{ target_pos_x: { text: "Target: %.3f" } }` |
85+
| **Homing** |
86+
| `x_is_homed` | `true` if the X axis has been homed. | `bool` | `{ x_is_homed: { style: { true: '@style_homed' } } }` |
87+
| `y_is_homed` | `true` if the Y axis has been homed. | `bool` | `{ y_is_homed: { checked: { true: true, false: false } } }` |
88+
| `z_is_homed` | `true` if the Z axis has been homed. | `bool` | `{ z_is_homed: { disabled: { true: false, false: true } } }` |
89+
| **Coordinate Systems (WCS)** |
90+
| `wcs_name` | The name of the active WCS (e.g., "G54", "G55"). | `string` | `{ wcs_name: { text: "%s" } }` |
91+
| `wcs_number` | The number of the active WCS (1 for G54, etc.). | `float` | `{ wcs_number: { value: [LV_ANIM_OFF] } }` |
92+
| `z_offset` | The current Z-offset value. | `float` | `{ z_offset: { text: "Z-Off: %.3f" } }` |
93+
| **Feed & Speed** |
94+
| `feed` | The current actual feed rate. | `float` | `{ feed: { text: "Feed: %.0f" } }` |
95+
| `feed_requested` | The requested feed rate from G-code. | `float` | `{ feed_requested: { text: "Req: %.0f" } }` |
96+
| `feed_override` | The current feed override percentage (e.g., 100.0 for 100%). | `float` | `{ feed_override: { value: [LV_ANIM_ON] } }` |
97+
| **Spindle & Tool** |
98+
| `spindle_0_rpm` | RPM of the first spindle. | `float` | `{ spindle_0_rpm: { text: "RPM: %.0f" } }` |
99+
| `spindle_0_name` | Name of the first spindle. | `string` | `{ spindle_0_name: { text: "%s" } }` |
100+
| `spindle_0_on` | `true` if the first spindle is active. | `bool` | `{ spindle_0_on: { style: { true: '@style_active' } } }` |
101+
| `tool_name` | The name of the currently active tool. | `string` | `{ tool_name: { text: "Tool: %s" } }` |
102+
| **Jogging & UI State** |
103+
| `move_is_relative` | `true` if G-code moves are in relative mode (G91). | `bool` | `{ move_is_relative: { visible: true } }` |
104+
| `move_is_step` | `true` if jogging is in step mode. | `bool` | `{ move_is_step: { style: { true: '@style_active' } } }` |
105+
| `current_move_axis`| The currently selected jog axis ("X", "Y", "Z", or "OFF"). | `string` | `{ current_move_axis: { text: "Axis: %s" } }` |
106+
| `jog_step` | The current step distance for jogging. | `float` | `{ jog_step: { text: "%.2f" } }` |
107+
| **Sensors** |
108+
| `probe_0_value` | The value of the first probe. | `float` | `{ probe_0_value: { text: "%.3f" } }` |
109+
| `end_stop_0_triggered`| `true` if the first endstop is triggered. | `bool` | `{ end_stop_0_triggered: { visible: true } }` |
110+
| **Files** |
111+
| `filelist_gcodes`| A newline-separated list of g-code files. For `dropdown` widgets.| `string`| `{ filelist_gcodes: { text: "%s" } }`|
112+
| `filelist_macros`| A newline-separated list of macro files. For `dropdown` widgets.| `string`| `{ filelist_macros: { text: "%s" } }`|
113+
| **Dialogs / Modals** |
114+
| `dialog_active` | `true` if a modal dialog is currently active. | `bool` | `{ dialog_active: { visible: true } }` |
115+
| `dialog_title`| The title of the current dialog. | `string` | `{ dialog_title: { text: "%s" } }` |
116+
| `dialog_text` | The main text/message of the current dialog. | `string` | `{ dialog_text: { text: "%s" } }` |
117+
| `dialog_choices`| Newline-separated list of choices for a choice dialog. | `string` | `{ dialog_choices: { text: "%s" } }` |
118+
| `dialog_mode` | The type of dialog (see `message_box_mode_t`). | `float` | `{ dialog_mode: { visible: { 2.0: true } } }` |
119+

firmware/cnc_interface/platformio.ini

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,10 @@ build_flags = ${env:display32s3.build_flags}
243243
-D USB_UART_PIN_RX=44
244244
upload_speed = 460800
245245

246+
[env:display-dwc-guition-4848s040]
247+
extends = env:display-guition-4848s040
248+
build_flags = ${env:display-guition-4848s040.build_flags}
249+
-D DWC_MACHINE_MODE=1
246250

247251
[env:display-wt32-sc01-plus]
248252
extends = env:display32s3
@@ -286,6 +290,14 @@ build_flags = ${env:display32s3.build_flags}
286290

287291
upload_speed = 921600
288292

293+
[env:display-dwc-wt32-sc01-plus]
294+
extends = env:display-wt32-sc01-plus
295+
build_flags = ${env:display-wt32-sc01-plus.build_flags}
296+
-D DWC_MACHINE_MODE=1
297+
lib_deps = ${env:display-wt32-sc01-plus.lib_deps}
298+
arduino-libraries/ArduinoHttpClient
299+
arduino-libraries/ArduinoMDNS
300+
289301
[env:display-jc1060p470-old]
290302
; https://github.com/esp-arduino-libs/ESP32_Display_Panel
291303
; https://github.com/espressif/esp-idf/tree/master/examples/peripherals/lcd/mipi_dsi

firmware/cnc_interface/src/compat/queue.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
extern "C" {
1313
#endif
1414

15+
const char *TAG = "queue";
16+
1517
// Initialize the G-code queue
1618
void gcode_queue_init(gcode_queue_t *queue) {
1719
queue->head = 0;
@@ -22,13 +24,13 @@ void gcode_queue_init(gcode_queue_t *queue) {
2224
// Add a G-code command to the queue (FIFO). Handles wraparound.
2325
bool gcode_queue_push(gcode_queue_t *queue, const char *gcode) {
2426
if (queue->count >= MAX_GCODE_Q_LEN) {
25-
_d(2, "G-code queue overflow!");
27+
LOGE(TAG, "G-code queue overflow!");
2628
return false; // Indicate failure
2729
}
2830

2931
size_t len = strlen(gcode);
3032
if (len >= sizeof(queue->buffer[0])) {
31-
_df(2, "G-code command too long: %s", gcode);
33+
LOGE(TAG, "G-code command too long: %s", gcode);
3234
return false; // Command too long for buffer
3335
}
3436

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#include "dwc_settings.h"
2+
#include <string.h>
3+
4+
#ifdef ESP32_HW
5+
#include <Preferences.h>
6+
#endif
7+
8+
#include "debug.h"
9+
10+
static const char* TAG = "dwc_settings";
11+
static const char* NVS_NAMESPACE = "dwc-config";
12+
13+
// Keys for NVS
14+
static const char* KEY_SSID = "ssid";
15+
static const char* KEY_PASSWORD = "password";
16+
static const char* KEY_HOST = "host";
17+
18+
static dwc_settings_t g_settings;
19+
static bool g_loaded = false;
20+
21+
#ifdef ESP32_HW
22+
static Preferences preferences;
23+
#endif
24+
25+
void dwc_settings_init() {
26+
memset(&g_settings, 0, sizeof(dwc_settings_t));
27+
g_loaded = false;
28+
}
29+
30+
bool dwc_settings_load() {
31+
#ifdef ESP32_HW
32+
if (!preferences.begin(NVS_NAMESPACE, false)) {
33+
LOGE(TAG, "Failed to initialize preferences");
34+
return false;
35+
}
36+
37+
preferences.getString(KEY_SSID, g_settings.ssid, sizeof(g_settings.ssid));
38+
preferences.getString(KEY_PASSWORD, g_settings.password, sizeof(g_settings.password));
39+
preferences.getString(KEY_HOST, g_settings.host, sizeof(g_settings.host));
40+
41+
preferences.end();
42+
g_loaded = true;
43+
LOGI(TAG, "Loaded settings: SSID='%s', Host='%s'", g_settings.ssid, g_settings.host);
44+
return true;
45+
#else
46+
LOGW(TAG, "DWC settings not supported on this platform.");
47+
// Simulate empty settings for native builds
48+
g_loaded = true;
49+
return true;
50+
#endif
51+
}
52+
53+
void dwc_settings_clear() {
54+
#ifdef ESP32_HW
55+
preferences.clear();
56+
#endif
57+
}
58+
59+
bool dwc_settings_save(const dwc_settings_t* settings) {
60+
#ifdef ESP32_HW
61+
if (!preferences.begin(NVS_NAMESPACE, false)) {
62+
LOGE(TAG, "Failed to initialize preferences for writing");
63+
return false;
64+
}
65+
66+
bool success = true;
67+
if (preferences.putString(KEY_SSID, settings->ssid) == 0) {
68+
LOGE(TAG, "Failed to save SSID");
69+
success = false;
70+
}
71+
if (preferences.putString(KEY_PASSWORD, settings->password) == 0) {
72+
LOGE(TAG, "Failed to save password");
73+
success = false;
74+
}
75+
if (preferences.putString(KEY_HOST, settings->host) == 0) {
76+
LOGE(TAG, "Failed to save host");
77+
success = false;
78+
}
79+
80+
preferences.end(); // This commits the changes to flash and returns void.
81+
82+
if (success) {
83+
LOGI(TAG, "Saved settings: SSID='%s', Host='%s'", settings->ssid, settings->host);
84+
memcpy(&g_settings, settings, sizeof(dwc_settings_t));
85+
} else {
86+
LOGE(TAG, "Failed to write one or more settings to NVS");
87+
}
88+
return success;
89+
#else
90+
LOGW(TAG, "DWC settings not supported on this platform.");
91+
return false;
92+
#endif
93+
}
94+
95+
bool dwc_settings_are_valid() {
96+
return g_loaded && g_settings.ssid[0] != '\0' && g_settings.host[0] != '\0';
97+
}
98+
99+
const dwc_settings_t* dwc_settings_get() {
100+
return &g_settings;
101+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#ifndef DWC_SETTINGS_H
2+
#define DWC_SETTINGS_H
3+
4+
#include <stdbool.h>
5+
6+
#ifdef __cplusplus
7+
extern "C" {
8+
#endif
9+
10+
// Structure to hold the DWC configuration
11+
typedef struct {
12+
char ssid[33]; // 32 max SSID length + null terminator
13+
char password[65]; // 64 max password length + null terminator
14+
char host[64]; // Hostname or IP address
15+
} dwc_settings_t;
16+
17+
/**
18+
* @brief Initializes the DWC settings module.
19+
*/
20+
void dwc_settings_init();
21+
22+
/**
23+
* @brief Loads the DWC settings from non-volatile storage.
24+
*
25+
* @return true if settings were loaded successfully, false otherwise.
26+
*/
27+
bool dwc_settings_load();
28+
29+
/**
30+
* @brief Clears the DWC settings from non-volatile storage.
31+
*/
32+
void dwc_settings_clear();
33+
34+
/**
35+
* @brief Saves the DWC settings to non-volatile storage.
36+
*
37+
* @param settings A pointer to the settings structure to save.
38+
* @return true if settings were saved successfully, false otherwise.
39+
*/
40+
bool dwc_settings_save(const dwc_settings_t* settings);
41+
42+
/**
43+
* @brief Checks if valid settings have been loaded.
44+
*
45+
* @return true if the loaded SSID is not empty, false otherwise.
46+
*/
47+
bool dwc_settings_are_valid();
48+
49+
/**
50+
* @brief Gets a pointer to the currently loaded settings.
51+
*
52+
* @return A const pointer to the internal settings structure. Do not modify.
53+
*/
54+
const dwc_settings_t* dwc_settings_get();
55+
56+
#ifdef __cplusplus
57+
}
58+
#endif
59+
60+
#endif // DWC_SETTINGS_H

0 commit comments

Comments
 (0)