adding files

This commit is contained in:
2026-03-21 23:51:53 +01:00
commit 8cb0240ca2
19 changed files with 2162 additions and 0 deletions

93
README.md Normal file
View File

@@ -0,0 +1,93 @@
# pi-dro
## Getting started
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin http://gitlab.example.com:8929/johan/pi-dro.git
git branch -M main
git push -uf origin main
```
## Integrate with your tools
- [ ] [Set up project integrations](http://gitlab.example.com:8929/johan/pi-dro/-/settings/integrations)
## Collaborate with your team
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
## Test and Deploy
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.

0
RPi/.gitkeep Normal file
View File

37
RPi/fake_smbus.py Normal file
View File

@@ -0,0 +1,37 @@
import tkinter as tk
encoders = []
class SMBus:
def __init__(self, bus):
print(f"fake SMBus initialized on bus {bus}")
self.bus = bus
def read_i2c_block_data(self, addr, cmd, length):
data = []
for encoder in encoders:
pos_as_byte_array = encoder.get_position().to_bytes(2, 'little', signed=True)
data.append(pos_as_byte_array[0])
data.append(pos_as_byte_array[1])
#print(f"SMBus data set to: {data}")
return data
def createTestEncoder():
encoder = TestEncoder()
encoders.append(encoder)
return encoder
class TestEncoder:
""" represent a test encoder controlled by keyboard input """
def __init__(self):
self._position = 0
def _rotate_cw(self, event=None):
self._position += 1
def _rotate_ccw(self, event=None):
self._position -= 1
def get_position(self) -> int:
return self._position

4
autostart.sh Normal file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
cd /home/johan/git-clones/pi-dro
python3 -u dro.py>dro.log 2>&1
#galculator

View File

@@ -0,0 +1,228 @@
# Arduino I2C Protocol
This document describes the I2C communication protocol between the Raspberry Pi (DRO application) and the Arduino (encoder interface).
## Hardware Setup
### I2C Configuration
- **Bus**: I2C Bus 1 (standard Raspberry Pi I2C)
- **Arduino Slave Address**: 0x08
- **Data Rate**: Standard (100 kHz) or Fast (400 kHz)
- **Pull-up Resistors**: 4.7 kΩ (typical I2C standards)
### Connections
```
Raspberry Pi Arduino
GPIO2 (SDA) <-> SDA
GPIO3 (SCL) <-> SCL
GND <- GND
5V (optional)-> 5V (if Arduino powered separately)
```
## Data Format
The Arduino transmits encoder positions as a 4-byte block:
### Memory Layout
```
Byte 0-1: X Position (little-endian signed 16-bit integer)
Byte 2-3: Z Position (little-endian signed 16-bit integer)
```
### Example
If encoder reads:
- X encoder: +5000 steps
- Z encoder: -200 steps
The transmitted bytes would be:
```
Byte 0: 0x88 (LSB of 5000)
Byte 1: 0x13 (MSB of 5000)
Byte 2: 0x38 (LSB of -200)
Byte 3: 0xFF (MSB of -200 in two's complement)
```
### Python Parsing
```python
import struct
# Read from I2C
data = bus.read_i2c_block_data(address, 0, 4)
# Convert to signed integers
x_position = int.from_bytes(bytes(data[0:2]), 'little', signed=True)
z_position = int.from_bytes(bytes(data[2:4]), 'little', signed=True)
# Or using struct:
x_pos, z_pos = struct.unpack('<hh', bytes(data))
```
## Communication Protocol
### Single Read Operation
```
Master (RPi) Slave (Arduino)
├─ START condition ────────────────→
├─ Address + READ ─────────────────→
↓ ↓
┌─────────────────────────────────────┐
│ Arduino sends 4 bytes of data │
│ (Acknowledgement from master) │
└─────────────────────────────────────┘
├─ STOP condition ──────────────────→
```
### Timing
- **Read Operation Time**: ~1-2 ms at 100 kHz
- **Poll Interval**: 100 ms (10 Hz refresh)
- **Data Transmission**: Asynchronous (happens in parallel with GUI rendering)
## Arduino Firmware Requirements
The Arduino sketch must:
1. **Initialize I2C Slave**
```cpp
Wire.begin(0x08); // Address 0x08
Wire.onRequest(requestEvent); // Register event handler
```
2. **Maintain Encoder Position Variables**
```cpp
volatile int16_t x_position = 0;
volatile int16_t z_position = 0;
```
3. **Update Positions from Encoders**
- Attach interrupts to encoder pins
- Increment/decrement positions on encoder pulses
4. **Respond to I2C Read Requests**
```cpp
void requestEvent() {
byte buffer[4];
// Convert to bytes (little-endian)
buffer[0] = (byte)x_position;
buffer[1] = (byte)(x_position >> 8);
buffer[2] = (byte)z_position;
buffer[3] = (byte)(z_position >> 8);
Wire.write(buffer, 4);
}
```
## Encoder Interface
The Arduino handles the low-level encoder reading:
### Encoder Connection
```
Rotary Encoder (2-bit Gray code or quadrature)
├─ Signal A ──→ Arduino Interrupt Pin
├─ Signal B ──→ Arduino Pin
└─ GND ──────→ Arduino GND
```
### Common Encoder Types
1. **Quadrature Encoder**
- 2 signals 90° out of phase
- Allows detection of direction and speed
- Typical: KY-040 module
2. **Incremental Encoder**
- Pulse + Direction signals
- Requires external direction determination
3. **Absolute Encoder**
- Maintains position across power cycles
- More complex protocol (often SPI/USB)
## Error Handling
### Arduino Side
- **Buffer Overrun**: Ensure encoder ISR is fast (< 100 µs)
- **I2C Collision**: Wire library handles this automatically
- **Power Loss**: Position may reset unless using volatile storage
### Raspberry Pi Side
The DRO application handles:
```python
try:
data = self._bus.read_i2c_block_data(self._address, 0, 4)
except IOError as e:
# I2C bus error (timeout, NACK, collision)
logger.error(f"I2C read error: {e}")
except OSError as e:
# System-level I2C error
logger.error(f"I2C OS error: {e}")
```
If a read fails, the last known position is retained.
## Debugging I2C Communication
### Check I2C Bus
```bash
# List I2C devices
i2cdetect -y 1
# Expected output for Arduino at 0x08:
# 0 1 2 3 4 5 6 7 8 9 a b c d e f
# 00: -- -- -- -- -- -- -- -- 08 -- -- -- --
```
### Manual Read Test
```bash
# Read 4 bytes from address 0x08, register 0
i2cget -y 1 0x08 0 i 4
# Expected output: four hex bytes
```
### Python Test
```python
import smbus2
bus = smbus2.SMBus(1)
data = bus.read_i2c_block_data(0x08, 0, 4)
print(f"Raw bytes: {[hex(b) for b in data]}")
x = int.from_bytes(bytes(data[0:2]), 'little', signed=True)
z = int.from_bytes(bytes(data[2:4]), 'little', signed=True)
print(f"X: {x}, Z: {z}")
bus.close()
```
## Performance Characteristics
- **Latency**: ~2-5 ms (I2C + processing)
- **Throughput**: 100 reads/second (limited by 100 ms GUI update interval)
- **Bandwidth**: 4 bytes × 10 Hz = 320 bytes/second
- **Jitter**: ±10 ms (non-deterministic on Linux)
## Troubleshooting
| Problem | Likely Cause | Solution |
|---------|--------------|----------|
| I2C not detected | Arduino not running | Upload firmware to Arduino |
| Data is zeros | Encoder not moving | Check encoder connections |
| Erratic values | Noise on I2C bus | Add pull-up resistors, shorten wires |
| Periodic dropouts | I2C collision | Use slower clock speed (100 kHz) |
| Position drifts | Encoder misconfiguration | Calibrate scale factor |
## References
- [I2C Specification](https://www.i2c-bus.org/)
- [Arduino Wire Library](https://www.arduino.cc/en/Reference/Wire)
- [Raspberry Pi I2C Setup](https://learn.adafruit.com/adafruit-16-channel-pwm-servo-driver/using-the-adafruit-library)

282
docs/DRO.md Normal file
View File

@@ -0,0 +1,282 @@
# Digital Read Out (DRO) Application
The `dro.py` file implements a Digital Read Out system for a lathe. It displays the current position of the lathe tool on two axes (X and Z) by reading encoder data from an Arduino via I2C communication.
## Overview
The application follows the **Model-View-Controller (MVC)** architectural pattern, which separates concerns:
- **Model**: Manages the state of the axes and their positions
- **View**: Displays the positions in a Tkinter GUI
- **Controller**: Handles user input and I2C communication
## Architecture
### Data Flow
```
Arduino (Encoder Data)
↓ (I2C)
Controller (Serial Communication) → Model (Position Calculation)
↓ ↓
View (GUI Update) ← ← ← ← ← ← ← ← ← ← Observers
```
## Classes
### Axis
Represents a single axis (X or Z) with position, scale, and offset.
**Attributes:**
- `_position`: Current position in mm
- `_scale`: Scale factor (mm per encoder step)
- `_offset`: Offset applied to position (mm)
- `_name`: Axis identifier ('x' or 'z')
**Methods:**
- `set_position(pos)`: Set the raw position
- `get_position()`: Get the raw position
- `set_scale(scale)`: Set the scale factor
- `get_scale()`: Get the scale factor
- `set_offset(offset)`: Set the offset
- `get_offset()`: Get the offset
### Model
Manages the state of both axes and notifies observers of changes. Implements the **Observer pattern** for MVC binding.
**Enums:**
- `XMode.RADIUS (0)`: X axis displays radius (default)
- `XMode.DIAMETER (1)`: X axis displays diameter
- `Updated.POS_X (0)`: X position changed
- `Updated.POS_Z (1)`: Z position changed
- `Updated.X_MODE (2)`: X mode (radius/diameter) changed
- `Updated.FULLSCREEN (3)`: Fullscreen state changed
**Key Methods:**
- `attach(observer)`: Register an observer (typically the View)
- `notify(updated)`: Notify all observers of changes
- `steps_to_position(axis, steps)`: Convert encoder steps to millimeters
- `set_position(axis, pos)`: Update a position (notifies if changed)
- `set_offset(axis, offset)`: Set the zero offset for calibration
- `get_effective_position(axis)`: Get position with offset applied; converts diameter to radius if needed
- `set_toggle_x_mode()`: Toggle between radius and diameter display
- `set_scale(axis, scale)`: Set the steps-to-mm conversion factor
**Special Behavior:**
- Position updates only trigger notifications if the change is ≥ 0.005 mm (prevents chatter)
- When X axis is in DIAMETER mode, displayed position is doubled
### Controller
Handles user interaction and I2C communication with the Arduino.
**Hardware:**
- I2C Bus: 1 (standard Raspberry Pi I2C)
- Arduino Address: 0x08
**Data Format:**
- Reads 4 bytes from Arduino every poll cycle
- Bytes 0-1: X position (little-endian signed 16-bit integer)
- Bytes 2-3: Z position (little-endian signed 16-bit integer)
**Methods:**
- `poll_i2c_data()`: Read encoder positions from Arduino via I2C
- `handle_x_position_update(steps)`: Process X encoder data
- `handle_z_position_update(steps)`: Process Z encoder data
- `handle_btn_x0_press()`: Set X zero point (sets offset to -current position)
- `handle_btn_z0_press()`: Set Z zero point
- `handle_btn_toggle_x_mode()`: Switch between radius/diameter display
- `hanlde_btn_x()`: Open dialog to manually set X position
- `hanlde_btn_z()`: Open dialog to manually set Z position
- `toggle_fullscreen()`: Toggle fullscreen display (F11)
- `end_fullscreen()`: Exit fullscreen mode (Escape)
- `handle_btn_calc()`: Launch system calculator (galculator)
- `shutdown()`: Shutdown the system (produces clean shutdown)
### View
Tkinter GUI displaying positions, buttons, and status. Acts as an observer of the Model.
**GUI Layout:**
```
┌─────────────────────────────────┐
│ X Position │ [X] [X_0] │
│ Z Position │ [Z] [Z_0] │
│ Mode │ │
│ │ [r/D] [Calc] [Off]│
└─────────────────────────────────┘
```
**Display Features:**
- Large font (80pt) for easy reading from distance
- Green text on black background for visibility
- Real-time position updates at 10 Hz
- Mode indicator (R for radius, D for diameter)
**Button Functions:**
- **X / Z**: Open dialog to set position
- **X_0 / Z_0**: Set zero point (calibrate)
- **r/D**: Toggle radius/diameter mode
- **Calc**: Launch calculator
- **Power**: Shutdown system
**Keyboard Shortcuts:**
- **F11**: Toggle fullscreen
- **Escape**: Exit fullscreen
- **A/Z** (test mode): Rotate X encoder clockwise/counterclockwise
- **S/X** (test mode): Rotate Z encoder clockwise/counterclockwise
## Usage
### Basic Startup
```bash
python3 dro.py
```
### Command Line Options
```bash
# Maximize window on startup
python3 dro.py --zoomed
# Run in test mode (no I2C communication, use keyboard to simulate encoders)
python3 dro.py --test
# Both options
python3 dro.py --zoomed --test
```
### Typical Workflow
1. **Startup**: Application connects to Arduino via I2C and displays current encoder positions
2. **Zero Axes**: Press X_0 and Z_0 buttons at the tool's starting position
3. **Move Tool**: As you move the tool, positions update in real-time
4. **Set Position**: Click X or Z button to set a specific position (useful for manual adjustments)
5. **Toggle Mode**: Press r/D to switch X axis between radius and diameter display
6. **Shutdown**: Click the power button or use the system menu
### Offset/Zero Calibration
The offset mechanism allows setting the zero point at any position:
```
Displayed Position = Raw Position + Offset
```
When you press X_0 or Z_0, the system sets:
```
Offset = -Current_Position
```
This makes the current display read zero.
### Radius vs Diameter Mode
The X axis can display in two modes:
- **Radius (r)**: Shows actual distance from spindle centerline
- **Diameter (D)**: Shows diameter (actual = radius × 2)
The Model automatically converts when displaying:
```
Diameter Display = Radius × 2
```
## Configuration
### Default Scales
```python
model.set_scale('x', -2.5/200) # -12.5 mm per 1000 steps (negative = flip direction)
model.set_scale('z', 90/1000) # 0.09 mm per step
```
These calibration values should be adjusted based on your encoder specifications:
- Negative X scale flips the direction to match lathe conventions
- Scale = (Distance Travel in mm) / (Encoder Steps)
### Update Frequency
- GUI updates every 100ms (10 Hz)
- I2C polling rate matches GUI updates
## Dependencies
### Python Packages
- `tkinter`: GUI framework (built-in with Python)
- `smbus2`: I2C communication (falls back to fake_smbus.py if not available)
- Standard library: `enum`, `subprocess`, `getopt`, `sys`, `shutil`, `logging`
### Hardware
- Raspberry Pi (tested on Pi 4)
- Arduino Nano (with I2C firmware at address 0x08)
- 2 Rotary encoders connected to Arduino
### System
- `galculator`: Calculator application (optional)
- `sudo` access for shutdown command
## Error Handling
The application gracefully handles several error conditions:
### I2C Communication Errors
```python
try:
data = self._bus.read_i2c_block_data(self._address, 0, 4)
except IOError as e:
logger.error(f"I2C read error: {e}")
except OSError as e:
logger.error(f"I2C OS error: {e}")
```
Errors are logged but don't crash the application. The last known position is retained.
### Missing Dependencies
If `smbus2` is not installed, the application falls back to the local `RPi/fake_smbus.py`:
```python
try:
import smbus2
except ImportError:
import RPi.fake_smbus as smbus2
logger.warning('smbus2 not available; using fake smbus2.py')
```
## Testing Mode
The `--test` flag enables keyboard simulation of encoder rotation:
```
X Axis:
A = Rotate clockwise (increase X)
Z = Rotate counterclockwise (decrease X)
Z Axis:
S = Rotate clockwise (increase Z)
X = Rotate counterclockwise (decrease Z)
```
This allows testing the GUI without physical encoders or Arduino.
## Known Issues / Notes
- Method name typo: `hanlde_btn_x()` and `hanlde_btn_z()` should be `handle_btn_x()` and `handle_btn_z()`
- Position updates use a 0.005 mm hysteresis threshold (prevents noise-induced updates)
- Fullscreen mode requires the Tkinter window to support the platform's fullscreen API
## Performance Considerations
- GUI updates are non-blocking and scheduled on the Tkinter event loop
- I2C reads are synchronous and run every 100ms (10 Hz update rate)
- Position filtering reduces redundant updates by ~99% in typical operation
- StringVar widgets minimize GUI redraws by only updating when values change
## Future Enhancements
- Add tool offset/wear compensation
- Support more than 2 axes
- Persistent configuration file for scales and offsets
- Network interface for remote monitoring
- Data logging of position history

307
docs/INSTALLATION.md Normal file
View File

@@ -0,0 +1,307 @@
# DRO Installation & Setup Guide
This guide walks through setting up the Digital Read Out application on a Raspberry Pi.
## System Requirements
- **Hardware**: Raspberry Pi 4 (or later)
- **OS**: Raspbian/Raspberry Pi OS (Bullseye or later)
- **Python**: 3.7+
- **Arduino**: Arduino Nano with I2C encoder firmware
## Step 1: Install Dependencies
### Enable I2C
```bash
sudo raspi-config
# Navigate to: Interfacing Options → I2C → Yes
# Reboot
sudo reboot
```
### Install Python Libraries
```bash
# Update package manager
sudo apt-get update
sudo apt-get upgrade
# Install required packages
sudo apt-get install python3-tk python3-dev python3-pip
# Install Python dependencies
pip3 install smbus2
pip3 install adafruit-circuitpython-ads1x15 # Optional: for ADC
```
### Optional: System Calculator
```bash
sudo apt-get install galculator
```
## Step 2: Configure I2C for Shutdown
Allow the application to shutdown without a password:
```bash
sudo visudo
```
Add this line to the end of the file (replace `pi` with your username):
```
pi ALL=(ALL) NOPASSWD: /sbin/poweroff, /sbin/reboot, /sbin/shutdown
```
Save with Ctrl+X, then Y, then Enter.
## Step 3: Upload Arduino Firmware
The Arduino must run firmware that:
1. Reads from two rotary encoders
2. Maintains position counters
3. Responds to I2C read requests at address 0x08
See [ARDUINO_I2C_PROTOCOL.md](./ARDUINO_I2C_PROTOCOL.md) for protocol details.
Example firmware structure:
```cpp
#include <Wire.h>
volatile int16_t x_position = 0;
volatile int16_t z_position = 0;
void setup() {
Wire.begin(0x08);
Wire.onRequest(requestEvent);
// Setup encoder interrupts
pinMode(2, INPUT);
pinMode(3, INPUT);
attachInterrupt(0, x_encoder_isr, CHANGE);
attachInterrupt(1, z_encoder_isr, CHANGE);
}
void requestEvent() {
byte buffer[4];
buffer[0] = (byte)x_position;
buffer[1] = (byte)(x_position >> 8);
buffer[2] = (byte)z_position;
buffer[3] = (byte)(z_position >> 8);
Wire.write(buffer, 4);
}
// Encoder ISR routines...
void loop() {}
```
Refer to the `i2c_encoder/` directory for the complete Arduino sketch.
## Step 4: Verify I2C Connection
Test I2C communication:
```bash
# Install I2C tools
sudo apt-get install i2c-tools
# Scan for I2C devices
i2cdetect -y 1
# Should show Arduino at address 0x08:
# 0 1 2 3 4 5 6 7 8 9 a b c d e f
# 00: -- -- -- -- -- -- -- -- 08 -- -- -- --
```
## Step 5: Auto-Start Configuration
### Option A: Desktop File (GUI Autostart)
Create `/home/pi/.config/autostart/dro.desktop`:
```ini
[Desktop Entry]
Type=Application
Name=DRO
Exec=python3 /home/pi/dro/dro.py --zoomed
StartupNotify=false
X-GNOME-Autostart-always-run=true
```
### Option B: Systemd Service
Create `/etc/systemd/system/dro.service`:
```ini
[Unit]
Description=Digital Read Out (DRO) Application
After=graphical.target
[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/dro
ExecStart=/usr/bin/python3 /home/pi/dro/dro.py --zoomed
Restart=on-failure
RestartSec=5
[Install]
WantedBy=graphical.target
```
Enable and start:
```bash
sudo systemctl daemon-reload
sudo systemctl enable dro.service
sudo systemctl start dro.service
```
### Option C: Shell Script Autostart
Create `/home/pi/dro/autostart.sh`:
```bash
#!/bin/bash
cd /home/pi/dro
python3 dro.py --zoomed
```
Make executable:
```bash
chmod +x /home/pi/dro/autostart.sh
```
Add to crontab (for user pi):
```bash
crontab -e
# Add line:
@reboot /home/pi/dro/autostart.sh
```
## Step 6: Test the Installation
### Test Mode (without Arduino)
```bash
cd ~/dro
python3 dro.py --test
# Use keyboard to test:
# A/Z = X encoder up/down
# S/X = Z encoder up/down
```
### With Hardware
```bash
# Start the application
python3 dro.py --zoomed
# Check logs (if systemd service)
journalctl -u dro.service -f
```
## Step 7: Calibration
### Set Scale Factors
Edit `dro.py` and adjust these lines based on your encoder specifications:
```python
# X axis: negative to flip direction
model.set_scale('x', -2.5/200) # mm per step
# Z axis
model.set_scale('z', 90/1000) # mm per step
```
**How to determine:**
1. Move the tool a known distance (e.g., 10 mm)
2. Record the encoder step count
3. Scale = Distance (mm) / Steps
4. Use negative for X if the direction is backwards
### Set Zero Points
1. Position tool at starting location
2. Press **X_0** button to zero X axis
3. Press **Z_0** button to zero Z axis
## Troubleshooting
### No I2C Communication
```bash
# Check I2C is enabled
raspi-config
# Verify devices are connected
i2cdetect -y 1
# Check for USB connections (if Arduino is USB)
ls -la /dev/ttyUSB*
```
### GUI Not Appearing
```bash
# Check display settings
export DISPLAY=:0
python3 dro.py
# Verify Tkinter is installed
python3 -c "import tkinter; print('Tkinter OK')"
```
### I2C Errors
- Check pull-up resistors (4.7 kΩ is standard)
- Verify Arduino firmware is running
- Try slower I2C speed: edit smbus2 initialization
### Position Drift
- Recalibrate scale factors
- Check encoder hardware connections
- Verify encoder isn't slipping
## File Structure
```
/home/pi/dro/
├── dro.py # Main application
├── autostart.sh # Auto-start script
├── power.png # Power button icon
├── test_model.py # Unit tests
├── README.md # Project overview
├── docs/
│ ├── DRO.md # Application documentation
│ └── ARDUINO_I2C_PROTOCOL.md # I2C protocol
└── i2c_encoder/
├── i2c_encoder.ino # Arduino firmware
└── i2c_test.py # Arduino test script
```
## Performance Tips
1. **Reduce Update Interval** (for faster response):
```python
self._update_interval_ms = 50 # 20 Hz instead of 10 Hz
```
2. **Increase I2C Speed**:
```python
bus = smbus2.SMBus(1)
bus.bus.set_clock(400000) # 400 kHz fast mode
```
3. **Use GPIO Directly** (instead of I2C) for more performance:
- Requires hardware changes
- RPi.GPIO library for direct GPIO reading
## Security Notes
- The DRO needs `sudo` access to shutdown (configured via sudoers)
- Store any sensitive config files outside the repo
- Use `.env` files for sensitive data (not committed to git)
## Getting Help
- Check logs: `journalctl -u dro.service -f`
- Test I2C: `i2cget -y 1 0x08 0 i 4`
- Python console test:
```bash
python3
import smbus2
bus = smbus2.SMBus(1)
print(bus.read_i2c_block_data(0x08, 0, 4))
```

158
docs/README.md Normal file
View File

@@ -0,0 +1,158 @@
# Documentation
This directory contains comprehensive documentation for the DRO (Digital Read Out) application.
## Quick Start
1. **[INSTALLATION.md](./INSTALLATION.md)** - Setup guide for Raspberry Pi
- System requirements
- Dependency installation
- Arduino firmware setup
- Auto-start configuration
- Troubleshooting
2. **[DRO.md](./DRO.md)** - Application architecture and usage
- Architecture overview (MVC pattern)
- Class descriptions (Axis, Model, Controller, View)
- Usage instructions and workflows
- Configuration and calibration
- GUI layout and keyboard shortcuts
3. **[ARDUINO_I2C_PROTOCOL.md](./ARDUINO_I2C_PROTOCOL.md)** - Hardware protocol details
- I2C communication format
- Data layout (4-byte packets)
- Arduino firmware requirements
- Encoder interface details
- Debugging and troubleshooting
## Documentation Structure
```
DRO.md
├── Overview - What is DRO?
├── Architecture - MVC pattern and data flow
├── Classes - Detailed class documentation
│ ├── Axis - Single axis representation
│ ├── Model - State management and observers
│ ├── Controller - User input and I2C
│ └── View - Tkinter GUI
├── Usage - How to run the application
├── Configuration - Scale factors and settings
├── Error Handling - Exception handling
├── Testing - Test mode documentation
└── Performance - Design considerations
INSTALLATION.md
├── Requirements - Hardware and software
├── Step 1-7 - Complete setup walkthrough
│ ├── Dependencies
│ ├── I2C configuration
│ ├── Arduino firmware
│ ├── I2C verification
│ ├── Auto-start
│ ├── Testing
│ └── Calibration
├── Troubleshooting - Common issues and fixes
└── Tips - Performance optimization
ARDUINO_I2C_PROTOCOL.md
├── Hardware Setup - Pinout and connections
├── Data Format - 4-byte packet structure
├── Communication Protocol - I2C timing diagrams
├── Arduino Requirements - Firmware template
├── Encoder Interface - Quadrature signals
├── Error Handling - Error cases
├── Debugging - Testing tools
└── Troubleshooting - Problem solving
```
## Key Concepts
### MVC Architecture
The application separates concerns:
- **Model**: Manages encoder positions, scale factors, and offsets
- **View**: Tkinter GUI displaying positions and buttons
- **Controller**: I2C communication, button handling, input processing
### Position Calculation
```
Raw Position (steps from encoder)
× scale factor
Device Position (mm)
↓ + offset
Displayed Position (mm, possibly × 2 for diameter)
```
### I2C Communication
```
Arduino → 4 bytes (X lo, X hi, Z lo, Z hi) → DRO Application
Convert to signed 16-bit integers
Apply scale factors
Update display (10 Hz)
```
## Common Tasks
### Change Scale Factors
**File**: `dro.py` → Search for `model.set_scale()`
```python
model.set_scale('x', -2.5/200) # X axis: mm per step
model.set_scale('z', 90/1000) # Z axis: mm per step
```
### Change GUI Update Rate
**File**: `dro.py` → Class `View.__init__()`
```python
self._update_interval_ms = 100 # Change to 50 for 20 Hz
```
### Enable Auto-Start
**See**: `INSTALLATION.md` → Step 5
Three options: Desktop file, Systemd service, or Cron job
### Test Without Arduino
**Command**: `python3 dro.py --test`
Simulates encoders with keyboard (A/Z/S/X keys)
### Debug I2C Issues
**See**: `ARDUINO_I2C_PROTOCOL.md` → Debugging section
Tools: `i2cdetect`, `i2cget`, Python test script
## Troubleshooting Index
| Issue | Documentation |
|-------|----------------|
| Application won't start | INSTALLATION.md: Troubleshooting |
| I2C not detected | ARDUINO_I2C_PROTOCOL.md: Debugging |
| Positions are wrong | DRO.md: Configuration, INSTALLATION.md: Calibration |
| Arduino not communicating | ARDUINO_I2C_PROTOCOL.md: Troubleshooting |
| Missing dependencies | INSTALLATION.md: Step 1 |
| Auto-start not working | INSTALLATION.md: Step 5 |
## Contributing to Docs
When updating documentation:
1. Keep technical accuracy
2. Include code examples where relevant
3. Update the table of contents
4. Cross-reference related sections
5. Test all commands/instructions before documenting
## See Also
- **README.md** - Project overview
- **code-review-generic.instructions.md** - Code review guidelines
- **python.instructions.md** - Python coding standards

File diff suppressed because one or more lines are too long

106
docs/mock-gui.drawio Normal file
View File

@@ -0,0 +1,106 @@
<mxfile host="Electron" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/26.0.16 Chrome/132.0.6834.196 Electron/34.2.0 Safari/537.36" version="26.0.16">
<diagram name="Sida-1" id="yWZbiVy1Htm2IWZYSUBU">
<mxGraphModel dx="1087" dy="1162" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="1AcGMTNSweSiUO7pp5MJ-3" value="" style="shape=mxgraph.mockup.containers.marginRect;rectMarginTop=10;strokeColor=#666666;strokeWidth=1;dashed=0;rounded=1;arcSize=5;recursiveResize=0;html=1;whiteSpace=wrap;" vertex="1" parent="1">
<mxGeometry x="59" y="240" width="460" height="360" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-4" value="w-frame" style="shape=rect;strokeColor=none;fillColor=#008cff;strokeWidth=1;dashed=0;rounded=1;arcSize=20;fontColor=#ffffff;fontSize=17;spacing=2;spacingTop=-2;align=left;autosize=1;spacingLeft=4;resizeWidth=0;resizeHeight=0;perimeter=none;html=1;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-3">
<mxGeometry x="5" width="90" height="30" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-27" value="" style="shape=mxgraph.mockup.containers.marginRect;rectMarginTop=10;strokeColor=#666666;strokeWidth=1;dashed=0;rounded=1;arcSize=5;recursiveResize=0;html=1;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-3">
<mxGeometry x="70" y="30" width="370" height="155" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-28" value="x-frame" style="shape=rect;strokeColor=none;fillColor=#008cff;strokeWidth=1;dashed=0;rounded=1;arcSize=20;fontColor=#ffffff;fontSize=17;spacing=2;spacingTop=-2;align=left;autosize=1;spacingLeft=4;resizeWidth=0;resizeHeight=0;perimeter=none;html=1;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-27">
<mxGeometry x="5" width="80" height="30" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-29" value="X0" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-27">
<mxGeometry x="250" y="60" width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-30" value="X" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-27">
<mxGeometry x="310" y="60" width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-33" value="&lt;font style=&quot;font-size: 43px;&quot;&gt;1000.000&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;align=right;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-27">
<mxGeometry x="10" y="55" width="230" height="60" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-34" value="" style="shape=mxgraph.mockup.containers.marginRect;rectMarginTop=10;strokeColor=#666666;strokeWidth=1;dashed=0;rounded=1;arcSize=5;recursiveResize=0;html=1;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-3">
<mxGeometry x="70" y="185" width="370" height="155" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-35" value="z-frame" style="shape=rect;strokeColor=none;fillColor=#008cff;strokeWidth=1;dashed=0;rounded=1;arcSize=20;fontColor=#ffffff;fontSize=17;spacing=2;spacingTop=-2;align=left;autosize=1;spacingLeft=4;resizeWidth=0;resizeHeight=0;perimeter=none;html=1;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-34">
<mxGeometry x="5" width="80" height="30" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-36" value="Z0" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-34">
<mxGeometry x="250" y="60" width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-37" value="Z" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-34">
<mxGeometry x="310" y="60" width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-38" value="&lt;font style=&quot;font-size: 43px;&quot;&gt;1000.000&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;align=right;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-34">
<mxGeometry x="10" y="55" width="230" height="60" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-6" value="" style="shape=mxgraph.mockup.containers.marginRect;rectMarginTop=10;strokeColor=#666666;strokeWidth=1;dashed=0;rounded=1;arcSize=5;recursiveResize=0;html=1;whiteSpace=wrap;" vertex="1" parent="1">
<mxGeometry x="519" y="240" width="250" height="360" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-7" value="e-frame" style="shape=rect;strokeColor=none;fillColor=#008cff;strokeWidth=1;dashed=0;rounded=1;arcSize=20;fontColor=#ffffff;fontSize=17;spacing=2;spacingTop=-2;align=left;autosize=1;spacingLeft=4;resizeWidth=0;resizeHeight=0;perimeter=none;html=1;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-6">
<mxGeometry x="5" width="80" height="30" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-5" value="1" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-6">
<mxGeometry x="10" y="70" width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-8" value="2" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-6">
<mxGeometry x="70" y="70" width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-9" value="3" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-6">
<mxGeometry x="130" y="70" width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-11" value="4" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-6">
<mxGeometry x="10" y="130" width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-12" value="5" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-6">
<mxGeometry x="70" y="130" width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-13" value="6" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-6">
<mxGeometry x="130" y="130" width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-14" value="7" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-6">
<mxGeometry x="10" y="190" width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-15" value="8" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-6">
<mxGeometry x="70" y="190" width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-16" value="9" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-6">
<mxGeometry x="130" y="190" width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-17" value="." style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-6">
<mxGeometry x="10" y="250" width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-18" value="0" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-6">
<mxGeometry x="70" y="250" width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-19" value="+/-" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-6">
<mxGeometry x="130" y="250" width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-20" value="enter" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-6">
<mxGeometry x="190" y="190" width="50" height="110" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-21" value="r/D" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-6">
<mxGeometry x="190" y="130" width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-40" value="" style="group" vertex="1" connectable="0" parent="1AcGMTNSweSiUO7pp5MJ-6">
<mxGeometry x="190" y="70" width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-22" value="" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;" vertex="1" parent="1AcGMTNSweSiUO7pp5MJ-40">
<mxGeometry width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="1AcGMTNSweSiUO7pp5MJ-39" value="" style="shape=flexArrow;endArrow=classic;html=1;rounded=0;endWidth=11.03448275862069;endSize=5.3517241379310345;strokeColor=#FFFFFF;strokeWidth=3;" edge="1" parent="1AcGMTNSweSiUO7pp5MJ-40">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="43" y="25" as="sourcePoint" />
<mxPoint x="7" y="24.660000000000025" as="targetPoint" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

9
dro.desktop Normal file
View File

@@ -0,0 +1,9 @@
[Desktop Entry]
Version=1.0
Name=Dro
Type=Application
Exec=/usr/bin/python3 /home/johan/git-clones/pi-dro/dro.py
Terminal=true
TerminalOptions=\s--noclose
#StartupNotify=true
Path=/home/johan/git-clones/pi-dro

383
dro.py Normal file
View File

@@ -0,0 +1,383 @@
#/usr/bin/python3
import tkinter as tk
from tkinter import simpledialog
from enum import IntEnum
import subprocess
import getopt
import sys
import shutil
import logging
logger = logging.getLogger(__name__)
# Prefer system-installed `smbus2`, otherwise fall back to local `smbus2.py`.
try:
import smbus2
except ImportError:
import RPi.fake_smbus as smbus2 # type: ignore
logger.warning('smbus2 not available; using fake smbus2.py')
class XMode(IntEnum):
RADIUS = 0
DIAMETER = 1
class Updated(IntEnum):
POS_X = 0
POS_Z = 1
X_MODE = 2
FULLSCREEN = 3
class Axis:
""" represent one axis in the model """
def __init__(self, name: str):
self._position = 0 # raw position from encoder
self._scale = 1.0
self._offset = 0.0
self._name = name
def set_position(self, pos: float):
""" set the position (mm)."""
self._position = pos
def get_position(self) -> float:
"""Return the position (mm)."""
return self._position
def set_offset(self, offset: float):
""" set the offset (mm)."""
self._offset = offset
def get_offset(self) -> float:
"""Return the offset (mm)."""
return self._offset
def set_scale(self, scale: float):
""" set the scale factor (mm per step)."""
self._scale = scale
def get_scale(self) -> float:
"""Return the scale factor (mm per step)."""
return self._scale
class Model:
""" represent the DRO model """
def __init__(self, fullscreen = False):
self._observers = []
self._x_mode = XMode.RADIUS # denote x axis mode (radius/diameter)
self._axis = {}
self._axis['x'] = Axis('x')
self._axis['z'] = Axis('z')
self._fullscreen = fullscreen
def attatch(self, o):
self._observers.append(o)
def notify(self, updated = None):
for o in self._observers:
o.update(updated)
def steps_to_position(self, axis, steps : int) -> float:
""" convert steps to position in mm. Always radius for x axis """
if axis in self._axis:
return steps * self._axis[axis].get_scale()
return 0
def set_position(self, axis, pos : float):
""" set position in mm. Always radius for x axis """
if axis in self._axis:
if abs(self._axis[axis].get_position() - pos) >= 0.005:
self._axis[axis].set_position(pos)
self.notify(self._string_to_updated_axis(axis))
def set_offset(self, axis, offset : float):
""" set offset in mm. Always radius for x axis """
if axis in self._axis:
self._axis[axis].set_offset(offset)
self.notify(self._string_to_updated_axis(axis))
def get_effective_position(self, axis) -> float:
""" get position with offset applied """
if axis in self._axis:
pos = self._axis[axis].get_position() + self._axis[axis].get_offset()
""" adjust for x mode """
if axis == 'x' and self._x_mode == XMode.DIAMETER:
return pos * 2.0
else:
return pos
return None
def get_position(self, axis):
""" get position without offset applied """
if axis in self._axis:
return self._axis[axis].get_position()
return None
def get_x_mode(self):
""" get x axis mode """
return self._x_mode
def set_toggle_x_mode(self):
""" toggle x axis mode """
if self._x_mode == XMode.RADIUS:
self._x_mode = XMode.DIAMETER
elif self._x_mode == XMode.DIAMETER:
self._x_mode = XMode.RADIUS
self.notify(Updated.X_MODE)
def set_scale(self, axis, scale):
if axis in self._axis:
self._axis[axis].set_scale(scale)
def set_fullscreen(self, v):
self._fullscreen = v
self.notify(Updated.FULLSCREEN)
def is_fullscreen(self):
return self._fullscreen
def _string_to_updated_axis(self, axis_str):
if axis_str == 'x':
return Updated.POS_X
if axis_str == 'z':
return Updated.POS_Z
return None
class Controller:
""" represent the controller in the MVC pattern """
def __init__(self, model, test_mode = False):
self._model = model
self._test_mode = test_mode
self._bus = smbus2.SMBus(1)
self._address = 0x08 # Arduino I2C Address
def handle_x_position_update(self, steps):
self._model.set_position('x', self._model.steps_to_position('x', steps))
def handle_z_position_update(self, steps):
self._model.set_position('z', self._model.steps_to_position('z', steps))
def handle_btn_x0_press(self):
self._model.set_offset('x', -self._model.get_position('x'))
def handle_btn_z0_press(self):
self._model.set_offset('z', -self._model.get_position('z'))
def handle_btn_toggle_x_mode(self):
self._model.set_toggle_x_mode()
def hanlde_btn_x(self):
title = 'current radius' if self._model.get_x_mode() == XMode.RADIUS else 'current diameter'
pos = simpledialog.askfloat('x', title, initialvalue=0.0, minvalue=-4000.0, maxvalue=4000.0)
if self._model.get_x_mode() == XMode.DIAMETER and pos is not None:
pos = pos / 2.0
if pos is not None:
self._model.set_offset('x', pos-self._model.get_position('x'))
def hanlde_btn_z(self):
pos = simpledialog.askfloat('z', 'current z position', initialvalue=0.0, minvalue=-4000.0, maxvalue=4000.0)
if pos is not None:
self._model.set_offset('z', pos-self._model.get_position('z'))
def toggle_fullscreen(self, event=None):
if self._model.is_fullscreen() == True:
self._model.set_fullscreen(False)
else:
self._model.set_fullscreen(True)
def end_fullscreen(self, event=None):
self._model.set_fullscreen(False)
def handle_btn_calc(self):
calculator_cmd = shutil.which('galculator')
if calculator_cmd != None:
try:
subprocess.Popen(calculator_cmd)
except:
print("can't start calculator")
else:
print("can't find calculator")
def shutdown(self):
if not self._test_mode:
subprocess.call('sudo shutdown -h now', shell=True)
else:
print('will not shutdown in test mode')
def poll_i2c_data(self):
""" poll I2C data from Arduino """
try:
data = self._bus.read_i2c_block_data(self._address, 0, 4)
self.handle_x_position_update(int.from_bytes(bytes(data[0:2]), 'little', signed=True))
self.handle_z_position_update(int.from_bytes(bytes(data[2:4]), 'little', signed=True))
except IOError as e:
logger.error(f"I2C read error: {e}")
except OSError as e:
logger.error(f"I2C OS error: {e}")
class View:
""" represent the view in the MVC pattern """
def __init__(self, model, test_mode = False):
self._model = model
self._model.attatch(self)
self._controller = Controller(model, test_mode)
self.window = tk.Tk()
self.window.resizable(False, False)
#self.window.attributes('-zoomed', zoomed) # This just maximizes it so we can see the window. It's nothing to do with fullscreen.
self.window.attributes("-fullscreen", self._model.is_fullscreen())
w_frame = tk.Frame(self.window)
w_frame.grid(row=0, column=0, sticky='n')
e_frame = tk.Frame(self.window)
e_frame.grid(row=0, column=1, sticky='n')
# x frame
x_frame = tk.Frame(w_frame, relief=tk.GROOVE, borderwidth=3)
x_frame.grid(row=0, column=0, padx=5, pady=5)
# use a StringVar for quicker updates from the main thread
self.x_var = tk.StringVar(value=self._position_to_string(self._model.get_effective_position('x')))
self.x_label = tk.Label(x_frame, textvariable=self.x_var, anchor="e", font=("Helvetica", 80), fg="green", bg="black", width=6)
self.x_label.grid(row=0, column=0, padx=5, pady=5)
btn_set_x = tk.Button(master=x_frame, text="X", padx=5, pady=5, font=("Helvetica", 80), command=self._controller.hanlde_btn_x)
btn_set_x.grid(row=0, column=1, padx=5, pady=5, sticky="nw")
btn_set_x0 = tk.Button(master=x_frame, text="X_0", padx=5, pady=5, font=("Helvetica", 80), command=self._controller.handle_btn_x0_press)
btn_set_x0.grid(row=0, column=2, padx=5, pady=5, sticky="nw")
# Z frame
z_frame = tk.Frame(w_frame, relief=tk.GROOVE, borderwidth=3)
z_frame.grid(row=1, column=0, padx=5, pady=5)#, sticky="nw")
self.z_var = tk.StringVar(value=self._position_to_string(self._model.get_effective_position('z')))
self.z_label = tk.Label(z_frame, textvariable=self.z_var, anchor="e", font=("Helvetica", 80), fg="green", bg="black", width=6)
self.z_label.grid(row=0, column=0, padx=5, pady=5)
btn_set_z = tk.Button(master=z_frame, text="Z", padx=5, pady=5, font=("Helvetica", 80), command=self._controller.hanlde_btn_z)
btn_set_z.grid(row=0, column=1, padx=5, pady=5, sticky="nw")
btn_set_z0 = tk.Button(master=z_frame, text="Z_0", padx=5, pady=5, font=("Helvetica", 80), command=self._controller.handle_btn_z0_press)
btn_set_z0.grid(row=0, column=2, padx=5, pady=5, sticky="nw")
# status frame
status_frame = tk.Frame(w_frame, relief=tk.GROOVE, borderwidth=3)
status_frame.grid(row=3, column=0, padx=5, pady=5, sticky='e')
self.x_mode_var = tk.StringVar(value=str(self._mode_to_text()))
self.x_mode_label = tk.Label(status_frame, textvariable=self.x_mode_var, font=("Helvetica", 80), fg="red")
self.x_mode_label.grid(row=0, column=0, padx=5, pady=5, sticky="e")
# button frame
btn_frame = tk.Frame(e_frame, relief=tk.GROOVE, borderwidth=3)
btn_frame.grid(row=0, column=0, padx=5, pady=5)
btn_rd = tk.Button(master=btn_frame, text="r/D", padx=5, pady=5, font=("Helvetica", 80), width=4, command=self._controller.handle_btn_toggle_x_mode)
btn_rd.grid(row=1, column=0, padx=5, pady=5, sticky="nw")
btn_calc = tk.Button(master=btn_frame, text="Calc", padx=5, pady=5, font=("Helvetica", 80), width=4, command=self._controller.handle_btn_calc)
btn_calc.grid(row=2, column=0, padx=5, pady=15, sticky="nw")
#btn_enter = tk.Button(master=btn_frame, text="TBD", padx=5, pady=5, font=("Helvetica", 80), width=4)#, command=self._controller.horizontal_end)
self.photo = tk.PhotoImage(file="power.png")
btn_off = tk.Button(master=btn_frame, image=self.photo, command=self._controller.shutdown)
btn_off.grid(row=3, column=0, padx=5, pady=15, sticky="nw")
# bind keyboar inputs
self.window.bind("<F11>", self._controller.toggle_fullscreen)
self.window.bind("<Escape>", self._controller.end_fullscreen)
# schedule the GUI update loop on the main thread
self._update_interval_ms = 100 # update at 10Hz by default
self.window.after(self._update_interval_ms, self._poll_input)
self.window.update()
def _mode_to_text(self):
if self._model.get_x_mode() == XMode.DIAMETER:
return 'D'
if self._model.get_x_mode() == XMode.RADIUS:
return 'r'
def _position_to_string(self, pos):
return str("{:.2f}".format(pos))
def start(self):
self.window.mainloop()
def update_x(self):
""" update x position in view (main thread) """
pos = self._model.get_effective_position('x')
self.x_var.set(self._position_to_string(pos))
def update_z(self):
""" update z position in view (main thread) """
pos = self._model.get_effective_position('z')
self.z_var.set(self._position_to_string(pos))
def update(self, updated = None):
""" called by model when something has changed """
if updated == Updated.POS_X:
self.update_x()
if updated == Updated.POS_Z:
self.update_z()
if updated == Updated.X_MODE:
# main thread will pick this up through the scheduled update
self.x_mode_var.set(str(self._mode_to_text()))
self.update_x()
if updated == Updated.FULLSCREEN:
self.window.attributes("-fullscreen", self._model.is_fullscreen())
def _poll_input(self):
""" scheduled function (runs on tkinter main loop) to update the view."""
try:
# request data
self._controller.poll_i2c_data()
finally:
# re-schedule
self.window.after(self._update_interval_ms, self._poll_input)
def main():
_zoomed = False
_test = False
try:
opts, args = getopt.getopt(sys.argv[1:], "", ["zoomed", "test"])
except getopt.GetoptError as err:
# print help information and exit:
print(err) # will print something like "option -a not recognized"
sys.exit(2)
for o, a in opts:
if o == "--zoomed":
_zoomed = True
elif o == "--test":
_test = True
else:
assert False, "unhandled option"
model = Model(_zoomed)
view = View(model, _test)
model.set_scale('x', -2.5/200) #negative to flip direction
model.set_scale('z', 90/1000)
if _test:
try:
x_test_encoder = smbus2.createTestEncoder()
view.window.bind('a', x_test_encoder._rotate_cw)
view.window.bind('z', x_test_encoder._rotate_ccw)
z_test_encoder = smbus2.createTestEncoder()
view.window.bind('s', z_test_encoder._rotate_cw)
view.window.bind('x', z_test_encoder._rotate_ccw)
except:
logger.error("couldn't create test encoders...")
view.start()
if __name__ == '__main__':
main()

48
dro_print.py Normal file
View File

@@ -0,0 +1,48 @@
#/usr/bin/python3
"""
is the port available:
ls /dev/*i2c*
scan connected I2C slaves:
sudo i2cdetect -y 1
"""
from enum import IntEnum
import time
# Prefer system-installed `smbus2`, otherwise fall back to local `smbus2.py`.
try:
import smbus2
except ImportError:
import RPi.fake_smbus as smbus2 # type: ignore
print('smbus2 not available; using fake smbus2.py')
_bus = smbus2.SMBus(1)
def poll_i2c_data():
""" poll I2C data from Arduino """
try:
data = _bus.read_i2c_block_data(0x08, 0, 4)
x_position_update = int.from_bytes(bytes(data[0:2]), 'little', signed=True)
z_position_update = int.from_bytes(bytes(data[2:4]), 'little', signed=True)
print(f"X Position: {x_position_update}, Z Position: {z_position_update}")
except IOError as e:
print(f"I2C read error: {e}")
except OSError as e:
print(f"I2C OS error: {e}")
def main():
try:
while 1:
poll_i2c_data()
time.sleep(0.2) # Poll every 200ms
except KeyboardInterrupt:
raise SystemExit
if __name__ == '__main__':
main()

28
i2c_encoder/encoder.cpp Normal file
View File

@@ -0,0 +1,28 @@
#include "encoder.h"
// public:
// constructor : sets pins as inputs and turns on pullup resistors
RotaryEncoder::RotaryEncoder( const String& argName, int argClkPin, int argDtPin, int argPinMode) : name(argName), clkPin ( argClkPin), dtPin( argDtPin )
{
// set pin a and b to be input with pull up enabled
pinMode(clkPin, argPinMode);
pinMode(dtPin, argPinMode);
position = 0;
}
void RotaryEncoder::update(void)
{
digitalRead(dtPin) ? position++ : position--;
}
int RotaryEncoder::getPosition ()
{
return position;
}
void RotaryEncoder::clearPosition()
{
position = 0;
}

64
i2c_encoder/encoder.h Normal file
View File

@@ -0,0 +1,64 @@
#ifndef __C_ROTARY_ENCODER_H__
#define __C_ROTARY_ENCODER_H__
#include <Arduino.h>
class RotaryEncoder {
/*
wraps encoder setup and update functions in a class
!!! NOTE : user must call the encoders update method from an
interrupt function himself! i.e. user must attach an interrupt to the
encoder pin A and call the encoder update method from within the
interrupt
uses Arduino pull-ups on A & B channel outputs
turning on the pull-ups saves having to hook up resistors
to the A & B channel outputs
// ------------------------------------------------------------------------------------------------
// Example usage :
// ------------------------------------------------------------------------------------------------
#include "Encoder.h"
Encoder encoder(2, 4);
void setup() {
attachInterrupt(0, doEncoder, CHANGE);
Serial.begin (115200);
Serial.println("start");
}
void loop(){
// do some stuff here - the joy of interrupts is that they take care of themselves
}
void doEncoder(){
encoder.update();
Serial.println( encoder.getPosition() );
}
// ------------------------------------------------------------------------------------------------
// Example usage end
// ------------------------------------------------------------------------------------------------
*/
public:
// constructor : sets pins as inputs and turns on pullup resistors
RotaryEncoder( const String& argName, int argClkPin, int argDtPin, int argPinMode = INPUT_PULLUP);
~RotaryEncoder() {};
// call this from your interrupt function
void update();
int getPosition();
void clearPosition();
private:
String name;
volatile int position;
int clkPin; // clock pin
int dtPin; // direction pin
};
#endif // __C_ROTARY_ENCODER_H__

View File

@@ -0,0 +1,58 @@
#include <Wire.h>
#include "encoder.h"
constexpr auto kSlaveAddress = 0x08; // I2C address for Arduino
constexpr auto kXEncoderClockPin = 2;
constexpr auto kXEncoderDirectionPin = 8;
constexpr auto kZEncoderClockPin = 3;
constexpr auto kZEncoderDirectionPin = 9;
static void x_isr(void);
static void z_isr(void);
static void sendData(void);
static RotaryEncoder x_encoder{"X", kXEncoderClockPin, kXEncoderDirectionPin, INPUT};
static RotaryEncoder z_encoder{"Z", kZEncoderClockPin, kZEncoderDirectionPin, INPUT_PULLUP};
static char data[4] = {0, 0, 0, 0};
void setup(){
//Serial.begin(38400);
//Serial.setTimeout(500);
//Serial.println("setup()");
Wire.begin(kSlaveAddress);
Wire.onRequest(sendData);
attachInterrupt(digitalPinToInterrupt(kXEncoderClockPin), x_isr, RISING);
attachInterrupt(digitalPinToInterrupt(kZEncoderClockPin), z_isr, RISING);
}
void loop() {
/*Serial.println(x_encoder.getPosition());
Serial.println(z_encoder.getPosition());
delay(250);*/
}
// Handle request to send I2C data
void sendData() {
int x_pos = x_encoder.getPosition();
int z_pos = z_encoder.getPosition();
data[0] = 0x00ff & x_pos;
data[1] = (0xff00 & x_pos) >> 8;
data[2] = 0x00ff & z_pos;
data[3] = (0xff00 & z_pos) >> 8;
Wire.write(data, 4);
}
static void x_isr(void)
{
x_encoder.update();
}
static void z_isr(void)
{
z_encoder.update();
}

BIN
power.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

118
test_model.py Normal file
View File

@@ -0,0 +1,118 @@
import unittest
import dro
class Observer:
def __init__(self):
self.reset()
self.nbr_of_calls = 0
self.has_been_called = False
def update(self, updated = None):
self.nbr_of_calls += 1
self.has_been_called = True
self.updated = updated
def reset(self):
self.nbr_of_calls = 0
self.has_been_called = False
class TestModel(unittest.TestCase):
#def check_pos(self, actual, expected):
# self.assertTrue(int(self.model.get_position_as_string('x')) == 1)
def setUp(self):
self.model = dro.Model()
def test_init(self):
self.assertEqual(self.model.get_position('x'), 0)
self.assertEqual(self.model.get_position('z'), 0)
self.assertEqual(self.model.get_effective_position('x'), 0)
self.assertEqual(self.model.get_effective_position('z'), 0)
def test_wrong_axis(self):
o = Observer()
self.model.attatch(o)
self.model.set_position('apa', 1)
self.assertFalse(o.has_been_called)
def test_set_pos(self):
o = Observer()
self.model.attatch(o)
self.model.set_position('x', 1)
self.assertTrue(o.updated == dro.Updated.POS_X)
self.model.set_position('z', -11)
self.assertTrue(o.updated == dro.Updated.POS_Z)
self.assertEqual(o.nbr_of_calls, 2)
self.assertEqual(int(self.model.get_effective_position('x')), 1)
self.assertEqual(int(self.model.get_effective_position('z')), -11)
def test_set_offset(self):
o = Observer()
self.model.attatch(o)
self.model.set_offset('x', -1)
self.assertTrue(o.updated == dro.Updated.POS_X)
self.model.set_offset('z', 12.0)
self.assertTrue(o.updated == dro.Updated.POS_Z)
self.assertEqual(o.nbr_of_calls, 2)
self.assertEqual(int(self.model.get_effective_position('x')), -1)
self.assertEqual(int(self.model.get_effective_position('z')), 12.0)
def test_toogle_x_mode(self):
o = Observer()
self.model.attatch(o)
self.model.set_position('x', -11.49)
self.model.set_offset('x', 0.49)
self.model.set_toggle_x_mode()
self.assertEqual(self.model.get_position('x'), -11.49)
self.assertEqual(self.model.get_effective_position('x'), -22)
self.assertTrue(o.updated == dro.Updated.X_MODE)
def test_toogle_x_mode_z_unchanged(self):
o = Observer()
self.model.attatch(o)
self.model.set_position('z', 11.49)
self.model.set_offset('z', -0.49)
self.model.set_toggle_x_mode()
self.assertEqual(self.model.get_position('z'), 11.49)
self.assertEqual(self.model.get_effective_position('z'), 11.0)
self.assertTrue(o.updated == dro.Updated.X_MODE)
def test_set_position_no_notify_if_unchanged(self):
o = Observer()
self.model.attatch(o)
self.model.set_position('x', 10)
self.assertEqual(o.nbr_of_calls, 1)
self.model.set_position('x', 10)
self.assertEqual(o.nbr_of_calls, 1) # No change, no notify
self.model.set_position('x', 20)
self.assertEqual(o.nbr_of_calls, 2) # Changed, should notify
def test_set_position_no_notify_if_small_change(self):
o = Observer()
self.model.attatch(o)
self.model.set_position('x', 10)
self.assertEqual(o.nbr_of_calls, 1)
self.model.set_position('x', 10+0.003)
self.assertEqual(o.nbr_of_calls, 1) # No change, no notify
def test_steps_to_position(self):
self.model.set_scale('x', 2.5/200) # 200 steps per unit
pos = self.model.steps_to_position('x', 500)
self.assertEqual(pos, 6.25) # 200 * 1.5 = 300 steps
if __name__ == '__main__':
unittest.main()

48
todo.txt Normal file
View File

@@ -0,0 +1,48 @@
askfloat funkar inte med alltid??????
FUNKAR inte
z stör x
om man drar ut z störs inte x
x verkar inte störa z
FUNKAR inte
Pi'n vaknar när encodern rör sig
ta bort
dtoverlay=gpio-shutdown
från /boot/config.txt:
KLAR starta med knapp
https://forums.raspberrypi.com/viewtopic.php?t=217442
löd header på global_en och gnd för startknapp
KLAR i2c:
handle_btn_x0_press måste sätta en offset i axis som gör att offset+position blir 0
self._model.set_offset('x', -get_position)
motsvarande för hanlde_btn_x
KLART
desktop-filen pekar på ett shell-script som startar dro.py
dro.desktop autostartar inte???
men går att starta via file-explorer?????
KLART
stänga av med shutdown utan lösen:
sudo visudo
user_name ALL=(ALL) NOPASSWD: /sbin/poweroff, /sbin/reboot, /sbin/shutdown
KLART avstängningsknapp
knapp som kortsluter pin 5 (GPIO3, grön) och jord
och
"adding this to /boot/config.txt:
dtoverlay=gpio-shutdown
"
och
https://askubuntu.com/questions/168879/shutdown-from-terminal-without-entering-password
SW
knapp till funktion/command:
subprocess.call('sudo shutdown -h now', shell=True)