Use Your AI Agent to Talk to Your Products with Notehub IQ

Blues Developers
What’s New
Resources
API Reference
Notecard and Notehub API documentation
Blog
Technical articles for developers
Connected Product Guidebook
In-depth guides for connected product development
Developer Certification
Get certified on wireless connectivity with Blues
Newsletter
The monthly Blues developer newsletter
Terminal
Connect to a Notecard in your browser
Webinars
Listing of Blues technical webinars
Blues.comNotehub
Shop
Docs
Help
Support DocsNotehub StatusSecurity AdvisoriesVisit our Forum
Sign In
Docs Home
What’s New
Resources
API Reference
Notecard and Notehub API documentation
Blog
Technical articles for developers
Connected Product Guidebook
In-depth guides for connected product development
Developer Certification
Get certified on wireless connectivity with Blues
Newsletter
The monthly Blues developer newsletter
Terminal
Connect to a Notecard in your browser
Webinars
Listing of Blues technical webinars
Blues.comNotehub
Shop
Docs
Example Apps
Samples
Building a Simple Asset Tracker
Building Edge AI Applications
Continuous Asset Tracking with External GPS and Immediate Location Sync
Controlling Device State and Receiving Acknowledgment
Managing a Notecard's WiFi Network Remotely
Putting a Host to Sleep Between Sensor Readings
Routing Data from Notehub to a Custom Cloud Endpoint
Using LEDs and NeoPixels to Monitor Notecard Status
Accelerators
Asset Location Tracking
Heavy Equipment Hours-of-Use & Utilization Tracker
Returnable Container / Tote Pool Tracker
Untethered Trailer and Chassis Fleet Tracker
Asset Performance Optimization
Legacy Diesel Generator Fleet Performance Uplift
Solar Array String-Level Performance Dashboard
Battery Management Systems
Aerial Lift / Rental Equipment Battery Health Monitor
Off-Grid Solar Battery Site Controller
Remote Cabinet Backup Battery Sentinel
Downtime Prevention
Municipal Wastewater Lift Station Monitor
Rooftop HVAC Predictive Maintenance
VFD Pump Predictive Maintenance
Energy Monitoring Solutions
Commercial Tenant Sub-Metering Bridge
EV Charger Session & Utilization Monitor
Utility Distribution Transformer Load Monitor
Energy Savings
Commercial Plug-Load & After-Hours Waste Dashboard
Demand-Response Solar + Battery Dispatcher
Multi-Site Walk-In Cooler Energy & Setpoint Monitor
Industrial Equipment Monitoring
CNC Machine Spindle Load and Cycle Time Tracker
Injection Molding Shot-to-Shot Process Monitor
Trailer Manufacturer Connected Trailer Platform
Loss Prevention
Construction Equipment Anti-Theft Tracker with Immobilizer
Reefer Trailer Cold-Chain & Door-Event Monitor
Remote Patient Monitoring
Cellular Medication Adherence Pillbox
Post-Discharge Vitals Relay Hub
Remotely Monitored Assets
Off-Grid Livestock Water Tank Monitor
Remote Apiary Hive Health Monitor
Safety Assurance
Construction Site Environmental & Noise Exposure Monitor
Lone Worker Panic and Fall Detection Beacon
Regulatory-Grade Pharmacy and Lab Cold Storage Audit Monitor
Supply Chain Tracking
Pallet-Attached Cold Chain Logger
Rail Car Condition & Interchange Tracker
Truck Roll Reduction
Commercial Grease Interceptor Level Monitor
Propane / LPG Tank Fill Telemetry
homechevron_rightExample Appschevron_rightAcceleratorschevron_rightLegacy Diesel Generator Fleet Performance Uplift

Legacy Diesel Generator Fleet Performance Uplift

Legacy Diesel Generator Fleet Performance Uplift banner
note

This reference application is here to give you ideas and help you get started quickly. It's built on specific hardware that may not match yours, so focus on the sections that fit your use case. If you'd like to talk through your project and whether it's a good fit for Blues, reach out.

Building with an AI assistant? Point it at this accelerator's firmware in firmware/diesel_gen_monitor/ so it can learn how the device works and help you adapt it to your hardware.

This project is an asset performance optimization retrofit for operators of standby diesel generators at hospitals, data centers, and industrial sites. The device sits on the DIN rail inside the generator panel and reads the controller's existing Modbus port. Over cellular, it reports run hours, fault codes, fuel level, coolant temperature, oil pressure, and load, and it sends an alert right away on faults, coolant overtemp, low oil pressure, low fuel, or lost communication. It runs off the panel's battery-backed DC control bus and uses an external antenna routed outside the metal enclosure. That keeps the monitoring path alive when mains power and facility WiFi go dark, which is exactly the moment a standby generator either starts or doesn't. The hardware is a Finder OPTA with a Blues Wireless for OPTA cellular expansion (see §4 for the BOM). The wiring is strictly read-only, with no writes to start/stop or safety circuits.

1. Project Overview

The problem. Standby diesel generators at hospitals, data centers, and industrial sites are among the most important assets on the premises, and among the least watched. The controllers that run them (DeepSea (DSE), Woodward, Kohler, Caterpillar, Cummins, and a dozen others) have spoken Modbus RTU (Remote Terminal Unit, the most widely deployed industrial serial protocol) for decades. That interface already holds fuel level, load percentage, coolant temperature, oil pressure, run hours, and the active alarm register that reflects the controller's current fault state. When a failure-to-start event is asserted, it shows up there too. Hardly anyone reads any of it. The controller reports faults to an annunciator panel in the same room, which is exactly where nobody is once the building is dark and the emergency has already started.

A standby generator that fails to start during a utility outage is worse than no generator at all, because the false sense of protection can delay the emergency response. One that starts but is low on fuel, or trending toward coolant overtemp, will quit mid-outage with no warning. And run-hour data that never leaves the controller can't help anyone schedule preventive maintenance before a critical service interval slips past. Facilities that have generators but not generator monitoring run into all three of these regularly.

This project closes that gap for active alarm state, a firmware-observed alarm chronology, and operating data. A Blues Wireless for OPTA snapped onto a Finder OPTA industrial PLC (programmable logic controller) sits on the DIN rail inside the generator panel. Once a minute it polls seven holding registers over the controller's Modbus port, including the active alarm bitmask. It then sends alert events, an hourly alarm-history log, and hourly summaries over cellular to the Blues Notehub cloud service. You'll need to run new RS-485 wiring to the controller's communications terminals and match the controller's Modbus serial settings (baud rate, parity, slave address). Nothing writes to start/stop or safety circuits, because the firmware is strictly read-only over Modbus, and nothing depends on the facility network.

Why Notecard. It comes down to timing. A standby generator exists to supply power when mains fails, and that's exactly when the facility's WiFi router goes dark too. A monitor that depends on the building's LAN can't report a failure-to-start at the one moment it matters most. Worse, a LAN-only monitor can look like evidence that everything is fine, right up until the UPS batteries run dry. Cellular, with an antenna routed outside the metal generator enclosure, gives you a communication path that doesn't depend on the facility's power or network at all. It stays up because it draws from the generator panel's own battery-backed DC control bus, which runs off the same battery that starts the engine.

New to Blues?

Learn what we do in this 3-minute video that walks you through how we simplify wireless connectivity.

Learn what we do in this 3-minute video that walks you through how we simplify wireless connectivity.

All of this works because a well-designed generator panel already has a battery-backed 12–24 VDC control bus whose whole job is to stay live when mains fails. The OPTA and Wireless for OPTA draw from that bus. So when the utility fails, the transfer switch opens, and the office lights go out, the monitor is already running on cellular and polling the controller once a minute for the data the facility manager needs.

