Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e11ac0c
rs274ngc: native lathe T0101 and tool-table wear
jorgeviloria Sep 19, 2026
4a22fc2
gladevcp: edit native WX/WZ wear in tooledit
jorgeviloria Sep 19, 2026
7190474
rs274ngc, task, tooldata: gate wear migration, sync wear over NML
jorgeviloria Sep 19, 2026
16ddf74
gladevcp, qtvcp, tcl: preserve and edit native wear in tool editors
jorgeviloria Sep 19, 2026
bc9f4c0
docs: document LATHE_TXXXX wear, migration and editor support
jorgeviloria Sep 19, 2026
7aa0011
tests/interp: cover G10 L12 wear, cutter comp and LATHE_TXXXX off
jorgeviloria Sep 19, 2026
bec67cc
iocontrol: add toolchanger fault/reason pins
jorgeviloria Sep 19, 2026
a709d11
turret: add the Python controller, QtPyVCP panel and simulator
jorgeviloria Sep 19, 2026
b9244ea
turret: add the compiled userspace component (F2)
jorgeviloria Sep 19, 2026
9255acf
turret: reference on power-up and manual jog
jorgeviloria Sep 19, 2026
b801700
turret: robust position 1 detection and service jog
jorgeviloria Sep 19, 2026
57c057b
turret: latch the iocontrol handshake for Fanuc T changes
jorgeviloria Sep 19, 2026
2aa1d59
turret: fix T/M6 handshake conflicts
jorgeviloria Sep 19, 2026
424fec0
configs: add a turret install template and checklist
jorgeviloria Sep 19, 2026
47ddab6
turret: installation and sampling fixes from the final review
jorgeviloria Sep 19, 2026
cd01111
turret: logic review fixes (safe fault behavior)
jorgeviloria Sep 19, 2026
137ed44
rs274ngc: Fanuc lathe G-code system A (G90/G92/G94, G50, G98/G99)
jorgeviloria Sep 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions configs/sim/turret/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Turret simulation (mechanism + manager, no nets)
================================================

Run (F1, Python manager):

./run_sim.sh

or the compiled component (F2, after `halcompile --install
src/hal/user_comps/turret.comp` and generating `turret.conf`):

python3 -m turret.genconf turret.json
./run_sim.sh f2

That loads `sim_turret.py` (mechanism model) and `turret.manager`
(controller) inside `halrun`. The manager drives the simulator through
`hal.set_p`/`hal.get_value`, so changing the pin mapping only requires
editing `turret.json`.

Useful checks inside halrun (or `halcmd` in another terminal while
LinuxCNC runs):

show pin turret.*
show pin sim_turret.*
setp turret.test-enable 1
set turret.home 1 # re-reference (pulse)
set turret.release 1 # service: unclamp and hold
set turret.lock 1 # service: clamp again
setp turret.pocket-request 3 ; set turret.prepare 1 # fake a T3

The QtPyVCP panel (`qtvcp turret`) monitors the same pins and can be
embedded as a tab with, in the [DISPLAY] section:

EMBED_TAB_NAME = Torreta
EMBED_TAB_COMMAND = qtvcp turret
EMBED_TAB_LOCATION = tabWidget_utilities
17 changes: 17 additions & 0 deletions configs/sim/turret/run_sim.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash
# Run the turret simulation (mechanism + controller) under halrun.
#
# ./run_sim.sh # F1: Python manager (turret.manager)
# ./run_sim.sh f2 # F2: compiled component (loadusr turret)
set -e
cd "$(dirname "$0")"

# RIP environment (interpreter and lib/python on PYTHONPATH)
if [ -f ../../../scripts/rip-environment ]; then
. ../../../scripts/rip-environment >/dev/null
fi

if [ "$1" = "f2" ]; then
exec halrun -f turret_sim_f2.hal
fi
exec halrun -f turret_sim.hal
159 changes: 159 additions & 0 deletions configs/sim/turret/sim_turret.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#!/usr/bin/env python3
"""HAL simulator for a hydraulic single-solenoid lathe turret.

It behaves like the real mechanism so the ``turret.manager`` can be
tested without a machine:

* ``motor`` (in) : energized = rotate one direction
* ``unclamp`` (in) : ON = clamp released (spring closes)
* ``pos1`` (out) : station 1 index sensor
* ``strobe`` (out) : pulse per station while rotating
* ``changepos`` (out): turret seated, clamp allowed
* ``clamped`` (out) : clamp closed
* ``station`` (out) : current station (simulation only)

Everything runs in a ``hal.component``; the manager drives/reads it with
``hal.set_p`` / ``hal.get_value`` so no HAL nets are needed.
"""

import argparse
import sys
import time

import hal

LOOP_PERIOD = 0.005


