15  Managing Libraries and Version Control

Choose dependencies as reviewable evidence, then lock, test, and hand them off without turning prototype code into an untraceable build

prototyping
firmware
dependencies
version-control
embedded-software
Keywords

IoT library management, firmware dependency record, prototype version control, dependency pinning, build reproducibility

In 60 Seconds

Libraries save time only when they make a prototype easier to review. A useful IoT dependency choice records what problem the library solves, why it fits the target, how much behavior it owns, how the version is locked, what was tested, and what must be checked again before the prototype becomes production code.

15.1 Start With the Story

A library can make a prototype look finished in an afternoon. It can also hide timeouts, stale readings, retry loops, memory pressure, credential handling, update behavior, or a license problem that matters later. The story is not “install a package”; it is “prove that this dependency exposes the behavior the project must trust.”

This chapter treats libraries as evidence boundaries. Accept a dependency only after its role, version, failure modes, configuration, and replacement cost are visible in the prototype record.

15.2 Dependencies Shift Responsibility

Adding a library is an architecture decision. The prototype gains speed, but some behavior moves from code the team owns into code the team must understand, configure, test, and update.

Route from prototype question through dependency candidate, version record, integration run, fault run, dependency decision, handoff, and update plan.
A dependency choice should move from the prototype question through candidate review, version record, evidence runs, handoff decision, and update plan.

The review question is not whether a dependency is popular. It is whether the dependency makes the target behavior clearer, repeatable, and safer to hand off.

For a cold-room monitor, a SHT31 driver may own I2C transactions and unit conversion, PubSubClient or AsyncMqttClient may own MQTT connection details, ArduinoJson or nanopb may own payload encoding, LittleFS or NVS may own retained records, and a display helper may own rendering primitives. Each choice saves implementation time, but each also moves timing, memory allocation, error reporting, retry behavior, and version risk into code outside the application boundary.

The dependency record should therefore say what the library proves and what it does not prove. A driver that returns clean readings on one bench board does not prove bus timeout behavior. A gateway client that publishes once does not prove reconnect backoff, queue pressure, TLS configuration, or broker permission failure. A storage helper that writes a file does not prove full-storage, corrupt-record, migration, or power-loss behavior.

Make the boundary visible before the dependency spreads. If the MQTT client owns reconnect timing, the application should still own stale-state policy and local alarm behavior. If ArduinoJson owns parsing, the application should still own required fields, units, schema version, and rejected payload states. If a LoRaWAN stack owns join and uplink scheduling, the prototype should still record the duty-cycle, queue, and payload decisions that the product depends on.

Good dependency practice is not hostility to libraries. It is controlled trust. Pin the package source, version, board support package, compiler or runtime, configuration flags, and build command. Then run the normal path and the one fault path that matters to the current prototype. If that evidence is clear, the dependency can move forward with a known owner and a known trigger for checking it again.

15.3 Record Contract Before Import

Before adding a sensor, MQTT, storage, display, or OTA helper library, write down the contract the prototype expects from it.

Start with the role. A sensor library might be acceptable if it exposes status codes, lets the application set timeouts, and does not hide stale readings. A messaging library needs a visible connection state, bounded publish call, retry policy, queue limit, and TLS or credential configuration. A storage library needs an explicit policy for full flash, corrupt records, write interruption, and schema changes. An OTA library needs package verification, compatibility check, health confirmation, rollback behavior, and a record of which image slot is active.

Then prove the contract in the smallest useful run. For Arduino or PlatformIO firmware, record platformio.ini, board package version, lib_deps, compiler output, binary size, and the serial log for one normal and one fault case. For ESP-IDF or Zephyr, record component revision, Kconfig or devicetree options, build target, partition table, stack or heap evidence, and the command that flashed the board. For gateway code, record the npm, pip, Go module, Cargo, or Docker lockfile plus the local broker, SQLite queue, systemd unit, or container configuration used during the run.

  • Owned behavior: note whether the library owns timing, retries, parsing, buffering, memory allocation, transport security, or error handling.
  • Version lock: record package source, exact version, configuration, board profile, compiler/runtime, and build command.
  • Fault proof: test the failure that matters, such as missing sensor, malformed packet, reconnect storm, full storage, or rejected update.
  • Update risk: name what must be checked again if the dependency, board package, compiler, or transitive library changes.

