Arduino control software for the N2 nitrogen generator project.
This repository contains the current controller-oriented sketch for the N2 system.
Current sketch:
N2V5.ino
Primary controller components:
TimedStateMachineTowerControllerO2ControllerN2Controller
Support components:
SystemConfigSystemContextSystemRuntimeSystemSnapshotArduinoDigitalOutput- board/profile-specific
SystemProfile_*
The code is structured so that the controller logic is host-testable, while hardware-specific behavior is kept in the sketch and the system profile files.
At a high level, the sketch:
- Reads the operator enable switch ("black switch") and pressure inputs.
- Maintains a tower-valve timing sequence for the PSA towers.
- Samples the oxygen sensor on its own timed cycle and derives nitrogen percentage as
100 - O2. - Uses low-side and high-side pressure hysteresis to decide when the compressor SSR should run.
- Updates the 4-digit display and 20x4 LCD.
- Supports a WiFi-board scenario mode that drives the whole system from scripted inputs instead of live hardware.
Files of interest:
N2V5.ino- main Arduino sketchsrc/TowerController.*- tower valve controllersrc/O2Controller.*- oxygen measurement controllersrc/N2Controller.*- compressor / nitrogen pressure controllersrc/TimedStateMachine.*- shared timed-state helpersrc/SystemProfile_minima.*- live hardware profile for Uno R4 Minimasrc/SystemProfile_wifi_scenario.*- scripted scenario profile for Uno R4 WiFihost_tests/- host-side testsMakefile- host test / coverage entry points
The current sketch uses several separate I2C buses:
- 4-digit LED display on a dedicated bit-banged bus
- O2 sensor on a dedicated bit-banged bus
- 20x4 LCD on a dedicated bit-banged bus
- RTC on a dedicated bit-banged bus
- rotary switch through the standard
Wirebus
Pressure inputs are read through analog pins and converted into fixed-point engineering units:
*_x10means tenths*_x100means hundredths
Examples:
supplyPsi_x10 = 1200means 120.0 PSIlowN2Psi_x100 = 2500means 25.00 PSI
This repo currently supports two board-profile modes.
Selected when ARDUINO_MINIMA is defined.
This is the live hardware profile:
- real
millis()clock - real oxygen sensor implementation (
TCP0465) - real analog pressure reads
- real displays
- full-duration timing values intended for hardware operation
Selected when ARDUINO_UNOWIFIR4 is defined.
This is a scripted scenario profile:
- synthetic clock
- synthetic pressure inputs
- synthetic O2 sensor values
- serial-driven scenario stepping
- shortened controller timings for fast interactive testing
See WiFi scenario mode below.
The main loop is intentionally simple:
- Refresh inputs.
- On Minima, this means reading the black switch and the pressure sensors.
- On WiFi scenario mode, this means applying the current scripted scenario step.
- Apply black-switch power gating.
- If disabled:
- shut off displays
- force compressor output off
- force O2 flush valve off
- disable the tower controller
- return early
- If enabled:
- step
O2Controller - enable and step
TowerController - step
N2Controller - refresh a consolidated system snapshot
- update the displays
- step
The controllers are therefore independent state machines that all consume the same InputSnapshot, but each owns its own internal timing and outputs.
The black switch is the operator enable gate for the running system.
When the black switch is off, the sketch:
- disables the 4-digit display
- disables the 20x4 LCD
- disables the tower controller
- turns the compressor SSR output off
- turns the O2 flush valve output off
So black-switch off/on is currently an output-gating and loop-gating action, not a full controller reinitialization.
That means, for example:
- tower sequencing is explicitly restarted because
TowerController::setEnabled(false/true)is used - O2 warmup is not restarted by a black-switch off/on toggle
- N2 hysteresis state is not explicitly reset by the black switch
TowerController manages the two tower valves with a timed alternating sequence.
Its job is to alternate flow between left and right towers with a short overlap interval during switchover, while also refusing to run when the supply pressure is too low.
The current state set is:
InactiveLeftOnlyBothAfterLeftRightOnlyBothAfterRightLowSupply
When enabled and supply pressure is sufficient, the tower controller cycles like this:
LeftOnlyBothAfterLeftRightOnlyBothAfterRight- back to
LeftOnly
This gives a timed repeating left/both/right/both pattern.
Valve outputs by state:
Inactive-> both valves offLowSupply-> both valves offLeftOnly-> left on, right offBothAfterLeft-> left on, right onRightOnly-> left off, right onBothAfterRight-> left on, right on
The controller checks supplyPsi_x10 against lowSupplyPsi_x10.
Default threshold:
- 900 (
90.0 PSI)
Behavior:
- if supply drops below threshold while enabled, the controller enters
LowSupply - in
LowSupply, both tower valves are off - when supply recovers, the controller restarts at
LeftOnly
Minima hardware profile defaults:
leftOpenMs = 60000overlapMs = 750rightOpenMs = 60000
WiFi scenario profile defaults:
leftOpenMs = 300overlapMs = 100rightOpenMs = 300
O2Controller manages the oxygen sensor measurement cycle and the O2/N2 display value.
Its job is to:
- initialize the oxygen sensor
- enforce warmup time
- flush the sensor line
- wait for settling
- collect multiple O2 samples
- average them
- cache the result
- mark the result fresh or stale by age
- back off briefly if a read fails
The main sketch displays nitrogen percentage as:
N2 % = 100.00 - O2 %
The current state set is:
UninitializedWarmupWaitingToFlushFlushingSettlingSamplingWaitingForNextSampleErrorBackoff
During init() the controller:
- forces flush valve off
- validates
sampleCount > 0 - calls
sensor.begin() - clears cached measurement state
- enters
WarmupforwarmupDurationMs
On the Minima hardware profile, default warmup is:
300000 ms(5 minutes)
On the WiFi scenario profile, default warmup is shortened to:
100 ms
After warmup, the controller waits in WaitingToFlush.
A measurement cycle then proceeds as:
Flushing- flush valve on
- wait
flushDurationMs
Settling- flush valve off
- wait
settleDurationMs
Sampling- read one O2 sample
- accumulate running sum
WaitingForNextSample- wait
sampleIntervalMs
- wait
- repeat sampling until
sampleCountsamples are collected - compute average O2 percentage
- cache the result
- record completion time
- return to
WaitingToFlush
If a sensor read fails during sampling:
- flush valve is turned off
- the controller stores the sensor error string
- the controller enters
ErrorBackoff - after
errorBackoffMs, it returns toWaitingToFlush
A cached reading is considered fresh only for a bounded time.
Minima hardware defaults:
measurementIntervalMs = 60000freshnessThresholdMs = 15000flushDurationMs = 3000settleDurationMs = 2000sampleIntervalMs = 250sampleCount = 10errorBackoffMs = 1000
WiFi scenario defaults are shortened:
measurementIntervalMs = 500freshnessThresholdMs = 250flushDurationMs = 50settleDurationMs = 50sampleIntervalMs = 50sampleCount = 2errorBackoffMs = 100
If no O2 value has been completed yet:
- the cached value is absent
- the sketch displays nitrogen as
0.00
So during warmup or before the first successful measurement, the current display behavior is effectively a zero nitrogen reading rather than a placeholder string.
O2Controller warmup starts during setup() when o2Controller.init() is called.
The black switch does not currently restart that warmup cycle, because:
- the controller is not shut down when the switch turns off
- the controller is simply not stepped while the system is disabled
When the system is re-enabled, the controller resumes from its existing timed state.
N2Controller decides whether the compressor SSR should be on, based on low-side and high-side nitrogen pressure hysteresis.
Its job is to protect against compressor chatter and to enforce a two-latch hysteresis policy:
- a low-pressure permit latch
- a high-pressure permit latch
The compressor runs only when both latches permit it.
The current state set is:
LowInhibitHighPermitLowPermitHighPermitLowPermitHighInhibitLowInhibitHighInhibit
These names reflect the two internal permits directly.
The controller starts in:
LowInhibitHighPermit
Each loop it updates two boolean latches:
- low-pressure permit
- high-pressure permit
Then it maps those latches to one of the four states.
Inputs:
lowN2Psi_x100
Default thresholds:
lowOffPsi_x100 = 1000(10.00 PSI)lowOnPsi_x100 = 2000(20.00 PSI)
Behavior:
- below
10.00 PSI, low permit is forced off - above
20.00 PSI, low permit is forced on - between thresholds, the previous low permit state is retained
Inputs:
highN2Psi_x10
Default thresholds:
highOnPsi_x10 = 1000(100.0 PSI)highOffPsi_x10 = 1200(120.0 PSI)
Behavior:
- above
120.0 PSI, high permit is forced off - below
100.0 PSI, high permit is forced on - between thresholds, the previous high permit state is retained
The compressor SSR is on only in:
LowPermitHighPermit
In all other states, the compressor SSR is off.
Useful interpretation of the four states:
LowInhibitHighPermit- low side says "do not run"
- high side says "allowed if low side later permits"
LowPermitHighPermit- both sides permit
- compressor on
LowPermitHighInhibit- low side wants more pressure
- high side says stop
- compressor off
LowInhibitHighInhibit- neither side permits
- compressor off
As with O2Controller, the main sketch does not call n2Controller.shutdown() when the black switch turns off.
So:
- the compressor output is forced off while disabled
- the controller object itself is not reinitialized by that event
The 4-digit display shows one selected value at a time.
On Minima hardware, selection comes from the rotary switch.
Selectable values are:
- off
- supply pressure
- left tower pressure
- right tower pressure
- low-side N2 pressure
- nitrogen percent
Decimal-point placement is controlled in software:
- tenths for
*_x10values - hundredths for
*_x100values
The 20x4 LCD shows a live multi-line summary of the current snapshot.
Current format is:
- line 0: air supply PSI
- line 1: left/right tower PSI
- line 2: low-side N2 PSI and high-side N2 PSI
- line 3: nitrogen percent
If the black switch is off, both displays are disabled.
The Uno R4 WiFi profile is not the live hardware mode. It is a scripted controller-exercise mode.
It exists to let you:
- exercise controller sequencing quickly
- inspect serial output
- test state transitions without live sensors
- use a shortened timing model for fast manual stepping
Instead of reading live hardware inputs, the WiFi scenario profile applies predefined scenario steps.
Each step sets:
- current simulated time
- black switch state
- supply pressure
- left tower pressure
- right tower pressure
- low-side N2 pressure
- high-side N2 pressure
- whether O2 data is valid
- O2 percent value
The profile also supplies:
- a synthetic
ProfileClock - a synthetic
ProfileO2Sensor
The current scenario data is hard-coded in SystemProfile_wifi_scenario.cpp.
As each step is advanced:
- the profile updates the synthetic time
- the profile updates the synthetic inputs
- the sketch runs the normal controller loop against those values
- serial output prints a scenario banner with current inputs and controller states
In WiFi scenario mode:
Enter-> advance to the next scenario stepr+Enter-> reset the scenario?+Enter-> print help / status
At the end of the scenario:
- the scenario is marked done
- the black switch is forced off
In WiFi scenario mode, the sketch pretends the rotary switch is set to nitrogen percent, so the 4-digit display always presents the nitrogen-percentage view instead of polling the physical rotary switch.
The WiFi scenario profile intentionally shortens controller timings so that:
- warmup completes quickly
- flush/settle/sample phases are easy to observe
- tower alternation is easy to step through interactively
This mode is for controller bring-up and demonstration, not for matching full-duration hardware timing.
The repository includes host-side tests under host_tests/.
The Makefile currently provides:
make host-testmake coveragemake clean
The host build compiles the common controller sources and excludes the board-specific SystemProfile_*.cpp files.
That keeps the controller logic testable without the Arduino runtime.
The sketch currently includes or depends on these libraries/components:
- Arduino core
WireBitBang_I2CTCP1650TCP20x4TCP0465TCP3231
This repo is in active controller-centric development.
The current codebase already has:
- separated controller classes
- a shared timed-state abstraction
- host-testable logic
- two board/profile modes
- multiple dedicated I2C buses
- serial-driven WiFi scenario support
MIT