class TurretSim:
def __init__(self, stations=8, pulse_period=0.08, coast_pulses=1,
start=2, unclamp_delay=0.05, clamp_delay=0.10,
changepos_delay=0.10, pulse_width=0.03):
self.stations = stations
self.pulse_period = pulse_period
self.coast_pulses = coast_pulses
self.station = start
self.unclamp_delay = unclamp_delay
self.clamp_delay = clamp_delay
self.changepos_delay = changepos_delay
self.pulse_width = pulse_width

self.comp = hal.component("sim_turret")
self.motor = self.comp.newpin("motor", hal.Type.BIT, hal.Dir.IN)
self.unclamp = self.comp.newpin("unclamp", hal.Type.BIT, hal.Dir.IN)
self.pos1 = self.comp.newpin("pos1", hal.Type.BIT, hal.Dir.OUT)
self.strobe = self.comp.newpin("strobe", hal.Type.BIT, hal.Dir.OUT)
self.changepos = self.comp.newpin("changepos", hal.Type.BIT, hal.Dir.OUT)
self.clamped = self.comp.newpin("clamped", hal.Type.BIT, hal.Dir.OUT)
self.station_pin = self.comp.newpin("station", hal.Type.S32, hal.Dir.OUT)

self._pulse_next = None
self._strobe_until = None
self._coast_left = 0
self._motor_prev = False
self._clamped = True
self._unclamp_since = None
self._clamp_since = None
self._stop_since = None

# initial output values (powered up, clamped, seated)
self.pos1.value = 1 if self.station == 1 else 0
self.strobe.value = 0
self.changepos.value = 1
self.clamped.value = 1
self.station_pin.value = self.station

# ------------------------------------------------------------------
def tick(self, now, dt):
motor = bool(self.motor.value)
unclamp = bool(self.unclamp.value)

# clamp / unclamp dynamics
if unclamp:
if self._unclamp_since is None:
self._unclamp_since = now
if (now - self._unclamp_since) >= self.unclamp_delay:
self._clamped = False
self._clamp_since = None
else:
self._unclamp_since = None
if self._clamped:
self._clamp_since = None
elif self._clamp_since is None:
self._clamp_since = now
if self._clamp_since is not None and \
(now - self._clamp_since) >= self.clamp_delay:
self._clamped = True

# rotation and coast
if motor and not self._motor_prev:
self._pulse_next = now + self.pulse_period
self._stop_since = None
if not motor and self._motor_prev:
self._coast_left = self.coast_pulses
self._pulse_next = (now + self.pulse_period
if self._coast_left else None)
self._stop_since = None
self._motor_prev = motor

strobe = False
if self._strobe_until is not None and now < self._strobe_until:
strobe = True
elif self._pulse_next is not None and now >= self._pulse_next:
strobe = True
self._strobe_until = now + self.pulse_width
self.station = (self.station % self.stations) + 1
self._stop_since = None
if motor:
self._pulse_next = now + self.pulse_period
elif self._coast_left > 0:
self._coast_left -= 1
self._pulse_next = (now + self.pulse_period
if self._coast_left else None)
else:
self._pulse_next = None
elif self._pulse_next is None and not motor:
if self._stop_since is None:
self._stop_since = now

moving = motor or self._coast_left > 0 or self._pulse_next is not None
if not moving and self._stop_since is None:
self._stop_since = now
located = (not moving and self._stop_since is not None and
(now - self._stop_since) >= self.changepos_delay)

self.pos1.value = 1 if self.station == 1 else 0
self.strobe.value = 1 if strobe else 0
self.changepos.value = 1 if located else 0
self.clamped.value = 1 if self._clamped else 0
self.station_pin.value = self.station

def run(self):
self.comp.ready()
last = time.monotonic()
try:
while True:
now = time.monotonic()
dt = now - last
last = now
self.tick(now, min(dt, 0.1))
time.sleep(LOOP_PERIOD)
except KeyboardInterrupt:
pass


def main(argv=None):
parser = argparse.ArgumentParser(description="turret simulator")
parser.add_argument("--stations", type=int, default=8)
parser.add_argument("--pulse-period", type=float, default=0.08)
parser.add_argument("--coast-pulses", type=int, default=1)
parser.add_argument("--start", type=int, default=2)
args = parser.parse_args(argv)
sim = TurretSim(stations=args.stations, pulse_period=args.pulse_period,
coast_pulses=args.coast_pulses, start=args.start)
sim.run()
return 0


