Purpose: Document patterns, corrections, and lessons learned during development to improve future interactions.
Before debugging protocol/packet issues:
- Read
.plan/INDEX.md- Master index for protocol documentation (links to controller-specific indexes) - Check equipment-specific protocol files (e.g.,
.plan/201-intellicenter-circuits-features.md,.plan/202-intellicenter-bodies-temps.md) - Review
.plan/203-intellicenter-action-registry.mdfor action code meanings
Protocol documentation is in .plan/, NOT in this file. This file contains coding patterns and lessons learned.
Rule: Do NOT set/get nested state JSON objects directly unless the accessor is explicitly designed to work with an object.
- Most state fields are persisted as JSON, but the public API should be scalar getters/setters (number/string/boolean) that:
- normalize persisted shapes
- apply transforms/valueMaps
- manage
hasChanged/state.dirtycorrectly
- Only use object-level access when there is an explicit object accessor whose contract is “you pass an object” (and it manages diffing/emits).
Examples
- Scalar (preferred):
- Set:
state.status = 0 - Get:
if (state.status === 0) { /* ... */ }
- Set:
- Explicit object accessor (allowed):
- Set:
state.temps.bodies.getItemById(1).heaterOptions = { total: 2, gas: 1, heatpump: 1 } - Get:
const opts = state.temps.bodies.getItemById(1).heaterOptions
- Set:
Rule: NO direct message processing logic in Messages.ts
Messages.tsis a ROUTER only- Delegate to appropriate message files:
- Status messages →
EquipmentStateMessage.ts - Config messages →
ConfigMessage.ts,ExternalMessage.ts, etc.
- Status messages →
Violated in this chat:
- Action 217 handler with logic directly in Messages.ts (lines 792-810)
- Should be in
EquipmentStateMessage.tsor similar
Correction: ✅ FIXED - Action 217 now delegated to EquipmentStateMessage.ts
Rule: NO local class variables for cross-session state
- Ephemeral state (current session only) → Local variables OK
- Persistent state (survives restarts) → Must use:
poolConfig.json(configuration data)poolState.json(runtime state)- With appropriate Equipment/State class methods
Violated in this chat:
_hasRegisteredand_registrationConfirmedas class variables- These need to survive restarts if OCP remembers registration
Correction: ✅ FIXED - Now using state.equipment.registration in poolState.json
Rule: Equipment-related state belongs in EquipmentState, not top-level State
- Follow existing pattern: properties like
controllerType,model,maxCircuitsare inEquipmentState - Equipment-related state →
state.equipment.propertyName - Top-level state is for collections (pumps, circuits, etc.) or system-wide state (temps, heliotrope)
Violated in this chat:
- Initially added
registrationproperty to top-levelStateclass - Should be in
EquipmentStatesince it's controller registration state
Correction: ✅ FIXED - Moved to state.equipment.registration with getter/setter in EquipmentState
Rule: Only use values that come from the actual protocol
- Don't invent intermediate states that aren't in the protocol
- State should reflect what OCP reports, not our internal workflow
- Example: Action 217 byte[2] has values 0, 1, 4 - don't add status=5 for "requested"
Violated in this chat:
- Initially added status=5 for "requested" state
- This value doesn't exist in the protocol, only 0/1/4 come from OCP
Correction: ✅ FIXED - Removed status=5, only track OCP's response (0/1/4)
Rule: Always verify address mappings before implementing
- 15 = Broadcast
- 16 = OCP
- Got this backwards initially, causing confusion
Lesson: When dealing with protocol constants, explicitly verify from working examples first
Rule: Never “rebind” / “re-initialize” the singleton board instance to a local variable (e.g., const board = sys.board as IntelliCenterBoard)
- Boards are initialized once at app startup
- Access the board via
sys.boarddirectly - Persistent config/state should be accessed via
sys(poolConfig.json) andstate(poolState.json), not via cached board references
Violated in this chat:
EquipmentStateMessage.tsusedconst board = sys.board as IntelliCenterBoardinside message handling
Correction: ✅ FIXED - Use sys.board directly without local rebinding
Rule: Do NOT add controller-specific branching (e.g., if (sys.controllerType === ControllerType.IntelliCenter) ..., if (sys.equipment.isIntellicenterV3) ...) inside shared base command classes in controller/boards/SystemBoard.ts.
- Why: The default
byteValueMapsinSystemBoard.tsinclude generic/*Touch-style defaults (e.g.,heatModes) which are wrong for IntelliCenter v3.004+. - Correct pattern: Each controller board overrides the relevant command object (e.g.,
IntelliCenterBoardusesIntelliCenterHeaterCommands extends HeaterCommands) and implements controller-accurate behavior there. - Call-site rule: Always call
sys.board.heaters.updateHeaterServices()(or the appropriatesys.board.<domain>command), and rely on polymorphism to dispatch to the controller-specific implementation.
Bug example (dashPanel heat mode labels):
/config/options/heatersreturnssys.board.valueMaps.heatModes.toArray()- If IntelliCenter-specific heater service mapping isn’t used, the UI can map numeric heatMode values to the wrong names (e.g., mapping
5to “Solar Only” instead of “UltraTemp Only” on IC v3).
Fix pattern:
- Put IntelliCenter mapping in
IntelliCenterHeaterCommands.updateHeaterServices()(already present incontroller/boards/IntelliCenterBoard.ts) - Keep
HeaterCommands.updateHeaterServices()inSystemBoard.tsgeneric for non-IntelliCenter controllers
Same pattern for body picklists:
- Keep
BodyCommands.getHeatSources()/getHeatModes()inSystemBoard.tsgeneric. - Any IntelliCenter v3-specific filtering (e.g., suppressing
solarpref/ultratempprefoptions for body-level mode picklists) must live inIntelliCenterBodyCommandsincontroller/boards/IntelliCenterBoard.ts.
Generalization: This applies to anything that is overridden by controller boards (IntelliCenter, EasyTouch, IntelliTouch, AquaLink, SunTouch, Nixie, etc.).
If you find yourself writing if (sys.controllerType === ...) inside SystemBoard.ts, that is almost always a design smell—create/extend the controller-specific command class in the appropriate board file and keep SystemBoard.ts purely generic.
Rule: For Nixie/REM chemistry relay status sync, treat circuits as the hardware-bound source of truth. Do not implement hardware binding logic on Feature.
Featureis logical/virtual state in this context; relay bindings (connectionId,deviceBinding) belong onCircuit.- When chemistry pump state changes (
NixieChemPump.turnOn/turnOff), locate matching entries insys.circuitsbyconnectionId+deviceBinding. - Reflect state in
state.circuits(includingsetEndTimesemantics) and emit changes; do not bind/sync throughstate.features. - Avoid duplicate relay writes: if the chemistry code already sent the hardware command, sync in-memory circuit state instead of issuing an additional toggle command for that same relay.
- If the UI "Features" panel must show the relay, use a circuit with
showInFeatures=true(presentation concern), not a boundFeature.
Context: Added after investigation/fix for discussion #1159 (acid feeder relay dot not reflecting REM dosing state in dashPanel).
Rule: When debugging, look for WORKING periods in captures
- User insight: "Packet #576 shows byte[2]=1 (REGISTERED)"
- This means njsPC WAS working at that point
- Should analyze: What changed between #576 (working) and #999 (failed)?
- Should check: Are config requests working between #576 and #999?
Violated in this chat:
- Focused on failure at packet #999
- Didn't analyze the working period from #576 to #999
- Missed opportunity to see if config WAS loading successfully
Correction needed: Analyze packets #576-#999 to see if config requests worked
Rule: Identify authoritative data sources in protocol
- User insight: "Action 217 is the source of truth"
- Should have recognized this earlier from captures
Lesson: When multiple messages contain similar data, identify which is authoritative
Rule: When debugging temperature-driven behavior (solar/heater thresholds), do NOT rely on poolState.json for trends.
poolState.jsonrepresents the last/current state snapshot, not a time series.- To understand when thresholds were crossed, extract temps from packet logs (and/or other raw-sample logs that capture temps over time).
- Use the time-series to correlate: AIR/WATER/SOLAR temps ↔ heater/valve commands ↔ triggering events.
Rule: ONE markdown file per topic in .plan/ directory
- Before creating new
.plan/*.mdfile, check if topic already covered - Update existing file rather than creating duplicate
- If splitting needed, cross-reference between files
- Periodically review and delete outdated/superseded files
- When completing work: Update the EXISTING plan file with status, don't create new "_COMPLETE" files
Rule: Never add packet-specific learnings/evidence to AGENTS.md.
AGENTS.mdis for durable, cross-cutting coding/process rules only.- Put packet-level findings, offsets, per-capture discoveries, and protocol examples in
.plan/documentation. - If the packet finding is a tracked defect/regression workflow item, record it in a
/plan-qaissue instead. - If a proposed AGENTS update is packet-specific, STOP and redirect it to
.plan/or/plan-qa.
Rule: For this repository, any issues analyzed/tracked via /plan-qa must be stored in the .plan/ folder, not ./docs/.
- Do not create or update
docs/ISSUES*.mdfor/plan-qaworkflows in this repo. - Keep
/plan-qaissue logs, completed issues, and indexes under.plan/instead. - If an issue was accidentally logged in
./docs/during/plan-qa, stop and move future updates to.plan/.
Violated in this chat (Dec 10, 2025):
- Created
V3_ARCHITECTURE_FIX_COMPLETE.mdimmediately after establishing "one file per topic" rule - Should have updated existing
V3_REGISTRATION_FIX_PLAN.mdinstead - User feedback: "stop creating new .md files in .plan. Consolidate with the latest known info. follow the new rules in agent.md"
Correction: ✅ FIXED - Deleted duplicate file, updated existing plan file with completion status
Hierarchy:
-
Master docs (keep these):
200-intellicenter-v3-index.md- Main implementation guide204-intellicenter-v3-protocol.md- Complete protocol reference / findings summary203-intellicenter-action-registry.md- Action code reference
-
Active work (current tasks):
V3_REGISTRATION_FIX_PLAN.md- Current work plan
-
Key insights (important discoveries):
V3_ACTION_217_DISCOVERY.md- Action 217 as source of truth
-
Reference (technical analysis):
ACTION_204_V1_VS_V3_ANALYSIS.md- Module detection
Delete: Temporary files, superseded analysis, old troubleshooting
Rule: When user provides correction or clarification, ASK:
"Should I add this to AGENTS.md for future reference?"
This creates a feedback loop for continuous improvement.
Rule: When making architectural changes, state assumptions and ask for validation
- Example: "I'm planning to use local variables for this. Is that appropriate?"
- Don't assume patterns - verify first
Rule: Treat dashPanel errors as UI-initiated command failures only.
- dashPanel only shows errors for actions a user initiates from dashPanel (e.g., a “turn on/off” click).
- It will NOT surface retries/timeouts from njsPC background flows (config polling, heartbeats, etc.).
- Therefore: absence of dashPanel errors is not evidence that the underlying protocol traffic is healthy; always confirm via packet logs and njsPC logs.
Rule: For IntelliCenter v3 (v3.004+), the OCP (addr 16) is the only source of truth for state/config changes.
- Do not process packets that are Wireless/ICP/Indoor → OCP (e.g. src=36 dest=16) as if they were authoritative.
- These are requests; the OCP may ACK or ignore them, and the final state can differ.
- Only update state/config from OCP-originated messages (source=16), and preferably from the established authoritative message types:
- Config/state broadcasts/responses (e.g., Action 30 / Action 204 / other status broadcasts)
- NOT from third-party device requests
Implementation note: In Messages.ts, gate IntelliCenter Action 168 handling to ignore non-OCP sources (especially src!=16 dest==16).
Rule: Do NOT adopt, impersonate, or respond on an address that belongs to a real hardware device (for example Wireless/WCP 36), even if the OCP mirrors njsPC's MAC/registration payload onto that address.
- Keep njsPC on its configured/plugin address (for example
33) unless the user explicitly approves a different app address. - Treat
253/217/179traffic for a different live device address as evidence of an unresolved registration/mapping problem, not as success. - Do not answer heartbeat (
180), send registration (251), or request config (228/222) on the mirrored hardware address. - If the OCP associates njsPC's MAC with a real device address, stop treating that as a valid fix path and investigate why the standalone app address is not being accepted.
Rationale: Reusing the same address as real hardware is an anti-pattern on IntelliCenter and can hide the real registration bug while colliding with actual devices (WCP/ICP/other panels).
Rule: Never modify, remove, or contradict existing rules in AGENTS.md without explicit user approval
- When a situation arises that conflicts with an existing rule, STOP and ask the user
- Present: "Existing rule X says Y, but I think Z. Which should I follow?"
- Let user decide whether to update the rule, create an exception, or follow existing rule
- Document the resolution in AGENTS.md
Rationale: Rules in AGENTS.md represent learned patterns from user feedback. Changing them without approval risks losing important context.
Rule: Don't guess protocol behavior - ask user to test
- We can't test with actual hardware
- User has the hardware and can capture real behavior
- Iterate based on real captures, not assumptions
This chat did well: Multiple capture iterations with user feedback
Rule: Protocol understanding evolves through captures
- Initial understanding: "Action 179 is proactive heartbeat" ✗
- Corrected: "Action 179 is request/response from OCP" ✓
- User observation: "Wireless sends Action 180 to OCP, not broadcast"
Lesson: Be ready to revise understanding as new evidence emerges
- Problem: Logic in Messages.ts
- Solution: Moved to EquipmentStateMessage.ts
- Status: Complete
- Problem: Using local class variables
- Solution: Stored in poolState.json via state.equipment.registration
- Status: Complete
- Problem: Two flags when one suffices
- Solution: Single state.equipment.registration status (values: 0/1/4) exposed via accessor (persisted JSON is internal)
- Status: Complete
- Problem: byte[2]=1 (registered) doesn't mean config is loading
- Evidence: Packets #576-#999 showed byte[2]=1, heartbeat working, but ZERO Action 30 responses
- Analysis:
- OCP acknowledged njsPC (Action 179/180 heartbeat working)
- njsPC sent 462 Action 222 config requests
- OCP responded to NONE of them
- Registration alone is not sufficient for config loading
- Question: What else is required beyond registration?
- Priority: CRITICAL (blocks entire feature)
- What triggers config responses beyond registration?
- Why does byte[2]=1 not enable config loading?
- Is there a timing window after registration?
- Does OCP need fresh registration each session (not remember from previous)?
Rule: Before debugging IntelliCenter v3 issues, read:
.plan/INDEX.md- Protocol documentation index (then IntelliCenter:.plan/200-intellicenter-v3-index.md).plan/203-intellicenter-action-registry.md- Action code meanings.plan/204-intellicenter-v3-protocol.md- Protocol findings summary
Rule: The packet header is:
[165, 1, DEST, SRC, ACTION, LEN]
Do NOT grep/read this backwards. DEST comes before SRC in the header, for every packet.
Examples:
- Wireless → OCP command:
DEST=16,SRC=36→ header starts with[165, 1, 16, 36, ...] - OCP → Wireless ACK:
DEST=36,SRC=16→ header starts with[165, 1, 36, 16, ...]
Packet → Messages.ts (router) → Handler Files:
┌─────────────────┐
│ RS-485 Packet │
└────────┬────────┘
▼
┌─────────────────┐
│ Messages.ts │ ← ROUTER ONLY, no business logic!
│ (by action #) │
└────────┬────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ Action 2/204 → EquipmentStateMessage.ts (status) │
│ Action 30 → ConfigMessage.ts → category handlers │
│ Action 168 → ExternalMessage.ts (wireless changes) │
│ Action 184 → EquipmentStateMessage.ts (v3 circuits) │
│ Action 217 → EquipmentStateMessage.ts (device list) │
│ Action 251/253→ Registration handshake │
│ Chlorinator → ChlorinatorStateMessage.ts │
│ Pump protocol → PumpStateMessage.ts │
└─────────────────────────────────────────────────────────┘
Config Handlers (Action 30, routed by payload byte[0]):
| Category | Handler | Purpose |
|---|---|---|
| 0 | OptionsMessage | System options |
| 1 | CircuitMessage | Circuit config |
| 2 | FeatureMessage | Feature config |
| 3 | ScheduleMessage | Schedule times/days |
| 4 | PumpMessage | Pump config |
| 5 | RemoteMessage | Remote buttons |
| 6 | CircuitGroupMessage | Light/circuit groups |
| 7 | ChlorinatorMessage | Chlorinator settings |
| 8 | IntellichemMessage | Chemistry controller |
| 9 | ValveMessage | Valve assignments |
| 10 | HeaterMessage | Heater config |
| 11 | SecurityMessage | PIN codes |
| 12 | GeneralMessage | Pool name, location |
| 13 | EquipmentMessage | Body/equipment config |
| 14 | CoverMessage | Pool covers |
External Message Handlers (Action 168, routed by payload byte[0]):
| Case | Handler Method | Purpose |
|---|---|---|
| 0 | processTempSettings | Setpoints/heat mode |
| 1 | processCircuit | Circuit changes |
| 2 | processFeature | Feature changes |
| 3 | processSchedules | Schedule changes |
| 4 | processPump | Pump changes |
| 7 | processChlorinator | Chlorinator changes |
| 10 | processHeater | Heater changes |
Rule: All v3.004+ IntelliCenter changes started on November 26, 2025. Everything in the codebase before that date was working correctly for v1.x.
Implications:
- When investigating v3 issues, only look at commits from 11/26/2025 onwards
- Any code that existed before 11/26/2025 should be assumed correct for v1.x
- v3 changes MUST be gated behind
sys.equipment.isIntellicenterV3to avoid breaking v1 - Do NOT modify pre-11/26 code paths without explicit approval
Git command to see v3 changes:
git log --oneline --since="2025-11-26" -- <file>Rule: v3.004 uses BIG-ENDIAN for 16-bit time values; v1.x uses LITTLE-ENDIAN.
Check: Any extractPayloadInt() for schedule/time values needs v3 handling.
Fix pattern:
if (sys.controllerType === ControllerType.IntelliCenter && sys.equipment.isIntellicenterV3) {
time = msg.extractPayloadIntBE(offset); // Big-endian for v3.004+
} else {
time = msg.extractPayloadInt(offset); // Little-endian for v1.x
}Already fixed:
ScheduleMessage.processStartTimes()- schedule start timesScheduleMessage.processEndTimes()- schedule end timesExternalMessage.processSchedules()- wireless schedule changes
Check for similar issues in: Any message handler parsing 16-bit time values.
Rule: When config properties are set, ALWAYS sync corresponding state properties.
Bug pattern: Config property set but state not synced → UI shows undefined/blank.
Example (chlorinator name bug we fixed):
// BAD - name set in config but never synced to state
chlor.name = "Chlorinator 1";
// schlor.name never set → dashPanel shows blank name!
// GOOD - always sync config properties to state
chlor.name = "Chlorinator 1";
schlor.name = chlor.name; // ← Don't forget this!Checklist when reviewing config handlers:
- Find all
chlor.property = valueassignments - Verify each has corresponding
schlor.property = chlor.property - Check for default values when property might be undefined
Rule: Feature/circuit names and IDs are in configuration files, not guesswork.
Locations:
- Live system:
./data/poolConfig.jsonand./data/poolState.json - Replay captures:
[replayname]/njspc/data/poolConfig.jsonandpoolState.json
Key ID ranges (IntelliCenter):
- Circuits: 1-40 (varies by system)
- Features: Start at 129 (
sys.board.equipmentIds.features.start) - Circuit Groups: Start at 192 (
sys.board.equipmentIds.circuitGroups.start)
Example lookup:
// From poolConfig.json
"features": [
{ "id": 129, "name": "Test_Feature" },
{ "id": 130, "name": "Air Blower222" },
{ "id": 131, "name": "TestF2" }
]Rule: When analyzing packet captures, decode payloads systematically.
CRITICAL: Always include packet IDs when discussing packets. The id field in each packet log entry is essential for:
- Cross-referencing with user observations
- Correlating request/response pairs
- Debugging timing issues
- Reproducing specific scenarios
Format for discussing packets:
#2605 23:09:49 Action30/15 byte9=3 features: ['129', '130'] <-- CONFIG RESPONSE
#2607 23:09:52 Action204 byte19=2 features: ['130'] <-- OVERWRITES!
Always prefix with # and packet ID.
Packet structure: [header][payload][checksum]
- Header:
[165, 1, dest, src, action, length] - Action codes: See
.plan/203-intellicenter-action-registry.md
Common analysis steps:
- Filter by action code:
grep "action\":168"for external messages - Decode payload bytes using handler code as reference
- Check timestamps for sequence/timing issues
- Look for request/response pairs (outbound
dir":"out"→ inbounddir":"in") - Always extract and display packet IDs in any analysis output
Replay caveat (API lines):
- Some replay packet logs include captured API requests (for example
/app/config/stopPacketCaptureinv3.008_replay.157_config1andv3.008_replay.160_config2). - Treat these as tooling/control-plane events, not protocol source-of-truth packets.
- If replaying API lines out-of-order causes app errors, classify separately from RS-485 protocol regressions.
Decoding times (v3.004 big-endian):
- Two bytes
[hi, lo]→hi * 256 + lo= minutes since midnight - Example:
[2, 33]→2*256 + 33 = 545→ 9:05 AM
Rule: v3.004+ uses Action 184 for circuit control, NOT Action 168.
Problem: When njsPC sends Action 168 to control circuits on v3.004+, OCP accepts it briefly then reverts the state. The Wireless remote uses Action 184, which OCP accepts permanently.
Solution (indexed outbound control): njsPC does not persist per-circuit target IDs. Outbound control uses a fixed Wireless-style frame:
- Circuits: channel
0x688F(104,143), byte 2 =circuitId - 1, format 255, target0xA8ED(168,237) — the shared control primitive — state in byte 6. - Features: channel
0xE89D(232,157), index derived from feature id, same fixed target pattern (seeIntelliCenterBoardfeature path). - Bodies (Pool/Spa): multi-step Action 184 sequence with dedicated targets (body select / context / toggle), not the simple indexed circuit frame.
Action 184 Payload (10 bytes):
[channelHi, channelLo, seqOrIndex, format, targetHi, targetLo, state, 0, 0, 0]
├─ Channel: family-specific (e.g. 104,143 for indexed circuits; 232,157 for features; other families on the bus)
├─ Byte 2: circuit index (circuits), sequence, or other selector depending on channel/target family
├─ Target: often 168,237 (0xA8ED) for the indexed circuit/feature primitive; bodies use additional targets (e.g. 114,145 / 212,182 / 168,237) in sequence
└─ State: 0=OFF, 1=ON (meaning depends on target family)
Inbound Action 184: OCP and panels broadcast many 184 families; EquipmentStateMessage case 184 is intentionally a no-op (no state mutation from inbound 184). Use Action 30 / Action 2 / authoritative paths for state.
Code locations:
- Circuit/feature/body send:
IntelliCenterBoard.setCircuitStateAsync(),setFeatureStateAsync(),sendV3BodyControlSequenceAsync() - Inbound router:
EquipmentStateMessage.ts(case 184)
Rule: v3.004 Wireless Action 168 type 0 has DIFFERENT byte offsets than Action 30 type 0!
Problem: When parsing body setpoints/heat modes from Wireless remote messages (Action 168 type 0), using Action 30 offsets (19-24) produces garbage values.
Root Cause: The Wireless Action 168 payload has an extra byte in early positions, shifting all indices by +1.
Correct Offsets:
| Field | Action 30 | Action 168 |
|---|---|---|
| Pool setpoint | 19 | 20 |
| Pool cool | 20 | 21 |
| Spa setpoint | 21 | 22 |
| Spa cool | 22 | 23 |
| Pool mode | 23 | 24 |
| Spa mode | 24 | 25 |
Code Location: ExternalMessage.ts → processTempSettings() detects v3 and uses correct offsets.
Verified from captures: Replay 30 (5 packets), Replay 48 (multiple packets).
Rule: Detailed packet/flow documentation lives in .plan/ directory, organized by controller type and equipment type.
ALWAYS read .plan/INDEX.md first when working on protocol issues. It indexes all protocol documentation (including IntelliCenter: .plan/200-intellicenter-v3-index.md).
IntelliCenter:
.plan/201-intellicenter-circuits-features.md- Circuits, features, groups (current).plan/203-intellicenter-action-registry.md- Action code reference.plan/204-intellicenter-v3-protocol.md- Protocol findings summary.plan/200-intellicenter-v3-index.md- IntelliCenter v3.004 index (linked from.plan/INDEX.md)
Active protocol files:
201-intellicenter-circuits-features.md- Circuits, features, groups202-intellicenter-bodies-temps.md- Body temps/setpoints/heat modes
Future files (create as needed):
INTELLICENTER_PUMPS_PROTOCOL.md- Pump control/statusINTELLICENTER_HEATERS_PROTOCOL.md- Heater control/statusINTELLICENTER_SCHEDULES_PROTOCOL.md- Schedule managementINTELLICENTER_CHEMISTRY_PROTOCOL.md- IntelliChem/chlorinator
Other Controllers (create as needed):
INTELLITOUCH_*.md- IntelliTouch protocolsEASYTOUCH_*.md- EasyTouch protocolsNIXIE_*.md- Nixie/virtual protocols
Each protocol file should include:
- Message Types Summary - Table of all relevant actions
- Complete Flow Diagrams - Step-by-step packet sequences
- Byte Offset Tables - Where data lives in payloads
- Version Differences - v1.x vs v3.004+ variations
- Handler Routing - Which code files process each message
- Troubleshooting - Common issues and solutions
Authoritative source: Action 30 case 15 (NOT Action 204!)
Key v3.004+ bug: Action 204 byte 19 contains STALE feature state. Must skip processing.
See: .plan/201-intellicenter-circuits-features.md for full details.
| Field | Action 30 (config) | Action 168 (Wireless) |
|---|---|---|
| Pool setpoint | byte 19 | byte 20 |
| Pool cool | byte 20 | byte 21 |
| Spa setpoint | byte 21 | byte 22 |
| Spa cool | byte 22 | byte 23 |
| Pool mode | byte 23 | byte 24 |
| Spa mode | byte 24 | byte 25 |
Root cause: Wireless Action 168 payload has extra byte in early positions, shifting indices by +1.
Heat mode valueMap fix: Use htypes.total > 1 to check for multi-heater setups (Solar+UltraTemp).
See: .plan/202-intellicenter-bodies-temps.md for full details.
Last Updated: March 13, 2026
Source: nodejs-poolController IntelliCenter v3.004 compatibility work