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_rightCNC Machine Spindle Load and Cycle Time Tracker

CNC Machine Spindle Load and Cycle Time Tracker

CNC Machine Spindle Load and Cycle Time Tracker 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/cnc_spindle_tracker/ so it can learn how the device works and help you adapt it to your hardware.

This project is an industrial equipment monitoring reference design for machine-tool OEMs who want a continuous view of how their installed base is actually being used. The device sits on the customer's CNC machine and reports the signals an OEM cares about: how hard the spindle is working, how many cycles ran each hour, how often the machine sits idle, which operator is logged in, and what alarm codes the controller raises. Everything goes back to the OEM's cloud over cellular and never touches the customer's plant network. Routine telemetry rolls up into hourly summaries, while alarms and operator-ID changes go out right away. The hardware is a Finder OPTA with a Blues Wireless for OPTA cellular expansion (§4 has the full BOM), and the data comes from any CNC controller that exposes telemetry over Modbus TCP.

1. Project Overview

The problem. A CNC (computer numerical control) machining center is a six-figure piece of capital equipment, and once it ships, the OEM that built it usually has no idea what happens to it. The spindle might be running at 80% load twelve hours a day, or it might be sitting idle because the shop scheduled it wrong. Maybe the same alarm code fires every Tuesday morning. With data, that pattern is obviously a tooling-change reminder. Without data, it's an invisible warranty claim. An operator could be overriding the feed rate up to 140% on a finishing pass and burning through inserts, and nobody at the OEM would know.

That blind spot matters because the whole industry is moving toward EaaS (equipment as a service) and usage-based billing. You can't sell spindle-hours when you don't know how many are actually running, and you can't offer a predictive-maintenance contract without seeing the telemetry. Benchmarking your machine's OEE (Overall Equipment Effectiveness, a manufacturing KPI that combines availability, performance, and quality into a single utilization score) takes a continuous stream of cycle-time and run/idle data coming back from the shop floor.

On controls that expose it, the data is already there. A good number of modern CNC controls publish live spindle load, feed-rate override, alarm codes, cycle state, cumulative part counts, and the current operator ID over Modbus TCP, through an Ethernet port on the back of the cabinet. Support depends on the vendor, the model, and which optional feature packages are installed, so check the controller's Modbus/Ethernet option documentation for availability and the exact register layout before you deploy. Some vendors use proprietary protocols or OPC-UA instead (Siemens SINUMERIK, Fanuc FOCAS-over-Ethernet) and need a protocol translation layer that this design doesn't cover. Where Modbus TCP is available, the hard part is getting the data off the machine and back to the OEM without involving the machine shop's network.

Why Notecard. Machine shops run their CNC machines on isolated OT (operational technology) networks. Those networks are air-gapped from the corporate LAN by design, and plant IT is deeply skeptical of anyone who wants to plug an OEM's monitoring device into their subnet. Asking every customer to configure a VLAN, assign a static routable IP, punch a firewall rule, and keep all of that working through staff turnover is a nightmare for customer acquisition and for support. The OEM's monitoring device has to work the same way in a shop in Ohio, Stuttgart, or Osaka, with no help from site IT.

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.

That's why this design uses cellular. Snap a Blues Wireless for OPTA onto the Finder OPTA's expansion port and the device gets its own cellular data channel, a private uplink to the Blues Notehub cloud service. It doesn't need plant LAN credentials, a firewall exception, or an IT ticket. The Modbus TCP connection to the CNC runs over a direct point-to-point Ethernet cable between the OPTA and the machine control, so the two devices sit on a private subnet with no route to the shop floor network at all. Plant IT never sees the monitoring device, because it never touches their infrastructure.

Deployment scenario. One Finder OPTA with Blues Wireless for OPTA mounts on the DIN rail in the machine's electrical panel. A two-meter Cat6 patch cable runs from the OPTA's RJ45 port straight to the CNC controller's Modbus TCP Ethernet port, which makes a closed, private connection with its own subnet. The cellular antenna exits through a cable gland on the panel door, and the panel's existing 24 VDC supply powers the assembly. Getting to first light takes an hour of wiring, and the OEM's service technician never has to deal with the shop's network team.

2. System Architecture

System architecture: CNC controller (Modbus TCP server) → OPTA + Wireless for OPTA via point-to-point Cat6 → cellular → Notehub → OEE / CMMS / paging

Device-side responsibilities. The OPTA's STM32H747 Cortex-M7 host acts as the Modbus TCP client. Once a minute it asks the CNC controller (the Modbus server) for the same block of six holding registers over the OPTA's built-in Ethernet port. Each poll feeds the rolling hourly statistics the OEM cares about: mean and peak spindle load, mean feed-rate override, run/idle minute counts, cycle completions, and average cycle time. It also runs the two sample-based alert rules (spindle_overload and cnc_alarm). A third, connection-failure alarm (modbus_unreachable) fires when the Modbus TCP link still can't be reached after a retry. The same poll catches operator-ID transitions, so any change in the operator-ID register fires a cnc_operator.qo Note right away. Everything the host produces goes over I²C, through the expansion's AUX connector, to the Notecard inside Blues Wireless for OPTA. The host never deals with the cellular modem or its session state.

Notecard responsibilities. From there, the Notecard queues every Note on the device and brings up a cellular session on the hub.set outbound cadence (60 minutes by default). For anything marked sync:true, it wakes the radio within a minute of the alert firing. Configuration comes the other way through the Notecard too. OEM application engineers can push environment variables from Notehub to retune the Modbus register block base address, alert thresholds, and reporting cadence for each fleet, without touching firmware. When report_minutes changes, the firmware reissues hub.set so the Notecard's outbound cadence stays in step with the summary window.

Notehub responsibilities. The Notecard manages its own cellular session on supported carrier networks worldwide through its embedded global SIM, then hands the data to Notehub. Notehub ingests and stores every event and applies project-level routes. For an OEM, the natural way to organize devices is with Fleets and Smart Fleets: one fleet per controller family or model profile, with the register block base address, unit ID, port, and alert thresholds stored as fleet-level environment variables. Group by controller model instead of by customer site, because register maps often differ across CNC models even inside the same facility. IP addressing is still compile-time in this reference design, so machines on different network configurations need a per-build adjustment (see §12 Limitations).

