Skip to content

feat(services): add hil terminal services for hardware-in-the-loop validation - #152

Merged
gabrielfrasantos merged 6 commits into
mainfrom
claude/hil-services
Sep 28, 2026
Merged

gabrielfrasantos merged 6 commits into
mainfrom
claude/hil-services

Conversation

@gabrielfrasantos

@gabrielfrasantos gabrielfrasantos commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds services/hil, a HAL-independent command terminal for hardware-in-the-loop validation. It was extracted from hal-ti's validation firmware (embedded-pro/hal-ti#120), so any HAL, e.g. hal-st, can expose its drivers to a host test bench such as ad3-waveforms-bench.

A HAL supplies only three things:

  • pin naming and a pin factory
  • board info
  • one small factory per peripheral, mapping its *.open arguments to the driver's Config

Everything lives directly in namespace services. Public types carry a Hil prefix, following the existing Sesame*/Http* convention. Documentation is in docs/Hil.md, linked from the README and docs/index.md.

services.hil: framework

  • HilTerminal: a TerminalWithCommandsImplBase that brackets each command line and answers ERR usage to unknown commands.
  • HilResponse: the line protocol.
    • OK [k=v...] / ERR <reason> / EVT <peripheral> [k=v...]
    • exactly one final line per command
    • a \r\n prefix on lines produced outside a command
  • HilArguments: parses positionals and key=value, 0x numbers, hex payloads (- for empty), duty cycles, choice tables and pins.
  • Pins:
    • HilPinId, HilPull and HilPinNaming (HilPinNamingDefault is configurable for Tiva PF1 (0–7) or STM32 PA15)
    • HilPinPool: owner bitmask, analog sharing, reserved pins, over an injected HilPinFactory
  • HilSystemCommands: ping, delay, reset (via hal::Reset), info, board.pins, and the EVT boot line, using HilBoardInfo.
  • Helpers: HilBind, which has a const-method overload, HilWithDriver and HilDeadlineTimeKeeper.

services.hil.commands: per-peripheral groups over hal:: interfaces

Group Interfaces
GPIO hal::GpioPin (config, set, get, pulse, interrupts with edge counters)
UART hal::SerialCommunication, SynchronousSerialCommunication
SPI hal::SpiMaster, SynchronousSpi
ADC hal::AdcMultiChannel, SynchronousAdc; the HAL defines the instance key
CAN hal::Can; receive and error EVTs
EEPROM hal::Eeprom; always answers from the completion callback
PWM HilPwmHandle + HilPwmAdapter<Driver> over Single…FourChannelsPwm (sync or async)
QEI SynchronousQuadratureEncoder
Comparator AnalogComparator, SynchronousAnalogComparator
Watchdog hal::Watchdog; early-warning EVT, auto-feed
Ethernet HilEthernetMonitor over EthernetSmi/EthernetMac

HilUnsupportedCommands answers ERR unsupported for peripherals the running MCU lacks.

Shared building blocks:

  • HilInstanceFactory + HilSingleInstanceGroup: one open/close lifecycle shared by six groups.
    • Opening is two steps, Prepare then Open, so argument errors come before busy and pins are claimed only after it.
    • Closing is asynchronous, so a HAL can quiesce its interrupts before destroying a driver.
  • HilPendingOperation: one busy/timeout/stale-completion helper, used by UART, SPI, CAN and EEPROM.
  • HilEdge / HilEdgeCounter: shared by GPIO and the comparator.

Reuses existing EMIL facilities: infra::AtomicTriggerScheduler, infra::TimerLimitedRepeating, QueueForOneReaderOneIrqWriter::ContiguousRange/Consume, the BoundedVector range constructor, infra::AsHex, infra::Tokenizer and Tracer::Continue.

Changes to existing code

  • TerminalWithCommandsImplBase gains protected virtual OnCommandStart(), OnCommandEnd() and OnUnrecognizedCommand(); the defaults keep today's behaviour. TerminalBase narrowing conversions are now explicit.
  • hal::DutyCycle::FromRatio(numerator, denominator): FromPercent uses it, with identical results.
  • New test doubles: CanMock, EthernetSmiMock/EthernetMacMock, SynchronousAdcMock.
  • Fixes to existing test code:
    • SynchronousPwmMock.hpp shared the header guard HAL_PWM_MOCK_HPP with PwmMock.hpp, so including both silently dropped one. It also mocked Start without overriding anything. It now has its own guard and derives from SynchronousSingleChannelPwm.
    • TerminalCommandsStub in TestTerminal.cpp kept its commands in a static array whose lambdas captured the first instance's this. The array is now a member.

