adding more files

This commit is contained in:
2026-03-21 14:24:10 +01:00
parent dd537b7961
commit 019223b225
8 changed files with 1358 additions and 0 deletions

93
README.md Normal file
View File

@@ -0,0 +1,93 @@
# ai-agents
## 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://192.168.50.14:8929/johan/ai-agents.git
git branch -M main
git push -uf origin main
```
## Integrate with your tools
- [ ] [Set up project integrations](http://192.168.50.14:8929/johan/ai-agents/-/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.

116
ask_llm.py Normal file
View File

@@ -0,0 +1,116 @@
import requests, json
import getopt
import sys
from enum import IntEnum
#docker exec -it ollama ollama run llama3.2
class State(IntEnum):
IDLE = 0
THINKING = 1
RESPONDING = 2
DONE = 3
state = State.IDLE
#spinner
#https://www.youtube.com/watch?v=vk_s-gDTqkM
def _usage():
print("Usage: ask_llm.py [options] \"your question here\"")
print("Options:")
print(" -h, --help Show this help message and exit")
print(" -p, --print Print the command without executing (dry run)")
print(" -i, --ip=IP Specify the IP address of the Ollama server (default: 192.168.50.215)")
print(" -m, --model=MODEL Specify the model to use (default: llama3.2)")
print("Example:")
print('e.g python3 ask_llm.py --ip="192.168.50.215" -m "dolphin3:8b" "summarize this file: $(cat docker_log_pihole.txt)"')
def update_state(new_state):
global state
if new_state != state:
print(f"\n\n---\n\n## State changed from {state.name} to {new_state.name}\n\n")
state = new_state
def ask_llm(url, payload):
response = requests.post(url, json=payload, stream=True) # stream=True tells requests to read the response as a live data stream
# Ollama sends one JSON object per line as it generates text
for line in response.iter_lines():
if line:
data = json.loads(line.decode("utf-8"))
#print(data)
try:
if 'error' in data:
print("Error from Ollama API: " + data['error'])
update_state(State.DONE)
continue
except Exception as e:
print("Exception while checking for error: " + str(e))
if 'thinking' in data and data['thinking'] != '':
update_state(State.THINKING)
print(data['thinking'], end="", flush=True)
if 'response' in data and data['response'] != '':
update_state(State.RESPONDING)
print(data['response'], end="", flush=True)
if 'done' in data and data['done'] == True:
update_state(State.DONE)
print('done_reason: ' + data['done_reason'])
print('total_duration: ' + str(data['total_duration']))
print('load_duration: ' + str(data['load_duration']))
print('prompt_eval_count: ' + str(data['prompt_eval_count']))
print('prompt_eval_duration: ' + str(data['prompt_eval_duration']))
print('eval_count: ' + str(data['eval_count']))
print('eval_duration: ' + str(data['eval_duration']))
print('\n')
def main():
# Define the Ollama API endpoint
try:
opts, args = getopt.getopt(sys.argv[1:], "phi:m:", ["ip=", "print", "help", "model="])
except getopt.GetoptError as err:
# print help information and exit:
print(err) # will print something like "option -a not recognized"
sys.exit(2)
_ip = "192.168.50.14"
_model = "llama3.2" # or "gpt-oss:20b" or "gemma3:4b" or "qwen3:8b"
### parse input command line
for o, a in opts:
if o in ("-h", "--help"):
_usage()
sys.exit()
elif o in ("-p", "--print"):
# Dry run option, not implemented in this snippet
pass
elif o in ("-i", "--ip"):
_ip = a
elif o in ("-m", "--model"):
_model = a
else:
print(o, a)
assert False, "unhandled option"
if args =="" or len(args) == 0:
print("No command provided. Use -h for help.")
sys.exit(2)
_url = "http://" + _ip + ":11434/api/generate"
#print ("ask: " + args[0])
# Send a prompt to the model
payload = {
"model": _model,
"prompt": args[0]
}
ask_llm(_url, payload)
if __name__ == '__main__':
main()

185
docker_log_pihole.txt Normal file
View File

@@ -0,0 +1,185 @@
[i] Setting up user & group for the pihole user
[i] PIHOLE_UID not set in environment, using default (1000)
[i] PIHOLE_GID not set in environment, using default (1000)
[i] Starting FTL configuration
[i] Password already set in config file
[i] Starting crond for scheduled scripts. Randomizing times for gravity and update checker
[i] Ensuring logrotate script exists in /etc/pihole
[i] Gravity migration checks
[i] Existing gravity database found - schema will be upgraded if necessary
[i] pihole-FTL pre-start checks
[i] Setting capabilities on pihole-FTL where possible
[i] Applying the following caps to pihole-FTL:
* CAP_CHOWN
* CAP_NET_BIND_SERVICE
* CAP_NET_RAW
* CAP_NET_ADMIN
[i] Starting pihole-FTL (no-daemon) as pihole
[i] Version info:
Core version is v6.3 (Latest: v6.3)
Web version is v6.4 (Latest: v6.4)
FTL version is v6.4.1 (Latest: v6.4.1)
2025-11-30 17:09:32.751 UTC [70M] INFO: ########## FTL started on 1be86b5a6692! ##########
2025-11-30 17:09:32.752 UTC [70M] INFO: FTL branch: master
2025-11-30 17:09:32.752 UTC [70M] INFO: FTL version: v6.4.1
2025-11-30 17:09:32.752 UTC [70M] INFO: FTL commit: 8d1add8d
2025-11-30 17:09:32.752 UTC [70M] INFO: FTL date: 2025-11-27 18:02:19 +0000
2025-11-30 17:09:32.752 UTC [70M] INFO: FTL user: pihole
2025-11-30 17:09:32.752 UTC [70M] INFO: Compiled for linux/amd64 (compiled on CI) using cc (Alpine 14.2.0) 14.2.0
2025-11-30 17:09:32.754 UTC [70M] INFO: Wrote config file:
2025-11-30 17:09:32.754 UTC [70M] INFO: - 162 total entries
2025-11-30 17:09:32.754 UTC [70M] INFO: - 159 entries are default
2025-11-30 17:09:32.754 UTC [70M] INFO: - 3 entries are modified
2025-11-30 17:09:32.754 UTC [70M] INFO: - 0 entries are forced through environment
2025-11-30 17:09:32.756 UTC [70M] INFO: Parsed config file /etc/pihole/pihole.toml successfully
2025-11-30 17:09:32.756 UTC [70M] INFO: PID file does not exist or not readable
2025-11-30 17:09:32.756 UTC [70M] INFO: No other running FTL process found.
2025-11-30 17:09:32.756 UTC [70M] WARNING: Insufficient permissions to set process priority to -10 (CAP_SYS_NICE required), process priority remains at 0
2025-11-30 17:09:32.758 UTC [70M] INFO: PID of FTL process: 70
2025-11-30 17:09:32.758 UTC [70M] INFO: listening on 0.0.0.0 port 53
2025-11-30 17:09:32.758 UTC [70M] INFO: listening on :: port 53
2025-11-30 17:09:32.759 UTC [70M] INFO: PID of FTL process: 70
2025-11-30 17:09:32.760 UTC [70M] INFO: Database version is 21
2025-11-30 17:09:32.760 UTC [70M] INFO: Database successfully initialized
2025-11-30 17:09:33.187 UTC [70M] INFO: Imported 43832 queries from the on-disk database (it has 1248190 rows)
2025-11-30 17:09:33.188 UTC [70M] INFO: Parsing queries in database
2025-11-30 17:09:33.225 UTC [70M] INFO: 10000 queries parsed...
2025-11-30 17:09:33.253 UTC [70M] INFO: 20000 queries parsed...
2025-11-30 17:09:33.284 UTC [70M] INFO: 30000 queries parsed...
2025-11-30 17:09:33.316 UTC [70M] INFO: 40000 queries parsed...
2025-11-30 17:09:33.329 UTC [70M] INFO: Imported 43832 queries from the long-term database
2025-11-30 17:09:33.329 UTC [70M] INFO: -> Total DNS queries: 43832
2025-11-30 17:09:33.329 UTC [70M] INFO: -> Cached DNS queries: 29711
2025-11-30 17:09:33.329 UTC [70M] INFO: -> Forwarded DNS queries: 8206
2025-11-30 17:09:33.329 UTC [70M] INFO: -> Blocked DNS queries: 5721
2025-11-30 17:09:33.329 UTC [70M] INFO: -> Unknown DNS queries: 0
2025-11-30 17:09:33.329 UTC [70M] INFO: -> Unique domains: 2831
2025-11-30 17:09:33.329 UTC [70M] INFO: -> Unique clients: 2
2025-11-30 17:09:33.329 UTC [70M] INFO: -> DNS cache records: 372
2025-11-30 17:09:33.329 UTC [70M] INFO: -> Known forward destinations: 2
2025-11-30 17:09:33.437 UTC [70M] WARNING: Insufficient permissions to set system time (CAP_SYS_TIME required), NTP client not available
2025-11-30 17:09:33.437 UTC [70/T72] INFO: NTP server listening on 0.0.0.0:123 (IPv4)
2025-11-30 17:09:33.437 UTC [70/T73] INFO: NTP server listening on :::123 (IPv6)
2025-11-30 17:09:33.437 UTC [70M] INFO: FTL is running as user pihole (UID 1000)
2025-11-30 17:09:33.438 UTC [70M] INFO: Reading certificate from /etc/pihole/tls.pem ...
2025-11-30 17:09:33.440 UTC [70M] INFO: Using SSL/TLS certificate file /etc/pihole/tls.pem
2025-11-30 17:09:33.441 UTC [70M] INFO: Web server ports:
2025-11-30 17:09:33.441 UTC [70M] INFO: - 0.0.0.0:80 (HTTP, IPv4, optional, OK)
2025-11-30 17:09:33.441 UTC [70M] INFO: - 0.0.0.0:443 (HTTPS, IPv4, optional, OK)
2025-11-30 17:09:33.441 UTC [70M] INFO: - [::]:80 (HTTP, IPv6, optional, OK)
2025-11-30 17:09:33.441 UTC [70M] INFO: - [::]:443 (HTTPS, IPv6, optional, OK)
2025-11-30 17:09:33.441 UTC [70M] INFO: Restored 0 API sessions from the database
2025-11-30 17:09:33.442 UTC [70M] INFO: Blocking status is enabled
2025-11-30 17:09:33.541 UTC [70/T74] INFO: Compiled 0 allow and 0 deny regex for 2 clients in 0.1 msec
[i] Container stop requested...
[i] pihole-FTL is running - Attempting to shut it down cleanly
2025-12-05 22:02:06.985 UTC [70M] INFO: Asked to terminate by "N/A" (PID 135431, user root UID 0)
2025-12-05 22:02:07.051 UTC [70/T77] INFO: Terminating timer thread
2025-12-05 22:02:07.063 UTC [70/T74] INFO: Terminating database thread
2025-12-05 22:02:07.255 UTC [70M] INFO: Finished final database update
2025-12-05 22:02:07.255 UTC [70M] INFO: Waiting for threads to join
2025-12-05 22:02:07.255 UTC [70M] INFO: Thread housekeeper (1) is idle, terminating it.
2025-12-05 22:02:07.255 UTC [70M] INFO: Thread dns-client (2) is idle, terminating it.
2025-12-05 22:02:07.255 UTC [70M] INFO: Thread webserver (7) is idle, terminating it.
2025-12-05 22:02:07.255 UTC [70M] INFO: All threads joined
2025-12-05 22:02:07.257 UTC [70M] INFO: PID file emptied
2025-12-05 22:02:07.258 UTC [70M] INFO: Stored 0 API sessions in the database
2025-12-05 22:02:07.601 UTC [70M] INFO: ########## FTL terminated after 5d 4h 52m 34s (code 0)! ##########
[i] pihole-FTL exited with status 0
[i] Container will now stop or restart depending on your restart policy
https://docs.docker.com/engine/containers/start-containers-automatically/#use-a-restart-policy
[i] Setting up user & group for the pihole user
[i] PIHOLE_UID not set in environment, using default (1000)
[i] PIHOLE_GID not set in environment, using default (1000)
[i] Starting FTL configuration
[i] Password already set in config file
[i] Starting crond for scheduled scripts. Randomizing times for gravity and update checker
[i] Ensuring logrotate script exists in /etc/pihole
[i] Gravity migration checks
[i] Existing gravity database found - schema will be upgraded if necessary
[i] pihole-FTL pre-start checks
[i] Setting capabilities on pihole-FTL where possible
[i] Applying the following caps to pihole-FTL:
* CAP_CHOWN
* CAP_NET_BIND_SERVICE
* CAP_NET_RAW
* CAP_NET_ADMIN
[i] Starting pihole-FTL (no-daemon) as pihole
[i] Version info:
Core version is v6.3 (Latest: v6.3)
Web version is v6.4 (Latest: v6.4)
FTL version is v6.4.1 (Latest: v6.4.1)
2025-12-05 22:02:19.999 UTC [68M] INFO: ########## FTL started on 1be86b5a6692! ##########
2025-12-05 22:02:19.999 UTC [68M] INFO: FTL branch: master
2025-12-05 22:02:19.999 UTC [68M] INFO: FTL version: v6.4.1
2025-12-05 22:02:19.999 UTC [68M] INFO: FTL commit: 8d1add8d
2025-12-05 22:02:19.999 UTC [68M] INFO: FTL date: 2025-11-27 18:02:19 +0000
2025-12-05 22:02:19.999 UTC [68M] INFO: FTL user: pihole
2025-12-05 22:02:19.999 UTC [68M] INFO: Compiled for linux/amd64 (compiled on CI) using cc (Alpine 14.2.0) 14.2.0
2025-12-05 22:02:20.002 UTC [68M] INFO: Wrote config file:
2025-12-05 22:02:20.002 UTC [68M] INFO: - 162 total entries
2025-12-05 22:02:20.002 UTC [68M] INFO: - 159 entries are default
2025-12-05 22:02:20.002 UTC [68M] INFO: - 3 entries are modified
2025-12-05 22:02:20.002 UTC [68M] INFO: - 0 entries are forced through environment
2025-12-05 22:02:20.004 UTC [68M] INFO: Parsed config file /etc/pihole/pihole.toml successfully
2025-12-05 22:02:20.004 UTC [68M] INFO: PID file does not exist or not readable
2025-12-05 22:02:20.004 UTC [68M] INFO: No other running FTL process found.
2025-12-05 22:02:20.004 UTC [68M] WARNING: Insufficient permissions to set process priority to -10 (CAP_SYS_NICE required), process priority remains at 0
2025-12-05 22:02:20.005 UTC [68M] INFO: PID of FTL process: 68
2025-12-05 22:02:20.006 UTC [68M] INFO: listening on 0.0.0.0 port 53
2025-12-05 22:02:20.006 UTC [68M] INFO: listening on :: port 53
2025-12-05 22:02:20.007 UTC [68M] INFO: PID of FTL process: 68
2025-12-05 22:02:20.008 UTC [68M] INFO: Database version is 21
2025-12-05 22:02:20.008 UTC [68M] INFO: Database successfully initialized
2025-12-05 22:02:20.421 UTC [68M] INFO: Imported 31484 queries from the on-disk database (it has 1416729 rows)
2025-12-05 22:02:20.421 UTC [68M] INFO: Parsing queries in database
2025-12-05 22:02:20.452 UTC [68M] INFO: 10000 queries parsed...
2025-12-05 22:02:20.488 UTC [68M] INFO: 20000 queries parsed...
2025-12-05 22:02:20.523 UTC [68M] INFO: 30000 queries parsed...
2025-12-05 22:02:20.529 UTC [68M] INFO: Imported 31484 queries from the long-term database
2025-12-05 22:02:20.529 UTC [68M] INFO: -> Total DNS queries: 31484
2025-12-05 22:02:20.529 UTC [68M] INFO: -> Cached DNS queries: 22035
2025-12-05 22:02:20.529 UTC [68M] INFO: -> Forwarded DNS queries: 6135
2025-12-05 22:02:20.529 UTC [68M] INFO: -> Blocked DNS queries: 3246
2025-12-05 22:02:20.529 UTC [68M] INFO: -> Unknown DNS queries: 0
2025-12-05 22:02:20.529 UTC [68M] INFO: -> Unique domains: 2286
2025-12-05 22:02:20.529 UTC [68M] INFO: -> Unique clients: 2
2025-12-05 22:02:20.529 UTC [68M] INFO: -> DNS cache records: 238
2025-12-05 22:02:20.529 UTC [68M] INFO: -> Known forward destinations: 2
2025-12-05 22:02:20.677 UTC [68M] WARNING: Insufficient permissions to set system time (CAP_SYS_TIME required), NTP client not available
2025-12-05 22:02:20.677 UTC [68/T70] INFO: NTP server listening on 0.0.0.0:123 (IPv4)
2025-12-05 22:02:20.677 UTC [68M] INFO: FTL is running as user pihole (UID 1000)
2025-12-05 22:02:20.677 UTC [68M] INFO: Reading certificate from /etc/pihole/tls.pem ...
2025-12-05 22:02:20.677 UTC [68M] INFO: Using SSL/TLS certificate file /etc/pihole/tls.pem
2025-12-05 22:02:20.678 UTC [68M] INFO: Web server ports:
2025-12-05 22:02:20.678 UTC [68M] INFO: - 0.0.0.0:80 (HTTP, IPv4, optional, OK)
2025-12-05 22:02:20.678 UTC [68M] INFO: - 0.0.0.0:443 (HTTPS, IPv4, optional, OK)
2025-12-05 22:02:20.678 UTC [68M] INFO: - [::]:80 (HTTP, IPv6, optional, OK)
2025-12-05 22:02:20.678 UTC [68M] INFO: - [::]:443 (HTTPS, IPv6, optional, OK)
2025-12-05 22:02:20.678 UTC [68M] INFO: Restored 0 API sessions from the database
2025-12-05 22:02:20.678 UTC [68/T71] INFO: NTP server listening on :::123 (IPv6)
2025-12-05 22:02:20.680 UTC [68M] INFO: Blocking status is enabled
2025-12-05 22:02:20.784 UTC [68/T72] INFO: Compiled 0 allow and 0 deny regex for 2 clients in 0.0 msec
2025-12-07 04:45:01.079 UTC [68/T72] INFO: Gravity database has been updated, reloading now
2025-12-07 04:45:01.086 UTC [68/T72] INFO: Compiled 0 allow and 0 deny regex for 2 clients in 0.1 msec

108
fake_ChatOllama.py Normal file
View File

@@ -0,0 +1,108 @@
"""Minimal fallback ChatOllama implementation for testing.
This provides a tiny `ChatOllama` class that works with expressions
like `prompt | llm` by implementing `__ror__` and returns a simple
fake response object with a `content` attribute.
"""
from typing import Any
class FakeResponse:
def __init__(self, content: str) -> None:
self.content = content
class _Chain:
def __init__(self, llm: "ChatOllama", prompt: Any) -> None:
self.llm = llm
self.prompt = prompt
def invoke(self, inputs: dict) -> FakeResponse:
service = inputs.get("service", "<unknown>")
data = inputs.get("data", "")
# Summarize the provided data so the fake response is slightly useful
try:
if isinstance(data, list):
summary = f"{len(data)} items"
elif isinstance(data, str):
summary = f"{len(data)} characters"
else:
summary = type(data).__name__
except Exception:
summary = "uninspectable"
content = (
f"[FAKE LLM] Analysis for service '{service}': input contains {summary}."
)
return FakeResponse(content)
class ChatOllama:
"""A tiny fake ChatOllama-compatible object.
Usage (matches fallback import in `log_reader_agent.py`):
from fake_ChatOllama import ChatOllama
llm = ChatOllama(model="llama3:8b")
chain = prompt | llm
resp = chain.invoke({"service": "X", "data": "..."})
print(resp.content)
"""
def __init__(self, model: str = "llama", base_url: str | None = None, **kwargs) -> None:
self.model = model
self.base_url = base_url
self.kwargs = kwargs
def __repr__(self) -> str: # pragma: no cover - trivial
return f"<ChatOllama model={self.model!r} base_url={self.base_url!r}>"
def __ror__(self, other: Any) -> _Chain:
"""Support `prompt | llm` by returning a chain-like object."""
return _Chain(self, other)
# Convenience: allow direct invoke if someone calls llm.invoke(...)
def invoke(self, inputs: dict) -> FakeResponse:
return _Chain(self, None).invoke(inputs)
# Make the object callable so langchain_core can coerce it into a Runnable
def __call__(self, inputs: dict, *args, **kwargs) -> FakeResponse:
# Support being called with either a plain dict (from manual invoke)
# or with a Prompt/ChatPromptValue object (from `prompt | llm` execution).
if isinstance(inputs, dict):
return self.invoke(inputs)
# Fallback: coerce non-dict prompt values to text and return a fake response
try:
# Try common method names for prompt-like objects
if hasattr(inputs, "to_string") and callable(inputs.to_string):
text = inputs.to_string()
elif hasattr(inputs, "to_text") and callable(inputs.to_text):
text = inputs.to_text()
else:
text = str(inputs)
except Exception:
text = str(inputs)
content = """A Pi-Hole docker log!
Let's analyze the service health:
**System Time**: There is a warning indicating that the system time cannot be set (CAP_SYS_TIME required). This might impact the accuracy of logs and timestamps.
**NTP Client**: The NTP client is not available, which could lead to issues with synchronizing system time and potentially affecting DNS resolution.
**DNS Queries**: A significant number of DNS queries are being processed (31484 in this case), indicating that the Pi-Hole is handling a substantial volume of traffic.
**Cache Records**: The cache records count is relatively low (238), suggesting that the cache might not be as effective at reducing the load on the Pi-Hole.
**Blocked Queries**: There are 3246 blocked queries, which could indicate a decent amount of malicious traffic being filtered out.
**Unique Domains and Clients**: There are 2286 unique domains and 2 clients being tracked, suggesting that the Pi-Hole is handling traffic from at least two distinct sources.
**Web Server Ports**: The web server is listening on HTTP (port 80) and HTTPS (port 443) for both IPv4 and IPv6 traffic.
**API Sessions**: There are no API sessions restored from the database, which might indicate that the Pi-Hole is not currently interacting with any external services or APIs.
Overall, it appears that the Pi-Hole is functioning correctly, handling DNS queries, blocking malicious traffic, and maintaining a cache of records. However, the system time issue and lack of NTP client availability might warrant further investigation to ensure accurate logging and timestamping."""
return FakeResponse(content)

26
llm.py Normal file
View File

@@ -0,0 +1,26 @@
import requests, json
#docker exec -it ollama ollama run llama3.2
# Define the local Ollama API endpoint
#url = "http://192.168.50.14:11434/api/generate"
#url = "http://localhost:11434/api/generate"
url = "http://192.168.50.215:11434/api/generate"
# Send a prompt to the Gemma 3 model
payload = {
"model": "llama3.2",
#"model": "gpt-oss:20b",
"prompt": "list all running docker containers"
}
# stream=True tells requests to read the response as a live data stream
response = requests.post(url, json=payload, stream=True)
#print(response)
# Ollama sends one JSON object per line as it generates text
for line in response.iter_lines():
if line:
data = json.loads(line.decode("utf-8"))
# Each chunk has a "response" key containing part of the text
if "response" in data:
print(data["response"], end="", flush=True)

10
mail-test.sh Normal file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
function curl_mail {
curl --url 'smtps://mailout.privat.bahnhof.se:465' --ssl-reqd\
--mail-from 'johan@rydson.st'\
--mail-rcpt 'johan.p1sson@gmail.com'\
--user 'mc536304:Traceur22Traceur'\
-T <(echo -e 'From: johan@rydson.st\nTo: johan.p1sson@gmail.com\nSubject: yo-bot Test\n\n'$1)
}
curl_mail "$(python3 ask_llm.py --ip="192.168.50.215" -m "dolphin3:8b" 'you are Yo-bot. Write a short, friendly mail to Johan, ask how he is doing. do not add any mail subject')"

61
pr_review.sh Normal file
View File

@@ -0,0 +1,61 @@
#!/bin/bash
set -euo pipefail
usage() {
printf "Usage: %s [-s|--source <branch>] [-t|--target <branch>] [source target] (target defaults to 'main')\n" "$0"
exit 2
}
source_branch=""
# default target branch to main
target_branch="main"
# Parse options: -s|--source and -t|--target. Falls back to positional args.
while [[ $# -gt 0 ]]; do
case "$1" in
-s|--source)
source_branch="$2"; shift 2;;
-t|--target)
target_branch="$2"; shift 2;;
-h|--help)
usage;;
--)
shift; break;;
-*)
printf "Unknown option: %s\n" "$1"; usage;;
*)
if [[ -z "$source_branch" ]]; then
source_branch="$1"
elif [[ -z "$target_branch" ]]; then
target_branch="$1"
fi
shift;;
esac
done
if [[ -z "$source_branch" ]]; then
printf "no source branch specified, using current HEAD\n"
source_branch="HEAD"
else
printf "checking out source branch: %s\n" "$source_branch"
git checkout "$source_branch"
fi
# Run unit tests
printf "\nUnit test results:\n´´´bash\n"
python3 -m unittest discover
printf "´´´\n"
# Get list of changed files between source and target branches
export CHANGED_FILES=$(git diff --name-only "$source_branch" "$target_branch")
for file in $CHANGED_FILES; do
if [ -f "${file}" ]; then
printf "\nHere is the diff between source (%s) version of %s and target (%s):\n\n" "$source_branch" "$file" "$target_branch"
printf "´´´diff\n"
git --no-pager diff -U1000 -W "$target_branch" -- "$file"
printf "´´´\n"
fi
done
# adjust model context length if needed

759
pr_review.txt Normal file
View File

@@ -0,0 +1,759 @@
review my pull request.
this files has changed:
RPi/fake_smbus.py
dro.py
test_model.py
todo.txt
test_model.py passes all tests, here is the result:
´´´bash
smbus2 not available; using fake smbus2.py
........
----------------------------------------------------------------------
Ran 8 tests in 0.000s
OK
´´´
here is the diff between my version of RPi/fake_smbus.py and main:
´´´diff
+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
´´´
---
here is the diff between my version of dro.py and main:
´´´diff
diff --git a/dro.py b/dro.py
index 5071466..6e3b919 100755
--- a/dro.py
+++ b/dro.py
@@ -1,396 +1,370 @@
#/usr/bin/python3
-import math
-import encoder
-import threading
-import time
import tkinter as tk
from tkinter import simpledialog
from enum import IntEnum
-import RPi.GPIO as GPIO
import atexit
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
-X_DIRECTION_PIN = 13 #gpio 23
-X_CLOCK_PIN = 11 #gpio 24
-Z_DIRECTION_PIN = 18 #gpio 17
-Z_CLOCK_PIN = 16 #gpio 27
-
class Axis:
""" represent one axis in the model """
- def __init__(self, name):
- self._position = 0
- self._scale = 1
+ 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):
+ def get_raw_position(self):
+ """Return the uncompensated raw position (no offset applied)."""
+ return self._position
+
+ def set_position(self, pos: int):
self._position = pos
- def get_position(self):
- return self._position
+ def set_offset(self, offset: float):
+ self._offset = offset
- def get_position_as_string(self):
- return str("{:.2f}".format(self._position))
+ def get_position(self):
+ return self._position + self._offset
- def set_scale(self, scale):
- self._scale = scale
+ def set_scale(self, scale: float):
+ self._scale = scale
- def update_position(self, direction):
- self._position += direction * self._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 set_position(self, pos, axis):
+ def steps_to_position(self, axis, steps):
if axis in self._axis:
- factor = 0.5 if axis == 'x' and self.get_x_mode() == XMode.DIAMETER else 1
- self._axis[axis].set_position(factor*pos)
+ return steps * self._axis[axis].get_scale()
+ return 0
- self.notify(self._string_to_updated_axis(axis))
+ def set_position(self, axis, pos):
+ """ 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 get_position_as_string(self, axis):
+ def set_offset(self, axis, offset):
+ """ set offset in mm. Always radius for x axis """
if axis in self._axis:
- return str("{:.2f}".format(self.get_position(axis)))
- return None
+ self._axis[axis].set_offset(offset)
+ self.notify(self._string_to_updated_axis(axis))
def get_position(self, axis):
if axis in self._axis:
- factor = 2 if axis == 'x' and self.get_x_mode() == XMode.DIAMETER else 1
- return factor*self._axis[axis].get_position()
+ return self._axis[axis].get_position()
return None
- def update_position(self, direction, axis):
- """ update position of given axis and notify observers """
+ def get_position_uncompensated(self, axis):
+ """ get position without offset applied """
if axis in self._axis:
- self._axis[axis].update_position(direction)
-
- self.notify(self._string_to_updated_axis(axis))
+ return self._axis[axis].get_raw_position()
+ return None
def get_x_mode(self):
return self._x_mode
def set_toggle_x_mode(self):
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
- def handle_x_position_update(self, dir):
- self._model.update_position(dir, 'x')
+ 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, dir):
- self._model.update_position(dir, 'z')
+ 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_position(0, 'x')
+ self._model.set_offset('x', -self._model.get_position_uncompensated('x'))
def handle_btn_z0_press(self):
- self._model.set_position(0, 'z')
+ self._model.set_offset('z', -self._model.get_position_uncompensated('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_position(pos, 'x')
+ self._model.set_offset('x', pos-self._model.get_position_uncompensated('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_position(pos, 'z')
+ self._model.set_offset('z', pos-self._model.get_position_uncompensated('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(data[0] + (data[1] << 8)))
+ 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 Exception as e:
+ logger.error(f"I2C read 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.x_dirty = False
- self.z_dirty = False
-
- # use tkinter's after() loop on the main thread instead of a background thread
- # This avoids cross-thread UI calls and reduces context switching overhead.
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._model.get_position_as_string('x'))
+ self.x_var = tk.StringVar(value=self._position_to_string(self._model.get_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._model.get_position_as_string('z'))
+ self.z_var = tk.StringVar(value=self._position_to_string(self._model.get_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._last_x = self._model.get_position('x')
- self._last_z = self._model.get_position('z')
self._update_interval_ms = 100 # update at 10Hz by default
- self.window.after(self._update_interval_ms, self._update_view)
+ self.window.after(self._update_interval_ms, self._poll_input)
self.window.update()
- #self.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) """
- self.x_dirty = False
pos = self._model.get_position('x')
- # avoid frequent updates when change is negligible
- if abs(pos - self._last_x) >= 0.005:
- self._last_x = pos
- self.x_var.set(self._position_to_string(pos))
+ if self._model.get_x_mode() == XMode.DIAMETER:
+ pos = pos * 2.0
+ self.x_var.set(self._position_to_string(pos))
def update_z(self):
""" update z position in view (main thread) """
- self.z_dirty = False
pos = self._model.get_position('z')
- if abs(pos - self._last_z) >= 0.005:
- self._last_z = pos
- self.z_var.set(self._position_to_string(pos))
+ 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.x_dirty = True
+ self.update_x()
if updated == Updated.POS_Z:
- self.z_dirty = True
+ 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 _update_view(self):
+ def _poll_input(self):
""" scheduled function (runs on tkinter main loop) to update the view."""
try:
- if self.x_dirty:
- self.update_x()
- if self.z_dirty:
- self.update_z()
+ # request data
+ self._controller.poll_i2c_data()
+
finally:
# re-schedule
- self.window.after(self._update_interval_ms, self._update_view)
-
-@atexit.register
-def cleanup():
- GPIO.cleanup(X_CLOCK_PIN)
- GPIO.cleanup(X_DIRECTION_PIN)
- GPIO.cleanup(Z_CLOCK_PIN)
- GPIO.cleanup(Z_DIRECTION_PIN)
- print("done")
-
-def test(arg):
- print(arg)
-
-
-class TestEncoder:
- """ represent a test encoder controlled by keyboard input """
- def __init__(self, view, event_cbk, inc_key = 'a', dec_key = 'b'):
-
- view.window.bind(inc_key, self._rotate_cw)
- view.window.bind(dec_key, self._rotate_ccw)
-
- self._event_cbk = event_cbk
-
- def _rotate_cw(self, event=None):
- self._event_cbk(encoder.Direction.CW)
-
- def _rotate_ccw(self, event=None):
- self._event_cbk(encoder.Direction.CCW)
+ 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:
- print(o)
- print(a)
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)
- GPIO.setwarnings(False) ## Turn off warnings
- GPIO.setmode(GPIO.BOARD) ## Use BOARD pin numbering
- GPIO.setup(X_DIRECTION_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
- GPIO.setup(Z_DIRECTION_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
- GPIO.setup(X_CLOCK_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
- GPIO.setup(Z_CLOCK_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
-
if _test:
- x_test_encoder = TestEncoder(view, view._controller.handle_x_position_update, 'a', 'z')
- z_test_encoder = TestEncoder(view, view._controller.handle_z_position_update, 's', 'x')
-
- x_encoder = encoder.Encoder(X_CLOCK_PIN, X_DIRECTION_PIN, view._controller.handle_x_position_update)
- z_encoder = encoder.Encoder(Z_CLOCK_PIN, Z_DIRECTION_PIN, view._controller.handle_z_position_update)
-
- GPIO.add_event_detect(X_CLOCK_PIN, GPIO.RISING)
- GPIO.add_event_detect(Z_CLOCK_PIN, GPIO.RISING)
-
- GPIO.add_event_callback(X_CLOCK_PIN, x_encoder.update)
- GPIO.add_event_callback(Z_CLOCK_PIN, z_encoder.update)
+ 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()
´´´
---
here is the diff between my version of test_model.py and main:
´´´diff
diff --git a/test_model.py b/test_model.py
index dae5311..91e21b5 100644
--- a/test_model.py
+++ b/test_model.py
@@ -1,95 +1,93 @@
import unittest
import dro
-import encoder
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)
- def test_notify_observers(self):
- o = Observer()
- self.model.attatch(o)
-
- self.assertFalse(o.has_been_called)
- self.model.update_position(encoder.Direction.CW, 'x')
- self.assertTrue(o.has_been_called)
- self.assertTrue(int(self.model.get_position('x')) == 1)
- self.assertTrue(o.updated == dro.Updated.POS_X)
-
- o.reset()
-
- self.model.update_position(encoder.Direction.CW, 'z')
- self.assertTrue(int(self.model.get_position('z')) == 1)
- self.assertTrue(o.has_been_called)
- self.assertTrue(int(self.model.get_position('z')) == 1)
- self.assertTrue(o.updated == dro.Updated.POS_Z)
-
def test_wrong_axis(self):
o = Observer()
self.model.attatch(o)
- self.model.set_position(1, 'apa')
+ 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(1, 'x')
+ self.model.set_position('x', 1)
self.assertTrue(o.updated == dro.Updated.POS_X)
- self.model.set_position(-11, 'z')
+ 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_position('x')), 1)
self.assertEqual(int(self.model.get_position('z')), -11)
def test_get_pos_as_string(self):
- self.model.set_position(-11.49, 'x')
+ self.model.set_position('x', -11.49)
self.assertEqual(self.model.get_position('x'), -11.49)
def test_toogle_x_mode(self):
o = Observer()
self.model.attatch(o)
- self.model.set_position(-11.49, 'x')
+ self.model.set_position('x', -11.49)
self.model.set_toggle_x_mode()
- self.assertEqual(self.model.get_position('x'), -22.98)
+ self.assertEqual(self.model.get_position('x'), -11.49)
self.assertTrue(o.updated == dro.Updated.X_MODE)
- def test_set_diam(self):
- if self.model.get_x_mode() == dro.XMode.RADIUS:
- self.model.set_toggle_x_mode()
- self.assertEqual(self.model.get_x_mode(), dro.XMode.DIAMETER)
-
- self.model.set_position(10, 'x')
- self.assertEqual(self.model.get_position('x'), 10)
- self.model.set_toggle_x_mode()
- self.assertEqual(self.model.get_position('x'), 5)
+ 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()
´´´
---
here is the diff between my version of todo.txt and main:
´´´diff
diff --git a/todo.txt b/todo.txt
index cf193f0..fd78b5e 100644
--- a/todo.txt
+++ b/todo.txt
@@ -1,46 +1,50 @@
-askfloat funkar inte med alltid??????
-tester för r/D och ser/read position
+askfloat funkar inte med alltid??????
+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
stänga av med fysisk knapp:
"adding this to /boot/config.txt:
dtoverlay=gpio-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)
utöka test.py
*funkar det med olika ISR? eller måste det vara en central?
VERKAR FUNKA
lägg till z också...
GJODE ETT TEST MED z_clock_pin på gpio13 FUNKAR...
HW funkar inte
testa enligt c:\Users\johan\Documents\verkstad\emco compact 5\bob\my-bob.drawio
// 1. 4.7kOhm motstånd pull up, inte pull up i HW...
// verkar bli för liten spänning
eller
2. inget motstånd, pull up i HW... FUNKAR...
det blir 3.22V HIGH på GPIO
´´´