From 8cb0240ca2bb85b5c2d7535924152d8b65764f9e Mon Sep 17 00:00:00 2001 From: Johan Date: Sat, 21 Mar 2026 23:51:53 +0100 Subject: [PATCH] adding files --- README.md | 93 ++++++++ RPi/.gitkeep | 0 RPi/fake_smbus.py | 37 ++++ autostart.sh | 4 + docs/ARDUINO_I2C_PROTOCOL.md | 228 ++++++++++++++++++++ docs/DRO.md | 282 ++++++++++++++++++++++++ docs/INSTALLATION.md | 307 ++++++++++++++++++++++++++ docs/README.md | 158 ++++++++++++++ docs/dro-schematics_mk2.drawio | 191 ++++++++++++++++ docs/mock-gui.drawio | 106 +++++++++ dro.desktop | 9 + dro.py | 383 +++++++++++++++++++++++++++++++++ dro_print.py | 48 +++++ i2c_encoder/encoder.cpp | 28 +++ i2c_encoder/encoder.h | 64 ++++++ i2c_encoder/i2c_encoder.ino | 58 +++++ power.png | Bin 0 -> 14445 bytes test_model.py | 118 ++++++++++ todo.txt | 48 +++++ 19 files changed, 2162 insertions(+) create mode 100644 README.md create mode 100644 RPi/.gitkeep create mode 100644 RPi/fake_smbus.py create mode 100644 autostart.sh create mode 100644 docs/ARDUINO_I2C_PROTOCOL.md create mode 100644 docs/DRO.md create mode 100644 docs/INSTALLATION.md create mode 100644 docs/README.md create mode 100644 docs/dro-schematics_mk2.drawio create mode 100644 docs/mock-gui.drawio create mode 100644 dro.desktop create mode 100644 dro.py create mode 100644 dro_print.py create mode 100644 i2c_encoder/encoder.cpp create mode 100644 i2c_encoder/encoder.h create mode 100644 i2c_encoder/i2c_encoder.ino create mode 100644 power.png create mode 100644 test_model.py create mode 100644 todo.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..8991616 --- /dev/null +++ b/README.md @@ -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. diff --git a/RPi/.gitkeep b/RPi/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/RPi/fake_smbus.py b/RPi/fake_smbus.py new file mode 100644 index 0000000..0522055 --- /dev/null +++ b/RPi/fake_smbus.py @@ -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 \ No newline at end of file diff --git a/autostart.sh b/autostart.sh new file mode 100644 index 0000000..292aa7f --- /dev/null +++ b/autostart.sh @@ -0,0 +1,4 @@ +#!/bin/bash +cd /home/johan/git-clones/pi-dro +python3 -u dro.py>dro.log 2>&1 +#galculator diff --git a/docs/ARDUINO_I2C_PROTOCOL.md b/docs/ARDUINO_I2C_PROTOCOL.md new file mode 100644 index 0000000..56e7ccf --- /dev/null +++ b/docs/ARDUINO_I2C_PROTOCOL.md @@ -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('> 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) diff --git a/docs/DRO.md b/docs/DRO.md new file mode 100644 index 0000000..53345ac --- /dev/null +++ b/docs/DRO.md @@ -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 diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md new file mode 100644 index 0000000..5a7dad8 --- /dev/null +++ b/docs/INSTALLATION.md @@ -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 + +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)) + ``` diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..3dccbdb --- /dev/null +++ b/docs/README.md @@ -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 diff --git a/docs/dro-schematics_mk2.drawio b/docs/dro-schematics_mk2.drawio new file mode 100644 index 0000000..ce554ff --- /dev/null +++ b/docs/dro-schematics_mk2.drawio @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/mock-gui.drawio b/docs/mock-gui.drawio new file mode 100644 index 0000000..3c7f2b1 --- /dev/null +++ b/docs/mock-gui.drawio @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dro.desktop b/dro.desktop new file mode 100644 index 0000000..ee57d6e --- /dev/null +++ b/dro.desktop @@ -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 diff --git a/dro.py b/dro.py new file mode 100644 index 0000000..6f3fe22 --- /dev/null +++ b/dro.py @@ -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("", self._controller.toggle_fullscreen) + self.window.bind("", 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() diff --git a/dro_print.py b/dro_print.py new file mode 100644 index 0000000..84f6cda --- /dev/null +++ b/dro_print.py @@ -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() diff --git a/i2c_encoder/encoder.cpp b/i2c_encoder/encoder.cpp new file mode 100644 index 0000000..ac04f6e --- /dev/null +++ b/i2c_encoder/encoder.cpp @@ -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; +} diff --git a/i2c_encoder/encoder.h b/i2c_encoder/encoder.h new file mode 100644 index 0000000..81ea062 --- /dev/null +++ b/i2c_encoder/encoder.h @@ -0,0 +1,64 @@ + #ifndef __C_ROTARY_ENCODER_H__ +#define __C_ROTARY_ENCODER_H__ + +#include + +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__ diff --git a/i2c_encoder/i2c_encoder.ino b/i2c_encoder/i2c_encoder.ino new file mode 100644 index 0000000..250874f --- /dev/null +++ b/i2c_encoder/i2c_encoder.ino @@ -0,0 +1,58 @@ +#include +#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(); +} diff --git a/power.png b/power.png new file mode 100644 index 0000000000000000000000000000000000000000..f781aa076c5498786aea2041fd6c8f564a4a0140 GIT binary patch literal 14445 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 literal 0 HcmV?d00001 diff --git a/test_model.py b/test_model.py new file mode 100644 index 0000000..5fe8014 --- /dev/null +++ b/test_model.py @@ -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() \ No newline at end of file diff --git a/todo.txt b/todo.txt new file mode 100644 index 0000000..1159a26 --- /dev/null +++ b/todo.txt @@ -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)