Przeglądaj źródła

Add support for Raytac MDBT50Q-CX-40 (#137)

Co-authored-by: Jari <safijari@isu.edu>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brett Profitt 2 miesięcy temu
rodzic
commit
f0f8c63df1

+ 3 - 0
.github/workflows/build.yml

@@ -91,6 +91,9 @@ jobs:
           mkdir -p build/openpuck build/cache/openpuck
           make build BUILD_PATH=build/cache/openpuck OUTPUT_DIR=build/openpuck
 
+      - name: Compile MDBT50Q-CX-40
+        run: make build-raytac
+
       - name: Generate UF2 (OpenPuck)
         # The Adafruit nRF52 core leaves its UF2 objcopy recipe commented out in
         # platform.txt, so arduino-cli only emits .hex/.zip. Convert the .hex to

+ 55 - 2
Makefile

@@ -50,7 +50,9 @@ _PATH_FLAGS = $(if $(BUILD_PATH),--clean --build-path $(BUILD_PATH) --output-dir
 # (No auto-detect -- uploading to a guessed serial port risks writing to the wrong device. List with
 # `arduino-cli board list`.) FLASH_PORT = whatever goal isn't one of our real targets; the catch-all rule at
 # the bottom swallows it so make doesn't try to build the port path as a target.
-FLASH_PORT := $(filter-out format format-check check build build-recovery reversepuck reversepuck-flash reversepuck-deploy flash deploy,$(MAKECMDGOALS))
+FLASH_PORT := $(filter-out format format-check check build build-raytac \
+	package-raytac flash-raytac deploy-raytac provision-raytac-softdevice \
+	build-recovery reversepuck reversepuck-flash reversepuck-deploy flash deploy,$(MAKECMDGOALS))
 UPLOAD = arduino-cli upload -b $(FQBN) -p "$(FLASH_PORT)" OpenPuck
 
 # ReversePuck (controller dongle, 28DE:1302) build flags. It has ONE HID interface (core default 2 is fine),
@@ -59,13 +61,64 @@ UPLOAD = arduino-cli upload -b $(FQBN) -p "$(FLASH_PORT)" OpenPuck
 RP_USB_FLAGS = -DNRF52840_XXAA {build.flags.usb} -DCFG_TUD_TASK_QUEUE_SZ=$(CFG_TUD_TASK_QUEUE_SZ) -DCFG_TUD_VENDOR_TX_BUFSIZE=$(CFG_TUD_VENDOR_TX_BUFSIZE) $(EXTRA_FLAGS)
 RP_UPLOAD = arduino-cli upload -b $(FQBN) -p "$(FLASH_PORT)" ReversePuckFirmware
 
-.PHONY: format format-check check build build-recovery reversepuck reversepuck-flash reversepuck-deploy flash deploy
+.PHONY: format format-check check build build-raytac package-raytac \
+	flash-raytac deploy-raytac provision-raytac-softdevice build-recovery \
+	reversepuck reversepuck-flash reversepuck-deploy flash deploy
 
 ## Compile the firmware with the required USB flags baked in. Override CFG_TUD_HID / CFG_TUD_TASK_QUEUE_SZ /
 ## EXTRA_FLAGS / FQBN as make variables if needed.
 build:
 	arduino-cli compile -b $(FQBN) $(_PATH_FLAGS) --build-property "build.extra_flags=$(USB_EXTRA_FLAGS)" OpenPuck
 
+## Build for the Raytac MDBT50Q-CX-40 without replacing its Open DFU bootloader.
+## Like `build`, this does NOT run gen_version.sh -- run it yourself first if you want version provenance
+## baked in. (It must not run here: CI generates git_version.h once with OPK_BUILD_VERSION set, and a
+## regeneration mid-workflow would strip the release version out of every build that follows.)
+build-raytac:
+	mkdir -p build/raytac build/cache/raytac
+	arduino-cli compile --clean -b adafruit:nrf52:mdbt50qrx \
+		--build-path build/cache/raytac \
+		--output-dir build/raytac \
+		--build-property "build.extra_flags=$(USB_EXTRA_FLAGS) -DOPK_BOARD_MDBT50Q_CX_40=1" OpenPuck
+
+## Package an existing Raytac build for its factory Nordic Open DFU bootloader.
+package-raytac:
+	@test -f build/raytac/OpenPuck.ino.hex || { \
+		echo "run 'make build-raytac' first"; exit 1; }
+	$(RM) build/raytac/OpenPuck-mdbt50q-cx-40.zip
+	nrfutil nrf5sdk-tools pkg generate --hw-version 52 --sd-req 0xB6 \
+		--application-version 1 \
+		--application build/raytac/OpenPuck.ino.hex \
+		build/raytac/OpenPuck-mdbt50q-cx-40.zip
+
+## Program the application package with the device in physical Open DFU mode.
+flash-raytac:
+	@test -f build/raytac/OpenPuck-mdbt50q-cx-40.zip || { \
+		echo "run 'make package-raytac' first"; exit 1; }
+	nrfutil device program \
+		--firmware build/raytac/OpenPuck-mdbt50q-cx-40.zip \
+		--traits nordicDfu
+
+## Build, package, and program the application without changing the SoftDevice.
+deploy-raytac:
+	$(MAKE) build-raytac
+	$(MAKE) package-raytac
+	$(MAKE) flash-raytac
+
+## One-time S140 6.1.1 provisioning; re-enter Open DFU before flashing the app.
+provision-raytac-softdevice:
+	@test -f "$(SOFTDEVICE_HEX)" || { \
+		echo "set SOFTDEVICE_HEX to Nordic's S140 6.1.1 HEX"; \
+		exit 1; }
+	mkdir -p build/raytac
+	$(RM) build/raytac/s140-6.1.1.zip
+	nrfutil nrf5sdk-tools pkg generate --hw-version 52 \
+		--sd-req 0xAE,0xB6 --sd-id 0xB6 \
+		--softdevice "$(SOFTDEVICE_HEX)" \
+		build/raytac/s140-6.1.1.zip
+	nrfutil device program --firmware build/raytac/s140-6.1.1.zip \
+		--traits nordicDfu
+
 ## One-time factory-reset recovery image (wipes persistent storage once on first boot). See §6 of the build doc.
 build-recovery:
 	$(MAKE) build BUILD_PATH=$(BUILD_PATH) OUTPUT_DIR=$(OUTPUT_DIR) EXTRA_FLAGS="$(EXTRA_FLAGS) -DOPK_FACTORY_RESET=1"

+ 5 - 0
OpenPuck/OpenPuck.ino

@@ -20,6 +20,7 @@
 using namespace Adafruit_LittleFS_Namespace;
 
 #include "config.h"
+#include "board_config.h"
 
 // OPK_GIT_HASH: tags the one-time factory-reset build
 #include "build_info.h"
@@ -123,11 +124,15 @@ void setup()
 	// full-board wipe (debug-only "erase everything"): if the panel armed one, this never returns -- it erases
 	// the app + config/bond + bootloader-settings flash from RAM and resets into the app-less UF2 bootloader.
 	// Checked before the staged-update apply (both use the meta page; only one can be armed at a time).
+	// Both are Adafruit-bootloader-specific (UF2 recovery drive + Adafruit-format settings page), so on
+	// boards without one they're compiled out -- the panel ops that arm them are rejected there too.
+#if OPK_HAS_ADAFRUIT_DFU
 	fwupWipeIfArmed();
 	// staged firmware update (WebUSB "flash on reboot"): if the panel committed one, this never returns --
 	// it copies staged->app from RAM and resets into the new firmware. MUST run before anything else touches
 	// hardware; the board looks dead for the ~5 s the copy takes.
 	fwupApplyIfArmed();
+#endif
 	genSerial();
 	ledInit();
 

+ 8 - 0
OpenPuck/board_config.h

@@ -0,0 +1,8 @@
+#pragma once
+
+#if defined(OPK_BOARD_MDBT50Q_CX_40)
+// The staged updater rewrites an Adafruit-format bootloader settings page.
+#define OPK_HAS_ADAFRUIT_DFU 0
+#else
+#define OPK_HAS_ADAFRUIT_DFU 1
+#endif

+ 17 - 0
OpenPuck/status_led.cpp

@@ -1,6 +1,15 @@
 #include "status_led.h"
 #include <Arduino.h>
 
+#if defined(OPK_BOARD_MDBT50Q_CX_40)
+#include <nrf_gpio.h>
+
+// The borrowed RX variant does not map the CX-40's active-low P0.08 LED.
+#define WAKE_LED_PIN NRF_GPIO_PIN_MAP(0, 8)
+#undef WAKE_LED_ON
+#define WAKE_LED_ON LOW
+#endif
+
 #define WAKE_LED_OFF ((WAKE_LED_ON) == HIGH ? LOW : HIGH)
 #define PULSE_MS 500u // wake flash duration
 
@@ -9,14 +18,22 @@ static bool g_lit = false;
 
 static void ledWrite(int level)
 {
+#if defined(OPK_BOARD_MDBT50Q_CX_40)
+	nrf_gpio_pin_write(WAKE_LED_PIN, level);
+#else
 	digitalWrite(WAKE_LED_PIN_A, level);
 	digitalWrite(WAKE_LED_PIN_B, level);
+#endif
 }
 
 void ledInit()
 {
+#if defined(OPK_BOARD_MDBT50Q_CX_40)
+	nrf_gpio_cfg_output(WAKE_LED_PIN);
+#else
 	pinMode(WAKE_LED_PIN_A, OUTPUT);
 	pinMode(WAKE_LED_PIN_B, OUTPUT);
+#endif
 	ledWrite(WAKE_LED_OFF);
 }
 

+ 25 - 0
OpenPuck/webusb_config.cpp

@@ -1,4 +1,5 @@
 #include "webusb_config.h"
+#include "board_config.h"
 #include "config.h"
 #include "bonds.h"
 #include "rf_link.h"
@@ -39,6 +40,25 @@ static void fwupAckPost(uint8_t status)
 	g_fwupAckPend = true;
 }
 
+static bool boardCommand(uint8_t op)
+{
+#if !OPK_HAS_ADAFRUIT_DFU
+	// The factory Open DFU bootloader uses a different settings page.
+	// 0x25 (full board wipe) is rejected for the same reason AND because its recovery story doesn't exist
+	// here: it erases the app expecting the resident UF2 bootloader to come up as a drag-and-drop drive,
+	// and its bootloader-settings page (0xFF000) lands inside this board's factory bootloader region.
+	if (op == 0x0B || op == 0x0C || op == 0x25) {
+		g_blobRequest = true;
+		return true;
+	}
+	if (op >= 0x20 && op <= 0x24) {
+		fwupAckPost(FWUP_ERR_STATE);
+		return true;
+	}
+#endif
+	return false;
+}
+
 // blob payload = [ver=10][mode][mDiv][mFric][qamMap(active)][abSwap(active)][back0..3(active)][connSlot(0xFF=none)][linkUp]
 //                [f1ps_lo][f1ps_hi][pollU100][newps_lo][newps_hi][e7b][relayOp][relaySub][fwdNewOnly]
 //                [qos][persistMode][chordBtn B][chordBtn X][chordBtn Y][pollsps_lo][pollsps_hi]
@@ -633,6 +653,11 @@ void webusbPoll()
 					       1;
 			if (n < need)
 				break; // wait for more bytes
+			if (boardCommand(op)) {
+				memmove(buf, buf + need, n - need);
+				n -= need;
+				continue;
+			}
 			if (op == 0x01) {
 				g_blobRequest =
 					true; // sent from the usbd task (webusbSofDrain)

+ 79 - 0
docs/BUILD_AND_DEPLOY.md

@@ -73,6 +73,85 @@ make build FQBN=adafruit:nrf52:somethingelse          # a different nRF52840 boa
 arduino-cli compile -b adafruit:nrf52:feather52840 --build-property "build.extra_flags=-DNRF52840_XXAA {build.flags.usb} -DCFG_TUD_HID=6 -DCFG_TUD_TASK_QUEUE_SZ=512 -DCFG_TUD_VENDOR_TX_BUFSIZE=256" OpenPuck
 ```
 
+### 4a. Raytac MDBT50Q-CX-40
+
+Retail listings may shorten MDBT50Q-CX-40 to `MDBT50Q-CX`. This target is only
+for the nRF52840 model with 1 MB flash and 256 KB RAM, not the CX-33.
+
+The build borrows Adafruit's `adafruit:nrf52:mdbt50qrx` profile for its
+nRF52840, S140, and linker settings; it does not modify that installed profile.
+CX-40 GPIO differences remain in OpenPuck.
+
+#### Prerequisites
+
+Download [nRF Util](https://www.nordicsemi.com/Products/Development-tools/nRF-Util)
+and put it in your path.
+
+```bash
+mkdir -p ~/.local/bin
+install -m 755 ~/Downloads/nrfutil ~/.local/bin/nrfutil
+export PATH="$HOME/.local/bin:$PATH"
+
+# Install the DFU packaging and flashing commands.
+nrfutil --version
+nrfutil install nrf5sdk-tools
+nrfutil install device
+```
+
+#### First-time S140 provisioning
+
+OpenPuck requires S140 6.1.1 (`0xB6`). Download and extract Nordic's
+[nRF5 SDK](https://www.nordicsemi.com/Products/Development-software/nRF5-SDK),
+then provision its SoftDevice before installing OpenPuck for the first time:
+
+```bash
+# Hold the dongle button while inserting it; release when the LED fades.
+SDK=/path/to/nRF5_SDK
+make provision-raytac-softdevice \
+  SOFTDEVICE_HEX="$SDK/components/softdevice/s140/hex/\
+s140_nrf52_6.1.1_softdevice.hex"
+
+# Unplug the dongle and re-enter Open DFU before installing the application.
+```
+
+Provisioning is separate from routine builds and updates.
+
+#### Build and flash
+
+```bash
+# Build and create a Nordic Secure DFU application package.
+./gen_version.sh   # recommended: embeds version (when tagged) + git hash provenance
+make build-raytac
+make package-raytac
+
+# Enter physical Open DFU mode, then flash the package.
+make flash-raytac
+
+# For later updates, this runs all three commands above.
+make deploy-raytac
+```
+
+The Adafruit compile recipe also emits `build/raytac/OpenPuck.ino.zip` in its
+older DFU v0.5 format; ignore that automatic output. `make package-raytac`
+creates `OpenPuck-mdbt50q-cx-40.zip`, the Secure DFU package used by the
+factory Open DFU bootloader. These targets do not replace that bootloader or
+use `arduino-cli upload`.
+
+The relevant flash layout is:
+
+- S140 and the MBR occupy the region below the application.
+- OpenPuck starts at `0x26000` and must end before `0xED000`.
+- InternalFS occupies `0xED000` through `0xF3FFF`.
+- The factory bootloader occupies `0xF4000` through `0xFFFFF`.
+
+The populated active-low blue LED is P0.08. OpenPuck reserves it for the
+existing remote-wake diagnostic rather than connection or pairing state.
+The WebUSB firmware updater (§5b), its serial/UF2 DFU buttons, and the debug
+full-board wipe all target the Adafruit bootloader and are rejected by this
+build; use the physical Open DFU workflow above instead. The panel's factory
+erase (§6, filesystem-only) and the serial `ERASE-ALL` console command work
+normally.
+
 ## 5. Upload the firmware
 
 The quickest path is `make`. The serial port is a **required argument** (find it with `arduino-cli board list`):