if __name__ == "__main__":
sys.exit(main())
46 changes: 46 additions & 0 deletions configs/sim/turret/turret.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# turret configuration (generated from turret.json)
stations = 8
pins.pos1 = sim_turret.pos1
pins.strobe = sim_turret.strobe
pins.changepos = sim_turret.changepos
pins.clamped = sim_turret.clamped
pins.motor = sim_turret.motor
pins.unclamp = sim_turret.unclamp
pins.clamp = none
pins.interlock = iocontrol.0.user-enable-out
pins.watchdog_pet = none
pins.tool_prepare = iocontrol.0.tool-prepare
pins.tool_prepared = iocontrol.0.tool-prepared
pins.tool_change = iocontrol.0.tool-change
pins.tool_changed = iocontrol.0.tool-changed
pins.tool_prep_pocket = iocontrol.0.tool-prep-pocket
pins.tool_prep_number = iocontrol.0.tool-prep-number
pins.toolchanger_fault = iocontrol.0.toolchanger-fault
pins.toolchanger_reason = iocontrol.0.toolchanger-reason
timing.strobe_filter_ms = 3
timing.unclamp_timeout = 2
timing.home_timeout = 20
timing.rotate_timeout = 20
timing.settle_time = 0.4
timing.locate_timeout = 1.5
timing.locate_stable_ms = 150
timing.clamp_timeout = 2
timing.max_retries = 2
timing.lead_pulses = 1
timing.lead_auto = 1
timing.jog_timeout = 10
logic.clamped_inverted = 0
logic.require_changepos = 1
logic.unclamp_before_rotate = 1
logic.interlock_required = 0
logic.abort_on_fault = 0
logic.test_enable_required = 1
maint.1.name = Pinza: revisar sellos y presion
maint.1.every_changes = 50000
maint.1.every_hours = 0
maint.2.name = Engrase de la corona
maint.2.every_changes = 500000
maint.2.every_hours = 0
maint.3.name = Filtro hidraulico
maint.3.every_changes = 0
maint.3.every_hours = 2000
50 changes: 50 additions & 0 deletions configs/sim/turret/turret.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"version": 1,
"stations": 8,
"mode": "hydraulic-single-solenoid",
"pins": {
"pos1": "sim_turret.pos1",
"strobe": "sim_turret.strobe",
"changepos": "sim_turret.changepos",
"clamped": "sim_turret.clamped",
"motor": "sim_turret.motor",
"unclamp": "sim_turret.unclamp",
"clamp": null,
"interlock": "iocontrol.0.user-enable-out",
"watchdog_pet": null,
"tool_prepare": "iocontrol.0.tool-prepare",
"tool_prepared": "iocontrol.0.tool-prepared",
"tool_change": "iocontrol.0.tool-change",
"tool_changed": "iocontrol.0.tool-changed",
"tool_prep_pocket": "iocontrol.0.tool-prep-pocket",
"tool_prep_number": "iocontrol.0.tool-prep-number",
"toolchanger_fault": "iocontrol.0.toolchanger-fault",
"toolchanger_reason": "iocontrol.0.toolchanger-reason"
},
"timing": {
"strobe_filter_ms": 3.0,
"unclamp_timeout": 2.0,
"home_timeout": 20.0,
"rotate_timeout": 20.0,
"settle_time": 0.4,
"locate_timeout": 1.5,
"locate_stable_ms": 150.0,
"clamp_timeout": 2.0,
"max_retries": 2,
"lead_pulses": 1,
"lead_auto": true
},
"logic": {
"clamped_inverted": false,
"require_changepos": true,
"unclamp_before_rotate": true,
"interlock_required": false,
"abort_on_fault": false,
"test_enable_required": true
},
"maintenance": [
{"code": 1, "name": "Pinza: revisar sellos y presion", "every_changes": 50000, "every_hours": 0.0},
{"code": 2, "name": "Engrase de la corona", "every_changes": 500000, "every_hours": 0.0},
{"code": 3, "name": "Filtro hidraulico", "every_changes": 0, "every_hours": 2000.0}
]
}
15 changes: 15 additions & 0 deletions configs/sim/turret/turret_sim.hal
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Turret simulation: mechanism + manager (no HAL nets required).
#
# The manager reads/writes the simulator pins directly through the HAL
# Python API, so the pin mapping lives in turret.json and can be changed
# live without restarting LinuxCNC.

loadusr -W python3 sim_turret.py --stations 8 --coast-pulses 1 --start 2
loadusr -W python3 -m turret.manager --config turret.json

# Optional hardware watchdog: pet pin is disabled in turret.json; wire it
# in a real machine so a dead manager drops the valve relay, e.g.:
# loadrt watchdog num_inputs=1
# setp watchdog.0.timeout 0.5
# net turret-heartbeat turret.watchdog-pet watchdog.0.input-0
# net valve-relay watchdog.0.output hm2_7i76e.0.7i76.0.0.output-07
15 changes: 15 additions & 0 deletions configs/sim/turret/turret_sim_f2.hal
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Turret simulation with the compiled component (F2).
#
# The F2 component reads/writes the same pins by name (no HAL nets
# needed) from turret.conf, generated from turret.json with:
#
# python3 -m turret.genconf turret.json
#
# Build/install the component first (from the source tree):
#
# halcompile --install src/hal/user_comps/turret.comp
#
# then run this file.

loadusr -W python3 sim_turret.py --stations 8 --coast-pulses 1 --start 2
loadusr -W turret --config turret.conf
Loading
Loading