Keep rejected options useful. If a heavier MQTT client is rejected because it blocks local alarm handling during reconnect, record that reason. If a storage helper is postponed because its migration behavior is unclear, record the simpler queue that will be used for the current prototype. Those notes prevent the same debate from restarting when the project moves from bench evidence to pilot handoff.

15.4 Version Control as Evidence

A prototype cannot be reviewed if the build cannot be recreated. Source code, dependency versions, board support packages, generated artifacts, configuration examples, and test outputs need clear boundaries in the repository.

Small commits make dependency behavior visible. A commit that only adds a library and its evidence is easy to review. A commit that changes architecture, pins a dependency, rewrites tests, and edits hardware wiring at the same time hides which change created the new behavior.

Version control should capture both code and the conditions that made the evidence true. For PlatformIO, that means the board environment, framework, library versions, and any lock or manifest file. For Zephyr, it includes west manifest revisions, module versions, Kconfig selections, overlays, and generated configuration summaries. For ESP-IDF, it includes component revisions, sdkconfig, partition layout, and toolchain version. For Node.js, Python, Go, or Rust gateway code, the lockfile and runtime version are part of the dependency evidence.

Generated files need an explicit policy. Protocol Buffer output, OpenAPI clients, firmware images, map files, coverage reports, and built app bundles can help review, but they should not be mixed into source commits unless the repository policy says they are source-of-truth artifacts. If generated output is ignored, record the generator version and command. If generated output is committed, keep it in the same change as the schema or generator input that produced it.

The under-the-hood risk is drift. A transitive update can change TLS defaults, MQTT keepalive behavior, JSON parsing, flash write timing, memory footprint, or exception handling without changing application source. A reviewer should be able to compare the lockfile, manifest, board package, and evidence record and know exactly which dependency changed before deciding whether to repeat the normal run, gateway outage run, full queue run, or update rollback run.

15.5 Learning Objectives

By the end of this chapter, you will be able to:

  • Choose libraries from evidence needs rather than popularity or habit.
  • Review target support, resource cost, API fit, update risk, and testability before adding a dependency.
  • Record exact library versions, sources, configuration, build commands, and evidence runs.
  • Structure a firmware repository so source, tests, hardware notes, configuration examples, and generated artifacts are easy to separate.
  • Use source control to keep dependency changes small, reviewable, and recoverable.

15.6 Prerequisites

This chapter builds on:


15.7 Dependency Choice Is Evidence Choice

A software prototype often starts by adding a sensor driver, communication client, display library, storage helper, or cloud connector. That shortcut is useful only if the team can still explain what the dependency owns and what evidence proves it behaves well enough for the prototype.

Start with the dependency question:

Problem fitWhat exact prototype problem does the library solve: sensor access, packet parsing, retry behavior, local storage, display output, or test harness support?
Ownership boundaryWhich behavior moves into the library, and which behavior remains in your application code?
Target fitDoes the library support the selected board, runtime, compiler, bus, network path, memory budget, and build environment?
Evidence fitCan you run a normal case, a failure case, and a repeatable build that another reviewer can reproduce?
Avoid Invisible Complexity

A library can hide bus timing, memory allocation, retry loops, blocking calls, and default configuration. Add the dependency only when the hidden behavior is acceptable or made visible through a record, log, test, or boundary.

15.8 Library Selection Gates

Do not evaluate a dependency as a single yes/no decision. Review it through gates that match the prototype risk. A small helper can pass quickly; a networking, storage, update, or security-sensitive dependency needs a stronger record.

Software library selection gates covering problem fit, API fit, target support, measured resource cost, failure visibility, maintenance signal, testability, ownership boundary, and handoff decision.
Figure 15.1: Software library selection gates

Use these gates before a dependency becomes part of the prototype baseline:

API fitThe library expresses the job clearly and does not force application code into confusing workarounds.
Target supportThe library builds with the selected environment and works on the intended board, runtime, bus, and communication path.
Resource costThe prototype records observed build size, memory pressure, timing behavior, or other relevant constraints instead of relying on guesses.
Failure visibilityThe library exposes missing device, timeout, reconnect, parse failure, full queue, corrupt data, or retry behavior that matters to the chapter scenario.
Maintenance signalThe project has enough documentation, release history, issue activity, or ownership clarity for the prototype risk level.
TestabilityThe dependency can be exercised through a board test, host harness, mock boundary, or recorded integration run.
Make the Rejection Useful

