diff options
Diffstat (limited to 'Makefile')
| -rw-r--r-- | Makefile | 192 |
1 files changed, 178 insertions, 14 deletions
| @@ -19,13 +19,71 @@ 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 |
| 25 | .PHONY: test-smoke test-api test-network test-portal test-payment | 81 | .PHONY: test-smoke test-api test-network test-portal test-payment |
| 26 | .PHONY: test-reset-auth test-session-expiry test-dns-firewall | 82 | .PHONY: test-reset-auth test-session-expiry test-dns-firewall test-cvm |
| 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 |
| 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 | ||
| 29 | 87 | ||
| 30 | help: | 88 | help: |
| 31 | @echo "TollGate ESP32 — Makefile" | 89 | @echo "TollGate ESP32 — Makefile" |
| @@ -50,6 +108,12 @@ help: | |||
| 50 | @echo " test-reset-auth Reset auth + per-client NAT filter test" | 108 | @echo " test-reset-auth Reset auth + per-client NAT filter test" |
| 51 | @echo " test-dns-firewall DNS hijack + NAT filter test" | 109 | @echo " test-dns-firewall DNS hijack + NAT filter test" |
| 52 | @echo " test-session-expiry Session lifecycle with 65s expiry wait" | 110 | @echo " test-session-expiry Session lifecycle with 65s expiry wait" |
| 111 | @echo " test-cvm ContextVM protocol integration test" | ||
| 112 | @echo "" | ||
| 113 | @echo "ContextVM:" | ||
| 114 | @echo " cvm-pubkey Print board's ContextVM npub" | ||
| 115 | @echo " cvm-announce Trigger re-publish of CEP-6 announcements" | ||
| 116 | @echo " cvm-test-tool Send single MCP tools/call (METHOD=get_config)" | ||
| 53 | @echo "" | 117 | @echo "" |
| 54 | @echo "Wallet:" | 118 | @echo "Wallet:" |
| 55 | @echo " wallet-setup Initialize nutshell wallet for test mint" | 119 | @echo " wallet-setup Initialize nutshell wallet for test mint" |
| @@ -122,13 +186,18 @@ setup: | |||
| 122 | 186 | ||
| 123 | flash: build | 187 | flash: build |
| 124 | @echo "=== Flashing to $(PORT) ===" | 188 | @echo "=== Flashing to $(PORT) ===" |
| 125 | . $(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 | ||
| 126 | 191 | ||
| 127 | flash-a: PORT=$(PORT_A) | 192 | flash-a: build |
| 128 | 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 | ||
| 129 | 196 | ||
| 130 | flash-b: PORT=$(PORT_B) | 197 | flash-b: build |
| 131 | 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 | ||
| 132 | 201 | ||
| 133 | build: | 202 | build: |
| 134 | @echo "=== Building $(TARGET) ===" | 203 | @echo "=== Building $(TARGET) ===" |
| @@ -136,14 +205,13 @@ build: | |||
| 136 | idf.py set-target $(TARGET) 2>/dev/null; \ | 205 | idf.py set-target $(TARGET) 2>/dev/null; \ |
| 137 | idf.py build | 206 | idf.py build |
| 138 | 207 | ||
| 139 | monitor: | 208 | monitor-a: |
| 140 | . $(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 | ||
| 141 | 211 | ||
| 142 | monitor-a: PORT=$(PORT_A) | 212 | monitor-b: |
| 143 | monitor-a: monitor | 213 | $(call require_lock_b) |
| 144 | 214 | . $(IDF_PATH)/export.sh && idf.py -p $(PORT_B) monitor | |
| 145 | monitor-b: PORT=$(PORT_B) | ||
| 146 | monitor-b: monitor | ||
| 147 | 215 | ||
| 148 | # ────────────────────────────────────────────── | 216 | # ────────────────────────────────────────────── |
| 149 | # Testing | 217 | # Testing |
| @@ -153,10 +221,11 @@ test-unit: | |||
| 153 | @echo "=== Running host unit tests ===" | 221 | @echo "=== Running host unit tests ===" |
| 154 | $(MAKE) -C tests/unit test | 222 | $(MAKE) -C tests/unit test |
| 155 | 223 | ||
| 156 | test-integration: test-api test-network test-reset-auth test-dns-firewall | 224 | test-integration: test-api test-network test-reset-auth test-dns-firewall test-cvm |
| 157 | @echo "=== Integration tests passed ===" | 225 | @echo "=== Integration tests passed ===" |
| 158 | 226 | ||
| 159 | test-e2e: | 227 | test-e2e: |
| 228 | $(call _require_board_lock) | ||
| 160 | @echo "=== Running Playwright E2E tests ===" | 229 | @echo "=== Running Playwright E2E tests ===" |
| 161 | cd tests/e2e && npx playwright test | 230 | cd tests/e2e && npx playwright test |
| 162 | 231 | ||
| @@ -167,37 +236,50 @@ test: test-unit test-integration | |||
| 167 | @echo "=== Tests passed ===" | 236 | @echo "=== Tests passed ===" |
| 168 | 237 | ||
| 169 | test-smoke: | 238 | test-smoke: |
| 239 | $(call _require_board_lock) | ||
| 170 | @echo "=== Running smoke test (30s) ===" | 240 | @echo "=== Running smoke test (30s) ===" |
| 171 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/smoke.mjs | 241 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/smoke.mjs |
| 172 | 242 | ||
| 173 | test-api: | 243 | test-api: |
| 244 | $(call _require_board_lock) | ||
| 174 | @echo "=== Running API tests ===" | 245 | @echo "=== Running API tests ===" |
| 175 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/api.mjs | 246 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/api.mjs |
| 176 | 247 | ||
| 177 | test-network: | 248 | test-network: |
| 249 | $(call _require_board_lock) | ||
| 178 | @echo "=== Running network tests ===" | 250 | @echo "=== Running network tests ===" |
| 179 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/network.mjs | 251 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/network.mjs |
| 180 | 252 | ||
| 181 | test-portal: | 253 | test-portal: |
| 254 | $(call _require_board_lock) | ||
| 182 | @echo "=== Running Playwright portal tests ===" | 255 | @echo "=== Running Playwright portal tests ===" |
| 183 | cd tests/e2e && npx playwright test captive-portal.spec.mjs | 256 | cd tests/e2e && npx playwright test captive-portal.spec.mjs |
| 184 | 257 | ||
| 185 | test-payment: | 258 | test-payment: |
| 259 | $(call _require_board_lock) | ||
| 186 | @echo "=== Running payment tests ===" | 260 | @echo "=== Running payment tests ===" |
| 187 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/phase2.mjs | 261 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/phase2.mjs |
| 188 | 262 | ||
| 189 | test-reset-auth: | 263 | test-reset-auth: |
| 264 | $(call _require_board_lock) | ||
| 190 | @echo "=== Running reset auth test ===" | 265 | @echo "=== Running reset auth test ===" |
| 191 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/test-reset-auth.mjs | 266 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/test-reset-auth.mjs |
| 192 | 267 | ||
| 193 | test-session-expiry: | 268 | test-session-expiry: |
| 269 | $(call _require_board_lock) | ||
| 194 | @echo "=== Running session expiry test (65s wait, ~80s total) ===" | 270 | @echo "=== Running session expiry test (65s wait, ~80s total) ===" |
| 195 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/test-session-expiry.mjs | 271 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/test-session-expiry.mjs |
| 196 | 272 | ||
| 197 | test-dns-firewall: | 273 | test-dns-firewall: |
| 274 | $(call _require_board_lock) | ||
| 198 | @echo "=== Running DNS + firewall test ===" | 275 | @echo "=== Running DNS + firewall test ===" |
| 199 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/test-dns-firewall.mjs | 276 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/test-dns-firewall.mjs |
| 200 | 277 | ||
| 278 | test-cvm: | ||
| 279 | $(call _require_board_lock) | ||
| 280 | @echo "=== Running CVM integration test ===" | ||
| 281 | TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/test-cvm.mjs | ||
| 282 | |||
| 201 | # ────────────────────────────────────────────── | 283 | # ────────────────────────────────────────────── |
| 202 | # Wallet | 284 | # Wallet |
| 203 | # ────────────────────────────────────────────── | 285 | # ────────────────────────────────────────────── |
| @@ -230,6 +312,33 @@ send-token: | |||
| 230 | tokens: send-token | 312 | tokens: send-token |
| 231 | 313 | ||
| 232 | # ────────────────────────────────────────────── | 314 | # ────────────────────────────────────────────── |
| 315 | # ContextVM | ||
| 316 | # ────────────────────────────────────────────── | ||
| 317 | |||
| 318 | cvm-pubkey: | ||
| 319 | @echo "=== Board ContextVM npub ===" | ||
| 320 | @nak key public a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 | xargs -I{} nak encode npub {} | ||
| 321 | @echo "" | ||
| 322 | @echo "Search for this npub on https://contextvm.org/servers" | ||
| 323 | |||
| 324 | cvm-announce: | ||
| 325 | @echo "=== Triggering CEP-6 re-announcement ===" | ||
| 326 | curl -s http://$(TOLLGATE_IP):2121/ | head -1 || echo "Board not reachable" | ||
| 327 | |||
| 328 | cvm-test-tool: | ||
| 329 | $(call _require_board_lock) | ||
| 330 | @METHOD=$${METHOD:-get_config}; \ | ||
| 331 | PARAMS=$${PARAMS:-{}}; \ | ||
| 332 | echo "=== Calling $$METHOD via CVM ==="; \ | ||
| 333 | NPUB_HEX=$$(nak key public a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2); \ | ||
| 334 | CONTENT="$$(echo "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"$$METHOD\",\"arguments\":$$PARAMS}}" | jq -c .)"; \ | ||
| 335 | EVENT_JSON="$$(nak event --kind 25910 --tag p=$$NPUB_HEX --content "$$CONTENT" wss://relay.damus.io 2>/dev/null)"; \ | ||
| 336 | echo "Published: $$EVENT_JSON"; \ | ||
| 337 | echo "Waiting for response..."; \ | ||
| 338 | sleep 3; \ | ||
| 339 | nak req -k 25910 -a $$NPUB_HEX -l 5 wss://relay.damus.io | ||
| 340 | |||
| 341 | # ────────────────────────────────────────────── | ||
| 233 | # Utilities | 342 | # Utilities |
| 234 | # ────────────────────────────────────────────── | 343 | # ────────────────────────────────────────────── |
| 235 | 344 | ||
| @@ -238,16 +347,19 @@ clean: | |||
| 238 | . $(IDF_PATH)/export.sh && idf.py fullclean | 347 | . $(IDF_PATH)/export.sh && idf.py fullclean |
| 239 | 348 | ||
| 240 | erase-nvs: | 349 | erase-nvs: |
| 350 | $(call _require_board_lock) | ||
| 241 | @echo "=== Erasing NVS on $(PORT) ===" | 351 | @echo "=== Erasing NVS on $(PORT) ===" |
| 242 | . $(IDF_PATH)/export.sh && \ | 352 | . $(IDF_PATH)/export.sh && \ |
| 243 | 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}'); \ |
| 244 | python3 -m esptool --port $(PORT) erase_region $$partition_offset 0x6000 | 354 | python3 -m esptool --port $(PORT) erase_region $$partition_offset 0x6000 |
| 245 | 355 | ||
| 246 | reset: | 356 | reset: |
| 357 | $(call _require_board_lock) | ||
| 247 | @echo "=== Resetting device on $(PORT) ===" | 358 | @echo "=== Resetting device on $(PORT) ===" |
| 248 | python3 -m esptool --port $(PORT) run 2>/dev/null || true | 359 | python3 -m esptool --port $(PORT) run 2>/dev/null || true |
| 249 | 360 | ||
| 250 | serial-log: | 361 | serial-log: |
| 362 | $(call _require_board_lock) | ||
| 251 | @echo "=== Capturing serial output from $(PORT) ===" | 363 | @echo "=== Capturing serial output from $(PORT) ===" |
| 252 | python3 -c "import serial; s=serial.Serial('$(PORT)',115200,timeout=1); \ | 364 | python3 -c "import serial; s=serial.Serial('$(PORT)',115200,timeout=1); \ |
| 253 | [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'')]" |
| @@ -256,3 +368,55 @@ bootstrap-config: | |||
| 256 | @echo "=== Bootstrapping config.json ===" | 368 | @echo "=== Bootstrapping config.json ===" |
| 257 | @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 |
| 258 | @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 | ||