Deployment scenario. One OPTA RS485 with Wireless for OPTA mounts on the DIN rail inside the generator's main control panel and runs on the panel's existing battery-backed DC control bus. RS-485 wiring goes to the controller's communications port, and the cellular antenna leaves through a cable gland. You do have to wire RS-485 and match the Modbus communication settings. Beyond that, nothing writes to safety circuits, and you don't need the OEM's cooperation or anything from IT.

2. System Architecture

System architecture: Generator Controller ↔ Modbus RTU over RS-485 ↔ OPTA RS485 + Wireless for OPTA ↔ cellular ↔ Notehub ↔ routes

Device-side responsibilities. The OPTA's Cortex-M7 host acts as the Modbus RTU client (master), and the generator controller is the server (slave). Once a minute, the host reads the same seven holding registers through the onboard RS-485 transceiver (engine RPM, fuel %, load %, oil pressure, coolant temperature, cumulative run hours, and the active alarm bitmask) and folds the results into rolling hourly statistics in RAM. Every poll feeds the three threshold-based alert rules and the alarm-word transition detector, so the host decides locally whether a cycle deserves an event or just goes into the next summary. Queued Notes travel from the host to the Notecard over I²C through the AUX connector on Wireless for OPTA. The host never has to deal with modem AT commands or raw sockets.

Notecard responsibilities. Every Note the host hands off goes into the Notecard's on-device queue, and the Notecard handles the radio side from there. It brings up cellular on the hub.set outbound cadence and flushes anything marked sync:true right away. The NOTE-WBNAW has WiFi hardware, but production deployments leave it unconfigured on purpose, because the facility's WiFi access point is offline during the very utility failures this monitor is there to catch. Environment variables come down the same channel in the other direction. The facility manager can retune thresholds and per-register addresses from Notehub without anyone touching firmware, at least for controller families that expose the required points as individually readable 16-bit registers with compatible units. Vendor-specific scaling, signedness, and 32-bit fields need the production extensions described in Limitations.

Notehub responsibilities. The data lands in Notehub. Events arrive over the Internet through the Notecard's embedded global SIM, Notehub stores every one, and project-level routes send them wherever the operator's downstream system needs them. Fleet-level environment variables let operators retune Modbus register addresses and alert thresholds without reflashing firmware. The shipped firmware does individually addressed 16-bit Modbus reads and doesn't scale register values. Vendor-specific scaling, signed-field handling beyond coolant temperature (which the demo already reads as int16_t), and 32-bit register handling are production extensions, described in Limitations. Smart Fleets are a natural way to organize an installed base by controller family or site.

Routing to the cloud (high level). Notehub can route to HTTP, MQTT, AWS, Azure, GCP, Snowflake, and several other destinations. Route setup depends on your project, and this one doesn't ship a specific downstream endpoint. See the Notehub routing docs for details.

3. Technical Summary

You will have: A first gen_summary.qo Note in Notehub whose data_ok = 1 confirms Modbus connectivity, hourly summaries of generator status, and immediate alerts on controller faults.

Minimum prerequisites: Arduino IDE or arduino-cli v1.3+, a generator controller with an accessible Modbus port (DeepSea, Woodward, Kohler, Caterpillar, Cummins, or equivalent), the baud rate and slave address from the controller's commissioning menu, and a Notehub project.

Time required: 45 minutes for hardware assembly and wiring, 15 minutes for firmware and Notehub setup, and 5 minutes for first validation. The first event arrives in about an hour (one Modbus poll plus one report boundary).

Steps:

  1. Assemble hardware: Mount the OPTA RS485 and Wireless for OPTA on the DIN rail, run the antenna through a cable gland, and power the pair from the generator panel's battery-backed 12–24 VDC bus (fused at 3 A, per Wiring and Assembly Step 2).

  2. Wire Modbus: Connect OPTA A/B/COM to controller A/B/COM, with 120 Ω terminators at both ends (OPTA and controller). Use shielded, RS-485-rated twisted pair.

  3. Flash firmware: Copy the PRODUCT_UID from your Notehub project settings and paste it into diesel_gen_monitor.ino at line 39. Compile and upload with the Arduino IDE, or run:

    arduino-cli core install "Arduino Mbed OS Opta Boards"
    arduino-cli lib install "Blues Wireless Notecard" "ArduinoModbus" "ArduinoRS485"
    sed -i '' 's|com.your-company.your-name:diesel_gen_monitor|YOUR_PRODUCT_UID|g' firmware/diesel_gen_monitor/diesel_gen_monitor.ino
    arduino-cli compile --fqbn arduino:mbed_opta:opta firmware/diesel_gen_monitor/ --upload
    note

    The Finder OPTA Codesys ships running the CODESYS runtime. The first time you upload Arduino firmware to it, you have to put it into bootloader mode yourself by double-tapping its RESET button, as described in the Wireless for OPTA Quickstart. That upload replaces the CODESYS runtime, and later uploads don't need the double tap.

  4. Set Modbus register addresses: In Notehub, go to Fleet → Environment. Add reg_engine_rpm, reg_fuel_pct, reg_load_pct, reg_oil_kpa, reg_coolant_c, reg_run_hours, and reg_alarm_word, using the correct addresses from your controller's Modbus map. Set modbus_baud, modbus_slave_id, modbus_parity, and modbus_stop_bits to match the controller's configuration. The device picks these up on its next inbound sync, which is 120 minutes by default. To test right away, set inbound to 1 minute in hub.set on the device.

  5. Validate: Open Notehub and wait for the device to show up in the project. After the first Modbus poll (1 minute) and the first report boundary (60 minutes), you'll see a gen_summary.qo Note with data_ok = 1, fuel level, alarm word, and run status. If every Note shows data_ok = 0, check the wiring, register addresses, baud rate, and slave address against the controller's commissioning menu (see "Modbus first-light" in Validation and Testing).

Sample Note (gen_summary.qo): Here's a sample Note from this device:

{
  "file": "gen_summary.qo",
  "body": {
    "data_ok": 1,
    "fuel_pct": 78.0,
    "load_pct": -1.0,
    "load_pct_peak": -1.0,
    "oil_kpa_mean": -1.0,
    "oil_kpa_peak": -1.0,
    "coolant_c_mean": 24,
    "coolant_c_peak": 24,
    "run_hours": 1847,
    "run_min": 0,
    "stop_min": 60,
    "engine_starts": 0,
    "alarm_word": 0,
    "alarm_word_stale": 0,
    "samples_ok": 60,
    "samples_failed": 0
  }
}

Firmware-observed alarm chronology. The firmware polls the controller's active alarm bitmask once per sample_minutes (1 minute by default). It fires a controller_alarm event the first time it sees an assertion, either on a zero→non-zero transition or at the first valid poll after boot if those bits are already set. Every distinct alarm-word transition goes into an 8-slot on-device ring buffer (AlarmHistoryEntry): the initial assertion, any change while the word stays non-zero (bits added or cleared while at least one fault is still active), and the final clearance to zero. The buffer is flushed as gen_alarm_log.qo at every report boundary, which gives operations teams a fault-set chronology for each window. A transient fault that asserts and clears entirely between two consecutive polls can still slip through. Reading the controller's own internal timestamped alarm log (vendor-specific multi-register reads that may cover events from before the monitor was installed) is a production extension. See Limitations.

4. Hardware Requirements