If a library is rejected, record why. A rejected option can still be useful evidence when a later reviewer asks why the prototype chose a smaller, slower, simpler, or less familiar dependency.

15.9 Define the Reuse Boundary

The safest dependency is not always the smallest one. The safer choice is the dependency whose boundary is understandable. A sensor library may own bus reads and calibration conversion, but the application should still own stale-data policy. A communication library may own connection details, but the application should still own payload meaning and offline behavior.

Driver dependencyOwns hardware protocol details. Record initialization, read status, error return behavior, and bus conflict assumptions.
Protocol dependencyOwns message transport. Record connection state, retry behavior, timeout behavior, queue limits, and payload ownership.
Storage dependencyOwns file, flash, database, or key-value operations. Record write limits, full-storage behavior, corruption handling, and migration expectation.
Display or UI dependencyOwns rendering primitives. Record refresh timing, blocking behavior, memory use, and what happens when the display is absent.
Test dependencyOwns fixtures, mocks, assertions, or simulation support. Record what it proves and what still requires target hardware.
Generated dependencyOwns code produced by a tool. Record generator version, source input, output policy, and whether generated files are committed.

15.10 Version and Build Records

Version control does not make a build reproducible by itself. A reproducible prototype also needs dependency versions, source locations, lock files or equivalent records, configuration, and the command used to build or test the result.

VS Code IDE with PlatformIO, version control with Git, deployment to ESP32/STM32 via serial or OTA.
Figure 15.2: Modern IoT Development Stack

A dependency record should answer these questions:

dependency_name=
dependency_role=
source=
version_or_commit=
lock_or_manifest_file=
target_environment=
configuration_options=
build_command=
normal_run_evidence=
fault_run_evidence=
known_limits=
update_rule=
review_owner=
review_date=

For early experiments, a loose version can be acceptable if the record says it is temporary. Before a prototype is handed to another team, used in a pilot, or used as the basis for production work, the dependency set should be locked tightly enough that another reviewer can rebuild the same result.

What to Lock

Lock the dependency source, version, board or target profile, toolchain version, configuration flags, generated-code source, and the command used for the evidence run. If the project uses a package manager with a lock file, keep that lock file under review with the source.

15.11 Firmware Evidence Repository

A repository should help reviewers separate source code, dependency configuration, tests, hardware notes, generated output, and secrets. The exact folders vary by project, but the purpose should be clear.

Firmware repository structure separating source, reusable libraries, tests, hardware notes, docs, configuration examples, build artifacts, and secrets outside the repository.
Figure 15.3: Firmware repository structure

One reviewable layout is:

prototype-firmware/
  firmware/
    src/
    lib/
    test/
    dependency-manifest
    dependency-lock
  hardware/
    wiring-notes/
    board-revisions/
  docs/
    evidence-records/
    runbooks/
  config/
    example.env
    example-device-config.json
  scripts/
    build
    test
    capture-evidence
  .gitignore
  README.md

Keep real credentials, private keys, device-specific tokens, and local machine paths out of version control. Commit examples and instructions instead, so a reviewer knows what is required without receiving a secret.

# Generated build output
build/
dist/
*.bin
*.hex
*.elf

# Local environment and secrets
.env
secrets.*
device-credentials.*

# Local editor and machine files
.vscode/
.idea/
.DS_Store

15.12 Source-Control Workflow

Dependency changes deserve small, reviewable commits because one version bump can affect timing, memory, protocol behavior, and failure recovery. Treat each dependency change like a design decision, not a background cleanup.

Source-control workflow from branch through small dependency change, build, board or harness test, review, tag, and handoff.
Figure 15.4: Source-control review workflow

Use this workflow:

Create a focused branchOne dependency addition, removal, or update should be easy to review without unrelated formatting or feature work.
Update the recordChange the manifest, lock file, configuration, and evidence record in the same review.
Run the right evidenceAt minimum, rebuild and run the dependency’s normal path. For risky dependencies, run a relevant fault path too.
Review behavior, not only compile statusA successful compile does not prove timeout, retry, stale-data, memory, or recovery behavior.
Tag useful baselinesTag or otherwise mark the dependency set used for demos, field trials, pilot runs, or handoff packages.
Record update policyState who will review future updates, what evidence is required, and when unpinned or temporary choices must be revisited.

Good commit messages say what changed and why:

Add sensor-driver dependency record for cold-room probe test
Lock messaging dependency after gateway reconnect fault run
Replace display helper after refresh timing review
Remove unused parser dependency from firmware baseline

