scirodev 0.4.1__tar.gz → 0.5.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.
- {scirodev-0.4.1 → scirodev-0.5.0}/PKG-INFO +29 -1
- {scirodev-0.4.1 → scirodev-0.5.0}/README.md +28 -0
- {scirodev-0.4.1 → scirodev-0.5.0}/pyproject.toml +1 -1
- scirodev-0.5.0/sciro/robotics.py +84 -0
- scirodev-0.5.0/sciro/tools.py +91 -0
- {scirodev-0.4.1 → scirodev-0.5.0}/scirodev.egg-info/PKG-INFO +29 -1
- {scirodev-0.4.1 → scirodev-0.5.0}/scirodev.egg-info/SOURCES.txt +2 -0
- {scirodev-0.4.1 → scirodev-0.5.0}/LICENSE +0 -0
- {scirodev-0.4.1 → scirodev-0.5.0}/sciro/__init__.py +0 -0
- {scirodev-0.4.1 → scirodev-0.5.0}/sciro/_common.py +0 -0
- {scirodev-0.4.1 → scirodev-0.5.0}/sciro/floorpro.py +0 -0
- {scirodev-0.4.1 → scirodev-0.5.0}/sciro/hubs.py +0 -0
- {scirodev-0.4.1 → scirodev-0.5.0}/sciro/iodevices.py +0 -0
- {scirodev-0.4.1 → scirodev-0.5.0}/sciro/parameters.py +0 -0
- {scirodev-0.4.1 → scirodev-0.5.0}/sciro/pump.py +0 -0
- {scirodev-0.4.1 → scirodev-0.5.0}/sciro/py.typed +0 -0
- {scirodev-0.4.1 → scirodev-0.5.0}/scirodev/__init__.py +0 -0
- {scirodev-0.4.1 → scirodev-0.5.0}/scirodev/cli.py +0 -0
- {scirodev-0.4.1 → scirodev-0.5.0}/scirodev.egg-info/dependency_links.txt +0 -0
- {scirodev-0.4.1 → scirodev-0.5.0}/scirodev.egg-info/entry_points.txt +0 -0
- {scirodev-0.4.1 → scirodev-0.5.0}/scirodev.egg-info/requires.txt +0 -0
- {scirodev-0.4.1 → scirodev-0.5.0}/scirodev.egg-info/top_level.txt +0 -0
- {scirodev-0.4.1 → scirodev-0.5.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scirodev
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: PeakHub / sciro extensions to Pybricks: typed API stubs (sciro.*) and hub tooling on top of pybricksdev
|
|
5
5
|
Author: Thomas Schank
|
|
6
6
|
License: MIT
|
|
@@ -43,6 +43,8 @@ from sciro.parameters import Port # Port.A .. Port.H (PeakHub has 8 por
|
|
|
43
43
|
from sciro.iodevices import PUMPDevice # generic PUMP device access
|
|
44
44
|
from sciro.pump import FloorPro # PUMP devices (LP-FloorPro, ...)
|
|
45
45
|
from sciro.hubs import PeakHub # hub class incl. display.device()
|
|
46
|
+
from sciro.tools import RingBuffer # experimental: RAM recorder -> CSV file via the console
|
|
47
|
+
from sciro.robotics import PIDController # experimental: PID with optional logging
|
|
46
48
|
|
|
47
49
|
hub = PeakHub()
|
|
48
50
|
fp = FloorPro(Port.G)
|
|
@@ -50,6 +52,32 @@ cog_dark, cog_bright, brightness, darkness, mask, calibrating = fp.line.read()
|
|
|
50
52
|
hub.display.device(fp, brightness=50) # mirror the sensor's LED strip on the 5x5
|
|
51
53
|
```
|
|
52
54
|
|
|
55
|
+
Experimental (API may change): a RAM recorder that publishes CSV through the
|
|
56
|
+
console, and a PID controller that can log into it:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from sciro.robotics import PIDController
|
|
60
|
+
from sciro.tools import RingBuffer
|
|
61
|
+
|
|
62
|
+
pid = PIDController(kp=15, kd=0.2, output_limit=300)
|
|
63
|
+
log = pid.make_log(seconds=10, rate=100) # RingBuffer of pid.LOG_FIELDS
|
|
64
|
+
turn_rate = pid.update(error, derivative=-hub.imu.angular_velocity(Axis.Z))
|
|
65
|
+
log.publish("doc/pid_run.csv") # scirodev run writes the file next to the script
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The drivebase's own controllers log too (built into Pybricks, undocumented
|
|
69
|
+
upstream): `db.heading_control.log.start(5000, down_sample=2)` records the
|
|
70
|
+
reference trajectory, the estimated state and the P/I/D terms at 100 Hz from the
|
|
71
|
+
firmware's control loop; `db.heading_control.log.publish("heading.csv")` writes
|
|
72
|
+
it with a header line. `sciro.robotics.DriveBase` is the Pybricks class with
|
|
73
|
+
these attributes typed. `log.record(db.state)` is the coroutine form for
|
|
74
|
+
anything else.
|
|
75
|
+
|
|
76
|
+
`publish(path)` wraps the CSV in pybricksdev's `_file_begin_ <path>` /
|
|
77
|
+
`_file_end_` lines; `scirodev run` (and `pybricksdev run`) then write the block
|
|
78
|
+
to that file, relative to the script's folder, instead of echoing it. Without a
|
|
79
|
+
path the CSV goes to the terminal.
|
|
80
|
+
|
|
53
81
|
Full demo programs for the LP FloorPro (both for LEGO hubs and the PeakHub) live in
|
|
54
82
|
[sciurus-robotics/FloorPro-CodeDemos](https://github.com/sciurus-robotics/FloorPro-CodeDemos);
|
|
55
83
|
`examples/` here stays minimal.
|
|
@@ -23,6 +23,8 @@ from sciro.parameters import Port # Port.A .. Port.H (PeakHub has 8 por
|
|
|
23
23
|
from sciro.iodevices import PUMPDevice # generic PUMP device access
|
|
24
24
|
from sciro.pump import FloorPro # PUMP devices (LP-FloorPro, ...)
|
|
25
25
|
from sciro.hubs import PeakHub # hub class incl. display.device()
|
|
26
|
+
from sciro.tools import RingBuffer # experimental: RAM recorder -> CSV file via the console
|
|
27
|
+
from sciro.robotics import PIDController # experimental: PID with optional logging
|
|
26
28
|
|
|
27
29
|
hub = PeakHub()
|
|
28
30
|
fp = FloorPro(Port.G)
|
|
@@ -30,6 +32,32 @@ cog_dark, cog_bright, brightness, darkness, mask, calibrating = fp.line.read()
|
|
|
30
32
|
hub.display.device(fp, brightness=50) # mirror the sensor's LED strip on the 5x5
|
|
31
33
|
```
|
|
32
34
|
|
|
35
|
+
Experimental (API may change): a RAM recorder that publishes CSV through the
|
|
36
|
+
console, and a PID controller that can log into it:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from sciro.robotics import PIDController
|
|
40
|
+
from sciro.tools import RingBuffer
|
|
41
|
+
|
|
42
|
+
pid = PIDController(kp=15, kd=0.2, output_limit=300)
|
|
43
|
+
log = pid.make_log(seconds=10, rate=100) # RingBuffer of pid.LOG_FIELDS
|
|
44
|
+
turn_rate = pid.update(error, derivative=-hub.imu.angular_velocity(Axis.Z))
|
|
45
|
+
log.publish("doc/pid_run.csv") # scirodev run writes the file next to the script
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The drivebase's own controllers log too (built into Pybricks, undocumented
|
|
49
|
+
upstream): `db.heading_control.log.start(5000, down_sample=2)` records the
|
|
50
|
+
reference trajectory, the estimated state and the P/I/D terms at 100 Hz from the
|
|
51
|
+
firmware's control loop; `db.heading_control.log.publish("heading.csv")` writes
|
|
52
|
+
it with a header line. `sciro.robotics.DriveBase` is the Pybricks class with
|
|
53
|
+
these attributes typed. `log.record(db.state)` is the coroutine form for
|
|
54
|
+
anything else.
|
|
55
|
+
|
|
56
|
+
`publish(path)` wraps the CSV in pybricksdev's `_file_begin_ <path>` /
|
|
57
|
+
`_file_end_` lines; `scirodev run` (and `pybricksdev run`) then write the block
|
|
58
|
+
to that file, relative to the script's folder, instead of echoing it. Without a
|
|
59
|
+
path the CSV goes to the terminal.
|
|
60
|
+
|
|
33
61
|
Full demo programs for the LP FloorPro (both for LEGO hubs and the PeakHub) live in
|
|
34
62
|
[sciurus-robotics/FloorPro-CodeDemos](https://github.com/sciurus-robotics/FloorPro-CodeDemos);
|
|
35
63
|
`examples/` here stays minimal.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scirodev"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "PeakHub / sciro extensions to Pybricks: typed API stubs (sciro.*) and hub tooling on top of pybricksdev"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Stubs for ``sciro.robotics`` (PeakHub firmware) -- control helpers.
|
|
2
|
+
EXPERIMENTAL: the API may still change between releases.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from pybricks import _common
|
|
10
|
+
from pybricks.robotics import DriveBase as _DriveBase
|
|
11
|
+
from pybricks.tools import StopWatch
|
|
12
|
+
|
|
13
|
+
from sciro.tools import Logger, RingBuffer
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Control(_common.Control):
|
|
17
|
+
"""A drivebase / motor controller, with its built-in logger exposed."""
|
|
18
|
+
|
|
19
|
+
log: Logger
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DriveBase(_DriveBase):
|
|
23
|
+
"""``pybricks.robotics.DriveBase`` (the same class on the hub); the stub
|
|
24
|
+
adds the ``log`` on ``heading_control`` / ``distance_control``."""
|
|
25
|
+
|
|
26
|
+
heading_control: Control # type: ignore[assignment]
|
|
27
|
+
distance_control: Control # type: ignore[assignment]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PIDController:
|
|
31
|
+
"""Discrete PID controller: ``output = kp*e + ki*integral(e) + kd*de/dt``.
|
|
32
|
+
|
|
33
|
+
``ki = kd = 0`` gives a plain P or PD controller. ``integral_limit`` clamps
|
|
34
|
+
the I term (anti-windup), ``output_limit`` the total output; ``None`` means
|
|
35
|
+
unlimited. A gap between updates longer than ``reset_after`` milliseconds
|
|
36
|
+
restarts the controller (a new movement); ``None`` never. ``stopwatch``
|
|
37
|
+
is the time source (default: an own ``StopWatch``); ``log`` a
|
|
38
|
+
:class:`RingBuffer` with :attr:`LOG_FIELDS` (see :meth:`make_log`).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
LOG_FIELDS: Tuple[Tuple[str, str], ...]
|
|
42
|
+
"""``t_ms, dt, error, p, i, d, output`` -- the row layout of the log."""
|
|
43
|
+
|
|
44
|
+
kp: float
|
|
45
|
+
ki: float
|
|
46
|
+
kd: float
|
|
47
|
+
integral_limit: Optional[float]
|
|
48
|
+
output_limit: Optional[float]
|
|
49
|
+
reset_after: Optional[int]
|
|
50
|
+
stopwatch: StopWatch
|
|
51
|
+
log: Optional[RingBuffer]
|
|
52
|
+
name: str
|
|
53
|
+
output: float
|
|
54
|
+
"""The last output."""
|
|
55
|
+
integral: float
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
kp: float,
|
|
60
|
+
ki: float = 0.0,
|
|
61
|
+
kd: float = 0.0,
|
|
62
|
+
integral_limit: Optional[float] = None,
|
|
63
|
+
output_limit: Optional[float] = None,
|
|
64
|
+
reset_after: Optional[int] = 100,
|
|
65
|
+
stopwatch: Optional[StopWatch] = None,
|
|
66
|
+
log: Optional[RingBuffer] = None,
|
|
67
|
+
name: str = "PID",
|
|
68
|
+
) -> None: ...
|
|
69
|
+
|
|
70
|
+
def make_log(self, seconds: float, rate: int = 100) -> RingBuffer:
|
|
71
|
+
"""Attach and return a RingBuffer for ``seconds`` of updates at ``rate`` Hz."""
|
|
72
|
+
|
|
73
|
+
def reset(self) -> None:
|
|
74
|
+
"""Forget the integral, the last error and the last update time."""
|
|
75
|
+
|
|
76
|
+
def update(self, error: float, derivative: Optional[float] = None, p_limit: Optional[float] = None) -> float:
|
|
77
|
+
"""One controller step; returns the output.
|
|
78
|
+
|
|
79
|
+
``error`` is setpoint minus measurement. ``derivative`` is d(error)/dt
|
|
80
|
+
when measured (e.g. minus the gyro rate for a heading error); ``None``
|
|
81
|
+
differentiates the error numerically. ``p_limit`` clamps the P term for
|
|
82
|
+
this step only, e.g. a braking profile that leaves the D term free to
|
|
83
|
+
damp.
|
|
84
|
+
"""
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Stubs for ``sciro.tools`` (PeakHub firmware) -- recording and publishing
|
|
2
|
+
helpers. EXPERIMENTAL: the API may still change between releases.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Awaitable, Callable, Iterable, Iterator, Optional, Sequence, Tuple
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RingBuffer:
|
|
11
|
+
"""Fixed-size recorder of rows with a fixed binary layout, kept in RAM.
|
|
12
|
+
|
|
13
|
+
``fields`` are ``(name, format)`` pairs with ``struct`` formats (``"I"``
|
|
14
|
+
unsigned 32 bit, ``"i"`` signed, ``"f"`` float, ``"h"`` 16 bit, ``"B"``
|
|
15
|
+
byte, ...); ``capacity`` rows are kept, older rows are overwritten.
|
|
16
|
+
RAM used = capacity * row size (100 Hz * 30 s * 7 floats = 84 KB).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
fields: Tuple[Tuple[str, str], ...]
|
|
20
|
+
names: Tuple[str, ...]
|
|
21
|
+
format: str
|
|
22
|
+
row_size: int
|
|
23
|
+
capacity: int
|
|
24
|
+
name: Optional[str]
|
|
25
|
+
count: int
|
|
26
|
+
"""Rows appended since the last clear (not capped at capacity)."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, fields: Iterable[Sequence[str]], capacity: int, name: Optional[str] = None) -> None: ...
|
|
29
|
+
|
|
30
|
+
def __len__(self) -> int:
|
|
31
|
+
"""Rows currently kept (at most ``capacity``)."""
|
|
32
|
+
|
|
33
|
+
def __getitem__(self, idx: int) -> Tuple[float | int, ...]:
|
|
34
|
+
"""Row ``idx`` as a tuple; 0 is the oldest kept row, -1 the newest."""
|
|
35
|
+
|
|
36
|
+
def __iter__(self) -> Iterator[Tuple[float | int, ...]]:
|
|
37
|
+
"""Rows, oldest first."""
|
|
38
|
+
|
|
39
|
+
def clear(self) -> None: ...
|
|
40
|
+
|
|
41
|
+
def append(self, *values: float | int) -> None:
|
|
42
|
+
"""Append one row; values in field order."""
|
|
43
|
+
|
|
44
|
+
def record(self, source: Callable[[], Sequence[float | int]], period: int = 10) -> Awaitable[None]:
|
|
45
|
+
"""Coroutine: append ``source()`` every ``period`` ms until cancelled.
|
|
46
|
+
|
|
47
|
+
Run it next to the movement, e.g.
|
|
48
|
+
``await multitask(drive(), log.record(db.state), race=True)``.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def publish(self, path: Optional[str] = None, decimals: int = 3) -> None:
|
|
52
|
+
"""Print the rows as CSV (``#`` header comments, then the names).
|
|
53
|
+
|
|
54
|
+
With ``path`` the block is wrapped in the pybricksdev file markers
|
|
55
|
+
(``_file_begin_ <path>`` ... ``_file_end_``): ``scirodev run`` writes it
|
|
56
|
+
to that file, relative to the folder of the running script, instead of
|
|
57
|
+
the terminal. Expect on the order of 1000 rows per few seconds over BLE.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Logger:
|
|
62
|
+
"""The built-in pbio logger of a controller (``DriveBase.heading_control.log``,
|
|
63
|
+
``DriveBase.distance_control.log``, ``Motor.log``): rows are written by
|
|
64
|
+
the firmware's 5 ms control loop while the controller is active, into a
|
|
65
|
+
fixed buffer (not a ring) allocated by :meth:`start`.
|
|
66
|
+
|
|
67
|
+
Controller columns: ``t_ms, traj_ms, position, speed, status, torque,
|
|
68
|
+
ref_position, ref_speed, est_position, est_speed, p, i, d`` (position in
|
|
69
|
+
the controller's units: mm or deg; torque in uNm; status bits: actuation
|
|
70
|
+
0..1, stalled 2, on target 3, integration paused 4).
|
|
71
|
+
Servo (Motor) columns: ``t_ms, time_ms, angle, speed, status, voltage_mv,
|
|
72
|
+
est_angle, est_speed, torque_fb, torque_ff, observer_mv``.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def start(self, duration: int, down_sample: int = 1) -> None:
|
|
76
|
+
"""Allocate and start: ``duration`` ms, one row every
|
|
77
|
+
``5 * down_sample`` ms. RAM = rows * 52 bytes (controller), so 30 s at
|
|
78
|
+
``down_sample=2`` (100 Hz) is 156 KB; prefer 10 (20 Hz) for long runs.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def stop(self) -> None: ...
|
|
82
|
+
|
|
83
|
+
def save(self, path: Optional[str] = None) -> None:
|
|
84
|
+
"""Upstream: print the rows framed with ``PB_OF:<path>`` / ``PB_EOF``,
|
|
85
|
+
no header. Kept unchanged."""
|
|
86
|
+
|
|
87
|
+
def publish(self, path: Optional[str] = None, names: Optional[str] = None) -> None:
|
|
88
|
+
"""PeakHub: print the rows as CSV with a header line, framed with the
|
|
89
|
+
``_file_begin_ <path>`` / ``_file_end_`` markers like
|
|
90
|
+
:meth:`RingBuffer.publish`. ``names`` overrides the header
|
|
91
|
+
(comma separated). Stops logging first."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scirodev
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: PeakHub / sciro extensions to Pybricks: typed API stubs (sciro.*) and hub tooling on top of pybricksdev
|
|
5
5
|
Author: Thomas Schank
|
|
6
6
|
License: MIT
|
|
@@ -43,6 +43,8 @@ from sciro.parameters import Port # Port.A .. Port.H (PeakHub has 8 por
|
|
|
43
43
|
from sciro.iodevices import PUMPDevice # generic PUMP device access
|
|
44
44
|
from sciro.pump import FloorPro # PUMP devices (LP-FloorPro, ...)
|
|
45
45
|
from sciro.hubs import PeakHub # hub class incl. display.device()
|
|
46
|
+
from sciro.tools import RingBuffer # experimental: RAM recorder -> CSV file via the console
|
|
47
|
+
from sciro.robotics import PIDController # experimental: PID with optional logging
|
|
46
48
|
|
|
47
49
|
hub = PeakHub()
|
|
48
50
|
fp = FloorPro(Port.G)
|
|
@@ -50,6 +52,32 @@ cog_dark, cog_bright, brightness, darkness, mask, calibrating = fp.line.read()
|
|
|
50
52
|
hub.display.device(fp, brightness=50) # mirror the sensor's LED strip on the 5x5
|
|
51
53
|
```
|
|
52
54
|
|
|
55
|
+
Experimental (API may change): a RAM recorder that publishes CSV through the
|
|
56
|
+
console, and a PID controller that can log into it:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from sciro.robotics import PIDController
|
|
60
|
+
from sciro.tools import RingBuffer
|
|
61
|
+
|
|
62
|
+
pid = PIDController(kp=15, kd=0.2, output_limit=300)
|
|
63
|
+
log = pid.make_log(seconds=10, rate=100) # RingBuffer of pid.LOG_FIELDS
|
|
64
|
+
turn_rate = pid.update(error, derivative=-hub.imu.angular_velocity(Axis.Z))
|
|
65
|
+
log.publish("doc/pid_run.csv") # scirodev run writes the file next to the script
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The drivebase's own controllers log too (built into Pybricks, undocumented
|
|
69
|
+
upstream): `db.heading_control.log.start(5000, down_sample=2)` records the
|
|
70
|
+
reference trajectory, the estimated state and the P/I/D terms at 100 Hz from the
|
|
71
|
+
firmware's control loop; `db.heading_control.log.publish("heading.csv")` writes
|
|
72
|
+
it with a header line. `sciro.robotics.DriveBase` is the Pybricks class with
|
|
73
|
+
these attributes typed. `log.record(db.state)` is the coroutine form for
|
|
74
|
+
anything else.
|
|
75
|
+
|
|
76
|
+
`publish(path)` wraps the CSV in pybricksdev's `_file_begin_ <path>` /
|
|
77
|
+
`_file_end_` lines; `scirodev run` (and `pybricksdev run`) then write the block
|
|
78
|
+
to that file, relative to the script's folder, instead of echoing it. Without a
|
|
79
|
+
path the CSV goes to the terminal.
|
|
80
|
+
|
|
53
81
|
Full demo programs for the LP FloorPro (both for LEGO hubs and the PeakHub) live in
|
|
54
82
|
[sciurus-robotics/FloorPro-CodeDemos](https://github.com/sciurus-robotics/FloorPro-CodeDemos);
|
|
55
83
|
`examples/` here stays minimal.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|