PartQtyRationale
Finder OPTA Codesys (8A.04.9.024.832C)1Industrial PLC with an onboard RS-485 transceiver, DIN-rail mount, and 12–24 VDC supply input, included in the Wireless for OPTA Bundle. For this project it runs Arduino firmware that hosts the Modbus client and all the edge logic. The Arduino Opta RS485 and Arduino Opta WiFi also work, with no firmware changes.
Blues Wireless for OPTA (NA, SKU 992-00155-C)1Snaps onto the OPTA's right-hand expansion port and adds a Notecard Cell+WiFi (NOTE-WBNAW) over I²C. Cellular coverage is regional, so pick the SKU that matches where you're deploying (the EMEA variant is SKU 992-00156-C).
External cellular antenna, SMA, ~3m lead (e.g. SparkFun CEL-16432)1 required, 2 recommendedRoute the primary cellular antenna through a cable gland to the outside of the metal generator enclosure, because rubber-duck antennas inside a steel cabinet can't reliably hold a cellular session. A diversity antenna on the second port improves LTE Cat-1 performance at sites with marginal signal.
Blues Mojo1Coulomb counter for commissioning. Splice it inline between the power supply and the Wireless for OPTA input to check Notecard energy per cellular session. It's bench-only and isn't deployed to the field.
24 VDC DIN-rail supply, ≥10W (e.g. MeanWell HDR-15-24)1For bench testing only. In the field, power the OPTA and expansion from the generator panel's existing battery-backed DC control bus. The Wireless for OPTA Quickstart shows 12–24 VDC going to the OPTA's power terminals with that same rail jumped to the expansion's power input, which confirms the full 12–24 V range works end to end without a step-up converter.
120 Ω termination resistor1–2RS-485 termination at each physical end of the bus. Place one at the OPTA and one at the controller.
Shielded twisted pair, 22 AWG, RS-485 rated, 1–3 m1A → A, B → B, shield → controller signal ground between the OPTA and the generator controller. Length depends on the panel layout.
DIN rail section, ~15 cm1Mount for the OPTA and expansion. The generator panel probably has DIN rail already.
Inline fuse holder with 3 A ATO/ATC blade fuse1Required field-installation safety item. Wire it in series with the positive supply lead from the battery-backed DC control bus, ahead of the OPTA's + terminal (see Wiring and Assembly Step 2). It protects the control bus if the monitoring hardware develops a sustained hard fault. 3 A covers the worst-case combined draw of the OPTA host (≤183 mA continuous at 12 V) plus the Notecard expansion's peak LTE burst current at the bus supply input. Validation and Testing explains how to verify the fuse size during commissioning. If the panel already takes DIN-rail accessories on that supply branch, a DIN-rail fused terminal block (such as Phoenix Contact or Weidmüller series) is a cleaner option.

The Blues hardware ships with an active SIM that includes 500 MB of data and 10 years of service, with no activation fees and no monthly commitment.

5. Wiring and Assembly

Wiring: OPTA RS485 + Wireless for OPTA on DIN rail; RS-485 A/B/COM to controller with 120 Ω terminators at each end; antennas through cable glands; powered from panel's battery-backed DC bus

warning

Safety. Generator control panels contain hazardous voltages even when the generator is stopped. Installation must be done by qualified personnel following site lockout/tagout procedures, the generator and controller manufacturer's instructions, and applicable electrical codes. This reference design is read-only over Modbus. It never commands start/stop or changes any generator setpoint.

  1. Mount. Snap the OPTA onto the DIN rail, then snap Blues Wireless for OPTA onto the OPTA's right-hand expansion port. Join the two with the supplied solderless AUX connector, which carries the I²C lines the Notecard uses. As the Wireless for OPTA Quickstart points out, USB-C doesn't power the expansion, so use the 12–24 VDC supply rail for any testing beyond the bench.

  2. Power. In the field, install the 3 A inline fuse holder (see Hardware Requirements) in series with the positive supply lead from the battery-backed DC control bus before it reaches the OPTA's + terminal. That fused branch protects the control bus if the monitoring hardware develops a sustained hard fault. Wire the OPTA's + and − terminals to the generator panel's battery-backed DC control bus (12 or 24 VDC, whichever the installation uses), with the fuse in the positive lead. The engine start battery keeps this bus live when mains power fails, so the monitor still has its independent communication path when the stakes are highest. Jump the same fused supply rail to the expansion's power input terminal so Wireless for OPTA shares the fused branch. The Wireless for OPTA Quickstart documents this exact wiring pattern for 12–24 VDC supplies. For bench testing, wire the 24 VDC DIN-rail supply's +V and GND outputs to the OPTA's + and − terminals the same way, then power the supply from mains.

  3. Antennas. Thread an SMA-female bulkhead lead through a cable gland for the primary cellular antenna and tighten it onto the first antenna port on Wireless for OPTA. If the enclosure layout allows, add a second lead for the diversity antenna. It makes a real difference to LTE Cat-1 performance in generator rooms deep inside buildings. The bundled rubber-duck antennas are for the bench only, since a steel enclosure kills their signal.

  4. Modbus RS-485 bus. Wire the OPTA's RS-485 terminals to the generator controller's communication port:

    • OPTA A (+) → Controller A (+) / D+ / TxD+ (the name varies by vendor)
    • OPTA B (−) → Controller B (−) / D− / TxD−
    • OPTA COM (RS-485 GND) → the controller's documented RS-485 signal common or ground reference (check the controller's wiring diagram). Handle the cable shield/drain separately, following the controller vendor's grounding guidance, and don't assume it can stand in for signal common.
    • Put a 120 Ω resistor across A/B at each physical end of the cable run, one at the OPTA and one at the controller. With one OPTA and one controller, that's two terminators total. The controller's Modbus commissioning guide lists its terminal names. DeepSea 7000-series uses +D and −D, and Woodward EasyGen labels them RS485+ and RS485−.
  5. Controller Modbus configuration. Configure the controller as a Modbus RTU server (slave), with the OPTA as the client (master). In the controller's Modbus setup menu, match the baud rate, parity, stop bits, and slave address to the firmware defaults (19200 / none / 1 / slave 1), or override the defaults with the modbus_* environment variables on Notehub.

  6. Bench validation. During first-light testing, splice the Mojo inline between the 24 VDC supply and the Wireless for OPTA power input so it measures the per-session energy of the expansion + Notecard subsystem.

6. Notehub Setup

  1. Create a project. Sign up at notehub.io and create a project. Then open the project dashboard and find the ProductUID in the top-right corner (it looks like com.your-company.your-name:project). Copy it and paste it into firmware/diesel_gen_monitor/diesel_gen_monitor.ino at line 39, replacing com.your-company.your-name:diesel_gen_monitor.

  2. Claim the Notecard. Power up the panel. On its first cellular session, the Notecard provisions itself into your project.

  3. Create a Fleet per controller family. Fleets group devices for shared configuration and routing. Register addresses and alarm bitmasks differ from one generator controller brand to the next, so one fleet per controller vendor works well: one for DeepSea 7000-series generators, another for Woodward EasyGen, and so on. Fleet-level environment variables hold the register addresses and thresholds for that controller family, and a device can override them with its own values if a site is set up unusually. Smart Fleets can move devices between fleets automatically based on a device-level tag or environment variable.

  4. Set environment variables. Every variable below is optional, and the table shows the firmware defaults. Values you set in Notehub (Fleet → Environment Variables) override the compile-time defaults on the device's next inbound sync, with no reflashing.

    VariableDefaultPurpose
    sample_minutes1Minutes between Modbus polls.
    report_minutes60Minutes between summary Notes (gen_summary.qo).
    modbus_slave_id1Modbus server (slave) address of the generator controller.
    modbus_baud19200Bus baud rate. Must match the controller's configuration.
    modbus_paritynoneParity setting: none, even, or odd.
    modbus_stop_bits1Stop bits: 1 or 2.
    reg_engine_rpm768Holding-register address for engine speed (RPM).
    reg_fuel_pct769Holding-register address for fuel level (0–100%).
    reg_load_pct770Holding-register address for generator load (0–100%).
    reg_oil_kpa771Holding-register address for oil pressure (kPa).
    reg_coolant_c772Holding-register address for coolant temperature (°C, signed).
    reg_run_hours773Holding-register address for cumulative engine hours.
    reg_alarm_word774Holding-register address for the active alarm bitmask.
    fuel_low_pct25.0Fuel level (%) below which fuel_low fires.
    coolant_alarm_c95.0Coolant temperature (°C) above which coolant_overtemp fires.
    oil_low_kpa138.0Oil pressure (kPa, ≈20 psi) below which oil_low_pressure fires while running.
    alarm_mask_fts0Bitmask applied to alarm_word to detect failure-to-start events. 0 disables it. Set it to the decimal value of your controller's FTS bitmask (e.g. 1 for bit 0, 4 for bit 2). 0x-prefixed hex also works (e.g. 0x0001).
    rpm_running100Engine RPM at or above which the engine counts as running, for separating stats and evaluating oil pressure.

    Controller register-map gotchas. The register-address defaults are only examples, based on a made-up contiguous map. Real controllers differ in 0-based vs 1-based addressing, per-register scaling (oil pressure may be in 0.1 bar, tenths of kPa, or raw psi depending on the controller and configuration), signedness (coolant temperature is often signed 16-bit), run hours stored as a 32-bit value across two consecutive registers with vendor-specific word order, and whether a register holds active alarms or a latched alarm history. See Limitations for the production path.

  5. Configure routes. Add one route for gen_event.qo (low-volume real-time alerts bound for on-call paging or a CMMS, computerized maintenance management system) and a second for gen_summary.qo (long-term storage and trend analysis). Send gen_alarm_log.qo to the same real-time destination as gen_event.qo. The two share urgency, since fault chronology belongs next to alert notifications, but gen_alarm_log.qo rides along with the periodic outbound sync instead of opening its own immediate cellular session. Keeping the three Notefiles separate at the source means each one can go to its own destination at its own urgency, with no filter logic in the route.

