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
IoT library management, firmware dependency record, prototype version control, dependency pinning, build reproducibility
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.
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:
- Software Development Environments, where the build, upload, and observation tools are recorded.
- Choosing Languages for IoT Software Prototypes, where runtime constraints and mixed-language boundaries are selected.
- Choosing Architecture Patterns for IoT Software Prototypes, where driver, state, communication, storage, and update responsibilities are separated.
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:
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.
Use these gates before a dependency becomes part of the prototype baseline:
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.
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.
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.
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.
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.
Use this workflow:
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:
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
15.16 Common Failure Patterns
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.