opencplc 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opencplc-0.1.0/PKG-INFO +251 -0
- opencplc-0.1.0/opencplc/__init__.py +13 -0
- opencplc-0.1.0/opencplc/__main__.py +378 -0
- opencplc-0.1.0/opencplc/args.py +161 -0
- opencplc-0.1.0/opencplc/config.py +10 -0
- opencplc-0.1.0/opencplc/host.py +84 -0
- opencplc-0.1.0/opencplc/platforms.py +122 -0
- opencplc-0.1.0/opencplc/project.py +211 -0
- opencplc-0.1.0/opencplc/templates.py +32 -0
- opencplc-0.1.0/opencplc/utils/__init__.py +43 -0
- opencplc-0.1.0/opencplc/utils/common.py +69 -0
- opencplc-0.1.0/opencplc/utils/files.py +98 -0
- opencplc-0.1.0/opencplc/utils/hash.py +30 -0
- opencplc-0.1.0/opencplc/utils/install.py +126 -0
- opencplc-0.1.0/opencplc/utils/network.py +68 -0
- opencplc-0.1.0/opencplc/utils/text.py +127 -0
- opencplc-0.1.0/opencplc/utils/version.py +85 -0
- opencplc-0.1.0/opencplc.egg-info/PKG-INFO +251 -0
- opencplc-0.1.0/opencplc.egg-info/SOURCES.txt +23 -0
- opencplc-0.1.0/opencplc.egg-info/dependency_links.txt +1 -0
- opencplc-0.1.0/opencplc.egg-info/entry_points.txt +2 -0
- opencplc-0.1.0/opencplc.egg-info/top_level.txt +1 -0
- opencplc-0.1.0/pyproject.toml +23 -0
- opencplc-0.1.0/readme.md +240 -0
- opencplc-0.1.0/setup.cfg +4 -0
opencplc-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opencplc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Project configuration and build tool for OpenCPLC
|
|
5
|
+
Author: Xaeian
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/OpenCPLC/Forge
|
|
8
|
+
Keywords: embedded,stm32,opencplc,build,forge
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
## OpenCPLC ⚒️ Forge
|
|
13
|
+
|
|
14
|
+
**Forge** is a console app that makes working with **OpenCPLC** easier. Its job is to set up your environment so you 👨💻developer can focus on building apps instead of fighting with configs and compilation. Available as a **Python [`pip`](https://pypi.org/project/opencplc)** package or standalone **`opencplc.exe`** from 🚀[Releases](https://github.com/OpenCPLC/Forge/releases) _(in that case add its location to system **PATH** manually)_
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install opencplc
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Just pick a folder _(your workspace)_ open [CMD](#-console) and type:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
opencplc -n <project_name> -b <board>
|
|
24
|
+
opencplc -n myapp -b Uno
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This creates a directory _(or directory tree)_ in [projects location](#️-config) `${projects}` matching the name `<project_name>`. Two files are created inside: `main.c` and `main.h`: the minimal project setup. Don't delete them or move to subfolders.
|
|
28
|
+
|
|
29
|
+
When you have more projects, you can switch between them freely:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
opencplc <project_name>
|
|
33
|
+
opencplc myapp
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
You can also pick projects by number from list:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
opencplc -l # show project list
|
|
40
|
+
opencplc 3 # load project #3 from list
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
When creating new project or switching to existing one, all files needed for compilation _(`makefile`, `flash.ld`, ...)_ are regenerated. These transform everything _(project and framework files: `.c`, `.h`, `.s`)_ into binary files `.bin`/`.hex` that can be flashed to the PLC.
|
|
44
|
+
|
|
45
|
+
If you change `PRO_x` config values in **`main.h`** or modify **project structure**:
|
|
46
|
+
|
|
47
|
+
- adding new files,
|
|
48
|
+
- moving files,
|
|
49
|
+
- deleting files,
|
|
50
|
+
- renaming files,
|
|
51
|
+
|
|
52
|
+
you need to reload the project. If project is already active, no need to type its name `-r --reload`:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
opencplc <project_name>
|
|
56
|
+
opencplc -r
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Here _(roughly)_ ends **Forge** job, and further work goes like typical **embedded systems** project using [**✨Make**](#-make).
|
|
60
|
+
|
|
61
|
+
## ✨ Make
|
|
62
|
+
|
|
63
|
+
If you have proper project config and `makefile` generated by ⚒️**Forge**, to build and flash program to PLC just open console in workspace and type:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
make build # build C project to binary
|
|
67
|
+
make flash # upload binary to PLC memory
|
|
68
|
+
# or
|
|
69
|
+
make run # run = build + flash
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`makefile` has few more functions. Full list:
|
|
73
|
+
|
|
74
|
+
- **`make build`** or just **`make`**: Builds C project to `.bin`, `.hex`, `.elf` files
|
|
75
|
+
- **`make flash`**: Uploads program to PLC _(microcontroller)_ memory
|
|
76
|
+
- **`make run`**: Does `make build`, then `make flash`
|
|
77
|
+
- **`make clean`** or `make clr`: Removes built files for active project
|
|
78
|
+
- `make clean_all` or `make clr_all`: Removes built files for all projects
|
|
79
|
+
- `make dist`: Copies `.bin` and `.hex` files to the project folder
|
|
80
|
+
- **`make erase`**: Completely wipes microcontroller memory _(**erase** full chip)_
|
|
81
|
+
|
|
82
|
+
## ⚙️ Config
|
|
83
|
+
|
|
84
|
+
On first run ⚒️Forge creates config file **`opencplc.json`**. It contains:
|
|
85
|
+
|
|
86
|
+
- **`version`**: Default OpenCPLC framework version. This version gets installed. Replaces unspecified `-f --framework`. Value `latest` means newest stable version.
|
|
87
|
+
- `paths`: List of _(relative)_ paths
|
|
88
|
+
- `projects`: Main projects directory. New projects go here. You can also copy projects manually. All projects are detected automatically. Project name is the path after this location.
|
|
89
|
+
- `examples`: Directory with demo examples downloaded from [Demo](https://github.com/OpenCPLC/Demo) repository.
|
|
90
|
+
- `framework`: Directory with all OpenCPLC framework versions. Subdirectories are created for versions like `major.minor.patch`, `develop` or `main`. Each contains files for that framework version. Only needed versions are downloaded.
|
|
91
|
+
- `build`: Directory with built applications
|
|
92
|
+
- `default`: Default values _(`chip`, `flash`, `ram`, `optLevel`)_ for params not passed when creating new project
|
|
93
|
+
- **`pwsh`**: Setting this to `true` makes `makefile` for **PowerShell**. For `false` it's **Bash** version.
|
|
94
|
+
- `available-versions`: List of all available framework versions. Set automatically.
|
|
95
|
+
|
|
96
|
+
## 🤔 How works?
|
|
97
|
+
|
|
98
|
+
First **Forge** installs **GNU Arm Embedded Toolchain**, **OpenOCD**, **Make**, **Git** client and sets system variables if these apps aren't visible from console. For HOST platform, **MinGW** (GCC for Windows) is installed instead of ARM toolchain. If you don't want anyone messing with your system, you can [set it up manually](self-install.md). When ⚒️**Forge** installs missing apps, it asks to reset console because system variables load on startup and new ones were added.
|
|
99
|
+
|
|
100
|
+
Then if needed, it clones OpenCPLC framework from [repository](https://github.com/OpenCPLC/Core) to `${framework}` folder from `opencplc.json`. Version from config or specified with `-f --framework` gets cloned:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
opencplc <project_name> --new -f 1.0.2
|
|
104
|
+
opencplc <project_name> --new -f develop
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 📌 Project versioning
|
|
108
|
+
|
|
109
|
+
Each project stores in `main.h` the framework version it was created with _(definition `PRO_VERSION`)_. When switching to existing project:
|
|
110
|
+
|
|
111
|
+
- If project version differs from current framework, **Forge** tries to download matching version
|
|
112
|
+
- If download fails, warning about potential incompatibility shows up
|
|
113
|
+
- Demo examples `-e --example` always use version saved in project
|
|
114
|
+
|
|
115
|
+
This way old projects can compile even after framework update to newer version.
|
|
116
|
+
|
|
117
|
+
Main **Forge** function is preparing files needed for project:
|
|
118
|
+
|
|
119
|
+
- `flash.ld`: defines RAM and FLASH memory layout _(overwrites, STM32 only)_
|
|
120
|
+
- `makefile`: Contains build, clean and flash rules _(overwrites)_
|
|
121
|
+
- `c_cpp_properties.json`: sets header paths and IntelliSense config in VS Code _(overwrites)_
|
|
122
|
+
- `launch.json`: configures debugging in VSCode _(overwrites)_
|
|
123
|
+
- `tasks.json`: describes tasks like compile or flash _(overwrites)_
|
|
124
|
+
- `settings.json`: sets local editor preferences _(creates once, not overwritten)_
|
|
125
|
+
- `extensions.json`: suggests useful VSCode extensions _(creates once, not overwritten)_
|
|
126
|
+
|
|
127
|
+
There's also bunch of helper functions accessible through smart use of [**🚩flags**](#-flags).
|
|
128
|
+
|
|
129
|
+
### 🗂️ Workspace structure
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
workspace/
|
|
133
|
+
├─ opencplc.json # workspace config
|
|
134
|
+
├─ makefile # active project (generated by Forge)
|
|
135
|
+
├─ flash.ld # linker script (generated by Forge, STM32 only)
|
|
136
|
+
├─ .vscode/ # VSCode config (generated by Forge)
|
|
137
|
+
├─ opencplc/ # framework (downloaded automatically)
|
|
138
|
+
│ ├─ 1.0.3/
|
|
139
|
+
│ ├─ 1.2.0/
|
|
140
|
+
│ └─ develop/
|
|
141
|
+
├─ projects/ # user projects
|
|
142
|
+
│ ├─ myapp/
|
|
143
|
+
│ │ ├─ main.c
|
|
144
|
+
│ │ └─ main.h
|
|
145
|
+
│ └─ firm/app/ # projects can be nested
|
|
146
|
+
│ ├─ main.c
|
|
147
|
+
│ └─ main.h
|
|
148
|
+
├─ examples/ # demo examples
|
|
149
|
+
└─ build/ # compiled binary files
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
If IntelliSense stops working, use `F1` → _C/C++: Reset IntelliSense Database_.
|
|
153
|
+
|
|
154
|
+
## 🖥️ Host
|
|
155
|
+
|
|
156
|
+
Forge supports **Host** platform for developing and testing code on PC (Windows/Linux) without embedded hardware:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
opencplc -n myapp -c Host # desktop project
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
This creates project that compiles with native GCC (MinGW on Windows) instead of ARM toolchain. Useful for:
|
|
163
|
+
|
|
164
|
+
- Testing algorithms and logic without hardware
|
|
165
|
+
- Developing protocol parsers and data processing
|
|
166
|
+
- Unit testing framework components
|
|
167
|
+
- Quick prototyping before deploying to PLC
|
|
168
|
+
|
|
169
|
+
Host platform provides stub implementations for hardware-dependent modules (GPIO, timers, etc.) so code structure remains compatible with STM32 targets.
|
|
170
|
+
|
|
171
|
+
## 🚩 Flags
|
|
172
|
+
|
|
173
|
+
Beyond the basic flags described above, there are a few more worth knowing. Full list:
|
|
174
|
+
|
|
175
|
+
#### Basic
|
|
176
|
+
|
|
177
|
+
- **`name`**: Project name. Default first argument. Also defines the project path: `${projects}/name`, and output files (`.bin`, `.hex`, `.elf`) are tied to it. Can also be a project number from the `-l` list.
|
|
178
|
+
- `-n --new`: Creates a new project with the given name.
|
|
179
|
+
- `-e --example`: Loads a demo example by name from the [Demo](https://github.com/OpenCPLC/Demo) repository.
|
|
180
|
+
- `-r --reload`: Reads the project name and example flag from an existing `makefile`, then regenerates project files. **`name`** is not required.
|
|
181
|
+
- `-d --delete`: Deletes the project with the given **`name`**.
|
|
182
|
+
- `-g --get`: Downloads a project from Git (**GitHub**, **GitLab**, ...) or a remote ZIP and adds it as a new project. The second argument (first is the link) can be a reference (`branch`, `tag`). If **`name`** is not specified, it tries to read it from the `@name` field in `main.h`.
|
|
183
|
+
|
|
184
|
+
#### Hardware config
|
|
185
|
+
|
|
186
|
+
- `-b --board`: PLC board for the new project: `Uno`, `Dio`, `Aio`, `Eco`, `Custom` for a custom design, or `None` for a bare microcontroller. `Custom` provides the PLC layer without peripheral mapping — fill it in manually.
|
|
187
|
+
- `-c --chip`: Microcontroller or platform: `STM32G081`, `STM32G0C1`, `STM32WB55`, `HOST` (compile for PC). Without `-b --board`, the project runs without the PLC layer — only HAL and standard framework libraries. Useful for Nucleo boards or custom hardware.
|
|
188
|
+
- `-m --memory`: Memory in kB: `FLASH RAM [RESERVED]`. `RESERVED` is the memory allocated for config and EEPROM, subtracted from FLASH in the linker file `flash.ld`. _(STM32 only)_
|
|
189
|
+
|
|
190
|
+
#### Build config
|
|
191
|
+
|
|
192
|
+
- `-f --framework`: Framework version: `latest`, `develop`, `1.0.0`. If not provided, read from the `version` field in `opencplc.json`.
|
|
193
|
+
- `-o --opt-level`: Compiler optimization level: `O0` _(debug)_, `Og` _(default)_, `O1`, `O2`, `O3`. Levels `O2` and `O3` show a warning for STM32 _(timing/debugging issues)_ but are allowed for `HOST`.
|
|
194
|
+
|
|
195
|
+
#### Info
|
|
196
|
+
|
|
197
|
+
- `-l --list`: Lists existing projects, or examples when `-e --example` is active.
|
|
198
|
+
- `-i --info`: Returns basic info about the specified or active project, including project and framework versions.
|
|
199
|
+
- `-F --framework-versions`: Lists all available OpenCPLC framework versions.
|
|
200
|
+
- `-v --version`: Shows the ⚒️Forge version and repository link.
|
|
201
|
+
|
|
202
|
+
#### Tools
|
|
203
|
+
|
|
204
|
+
- `-a --assets`: Downloads helper materials for design _(docs, diagrams)_. Optionally accepts a folder name as destination.
|
|
205
|
+
- `-u --update`: Checks for and installs ⚒️Forge updates. Accepts a specific version or `latest`.
|
|
206
|
+
- `-y --yes`: Auto-confirms all prompts _(non-interactive mode)_.
|
|
207
|
+
|
|
208
|
+
#### Hash utilities
|
|
209
|
+
|
|
210
|
+
- `-hl --hash-list`: Generates an enum with DJB2 hashes from a tag list.
|
|
211
|
+
- `-ht --hash-title`: Enum type name for the hash generator.
|
|
212
|
+
- `-hd --hash-define`: Uses `#define` instead of `enum` for hash output.
|
|
213
|
+
|
|
214
|
+
🗑️ Deleting and 💾 copying projects can be done directly from the OS.
|
|
215
|
+
Each project stores all the information it needs in `main.h`, and its presence is auto-detected on startup.
|
|
216
|
+
|
|
217
|
+
## 📟 Console
|
|
218
|
+
|
|
219
|
+
⚒️Forge and ✨Make are console programs. Essential for working with OpenCPLC.
|
|
220
|
+
|
|
221
|
+
System console is available in many apps like **Command Prompt**, **PowerShell**, [**GIT Bash**](https://git-scm.com/downloads), even terminal in [**VSCode**](https://code.visualstudio.com/). If console call returns error, it probably wasn't opened in workspace. Close console and open it in right folder or navigate manually with `cd` command.
|
|
222
|
+
|
|
223
|
+
## 📋 Usage examples
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
# Creating new project
|
|
227
|
+
opencplc -n myapp -b Uno # project for OpenCPLC Uno board
|
|
228
|
+
opencplc -n myapp -b Eco -m 128 36 # project for Eco with 128kB/36kB memory
|
|
229
|
+
opencplc -n myapp -b Custom -c STM32G081 # custom hardware with PLC layer (no peripheral mapping)
|
|
230
|
+
opencplc -n myapp -c STM32G081 # bare-metal project for STM32G081 (e.g. Nucleo)
|
|
231
|
+
opencplc -n myapp -c Host # desktop project (Windows/Linux)
|
|
232
|
+
|
|
233
|
+
# Managing projects
|
|
234
|
+
opencplc myapp # load project 'myapp'
|
|
235
|
+
opencplc 3 # load project #3 from list
|
|
236
|
+
opencplc -r # reload active project
|
|
237
|
+
opencplc -l # list all projects
|
|
238
|
+
opencplc -i # info about active project
|
|
239
|
+
|
|
240
|
+
# Demo examples
|
|
241
|
+
opencplc -e blinky # load example 'blinky'
|
|
242
|
+
opencplc -e -l # list available examples
|
|
243
|
+
|
|
244
|
+
# Downloading projects
|
|
245
|
+
opencplc -g https://github.com/user/repo
|
|
246
|
+
opencplc -g https://github.com/user/repo v1.0.0
|
|
247
|
+
|
|
248
|
+
# Updates
|
|
249
|
+
opencplc -u # update Forge to latest version
|
|
250
|
+
opencplc -F # show available Core versions
|
|
251
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# opencplc/__init__.py
|
|
2
|
+
|
|
3
|
+
from .config import VER
|
|
4
|
+
|
|
5
|
+
__version__ = VER
|
|
6
|
+
__repo__ = "OpenCPLC/Forge"
|
|
7
|
+
__python__ = ">=3.12"
|
|
8
|
+
__description__ = "Project configuration and build tool for OpenCPLC"
|
|
9
|
+
__author__ = "Xaeian"
|
|
10
|
+
__keywords__ = ["embedded", "stm32", "opencplc", "build", "forge"]
|
|
11
|
+
__scripts__ = {
|
|
12
|
+
"opencplc": "opencplc.__main__:main",
|
|
13
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# opencplc/__main__.py
|
|
2
|
+
|
|
3
|
+
import signal, sys
|
|
4
|
+
from xaeian import Print, Color as c, Ico, FILE, DIR, JSON, PATH, replace_end
|
|
5
|
+
from .config import VER, URL_FTP, URL_CORE, URL_FORGE, URL_DEMO
|
|
6
|
+
from .args import flag, load_args, check_flags
|
|
7
|
+
from .platforms import resolve_chip
|
|
8
|
+
from .templates import load_templates
|
|
9
|
+
from .project import generate_project
|
|
10
|
+
from . import utils
|
|
11
|
+
|
|
12
|
+
p = Print()
|
|
13
|
+
|
|
14
|
+
def handle_sigint(signum, frame):
|
|
15
|
+
p.wrn(f"Closing {c.GREY}(Ctrl+C){c.END}...")
|
|
16
|
+
sys.exit(0)
|
|
17
|
+
|
|
18
|
+
signal.signal(signal.SIGINT, handle_sigint)
|
|
19
|
+
|
|
20
|
+
def load_lines(path:str) -> list[str]:
|
|
21
|
+
try: return [ln.rstrip('\n\r') for ln in FILE.load_lines(path)]
|
|
22
|
+
except Exception: return []
|
|
23
|
+
|
|
24
|
+
def main():
|
|
25
|
+
templates = load_templates()
|
|
26
|
+
# Load opencplc.json
|
|
27
|
+
forge_cfg = JSON.load("opencplc.json", templates["opencplc.json"])
|
|
28
|
+
missing = utils.find_missing_keys(templates["opencplc.json"], forge_cfg)
|
|
29
|
+
if missing:
|
|
30
|
+
p.err(f"Missing key {c.BLUE}{missing[0]}{c.END} in {c.ORANGE}opencplc.json{c.END}")
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
# Check available versions
|
|
33
|
+
versions = utils.git_get_refs(URL_CORE, "--ref", use_git=True)
|
|
34
|
+
if versions:
|
|
35
|
+
forge_cfg["available-versions"] = versions
|
|
36
|
+
else:
|
|
37
|
+
p.wrn(f"No internet access or {c.BLUE}GitHub{c.END} is not responding")
|
|
38
|
+
if "available-versions" not in forge_cfg:
|
|
39
|
+
p.err("First run requires network access to fetch available framework versions")
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
JSON.save_pretty("opencplc.json", forge_cfg)
|
|
42
|
+
forge_cfg["version"] = utils.version_real(forge_cfg["version"], forge_cfg["available-versions"][0])
|
|
43
|
+
# Parse arguments
|
|
44
|
+
args = load_args()
|
|
45
|
+
# Print-only actions
|
|
46
|
+
exit_flag = False
|
|
47
|
+
if args.version:
|
|
48
|
+
p.inf(f"OpenCPLC Forge {c.BLUE}{VER}{c.END}")
|
|
49
|
+
p.gap(utils.color_url("https://github.com/OpenCPLC/Forge"))
|
|
50
|
+
exit_flag = True
|
|
51
|
+
if args.framework_versions:
|
|
52
|
+
latest = f" {c.GREY}(latest){c.END}"
|
|
53
|
+
msg = "Framework Versions: "
|
|
54
|
+
color = c.BLUE
|
|
55
|
+
for ver in forge_cfg["available-versions"]:
|
|
56
|
+
msg += f"{color}{ver}{c.END}{latest}, "
|
|
57
|
+
color, latest = c.CYAN, ""
|
|
58
|
+
print(msg.rstrip(", "))
|
|
59
|
+
exit_flag = True
|
|
60
|
+
if args.hash_list:
|
|
61
|
+
print(utils.c_code_enum(args.hash_list, args.hash_title, args.hash_define))
|
|
62
|
+
exit_flag = True
|
|
63
|
+
if args.update:
|
|
64
|
+
new_ver = args.update in ("last", "latest")
|
|
65
|
+
versions = utils.git_get_refs(URL_FORGE, "--tags", use_git=True)
|
|
66
|
+
if not versions:
|
|
67
|
+
p.err(f"No access to {c.BLUE}GitHub{c.END}")
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
target = utils.version_real(args.update, versions[0])
|
|
70
|
+
if target != VER:
|
|
71
|
+
p.inf(f"Installed: {c.ORANGE}{VER}{c.END}")
|
|
72
|
+
p.inf(f"{'Latest' if new_ver else 'Target'}: {c.BLUE}{target}{c.END}")
|
|
73
|
+
p.run(f"{'Update' if new_ver else 'Replace'} required")
|
|
74
|
+
utils.install("forge.exe", f"{URL_FORGE}/releases/download/{target}", ".", args.yes, False)
|
|
75
|
+
else:
|
|
76
|
+
p.ok(f"Forge is at {'latest' if new_ver else 'target'} version {c.BLUE}{VER}{c.END}")
|
|
77
|
+
exit_flag = True
|
|
78
|
+
if args.assets:
|
|
79
|
+
DIR.ensure(args.assets)
|
|
80
|
+
files = [
|
|
81
|
+
"reference-manual-stm32g0x1.pdf", "datasheet-stm32g081rb.pdf",
|
|
82
|
+
"datasheet-stm32g0c1re.pdf", "pinout-nucleo.pdf", "pinout-opencplc.pdf",
|
|
83
|
+
]
|
|
84
|
+
for f in files:
|
|
85
|
+
dst = PATH.resolve(f"{args.assets}/{f}", read=False)
|
|
86
|
+
if not FILE.exists(dst):
|
|
87
|
+
utils.download(f"{URL_FTP}/{f}", dst)
|
|
88
|
+
p.ok(f"Assets downloaded to {c.ORANGE}{args.assets}{c.END}")
|
|
89
|
+
exit_flag = True
|
|
90
|
+
if exit_flag: sys.exit(0)
|
|
91
|
+
# Flag conflict check
|
|
92
|
+
check_flags(args, ("example", flag.e), ("reload", flag.r), ("info", flag.i))
|
|
93
|
+
check_flags(args, ("example", flag.e), ("new", flag.n), ("delete", flag.d), ("get", flag.g))
|
|
94
|
+
args.name, args.new = utils.assign_name(args.name, args.new, flag.n)
|
|
95
|
+
args.name, args.example = utils.assign_name(args.name, args.example, flag.e)
|
|
96
|
+
args.name, args.reload = utils.assign_name(args.name, args.reload, flag.r)
|
|
97
|
+
args.name, args.delete = utils.assign_name(args.name, args.delete, flag.d)
|
|
98
|
+
# Setup paths
|
|
99
|
+
PATHS = forge_cfg["paths"].copy()
|
|
100
|
+
# Validate paths - no path traversal
|
|
101
|
+
for key, path in PATHS.items():
|
|
102
|
+
if ".." in path:
|
|
103
|
+
p.err(f"Invalid path in opencplc.json: {c.ORANGE}{key}{c.END} contains '..'")
|
|
104
|
+
sys.exit(1)
|
|
105
|
+
if path.startswith("/") or (len(path) > 1 and path[1] == ":"):
|
|
106
|
+
p.wrn(f"Absolute path in opencplc.json: {c.ORANGE}{key}={path}{c.END}")
|
|
107
|
+
fw_ver = args.framework or forge_cfg["version"]
|
|
108
|
+
PATHS["fw"] = PATH.resolve(f"{PATHS['framework']}/{fw_ver}", read=False)
|
|
109
|
+
PATHS["pro"] = PATHS["examples"] if args.example else PATHS["projects"]
|
|
110
|
+
# Load existing makefile info
|
|
111
|
+
make_info = None
|
|
112
|
+
if FILE.exists("makefile"):
|
|
113
|
+
lines = load_lines("makefile")
|
|
114
|
+
lines = utils.lines_clear(lines, "#")
|
|
115
|
+
make_info = utils.get_vars(lines, ["NAME", "LIB", "PRO"])
|
|
116
|
+
# Reload/info mode - get name from makefile
|
|
117
|
+
if not args.name and (args.reload or args.info):
|
|
118
|
+
if not make_info:
|
|
119
|
+
p.err(f"No {c.ORANGE}makefile{c.END} found - required for reload and info")
|
|
120
|
+
p.inf(f"Provide project name as positional argument")
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
pro_rel = PATH.local(make_info["PRO"])
|
|
123
|
+
example_rel = PATH.local(PATHS["examples"])
|
|
124
|
+
if pro_rel.startswith(example_rel):
|
|
125
|
+
args.example = True
|
|
126
|
+
PATHS["pro"] = PATHS["examples"]
|
|
127
|
+
args.name = make_info["NAME"]
|
|
128
|
+
# Determine platform early for toolchain installation
|
|
129
|
+
is_embedded = True
|
|
130
|
+
if args.chip and args.chip.lower() == "host":
|
|
131
|
+
is_embedded = False
|
|
132
|
+
# Install toolchains based on platform
|
|
133
|
+
utils.install_toolchains(is_embedded, args.yes)
|
|
134
|
+
if utils.RESET_CONSOLE:
|
|
135
|
+
p.inf("Reset console after finishing work")
|
|
136
|
+
p.inf("This will reload system PATH with newly installed tools")
|
|
137
|
+
sys.exit(0)
|
|
138
|
+
# Verify compiler works
|
|
139
|
+
if not utils.verify_compiler(is_embedded):
|
|
140
|
+
compiler = "arm-none-eabi-gcc" if is_embedded else "gcc"
|
|
141
|
+
p.err(f"Compiler {c.CYAN}{compiler}{c.END} not working")
|
|
142
|
+
p.inf("Check installation and PATH")
|
|
143
|
+
sys.exit(1)
|
|
144
|
+
# Version check and clone framework
|
|
145
|
+
utils.version_check(fw_ver, forge_cfg["available-versions"],
|
|
146
|
+
f"{Ico.RUN} Check version list: {flag.F}")
|
|
147
|
+
utils.git_clone_missing(URL_CORE, PATHS["fw"], fw_ver, args.yes)
|
|
148
|
+
# Verify framework was cloned correctly
|
|
149
|
+
fw_hal = PATH.resolve(f"{PATHS['fw']}/hal", read=False)
|
|
150
|
+
fw_lib = PATH.resolve(f"{PATHS['fw']}/lib", read=False)
|
|
151
|
+
if not PATH.exists(fw_hal) or not PATH.exists(fw_lib):
|
|
152
|
+
p.err(f"Framework {c.VIOLET}{fw_ver}{c.END} is incomplete or corrupted")
|
|
153
|
+
p.inf(f"Try removing {c.ORANGE}{PATHS['fw']}{c.END} and run again")
|
|
154
|
+
sys.exit(1)
|
|
155
|
+
# Remote project download
|
|
156
|
+
if args.get:
|
|
157
|
+
url = args.get[0]
|
|
158
|
+
ref = args.get[1] if len(args.get) > 1 else None
|
|
159
|
+
args.name = utils.project_remote(url, PATHS["pro"], ref, args.name)
|
|
160
|
+
# Project list
|
|
161
|
+
PRO = utils.get_project_list(PATHS["pro"])
|
|
162
|
+
if args.example and not PRO:
|
|
163
|
+
p.wrn("Examples not downloaded")
|
|
164
|
+
utils.git_clone_missing(URL_DEMO, PATHS["examples"], "main", args.yes)
|
|
165
|
+
PRO = utils.get_project_list(PATHS["pro"])
|
|
166
|
+
# List projects
|
|
167
|
+
if args.project_list or (args.name and args.name.isdigit()):
|
|
168
|
+
if not PRO:
|
|
169
|
+
kind = "samples" if args.example else "projects"
|
|
170
|
+
p.wrn(f"No {kind} found")
|
|
171
|
+
p.inf(f"Create new with flag {flag.n}")
|
|
172
|
+
sys.exit(1)
|
|
173
|
+
i = 1
|
|
174
|
+
for name, path in PRO.items():
|
|
175
|
+
if args.project_list:
|
|
176
|
+
path = PATH.local(path)
|
|
177
|
+
path = replace_end(path, name, "")
|
|
178
|
+
nbr = f"{c.GOLD}{str(i).ljust(3)}{c.END}"
|
|
179
|
+
clr = c.TEAL if args.example else c.BLUE
|
|
180
|
+
print(f"{nbr} {c.GREY}{path}{c.END}{clr}{name}{c.END}")
|
|
181
|
+
else:
|
|
182
|
+
if int(args.name) == i:
|
|
183
|
+
args.name = name
|
|
184
|
+
break
|
|
185
|
+
i += 1
|
|
186
|
+
if args.project_list: sys.exit(0)
|
|
187
|
+
# Name validation
|
|
188
|
+
if not args.name and not args.reload and not args.info:
|
|
189
|
+
p.err(f"Name {c.YELLOW}name{c.END} not provided")
|
|
190
|
+
p.inf(f"Provide project name or use flag {flag.r}")
|
|
191
|
+
p.run(f"To reload the currently active project use flag {flag.r}")
|
|
192
|
+
sys.exit(1)
|
|
193
|
+
if args.name:
|
|
194
|
+
valid, reason = utils.validate_project_name(args.name)
|
|
195
|
+
if not valid:
|
|
196
|
+
p.err(f"Invalid project name: {c.MAGNTA}{reason}{c.END}")
|
|
197
|
+
sys.exit(1)
|
|
198
|
+
# Delete project
|
|
199
|
+
if args.delete:
|
|
200
|
+
key = next((k for k in PRO if k.lower() == args.name.lower()), None)
|
|
201
|
+
if key is None:
|
|
202
|
+
p.err(f"Project {c.MAGNTA}{args.name}{c.END} does not exist")
|
|
203
|
+
sys.exit(1)
|
|
204
|
+
try:
|
|
205
|
+
DIR.remove(PRO[key], force=True)
|
|
206
|
+
if make_info and key == make_info["NAME"]:
|
|
207
|
+
FILE.remove("makefile")
|
|
208
|
+
FILE.remove("flash.ld")
|
|
209
|
+
p.ok(f"Project {c.BLUE}{args.name}{c.END} deleted")
|
|
210
|
+
sys.exit(0)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
p.err(f"Failed to delete: {e}")
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
# Set project path
|
|
215
|
+
PATHS["pro"] = PATH.resolve(f"{PATHS['pro']}/{args.name}", read=False)
|
|
216
|
+
noun = "Sample" if args.example else "Project"
|
|
217
|
+
# New project
|
|
218
|
+
if args.new:
|
|
219
|
+
if args.name.lower() in (n.lower() for n in PRO.keys()):
|
|
220
|
+
p.err(f"{noun} {c.MAGNTA}{args.name}{c.END} already exists")
|
|
221
|
+
p.run(f"Use a different name or load it without flag {flag.n}")
|
|
222
|
+
sys.exit(1)
|
|
223
|
+
# Check for nested projects
|
|
224
|
+
new_name = args.name.replace("\\", "/").strip("/")
|
|
225
|
+
for existing_name in PRO.keys():
|
|
226
|
+
existing = existing_name.replace("\\", "/").strip("/")
|
|
227
|
+
if new_name.startswith(existing + "/"):
|
|
228
|
+
p.err(f"Cannot create {c.MAGNTA}{args.name}{c.END} inside existing project {c.BLUE}{existing_name}{c.END}")
|
|
229
|
+
sys.exit(1)
|
|
230
|
+
if existing.startswith(new_name + "/"):
|
|
231
|
+
p.err(f"Cannot create {c.MAGNTA}{args.name}{c.END} - project {c.BLUE}{existing_name}{c.END} already exists inside")
|
|
232
|
+
sys.exit(1)
|
|
233
|
+
# Check write permission
|
|
234
|
+
parent_dir = PATH.dirname(PATHS["pro"])
|
|
235
|
+
if not utils.check_write_permission(parent_dir):
|
|
236
|
+
p.err(f"No write permission in {c.ORANGE}{parent_dir}{c.END}")
|
|
237
|
+
sys.exit(1)
|
|
238
|
+
# Resolve chip and board
|
|
239
|
+
chip_cfg, board = resolve_chip(args.chip, args.board)
|
|
240
|
+
# If no board and no chip specified, ask about Uno
|
|
241
|
+
if not board and not args.chip:
|
|
242
|
+
if not args.yes and not utils.is_yes(f"Are you using OpenCPLC {c.TURQUS}Uno{c.END}"):
|
|
243
|
+
p.err(f"Specify board with flag {flag.b} or chip with flag {flag.c}")
|
|
244
|
+
sys.exit(1)
|
|
245
|
+
chip_cfg, board = resolve_chip("STM32G0C1", "uno")
|
|
246
|
+
# Memory override: [flash, ram] or [flash, ram, user]
|
|
247
|
+
if args.memory and len(args.memory) >= 2:
|
|
248
|
+
user_kB = args.memory[2] if len(args.memory) > 2 else 0
|
|
249
|
+
chip_cfg["flash_kB"] = args.memory[0] - user_kB
|
|
250
|
+
chip_cfg["ram_kB"] = args.memory[1]
|
|
251
|
+
# Build config
|
|
252
|
+
CFG = chip_cfg.copy()
|
|
253
|
+
CFG["pro_name"] = args.name
|
|
254
|
+
CFG["board"] = board
|
|
255
|
+
CFG["pro_ver"] = fw_ver
|
|
256
|
+
CFG["fw_ver"] = fw_ver
|
|
257
|
+
CFG["opt_level"] = args.opt_level or forge_cfg["default"]["optLevel"]
|
|
258
|
+
CFG["log_level"] = "LOG_LEVEL_INF"
|
|
259
|
+
else:
|
|
260
|
+
# Load existing project
|
|
261
|
+
if args.name.lower() not in (n.lower() for n in PRO.keys()):
|
|
262
|
+
p.err(f"{noun} {c.MAGNTA}{args.name}{c.END} does not exist")
|
|
263
|
+
if args.example:
|
|
264
|
+
p.run(f"Check available examples with flag {flag.l} or download with {flag.e}")
|
|
265
|
+
else:
|
|
266
|
+
p.run(f"Use flag {flag.n} to create a new project")
|
|
267
|
+
sys.exit(1)
|
|
268
|
+
main_h_path = PATH.resolve(f"{PRO[args.name]}/main.h", read=False)
|
|
269
|
+
if not FILE.exists(main_h_path):
|
|
270
|
+
p.err(f"File {c.ORANGE}main.h{c.END} not found in project")
|
|
271
|
+
p.inf(f"Project may be corrupted, consider recreating with {flag.n}")
|
|
272
|
+
sys.exit(1)
|
|
273
|
+
lines = load_lines(main_h_path)
|
|
274
|
+
if not lines:
|
|
275
|
+
p.err(f"File {c.ORANGE}main.h{c.END} is empty or unreadable")
|
|
276
|
+
sys.exit(1)
|
|
277
|
+
lines = utils.lines_clear(lines, "//")
|
|
278
|
+
info1 = utils.get_vars(lines, ["PRO_BOARD", "PRO_CHIP"], "_", "#define", required=False)
|
|
279
|
+
info2 = utils.get_vars(lines, ["PRO_VERSION", "PRO_FLASH_kB", "PRO_RAM_kB",
|
|
280
|
+
"PRO_OPT_LEVEL", "LOG_LEVEL", "SYS_CLOCK_FREQ"], " ", "#define", required=False)
|
|
281
|
+
info = info1 | info2
|
|
282
|
+
if not info.get("PRO_CHIP"):
|
|
283
|
+
p.err(f"File {c.ORANGE}main.h{c.END} missing PRO_CHIP definition")
|
|
284
|
+
p.inf(f"Check {c.ORANGE}{PATHS['pro']}/main.h{c.END}")
|
|
285
|
+
sys.exit(1)
|
|
286
|
+
# Get project version from main.h
|
|
287
|
+
pro_ver = info.get("PRO_VERSION", fw_ver)
|
|
288
|
+
# Resolve chip (override with args if provided)
|
|
289
|
+
stored_chip = info["PRO_CHIP"]
|
|
290
|
+
stored_board = info.get("PRO_BOARD", "").lower()
|
|
291
|
+
if stored_board == "none": stored_board = ""
|
|
292
|
+
# Board change warning
|
|
293
|
+
if args.board and args.board.lower() != "none" and stored_board:
|
|
294
|
+
pro_board = stored_board.capitalize()
|
|
295
|
+
arg_board = args.board.capitalize()
|
|
296
|
+
if arg_board.lower() != pro_board.lower():
|
|
297
|
+
p.wrn(f"Compiling for {c.TURQUS}{arg_board}{c.END}, but project was prepared for {c.TURQUS}{pro_board}{c.END}")
|
|
298
|
+
chip_cfg, board = resolve_chip(
|
|
299
|
+
args.chip or stored_chip,
|
|
300
|
+
args.board or stored_board or "none"
|
|
301
|
+
)
|
|
302
|
+
if args.board and args.board.lower() != "none":
|
|
303
|
+
board = args.board.capitalize()
|
|
304
|
+
elif not args.board and stored_board:
|
|
305
|
+
board = stored_board.capitalize()
|
|
306
|
+
else:
|
|
307
|
+
board = None
|
|
308
|
+
# Build config
|
|
309
|
+
CFG = chip_cfg.copy()
|
|
310
|
+
CFG["pro_name"] = args.name
|
|
311
|
+
CFG["board"] = board.lower() if board else None
|
|
312
|
+
CFG["pro_ver"] = pro_ver
|
|
313
|
+
CFG["fw_ver"] = fw_ver
|
|
314
|
+
CFG["flash_kB"] = int(info.get("PRO_FLASH_kB", chip_cfg["flash_kB"]))
|
|
315
|
+
CFG["ram_kB"] = int(info.get("PRO_RAM_kB", chip_cfg["ram_kB"]))
|
|
316
|
+
CFG["opt_level"] = info.get("PRO_OPT_LEVEL", "Og")
|
|
317
|
+
CFG["log_level"] = info.get("LOG_LEVEL", "LOG_LEVEL_INF")
|
|
318
|
+
CFG["freq_Hz"] = int(info.get("SYS_CLOCK_FREQ", chip_cfg.get("freq_Hz", 64000000)))
|
|
319
|
+
# Version check for project version
|
|
320
|
+
utils.version_check(pro_ver, forge_cfg["available-versions"],
|
|
321
|
+
f"{Ico.ERR} Invalid PRO_VERSION in main.h")
|
|
322
|
+
# Warn about outdated project version
|
|
323
|
+
latest = forge_cfg["available-versions"][0]
|
|
324
|
+
if pro_ver != latest and not args.example:
|
|
325
|
+
p.inf(f"Project uses {c.GREY}{pro_ver}{c.END}, latest is {c.VIOLET}{latest}{c.END}")
|
|
326
|
+
# Handle version mismatch
|
|
327
|
+
if args.example:
|
|
328
|
+
CFG["fw_ver"] = pro_ver
|
|
329
|
+
PATHS["fw"] = PATH.resolve(f"{PATHS['framework']}/{pro_ver}", read=False)
|
|
330
|
+
elif pro_ver != fw_ver:
|
|
331
|
+
fw_path = PATH.resolve(f"{PATHS['framework']}/{pro_ver}", read=False)
|
|
332
|
+
if not utils.git_clone_missing(URL_CORE, fw_path, pro_ver, args.yes, required=False):
|
|
333
|
+
p.wrn(f"Project {c.MAGNTA}{args.name}{c.END} version {c.GREY}({pro_ver}){c.END} differs from framework {c.GREY}({fw_ver}){c.END}")
|
|
334
|
+
p.wrn("This may prevent compilation or cause incorrect behavior")
|
|
335
|
+
else:
|
|
336
|
+
CFG["fw_ver"] = pro_ver
|
|
337
|
+
PATHS["fw"] = fw_path
|
|
338
|
+
# Warn about ignored flags
|
|
339
|
+
msg = f"is ignored when loading an existing {noun.lower()} — it's read from {c.ORANGE}main.h{c.END}"
|
|
340
|
+
if args.chip: p.wrn(f"Flag {flag.c} {msg}")
|
|
341
|
+
if args.memory: p.wrn(f"Flag {flag.m} {msg}")
|
|
342
|
+
# Normalize opt-level
|
|
343
|
+
opt = CFG.get("opt_level", "Og")
|
|
344
|
+
CFG["opt_level"] = opt[0].upper() + opt[1:].lower() if len(opt) > 1 else opt
|
|
345
|
+
valid_opts = ("O0", "Og", "O1", "O2", "O3")
|
|
346
|
+
if CFG["opt_level"] not in valid_opts:
|
|
347
|
+
p.wrn(f"Unknown optimization level {c.MAGNTA}{opt}{c.END}, using {c.CYAN}Og{c.END}")
|
|
348
|
+
p.inf(f"Valid options: {', '.join(valid_opts)}")
|
|
349
|
+
CFG["opt_level"] = "Og"
|
|
350
|
+
if CFG["platform"] == "STM32" and CFG["opt_level"] in ("O2", "O3"):
|
|
351
|
+
p.wrn(f"Optimization {c.MAGNTA}{CFG['opt_level']}{c.END} may cause issues on STM32 (timing, debugging)")
|
|
352
|
+
if not args.yes and not utils.is_yes(f"Continue with {c.CYAN}{CFG['opt_level']}{c.END}"):
|
|
353
|
+
p.inf(f"Using {c.CYAN}Og{c.END} instead")
|
|
354
|
+
CFG["opt_level"] = "Og"
|
|
355
|
+
# Info mode
|
|
356
|
+
if args.info:
|
|
357
|
+
rel_path = PATH.local(PATHS["pro"])
|
|
358
|
+
path_prefix = replace_end(rel_path, CFG["pro_name"], "")
|
|
359
|
+
msg = f"{c.GREY}{path_prefix}{c.END}{c.BLUE}{CFG['pro_name']}{c.END}"
|
|
360
|
+
sample_msg = f" {c.RED}(sample){c.END}" if args.example else ""
|
|
361
|
+
p.inf(f"Project: {msg}{sample_msg}")
|
|
362
|
+
p.gap(f"Platform: {c.CYAN}{CFG['platform']}{c.END}")
|
|
363
|
+
p.gap(f"Board {flag.b}: {c.TURQUS}{str(CFG.get('board') or 'None').capitalize()}{c.END}")
|
|
364
|
+
p.gap(f"Chip {flag.c}: {c.PINK}{CFG['chip']}{c.END}")
|
|
365
|
+
p.gap(f"Project version: {c.MAGNTA}{CFG['pro_ver']}{c.END}")
|
|
366
|
+
p.gap(f"Framework version: {c.MAGNTA}{CFG['fw_ver']}{c.END}")
|
|
367
|
+
if CFG["platform"] == "STM32":
|
|
368
|
+
p.gap(f"FLASH{c.GREY}/{c.END}RAM {flag.m}: {c.CYAN}{CFG['flash_kB']}{c.END}kB{c.GREY}/{c.END}{c.CYAN}{CFG['ram_kB']}{c.END}kB")
|
|
369
|
+
p.gap(f"System frequency clock: {c.CYAN}{CFG.get('freq_Hz', 64000000)}{c.END}Hz")
|
|
370
|
+
p.gap(f"Optimization level {flag.o}: {c.PINK}{CFG['opt_level']}{c.END}")
|
|
371
|
+
p.gap(f"Log level: {c.BLUE}{CFG['log_level'].replace('LOG_LEVEL_', '')}{c.END}")
|
|
372
|
+
p.gap(f"Last modification: {utils.last_modification(PATHS['pro'], ext=['.c','.h'])}")
|
|
373
|
+
sys.exit(0)
|
|
374
|
+
# Generate project
|
|
375
|
+
generate_project(CFG, PATHS, forge_cfg, is_example=args.example)
|
|
376
|
+
|
|
377
|
+
if __name__ == "__main__":
|
|
378
|
+
main()
|