Routing to the cloud (high level). Notehub can route to HTTP, MQTT, AWS IoT Core, Azure IoT Hub, GCP Pub/Sub, Snowflake, and several other targets. The Notehub routing docs cover the setup. This project doesn't ship a specific downstream endpoint, so the OEE dashboard is a project-specific integration you route to from Notehub.

3. Technical Summary

  1. Flash the OPTA with firmware/cnc_spindle_tracker/cnc_spindle_tracker.ino. You'll need the Arduino IDE and the Mbed OS Opta Boards core. On a Finder OPTA Codesys, double-tap RESET before the first upload (see the quickstart).
  2. Get ProductUID from notehub.io, paste it into the sketch, and reflash.
  3. Wire: Run Cat6 from the OPTA's RJ45 port to the CNC's Modbus TCP port (default 192.168.250.1:502), and bring the cellular antenna out through the panel door.
  4. Power: Apply 24 VDC to the OPTA. The Notecard auto-claims to your Notehub project on its first cellular session (≈5 minutes).
  5. Validate: Look for cnc_summary.qo Notes in Notehub within 60 minutes. You should get one summary per hour, plus one alarm per event whenever something triggers.

When you're done: you have a continuous stream of spindle load, cycle counts, alarm codes, operator IDs, and run/idle telemetry reaching Notehub every hour, and overloads or fault transitions raise real-time cellular alarms. Roll the summaries up into an OEE dashboard, and use Notehub routes to send alarms to your CMMS or on-call system.

Here's a sample Note from this device:

{
  "file": "cnc_summary.qo",
  "body": {
    "spindle_pct_mean": 71.4,
    "spindle_pct_peak": 88.2,
    "feed_override_pct_mean": 97.5,
    "run_min": 51,
    "idle_min": 9,
    "cycle_count": 25,
    "avg_cycle_sec": 122,
    "operator_id": 7,
    "alarm_count": 0,
    "valid_samples": 60
  }
}

4. Hardware Requirements

PartQtyRationale
Finder OPTA Codesys (8A.04.9.024.832C)1Industrial DIN-rail micro-PLC, included in the Wireless for OPTA Bundle. Its built-in RJ45 Ethernet port handles Modbus TCP to the CNC, RS-485 is there as a future bus option, and the Cortex-M7 host has enough RAM for the rolling stats. For this project it runs Arduino firmware. Any Arduino Opta variant also works, since all you need is Ethernet.
Blues Wireless for OPTA (NA, SKU 992-00155-C)1Snaps onto the OPTA's right-hand expansion port and adds a Notecard Cell+WiFi over I²C with a prepaid global SIM. Because it's cellular, nothing depends on the plant network. For European deployments, pick the EMEA variant (SKU 992-00156-C).
External cellular antenna(s), SMA, ~3 m lead (e.g. SparkFun CEL-16432)1 required, 2 recommendedMetal electrical panels block cellular signals, so route at least the primary antenna through a cable gland to the outside of the panel. Adding the diversity antenna improves LTE Cat-1 performance where signal is weak.
Blues Mojo1Coulomb counter for the bench. During commissioning it sits inline on the Wireless for OPTA power input to check the Notecard subsystem's per-session energy. It isn't deployed to the field.
24 VDC DIN-rail supply, ≥10 W (e.g. MeanWell HDR-15-24)1Powers the OPTA and the Wireless for OPTA expansion. Most machine panels already have a 24 VDC rail, so only source one locally if you need it.
Cat6 patch cable, ~2 m1Point-to-point Modbus TCP link from the OPTA's Ethernet port to the CNC controller's Modbus TCP port. A straight-through cable is fine, since modern NICs don't need a crossover.
DIN rail, ~15 cm1Mount for the OPTA and expansion. There's probably one inside the panel already.

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: 24 VDC supply, OPTA RS485, Wireless for OPTA, antennas on DIN rail; Cat6 Ethernet point-to-point to CNC at 192.168.250.1; six contiguous holding registers polled per minute

warning