15.13 Cold-Room Dependency Review

A team is prototyping a cold-room monitor. The device reads temperature, reports status to a gateway, stores short offline history, and exposes a small local display. The first demo works, but the team needs to decide which dependencies are safe enough to keep for a pilot.

15.13.1 Dependency Questions

The team lists four candidate dependencies:

  • A sensor driver that owns bus reads and unit conversion.
  • A messaging client that owns gateway connection and publish calls.
  • A storage helper that owns local queue persistence.
  • A display library that owns drawing primitives.

The architecture chapter already separated driver, state, communication, storage, display, and observability boundaries. The dependency review now asks whether each external library respects those boundaries or blurs them.

15.13.2 Evidence Runs

The team records one normal run and one failure run for each high-risk dependency:

Sensor driverNormal run shows stable readings. Fault run disconnects the probe and confirms the application receives a visible missing-sensor state.
Messaging clientNormal run publishes status. Fault run blocks the gateway and confirms timeout and retry behavior do not freeze local control.
Storage helperNormal run queues offline samples. Fault run fills the queue and confirms the drop or overwrite policy is explicit.
Display libraryNormal run updates status. Fault run disables the display path and confirms sensing and reporting still work.

15.13.3 Review Decision

The sensor driver and display library are kept because their boundaries are narrow and their failures are visible. The messaging client is kept only after adding an explicit timeout and reconnect record. The storage helper is postponed because the current prototype can use a simpler queue until pilot storage requirements are clearer.

prototype=cold-room-monitor
dependency_question=which libraries are safe enough to keep for pilot evidence?
kept=sensor-driver, messaging-client-with-timeout-record, display-library
postponed=storage-helper
normal_runs=sensor_read, gateway_publish, display_refresh
fault_runs=probe_missing, gateway_blocked, display_unavailable, queue_full
version_policy=lock exact versions for pilot baseline
handoff=testing chapter must repeat fault runs; OTA chapter must review update impact

15.14 Library Handoff Record

Every kept dependency should leave a handoff record. The next reviewer should not have to infer why a library exists or whether a version was accidental.

prototype=
dependency=
role=
owner_boundary=
selected_because=
rejected_alternatives=
version_or_commit=
lock_file=
configuration=
normal_evidence=
fault_evidence=
resource_observations=
known_limits=
update_policy=
keep_rewrite_or_check_again=
next_review_trigger=

15.15 Knowledge Check

Dependency Evidence
Match Record Field to Purpose

Order the Dependency Review

15.16 Common Failure Patterns

Unpinned dependencyThe build changes because a package manager resolves a newer version. Lock or record the version before handoff.
Library owns policy by accidentA helper silently decides retry, stale-data, drop, or reconnect behavior. Move policy into application code or record the behavior explicitly.
Only the happy path was testedThe library works during a demo but blocks, leaks state, or hides errors when hardware or the network fails.
Secrets enter the repositoryCredentials, private keys, or device tokens are committed with source. Keep examples in source control and real secrets outside it.
Generated files lack provenanceGenerated code is committed without the generator version or source input, so reviewers cannot reproduce it.
Version-control noise hides riskA dependency update is mixed with formatting, feature work, or unrelated refactoring. Keep dependency changes small and reviewable.

15.17 Summary

  • A library choice is an evidence decision, not just a convenience.
  • Review API fit, target support, resource observations, failure visibility, maintenance signal, and testability before adding a dependency to the baseline.
  • Lock or record source, version, configuration, build command, normal evidence, and fault evidence.
  • Keep secrets and generated output policies clear in the repository structure.
  • Use small source-control branches and explicit handoff records so future testing, OTA, and production work can trust the prototype baseline.

15.18 Key Takeaway

Libraries accelerate prototypes only when their licenses, maintenance, footprint, security posture, and failure behavior are understood.

15.19 What’s Next

Turn evidence into testsTesting & Debugging converts dependency records into repeatable checks.
Review update impactOver-the-Air Updates checks how locked dependencies affect field updates and rollback.
Keep firmware maintainableSoftware Best Practices connects dependency records to code quality and long-term maintenance.
Revisit architectureChoosing Architecture Patterns for IoT Software Prototypes defines the boundaries dependencies must respect.
Check environment fitSoftware Development Environments records toolchain and build context.
Check language fitChoosing Languages for IoT Software Prototypes records runtime and mixed-language constraints.