Verification

  • host-single-Debug (warnings as errors):
    • services.hil_test: 133/133
    • services.util_test: 149/149
    • hal.interfaces_test: 73/73
    • both test-doubles suites pass
  • embedded preset (Cortex-M4): both libraries compile with warnings as errors.
  • clang-format 18 clean; SonarCloud findings from the first analysis addressed; CRLF line endings preserved.
  • Consumer check: hal-ti's validation firmware (embedded-pro/hal-ti branch claude/hil-on-emil-services, pinned to this PR's head) builds from a clean FetchContent for TM4C123 and TM4C129.
    • The firmware shrinks from 5.1k to about 2.9k lines.
    • The wire protocol is unchanged, except that eeprom.read now answers as a deferred line.

Notes

  • Size: on targets where the consumer's build type leaves optimisation off, build these libraries size-optimised; docs/Hil.md says so.
  • Follow-up, not in this PR: hal::cortex::Reset::ResetModule writes AIRCR with no __DSB() and then returns, unlike NVIC_SystemReset.

🤖 Generated with Claude Code

https://claude.ai/code/session_014WbtsfrCF7vqzVkHsKYMoX

…lidation

services.hil provides a line-based OK/ERR/EVT command terminal
(HilTerminal, Response, Arguments, PinNaming, PinPool, SystemCommands);
services.hil.commands adds per-peripheral command groups written against
hal:: interfaces, with HAL-supplied factories for opening drivers.
TerminalWithCommandsImplBase gains command start/end and
unrecognized-command hooks; CanMock, Ethernet mocks and
SynchronousAdcMock are added to the test doubles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WbtsfrCF7vqzVkHsKYMoX
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

✅⚠️MegaLinter analysis: Success with warnings

Descriptor Linter Files Fixed Errors Max errors Warnings Elapsed time
✅ ACTION actionlint 5 0 0 0.11s
✅ ACTION zizmor 5 0 0 0 2.87s
✅ CPP clang-format 1317 10 0 0 9.57s
✅ DOCKERFILE hadolint 1 0 0 0.04s
✅ JSON jsonlint 8 0 0 0.1s
✅ JSON prettier 8 7 0 0 0.43s
⚠️ MARKDOWN markdownlint 30 7 52 0 2.15s
✅ MARKDOWN markdown-table-formatter 30 7 0 0 0.36s
⚠️ REPOSITORY betterleaks yes 1 4 1.19s
✅ REPOSITORY checkov yes no no 36.95s
✅ REPOSITORY git_diff yes no no 0.1s
✅ REPOSITORY grype yes no no 113.56s
✅ REPOSITORY ls-lint yes no no 0.02s
✅ REPOSITORY secretlint yes no no 68.61s
✅ REPOSITORY syft yes no no 1.7s
✅ REPOSITORY trivy yes no no 14.77s
✅ REPOSITORY trivy-sbom yes no no 0.32s
✅ REPOSITORY trufflehog yes no no 4.27s
⚠️ SPELL lychee 215 2 0 97.98s
✅ YAML prettier 8 8 0 0 0.55s
✅ YAML v8r 8 0 0 6.62s
✅ YAML yamllint 8 0 0 0.55s

Detailed Issues

⚠️ REPOSITORY / betterleaks - 1 error
warning: private-key has detected secret for file services/network/tls/test_doubles/Certificates.cpp.
   ┌─ services/network/tls/test_doubles/Certificates.cpp:56:15
   │  
56 │               "HIDDEN_BY_MEGALINTER\r\n";
   │ ╰──────────────────────────────────────────^

warning: private-key has detected secret for file services/network/tls/test_doubles/Certificates.cpp.
    ┌─ services/network/tls/test_doubles/Certificates.cpp:108:15
    │  
108 │               "HIDDEN_BY_MEGALINTER\r\n";
    │ ╰──────────────────────────────────────────^

warning: private-key has detected secret for file services/network/tls/CertificatesMbedTls.cpp.
    ┌─ services/network/tls/CertificatesMbedTls.cpp:125:21
    │  
125 │           stream << "HIDDEN_BY_MEGALINTER\r\n";
    │ ╰────────────────────────────────────────────────^

warning: generic-api-key has detected secret for file services/network/websocket/WebSocket.cpp.
   ┌─ services/network/websocket/WebSocket.cpp:81:64
   │
