From a961fa721b22351ce876e67d03564de732efb5ca Mon Sep 17 00:00:00 2001 From: MangaValk Date: Thu, 1 Oct 2026 15:29:21 +0200 Subject: [PATCH] Remote: match the old remote's hardware; old remote code in LEGACY - LEGACY/remote/teensy/: old remote sketch from https://mangavalk.com/Hexapod/remote.git @ 492dece (reference only). - remote/Config.h: sticks on A0-A5, Nextion on Serial1 (0/1), as in the old sketch; MPU6050 moved to Wire2 (25/24) since A4/A5 are Wire's pins. - NextionUI: controls on a vscope-global "control" page added to the old HMI; keeps the old t1 status texts; the old page-1 b2 button (and serial 'U') enter Nextion upload passthrough, following the PC's baud. - Docs: NEXTION.md, remote README wiring, CLAUDE.md. Co-Authored-By: Claude Opus 5.5 --- 2026-current/.vscode/extensions.json | 11 ++ 2026-current/CLAUDE.md | 46 ++++- 2026-current/LEGACY/remote/teensy/Joysticks.h | 127 +++++++++++++ 2026-current/LEGACY/remote/teensy/Nrf.h | 64 +++++++ 2026-current/LEGACY/remote/teensy/README.md | 3 + .../LEGACY/remote/teensy/SerialPasstrough.h | 122 ++++++++++++ .../remote/teensy/TransferDataStructure.h | 38 ++++ 2026-current/LEGACY/remote/teensy/remote.ino | 176 ++++++++++++++++++ 2026-current/remote/Config.h | 35 +++- 2026-current/remote/NEXTION.md | 36 +++- 2026-current/remote/NextionUI.cpp | 104 ++++++++--- 2026-current/remote/NextionUI.h | 12 +- 2026-current/remote/README.md | 14 +- 2026-current/remote/RemoteIMU.cpp | 26 +-- 2026-current/remote/RemoteState.cpp | 3 + 2026-current/remote/SerialConsole.cpp | 1 + 2026-current/remote/UiTypes.h | 1 + 2026-current/remote/remote.ino | 17 +- 18 files changed, 760 insertions(+), 76 deletions(-) create mode 100644 2026-current/.vscode/extensions.json create mode 100644 2026-current/LEGACY/remote/teensy/Joysticks.h create mode 100644 2026-current/LEGACY/remote/teensy/Nrf.h create mode 100644 2026-current/LEGACY/remote/teensy/README.md create mode 100644 2026-current/LEGACY/remote/teensy/SerialPasstrough.h create mode 100644 2026-current/LEGACY/remote/teensy/TransferDataStructure.h create mode 100644 2026-current/LEGACY/remote/teensy/remote.ino diff --git a/2026-current/.vscode/extensions.json b/2026-current/.vscode/extensions.json new file mode 100644 index 0000000..a91ca8f --- /dev/null +++ b/2026-current/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + // 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", + "pioarduino.pioarduino-ide" + ] +} diff --git a/2026-current/CLAUDE.md b/2026-current/CLAUDE.md index f9da40c..fb43d46 100644 --- a/2026-current/CLAUDE.md +++ b/2026-current/CLAUDE.md @@ -14,7 +14,8 @@ remote/ Teensy 4.1 firmware (PlatformIO env remote) shared/HexapodProtocol/ wire protocol, used by robot, remote and sim sim/ robot firmware -> hexapod_sim.dll (CMake + g++), headless smoke test Hexapod_Simulation_Unity/ Unity 6000.6 project that loads the DLL (Assets/HexapodSim/) -LEGACY/ owner's older material for reference (remote/remote.HMI = old Nextion UI, 6 pages) +LEGACY/ owner's older material for reference (remote/remote.HMI = old Nextion UI, 6 pages; + remote/teensy/ = old remote sketch from https://mangavalk.com/Hexapod/remote.git @ 492dece) scripts/ PlatformIO helper ``` @@ -113,7 +114,7 @@ link-loss disarm) → PacketBuilder → `radio.write` → Nextion render. | `RemoteState` | authoritative operator state and interlocks: arm needs centered sticks, disarm clears stand, stand needs armed, calibrate needs disarmed, link-loss auto-disarm. Smooths speed scale and fades remote tilt in/out | | `PacketBuilder` | the inverse of InputMapper: stick frame → body frame per the contract, per-submode right-stick mapping, `FLAG_WALK` with 250 ms linger (twist ignored in mode B), tilt deadzone/scale × fade, zeros motion when disarmed | | `RadioLink` | nRF24 TX, short retries (1, 3), ack-based link status and 32-packet quality % | -| `NextionUI` | library-free Nextion driver. HMI sends `'#' len cmd [val]` frames via `printh`. Remote pushes state by component name with diffing, a 2 s full refresh, and a redraw on the Nextion 0x88 "ready" frame | +| `NextionUI` | library-free Nextion driver on `Serial1`. HMI sends `'#' len cmd [val]` frames via `printh`. Remote pushes state by component name, as `control.` (the owner adds a `control` page with vscope-global components to the old HMI), with diffing, a 2 s full refresh, and a redraw on the Nextion 0x88 "ready" frame. Also writes the old HMI's `t1` status texts (`NEXTION_STATUS_TEXTS`). The old page-1 `b2` sends a standard 0x65 touch frame, which triggers `runUploadPassthrough()`: USB ↔ display bridge for Nextion Editor, following `Serial.baud()`, never returns. Serial key `U` does the same | | `SerialConsole` | USB keystrokes that mirror every UI command plus diagnostics | | `SmoothedValue.h` | copy of the robot's (kept separate so robot/ stays untouched) | | `NEXTION.md` | HMI build guide: component names, txt_maxl, touch event code | @@ -284,17 +285,46 @@ Robot (`robot/README.md` has detail): - `Limits::stancePolygonOk()` is computed but not acted on. Remote (`remote/README.md` has detail): -- All pins in `remote/Config.h` (radio CE 9 / CSN 10, sticks A0–A3/A6/A7, - MPU6050 on 18/19, Nextion on Serial2 7/8). +- Pins in `remote/Config.h`, taken from the old remote sketch: radio CE 9 / + CSN 10, sticks A0–A5, Nextion on Serial1 (0/1). The MPU6050 is on + **Wire2 (25/24)**, since A4/A5 = Wire's SDA/SCL are stick pins; that's + the owner's choice and needs rewiring the MPU. Which stick axis is on + which pin is still unknown. - `STICK_INVERT[]`, calibration defaults, deadzones, expo. - `IMU_AXIS_FROM_CHIP` / `IMU_AXIS_SIGN` depend on how the MPU6050 is mounted in the handset. - The Nextion must have `bauds=115200` in Program.s, and the HMI must match `NEXTION.md`. The owner's old 6-page HMI is in `LEGACY/remote/remote.HMI`; - its component names aren't readable outside Nextion Editor. Adapting - `NextionUI` to it needs the owner's component list, or the old remote - Teensy code, which the owner will add to `LEGACY/` when found. The - compiled `.tft` is gitignored. + its component names aren't readable outside Nextion Editor. The compiled + `.tft` is gitignored. +- What the old remote sketch (`LEGACY/remote/teensy/`, last ran on a + Teensy) tells us about the real hardware: + - **Nextion on `Serial1` (pins 0/1)**, driven by the ITEAD Nextion + library, whose `nexInit()` runs at 9600. `remote/Config.h` now uses + Serial1 and expects 115200 via `bauds=` in the HMI. + - The old HMI is a *servo monitor*, not the control UI in `NEXTION.md`: + - page 1: `t1` "connected"/"not connected", and `b2` = enter + upload mode; + - page 3: sliders `h0`–`h17` showing the 18 servo positions, plus + `t1` status; + - page 4: `uploadscreen`. + + None of the arm/stand/gait/mode/gyro/speed/height controls exist yet. + The owner chose to add a `control` page per `NEXTION.md`. + - `b2` → **Nextion upload passthrough**: shows `uploadscreen`, then + bridges USB ↔ Serial1 forever, so Nextion Editor can upload a `.tft` + through the remote's USB port. The new firmware keeps this. + - Sticks on **A0–A5**, contiguous (`analogInPin + i`). On the Teensy 4.1, + A4/A5 are pins 18/19 = Wire SDA/SCL, which the MPU6050 needs. The old + code had the MPU code disabled (`if (false)`). Owner's decision: keep + the sticks, move the MPU to Wire2. + - nRF24 on CE 9 / CSN 10, the same as `remote/Config.h`. It used + RF24Network on channel 90, which the new protocol doesn't. + - Stick calibration was a 10 s min/max sweep saved to EEPROM, output + 0–255, with no center or deadzone. + - USB serial at 250000. It received 18 servo bytes + checksum + `0xFE` + from the old Unity `Serial.cs` (servo passthrough, forwarded over RF + to the robot) and printed `i_value,...` stick lines. - The Unity remote stand-in's joystick axis assignment (which of axes 4/6 is forward) couldn't be recovered from the old code. Check it with the panel's raw-axis readout. diff --git a/2026-current/LEGACY/remote/teensy/Joysticks.h b/2026-current/LEGACY/remote/teensy/Joysticks.h new file mode 100644 index 0000000..61445e5 --- /dev/null +++ b/2026-current/LEGACY/remote/teensy/Joysticks.h @@ -0,0 +1,127 @@ +#include + +// start reading from the first byte (address 0) of the EEPROM +unsigned int address = 0; +byte value; + +// Joystick vars +const int analogInPin = A0; + +int sensorValue = 0; +int outputValue = 0; + +//volatile int center[6] = {0, 0, 0, 0, 0, 0}; +//volatile int minvalue[6] = {1000, 1000, 1000, 1000, 1000, 1000}; //ARDUINO +//volatile int maxvalue[6] = {0, 0, 0, 0, 0, 0}; //ARDUINO +int minvalue[6] = {1000, 1000, 1000, 1000, 1000, 1000}; //TEENSY +int maxvalue[6] = {0, 0, 0, 0, 0, 0}; //TEENSY + +void JoystickInit() +{ + // Initialize joysticks first + value = EEPROM.read(address); + + if (value != 0) + { + address++; + for (int i = 0; i < 6; i++) + { + address += sizeof(int); + EEPROM.get(address, minvalue[i]); + address += sizeof(int); + EEPROM.get(address, maxvalue[i]); + // Serial.println(minvalue[i]); + // Serial.println(maxvalue[i]); + } + } + else + { + // if eeprom empty + delay(500); + + // for (int i = 0; i < 6; i++) + // { + // center[i] = analogRead(analogInPin + i); + // } + + int time = 10000; + while (millis() < time) + { + for (int i = 0; i < 6; i++) + { + sensorValue = analogRead(analogInPin + i); + if (sensorValue > maxvalue[i]) { + maxvalue[i] = sensorValue; + } + if (sensorValue < minvalue[i]) { + minvalue[i] = sensorValue; + } + } + } + + // for (int i = 0; i < 6; i++) + // { + // minvalue[i] = center[i] - minvalue[i]; + // maxvalue[i] = (maxvalue[i] - center[i]); + // } + + // write callibration to eeprom + address = 0; + EEPROM.write(address, 1); + + address++; + for (int i = 0; i < 6; i++) + { + address += sizeof(int); + EEPROM.put(address, minvalue[i]); + address += sizeof(int); + EEPROM.put(address, maxvalue[i]); + // Serial.println(minvalue[i]); + // Serial.println(maxvalue[i]); + } + } +} + +struct joyStickdataStruct { + float value[6]; +} myJoyStickdataStruct; + +joyStickdataStruct JoystickGet(bool debug = false) +{ + for (int i = 0; i < 6; i++) + { + sensorValue = analogRead(analogInPin + i); + + if (sensorValue > maxvalue[i]) + maxvalue[i] = sensorValue; + if (sensorValue < minvalue[i]) + minvalue[i] = sensorValue; + + outputValue = map(sensorValue, minvalue[i], maxvalue[i], 0, 255); + outputValue = sensorValue > maxvalue[i] ? 255 : sensorValue < minvalue[i] ? 0 : outputValue; + // if (sensorValue <= center[i]) + // { + // outputValue = map(sensorValue, minvalue[i], center[i], 0, 127); + // } + // else + // { + // outputValue = map(sensorValue, center[i], maxvalue[i], 128, 255); + // } + + myJoyStickdataStruct.value[i] = outputValue; + + if (debug) + { + Serial.print(i); + Serial.print("_"); + Serial.print(myJoyStickdataStruct.value[i]); + if (i != 5) + Serial.print(","); + // Serial.print(" "); + } + } + if (debug) + Serial.println(); + + return myJoyStickdataStruct; +} diff --git a/2026-current/LEGACY/remote/teensy/Nrf.h b/2026-current/LEGACY/remote/teensy/Nrf.h new file mode 100644 index 0000000..f71bae6 --- /dev/null +++ b/2026-current/LEGACY/remote/teensy/Nrf.h @@ -0,0 +1,64 @@ +#include +#include "RF24.h" +#include +#include "TransferDataStructure.h" + +// Nrf24 settings +RF24 radio(9, 10); // CE, CSN +RF24Network network(radio); + +const uint16_t this_node = 00; // Address of our node in Octal format +const uint16_t other_node = 01; // Address of the other node in Octal format + +void NrfInit() +{ + SPI.begin(); + radio.begin(); + network.begin(/*channel*/ 90, /*node address*/ this_node); + + // Set the PA Level low to prevent power supply related issues since this is a + // getting_started sketch, and the likelihood of close proximity of the devices. RF24_PA_MAX is default. + radio.setPALevel(RF24_PA_LOW); +} + +bool NrfSendData(dataStruct2 data, bool debug = false) +//dataStruct2 NrfSendData(dataStruct2 data, bool debug = false) +{ + // network.update(); + + if (debug) + Serial.print("Sending..."); + + RF24NetworkHeader header(/*to node*/ other_node); + bool ok = network.write(header, &data, sizeof(data)); + + if (debug) + { + if (ok) + Serial.println("ok."); + else + Serial.println("failed."); + } + + return ok; + // return data; +} + +dataStruct2 NrfGetData(dataStruct2 data, bool debug = false) +{ + // network.update(); + int timeout = millis() + 100; + while ( network.available() && timeout > millis() ) + { + RF24NetworkHeader header; + network.read(header, &data, sizeof(data)); + + if (debug) + { + Serial.print("Received packet "); + Serial.println(data.servoPositions[0]); + } + } + + return data; +} diff --git a/2026-current/LEGACY/remote/teensy/README.md b/2026-current/LEGACY/remote/teensy/README.md new file mode 100644 index 0000000..c0869a6 --- /dev/null +++ b/2026-current/LEGACY/remote/teensy/README.md @@ -0,0 +1,3 @@ +# remote + +arduino code for remote control \ No newline at end of file diff --git a/2026-current/LEGACY/remote/teensy/SerialPasstrough.h b/2026-current/LEGACY/remote/teensy/SerialPasstrough.h new file mode 100644 index 0000000..54f2b60 --- /dev/null +++ b/2026-current/LEGACY/remote/teensy/SerialPasstrough.h @@ -0,0 +1,122 @@ +#include "TransferDataStructure.h" + +String getValue(String data, char separator, int index) +{ + int found = 0; + int strIndex[] = { 0, -1 }; + int maxIndex = data.length() - 1; + + for (int i = 0; i <= maxIndex && found <= index; i++) { + if (data.charAt(i) == separator || i == maxIndex) { + found++; + strIndex[0] = strIndex[1] + 1; + strIndex[1] = (i == maxIndex) ? i + 1 : i; + } + } + return found > index ? data.substring(strIndex[0], strIndex[1]) : ""; +} + +dataStruct2 GetSerialData(dataStruct2 &dataIn, bool debug = false) +{ + if (Serial.available() && Serial.peek() != -1) + { + // String data = Serial.readStringUntil('\r'); + // while (Serial.read() >= 0); + // int maxIndex = data.length() - 1; + // int index = 0; + // int next_index; + // String data_word; + // String channel; + // String angle; + // do { + // next_index = data.indexOf(',', index); + // + // data_word = data.substring(index, next_index); + // channel = data_word.substring(0, data_word.indexOf('_')); + // angle = data_word.substring(data_word.indexOf('_') + 1); + // + // if (isDigit(angle[0])) // prevent error overwriting first entry. + // dataIn.servoPositions[channel.toInt()] = angle.toFloat(); + // + // index = next_index + 1; + // } while ((next_index != -1) && (next_index < maxIndex)); + + + /*String s = Serial.readStringUntil('s'); + for (int i = 0; i < 18; i++) + { + Serial.setTimeout(10); + String c = Serial.readStringUntil('_'); + String v = Serial.readStringUntil(','); + String cc = Serial.readStringUntil('='); + int channel = c.toInt(); + int value = v.toInt(); + int crc = cc.toInt(); + + if ((crc == (channel) * value) && value >= 0 && value <= 180) + dataIn.servoPositions[channel] = value;*/ + //byte recv[18 + 1]; + if (Serial.available() >= 18 + 1 + 2 + 30) + { + bool recc = false; + byte recv[256] = {0}; + //Serial.readBytesUntil(0xffffff, recv, 255); + Serial.readBytesUntil(0xff, recv, 255); + for (int i = 0; i < 255 - 19; i++) + { + // if (recv[i + 1] == 0xff && recv[i + 2] == 0xff && recv[i + 3] == 0xff) + if (recv[i + 19] == 0xfe) + { + for (int j = 0; j < 18 + 1; j++) + { + recv[j] = recv[(i) + j]; + } + recc = true; + + break; + } + } + + + //s.getBytes(recv, s.length()); + byte crc = 0x00; + for (int i = 0; i < 18; i++) + { + crc += recv[i]; + } + + + if (crc == recv[18]) + { + + if (recc) + { + for (int i = 0; i < 18; i++) + { + dataIn.servoPositions[i] = recv[i]; + } + } + } + //} + + dataIn.remotePasstroughControl = true; + + if (debug) + { + Serial.println("Enabled Servo Passtrough mode!"); + + Serial.println("Output line: "); + for (int i = 0; i < 18; i++) + { + Serial.print(i); + Serial.print(", "); + Serial.print(dataIn.servoPositions[i]); + Serial.println(""); + } + Serial.println(""); + } + } + } + + return dataIn; +} diff --git a/2026-current/LEGACY/remote/teensy/TransferDataStructure.h b/2026-current/LEGACY/remote/teensy/TransferDataStructure.h new file mode 100644 index 0000000..0b3daea --- /dev/null +++ b/2026-current/LEGACY/remote/teensy/TransferDataStructure.h @@ -0,0 +1,38 @@ +#ifndef dataStruct +#define dataStruct + +struct __attribute__((__packed__)) dataStruct2 +{ + bool STOP = false; + + float walkSpeed = 10; //step size + float walkSpeed2 = 0; //gate speed + float walkRotation = 0 ; + float walkDirection = 0; + + float tranform[3] = {0, 0, 0}; + float tranform2[3] = {0, 0, 0}; + float rotation[3] = {0, 0, 0}; + + bool remotePasstroughControl = 0; + float servoPositions[18] = { + 90, 90, 90, + 90, 90, 90, + 90, 90, 90, + + 90, 90, 90, + 90, 90, 90, + 90, 90, 90 + }; +} dataStruct; + + +//template struct print_ct { +// print_ct() { +// static_assert(sizeof...(T) == -1, ""); +// } +//}; +// +//print_ct< sizeof(dataStruct2), alignof(dataStruct2) > v; + +#endif diff --git a/2026-current/LEGACY/remote/teensy/remote.ino b/2026-current/LEGACY/remote/teensy/remote.ino new file mode 100644 index 0000000..5656473 --- /dev/null +++ b/2026-current/LEGACY/remote/teensy/remote.ino @@ -0,0 +1,176 @@ +#include "Joysticks.h" +#include "Nrf.h" +#include "SerialPasstrough.h" +#include +#include +#include "Nextion.h" + +NexPage page0 = NexPage(4, 0, "uploadscreen"); + +NexText t0 = NexText(1, 6, "t1"); +NexText t1 = NexText(3, 22, "t1"); + +NexButton b2 = NexButton(1, 5, "b2"); +NexSlider sliders[18] = {NexSlider(3, 1, "h0"), + NexSlider(3, 2, "h1"), + NexSlider(3, 3, "h2"), + NexSlider(3, 4, "h3"), + NexSlider(3, 5, "h4"), + NexSlider(3, 6, "h5"), + NexSlider(3, 7, "h6"), + NexSlider(3, 8, "h7"), + NexSlider(3, 9, "h8"), + NexSlider(3, 10, "h9"), + NexSlider(3, 11, "h10"), + NexSlider(3, 12, "h11"), + NexSlider(3, 13, "h12"), + NexSlider(3, 14, "h13"), + NexSlider(3, 15, "h14"), + NexSlider(3, 16, "h15"), + NexSlider(3, 17, "h16"), + NexSlider(3, 18, "h17") + }; + +char buffer[100] = {0}; + +NexTouch *nex_listen_list[] = +{ + &b2, + &page0, + &t0, + &t1, + NULL +}; + +void b2PopCallback(void *ptr) +{ + passtrough(); +} + +MPU6050 mpu (Wire); + +dataStruct2 data; +joyStickdataStruct joystickData; + +void setup() { +// Serial.begin(250000, SERIAL_8E1); // ARDUINO + Serial.begin(250000); // TEENSY + // Serial.begin(115200); + Serial1.begin(115200); + // passtrough(); + + nexInit(); + b2.attachPush(b2PopCallback, &b2); + b2.attachPop(b2PopCallback, &b2); + // for (int i = 0; i < 18; i++) + // sliders[i] = NexSlider(3, i++, "h" + i); + // while (true) + // { + // nexLoop(nex_listen_list); + // sliders[0].setValue(30); + // } + + // Initialization + // Wire.begin(); + // mpu.begin(); + // + // mpu.calcGyroOffsets(true, 300, 1000); + // + // // Calibration + // Serial.println("====================================="); + // Serial.println("Starting calibration..."); + // mpu.calcGyroOffsets(); + // Serial.println("Calibration complete!"); + + JoystickInit(); + + NrfInit(); +} + +void passtrough() +{ + delay(100); + for (int i = 0; i < 10; i++) + { + // nexLoop(nex_listen_list); + // delay(10); + page0.show(); + delay(10); + } + + Serial.begin(115200); + Serial1.begin(115200); + do { + if (Serial.available()) { // If anything comes in Serial (USB), + Serial1.write(Serial.read()); // read it and send it out Serial1 (pins 0 & 1) + } + + if (Serial1.available()) { // If anything comes in Serial1 (pins 0 & 1) + Serial.write(Serial1.read()); // read it and send it out Serial (USB) + } + } while (true); +} + +void loop() +{ + nexLoop(nex_listen_list); + network.update(); + + if (false) + { + mpu.update(); + Serial.print("AngX = "); + Serial.print(mpu.getAngleX()); + Serial.print(" / AngY = "); + Serial.print(mpu.getAngleY()); + Serial.print(" / AngZ = "); + Serial.println(mpu.getAngleZ()); + } + + // Serial.print(" / AngX = "); + // Serial.print(mpu.getGyroAngleX()); + // Serial.print(" / AngY = "); + // Serial.print(mpu.getGyroAngleY()); + // Serial.print(" / AngZ = "); + // Serial.println(mpu.getGyroAngleZ()); + + joystickData = JoystickGet(true); + + // test + if (false) + { + data.remotePasstroughControl = true; + data.servoPositions[1] = (joystickData.value[2] * 0.78f); + data.servoPositions[4] = (joystickData.value[2] * 0.78f); + data.servoPositions[7] = (joystickData.value[2] * 0.78f); + + data.servoPositions[10] = 180 - (joystickData.value[2] * 0.78f); + data.servoPositions[13] = 180 - (joystickData.value[2] * 0.78f); + data.servoPositions[16] = 180 - (joystickData.value[2] * 0.78f); + } + + GetSerialData(data, false); + // data = GetSerialData(data, false); + + // data.STOP = true; + + for (int i = 0; i < 18; i++) + sliders[i].setValue(data.servoPositions[i]); + + bool ok = NrfSendData(data, false); + // data = NrfSendData(data, false); + + if (ok) + t0.setText("connected"); + else + t0.setText("not connected"); + if (ok) + t1.setText("connected"); + else + t1.setText("not connected"); + + //test + // data = NrfGetData(data, true); + + delay(20); +} diff --git a/2026-current/remote/Config.h b/2026-current/remote/Config.h index 96828d4..130a031 100644 --- a/2026-current/remote/Config.h +++ b/2026-current/remote/Config.h @@ -14,7 +14,7 @@ constexpr float TX_HZ = 100.0f; // ---- Radio (nRF24L01) on SPI0: MOSI 11, MISO 12, SCK 13 ---- -// BENCH: placeholder CE/CSN pins - match your wiring. +// CE/CSN as in the old remote sketch (LEGACY/remote/teensy/Nrf.h). constexpr uint8_t RADIO_CE_PIN = 9; constexpr uint8_t RADIO_CSN_PIN = 10; // Auto-retransmit: (delay units of 250us, count). Kept short on purpose - a @@ -31,9 +31,11 @@ constexpr uint32_t LINK_LOST_MS = 300; // Logical axis order used everywhere in the remote code. enum StickAxis : uint8_t { L_X = 0, L_Y, L_TWIST, R_X, R_Y, R_TWIST, NUM_AXES }; -// BENCH: placeholder pins. A4/A5 (18/19) are skipped because they're the I2C -// pins the MPU6050 uses. -constexpr uint8_t STICK_PINS[NUM_AXES] = { A0, A1, A2, A3, A6, A7 }; +// A0-A5 in a row, as wired for the old remote (LEGACY/remote/teensy/Joysticks.h). +// A4/A5 are also Wire's SDA/SCL - that's why the MPU6050 is on Wire2 below. +// BENCH: which physical axis is on which pin wasn't recoverable from the old +// code; reorder these so each lands on the right logical axis ('p' dump). +constexpr uint8_t STICK_PINS[NUM_AXES] = { A0, A1, A2, A3, A4, A5 }; // Each logical axis must end up: X + = pushed RIGHT, Y + = pushed FORWARD // (away from you), TWIST + = rotated COUNTERCLOCKWISE seen from above. @@ -67,7 +69,9 @@ constexpr float STICK_EXPO[NUM_AXES] = { 0.25f, 0.25f, 0.30f, 0.20f, 0.20f, // One-pole low-pass on raw ADC counts (1 = no filtering). Pots are noisy. constexpr float STICK_FILTER_ALPHA = 0.5f; -// ---- Remote IMU: MPU6050 on I2C (Wire: SDA 18, SCL 19) ---- +// ---- Remote IMU: MPU6050 on the second I2C bus (Wire2: SDA2 = 25, SCL2 = 24) ---- +// Not Wire (18/19): those pins are stick axes A4/A5. +#define IMU_WIRE Wire2 constexpr uint8_t IMU_ADDR = 0x68; // 0x69 if AD0 is pulled high // Mounting remap: which chip axis (0=X,1=Y,2=Z) and sign gives each handset // axis. Handset frame: +X = away from you, +Y = your left, +Z = up. @@ -103,11 +107,24 @@ constexpr uint32_t WALK_LINGER_MS = 250; // (post-deadzone, -1000..1000 scale), so the robot never arms mid-command. constexpr int16_t ARM_CENTER_TOLERANCE = 50; -// ---- Nextion touchscreen on Serial2 (RX2 = pin 7, TX2 = pin 8) ---- -#define NEXTION_SERIAL Serial2 -// BENCH: Nextion ships at 9600. The HMI's Program.s must set `bauds=115200` -// (see remote/NEXTION.md) or change this to match. +// ---- Nextion touchscreen on Serial1 (RX1 = pin 0, TX1 = pin 1), as in the old remote ---- +#define NEXTION_SERIAL Serial1 +// BENCH: the old sketch used the ITEAD library, whose nexInit() talks at 9600 +// unless it was modified. This firmware expects 115200: add `bauds=115200` +// to the HMI's Program.s (see remote/NEXTION.md) or set 9600 here. constexpr uint32_t NEXTION_BAUD = 115200; +// Controls live on this page (remote/NEXTION.md). Its components must have +// vscope = global, so the firmware can update them whichever page is showing. +#define NEXTION_CONTROL_PAGE "control" +// The old HMI's link-status texts (pages 1 and 3, both named t1), also +// vscope = global. BENCH: use your real page names (Nextion's default +// names are page0..page5; only page 4 "uploadscreen" is known). +constexpr const char* NEXTION_STATUS_TEXTS[] = { "page1.t1", "page3.t1" }; +// The old HMI's "upload" button: page 1, component id 5 (b2), sends the +// standard touch event. It puts the remote into Nextion upload passthrough. +constexpr uint8_t NEXTION_UPLOAD_BUTTON_PAGE = 1; +constexpr uint8_t NEXTION_UPLOAD_BUTTON_ID = 5; +#define NEXTION_UPLOAD_PAGE "uploadscreen" constexpr uint32_t NEXTION_FULL_REFRESH_MS = 2000; // re-push everything periodically (self-heals after a display reset) constexpr uint32_t NEXTION_LINK_REFRESH_MS = 250; // link-quality text update rate constexpr uint32_t STATUS_MESSAGE_MS = 2500; // how long transient status messages stay up diff --git a/2026-current/remote/NEXTION.md b/2026-current/remote/NEXTION.md index a312e0f..731a9a5 100644 --- a/2026-current/remote/NEXTION.md +++ b/2026-current/remote/NEXTION.md @@ -1,14 +1,25 @@ # Nextion HMI build guide -> The owner's earlier 6-page HMI is in `LEGACY/remote/remote.HMI`. This -> guide describes the minimal single page the current firmware expects. -> To reuse the old HMI instead, `NextionUI` has to be changed to match -> its component names. +Start from your existing HMI (`LEGACY/remote/remote.HMI`, the servo +monitor) and **add one page named `control`** with the components below. +Your existing pages keep working: + +| Existing | What the firmware does with it | +|----------|--------------------------------| +| page 1 `t1`, page 3 `t1` | writes `connected` / `not connected`, as the old firmware did. Set both to **vscope = global**, and put their real names (`page1.t1`, …) in `NEXTION_STATUS_TEXTS` in `Config.h` | +| page 1 `b2` (id 5) | **upload mode**: keep "Send Component ID" ticked on it. The remote shows `uploadscreen` and passes USB straight through to the display, so Nextion Editor can upload over the remote's USB port. Power-cycle to leave. Serial key `U` does the same | +| page 4 `uploadscreen` | shown on entering upload mode | +| page 3 sliders `h0`–`h17` | not driven yet: the robot doesn't send servo positions back (that needs the planned telemetry channel) | The firmware drives the display by **component name**, and the display talks back with small custom frames sent from each button's touch event. -Build one page in Nextion Editor to match the tables below. Layout, fonts -and colors are up to you. The names and event code must match exactly. +Layout, fonts and colors are up to you. The names and event code must +match exactly. + +**Set every component on `control` to vscope = global.** The firmware +writes them as `control.` and keeps them current whichever page is +showing. A local component on another page can't be written from outside, +and with `bkcmd=0` the display ignores those writes silently. The remote is authoritative. Use **plain Button** components (not Dual-state buttons) and don't toggle anything locally in the HMI. The @@ -24,9 +35,13 @@ bauds=115200 ``` This sets the display to the `NEXTION_BAUD` in `remote/Config.h`. The -display ships at 9600, and `bauds=` persists across power cycles. +display ships at 9600, and `bauds=` persists across power cycles. The old +firmware used the ITEAD library at 9600, so your HMI probably runs at 9600 +today. -Name the page `main` (page 0). The firmware never changes pages. +Name the new page `control`. The firmware never changes pages, except +into `uploadscreen` for upload mode. Add buttons on your other pages to +switch to it (`page control`). ## Components @@ -75,8 +90,9 @@ uses **Touch Move** as well so the value tracks your finger). Frame format: | `bCal` | `printh 23 01 43` | `C` start / save stick calibration | | `bCalX` | `printh 23 01 51` | `Q` cancel calibration | -**Uncheck "Send Component ID"** on every component. The firmware ignores -standard touch frames, and they only add serial traffic. +**Uncheck "Send Component ID"** on every component on `control`: those +standard touch frames only add serial traffic. The one exception is the old +page 1 `b2` upload button (see the top). Protocol details and the parser live in `NextionUI.h/.cpp`. To add a control, pick an unused command byte, add it to `NextionUI::decodeCustom()` diff --git a/2026-current/remote/NextionUI.cpp b/2026-current/remote/NextionUI.cpp index 94a244a..a18b65b 100644 --- a/2026-current/remote/NextionUI.cpp +++ b/2026-current/remote/NextionUI.cpp @@ -7,21 +7,26 @@ static constexpr uint16_t COL_OFF = 50712; // Nextion default light gray static constexpr uint16_t COL_DANGER = 63488; // red static constexpr uint16_t COL_WARN = 64512; // orange -// Serial2's default 64-byte TX buffer would make a full redraw (~300 bytes -// at 115200 baud) block the TX loop for ~25 ms. Give it room to queue. +// The default 64-byte TX buffer would make a full redraw (~400 bytes at +// 115200 baud) block the TX loop for ~35 ms. Give it room to queue. static uint8_t tx_extra[1024]; static uint8_t rx_extra[128]; +// Every control lives on NEXTION_CONTROL_PAGE with vscope = global, so it's +// addressed as page.component and can be updated whichever page is showing. +#define P NEXTION_CONTROL_PAGE "." + void NextionUI::begin() { NEXTION_SERIAL.begin(NEXTION_BAUD); NEXTION_SERIAL.addMemoryForWrite(tx_extra, sizeof(tx_extra)); NEXTION_SERIAL.addMemoryForRead(rx_extra, sizeof(rx_extra)); - cmd("bkcmd=0"); // no success/fail replies - we never need them + cmd("bkcmd=0"); // no success/fail replies - we never need them (and a + // write to a page the HMI doesn't have is then just ignored) force_full_ = true; } void NextionUI::cmd(const char* fmt, ...) { - char buf[64]; + char buf[80]; va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); @@ -50,11 +55,31 @@ bool NextionUI::decodeCustom(UiEvent& out) { case 'Z': out.cmd = UiCmd::ZeroGyro; break; case 'C': out.cmd = UiCmd::CalStartSave; break; case 'Q': out.cmd = UiCmd::CalCancel; break; + case 'U': out.cmd = UiCmd::NextionUpload; break; default: return false; } return true; } +// Standard Nextion frames (terminated FF FF FF). Two matter: +// 0x88 / 0x00 display (re)started -> redraw everything +// 0x65 page id event touch event from a component with "Send Component +// ID" ticked - how the old HMI's upload button (b2) +// reports, so it keeps working unchanged. +bool NextionUI::decodeNative(UiEvent& out) { + if (native_buf_[0] == 0x88 || native_buf_[0] == 0x00) { + force_full_ = true; + return false; + } + if (native_buf_[0] == 0x65 && native_len_ >= 4 && + native_buf_[1] == NEXTION_UPLOAD_BUTTON_PAGE && native_buf_[2] == NEXTION_UPLOAD_BUTTON_ID) { + out.cmd = UiCmd::NextionUpload; + out.value = 0; + return true; + } + return false; +} + bool NextionUI::poll(UiEvent& out) { while (NEXTION_SERIAL.available()) { uint8_t b = NEXTION_SERIAL.read(); @@ -64,7 +89,7 @@ bool NextionUI::poll(UiEvent& out) { rx_ = Rx::CustomLen; } else { rx_ = Rx::Native; - native_first_ = b; + native_buf_[0] = b; native_len_ = 1; ff_run_ = (b == 0xFF) ? 1 : 0; } @@ -89,12 +114,12 @@ bool NextionUI::poll(UiEvent& out) { break; case Rx::Native: + if (native_len_ < sizeof(native_buf_)) native_buf_[native_len_] = b; native_len_++; ff_run_ = (b == 0xFF) ? ff_run_ + 1 : 0; if (ff_run_ == 3) { - // 0x88 = "Nextion ready" after power-on/reset, 0x00 = startup. - if (native_first_ == 0x88 || native_first_ == 0x00) force_full_ = true; rx_ = Rx::Idle; + if (decodeNative(out)) return true; } else if (native_len_ > 16) { rx_ = Rx::Idle; // not a frame we understand } @@ -109,37 +134,40 @@ bool NextionUI::poll(UiEvent& out) { void NextionUI::pushState(const UiView& v, bool full) { const UiView& s = shown_; if (full || v.armed != s.armed) { - cmd("bArm.txt=\"%s\"", v.armed ? "ARMED" : "DISARMED"); - cmd("bArm.bco=%u", v.armed ? COL_DANGER : COL_OFF); + cmd(P "bArm.txt=\"%s\"", v.armed ? "ARMED" : "DISARMED"); + cmd(P "bArm.bco=%u", v.armed ? COL_DANGER : COL_OFF); } if (full || v.stand != s.stand) { - cmd("bStand.txt=\"%s\"", v.stand ? "STAND" : "SIT"); - cmd("bStand.bco=%u", v.stand ? COL_ON : COL_OFF); + cmd(P "bStand.txt=\"%s\"", v.stand ? "STAND" : "SIT"); + cmd(P "bStand.bco=%u", v.stand ? COL_ON : COL_OFF); } - if (full || v.gait_wave != s.gait_wave) cmd("bGait.txt=\"%s\"", v.gait_wave ? "WAVE" : "TRIPOD"); - if (full || v.mode_b != s.mode_b) cmd("bMode.txt=\"%s\"", v.mode_b ? "HEADING" : "DIRECT"); - if (full || v.rstick_xlate != s.rstick_xlate) cmd("bRs.txt=\"%s\"", v.rstick_xlate ? "R: MOVE" : "R: ROTATE"); - if (full || v.remote_gyro != s.remote_gyro) cmd("bRGyro.bco=%u", v.remote_gyro ? COL_ON : COL_OFF); - if (full || v.robot_gyro != s.robot_gyro) cmd("bBGyro.bco=%u", v.robot_gyro ? COL_ON : COL_OFF); + if (full || v.gait_wave != s.gait_wave) cmd(P "bGait.txt=\"%s\"", v.gait_wave ? "WAVE" : "TRIPOD"); + if (full || v.mode_b != s.mode_b) cmd(P "bMode.txt=\"%s\"", v.mode_b ? "HEADING" : "DIRECT"); + if (full || v.rstick_xlate != s.rstick_xlate) cmd(P "bRs.txt=\"%s\"", v.rstick_xlate ? "R: MOVE" : "R: ROTATE"); + if (full || v.remote_gyro != s.remote_gyro) cmd(P "bRGyro.bco=%u", v.remote_gyro ? COL_ON : COL_OFF); + if (full || v.robot_gyro != s.robot_gyro) cmd(P "bBGyro.bco=%u", v.robot_gyro ? COL_ON : COL_OFF); if (full || v.speed_target != s.speed_target) { - cmd("hSpeed.val=%u", v.speed_target); - cmd("nSpeed.val=%u", (v.speed_target * 100 + 127) / 255); + cmd(P "hSpeed.val=%u", v.speed_target); + cmd(P "nSpeed.val=%u", (v.speed_target * 100 + 127) / 255); } if (full || v.height != s.height) { - cmd("hHeight.val=%d", v.height / 10 + 100); // slider 0..200, 100 = default - cmd("nHeight.val=%d", v.height / 10); // -100..100 % + cmd(P "hHeight.val=%d", v.height / 10 + 100); // slider 0..200, 100 = default + cmd(P "nHeight.val=%d", v.height / 10); // -100..100 % } if (full || v.calibrating != s.calibrating) { - cmd("bCal.txt=\"%s\"", v.calibrating ? "SAVE CAL" : "CAL"); - cmd("bCal.bco=%u", v.calibrating ? COL_WARN : COL_OFF); + cmd(P "bCal.txt=\"%s\"", v.calibrating ? "SAVE CAL" : "CAL"); + cmd(P "bCal.bco=%u", v.calibrating ? COL_WARN : COL_OFF); } - if (full || v.message != s.message) cmd("tMsg.txt=\"%s\"", v.message); + if (full || v.message != s.message) cmd(P "tMsg.txt=\"%s\"", v.message); } void NextionUI::pushLink(const UiView& v) { - if (v.link_ok) cmd("tLink.txt=\"LINK %u%%\"", v.link_pct); - else cmd("tLink.txt=\"NO LINK\""); - cmd("tLink.pco=%u", v.link_ok ? COL_ON : COL_DANGER); + if (v.link_ok) cmd(P "tLink.txt=\"LINK %u%%\"", v.link_pct); + else cmd(P "tLink.txt=\"NO LINK\""); + cmd(P "tLink.pco=%u", v.link_ok ? COL_ON : COL_DANGER); + // The old HMI's own status texts, same wording as the old firmware. + for (const char* t : NEXTION_STATUS_TEXTS) + cmd("%s.txt=\"%s\"", t, v.link_ok ? "connected" : "not connected"); } void NextionUI::render(const UiView& v, uint32_t now_ms) { @@ -160,3 +188,27 @@ void NextionUI::render(const UiView& v, uint32_t now_ms) { shown_ = v; } + +// ---- Upload passthrough ---- + +// Lets Nextion Editor upload a new .tft through the remote's USB port, with +// the display still in the case: show the upload page, then wire USB serial +// straight through to the display, forever (power-cycle to leave). Nextion +// Editor switches baud rate mid-upload; Teensy's USB serial reports the +// rate the PC asked for, so the display side follows it. +void NextionUI::runUploadPassthrough() { + cmd("page " NEXTION_UPLOAD_PAGE); + NEXTION_SERIAL.flush(); + delay(100); + uint32_t baud = NEXTION_BAUD; + for (;;) { + uint32_t want = Serial.baud(); + if (want >= 2400 && want != baud) { + NEXTION_SERIAL.flush(); + NEXTION_SERIAL.begin(want); + baud = want; + } + while (Serial.available()) NEXTION_SERIAL.write(Serial.read()); + while (NEXTION_SERIAL.available()) Serial.write(NEXTION_SERIAL.read()); + } +} diff --git a/2026-current/remote/NextionUI.h b/2026-current/remote/NextionUI.h index 5cb55ef..f86bb1a 100644 --- a/2026-current/remote/NextionUI.h +++ b/2026-current/remote/NextionUI.h @@ -8,8 +8,9 @@ // '#' [value] e.g. printh 23 01 41 (ToggleArm) // printh 23 02 56 + prints hSpeed.val,1 // Command bytes are listed in NEXTION.md and in NextionUI.cpp. -// Native Nextion return frames (terminated FF FF FF) are also parsed so a -// display reset (0x88 "ready") triggers a full redraw. +// Native Nextion return frames (terminated FF FF FF) are also parsed: a +// display reset (0x88 "ready") triggers a full redraw, and the old HMI's +// upload button sends a standard touch event (0x65) that's recognized. // // remote -> Nextion: plain instructions ("bArm.txt=\"ARMED\"" + FF FF FF). // @@ -23,12 +24,16 @@ public: bool poll(UiEvent& out); // call every loop; returns one decoded event at a time void render(const UiView& v, uint32_t now_ms); + // Nextion Editor upload through the remote's USB port. Never returns. + [[noreturn]] void runUploadPassthrough(); + private: enum class Rx : uint8_t { Idle, CustomLen, CustomBody, Native }; Rx rx_ = Rx::Idle; uint8_t body_[4]; uint8_t body_len_ = 0, body_got_ = 0; - uint8_t native_first_ = 0, native_len_ = 0, ff_run_ = 0; + uint8_t native_buf_[8]; + uint8_t native_len_ = 0, ff_run_ = 0; UiView shown_; bool force_full_ = true; @@ -39,4 +44,5 @@ private: void pushState(const UiView& v, bool full); void pushLink(const UiView& v); bool decodeCustom(UiEvent& out); + bool decodeNative(UiEvent& out); }; diff --git a/2026-current/remote/README.md b/2026-current/remote/README.md index c06936d..f65c979 100644 --- a/2026-current/remote/README.md +++ b/2026-current/remote/README.md @@ -11,17 +11,19 @@ at 100 Hz. From the repo root: `pio run -e remote`, flash with `pio run -e remote -t upload`, monitor with `pio device monitor -e remote`. -## Wiring (placeholders; edit `Config.h` to match) +## Wiring (from the old remote sketch in `LEGACY/remote/teensy/`; edit `Config.h` if yours differs) | Part | Signal | Teensy 4.1 pin | |------|--------|----------------| | nRF24L01 | MOSI / MISO / SCK | 11 / 12 / 13 (SPI0) | | nRF24L01 | CE / CSN | 9 / 10 | | nRF24L01 | VCC | 3.3 V, with a 10–100 µF cap right at the module | -| MPU6050 | SDA / SCL | 18 / 19 (Wire) | -| Nextion | TX → RX2, RX ← TX2 | 7 / 8 (Serial2), 5 V supply | -| Left stick | X / Y / twist | A0 / A1 / A2 | -| Right stick | X / Y / twist | A3 / A6 / A7 | +| MPU6050 | SDA / SCL | **25 / 24 (Wire2)**: not 18/19, those are stick pins A4/A5 | +| Nextion | TX → RX1, RX ← TX1 | 0 / 1 (Serial1), 5 V supply | +| Sticks | 6 axes | A0–A5. Which axis is on which pin isn't known yet: reorder `STICK_PINS` until the `p` dump reads right | + +**MPU6050 rewiring:** the old remote had its MPU code switched off, probably +because A4/A5 double as Wire's pins. Move the MPU's SDA/SCL to pins 25/24. Stick pots are wired 3.3 V – wiper – GND. **Never feed 5 V into a Teensy 4.x analog pin.** @@ -101,7 +103,7 @@ display attached. | `]` / `[` | height up / down 10% | | | | `c` | start / save calibration | `x` | cancel calibration | | `p` | print status (raw + mapped sticks, IMU, link, state, cal) | `d` | toggle 10 Hz packet dump | -| `R` | reset stick cal to Config defaults | | | +| `R` | reset stick cal to Config defaults | `U` | Nextion upload mode (USB ↔ display; power-cycle to leave) | ## Bench-check checklist diff --git a/2026-current/remote/RemoteIMU.cpp b/2026-current/remote/RemoteIMU.cpp index 919f78b..3061883 100644 --- a/2026-current/remote/RemoteIMU.cpp +++ b/2026-current/remote/RemoteIMU.cpp @@ -9,15 +9,15 @@ static constexpr uint8_t REG_PWR_MGMT_1 = 0x6B; static constexpr uint8_t REG_ACCEL_XOUT_H = 0x3B; static bool writeReg(uint8_t reg, uint8_t val) { - Wire.beginTransmission(IMU_ADDR); - Wire.write(reg); - Wire.write(val); - return Wire.endTransmission() == 0; + IMU_WIRE.beginTransmission(IMU_ADDR); + IMU_WIRE.write(reg); + IMU_WIRE.write(val); + return IMU_WIRE.endTransmission() == 0; } bool RemoteIMU::begin() { - Wire.begin(); - Wire.setClock(400000); + IMU_WIRE.begin(); + IMU_WIRE.setClock(400000); ok_ = writeReg(REG_PWR_MGMT_1, 0x00); // wake out of sleep if (!ok_) return false; writeReg(REG_CONFIG, 0x03); // 44 Hz DLPF: a handheld remote doesn't need more bandwidth @@ -49,16 +49,16 @@ bool RemoteIMU::begin() { } bool RemoteIMU::readRaw(float a[3], float g[3]) { - Wire.beginTransmission(IMU_ADDR); - Wire.write(REG_ACCEL_XOUT_H); - if (Wire.endTransmission(false) != 0) return false; - Wire.requestFrom((int)IMU_ADDR, 14); - if (Wire.available() < 14) return false; + IMU_WIRE.beginTransmission(IMU_ADDR); + IMU_WIRE.write(REG_ACCEL_XOUT_H); + if (IMU_WIRE.endTransmission(false) != 0) return false; + IMU_WIRE.requestFrom((int)IMU_ADDR, 14); + if (IMU_WIRE.available() < 14) return false; int16_t raw[7]; // ax ay az temp gx gy gz, big-endian for (int k = 0; k < 7; k++) { - uint8_t hi = Wire.read(); // two statements: the order of calls inside one expression is unspecified - uint8_t lo = Wire.read(); + uint8_t hi = IMU_WIRE.read(); // two statements: the order of calls inside one expression is unspecified + uint8_t lo = IMU_WIRE.read(); raw[k] = (int16_t)((hi << 8) | lo); } diff --git a/2026-current/remote/RemoteState.cpp b/2026-current/remote/RemoteState.cpp index 1616380..000ef4e 100644 --- a/2026-current/remote/RemoteState.cpp +++ b/2026-current/remote/RemoteState.cpp @@ -78,6 +78,9 @@ bool RemoteState::handle(const UiEvent& e, Sticks& stick_hw, RemoteIMU& imu, uin setMessage("Calibration saved", now); return true; + case UiCmd::NextionUpload: + return false; // not operator state - remote.ino handles it + case UiCmd::CalCancel: if (!calibrating_) return false; stick_hw.cancelCalibration(); diff --git a/2026-current/remote/SerialConsole.cpp b/2026-current/remote/SerialConsole.cpp index 0e29255..ed83d10 100644 --- a/2026-current/remote/SerialConsole.cpp +++ b/2026-current/remote/SerialConsole.cpp @@ -22,6 +22,7 @@ bool SerialConsole::poll(UiEvent& ui, Local& local) { case 'z': ui.cmd = UiCmd::ZeroGyro; return true; case 'c': ui.cmd = UiCmd::CalStartSave; return true; case 'x': ui.cmd = UiCmd::CalCancel; return true; + case 'U': ui.cmd = UiCmd::NextionUpload; return true; case 'p': local = Local::PrintStatus; return true; case 'd': local = Local::ToggleDump; return true; case 'R': local = Local::ResetCal; return true; diff --git a/2026-current/remote/UiTypes.h b/2026-current/remote/UiTypes.h index c62b619..ecd4ccf 100644 --- a/2026-current/remote/UiTypes.h +++ b/2026-current/remote/UiTypes.h @@ -23,6 +23,7 @@ enum class UiCmd : uint8_t { ZeroGyro, // capture the handset's current attitude as "level" CalStartSave, // start sweep calibration, or save it if running CalCancel, + NextionUpload, // USB <-> display passthrough for Nextion Editor (handled in remote.ino) }; struct UiEvent { diff --git a/2026-current/remote/remote.ino b/2026-current/remote/remote.ino index ea411ac..683c670 100644 --- a/2026-current/remote/remote.ino +++ b/2026-current/remote/remote.ino @@ -47,12 +47,27 @@ static void dumpPacket(const CmdPacket& p) { p.remote_roll, p.remote_pitch, p.body_height, p.speed_scale, p.flags); } +// Nextion Editor upload through this USB port (the old remote's b2 button). +// Never returns: the TX loop stops, so the robot loses the link and +// disarms itself after LINK_TIMEOUT_SEC. Power-cycle the remote to leave. +static void enterNextionUpload() { + state.handle(UiEvent{ UiCmd::Disarm, 0 }, sticks, imu, millis()); + Serial.println("Nextion upload mode: connect Nextion Editor to this USB port and upload."); + Serial.println("Power-cycle the remote to leave."); + Serial.flush(); + nextion.runUploadPassthrough(); +} + static void pollInputs(uint32_t now_ms) { UiEvent ev; - while (nextion.poll(ev)) state.handle(ev, sticks, imu, now_ms); + while (nextion.poll(ev)) { + if (ev.cmd == UiCmd::NextionUpload) enterNextionUpload(); + state.handle(ev, sticks, imu, now_ms); + } SerialConsole::Local local; while (console.poll(ev, local)) { + if (ev.cmd == UiCmd::NextionUpload) enterNextionUpload(); if (ev.cmd != UiCmd::None) { bool ok = state.handle(ev, sticks, imu, now_ms); if (!ok && state.message(now_ms)[0]) Serial.println(state.message(now_ms));