diff options
| author | Your Name <you@example.com> | 2026-05-18 16:38:29 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-18 16:38:29 +0530 |
| commit | e0eeeab6eb714ab22d0bc6697730b71d2381746e (patch) | |
| tree | 5525d75ffd34d0b43cdce9e40a1a50d8a2be0301 | |
| parent | 36fa7451a3be1c49932e944859364fe4e9f9fcc4 (diff) | |
feat: per-board hardware locks (board-a/b/c.lock), fix port assignments
- Lock files in physical-router-test-automation/locks/
- Each board has its own lock: board-a.lock, board-b.lock, board-c.lock
- All hardware-touching targets (flash, monitor, erase-nvs, reset, serial-log, tests) require the corresponding board lock
- Read-only targets (build, cvm-pubkey, lock-status) work without lock
- Fixed port assignments: A=ACM1, B=ACM2, C=ACM0
- Router hardware lock kept separate (for OpenWRT operations)
| -rw-r--r-- | Makefile | 155 |
1 files changed, 141 insertions, 14 deletions
| @@ -7,8 +7,8 @@ export | |||
| 7 | IDF_PATH ?= $(HOME)/esp/esp-idf | 7 | IDF_PATH ?= $(HOME)/esp/esp-idf |
| 8 | PROJECT_DIR := $(shell pwd) | 8 | PROJECT_DIR := $(shell pwd) |
| 9 | BUILD_DIR := $(PROJECT_DIR)/build | 9 | BUILD_DIR := $(PROJECT_DIR)/build |
| 10 | PORT_A ?= /dev/ttyACM0 | 10 | PORT_A ?= /dev/ttyACM1 |
| 11 | PORT_B ?= /dev/ttyACM1 | 11 | PORT_B ?= /dev/ttyACM2 |
| 12 | PORT ?= $(PORT_A) | 12 | PORT ?= $(PORT_A) |
| 13 | BAUD ?= 460800 | 13 | BAUD ?= 460800 |
| 14 | TARGET ?= esp32s3 | 14 | TARGET ?= esp32s3 |
| @@ -19,6 +19,62 @@ PYTHON ?= python3 | |||
| 19 | 19 | ||
| 20 | TOLLGATE_IP ?= 10.192.45.1 | 20 | TOLLGATE_IP ?= 10.192.45.1 |
| 21 | 21 | ||
| 22 | BOARD ?= b | ||
| 23 | |||
| 24 | HARDWARE_LOCK_DIR := /home/c03rad0r/physical-router-test-automation/locks | ||
| 25 | |||
| 26 | RED := \033[31m | ||
| 27 | GREEN := \033[32m | ||
| 28 | YELLOW := \033[33m | ||
| 29 | BOLD := \033[1m | ||
| 30 | RESET := \033[0m | ||
| 31 | |||
| 32 | define require_lock_a | ||
| 33 | @if [ ! -f "$(HARDWARE_LOCK_DIR)/board-a.lock" ]; then \ | ||
| 34 | echo "$(RED)$(BOLD)Board A not locked — run 'make lock-a PHASE=\"description\"' first$(RESET)"; \ | ||
| 35 | echo "$(YELLOW)Another LLM session may be using Board A.$(RESET)"; \ | ||
| 36 | exit 1; \ | ||
| 37 | fi | ||
| 38 | endef | ||
| 39 | |||
| 40 | define require_lock_b | ||
| 41 | @if [ ! -f "$(HARDWARE_LOCK_DIR)/board-b.lock" ]; then \ | ||
| 42 | echo "$(RED)$(BOLD)Board B not locked — run 'make lock-b PHASE=\"description\"' first$(RESET)"; \ | ||
| 43 | echo "$(YELLOW)Another LLM session may be using Board B.$(RESET)"; \ | ||
| 44 | exit 1; \ | ||
| 45 | fi | ||
| 46 | endef | ||
| 47 | |||
| 48 | define _require_board_lock | ||
| 49 | @if [ ! -f "$(HARDWARE_LOCK_DIR)/board-$(BOARD).lock" ]; then \ | ||
| 50 | echo "$(RED)$(BOLD)Board $(BOARD) not locked — run 'make lock-$(BOARD) PHASE=\"description\"' first$(RESET)"; \ | ||
| 51 | echo "$(YELLOW)Another LLM session may be using Board $(BOARD).$(RESET)"; \ | ||
| 52 | exit 1; \ | ||
| 53 | fi | ||
| 54 | endef | ||
| 55 | |||
| 56 | define _acquire_lock | ||
| 57 | @if [ -f "$(HARDWARE_LOCK_DIR)/$(1).lock" ]; then \ | ||
| 58 | echo "$(RED)$(BOLD)Cannot acquire lock — $(1) already locked:$(RESET)"; \ | ||
| 59 | echo ""; \ | ||
| 60 | cat $(HARDWARE_LOCK_DIR)/$(1).lock | sed 's/^/ /'; \ | ||
| 61 | echo ""; \ | ||
| 62 | echo "$(YELLOW)Use 'make force-unlock-$(1)' to override.$(RESET)"; \ | ||
| 63 | exit 1; \ | ||
| 64 | fi; \ | ||
| 65 | branch=$$(git branch --show-current 2>/dev/null || echo "unknown"); \ | ||
| 66 | worktree=$$(pwd); \ | ||
| 67 | echo "locked: true" > $(HARDWARE_LOCK_DIR)/$(1).lock; \ | ||
| 68 | echo "board: $(1)" >> $(HARDWARE_LOCK_DIR)/$(1).lock; \ | ||
| 69 | echo "branch: $$branch" >> $(HARDWARE_LOCK_DIR)/$(1).lock; \ | ||
| 70 | echo "worktree: $$worktree" >> $(HARDWARE_LOCK_DIR)/$(1).lock; \ | ||
| 71 | echo "session: $$USER@$$HOSTNAME" >> $(HARDWARE_LOCK_DIR)/$(1).lock; \ | ||
| 72 | echo "timestamp: $$(date -u '+%Y-%m-%dT%H:%M:%SZ')" >> $(HARDWARE_LOCK_DIR)/$(1).lock; \ | ||
| 73 | echo "phase: $(PHASE)" >> $(HARDWARE_LOCK_DIR)/$(1).lock; \ | ||
| 74 | echo "$(GREEN)$(BOLD)$(1) lock acquired$(RESET)"; \ | ||
| 75 | cat $(HARDWARE_LOCK_DIR)/$(1).lock | ||
| 76 | endef | ||
| 77 | |||
| 22 | .PHONY: help setup detect-ports detect-chip detect-all | 78 | .PHONY: help setup detect-ports detect-chip detect-all |
| 23 | .PHONY: flash flash-a flash-b monitor monitor-a monitor-b | 79 | .PHONY: flash flash-a flash-b monitor monitor-a monitor-b |
| 24 | .PHONY: test test-unit test-integration test-e2e test-all | 80 | .PHONY: test test-unit test-integration test-e2e test-all |
| @@ -27,6 +83,7 @@ TOLLGATE_IP ?= 10.192.45.1 | |||
| 27 | .PHONY: tokens wallet-setup wallet-info wallet-balance mint-token send-token | 83 | .PHONY: tokens wallet-setup wallet-info wallet-balance mint-token send-token |
| 28 | .PHONY: clean erase-nvs reset serial-log bootstrap-config | 84 | .PHONY: clean erase-nvs reset serial-log bootstrap-config |
| 29 | .PHONY: cvm-pubkey cvm-test-tool cvm-announce | 85 | .PHONY: cvm-pubkey cvm-test-tool cvm-announce |
| 86 | .PHONY: lock-a lock-b unlock-a unlock-b force-unlock-a force-unlock-b lock-status | ||
| 30 | 87 | ||
| 31 | help: | 88 | help: |
| 32 | @echo "TollGate ESP32 — Makefile" | 89 | @echo "TollGate ESP32 — Makefile" |
| @@ -129,13 +186,18 @@ setup: | |||
| 129 | 186 | ||
| 130 | flash: build | 187 | flash: build |
| 131 | @echo "=== Flashing to $(PORT) ===" | 188 | @echo "=== Flashing to $(PORT) ===" |
| 132 | . $(IDF_PATH)/export.sh && idf.py -p $(PORT) -b $(BAUD) flash | 189 | @echo "$(RED)Error: use 'make flash-a' or 'make flash-b' (per-board lock required)$(RESET)" |
| 190 | @exit 1 | ||
| 133 | 191 | ||
| 134 | flash-a: PORT=$(PORT_A) | 192 | flash-a: build |
| 135 | flash-a: flash | 193 | $(call require_lock_a) |
| 194 | @echo "=== Flashing to $(PORT_A) (Board A) ===" | ||
| 195 | . $(IDF_PATH)/export.sh && idf.py -p $(PORT_A) -b $(BAUD) flash | ||
| 136 | 196 | ||
| 137 | flash-b: PORT=$(PORT_B) | 197 | flash-b: build |
| 138 | flash-b: flash | 198 | $(call require_lock_b) |
| 199 | @echo "=== Flashing to $(PORT_B) (Board B) ===" | ||
| 200 | . $(IDF_PATH)/export.sh && idf.py -p $(PORT_B) -b $(BAUD) flash | ||
| 139 | 201 | ||
| 140 | build: | 202 | build: |
| 141 | @echo "=== Building $(TARGET) ===" | 203 | @echo "=== Building $(TARGET) ===" |
| @@ -143,14 +205,13 @@ build: | |||
| 143 | idf.py set-target $(TARGET) 2>/dev/null; \ | 205 | idf.py set-target $(TARGET) 2>/dev/null; \ |
| 144 | idf.py build | 206 | idf.py build |
| 145 | 207 | ||
| 146 | monitor: | 208 | monitor-a: |
| 147 | . $(IDF_PATH)/export.sh && idf.py -p $(PORT) monitor | 209 | $(call require_lock_a) |
| 210 | . $(IDF_PATH)/export.sh && idf.py -p $(PORT_A) monitor | ||
| 148 | 211 | ||
| 149 | monitor-a: PORT=$(PORT_A) | 212 | monitor-b: |
| 150 | monitor-a: monitor | 213 | $(call require_lock_b) |
| 151 | 214 | . $(IDF_PATH)/export.sh && idf.py -p $(PORT_B) monitor | |
| 152 | monitor-b: PORT=$(PORT_B) | ||
| 153 | monitor-b: monitor | ||
| 154 | 215 | ||
| 155 | # ────────────────────────────────────────────── | 216 | # ────────────────────────────────────────────── |
| 156 | # Testing | 217 | # Testing |
| @@ -164,6 +225,7 @@ test-integration: test-api test-network test-reset-auth test-dns-firewall test-c | |||
| 164 | @echo "=== Integration tests passed ===" | 225 | @echo "=== Integration tests passed ===" |
| 165 | 226 | ||
| 166 | test-e2e: | 227 | test-e2e: |
| 228 | $(call _require_board_lock) | ||
| 167 | @echo "=== Running Playwright E2E tests ===" | 229 | @echo "=== Running Playwright E2E tests ===" |
| 168 | cd tests/e2e && npx playwright test | 230 | cd tests/e2e && npx playwright test |
| 169 | 231 | ||
| @@ -174,38 +236,47 @@ test: test-unit test-integration | |||
| 174 | @echo "=== Tests passed ===" | 236 | @echo "=== Tests passed ===" |
| 175 | 237 | ||
| 176 | test-smoke: | 238 | test-smoke: |
| 239 | $(call _require_board_lock) | ||
| 177 | @echo "=== Running smoke test (30s) ===" | 240 | @echo "=== Running smoke test (30s) ===" |
| 178 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/smoke.mjs | 241 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/smoke.mjs |
| 179 | 242 | ||
| 180 | test-api: | 243 | test-api: |
| 244 | $(call _require_board_lock) | ||
| 181 | @echo "=== Running API tests ===" | 245 | @echo "=== Running API tests ===" |
| 182 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/api.mjs | 246 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/api.mjs |
| 183 | 247 | ||
| 184 | test-network: | 248 | test-network: |
| 249 | $(call _require_board_lock) | ||
| 185 | @echo "=== Running network tests ===" | 250 | @echo "=== Running network tests ===" |
| 186 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/network.mjs | 251 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/network.mjs |
| 187 | 252 | ||
| 188 | test-portal: | 253 | test-portal: |
| 254 | $(call _require_board_lock) | ||
| 189 | @echo "=== Running Playwright portal tests ===" | 255 | @echo "=== Running Playwright portal tests ===" |
| 190 | cd tests/e2e && npx playwright test captive-portal.spec.mjs | 256 | cd tests/e2e && npx playwright test captive-portal.spec.mjs |
| 191 | 257 | ||
| 192 | test-payment: | 258 | test-payment: |
| 259 | $(call _require_board_lock) | ||
| 193 | @echo "=== Running payment tests ===" | 260 | @echo "=== Running payment tests ===" |
| 194 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/phase2.mjs | 261 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/phase2.mjs |
| 195 | 262 | ||
| 196 | test-reset-auth: | 263 | test-reset-auth: |
| 264 | $(call _require_board_lock) | ||
| 197 | @echo "=== Running reset auth test ===" | 265 | @echo "=== Running reset auth test ===" |
| 198 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/test-reset-auth.mjs | 266 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/test-reset-auth.mjs |
| 199 | 267 | ||
| 200 | test-session-expiry: | 268 | test-session-expiry: |
| 269 | $(call _require_board_lock) | ||
| 201 | @echo "=== Running session expiry test (65s wait, ~80s total) ===" | 270 | @echo "=== Running session expiry test (65s wait, ~80s total) ===" |
| 202 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/test-session-expiry.mjs | 271 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/test-session-expiry.mjs |
| 203 | 272 | ||
| 204 | test-dns-firewall: | 273 | test-dns-firewall: |
| 274 | $(call _require_board_lock) | ||
| 205 | @echo "=== Running DNS + firewall test ===" | 275 | @echo "=== Running DNS + firewall test ===" |
| 206 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/test-dns-firewall.mjs | 276 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/test-dns-firewall.mjs |
| 207 | 277 | ||
| 208 | test-cvm: | 278 | test-cvm: |
| 279 | $(call _require_board_lock) | ||
| 209 | @echo "=== Running CVM integration test ===" | 280 | @echo "=== Running CVM integration test ===" |
| 210 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/test-cvm.mjs | 281 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/test-cvm.mjs |
| 211 | 282 | ||
| @@ -255,6 +326,7 @@ cvm-announce: | |||
| 255 | curl -s http://$(TOLLGATE_IP):2121/ | head -1 || echo "Board not reachable" | 326 | curl -s http://$(TOLLGATE_IP):2121/ | head -1 || echo "Board not reachable" |
| 256 | 327 | ||
| 257 | cvm-test-tool: | 328 | cvm-test-tool: |
| 329 | $(call _require_board_lock) | ||
| 258 | @METHOD=$${METHOD:-get_config}; \ | 330 | @METHOD=$${METHOD:-get_config}; \ |
| 259 | PARAMS=$${PARAMS:-{}}; \ | 331 | PARAMS=$${PARAMS:-{}}; \ |
| 260 | echo "=== Calling $$METHOD via CVM ==="; \ | 332 | echo "=== Calling $$METHOD via CVM ==="; \ |
| @@ -275,16 +347,19 @@ clean: | |||
| 275 | . $(IDF_PATH)/export.sh && idf.py fullclean | 347 | . $(IDF_PATH)/export.sh && idf.py fullclean |
| 276 | 348 | ||
| 277 | erase-nvs: | 349 | erase-nvs: |
| 350 | $(call _require_board_lock) | ||
| 278 | @echo "=== Erasing NVS on $(PORT) ===" | 351 | @echo "=== Erasing NVS on $(PORT) ===" |
| 279 | . $(IDF_PATH)/export.sh && \ | 352 | . $(IDF_PATH)/export.sh && \ |
| 280 | partition_offset=$$(idf.py partition-table 2>/dev/null | grep nvs | awk '{print $$2}'); \ | 353 | partition_offset=$$(idf.py partition-table 2>/dev/null | grep nvs | awk '{print $$2}'); \ |
| 281 | python3 -m esptool --port $(PORT) erase_region $$partition_offset 0x6000 | 354 | python3 -m esptool --port $(PORT) erase_region $$partition_offset 0x6000 |
| 282 | 355 | ||
| 283 | reset: | 356 | reset: |
| 357 | $(call _require_board_lock) | ||
| 284 | @echo "=== Resetting device on $(PORT) ===" | 358 | @echo "=== Resetting device on $(PORT) ===" |
| 285 | python3 -m esptool --port $(PORT) run 2>/dev/null || true | 359 | python3 -m esptool --port $(PORT) run 2>/dev/null || true |
| 286 | 360 | ||
| 287 | serial-log: | 361 | serial-log: |
| 362 | $(call _require_board_lock) | ||
| 288 | @echo "=== Capturing serial output from $(PORT) ===" | 363 | @echo "=== Capturing serial output from $(PORT) ===" |
| 289 | python3 -c "import serial; s=serial.Serial('$(PORT)',115200,timeout=1); \ | 364 | python3 -c "import serial; s=serial.Serial('$(PORT)',115200,timeout=1); \ |
| 290 | [print(s.readline().decode(errors='replace'),end='') for _ in iter(lambda: s.readline(), b'')]" | 365 | [print(s.readline().decode(errors='replace'),end='') for _ in iter(lambda: s.readline(), b'')]" |
| @@ -293,3 +368,55 @@ bootstrap-config: | |||
| 293 | @echo "=== Bootstrapping config.json ===" | 368 | @echo "=== Bootstrapping config.json ===" |
| 294 | @echo '{"wifi_networks":[{"ssid":"$(WIFI_SSID)","password":"$(WIFI_PASSWORD)"}],"ap_ssid":"$(AP_SSID)","ap_password":"$(AP_PASSWORD)","mint_url":"$(MINT_URL)","lnurl_url":"$(LNURL_URL)","price_per_step":$(PRICE_PER_STEP),"step_size_ms":$(STEP_SIZE)}' > main/config.json | 369 | @echo '{"wifi_networks":[{"ssid":"$(WIFI_SSID)","password":"$(WIFI_PASSWORD)"}],"ap_ssid":"$(AP_SSID)","ap_password":"$(AP_PASSWORD)","mint_url":"$(MINT_URL)","lnurl_url":"$(LNURL_URL)","price_per_step":$(PRICE_PER_STEP),"step_size_ms":$(STEP_SIZE)}' > main/config.json |
| 295 | @echo "Config written to main/config.json" | 370 | @echo "Config written to main/config.json" |
| 371 | |||
| 372 | # ────────────────────────────────────────────── | ||
| 373 | # Per-Board Hardware Locks | ||
| 374 | # ────────────────────────────────────────────── | ||
| 375 | |||
| 376 | lock-a: ## Acquire Board A lock (set PHASE="description") | ||
| 377 | $(call _acquire_lock,board-a) | ||
| 378 | |||
| 379 | lock-b: ## Acquire Board B lock (set PHASE="description") | ||
| 380 | $(call _acquire_lock,board-b) | ||
| 381 | |||
| 382 | unlock-a: ## Release Board A lock | ||
| 383 | @if [ ! -f "$(HARDWARE_LOCK_DIR)/board-a.lock" ]; then \ | ||
| 384 | echo "$(YELLOW)Board A not locked.$(RESET)"; exit 0; \ | ||
| 385 | fi; \ | ||
| 386 | rm -f $(HARDWARE_LOCK_DIR)/board-a.lock; \ | ||
| 387 | echo "$(GREEN)Board A lock released.$(RESET)" | ||
| 388 | |||
| 389 | unlock-b: ## Release Board B lock | ||
| 390 | @if [ ! -f "$(HARDWARE_LOCK_DIR)/board-b.lock" ]; then \ | ||
| 391 | echo "$(YELLOW)Board B not locked.$(RESET)"; exit 0; \ | ||
| 392 | fi; \ | ||
| 393 | rm -f $(HARDWARE_LOCK_DIR)/board-b.lock; \ | ||
| 394 | echo "$(GREEN)Board B lock released.$(RESET)" | ||
| 395 | |||
| 396 | force-unlock-a: ## Force-release Board A lock | ||
| 397 | @if [ ! -f "$(HARDWARE_LOCK_DIR)/board-a.lock" ]; then \ | ||
| 398 | echo "$(YELLOW)Board A not locked.$(RESET)"; exit 0; \ | ||
| 399 | fi; \ | ||
| 400 | echo "$(RED)$(BOLD)WARNING: Force-releasing Board A!$(RESET)"; \ | ||
| 401 | cat $(HARDWARE_LOCK_DIR)/board-a.lock | sed 's/^/ /'; \ | ||
| 402 | rm -f $(HARDWARE_LOCK_DIR)/board-a.lock; \ | ||
| 403 | echo "$(GREEN)Board A force-released.$(RESET)" | ||
| 404 | |||
| 405 | force-unlock-b: ## Force-release Board B lock | ||
| 406 | @if [ ! -f "$(HARDWARE_LOCK_DIR)/board-b.lock" ]; then \ | ||
| 407 | echo "$(YELLOW)Board B not locked.$(RESET)"; exit 0; \ | ||
| 408 | fi; \ | ||
| 409 | echo "$(RED)$(BOLD)WARNING: Force-releasing Board B!$(RESET)"; \ | ||
| 410 | cat $(HARDWARE_LOCK_DIR)/board-b.lock | sed 's/^/ /'; \ | ||
| 411 | rm -f $(HARDWARE_LOCK_DIR)/board-b.lock; \ | ||
| 412 | echo "$(GREEN)Board B force-released.$(RESET)" | ||
| 413 | |||
| 414 | lock-status: ## Show all board lock statuses | ||
| 415 | @for board in a b; do \ | ||
| 416 | if [ -f "$(HARDWARE_LOCK_DIR)/board-$$board.lock" ]; then \ | ||
| 417 | echo "$(YELLOW)Board $$board: LOCKED$(RESET)"; \ | ||
| 418 | cat $(HARDWARE_LOCK_DIR)/board-$$board.lock | sed 's/^/ /'; \ | ||
| 419 | else \ | ||
| 420 | echo "Board $$board: $(GREEN)available$(RESET)"; \ | ||
| 421 | fi; \ | ||
| 422 | done | ||