81 │         headers.push_back(services::HttpHeader("Sec-Websocket-Key", "AQIDBAUGBbgJCgsMDQ4PEC=="));
   │                                                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

warning: 4 warnings emitted
⚠️ SPELL / lychee - 2 errors
📝 Summary
---------------------
🔍 Total..........489
🔗 Unique.........448
✅ Successful.....487
⏳ Timeouts.........0
🔀 Redirected.....188
👻 Excluded.........0
❓ Unknown..........0
🚫 Errors...........2
⛔ Unsupported......2

Errors in docs/index.md
[404] https://github.com/embedded-pro/embeddedinfralib/issues/37 (at 41:167) | Rejected status code: 404 Not Found

Errors in external/protoc/CMakeLists.txt
[404] https://github.com/protocolbuffers/protobuf/releases/download/v$%7Bprotobuf_tag%7D/protoc-$%7Bprotobuf_version%7D-$%7Bos_postfix%7D.zip (at 18:13) | Rejected status code: 404 Not Found

Hint: Followed 188 redirects. You might want to consider replacing redirecting URLs with the resolved URLs. Use verbose mode (`-v`/`-vv`) to see redirection details.
Hint: You can configure accepted/rejected response codes with `-a` or `--accept`
⚠️ MARKDOWN / markdownlint - 52 errors
.claude/agents/executor.md:7 error MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "You are the executor agent for..."]
.claude/agents/orchestrator.md:7 error MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "You are the orchestrator agent..."]
.claude/agents/planner.md:7 error MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "You are the planner agent for ..."]
.claude/agents/reviewer.md:7 error MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "You are the reviewer agent for..."]
.github/agents/executor.agent.md:11 error MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "You are the executor agent for..."]
.github/agents/executor.agent.md:108:401 error MD013/line-length Line length [Expected: 400; Actual: 416]
.github/agents/orchestrator.agent.md:18 error MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "You are the orchestrator agent..."]
.github/agents/planner.agent.md:11 error MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "You are the planner agent for ..."]
.github/agents/planner.agent.md:89:401 error MD013/line-length Line length [Expected: 400; Actual: 609]
.github/agents/reviewer.agent.md:14 error MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "You are the reviewer agent for..."]
.github/agents/reviewer.agent.md:116:401 error MD013/line-length Line length [Expected: 400; Actual: 514]
.github/prompts/orchestrate.prompt.md:8 error MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "Analyze the following task for..."]
docs/Ble.md:156:401 error MD013/line-length Line length [Expected: 400; Actual: 545]
docs/CodingStandard.md:98:401 error MD013/line-length Line length [Expected: 400; Actual: 457]
docs/CodingStandard.md:481:401 error MD013/line-length Line length [Expected: 400; Actual: 670]
docs/Containers.md:5:401 error MD013/line-length Line length [Expected: 400; Actual: 537]
docs/Containers.md:11:401 error MD013/line-length Line length [Expected: 400; Actual: 566]
docs/Containers.md:24:401 error MD013/line-length Line length [Expected: 400; Actual: 454]
docs/Containers.md:46:401 error MD013/line-length Line length [Expected: 400; Actual: 812]
docs/Containers.md:86:401 error MD013/line-length Line length [Expected: 400; Actual: 557]
docs/Echo.md:5:401 error MD013/line-length Line length [Expected: 400; Actual: 508]
docs/Echo.md:7:401 error MD013/line-length Line length [Expected: 400; Actual: 416]
docs/Echo.md:38:401 error MD013/line-length Line length [Expected: 400; Actual: 511]
docs/Echo.md:49:401 error MD013/line-length Line length [Expected: 400; Actual: 476]
docs/Echo.md:53:401 error MD013/line-length Line length [Expected: 400; Actual: 554]
docs/Echo.md:59 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/Echo.md:65 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/Echo.md:73:401 error MD013/line-length Line length [Expected: 400; Actual: 887]
docs/ExecutionModel.md:7:401 error MD013/line-length Line length [Expected: 400; Actual: 632]
docs/ExecutionModel.md:9:401 error MD013/line-length Line length [Expected: 400; Actual: 834]
docs/ExecutionModel.md:11:401 error MD013/line-length Line length [Expected: 400; Actual: 642]
docs/ExecutionModel.md:15:401 error MD013/line-length Line length [Expected: 400; Actual: 812]
docs/ExecutionModel.md:19:401 error MD013/line-length Line length [Expected: 400; Actual: 456]
docs/ExecutionModel.md:27:401 error MD013/line-length Line length [Expected: 400; Actual: 724]
docs/index.md:5:401 error MD013/line-length Line length [Expected: 400; Actual: 570]
docs/index.md:9 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/MemoryRange.md:3:401 error MD013/line-length Line length [Expected: 400; Actual: 862]
docs/MemoryRange.md:5:401 error MD013/line-length Line length [Expected: 400; Actual: 441]
docs/NetworkConnections.md:5:401 error MD013/line-length Line length [Expected: 400; Actual: 439]
docs/NetworkConnections.md:9:401 error MD013/line-length Line length [Expected: 400; Actual: 496]
docs/Sesame.md:9 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/Sesame.md:25:401 error MD013/line-length Line length [Expected: 400; Actual: 445]
docs/Sesame.md:29 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/Sesame.md:41 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/Sesame.md:58 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/Sesame.md:73 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/Sesame.md:91 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/Sesame.md:120 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
external/crypto/tiny-aes128/README.md:1 error MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "### Tiny AES128 in C"]
external/crypto/tiny-aes128/README.md:29 error MD046/code-block-style Code block style [Expected: fenced; Actual: indented]
external/crypto/tiny-aes128/README.md:39 error MD046/code-block-style Code block style [Expected: fenced; Actual: indented]
external/crypto/tiny-aes128/README.md:49 error MD046/code-block-style Code block style [Expected: fenced; Actual: indented]