7. Firmware Design

The firmware is one Arduino sketch, firmware/diesel_gen_monitor/diesel_gen_monitor.ino, with the Modbus, Notecard, and statistics helpers split out into diesel_gen_monitor_helpers.h and diesel_gen_monitor_helpers.cpp to keep the orchestration in .ino readable.

Dependencies:

  • Arduino Mbed OS Opta Boards core (install it from the Arduino IDE Boards Manager).
  • Blues Wireless Notecard (the note-arduino library). Install it from the Arduino Library Manager or with arduino-cli lib install "Blues Wireless Notecard". The note-arduino releases page lists the latest version.
  • ArduinoModbus and ArduinoRS485 (official Arduino libraries, also from the Library Manager).

Modules

ResponsibilityWhere
Notecard hub.set, template definitionnotecardConfigure, defineTemplates (.ino)
Environment-variable fetch and clampingfetchEnvOverrides (_helpers.cpp)
Modbus serial re-init on env changeapplyModbusSerialIfChanged (_helpers.cpp)
Hub cadence re-sync on env changeapplyHubSetIfChanged (_helpers.cpp)
Seven-register Modbus poll with retrypollGenerator, modbusReadOne (_helpers.cpp)
Rolling hourly statisticsRollingStats struct, accumulate (_helpers.cpp)
Alarm word latch detection, history logging (logAlarmHistory/flushAlarmHistory), start-event countingloop() (.ino)
Three threshold rules + edge triggerevaluateRules (_helpers.cpp)
Immediate-sync alert emissionsendEvent (.ino)
Hourly templated summarysendSummary (.ino)
Millis-based periodic scheduler (no sleep, since the host runs continuously on the battery-backed bus)loop() (.ino)

Sensor reading strategy

The firmware reads the seven holding registers one at a time with modbusReadOne(). Pump VFDs often keep their registers contiguous, so you can read them in a single burst. Generator controller Modbus maps vary so much from vendor to vendor that individual reads with configurable addresses are the only reliable approach. All seven reads at 19200 baud take under 500 milliseconds, which is negligible at a one-minute polling cadence.

A sample only counts as valid if all seven reads succeed in a single attempt. A partial success would look exactly like valid data with wrong values, and that's worse than having no data. If any register read fails, the firmware starts the set over, for up to three attempts in all, then declares the poll a failure and emits a modbus_unreachable event.

Event payload design

The device writes two template-backed Notefiles. Templates store records as fixed-length binary instead of free-form JSON, which shrinks the on-wire payload 3–5×. A fleet of 50 generators sending hourly summaries on a prepaid SIM with a finite data budget can't do without that compression. Notehub decodes the template binary and shows it as JSON in your browser and in API responses.

gen_summary.qo (periodic, hourly by default). This example is a standby window where the engine was stopped for the whole hour. It's the decoded JSON you'll see in the Notehub dashboard or when you pull Notes through the API:

{
  "file": "gen_summary.qo",
  "body": {
    "data_ok": 1,
    "fuel_pct": 78.0,
    "load_pct": -1.0,
    "load_pct_peak": -1.0,
    "oil_kpa_mean": -1.0,
    "oil_kpa_peak": -1.0,
    "coolant_c_mean": 24,
    "coolant_c_peak": 24,
    "run_hours": 1847,
    "run_min": 0,
    "stop_min": 60,
    "engine_starts": 0,
    "alarm_word": 0,
    "alarm_word_stale": 0,
    "samples_ok": 60,
    "samples_failed": 0
  }
}

Schema Notes for downstream integrators:

  • data_ok: the primary validity flag. 1 means at least one Modbus poll succeeded this window, and all measurement fields are valid. 0 means a complete telemetry blackout (all samples_failed, no controller contact). When data_ok = 0, treat every computed measurement field (fuel_pct, load_pct*, oil_kpa*, coolant_c*, run_min, stop_min) as undefined. alarm_word and run_hours work differently. A zero there would look the same as "no alarms" or "zero hours," so the firmware carries forward the last-known values from before the blackout and sets alarm_word_stale = 1 (see below).
  • alarm_word_stale: 0 under normal operation (data_ok = 1), meaning alarm_word comes from this window's Modbus polls. 1 when data_ok = 0, meaning alarm_word is the last-known value from before the blackout (the firmware preserves it across the stats reset) and run_hours is the last successfully polled reading. Use alarm_word_stale to tell "no active alarms this window" apart from "controller was unreachable; alarm state unknown."
  • -1.0 / -1 sentinel: shows up in running-only fields (load_pct, load_pct_peak, oil_kpa_mean, oil_kpa_peak) whenever the engine produced no running-state samples in the window, meaning the generator was stopped the whole hour. These fields only accumulate while the engine is spinning, so a 0.0 would look the same as a true zero reading. When data_ok = 0, the sentinel extends to every computed measurement field, including fuel_pct and coolant_c_*.
  • samples_ok: the number of successful Modbus polls in the window (run_samples + stop_samples). Together with samples_failed, it lets downstream analytics tell "generator stopped, telemetry healthy" apart from "controller unreachable or wiring fault."
  • samples_failed: the number of polls in the window where all three Modbus retry attempts failed. A non-zero value here alongside run_min = 0 is the main sign that monitoring coverage was degraded, rather than the generator just sitting idle.

gen_event.qo (immediate, sync:true). The two variants below, a per-poll alert and a report-window alert, show how trigger_val / trigger_threshold differ:

Per-poll alert (failure_to_start): the fresh sample already explains the trigger, so trigger_val and trigger_threshold are -1.0:

{
  "file": "gen_event.qo",
  "body": {
    "alert": "failure_to_start",
    "engine_rpm": 0,
    "fuel_pct": 82.0,
    "load_pct": 0.0,
    "oil_kpa": 0.0,
    "coolant_c": 22,
    "alarm_word": 1,
    "run_hours": 1847,
    "trigger_val": -1.0,
    "trigger_threshold": -1.0
  }
}

Report-window alert (coolant_overtemp): trigger_val is the window peak that crossed the threshold, and coolant_c is the last-known sample reading, which may have dropped back down by report time:

