Vibe coding example: phone alerts for a CNC router

A real conversation between Jon and Claude in which a non-trivial change to an open-source program (bCNC) went from a one-sentence wish to a tested, shareable patch.

The result: bCNC now sends phone notifications (via free ntfy.sh) when a job finishes, with the G-code file name and duration; when a job is stopped partway; and with urgent, rate-limited, repeating alerts when the controller goes into an alarm. It shipped as a diff, patched files, an explanation of every change, and a testing checklist. Each user supplies their own topic with --ntfy-topic, an environment variable, or a small file.

What made it work

The conversation (condensed)

Jon's messages are verbatim, except that a repeated phrase from a dictation error in the first one was removed. Assistant replies are summarized, and short exchanges (thanks, "let me test this") are omitted. Private details, such as the real notification topic, have been removed.

Jon

I use the program BCNC to control a CNC router. I would like to add the capability to alert me When the current job has finished. Is this something you can help with?

Claude

Yes. It listed three approaches: a built-in hook, a small edit to Sender.py where a run ends, or an external watcher script. It suggested ntfy.sh or Pushover for phone alerts and asked how Jon wanted to be alerted and how bCNC was installed.

Jon

I would like a phone notification

Claude

Recommended ntfy (free, no account), with a long hard-to-guess topic name acting as the password. It gave a small function that posts the message from a background thread, so a slow network never stalls the machine, and one line to add to runEnded. It flagged that the method structure was from memory and could differ by version.

def notify_phone(msg):
    def _send():
        try:
            req = urllib.request.Request(
                "https://ntfy.sh/YOUR-TOPIC", data=msg.encode("utf-8"),
                headers={"Title": "bCNC"}, method="POST")
            urllib.request.urlopen(req, timeout=10)
        except Exception:
            pass  # never let a failed alert interrupt the machine
    threading.Thread(target=_send, daemon=True).start()
Jon

Can you save these instructions in a web page for me? I run bcnc on another machine, and need to implement the changes there

Claude

Published the instructions as a page with copy buttons on the code blocks.

Jon

Thank you for the code - I incorporated it, and it worked great! I wonder if it would be possible to add a few features:

Claude

Noticed the message was cut off, and offered likely ideas: error and stop alerts, job duration, file name, higher-priority alarm alerts.

Jon

I would like the g-code file name and the job duration - thanks!

Claude

Gave the small edits (record a start time, build "bracket_v2.nc finished in 42m 10s"), noting that pauses count toward the duration and that method names were again from memory. Updated the web page on request.

Jon

Could you also add: Alerts when a job errors out or is stopped partway; A job duration in the notification; The G-code file name in the message; Higher-priority or repeating alerts for problems like an alarm state

Claude

Added stop alerts and high-priority controller-error alerts. It said plainly that the error hook was the shakiest part because it depended on the bCNC version, and asked for the real code if it didn't match.

Jon

If I upload a copy of the original version of Sender.py, could you create a diff file that adds these changes?

Claude

Yes, and explained it would also fix the weak spots, since it could use the real method names instead of guessing.

Jon

(uploaded Sender.py)

Claude

Read the real file and found its guesses were partly wrong: stopping a job doesn't pass through runEnded, and alarms are best caught in controllerStateChange, not by watching error lines. That also made repeating alarm alerts possible. It built a unified diff, confirmed the diff applied cleanly and compiled, and ran the helper logic in small simulations. It listed what remained uncovered: GRBL's own error:N lines are parsed in a different file.

Jon

Could you also provide the new Sender.py file with the diff applied?

Claude

Provided it, with a warning that it only matches the bCNC version the diff was made from.

Jon

Could you save the instructions for testing to an HTML file?

Claude

Created a checklist with a box and an expected result for each test (finished, stopped, alarm, alarm cleared), plus troubleshooting.

Jon

And finally, could you save the instructions for the changes needed to Sender.py, with the testing instructions at the end? This will users understand the changes that are in the diff file

Claude

Built a guide explaining each change, with the code snippets taken directly from the patched file so they could not drift from the diff, followed by the testing section. It replaced the earlier published page, whose instructions were now out of date.

Jon

One concern I have is that I'd like to share the diff and instructions with other users, so don't want the ntfy topic name "[redacted]" hard-coded into the diff or instructions. Is there a way to supply the topic name as a parameter to the diff, and use a more generic topic name in the instructions (like "your_name_cnc_random_string" or something)?

Claude

Explained that a diff can't take parameters, so it removed the topic from the code entirely. bCNC now reads it from an environment variable or a small file, and sends nothing if neither exists, so people who skip setup don't all publish to the same public topic. The instructions gained a setup section with a one-line command to generate a random topic.

Jon

This is nice - would it also be possible to add ntfy_topic as a parameter that is passed into bCNC when it is run? In this way different users using the same machine can get separate notifications

Claude

Said the environment variable already covered much of this, but a real command-line option needed the file where bCNC parses its arguments, and asked for it rather than guessing.

Jon

(uploaded __main__.py)

Claude

Added --ntfy-topic (in --help, and ahead of the environment variable and file in priority). It tested the option with stubbed modules and extended the diff to cover both files.

Jon

I tested, and when an alarm is triggered the notification loop sends many alarm messages in quick succession, exceeding the ntfy server's rate limit. Is there a reason why these would be sent at such a rapid rate?

Claude

Owned the bug: every call to the alarm function restarted the series and sent an alert immediately, which is a flood if the controller reports "Alarm" repeatedly. It named the likely cause and a way to confirm it from the console. The fix made the call do nothing while a series is already running and enforced a minimum gap between alerts. It tested the fix with 1,000 rapid calls and with rapid flapping.

Jon

I checked, and the alarm is in fact repeatedly triggered while the alarm condition is present. Let me test the new code with the rate limiting.

Claude

Confirmed that matched the diagnosis and said what to expect: one alert when the alarm starts, then a handful of repeats.

Jon

Tested, and it works fine - thanks!!

What was produced

Takeaway: the first answer was a reasonable guess, and the real file corrected it. The value came from the loop of asking for something concrete, giving the AI the real material, checking its work, and bringing back what actually happened.