Notices

⚠️ Your configuration references items that have been removed from MegaLinter and are ignored: REPOSITORY_GITLEAKS, REPOSITORY_KICS. See Removed linters to find their replacements.

See detailed reports in MegaLinter artifacts

Your project could benefit from a custom flavor, which would allow you to run only the linters you need, and thus improve runtime performances. (Skip this info by defining FLAVOR_SUGGESTIONS: false)

  • Documentation: Custom Flavors
  • Command: npx mega-linter-runner@10.0.0 --custom-flavor-setup --custom-flavor-linters ACTION_ACTIONLINT,ACTION_ZIZMOR,CPP_CLANG_FORMAT,DOCKERFILE_HADOLINT,JSON_JSONLINT,JSON_PRETTIER,MARKDOWN_MARKDOWNLINT,MARKDOWN_MARKDOWN_TABLE_FORMATTER,REPOSITORY_CHECKOV,REPOSITORY_GIT_DIFF,REPOSITORY_BETTERLEAKS,REPOSITORY_GRYPE,REPOSITORY_LS_LINT,REPOSITORY_SECRETLINT,REPOSITORY_SYFT,REPOSITORY_TRIVY,REPOSITORY_TRIVY_SBOM,REPOSITORY_TRUFFLEHOG,SPELL_LYCHEE,YAML_PRETTIER,YAML_YAMLLINT,YAML_V8R

MegaLinter is provided by OX Security
Show us your support by starring ⭐ the repository

…efix

Types move from services::hil to services and carry a Hil prefix
(HilResponse, HilArguments, HilGpioCommands, HilUartFactory, ...);
files are renamed to match. Also addresses SonarCloud findings: default
memory order on atomics, explicit optional has_value checks, no side
effects in logical operands, no strlen, const member functions, ranges
algorithms, and explicit narrowing in TerminalBase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WbtsfrCF7vqzVkHsKYMoX
- HilInstanceFactory and HilSingleInstanceGroup share the open/close
  lifecycle of the single-instance groups
- HilPendingOperation replaces four copies of busy/timeout/generation
  handling (uart send, spi transfer, can send, eeprom)
- reuse infra::AtomicTriggerScheduler (watchdog early warning) and
  infra::TimerLimitedRepeating (gpio pulse)
- HilEdge/HilEdgeCounter shared by gpio and comparator
- hal::DutyCycle::FromRatio, used by FromPercent and duty parsing
- uart receive uses ContiguousRange/Consume; can uses the BoundedVector
  range constructor
- SynchronousPwmMock gets its own header guard and a real base
- link docs/Hil.md from the README

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WbtsfrCF7vqzVkHsKYMoX
Subclasses reach the context, instance and pin owner through protected
accessors; uart queue consumption narrows explicitly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WbtsfrCF7vqzVkHsKYMoX
@sonarqubecloud

Copy link
Copy Markdown

@gabrielfrasantos
gabrielfrasantos merged commit dd77048 into main Sep 28, 2026
26 checks passed
@gabrielfrasantos
gabrielfrasantos deleted the claude/hil-services branch September 28, 2026 10:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants