Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Binary file modified Codesys/ArfBot.project
Binary file not shown.
10,983 changes: 10,904 additions & 79 deletions Codesys/ArfBot.xml

Large diffs are not rendered by default.

77 changes: 77 additions & 0 deletions Codesys/Scripts/PreparePlcCommit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Stamp GVL_Version.sPlcVersion from git, then export Codesys/ArfBot.xml.
#
# Daily commit helper: Tools -> Scripting -> Execute Script File
# Or: python Codesys\Scripts\RunCodesysScript.py --script Codesys\Scripts\PreparePlcCommit.py --project Codesys\ArfBot.project --no-ui --text-prompts
from __future__ import print_function
import os
import sys
import traceback

_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _SCRIPT_DIR)
import StampPlcVersion as stamp

stamp.bind_host(sys.modules.get("__main__"))
if not (len(sys.argv) > 2 and sys.argv[2]):
stamp.LOG_FILE = os.path.join(_SCRIPT_DIR, "PreparePlcCommit.out.txt")


def export_plcopen_xml(proj):
filename = str(os.path.splitext(proj.path)[0] + ".xml")
stamp.emit("Exporting PLCopen XML: {0}".format(filename))
objects = proj.get_children()
proj.export_xml(
objects,
path=filename,
recursive=True,
export_folder_structure=True,
declarations_as_plaintext=True,
)
stamp.emit("XML export done")


def main():
stamp.emit("PreparePlcCommit starting")
proj = stamp.ensure_project()
stamp.emit("Project: {0}".format(proj.path))

git_root = stamp.find_git_root(proj.path)
stamp.emit("Git root: {0}".format(git_root))

version = stamp.describe_version(git_root)
if len(version) > stamp.MAX_VERSION_LEN:
raise RuntimeError(
"Version '{0}' is {1} chars; STRING({2}) max".format(
version, len(version), stamp.MAX_VERSION_LEN
)
)
stamp.emit("sPlcVersion := '{0}'".format(version))

gvl = stamp.find_textual(proj, "GVL_Version")
if gvl is None or not gvl.has_textual_declaration:
raise RuntimeError("GVL_Version not found or has no textual declaration")

old = gvl.textual_declaration.text
new = stamp.stamp_declaration(old, version)
if new == old:
stamp.emit("GVL_Version.sPlcVersion already set")
else:
gvl.textual_declaration.replace(new_text=new)
stamp.emit("Updated GVL_Version.sPlcVersion")

proj.save()
stamp.emit("Project saved")
export_plcopen_xml(proj)
stamp.emit("script finished.")