{
  "file": "gen_event.qo",
  "body": {
    "alert": "coolant_overtemp",
    "engine_rpm": 1500,
    "fuel_pct": 78.0,
    "load_pct": 85.0,
    "oil_kpa": 312.0,
    "coolant_c": 88,
    "alarm_word": 0,
    "run_hours": 1849,
    "trigger_val": 97.0,
    "trigger_threshold": 95.0
  }
}

gen_alarm_log.qo (once per report_minutes when any alarm event occurred, batched with the periodic outbound sync, not template-backed):

{
  "file": "gen_alarm_log.qo",
  "body": {
    "count": 2,
    "events": [
      { "alert": "controller_alarm", "alarm_word": 4, "elapsed_s": 180 },
      { "alert": "alarm_clear",      "alarm_word": 0, "elapsed_s": 420 }
    ]
  }
}

Schema Notes for gen_event.qo downstream integrators:

  • trigger_val / trigger_threshold: present in every gen_event.qo. For report-window rules (fuel_low, coolant_overtemp, oil_low_pressure), trigger_val is the window aggregate or peak that crossed the configured limit (trigger_threshold). For per-poll alerts (controller_alarm, failure_to_start, modbus_unreachable), both fields are -1.0, because the sample fields already explain the trigger.
  • The sample fields (engine_rpm, fuel_pct, load_pct, oil_kpa, coolant_c) carry the last-known polled reading at event time. For per-poll alerts that's the reading closest to the trigger, and for report-window alerts it's context on the machine's state. With coolant_overtemp, coolant_c may be below the threshold at report time while trigger_val (the window peak) is above it, so always use trigger_val to confirm the threshold crossing.

Schema Notes for gen_alarm_log.qo downstream integrators:

  • count: the number of entries in this flush (1–8). The ring buffer holds 8 entries, so if more than 8 alarm transitions happen in one report window, the oldest ones get overwritten.
  • events[].alert: one of "controller_alarm", "alarm_clear", "failure_to_start", or "fts_clear". Assertions (controller_alarm, failure_to_start) include the asserted alarm_word, and clearances (alarm_clear, fts_clear) carry alarm_word: 0. One flush can hold several controller_alarm entries with different alarm_word values if the active fault set changes while at least one bit stays set. That happens when a second fault asserts before the first clears, for example, or when one of several active faults clears before the others. Each entry records the exact bitmask at that poll. Read them in elapsed_s order to get the full fault-set chronology for the window.
  • events[].elapsed_s: seconds since device boot (millis()/1000) at event time. Use the Note timestamp from Notehub for wall-clock time. elapsed_s gives the relative timing between entries within one flush.
  • The firmware only emits gen_alarm_log.qo when at least one alarm event happened in the window, so a window with no alarm transitions produces no Note. Route it to the same real-time channel as gen_event.qo for a complete fault chronology.

Sync and power strategy

The OPTA and expansion draw continuously from the generator panel's battery-backed DC control bus, so the main design concerns here are bus efficiency and data budget, not host MCU sleep. The Notecard runs in hub.set periodic mode with outbound equal to report_minutes (60 minutes by default) and inbound at twice that. Summary Notes collect in the on-device queue and go out together in one cellular session per hour. Alert Notes set sync:true and go out as soon as a session comes up, usually 15–60 seconds after the firmware detects the trigger condition, no matter where the outbound timer stands.

Battery autonomy tradeoff. The always-on OPTA host is the biggest continuous load on a battery whose main jobs are starting the engine and keeping the control circuits alive during a mains failure. The OPTA RS485 draws 0.6–2.2 W at its supply voltage, which works out to roughly 50–183 mA at 12 V or 25–92 mA at 24 V. With no charge input, a 40 Ah start battery at the worst-case 12 V draw (~183 mA) runs the monitor for about 218 hours (~9 days). At the best-case draw, the same battery lasts roughly 800 hours, and a 100 Ah battery scales proportionally. In most installations the panel's trickle charger keeps the battery topped up and every generator run adds more charge, so the monitor's draw alone is unlikely to drain a healthy battery under normal site conditions. If your site expects multi-day utility outages with no generator exercise or panel charging, though, measure the actual host current (see Validation) and compare it to the site's rated battery capacity. Limitations has production sizing guidance.

Retry and error handling

  • Each Modbus poll gets up to 3 attempts. If all three fail, the firmware skips the sample and emits a modbus_unreachable event Note right away, so wiring problems show up at first light during commissioning. After that it rate-limits modbus_unreachable events to once an hour, because a generator powered down for scheduled service shouldn't flood the event log.
  • Alarm-word monitoring uses per-alert latch flags instead of comparing raw alarm words. A fault that stays asserted for hours fires a single controller_alarm event. The latch blocks repeats while the condition persists and rearms only when the word clears. Generator monitoring falls apart once alarm fatigue sets in and people start ignoring the alerts.
  • fetchEnvOverrides() runs on every sample interval (every 1 minute by default) and again at every report boundary. It re-reads the Notecard's local environment-variable cache, but that cache only refreshes when Notehub delivers an inbound sync. The Notecard's inbound cadence is set to report_minutes * 2 (120 minutes at the default report_minutes = 60), so a change you make in Notehub can take up to one full inbound interval (120 minutes by default) to reach the device. Once an inbound sync updates the cache, the new values take effect within one sample period. The one exception is sample_minutes. A new value goes into g_pending_sample_minutes and only becomes the active cadence when the current window closes with a confirmed note.add, so run_min / stop_min always reflect one consistent cadence per window. At the report boundary, applyHubSetIfChanged() re-issues hub.set if report_minutes changed, which keeps the Notecard's outbound cadence in step with the local summary cadence.

Key code snippet 1: latch-based alarm detection, retry-on-send-failure, and alarm history logging

The firmware checks the alarm register on every successful poll. Detection is latch-based rather than transition-gated. The firmware evaluates the condition against the per-alert latch flags (g_active_controller_alarm, g_active_fts) instead of looking for a change in the stored alarm word. If sendEvent() fails because of a transient I²C hiccup or a Notecard-side error, the latch stays false, and the alert retries automatically on every later sample while the fault is still asserted. Once the Note is queued, the latch blocks re-firing, so a sustained fault still emits just one event per asserted-alarm period. g_current_alarm_word updates on every sample no matter what the latch state is, so the summary Note always carries the freshest alarm word.

History logging records every distinct alarm_word value, not just the initial assertion. A separate g_alarm_logged_controller / g_alarm_logged_fts flag tracks whether the first entry for an assertion period has been written, and the initial detection passes through that gate exactly once. While the alarm word stays non-zero, any later poll that returns a different nonzero value gets logged right away as another controller_alarm entry with the updated bitmask. That covers bits added when a second fault asserts and bits cleared while others persist. These mid-nonzero changes don't trigger a new sendEvent(), because one alert per assertion period keeps a slowly evolving fault set from causing alarm fatigue.

Keeping the history-logged state separate from the event-queued state matters when sendEvent() fails. The latch stays false, and the next retry calls sendEvent() again but does not re-log the initial entry. Without that separation, send retries would pile up duplicate entries, which would corrupt the chronology and push later events out of the 8-slot buffer. The matching clearance (alarm_clear / fts_clear) is logged once when the alarm word goes back to zero, or for fts_clear, when the masked FTS bits clear. It's gated on the history flag instead of the event-queued latch, so a clearance entry always pairs with its assertion, even if the Note never queued successfully.

Boot-seeding behavior. On the first valid poll after a boot or watchdog reset, the firmware checks the alarm word right away. If those bits are already asserted, it emits controller_alarm (and failure_to_start if alarm_mask_fts is configured) without waiting for a zero-to-nonzero transition that may never come during this boot session. Otherwise, a monitor that rebooted mid-outage would stay quiet about a fault that was already there. The boot-seeding block calls logAlarmHistory() and sets the matching g_alarm_logged_* flag, so the main detection loop won't add a second history entry for the same assertion period if sendEvent() fails at boot.

