Skip to content

Commit c7361ce

Browse files
committed
Expand client to support multiple monitor types
Refactored TankAlarm client to support monitoring of tanks, engines, pumps, gas systems, and flow meters with flexible sensor interfaces (digital, analog, current loop, pulse). Updated configuration structures, runtime state, and documentation to enable multi-purpose monitoring and improved RPM/flow measurement for very low rates.
1 parent 723c7fb commit c7361ce

File tree

7 files changed

+1696
-627
lines changed

7 files changed

+1696
-627
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Code Review: Tank Alarm 112025 (Opta + Blues)
2+
**Date:** December 22, 2025
3+
**Reviewer:** GitHub Copilot
4+
5+
## Overview
6+
This review covers the "112025" version of the Tank Alarm system, specifically the Server, Client, and Viewer components designed for the Arduino Opta and Blues Wireless Notecard.
7+
8+
**Scope:**
9+
- `TankAlarm-112025-Server-BluesOpta/`
10+
- `TankAlarm-112025-Client-BluesOpta/`
11+
- `TankAlarm-112025-Viewer-BluesOpta/`
12+
13+
## Summary
14+
The codebase is well-structured, modular, and demonstrates a high level of maturity for an embedded system. It effectively leverages the capabilities of the Arduino Opta (STM32H7) and the Blues Notecard. The code handles complex tasks such as telemetry aggregation, remote configuration, and reliable communication with robust error handling.
15+
16+
## Key Strengths
17+
1. **Architecture**: Clear separation of concerns between Server, Client, and Viewer roles. The Server acts as the central hub, aggregating data and managing configuration, while Clients are focused on sensing and reporting.
18+
2. **Hardware Abstraction**: The code uses conditional compilation (`#ifdef`) effectively to support different hardware platforms (Opta/Mbed vs. STM32duino), ensuring portability.
19+
3. **Robustness**:
20+
- **Watchdog Timer**: Integrated watchdog support (`IWatchdog` or Mbed `Watchdog`) prevents system hangs.
21+
- **Error Handling**: Notecard requests and JSON parsing are checked for errors.
22+
- **Memory Management**: Heap allocation for large JSON documents is handled correctly with explicit `delete` calls.
23+
4. **Configuration**: Extensive use of `#define` macros for compile-time configuration and `struct`s for runtime configuration.
24+
5. **Features**:
25+
- **Remote Configuration**: The system supports pushing configuration updates from the Server to Clients via the Notecard.
26+
- **Calibration**: A learning-based calibration system is implemented.
27+
- **Web Interface**: The Server hosts a comprehensive web dashboard and configuration tools.
28+
29+
## Findings & Recommendations
30+
31+
### 1. Network Configuration (High Priority)
32+
**Observation:** IP addresses and MAC addresses are hardcoded in the firmware.
33+
- `gStaticIp`, `gStaticGateway`, etc. are defined as `static` global variables.
34+
- `gMacAddress` is hardcoded.
35+
36+
**Risk:** This limits deployment flexibility. Multiple devices on the same network would require recompilation to avoid IP/MAC conflicts.
37+
38+
**Recommendation:**
39+
- **DHCP**: Enable DHCP by default, with a fallback to a static IP if DHCP fails.
40+
- **Configuration File**: Load network settings (IP, Gateway, Subnet) from a configuration file on the filesystem (e.g., `network.json`) or from the Notecard's environment variables.
41+
- **Unique MAC**: While the locally administered bit is used, ensure a mechanism to generate a unique MAC address (e.g., based on the MCU's unique ID) to avoid collisions.
42+
43+
### 2. String Usage (Medium Priority)
44+
**Observation:** The `String` class is used in several places (e.g., `performFtpBackup`, HTTP handling).
45+
46+
**Risk:** On embedded systems, excessive use of `String` can lead to heap fragmentation over time, potentially causing instability.
47+
48+
**Recommendation:**
49+
- While the Opta has ample RAM (1MB), it is best practice to minimize `String` usage. Prefer `char` arrays and `snprintf` for string manipulation where possible, especially in long-running loops.
50+
51+
### 3. Security (Medium Priority)
52+
**Observation:**
53+
- The system uses a PIN (`configPin`) for protecting sensitive web actions.
54+
- `SERVER_PRODUCT_UID` and `VIEWER_PRODUCT_UID` are hardcoded.
55+
56+
**Recommendation:**
57+
- Ensure the PIN is strong and changed from any default.
58+
- Consider loading Product UIDs from a secure configuration or Notecard environment variables to allow for easier fleet management without recompilation.
59+
60+
### 4. Code Duplication (Low Priority)
61+
**Observation:** There is some code duplication between the Server, Client, and Viewer sketches (e.g., `initializeNotecard`, `ensureTimeSync`, Watchdog setup).
62+
63+
**Recommendation:**
64+
- If the project grows, consider creating a shared library (e.g., `TankAlarmCommon`) to encapsulate common functionality. This would reduce maintenance effort and ensure consistency.
65+
66+
### 5. Memory Safety (Medium Priority)
67+
**Observation:** `DynamicJsonDocument` is allocated on the heap in multiple locations across Server, Client, and Viewer.
68+
69+
**Findings:**
70+
-**Server `sendClientDataJson`** (line 2668): Uses `std::unique_ptr` correctly - no leak possible.
71+
-**Server HTTP body parsing** (line 2811): Uses `std::unique_ptr` correctly.
72+
-**Viewer `sendTankJson`** (line 528): Uses `std::unique_ptr` correctly.
73+
-**Viewer `fetchViewerSummary`** (line 591): Uses `std::unique_ptr` and properly calls `NoteFree(json)` in both success and OOM paths.
74+
-**Client config loading** (lines 939, 958, 1154): Uses `std::unique_ptr` throughout.
75+
- ⚠️ **Server `handleContactsGet`** (line 4279): Uses raw `new` with manual `delete` - see issue below.
76+
77+
**Issue in `handleContactsGet`:**
78+
```cpp
79+
DynamicJsonDocument *docPtr = new DynamicJsonDocument(CONTACTS_JSON_CAPACITY);
80+
// ... processing ...
81+
delete docPtr; // Only called at end of function
82+
```
83+
If `serializeJson` throws or memory operations fail between allocation and delete, memory will leak.
84+
85+
**Recommendations:**
86+
- **Convert `handleContactsGet`** to use `std::unique_ptr<DynamicJsonDocument>` for consistency with other functions.
87+
- The Client code properly guards malloc with null checks and frees in error paths (line 929-946) - this pattern is correct.
88+
89+
### 6. FTP Implementation Review (Medium Priority)
90+
**Observation:** The FTP backup/restore implementation is functional but has several areas for improvement.
91+
92+
**Findings:**
93+
94+
#### A. Connection Management ✅
95+
- `ftpQuit()` properly sends QUIT command and closes the control connection.
96+
- `ftpConnectAndLogin()` validates host presence and connection success.
97+
- `FtpSession` struct correctly encapsulates the control connection.
98+
99+
#### B. Error Handling ⚠️
100+
- **Partial Failure Recovery**: When `performFtpBackup` fails mid-backup (e.g., after 2 of 5 files), there's no rollback or indication of which files succeeded.
101+
- **Silent Failures**: In `ftpBackupClientConfigs`, individual file failures are logged but don't stop the backup or report to caller.
102+
103+
**Recommendation:** Consider returning a more detailed result structure:
104+
```cpp
105+
struct FtpResult {
106+
bool success;
107+
uint8_t filesProcessed;
108+
uint8_t filesFailed;
109+
char lastError[128];
110+
};
111+
```
112+
113+
#### C. Buffer Size Concerns ⚠️
114+
- **`ftpBackupClientConfigs`** uses a 2KB manifest buffer (line 1867) - adequate for ~40 clients with typical UID/site lengths.
115+
- **`ftpRestoreClientConfigs`** uses a 1KB config buffer (line 1962) per client - may truncate large configs.
116+
- **`FTP_MAX_FILE_BYTES` = 24KB** - enforced correctly in `writeBufferToFile` but not in all retrieve paths.
117+
118+
**Recommendation:** The 1KB client config buffer should be increased to match `FTP_MAX_FILE_BYTES` or at least 2KB to prevent silent truncation of larger configurations.
119+
120+
#### D. Timeout Handling ✅
121+
- `FTP_TIMEOUT_MS` (8 seconds) is applied consistently in `ftpReadResponse` and `ftpRetrieveBuffer`.
122+
- The implementation correctly handles slow/stalled connections.
123+
124+
#### E. Data Connection Cleanup ⚠️
125+
- In `ftpStoreBuffer` and `ftpRetrieveBuffer`, the data connection (`dataClient`) is properly stopped before checking transfer completion.
126+
- However, if `ftpEnterPassive` succeeds but `dataClient.connect` fails, subsequent operations may leave the server in an unexpected state.
127+
128+
**Recommendation:** After a data connection failure, consider sending `ABOR` to reset server state before the next operation.
129+
130+
#### F. Path Traversal Security ✅
131+
- `buildRemotePath` constructs paths safely using `snprintf` with bounded output.
132+
- No user-controlled input flows directly into path construction.
133+
134+
### 7. Client Memory Management (Low Priority)
135+
**Observation:** The Client code (lines 929-946) uses `malloc`/`free` for file reading with proper null checks.
136+
137+
**Findings:**
138+
- ✅ Null check after `malloc` (line 930 equivalent).
139+
- ✅ `free(buffer)` called in both success (line 946) and error (line 941) paths.
140+
- ✅ File size validated before allocation (8KB limit, line 924-927).
141+
- ✅ `std::unique_ptr` used for `DynamicJsonDocument` in same function.
142+
143+
**Recommendation:** None - this pattern is correct and robust.
144+
145+
## Conclusion
146+
The "112025" code release is in excellent shape. The logic is sound, and the implementation is robust. Addressing the network configuration rigidity is the most significant improvement to be made for scalable deployment.
147+
148+
**Status:** **APPROVED** (with recommendations)

TankAlarm-112025-Client-BluesOpta/README.md

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,43 @@
11
# TankAlarm 112025 Client - Blues Opta
22

3-
Tank level monitoring client using Arduino Opta with Blues Wireless Notecard cellular connectivity.
3+
Multi-purpose monitoring client using Arduino Opta with Blues Wireless Notecard cellular connectivity.
44

55
## Overview
66

7-
The TankAlarm 112025 Client monitors tank levels using analog sensors and reports data to a central server via Blues Wireless Notecard. Key features include:
7+
The TankAlarm 112025 Client monitors tanks, engines, pumps, and other equipment using various sensor types and reports data to a central server via Blues Wireless Notecard. Key features include:
88

99
- **Cellular connectivity** via Blues Wireless Notecard
10-
- **Multi-tank support** - Monitor up to 8 tanks per device
11-
- **Configurable alarms** - High and low thresholds per tank
10+
- **Multi-monitor support** - Monitor up to 8 sensors per device
11+
- **Flexible object types** - Tanks, engines, pumps, gas systems, flow meters
12+
- **Multiple sensor interfaces** - Digital, analog voltage, 4-20mA, pulse/RPM
13+
- **Configurable alarms** - High and low thresholds per monitor
1214
- **Remote configuration** - Update settings from server web interface
1315
- **Persistent storage** - LittleFS internal flash (no SD card needed)
1416
- **Fleet-based communication** - Simplified device-to-device routing
1517
- **Watchdog protection** - Auto-recovery from hangs
1618

19+
## Architecture
20+
21+
### Object Types (What You're Monitoring)
22+
The system separates **what** is being monitored from **how** it's measured:
23+
24+
| Object Type | Description | Typical Sensors |
25+
|-------------|-------------|-----------------|
26+
| `tank` | Liquid storage tank | 4-20mA level sensor, float switch |
27+
| `engine` | Engine or motor | Hall effect RPM sensor |
28+
| `pump` | Pump operation | Digital status, flow meter |
29+
| `gas` | Gas pressure system | 4-20mA pressure transducer |
30+
| `flow` | Flow measurement | Pulse-based flow meter |
31+
| `custom` | User-defined | Any sensor type |
32+
33+
### Sensor Interfaces (How You're Measuring)
34+
| Interface | Description | Output Type |
35+
|-----------|-------------|-------------|
36+
| `digital` | Binary on/off | Float state (0/1) |
37+
| `analog` | Voltage output | Raw voltage (V) |
38+
| `currentLoop` | 4-20mA | Raw milliamps (mA) |
39+
| `pulse` | Pulse counting | RPM or flow rate |
40+
1741
## Hardware
1842

1943
- **Arduino Opta Lite** - Industrial controller with built-in connectivity
@@ -27,7 +51,7 @@ The TankAlarm 112025 Client monitors tank levels using analog sensors and report
2751
1. Install Blues Wireless for Opta carrier on Arduino Opta
2852
2. Activate Notecard with Blues Wireless (see [blues.io](https://blues.io))
2953
3. Connect Arduino Opta Ext A0602 for analog inputs
30-
4. Connect tank level sensors to analog inputs
54+
4. Connect sensors to appropriate inputs
3155

3256
### 2. Software Setup
3357
1. Install Arduino IDE 2.x or later
@@ -36,7 +60,7 @@ The TankAlarm 112025 Client monitors tank levels using analog sensors and report
3660
- **ArduinoJson** (version 7.x or later)
3761
- **Blues Wireless Notecard** (latest)
3862
- LittleFS (built into Mbed core)
39-
4. Open `TankAlarm-112025-Client-BluesOpta.ino` in Arduino IDE
63+
4. Open `TankAlarm-092025-Client-BluesOpta.ino` in Arduino IDE
4064
5. Update `PRODUCT_UID` to match your Blues Notehub project
4165
6. Compile and upload to Arduino Opta
4266

@@ -56,12 +80,12 @@ The client creates a default configuration on first boot. You can update configu
5680
1. Deploy the TankAlarm 112025 Server
5781
2. Access server dashboard at `http://<server-ip>/`
5882
3. Select client from dropdown
59-
4. Update settings (site name, tanks, thresholds, etc.)
83+
4. Update settings (site name, monitors, thresholds, etc.)
6084
5. Click "Send Config to Client"
6185

6286
**Default Configuration:**
6387
- Sample interval: 1800 seconds (30 minutes)
64-
- Single tank: "Tank A"
88+
- Single monitor: "Tank A" (tank, analog)
6589
- High alarm: 110 inches
6690
- Low alarm: 18 inches
6791
- Server fleet: "tankalarm-server"
@@ -75,15 +99,17 @@ The client creates a default configuration on first boot. You can update configu
7599

76100
### Sampling Settings
77101
- **Sample Interval**: Seconds between sensor readings (default: 1800)
78-
- **Level Change Threshold**: Inches of change to trigger telemetry (0 to disable)
79-
80-
### Tank Configuration (per tank)
81-
- **Tank ID**: Single letter identifier (A-H)
82-
- **Tank Name**: Descriptive name
83-
- **High Alarm**: Threshold in inches for high level alert
84-
- **Low Alarm**: Threshold in inches for low level alert
102+
- **Level Change Threshold**: Change amount to trigger telemetry (0 to disable)
103+
104+
### Monitor Configuration (per monitor)
105+
- **Monitor ID**: Single letter identifier (A-H)
106+
- **Monitor Name**: Descriptive name
107+
- **Object Type**: What is being monitored (tank, engine, pump, gas, flow, custom)
108+
- **Sensor Interface**: How measurement is taken (digital, analog, currentLoop, pulse)
109+
- **High Alarm**: Threshold for high alarm
110+
- **Low Alarm**: Threshold for low alarm
111+
- **Measurement Unit**: Display unit ("inches", "rpm", "psi", "gpm", etc.)
85112
- **Analog Pin**: Arduino Opta analog input (A0-A7, I1-I8)
86-
- **Sensor Type**: "voltage" (0-10V), "current" (4-20mA), "digital" (float switch), or "rpm" (hall effect)
87113

88114
### 4-20mA Current Loop Sensor Configuration
89115

@@ -240,10 +266,12 @@ Hall effect sensors can be used to measure RPM (rotations per minute) for applic
240266
**Detection Methods:**
241267

242268
1. **Pulse Counting** (default)
243-
- Counts all pulses over a fixed sampling period (3 seconds)
269+
- Counts all pulses over a configurable sampling period (default 60 seconds)
244270
- More accurate for steady speeds
245271
- Averages multiple revolutions for better precision
246272
- Formula: RPM = (pulses × 60000) / (sample_duration_ms × pulses_per_rev)
273+
- Minimum detectable RPM = 60000 / (sample_duration_ms × pulses_per_rev)
274+
- With 60s sampling and 1 pulse/rev: minimum 1 RPM
247275

248276
2. **Time-Based**
249277
- Measures the period between two consecutive pulses
@@ -252,6 +280,13 @@ Hall effect sensors can be used to measure RPM (rotations per minute) for applic
252280
- More flexible for different magnet types and orientations
253281
- Formula: RPM = 60000 / (period_ms × pulses_per_rev)
254282

283+
3. **Accumulated Mode** (for very low RPM < 1)
284+
- Counts pulses between telemetry reports (e.g., 30 minutes)
285+
- Enable with `rpmAccumulatedMode: true`
286+
- Ideal for slow-moving pumps, flow meters, or agitators
287+
- Formula: RPM = (accumulated_pulses × 60000) / (elapsed_ms × pulses_per_rev)
288+
- With sampleSeconds=1800 (30 min) and 1 pulse/rev: detects down to 0.033 RPM
289+
255290
**Configuration Parameters:**
256291
- `sensorType`: "rpm"
257292
- `rpmPin`: Digital input pin for hall effect sensor (uses internal pull-up)
@@ -261,6 +296,12 @@ Hall effect sensors can be used to measure RPM (rotations per minute) for applic
261296
- **Note:** For **bipolar** or **omnipolar** sensors, a single magnet generates 2 pulses per revolution (one for each pole). Set `pulsesPerRevolution` to `2 × number of magnets` in these cases.
262297
- `hallEffectType`: Sensor type - "unipolar", "bipolar", "omnipolar", or "analog"
263298
- `hallEffectDetection`: Detection method - "pulse" or "time"
299+
- `rpmSampleDurationMs`: Sample duration in milliseconds (default: 60000 = 60 seconds)
300+
- Longer durations detect lower RPM but increase measurement time
301+
- For 0.1 RPM detection without accumulated mode: use 600000 (10 minutes)
302+
- `rpmAccumulatedMode`: Set to `true` for very low RPM measurement (< 1 RPM)
303+
- Counts pulses between telemetry reports instead of during a fixed sample window
304+
- Best for applications where RPM is very slow (e.g., 0.1 RPM = 1 rotation per 10 minutes)
264305
- `highAlarm`: Maximum expected RPM for alarm (e.g., 3000)
265306
- `lowAlarm`: Minimum expected RPM for alarm (e.g., 100)
266307

@@ -278,6 +319,21 @@ Hall effect sensors can be used to measure RPM (rotations per minute) for applic
278319
```
279320
Note: With omnipolar sensor and 4 magnets, use pulsesPerRev = 8 (2 pulses per magnet × 4 magnets)
280321

322+
**Example Configuration** (Slow pump flow meter - 0.1 RPM detection):
323+
```json
324+
{
325+
"sensor": "rpm",
326+
"rpmPin": 3,
327+
"pulsesPerRev": 1,
328+
"hallEffectType": "unipolar",
329+
"hallEffectDetection": "pulse",
330+
"rpmAccumulatedMode": true,
331+
"highAlarm": 10,
332+
"lowAlarm": 0.05
333+
}
334+
```
335+
Note: With accumulated mode enabled and 30-minute sample intervals, this can detect down to 0.033 RPM
336+
281337
**Wiring:**
282338
- Connect hall effect sensor VCC to 5V or 3.3V (check sensor datasheet)
283339
- Connect sensor GND to Arduino GND
@@ -287,6 +343,7 @@ Note: With omnipolar sensor and 4 magnets, use pulsesPerRev = 8 (2 pulses per ma
287343
**Tips:**
288344
- Use "time" detection for faster response to speed changes
289345
- Use "pulse" detection for better accuracy at steady speeds
346+
- Use `rpmAccumulatedMode: true` for very slow rotation (< 1 RPM)
290347
- For multiple magnets, set `pulsesPerRev` to the number of magnets
291348
- For omnipolar sensors, each magnet passing creates 2 pulses (both N and S poles trigger)
292349
- Monitor both high and low thresholds to detect over-speed and stall conditions

0 commit comments

Comments
 (0)