try:
main()
except Exception as exc:
stamp.emit("ERROR: {0}".format(exc))
stamp.emit(traceback.format_exc())
stamp.write_log()
stamp.maybe_exit(1)
else:
stamp.write_log()
stamp.maybe_exit(0)
15 changes: 15 additions & 0 deletions Codesys/Scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,28 @@ Example:
python Codesys\Scripts\RunCodesysScript.py --script Codesys\Scripts\ListDeviceTree.py --project Codesys\ArfBot.project --no-ui --text-prompts
```

## Prepare a PLC commit (stamp + export)

With the ArfBot project open:

**Tools → Scripting → Execute Script File…** → `Codesys/Scripts/PreparePlcCommit.py`

That sets `GVL_Version.sPlcVersion` to `<tag-from-main>-<n>-g<sha>[-dirty]`, saves `ArfBot.project`, and exports `Codesys/ArfBot.xml`. Then `git add` both files and commit. Download if you want the HMI to show the new string.

The tag is `git describe --tags --abbrev=0 main` (falls back to `origin/main`). The hash and commit count are from the branch you have checked out. `-dirty` is included when tracked files differ from `HEAD` (computed before the GVL write).

Stamp-only: `StampPlcVersion.py`. Export-only: `PLCOpenExport.py`.

## Included Scripts
- `RunCodesysScript.py`: Python wrapper that detects the local CODESYS install and launches a script through the CODESYS command line.
- `ListDeviceTree.py`: Opens the target project if needed and writes the CODESYS device tree to `Codesys/Scripts/ListDeviceTree.out.txt`.
- `PatchTuningDeadTime.py`: Writes the deadtime-suggestion logic into `_00_Main` / `_M_Tuning` and relabels the Tuning DeadTime field. ST sources live in `Codesys/Scripts/st/`.
- `parse_retain.py`: Decodes `BackupRetain.ret` / `Application.ret` program data and regenerates `st/M_BuildTests_impl.st`.
- `PatchBuildTests.py`: Writes `st/M_BuildTests_impl.st` into `_00_Main.M_BuildTests` and saves `ArfBot.project`. Does not re-export PLCopen XML (that export changes format).
- `PatchLicenseStatus.py`: Creates/updates `FB_LicenseStatus`, wires `GVL.LicenseStatus` and `_00_Main`, adds Component Manager + CmpEventMgr, then builds.
- `PreparePlcCommit.py`: Daily helper — stamps `sPlcVersion` then exports `ArfBot.xml`. Run from **Tools → Scripting → Execute Script File**.
- `StampPlcVersion.py`: Stamp-only (`GVL_Version.sPlcVersion` from git).
- `PLCOpenExport.py`: Export-only PLCopen XML next to the open `.project`.

## Temp scripts
One-shot probes, dumps, and experiments go in `Codesys/Scripts/temp/` (gitignored). Do not commit them.
Expand Down
262 changes: 262 additions & 0 deletions Codesys/Scripts/StampPlcVersion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
# Stamp GVL_Version.sPlcVersion from git.
#
# Tag comes from main (where releases are tagged). The commit and dirty flag
# come from the current HEAD (dev, a feature branch, etc.).
#
# Format matches git describe: <tag>-<n>-g<sha>[-dirty]
#
# Run from CODESYS: Tools -> Scripting -> Execute Script File
# Or: python Codesys\Scripts\RunCodesysScript.py --script Codesys\Scripts\StampPlcVersion.py --project Codesys\ArfBot.project --no-ui --text-prompts
from __future__ import print_function
import os
import re
import sys
import traceback

from System.Diagnostics import Process, ProcessStartInfo


MAX_VERSION_LEN = 80
TAG_REFS = ("main", "origin/main", "master", "origin/master")
ALREADY_OPEN = False
LOG_LINES = []


def clean_arg(value):
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
return value[1:-1]
return value


def log_path():
if len(sys.argv) > 2 and sys.argv[2]:
return clean_arg(sys.argv[2])
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "StampPlcVersion.out.txt")


LOG_FILE = log_path()


def emit(line):
print(line)
LOG_LINES.append(line)


def write_log():
directory = os.path.dirname(LOG_FILE)
if directory and not os.path.isdir(directory):
os.makedirs(directory)
handle = open(LOG_FILE, "w")
handle.write("\n".join(LOG_LINES) + "\n")
handle.close()


def quote_arg(arg):
arg = str(arg)
if not arg or any(ch in arg for ch in ' \t"'):
return '"' + arg.replace('"', '\\"') + '"'
return arg


def run_git(args, cwd, allowed_codes=(0,)):
psi = ProcessStartInfo()
psi.FileName = "git.exe"
psi.Arguments = " ".join([quote_arg(a) for a in args])
psi.WorkingDirectory = cwd
psi.RedirectStandardOutput = True
psi.RedirectStandardError = True
psi.UseShellExecute = False
psi.CreateNoWindow = True

proc = Process()
proc.StartInfo = psi
if not proc.Start():
raise RuntimeError("Could not start git.exe")
out = proc.StandardOutput.ReadToEnd().strip()
err = proc.StandardError.ReadToEnd().strip()
proc.WaitForExit()
code = proc.ExitCode
proc.Close()
if code not in allowed_codes:
raise RuntimeError("git {0} failed in {1} (exit {2}): {3}".format(
" ".join(args), cwd, code, err or out or "no output"))
return out, code


def find_git_root(start):
path = os.path.abspath(start)
if os.path.isfile(path):
path = os.path.dirname(path)
while True:
if os.path.isdir(os.path.join(path, ".git")) or os.path.isfile(os.path.join(path, ".git")):
return path
parent = os.path.dirname(path)
if parent == path:
raise RuntimeError("No git repository found above {0}".format(start))
path = parent


def bind_host(host=None):
# CODESYS injects projects/system only into the executed script, not imports.
if host is None:
host = sys.modules.get("__main__")
if host is None:
return
g = globals()
for name in ("projects", "system"):
if hasattr(host, name):
g[name] = getattr(host, name)


def launched_headless():
return len(sys.argv) > 1 and str(sys.argv[1]).lower().endswith(".project")


def maybe_exit(code):
if launched_headless() and not ALREADY_OPEN:
try:
system.exit(code)
except Exception:
pass


def find_textual(root, name):
matches = root.find(name, recursive=True)
if not matches:
return None
for obj in matches:
try:
if obj.has_textual_declaration:
return obj
except Exception:
pass
return matches[0]


def ensure_project():
global ALREADY_OPEN
bind_host()
if projects.primary is not None:
ALREADY_OPEN = True
return projects.primary
if len(sys.argv) < 2 or not sys.argv[1]:
raise RuntimeError("No project is open and no project path was provided.")
project_path = os.path.abspath(clean_arg(sys.argv[1]))
proj = projects.open(project_path, primary=True)
if proj is None:
raise RuntimeError("Failed to open project: {0}".format(project_path))
return proj


def latest_tag_on_main(git_root):
last_err = None
for ref in TAG_REFS:
try:
tag, _code = run_git(["describe", "--tags", "--abbrev=0", ref], git_root)
if tag:
emit("Tag from {0}: {1}".format(ref, tag))
return tag, ref
except Exception as exc:
last_err = exc
emit("No tag on {0}: {1}".format(ref, exc))
raise RuntimeError("Could not read a tag from main ({0})".format(last_err))


def is_dirty(git_root):
porcelain, _code = run_git(["status", "--porcelain", "-uno"], git_root)
return porcelain != ""


def describe_version(git_root):
tag, ref = latest_tag_on_main(git_root)
sha, _code = run_git(["rev-parse", "--short", "HEAD"], git_root)
branch, _code = run_git(["rev-parse", "--abbrev-ref", "HEAD"], git_root)
count, _code = run_git(["rev-list", "--count", "{0}..HEAD".format(tag)], git_root)
dirty = is_dirty(git_root)

_out, ancestor_code = run_git(
["merge-base", "--is-ancestor", tag, "HEAD"],
git_root,
allowed_codes=(0, 1),
)
if ancestor_code != 0:
emit("Note: tag {0} is not in this branch history. Count {1} is commits on HEAD not reachable from that tag.".format(tag, count))

if count == "0":
version = tag
else:
version = "{0}-{1}-g{2}".format(tag, count, sha)
if dirty:
version = version + "-dirty"

emit("Branch: {0}".format(branch))
emit("HEAD: {0}".format(sha))
emit("Commits since {0} ({1}): {2}".format(tag, ref, count))
emit("Dirty: {0}".format(dirty))
return version


def stamp_declaration(text, version):
quoted = version.replace("'", "")
pattern = r"(sPlcVersion\s*:\s*STRING(?:\s*\(\s*\d+\s*\))?\s*:=\s*)'[^']*'"
new_text, n = re.subn(pattern, r"\g<1>'{0}'".format(quoted), text, count=1)
if n == 1:
return new_text
pattern_bare = r"(sPlcVersion\s*:\s*STRING(?:\s*\(\s*\d+\s*\))?\s*;)"
new_text, n = re.subn(
pattern_bare,
"sPlcVersion : STRING({0}) := '{1}';".format(MAX_VERSION_LEN, quoted),
text,
count=1,
)
if n == 1:
return new_text
raise RuntimeError("Could not find sPlcVersion in GVL_Version declaration")


def main():
emit("StampPlcVersion starting")
proj = ensure_project()
emit("Project: {0}".format(proj.path))

git_root = find_git_root(proj.path)
emit("Git root: {0}".format(git_root))

version = describe_version(git_root)
if len(version) > MAX_VERSION_LEN:
raise RuntimeError("Version '{0}' is {1} chars; STRING({2}) max".format(version, len(version), MAX_VERSION_LEN))
emit("sPlcVersion := '{0}'".format(version))

gvl = find_textual(proj, "GVL_Version")
if gvl is None or not gvl.has_textual_declaration:
raise RuntimeError("GVL_Version not found or has no textual declaration")

old = gvl.textual_declaration.text
new = stamp_declaration(old, version)
if new == old:
emit("GVL_Version.sPlcVersion already set")
else:
gvl.textual_declaration.replace(new_text=new)
emit("Updated GVL_Version.sPlcVersion")

proj.save()
emit("Project saved")
emit("script finished.")


def run():
try:
main()
except Exception as exc:
emit("ERROR: {0}".format(exc))
emit(traceback.format_exc())
write_log()
maybe_exit(1)
return
write_log()
maybe_exit(0)


if __name__ == "__main__":
run()
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ If you already have ArfBotOS running and have it connected to a robot, you will
## The Robot
In theory, you can use any 6 axis robot that you would like with ArfBotOS. However, you will need to integrate the connections to the servos and limit switches. Additionally, you will need recompile ArfBotOS with the Denavit-Hartenburg (DH) parmeters for the bot of your choice. I chose to use the fantastic open source AR4 robot designed and developed by Chris Annin of [Annin Robotics](https://www.anninrobotics.com/)

Instructions on how to integrate the AR4 with ArfBotOS can be found [here](https://github.com/dalethomas81/ArfBotOS/blob/main/Robots/AR4/README.MD).
Instructions on how to integrate the AR4 with ArfBotOS can be found [here](https://github.com/dalethomas81/ArfBotOS/blob/main/Robots/AR4/README.MD). Setup procedures (Teensy and EasyCAT firmware, drive tuning) live in [`Robots/AR4/Setup`](https://github.com/dalethomas81/ArfBotOS/tree/main/Robots/AR4/Setup).

<img src="https://github.com/dalethomas81/ArfBotOS/blob/main/Robots/AR4/Media/MyRobot.png" alt="menu" width="600"/>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion Robots/AR4/README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
The AR4 is a fantastic open-source 6-axis robot designed by Chris Annin of [Annin Robotics](https://www.anninrobotics.com/). This directory will show you how to interface the AR4 with ArfBotOS.

- The electrical schedmatics will be located [here](/Robots/AR4/Electrical).
- The setup procedure for tuning the drives, mastering the robot and programming the devices (microcontroller and EtherCAT card) will be located [here](/Robots/AR4/Setup).
- The setup procedure for tuning the drives, mastering the robot, and programming the devices (Teensy firmware and EasyCAT EEPROM) is [here](/Robots/AR4/Setup).
- Firmware for the Teensy microcontroller will be located [here](/Robots/AR4/Arduino).

## EtherCAT Motor Control Unit (EMCU) Overview
Expand Down
Loading
Loading