// Three-state detection: 0→nonzero (assertion), nonzero→different-nonzero
// (fault-set change while still alarming), and nonzero→0 (clearance).
// ...
if (s.alarm_word != 0 && !g_active_controller_alarm) {
    // 0→nonzero: first detection of this assertion period, OR a retry
    // of a previously-failed send while the assertion is still active.
    // ...
    if (!g_alarm_logged_controller || s.alarm_word != g_current_alarm_word) {
        logAlarmHistory("controller_alarm", s.alarm_word);
        g_alarm_logged_controller = true;
    }
    if (sendEvent("controller_alarm", &s)) {
        g_active_controller_alarm = true;
    }
} else if (s.alarm_word != 0 && g_active_controller_alarm &&
           s.alarm_word != g_current_alarm_word) {
    // nonzero→different-nonzero: fault bits changed while at least one
    // remains asserted (e.g. a second fault added, or one of several
    // cleared while others persist). Log the updated alarm_word to preserve
    // the fault-set chronology. No sendEvent() — one alert per assertion
    // period. g_alarm_logged_controller stays true so the final clearance
    // entry is still written when the word eventually reaches 0.
    logAlarmHistory("controller_alarm", s.alarm_word);
} else if (s.alarm_word == 0) {
    // nonzero→0: all alarms cleared. Log the clearance once, gated on
    // g_alarm_logged_controller so the clear entry is written even when
    // sendEvent() never succeeded for this assertion period.
    if (g_alarm_logged_controller) {
        logAlarmHistory("alarm_clear", 0);
    }
    g_active_controller_alarm = false;  // all alarms cleared; rearm
    g_alarm_logged_controller = false;  // rearm history flag for next assertion
}

// ...
if (g_alarm_mask_fts != 0) {
    bool fts_active = ((s.alarm_word & g_alarm_mask_fts) != 0);
    if (fts_active && !g_active_fts) {
        // Log once on first detection; subsequent send retries do not re-log.
        if (!g_alarm_logged_fts) {
            logAlarmHistory("failure_to_start", s.alarm_word);
            g_alarm_logged_fts = true;
        }
        if (sendEvent("failure_to_start", &s)) {
            g_active_fts = true;
        }
    } else if (!fts_active) {
        // Log the clearance once; gated on g_alarm_logged_fts so it fires
        // even when sendEvent() never succeeded for this assertion period.
        if (g_alarm_logged_fts) {
            logAlarmHistory("fts_clear", 0);
        }
        g_active_fts = false;   // rearm when FTS bits clear
        g_alarm_logged_fts = false;
    }
}

Key code snippet 2: immediate-sync alert with trigger fields

sync:true tells the Notecard to skip the outbound interval and open a cellular session immediately. Where the sample comes from, and what goes in the trigger fields, depends on the event type:

  • controller_alarm and failure_to_start pass the freshly polled sample directly, since it's the closest reading to the moment the alarm was detected. trigger_val and trigger_threshold default to -1.0 (not applicable).
  • modbus_unreachable carries g_last_known_sample when a valid prior sample exists, or null/zero fields if no poll has succeeded yet.
  • Report-boundary threshold rules (fuel_low, coolant_overtemp, oil_low_pressure) pass g_last_known_sample as machine-state context plus trigger_val (the window aggregate or peak that crossed the threshold) and trigger_threshold (the configured limit). With both fields, every report-window alert explains itself. A coolant_overtemp event shows trigger_val: 97.0 and trigger_threshold: 95.0 even when the last-known coolant_c sample has already dropped below 95°C.

sendEvent() makes up to three attempts. It uses requestAndResponse() so it can check each response for an error, and it returns true only once the Notecard accepts the Note. The callers set their latch flags on that true, which is how a failed send gets retried on the next sample or report window.

bool sendEvent(const char *alert, const GenSample *s,
               float trigger_val, float trigger_threshold) {
    const uint8_t MAX_TRIES = 3;
    for (uint8_t attempt = 0; attempt < MAX_TRIES; attempt++) {
        J *req = notecard.newRequest("note.add");
        if (!req) { delay(100); continue; }
        JAddStringToObject(req, "file", "gen_event.qo");
        JAddBoolToObject  (req, "sync", true);
        J *body = JAddObjectToObject(req, "body");
        JAddStringToObject(body, "alert",             alert);
        JAddNumberToObject(body, "engine_rpm",        s ? s->engine_rpm : 0);
        JAddNumberToObject(body, "fuel_pct",          s ? s->fuel_pct   : 0.0f);
        // ...
        JAddNumberToObject(body, "alarm_word",        s ? s->alarm_word : g_current_alarm_word);
        JAddNumberToObject(body, "run_hours",         s ? s->run_hours  : stats.run_hours_last);
        JAddNumberToObject(body, "trigger_val",       trigger_val);
        JAddNumberToObject(body, "trigger_threshold", trigger_threshold);
        J *rsp = notecard.requestAndResponse(req);
        bool ok = rsp && !notecard.responseError(rsp);
        notecard.deleteResponse(rsp);
        if (ok) return true;
        // ...
        delay(200);
    }
    // ...
    return false;
}

Key code snippet 3: oil-pressure rule (running-only, with trigger fields)

Oil pressure only means something when the engine is spinning. If the engine produced no running-state samples in the window, the rule is skipped entirely, which prevents a steady stream of false positives from a generator that sits in standby for days at a time. trigger_val carries the window mean that crossed the threshold, and trigger_threshold is the configured limit. The last-known sample fields give context on the machine's current state.

if (stats.run_samples > 0) {
    float oil_mean = stats.oil_sum / (float)stats.run_samples;
    if (oil_mean < g_oil_low_kpa) {
        if (!g_active_oil_low) {
            if (sendEvent("oil_low_pressure", &g_last_known_sample,
                          oil_mean, g_oil_low_kpa)) {
                g_active_oil_low = true;
            }
        }
    } else {
        g_active_oil_low = false;
    }
}

8. Data Flow

Data flow: poll 7 holding registers → per-poll alarm/FTS/Modbus rules (immediate) and report-window threshold rules (hourly) → gen_event.qo (sync:true) and gen_summary.qo (hourly templated) → Notehub

Collected. Every sample_minutes (1 minute by default): engine RPM, fuel level %, generator load %, oil pressure kPa, coolant temperature °C, cumulative run hours, and the active alarm bitmask. Each valid poll also updates the running/stopped sample counts, and an RPM threshold crossing bumps the engine-start counter.

Summarized. Every report_minutes (60 minutes by default): mean fuel level, mean and peak load and oil pressure (running-state samples only), mean and peak coolant temperature, cumulative run hours, minutes running and stopped this window, engine start count, and the latest alarm word polled in the window. That alarm word is non-zero whenever any alarm was active at the final sample in the window, so a fault that lasts across a report boundary also shows up as non-zero in the next summary.

Transmitted.

  • gen_summary.qo: once per report_minutes, queued and batched with the Notecard's periodic outbound sync. It's template-encoded to keep the on-wire size small.
  • gen_event.qo: sent immediately with sync:true whenever a rule trips. You get one event per condition transition, not one per sample interval.
  • gen_alarm_log.qo: once per report_minutes if any alarm-word transition (assertion or clearance) happened in the window. It's full JSON instead of template-encoded, because the array length varies, and it's batched with the periodic outbound sync. Windows with no alarm activity don't produce one.

Routed. Notehub sends gen_event.qo and gen_alarm_log.qo to whatever real-time channel the operator uses (CMMS tickets, on-call paging, a building management system webhook, and so on). gen_event.qo arrives immediately because of sync:true, while gen_alarm_log.qo rides the next periodic outbound sync and fills in the per-window alarm chronology behind the event stream. Point both at the same downstream destination so fault chronology and event notifications end up in one place. gen_summary.qo routes separately to a long-term store for fleet trend analysis.

Triggers. Six conditions generate alert events, and they fall into two timing groups.

Per-poll alerts are evaluated on every successful Modbus read, so their latency is about one sample_minutes interval (1 minute by default):

  • controller_alarm: the active alarm word is seen non-zero for the first time, either on a zero→non-zero transition or at the first valid poll after boot if it's already asserted. One event fires per asserted-alarm period, and the flag rearms when the alarm word returns to zero. It catches any fault the controller's alarm map defines, including failure-to-start, under/over voltage, over-speed, high temperature, and low coolant level. Every assertion and clearance also goes into the local alarm-history ring buffer and gets flushed as gen_alarm_log.qo.
  • failure_to_start: a subset of controller_alarm that watches the specific bit(s) the operator sets in alarm_mask_fts. It's disabled by default, so configure it for each controller family.
  • modbus_unreachable: all three Modbus retry attempts failed. The first occurrence fires immediately, and later ones are rate-limited to once an hour.

Report-window alerts are evaluated once per report_minutes (60 minutes by default) against the window's rolling aggregates, so by default they can lag the triggering condition by up to one full report interval:

  • fuel_low: window-mean fuel level below fuel_low_pct. It's edge-triggered and rearms when fuel rises back above the threshold (after a refill, for example).
  • coolant_overtemp: window-peak coolant temperature above coolant_alarm_c. It uses the peak instead of the mean because an overheat can spike and resolve within a single report window.
  • oil_low_pressure: window-mean oil pressure below oil_low_kpa while the engine was running. It's only evaluated when run_samples > 0.

9. Validation and Testing

Expected steady-state behavior. A correctly commissioned generator in standby should produce one gen_summary.qo event per hour with run_min = 0, engine_starts = 0, and alarm_word = 0. A weekly exercise test usually runs about 30 minutes. The summary windows that straddle it will show non-zero run_min, engine_starts = 1 in the window that caught the start, and non-zero load_pct and oil_kpa_mean. The main validation step is checking for that pattern during the first scheduled test after installation.

Modbus first-light. Before you connect to the real controller, run the firmware against a software Modbus simulator (Modbus Mechanic, ModRSsim2, or similar) through a USB-to-RS-485 adapter wired to the OPTA's RS-485 terminals. Confirm the seven register reads hit the expected addresses. Keep in mind that the firmware's serial output only shows Modbus retry failures and Notecard debug traffic, and it doesn't print decoded register values. To see register contents during bench bring-up, either add temporary Serial.print statements inside pollGenerator() or check the data in the gen_summary.qo Notes that show up in Notehub after the first report boundary.

Fault simulation. With a real controller connected, temporarily set fuel_low_pct to 99 (the highest value the firmware accepts) in the fleet's environment variables. The device pulls the new value on the next inbound sync, and the report boundary after that trips fuel_low unless the tank reads 99% or more. Check that the event shows up in Notehub as soon as a session comes up, then restore the original value to rearm.

Power validation with Mojo. Splice the Mojo inline between the 24 VDC supply and the Wireless for OPTA power input, and it reports cumulative mAh at 1% accuracy over its Qwiic connector. In that spot it measures the Wireless for OPTA expansion subsystem (the Notecard plus the expansion's onboard regulators and I²C glue). It does not measure the OPTA host, which draws power straight from the same control bus through its own +/− supply terminals. The Notecard-subsystem figures below come from the NOTE-WBNAW datasheet and the Notecard low-power design guide:

PhaseNotecard subsystem current (Mojo measurement)
Notecard idle (radio off, between syncs)~8–18 µA @ 5V
Modem active (cellular session, LTE Cat-1)~250 mA average, ≤2 A peak bursts
WiFi active (hardware capability; leave unconfigured in production)~80 mA average

The OPTA host runs all the time, because the firmware's millis-based scheduler keeps the Cortex-M7 awake with no sleep state. The OPTA RS485 datasheet specifies 0.6–2.2 W at 24 VDC (about 25–92 mA at 24 V, or roughly 50–183 mA at 12 V). That's the biggest steady-state load on the control bus, and you have to measure it separately. During bench bring-up, splice an ammeter or a second Mojo between the control-bus supply and the OPTA's +/− terminals.

On the Mojo trace, idle current between syncs should sit in the µA range. During hourly sync sessions the modem averages ~250 mA but can spike to ≤2 A in brief RF bursts, which is normal LTE Cat-1 behavior and not a fault. Instead of watching for a hard ceiling on instantaneous current, check energy per session. A healthy hourly sync should add only a few mAh to the Mojo counter, and the idle stretches between syncs should add almost nothing. Alert events (sync:true) open extra modem sessions between the scheduled syncs, each with the same average-then-burst current profile. If the device draws elevated current continuously with no idle periods, the Notecard's periodic mode isn't configured correctly. The Notecard low-power design guide lists the conditions needed to reach the lowest-power idle state.

In the field, the generator's start battery carries the control bus through a mains failure, but it won't last forever. The biggest continuous load is the OPTA host (0.6–2.2 W per the datasheet), and the Notecard expansion's 8–18 µA idle draw barely registers next to it. During commissioning, record the actual OPTA host current with the ammeter or second Mojo described above. Then compare it against the site's rated battery capacity to put a bound on worst-case autonomy, which matters most at 12 V sites with smaller batteries.

The 3 A fuse required in Hardware Requirements and installed per Wiring and Assembly Step 2 is sized for worst-case 12 V operation. The OPTA host draws up to ≤183 mA continuous, and the Notecard expansion can burst to ≤2 A at the bus input, for a combined worst-case total of ≤2.2 A. To check the sizing at your installation, add the measured OPTA supply current to the peak Notecard-expansion burst current (≤2 A) from the Mojo trace, and confirm the total stays below the installed fuse rating. At 24 V sites the bus currents are proportionally lower, which makes the 3 A rating conservative, but don't leave the fuse out.

10. Troubleshooting

Device appears in Notehub but all gen_summary.qo Notes show data_ok = 0

  • Modbus polling failed on every retry. Check that OPTA A/B/COM are wired to the matching terminals on the controller, and use a multimeter to verify continuity.
  • Baud rate mismatch. Compare modbus_baud, modbus_parity, and modbus_stop_bits in Notehub (Fleet → Environment) against the controller's Modbus commissioning menu. They have to match exactly.
  • Slave address mismatch. Make sure modbus_slave_id in Notehub matches the controller's configured address. You'll usually find it in the Modbus or network setup menu, and it defaults to 1.
  • Missing or incorrect termination resistors. Confirm there's a 120 Ω resistor at both physical ends of the RS-485 cable run, one at the OPTA and one at the controller.
  • First-light validation. Before commissioning on the real controller, test with a USB-to-RS-485 adapter and a Modbus simulator (Modbus Mechanic, ModRSsim2) to confirm the firmware's Modbus reads work at all (see Validation and Testing).

Register addresses appear wrong or show −1.0

  • Wrong register addresses. The defaults (768–774) come from a made-up map. Look up your controller vendor and model in its Modbus datasheet, then set reg_engine_rpm, reg_fuel_pct, and the rest in Notehub (Fleet → Environment) to the right addresses for your controller.
  • -1.0 sentinels in running-only fields (load_pct, oil_kpa_mean) mean the engine was stopped for the whole report window, which is normal for a standby generator. A fuel level of -1.0 or data_ok = 0 points to a communication failure, not a sensor problem.

Antenna connectivity issues

  • Rubber-duck antennas don't work inside metal generator enclosures. Route the primary cellular antenna (SMA) through a cable gland to the outside, and add a second diversity antenna if the signal is marginal.
  • Antenna not seated, or damaged. Make sure the SMA connector is fully tightened onto the antenna port.

Alert events not appearing in Notehub

  • Environment variables not applied yet. Changes you make in Notehub take effect on the device's next inbound sync, not right away. The default inbound cadence is 120 minutes. To test quickly, temporarily set it to 1 minute with hub.set on the device or with a direct Notehub API call, and change it back when you're done.
  • Out-of-range or unrealistic rule thresholds. The firmware ignores a value outside its allowed range and keeps the previous one (fuel_low_pct only accepts 1–99, for example). The report-window rules are also edge-triggered, so once fuel_low fires, it won't fire again until the fuel level rises back above the threshold. Check that the thresholds match what your site expects (e.g., fuel_low_pct = 25.0 for "alert when fuel falls below 25%").

11. Limitations and Next Steps

This reference design sticks to one path on purpose: taking a facility manager from a generator with a Modbus port to a generator with a live cellular uplink in well under a day, without writing anything back to the controller. A handful of details were kept simple so that path stays clean. Each one is listed below with the production extension that fills the gap.

Simplified for this reference design

These are the spots where the firmware stays generic on purpose. Once it's running against real controllers, a fleet deployment will want the extension noted for each one.

Register addresses, scaling, and signedness in the defaults describe a made-up contiguous map. Each controller vendor publishes its own Modbus map, so commissioning a real site means looking up the actual addresses, scaling factors (oil pressure may be in 0.1 bar, tenths of kPa, or raw integer psi), signedness, word counts, and addressing convention (0-based wire-level vs 1-based / Modicon "40001" notation). The demo already reads coolant temperature as signed int16_t, but oil pressure and other fields may be signed on some controllers too and need case-by-case handling. Run hours on the DeepSea 7000-series is a 32-bit value spanning two consecutive registers. The shipped firmware reads seven individually addressed 16-bit registers with no scaling, so production builds need vendor-specific handling.

Firmware-observed alarm history, not controller-internal log. The firmware keeps its own alarm-history log. Every alarm assertion and clearance it detects goes into an 8-slot ring buffer that's flushed as gen_alarm_log.qo at each report boundary (see §3 and §7). Reading the controller's own internal timestamped fault log is still a future enhancement. Most production controllers keep a multi-register circular buffer of events, which can include faults from before the monitor was installed, and getting at it takes a vendor-specific read sequence (see Production Next Steps below).

Sampled alarm detection, not latched. The firmware polls the alarm word once per sample_minutes (1 minute by default), so it never sees a transient fault that asserts and clears entirely between two consecutive polls. If a controller doesn't latch faults internally, the polling cadence has to be short enough to catch the briefest fault pulse you expect. If it does latch them, reading the alarm history log (above) is the more reliable option.

Failure-to-start detection requires operator configuration. The alarm_mask_fts variable defaults to 0 (disabled) because bit positions differ across controller families. A DeepSea 7000-series uses different alarm bits than a Woodward EasyGen, for example. You'll need to look up your controller's alarm register map and set the right mask. Until you do, the generic controller_alarm alert catches failure-to-start events.

Single controller per OPTA. The firmware reads one Modbus slave ID, so a facility with several generators needs either one OPTA + Wireless for OPTA per generator or a firmware extension that round-robins across slave IDs on the same bus (with per-slave stat tracking).

Weekly test coverage can be straddled by an hourly summary. A 30-minute test that starts and finishes inside one 60-minute summary window shows up in that window's data, but the start event and any transient fault that clears during the test are only visible in the event Notefile. A production build could emit a summary at engine stop (the end of each run event) to get full run-level detail.

No Modbus writes. The firmware only reads. Sending remote start commands to the generator is out of scope, because that takes a safety analysis, E-stop wiring, and possibly functional-safety certification.

Continuous OPTA host draw on the start battery comes from having no sleep state. The OPTA's Cortex-M7 stays awake the whole time and draws 0.6–2.2 W from the battery-backed control bus. Panel trickle chargers handle that load in normal operation. A site that sees long utility outages without generator exercise or charger input, though, should measure the actual OPTA supply current during commissioning and make sure the battery capacity gives acceptable autonomy. At 12 V and the worst-case 2.2 W draw (~183 mA), a 40 Ah battery keeps the monitor alone running for roughly 9 days. At the best-case draw, the same battery stretches to ~33 days. Larger batteries and 24 V systems extend autonomy proportionally.

Production Next Steps

With a real fleet reporting in, these extensions are how the design grows into a commercial-grade monitoring product.

Vendor-specific register-map builds cover DeepSea 7000/8000, Woodward EasyGen 3000, Kohler RDC, Caterpillar EMCP 4, and Cummins PowerCommand, each with the right addresses, scaling, signedness, and 32-bit run-hour handling for that family.

Controller-internal alarm log readout, following each vendor's spec (DeepSea Event Log registers, Woodward EasyGen fault record, Kohler RDC history block, and so on), adds timestamped events to the firmware-observed history. That includes events from before the monitor was installed and from times the firmware was offline.

Per-controller baseline learning tracks run hours and load history for each device, so it can spot drift toward scheduled service intervals instead of waiting for a threshold breach.

Over-the-air firmware updates through Notecard Outboard Firmware Update would let a service company push a new register-map build to an entire fleet without a truck roll. ODFU on the OPTA needs AUX wiring that Wireless for OPTA doesn't break out today, so for now host firmware changes go through a local USB-C update.

Fuel consumption rate falls out of how fast the fuel level changes across run events. Combine it with load data and you get a fuel-per-kWh efficiency figure that builds up over the maintenance history.

Automatic weekly test verification would flag a test_not_detected event when no engine start shows up in a configurable rolling window, so a skipped test gets caught before an audit or inspection.

12. Summary

The facility manager used to find out about a failed-to-start generator from a phone call after the UPS batteries gave up. Now fault transitions and threshold trips show up in real time. Weekly exercise tests leave a clear signature in the hourly summaries, with engine_starts, run_min, load_pct, and oil_kpa_mean all filled in for the window that caught the test. And the run-hour and load history needed to stay ahead of maintenance intervals streams in continuously.

The controller inside that enclosure has been measuring fuel level, coolant temperature, oil pressure, load, run hours, and active alarm state for years. What it lacked was a way to report when mains is dark, the WiFi router is dead, and the generator either starts or doesn't. An OPTA RS485 with Blues Wireless for OPTA, running off the generator's own battery-backed control bus, gives it that path: a cellular uplink that doesn't depend on facility power and doesn't care whether the WiFi router is working. Automatic detection of a missed test (flagging test_not_detected when no start shows up in a configurable rolling window) is a planned next step, covered in Limitations. The generator itself stays exactly as it was, and its data finally leaves the generator room.

On This Page

  • 1. Project Overview
  • 2. System Architecture
  • 3. Technical Summary
  • 4. Hardware Requirements
  • 5. Wiring and Assembly
  • 6. Notehub Setup
  • 7. Firmware Design
    • Modules
    • Sensor reading strategy
    • Event payload design
    • Sync and power strategy
    • Retry and error handling
    • Key code snippet 1: latch-based alarm detection, retry-on-send-failure, and alarm history logging
    • Key code snippet 2: immediate-sync alert with trigger fields
    • Key code snippet 3: oil-pressure rule (running-only, with trigger fields)
  • 8. Data Flow
  • 9. Validation and Testing
  • 10. Troubleshooting
  • 11. Limitations and Next Steps
    • Simplified for this reference design
    • Production Next Steps
  • 12. Summary

Resources

  • GitHub
  • Request More Info
Can we improve this page? Send us feedback
© 2026 Blues Inc.
© 2026 Blues Inc.
AboutDocsAPI ReferenceTermsPrivacy