Safety. Machine electrical panels carry hazardous voltages on the main power bus, even when the control wiring is low-voltage. Installation must be done by qualified personnel following site lockout/tagout procedures and applicable electrical codes. This reference design is read-only over Modbus and never sends a command to the CNC controller.

  1. Mount and power. Snap Wireless for OPTA onto the OPTA's right-hand expansion port, using the solderless AUX connector that comes with Wireless for OPTA to join the two. That connector carries the I²C bus the Notecard talks over. Mount the assembly on the DIN rail and wire 24 VDC from the panel supply to the OPTA's + and - terminals. The Wireless for OPTA quickstart calls for 24 VDC in field deployments. USB-C powers the host CPU for programming, but it doesn't power the OPTA's output stage or the expansion. Jumper the OPTA's +24V terminal to the matching power input on the expansion so both run from the same supply rail.

  2. Antennas. Drill or gland two SMA bulkhead connectors through the panel door or a side knockout. Run the primary cellular antenna lead to the first SMA port on Wireless for OPTA and the diversity lead to the second. The bundled rubber-duck antennas are only good for bench testing, so don't rely on them inside a metal panel once it's in the field.

  3. Modbus TCP link. Run a Cat6 patch cable from the OPTA's RJ45 Ethernet port to the CNC controller's dedicated Modbus TCP port. The controller's Modbus/Ethernet option documentation tells you which physical connector and TCP port number to use. That gives you a private point-to-point link, with the two devices on an isolated /24 subnet that has no route to the shop floor network. Give the OPTA a static IP on the same subnet as the CNC's Modbus TCP interface. The firmware defaults to 192.168.250.10 for the OPTA and 192.168.250.1 for the CNC. IP addressing is compile-time only in the current firmware, so before you build, change LOCAL_IP in cnc_spindle_tracker.ino (line 40) and _DEFAULT_CNC_IP in cnc_spindle_tracker_helpers.cpp (line 27) to match the real CNC network configuration. This reference design has no way to set IPs at runtime (see Limitations for the production path).

    Deployment variant: shared CNC main port. Some controllers only expose Modbus TCP on the same port the machine tool LAN uses. In that case the OPTA has to join the customer's machine network, even if all it does there is reach the CNC, and that gives up the private-subnet / no-plant-network-touch property described in §1. You'll need customer network and OT-security approval before deploying that way, and it's outside the primary scope of this design.

  4. Bench validation setup. During first-light commissioning, splice the Mojo inline between the 24 VDC supply and the Wireless for OPTA power terminal. It measures the whole expansion subsystem (the Notecard plus the expansion's onboard regulators) during a real cellular session.

6. Notehub Setup

  1. Create a project. Sign up at notehub.io and create a new project. Copy the ProductUID into firmware/cnc_spindle_tracker/cnc_spindle_tracker.ino. On line 19, paste your UID between the quotes in #define PRODUCT_UID "".

  2. Claim the device. Flash the OPTA and power the panel. The Notecard associates itself with your project on its first cellular session. Within 5–10 minutes, the device should show up under Fleet > Devices in Notehub.

  3. Create a fleet per controller family or model profile. Fleets let you push a common configuration to every machine with the same controller type. You set fleet-level environment variables in the Notehub UI under Fleet > Environment, and they hold the shared parameters: Modbus register block base address, unit ID, port, and alert thresholds. Every machine with the same controller model and register map uses that one configuration, with no separate firmware build. IP addressing (LOCAL_IP and _DEFAULT_CNC_IP) is still compile-time in this reference design, though, so machines whose CNC controllers sit on different network configurations need a per-build adjustment (see Limitations). Register maps often differ across CNC models even at the same customer site. Organizing fleets by controller family or model profile, not just by site, gives you the tightest control over shared settings. Smart Fleets can handle fleet assignment automatically, based on a device tag you set during commissioning.

  4. Set environment variables. In the Notehub web UI, go to Fleet > Environment to see every available variable. They're all optional, and the table shows the firmware defaults. When you save a change in Notehub, the Notecard downloads the new value on its next inbound sync. The host then applies it on the next scheduled env.get call, either at boot or at the end of the current report window.

    VariableDefaultPurpose
    sample_minutes1Minutes between Modbus TCP polls.
    report_minutes60Minutes between summary Notes (cnc_summary.qo). Changing it also realigns the Notecard's outbound sync cadence, so the local summary window and the cellular session schedule update together.
    modbus_port502Modbus TCP port on the CNC controller.
    modbus_unit_id1Modbus unit (slave) ID. Many CNC controllers default to 1, but check the CNC controller's Modbus/TCP settings.
    reg_spindle_load256Starting address (0-based, wire-level) of the contiguous six-register block the firmware reads in one transaction. The firmware always reads the six registers in order from this address: spindle load, feed-rate override, alarm code, cycle state, cycle count, operator ID. Set it to the base address where your CNC controller's block starts.
    spindle_overload_pct90.0Spindle load (%) above which a spindle_overload alarm fires while the machine is in-cycle.
    expected_cycle_sec120Reserved for future use. The device stores it in config, but the firmware doesn't evaluate it or send it in any Note.

    Cadence constraint. sample_minutes must not exceed report_minutes. For accurate run/idle minute totals, report_minutes should also be a whole-number multiple of sample_minutes (for example, sample_minutes=5, report_minutes=60). If it isn't (say, sample_minutes=7, report_minutes=60), the run+idle totals in the summary come up short of the window duration. The firmware enforces the first rule by clamping sample_minutes down to report_minutes whenever the env var would break it. It accepts non-divisible pairs but logs a Serial warning. See §12 Limitations.

    CNC register-map gotchas. The defaults above are only examples. Real CNC controllers vary widely in addressing convention (0-based wire-level vs. 1-based / Fanuc "PLC address" notation), per-register scaling (spindle load may be 0.1 %, 1 %, or % of rated torque), signedness, and whether the cycle count is a 16-bit or 32-bit (two-register) value. The big one is that the current firmware only supports a contiguous six-register layout. All six values have to sit back to back in the controller's Modbus map, starting at reg_spindle_load. Production deployments need a vendor-specific register map with a matching contiguous block, or individual per-register reads added to the firmware. See Limitations.

  5. Configure routes. Add routes to send data to your downstream systems:

    • cnc_alarm.qo → real-time delivery to your CMMS, on-call paging system, or Slack webhook.
    • cnc_summary.qo → batched hourly delivery to your OEE analytics or data warehouse.
    • cnc_operator.qo → real-time delivery to an operator-session or access-logging system. This one is best-effort, since comms outages can drop individual transitions, and a transition that happens and reverts between two polls never gets recorded.

    Because the three Notefiles are separate, each one can go to a different destination at a different priority with no filter logic downstream.

7. Firmware Design

The firmware lives in the firmware/ directory. It's split into three files (orchestration, shared types, and the helpers that talk to the Notecard and Modbus) so each concern has a clear home:

FileRole
cnc_spindle_tracker.inoMain sketch: setup(), loop(), sample accumulation, alert evaluation, and all global state definitions.
cnc_spindle_tracker_helpers.hShared types (Config, Sample, WindowStats), compile-time defaults, extern declarations for globals, and helper-function prototypes.
cnc_spindle_tracker_helpers.cppNotecard and Modbus helper implementations: notecardConfigure(), defineTemplates(), fetchEnvOverrides(), modbusConnect(), pollCnc(), sendSummary(), sendAlarm(), sendOperatorChange(), resetWindow().

Modules

ResponsibilityFunction(s)
Notecard init, hub.setnotecardConfigure()
Note template registrationdefineTemplates()
Environment variable fetch (incremental, time-gated)fetchEnvOverrides()
Ethernet/Modbus TCP connection managementmodbusConnect()
Modbus register poll (6 registers, 1 transaction)pollCnc()
Rolling hourly stats accumulationaccumulateSample()
Alert rule evaluationevaluateAlerts()
Hourly summary Note emissionsendSummary()
Immediate alarm Note emissionsendAlarm()
Immediate operator-change Note emissionsendOperatorChange()
Periodic scheduling (millis-based; no MCU sleep)loop()

Sensor reading strategy

The firmware reads six holding registers in a single Modbus TCP transaction (requestFrom with quantity 6, starting at reg_spindle_load). One TCP round trip is much cheaper than six separate reads, and it keeps the polling window short compared to the one-minute sample interval. The six registers must be contiguous in the CNC controller's holding-register map. This reference design doesn't support non-contiguous layouts (see Limitations).

Samples taken while the machine is idle (cycle state ≠ 1) stay out of the spindle load and feed-rate averages, because a 0 % spindle reading while the operator sets up a part doesn't belong in the same bucket as a loaded cut. Run and idle minutes are tracked separately, so the OEE dashboard can compute the Availability component straight from the summary Note.

Event payload design

This design uses three template-backed Notefiles. Templates store Notes on the Notecard as fixed-length binary records instead of free-form JSON, which cuts wire size by 3–5×. That matters for a device that may run for a decade on its included 500 MB.

cnc_summary.qo (queued hourly, templated):

{
  "file": "cnc_summary.qo",
  "body": {
    "spindle_pct_mean": 71.4,
    "spindle_pct_peak": 88.2,
    "feed_override_pct_mean": 97.5,
    "run_min": 51,
    "idle_min": 9,
    "cycle_count": 25,
    "avg_cycle_sec": 122,
    "operator_id": 7,
    "alarm_count": 0,
    "valid_samples": 60
  }
}

cycle_count is the per-window delta of the controller's own cumulative cycle-count holding register, meaning the unsigned difference between the register values observed at the start and end of the reporting window. Since it comes from the controller's own accumulator, it catches short cycles that finish entirely inside one poll interval, which edge detection alone can't see. Unsigned 16-bit arithmetic handles counter wrap (65535 → 0). A controller reset that drops the counter by more than 32767, though, looks just like a natural wrap and would inflate one window's delta.

avg_cycle_sec is an edge-timing heuristic built from cycleState register transitions (1 → non-1) and the host MCU's millis() clock. It is not a controller-native cycle timer, and it has known gaps, especially on fast-cycle jobs. §12 Limitations covers them.

valid_samples counts the successful Modbus reads in the window. A value of 0 means every poll in the window failed. Downstream analytics use that sentinel to tell a total communication outage apart from a machine that really was powered and idle.

cnc_alarm.qo (immediate, sync:true, templated):

{
  "file": "cnc_alarm.qo",
  "sync": true,
  "body": {
    "alert_type": "spindle_overload",
    "alarm_code": 0,
    "spindle_pct": 93.1,
    "operator_id": 7
  }
}

alert_type is one of spindle_overload (spindle load went over the threshold during a cut), cnc_alarm (the CNC control raised a fault code), or modbus_unreachable (the Modbus TCP connection was lost; rate-limited to once per summary window).

cnc_operator.qo (immediate, sync:true, templated):

{
  "file": "cnc_operator.qo",
  "sync": true,
  "body": {
    "operator_id": 9,
    "prev_operator_id": 7
  }
}

This Note fires whenever the value in the controller's operator-ID holding register changes between consecutive samples. operator_id holds the new (incoming) value and prev_operator_id the outgoing one. By convention, operator_id == 0 means nobody is logged in. These events are best-effort and only sampled at the poll interval. A brief comms outage can drop an individual transition Note, and the firmware never sees an operator-ID change that happens and reverts between two consecutive polls. The hourly cnc_summary.qo records the most recently observed operator ID when the window closes. That's a snapshot of the state, not a complete record of every transition during the window.

Power and sync strategy

The OPTA and Wireless for OPTA run on 24 VDC line power, so the design goal is bandwidth and bus efficiency, not host MCU sleep. The Notecard runs in hub.set periodic mode. At startup, outbound is set to DEFAULT_REPORT_MINUTES (60 minutes) and inbound to DEFAULT_REPORT_MINUTES × 2 (120 minutes). When an env-var update changes report_minutes, the firmware reissues hub.set with new outbound and inbound values, which keeps the Notecard's cellular session schedule in line with the summary window. Summary Notes queue on the device and go out together in one session per window. Alarm Notes carry sync:true and wake the radio as soon as a session can be established, usually within 15–60 seconds. The firmware doesn't push the Modbus bus any faster than the sample interval. It polls once per sample_minutes, then the host just yields in loop() until the next scheduled tick.

Retry and error handling

  • notecardConfigure() calls notecard.sendRequest(req) in a blocking loop that repeats every 30 seconds until hub.set succeeds. The firmware treats Notecard configuration as mandatory. Without a valid hub.set, the ProductUID isn't registered on the Notecard, the outbound/inbound cadence is wrong, and later Notes would be un-routable. The device won't enter the main loop until configuration is confirmed.
  • The firmware detects a lost Modbus TCP connection from the requestFrom() return code and retries once per sample (reconnect + re-poll). If both attempts fail, it skips the sample and fires a modbus_unreachable alarm. That alarm is rate-limited to once per summary window, so nobody gets buried in alarms when the CNC is just powered off.
  • Before reading any field from a notecard.requestAndResponse() response, the firmware checks for both a NULL return and an err field.
  • Environment variable fetches pass the time argument, so they only request variables modified since the last successful fetch and inbound syncs don't move any data they don't need to.
  • Alert de-duplication: spindle_overload has a 30-minute cooldown timer, so a slowly climbing load doesn't page the on-call person on every sample. CNC alarm codes fire on any transition to a nonzero value (0 → nonzero, or one nonzero code changing to a different nonzero code), not on every sample where a fault is asserted. The firmware updates its alarm-state tracking (g_lastAlarmCode, g_window.alarmCount) as soon as it observes the transition, whether or not the cnc_alarm Note queues successfully. sendAlarm() makes two note.add attempts. If both fail, the alarm goes into an in-memory ring buffer (g_alarmFifo, depth 8) and gets retried one slot per poll cycle, so a comm outage can't make the summary undercount alarm transitions or quietly lose a short-lived alarm. On a ring-buffer overflow, the firmware evicts the oldest slot and logs a warning to Serial.

Key code snippet 1: Notecard periodic sync configuration

// Notecard configuration is mandatory: without a successful hub.set the
// ProductUID is not registered on the Notecard, the outbound/inbound cadence
// is wrong, and subsequent Notes are un-routable. The function blocks —
// retrying hub.set every 30 s — until configuration succeeds, so setup()
// never returns with the Notecard in an unconfigured state.
void notecardConfigure(const char *productUid) {
    // ...
    while (true) {
        J *req = notecard.newRequest("hub.set");
        if (req == NULL) {
            usbSerial.println("[NOTECARD] hub.set allocation failed — retrying in 5 s.");
            delay(5000);
            continue;
        }
        JAddStringToObject(req, "product", productUid);
        JAddStringToObject(req, "mode", "periodic");
        JAddNumberToObject(req, "outbound", DEFAULT_REPORT_MINUTES);   // minutes
        JAddNumberToObject(req, "inbound",  DEFAULT_REPORT_MINUTES * 2);
        if (notecard.sendRequest(req)) {
            usbSerial.println("[NOTECARD] hub.set OK.");
            return;
        }
        usbSerial.println("[NOTECARD] hub.set failed — check wiring and PRODUCT_UID. Retrying in 30 s.");
        delay(30000);
    }
}

Key code snippet 2: Modbus TCP batch read (6 registers, 1 transaction)

// All 6 demo registers are contiguous starting at regSpindleLoad.
// One requestFrom() is ~6× faster than six individual reads.
const uint16_t regBase = cfg.regSpindleLoad;
const int      qty     = 6;

if (!_modbusTCPClient.requestFrom(cfg.modbusUnitId, HOLDING_REGISTERS, (int)regBase, qty)) {
    // Retry once: reconnect then re-poll.
    // ...
}
// ...
// Registers use 0.1-unit scaling for percentages (demo map convention).
int16_t rawSpindle      = (int16_t)_modbusTCPClient.read();
// Register N+1 is the feed-rate override (0–150 % of the programmed feed
// rate), NOT actual feed rate in engineering units (mm/min or in/min).
// Engineering-unit feed rate is not routinely available over Modbus TCP on
// most CNC controllers; see §12 Limitations on the dev.blues.io page.
int16_t rawFeedOverride = (int16_t)_modbusTCPClient.read();
s.alarmCode  = (uint16_t)_modbusTCPClient.read();
s.cycleState = (uint8_t)(_modbusTCPClient.read() & 0x00FF);
s.cycleCount = (uint16_t)_modbusTCPClient.read();
s.operatorId = (uint16_t)_modbusTCPClient.read();

s.spindleLoadPct  = rawSpindle      / 10.0f;  // e.g. 714  → 71.4 %
s.feedOverridePct = rawFeedOverride / 10.0f;  // e.g. 1000 → 100.0 % of programmed rate

Key code snippet 3: Immediate alarm Note with sync:true

// sync:true wakes the radio immediately rather than waiting for the outbound
// window. Returns true when the Note was successfully queued.
// A single automatic retry covers transient I²C / allocation errors without
// masking persistent failures — alarms are the highest-value events in the
// design.
bool sendAlarm(const char *alertType, const Sample &s) {
    for (int attempt = 0; attempt < 2; attempt++) {
        J *req = notecard.newRequest("note.add");
        // ...
        JAddStringToObject(req, "file", "cnc_alarm.qo");
        JAddBoolToObject(req, "sync", true);
        J *body = JAddObjectToObject(req, "body");
        JAddStringToObject(body, "alert_type",  alertType);
        JAddNumberToObject(body, "alarm_code",  (int)s.alarmCode);
        JAddNumberToObject(body, "spindle_pct", s.spindleLoadPct);
        JAddNumberToObject(body, "operator_id", (int)s.operatorId);
        if (notecard.sendRequest(req)) {
            // ...
            return true;
        }
        // ...
    }
    return false;
}

8. Build and Flash

Prerequisites:

  • Arduino IDE 2.3+ or arduino-cli (command-line). If you're using the IDE, install the Arduino Mbed OS Opta Boards board package from Tools > Board Manager by searching for "Opta".
  • Dependencies (install them from the Library Manager or with arduino-cli):
    • Blues Wireless Notecard (check releases)
    • ArduinoModbus
    • ArduinoRS485
    • Ethernet.h (included with the Mbed OS Opta Boards core)

Build steps (using arduino-cli):

# Install the board core
arduino-cli core install arduino:mbed_opta

# Install dependencies
arduino-cli lib install "Blues Wireless Notecard" "ArduinoModbus" "ArduinoRS485"

# Edit firmware/cnc_spindle_tracker/cnc_spindle_tracker.ino line 19 — paste your ProductUID into PRODUCT_UID
# Edit firmware/cnc_spindle_tracker/cnc_spindle_tracker.ino line 40 — set LOCAL_IP to match your Modbus subnet
# Edit firmware/cnc_spindle_tracker/cnc_spindle_tracker_helpers.cpp line 27 — set _DEFAULT_CNC_IP to your CNC controller IP

# Compile
arduino-cli compile -b arduino:mbed_opta:opta firmware/cnc_spindle_tracker/

# Flash (replace /dev/cu.usbmodem1234567 with your OPTA's USB port)
arduino-cli upload -b arduino:mbed_opta:opta -p /dev/cu.usbmodem1234567 firmware/cnc_spindle_tracker/

# Open the serial monitor to check for errors
arduino-cli monitor -p /dev/cu.usbmodem1234567 -c baudrate=115200

Alternatively, using the Arduino IDE: Open firmware/cnc_spindle_tracker/cnc_spindle_tracker.ino, set your ProductUID and IPs, select Tools > Board > Arduino Opta, and click Upload.

note

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

9. Data Flow

Data flow: minute-cadence Modbus poll → three edge rules + operator-ID transition detection → cnc_alarm.qo (sync), cnc_operator.qo (sync), cnc_summary.qo (hourly templated) → Notehub routes

Collected. Every sample_minutes (1 minute by default), the firmware reads six registers in one Modbus TCP transaction: spindle load (%), feed-rate override (%), active alarm code, cycle state, cumulative cycle count, and current operator ID (whatever value is in the controller's operator-ID register at that moment).

Accumulated. Each sample updates rolling counters: spindle load sum and peak (while running), feed-rate sum (while running), run/idle minute tallies, the per-window cycle-count register delta (the difference between consecutive cycleCount register reads, and the primary source for cycle_count), edge-timed cycle durations (used only for avg_cycle_sec), the count of observed active-alarm transitions (alarms that assert and clear between polls are never seen), and operator-ID change detection. The edge detector misses cycles that finish entirely within one poll interval, but the register delta still counts them. Limitations explains what the avg_cycle_sec heuristic means in practice.

Transmitted.

  • cnc_summary.qo: once per report_minutes (24 Notes per day per machine by default), queued locally and shipped on the Notecard's hourly outbound sync. Fields: spindle mean/peak, feed-rate override mean (feed_override_pct_mean, the override percentage of the programmed rate rather than an engineering-unit feed rate; see §12), run minutes, idle minutes, cycle_count (per-window delta of the controller's cumulative cycle-count register), avg_cycle_sec (edge-timing heuristic; see Limitations), the most recently observed operator ID, alarm_count (the number of observed active-alarm transitions in the window, not a complete fault history; see §12 Limitations), and valid_samples (where 0 signals a total Modbus communication outage for the window).
  • cnc_alarm.qo: sent right away when an alert triggers, with sync:true. There are three alert types: spindle_overload, cnc_alarm, and modbus_unreachable.
  • cnc_operator.qo: sent right away when the operator-ID register changes, with sync:true. Fields: operator_id (incoming value) and prev_operator_id (outgoing value). It's best-effort and sampled, so comms outages can drop individual events, and a transition that happens and reverts between consecutive polls is never recorded. The hourly summary holds the last ID seen at window close, not a complete transition history.

Routed. Notehub sends cnc_alarm.qo in real time to the OEM's CMMS or on-call system and cnc_summary.qo to the OEE analytics store every hour. cnc_operator.qo goes out in real time to an access-logging or per-operator OEE attribution system.

Alert triggers.

  • spindle_overload: spindle load exceeds spindle_overload_pct while the machine is in-cycle (state = 1). A 30-minute cooldown keeps a sustained overload from paging someone over and over.
  • cnc_alarm: the active alarm code changes to any nonzero value (0 → nonzero, or one nonzero code changing to a different nonzero code) as observed on the poll. It fires as soon as the poll sees the transition. Alarms that assert and clear between consecutive polls are never seen and produce no Note.
  • modbus_unreachable: the Modbus TCP connection fails on two consecutive attempts. It's rate-limited to once per report_minutes window.

10. Validation and Testing

Expected steady-state behavior. A healthy machine produces one cnc_summary.qo per hour and no cnc_alarm.qo Notes. During first commissioning, you might see one modbus_unreachable alarm before the Modbus TCP link is confirmed.

Check Notehub for events. Sign in to notehub.io, go to Fleet > Events, and filter for your device. Within 1 hour of power-up you should see a cnc_summary.qo Note with fields like spindle_pct_mean, run_min, and cycle_count. The body tab shows the actual JSON.

Modbus first-light (bench test). Before you connect to the real CNC:

  1. Run a Modbus TCP simulator on your laptop (Modbus Mechanic, ModRSsim2, ModbusMaster, or any open-source server).
  2. Connect it to the OPTA's Ethernet port, on the same subnet as the OPTA's IP (for example, both on 192.168.250.0/24).
  3. Set up six contiguous holding registers at address 256 with demo values, in this order: spindle load 650 (65%), feed-rate override 1000 (100%), alarm code 0, cycle state 1 (running), cycle count 10, operator ID 1.
  4. Flash the firmware pointed at your demo server by setting _DEFAULT_CNC_IP to the simulator's address.
  5. Wait one sample period (1 minute by default) plus one report window (60 minutes by default) for the first summary to show up in Notehub. Check that spindle_pct_mean ≈ 65, cycle_count = 0 (register delta), and valid_samples ≈ 60.
  6. Raise the spindle-load register to 950 (95%) and wait 1 minute. A cnc_alarm.qo with alert_type: "spindle_overload" should reach Notehub within 60 seconds of that poll.

Field alert testing. To test alert delivery on a live machine:

  1. In Notehub, go to Fleet > Environment.
  2. Temporarily set spindle_overload_pct = 0.
  3. Wait for the next Notecard inbound sync (every 120 minutes by default). The host applies the new threshold at the next report window boundary.
  4. After that, any nonzero spindle reading on the next in-cycle poll (cycle state 1) triggers a cnc_alarm.qo.
  5. When you're done testing, set spindle_overload_pct back to its normal value (for example, 90).

Power validation (optional Mojo). Splice the Mojo coulomb counter between the 24 VDC supply and the Wireless for OPTA power input. Here's the current envelope to expect, from Notecard low-power design:

PhaseExpected current
Notecard idle (radio off, between syncs)~8–18 µA @ 5 V
Cellular session (LTE Cat-1)~250 mA average; ≤2 A burst
WiFi fallback session~80 mA average

Confirm that idle current between syncs sits in the µA range (that's what lets the device run for years in the field), that per-session energy for one hourly sync is ~10–30 mAh, and that energy stays consistent across consecutive cycles.

tip

Bench caveat: When the OPTA is powered and programmed over USB-C, the USB rail holds the Notecard in a higher-power idle state, so you won't see µA idle current while USB is connected. For true field idle numbers, disconnect USB, power from 24 VDC only, and check the conditions in the Notecard low-power design docs.

11. Troubleshooting

SymptomProbable CauseSolution
Device doesn't claim to Notehub after first power-upMissing or malformed PRODUCT_UID in firmwareSet PRODUCT_UID on line 19 of cnc_spindle_tracker.ino, then reflash.
No Modbus connection; continuous modbus_unreachable alarmsWrong CNC IP or port, Ethernet cable unplugged, or CNC controller powered off or Modbus not enabledLook up the CNC controller's Modbus TCP IP and port in its documentation, and make sure _DEFAULT_CNC_IP (helpers.cpp) matches the CNC. Check that the Cat6 cable is plugged in at both ends. Confirm the modbus_port env var matches the CNC's TCP port (default 502).
No cnc_summary.qo Notes appear in NotehubDevice claimed, but no summaries visibleMake sure the Notecard's outbound sync interval (60 minutes by default) has passed. The firmware queues a summary even when every Modbus poll fails (with valid_samples = 0), so a missing summary points at the Notecard side, not the CNC link. Watch the Serial output with arduino-cli monitor for a Summary send failed message.
valid_samples = 0 in all summariesAll Modbus polls failedThe Serial console should show Modbus errors. Make sure the CNC is powered and Modbus TCP is enabled. From your laptop, use a Modbus client tool (QModBus, ModRSsim2) to confirm you can reach the CNC at the configured IP and port. If your laptop can but the OPTA can't, there may be a routing or firewall issue on the Ethernet segment.
Notecard not syncing; no Notes leaving the deviceI²C communication between OPTA and Notecard failed, or Notecard not poweredMake sure the AUX connector is fully seated, and check that 24 VDC reaches both the OPTA and Wireless for OPTA. The Notecard has a small blue LED near the SMA connectors that should blink during a cellular session. If it doesn't blink, the Notecard may not be powered or may have failed.
spindle_overload alarms fire too oftenThreshold too low, or noisy spindle-load sensor dataRaise spindle_overload_pct in the Fleet environment (for example, from 90 to 95). If that doesn't fix it, the CNC sensor may be noisy. Add averaging on the CNC side (most controllers have digital-filter registers) or increase SPINDLE_ALERT_COOLDOWN_MS in cnc_spindle_tracker_helpers.h.
avg_cycle_sec is wildly wrongCycle time shorter than the sample interval, or cycleState semantics differ from what the firmware expectsavg_cycle_sec is a heuristic limited by the sample period (1 minute by default), so it can't see cycles faster than that. sample_minutes only accepts whole minutes (1 is the minimum), so sampling faster means changing the firmware, and the run_min/idle_min math assumes whole-minute samples too. On fast jobs, rely on cycle_count instead. Check that the target CNC model maps cycleState == 1 to "program running" (this is vendor-specific).
Operator-ID transitions not appearingoperator_id register not exposed or always zeroConfirm the CNC controller supports operator ID over Modbus TCP (many don't). Check the controller's Modbus documentation to make sure the sixth register in the contiguous block (starting at reg_spindle_load) maps to operator ID. Then confirm the operator ID actually changes on the machine, since some controllers require a login/logout.

12. Limitations and Next Steps

This reference design is scoped on purpose to the moment the OEM's service technician cares about most, which is getting a single CNC from shipping crate to usable telemetry in an afternoon without one conversation with the shop's IT department. A few details were kept simple so that path stays clean. Each one is documented below, along with what production hardening looks like.

Simplified for this reference design

Every item below was simplified on purpose. Once the basic single-CNC monitor is proven, a production deployment will come back to each one to validate a vendor register map, add configurability, or harden a comms path.

Active alarm register only; transient alarms between polls are invisible. The firmware reads the CNC controller's currently active alarm-code holding register once per sample_minutes poll. It does not read a fault-history log, alarm queue, or event recorder. Most controllers keep a log like that internally, but standard Modbus TCP interfaces rarely expose it. If an alarm asserts and clears entirely within one poll interval, the firmware never sees it, no cnc_alarm.qo Note goes out, and the hourly summary's alarm_count doesn't include it. So alarm_count is the number of observed active-alarm transitions during the window, not a complete record of every fault the controller hit, and it isn't the same as reading the controller's internal fault history. For complete fault logging, go to the controller's native alarm log directly, through the operator panel, vendor software, or (where supported) a proprietary interface like Fanuc FOCAS or Siemens OPC-UA. In practice, the odds of missing a brief alarm rise in proportion to sample_minutes, so keep the poll interval at 1 minute. That shrinks the gap but doesn't close it.

Feed-rate override as a proxy for feed rate. The firmware reads register N+1 of the six-register block as the feed-rate override percentage, which is the operator-set multiplier on the programmed feed rate, usually 0–150 %. That's different from the actual feed rate in engineering units (mm/min or in/min). Typical CNC controllers rarely expose engineering-unit feed rate over Modbus TCP, because most vendors keep that value for internal NC interpolation. They either don't map it to a Modbus register or require a proprietary protocol to read it (Fanuc FOCAS or Siemens OPC-UA, for example). Feed-rate override still works as a production proxy. It shows the operator behavior described in §1 (overriding the programmed rate on finishing passes) and is enough for the EaaS monitoring use case, but it can't stand in for engineering-unit feed-rate telemetry in a rigorous Performance calculation. That's why the field in cnc_summary.qo is named feed_override_pct_mean, and your downstream analytics should document the difference.

sample_minutes must not exceed report_minutes, and ideally must divide it evenly. The firmware computes run_min and idle_min in cnc_summary.qo by adding sample_minutes to the right accumulator on every successful poll. Two rules have to hold for those totals to be valid. The first is sample_minutes ≤ report_minutes. Break it, and a single poll adds more minutes than the whole report window, so the run/idle counts end up longer than the window itself. The second is that report_minutes should be a whole-number multiple of sample_minutes (for example, sample_minutes=5, report_minutes=60). Otherwise the run+idle total comes in under the window duration even when the machine runs nonstop, because the accumulator moves in fixed steps that don't land exactly on the report boundary. The firmware enforces the first rule by clamping sample_minutes down to report_minutes in fetchEnvOverrides() and logging a warning to Serial. The second rule is only advisory. Non-divisible combinations get through with a Serial warning, but the run/idle totals will be approximate. Stick to divisible pairs when you set fleet environment variables.

Demo register map only. The firmware reads six contiguous 16-bit holding registers starting at address 256, with fixed 0.1-unit scaling. Real CNC controllers differ in addressing convention (0-based wire-level vs. Fanuc PLC notation vs. Siemens DBx addressing), per-register scaling, signedness, 32-bit cycle counters that span two registers with vendor-specific word order, and which registers are exposed over Modbus at all instead of a proprietary protocol (Fanuc FOCAS, Siemens OPC-UA, Haas NGC). You'll need a validated register map for each vendor before this design goes onto production machines.

IP addressing is hardcoded. The CNC server IP and the OPTA's local IP are compile-time #defines. A production deployment needs a web-based local configuration UI, an in-panel DIP-switch scheme, or env-var parsing of dotted-decimal IP strings. This design doesn't implement any of them.

Operator-change events are best-effort, not guaranteed. The firmware detects operator-ID transitions by comparing consecutive register reads, and it sends a cnc_operator.qo Note right away on each change. sendOperatorChange() doesn't retry on failure, so a brief I²C or cellular comms outage can drop an individual transition Note. The hourly cnc_summary.qo carries the most recently observed operator ID as a snapshot at window close. It doesn't capture every transition in the window, and a transition that happens and reverts between polls is never recorded. Any per-operator session durations or cumulative utilization you derive from summary Notes are approximate and can't replace durable session accounting. Like every register-based read, operator_id is only as reliable as the controller's implementation. Fanuc 0i-MF exposes operator ID in the PMC area, Siemens SINUMERIK exposes it through OPC-UA instead of Modbus, and some controllers have no external operator-identity interface at all. Where it isn't available, operator_id reads 0 on every sample.

cycle_count is authoritative; avg_cycle_sec is a heuristic. cycle_count in cnc_summary.qo is the per-window delta of the controller's own cumulative cycle-count holding register, computed with unsigned 16-bit subtraction of consecutive register reads. It counts every cycle that increments the controller's register, including cycles that finish inside a single poll interval, which edge detection alone can't see. Counter wrap (65535 → 0) is handled correctly. A mid-session controller reset that drops the counter by more than 32767 looks the same as a natural wrap and would inflate one window's delta, but resets like that are rare enough that it's an accepted corner case.

avg_cycle_sec works differently. It's an edge-timing heuristic: the firmware watches cycleState transitions (1 → non-1) and uses millis() to measure the wall-clock time between them. That has two systematic limits. Missed short cycles: the edge detector can't see a cycle that starts and finishes within one poll interval, so those cycles are left out of avg_cycle_sec even though cycle_count includes them. On fast-cycle jobs (cycle time shorter than sample_minutes), avg_cycle_sec skews toward the longer cycles it can observe. Timing granularity: accuracy is limited by the sample period (1 minute by default), not the controller's internal timer. Treat avg_cycle_sec as a first-order estimate, not a certified measurement. Whether cycleState == 1 means "program running" depends on the vendor, so verify it on each target CNC model before you rely on avg_cycle_sec.

Quality component of OEE is absent. OEE = Availability × Performance × Quality. This design provides the raw data for Availability (run/idle minutes) and the device-side inputs for Performance (cycle count and average cycle time, sent in cnc_summary.qo). For now, though, comparing average cycle time against an expected target has to be set up entirely in the downstream analytics layer. The device stores expected_cycle_sec in its config for future use, but the firmware doesn't evaluate it or send it in any Note. Quality, the ratio of conforming parts to total parts, needs a downstream measurement (CMM output, vision inspection, or operator scrap entry) that Modbus can't supply.

Single CNC per OPTA. The firmware polls one Modbus slave ID. A multi-machine cell with a shared Modbus gateway could handle multiple IDs with round-robin polling, but that complicates the per-machine summary bookkeeping, so it's out of scope here.

No Modbus writes. That's intentional, and it isn't negotiable in this reference design. Writing setpoints to a CNC (feed override, spindle speed, program selection, M-codes) brings in machine safety, interlocks, and functional safety certification, all of which are entirely outside the scope of a monitoring-only device.

Modbus TCP reconnection is simple. The firmware reconnects after every failed transaction. Production hardening would add exponential backoff, a connection health watchdog, and a per-session keep-alive that catches stale TCP connections before the next poll.

No host firmware updates wired up. Notecard Outboard Firmware Update works on STM32H7 (the OPTA's MCU family), but it needs AUX wiring that Blues Wireless for OPTA doesn't break out right now. On the current Wireless for OPTA hardware, host firmware updates are local-only over USB-C.

Production Next Steps

Getting this monitor ready for a production fleet comes down to validating vendor-specific register maps, adding configuration you can change in the field, and closing the rest of the OEE loop. The extensions below are listed roughly in the order you'd want them, from most immediately useful to most dependent on outside integrations.

Vendor-specific register-map builds are the first thing most deployments will need: Fanuc 30i/31i/32i (Series 30), Siemens SINUMERIK 840D sl (via an OPC-UA adapter), Mitsubishi M80/M800, and Haas NGC. Each one needs the right addressing and scaling, plus protocol translation wherever Modbus TCP isn't available natively.

IP addressing via Notehub environment variables (parsing dotted-decimal strings), or a local web configurator the OPTA serves on first boot, would get rid of the compile-time #define constraint.

32-bit cycle counter support means reading two consecutive registers and putting them together in the vendor's byte order.

Native elapsed-cycle-timer reads would replace the heuristic cycleState edge detection behind avg_cycle_sec with a read of the controller's own elapsed-cycle-timer register, where the vendor exposes one. That removes the timing bias that comes from poll-interval granularity, along with the dependence on vendor-specific cycleState semantics. cycle_count already uses the controller's cumulative register, and this step does the same for the average timing field.

OEE Quality component integration closes the OEE loop on the device. An inbound Notefile (cnc_quality.qi) would receive scrap/conforming counts from a CMM or vision system.

Per-machine baseline learning builds up a 30-day spindle-load-vs-feed-rate operating envelope, then triggers spindle_overload against the machine's own learned curve instead of a static percentage.

Wiring ODFU to the OPTA's BOOT/RESET pins would allow over-the-air host updates once the AUX path is available.

13. Summary

The OEM whose six-figure machining centers went silent the day the shop floor accepted delivery now gets the picture it never had. Spindle load, cycle counts, alarm codes, and operator IDs come back every hour, and overload and fault transitions arrive in real time, all over a cellular channel that machine-shop IT never sees and never has to approve. CNC controllers that expose telemetry over Modbus TCP always had this data on board. The OT-network policies that protect the plant are what kept it stranded there. A direct point-to-point Ethernet cable from the OPTA to the machine control reads the registers locally, and the Notecard inside Blues Wireless for OPTA carries them to the OEM's cloud on its own independent uplink. That's the raw material for a credible equipment-as-a-service offer. Utilization data is granular enough to bill by the spindle-hour, the alarm telemetry can back a proactive-service contract, and the OEE components are accurate enough to benchmark the fleet. What's left is a validated, vendor-specific Modbus TCP register map for each target CNC model, which is a commissioning task that doesn't touch the architecture.

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
    • Power and sync strategy
    • Retry and error handling
    • Key code snippet 1: Notecard periodic sync configuration
    • Key code snippet 2: Modbus TCP batch read (6 registers, 1 transaction)
    • Key code snippet 3: Immediate alarm Note with sync:true
  • 8. Build and Flash
  • 9. Data Flow
  • 10. Validation and Testing
  • 11. Troubleshooting
  • 12. Limitations and Next Steps
    • Simplified for this reference design
    • Production Next Steps
  • 13. Summary

Resources

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