Copied
This commit is contained in:
parent
64318902b4
commit
2c6c21b761
98
README.md
98
README.md
@ -1,93 +1,5 @@
|
||||
# physical-turing-sim
|
||||
|
||||
|
||||
|
||||
## 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/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
|
||||
|
||||
```
|
||||
cd existing_repo
|
||||
git remote add origin https://git.fslab.de/mwagne2s/physical-turing-sim.git
|
||||
git branch -M main
|
||||
git push -uf origin main
|
||||
```
|
||||
|
||||
## Integrate with your tools
|
||||
|
||||
- [ ] [Set up project integrations](https://git.fslab.de/mwagne2s/physical-turing-sim/-/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/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
|
||||
|
||||
## 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/index.html)
|
||||
- [ ] [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.
|
||||
## How to
|
||||
For every microcontroller you wish to run:
|
||||
1. Flash ESP32 with Micropython according to their docs
|
||||
2. Make sure the USB device in makefile is correct
|
||||
3. Run makefile
|
||||
|
15
html/docs.html
Normal file
15
html/docs.html
Normal file
@ -0,0 +1,15 @@
|
||||
<body>
|
||||
|
||||
<form action="/">
|
||||
<button type="submit">Go back</button>
|
||||
</form> <br>
|
||||
|
||||
<h1>Documentation</h1>
|
||||
<h2>Turing Machine</h2>
|
||||
<p> Currently deterministic, single-band TMs are supported.
|
||||
They can be passed with ..... </p> <br>
|
||||
|
||||
<h2>Input Band</h2>
|
||||
<p> Input bands can be ..... </p>
|
||||
|
||||
</body>
|
5
html/head.html
Normal file
5
html/head.html
Normal file
@ -0,0 +1,5 @@
|
||||
<head>
|
||||
<title>{page_title}</title>
|
||||
</head>
|
||||
|
||||
|
25
html/home.html
Normal file
25
html/home.html
Normal file
@ -0,0 +1,25 @@
|
||||
<body>
|
||||
|
||||
<h1>TM-SIM Setup</h1>
|
||||
<h2>Pass TM definition to selected slave </h2>
|
||||
<form action="/definition">
|
||||
{select_slave}
|
||||
<label for="tm">Definition</label>
|
||||
<input type="text" name="tm" value="">
|
||||
<button type="submit">Send</button>
|
||||
</form> <br>
|
||||
|
||||
<h2>Pass input band to selected slave</h2>
|
||||
<p>The selected slave will be assumed first in line</p>
|
||||
<form action="/input">
|
||||
{select_slave}
|
||||
<label for="band">Definition</label>
|
||||
<input type="text" name="band" value="">
|
||||
<button type="submit">Send</button>
|
||||
</form> <br>
|
||||
|
||||
<form action="/run">
|
||||
<button type="submit">Go to Controller</button>
|
||||
</form>
|
||||
|
||||
</body>
|
12
html/result.html
Normal file
12
html/result.html
Normal file
@ -0,0 +1,12 @@
|
||||
<body>
|
||||
<h1>TM-Sim Results</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Slave Number</th>
|
||||
<th>Result</th>
|
||||
</tr>
|
||||
{results}
|
||||
</table> <br>
|
||||
<h2>Final Result</h2>
|
||||
{result_final}
|
||||
</body>
|
44
html/run.html
Normal file
44
html/run.html
Normal file
@ -0,0 +1,44 @@
|
||||
<body>
|
||||
|
||||
<h1>TM-Sim Controller</h1>
|
||||
<h2>Status</h2>
|
||||
{slave_selected} <br>
|
||||
{band_current} <br>
|
||||
{rule_used} <br>
|
||||
|
||||
<br>
|
||||
<h2>Command Selected Slave</h2>
|
||||
<form action="/run_step">
|
||||
<button type="submit">Step</button>
|
||||
</form>
|
||||
|
||||
<form action="/run_all">
|
||||
<button type="submit">Run</button>
|
||||
</form>
|
||||
|
||||
<form action="/reset">
|
||||
<button type="submit">Reset</button>
|
||||
</form>
|
||||
|
||||
<br>
|
||||
<h2>Select Slave</h2>
|
||||
<form action="/run">
|
||||
{select_slave}
|
||||
<button type="submit">Select</button>
|
||||
</form> <br>
|
||||
|
||||
<br>
|
||||
<h2>Pass Band</h2>
|
||||
<form action="/pass">
|
||||
<label for="select_slave">Slave from</label> <br>
|
||||
{select_slave}
|
||||
<label for="to_slave">Slave to</label> <br>
|
||||
{to_slave}
|
||||
<button type="submit">Pass</button>
|
||||
</form> <br>
|
||||
|
||||
<form action="/">
|
||||
<button type="submit">Go to Setup</button>
|
||||
</form>
|
||||
|
||||
</body>
|
67
lib/html_builder.py
Normal file
67
lib/html_builder.py
Normal file
@ -0,0 +1,67 @@
|
||||
|
||||
class HTMLBuilder():
|
||||
"""
|
||||
Assembles HTML response through substitution according to the passed dictionary.
|
||||
The keys need to be in curly braces inside one of the passed files.
|
||||
|
||||
The list of files gets glued together in the order they are in, so a head.html
|
||||
would need to be the first element.
|
||||
|
||||
Can be used to glue files without substitution by not passing data dictionary.
|
||||
|
||||
Example:
|
||||
sub_dict = {
|
||||
"page_title": {
|
||||
"type": "item",
|
||||
"value": "My Booklist!"
|
||||
},
|
||||
"books": {
|
||||
"type": "list",
|
||||
"value": ["Book1", "Book2"]
|
||||
},
|
||||
"select": {
|
||||
"type": "radio",
|
||||
"value": ["Book1", "Book2"]
|
||||
}
|
||||
}
|
||||
response = HTMLBuilder(["html/head.html", "html/home.html"], sub_dict).build()
|
||||
|
||||
:param list files: the files that are glued together in order
|
||||
:param dict data: the dictionary with which placeholders get replaced
|
||||
"""
|
||||
def __init__(self, files: list, data=None) -> None:
|
||||
self.data = data
|
||||
self.file: str = ""
|
||||
for file in files:
|
||||
self.file += open(file, "r").read()
|
||||
|
||||
def build(self) -> bytes:
|
||||
if self.data == None:
|
||||
return self.__to_binary()
|
||||
|
||||
for key, value in self.data.items():
|
||||
key = '{' + key + '}'
|
||||
value_type = value["type"]
|
||||
value_repl = value["value"]
|
||||
|
||||
if value_type == "item":
|
||||
self.file = self.file.replace(key, value_repl)
|
||||
|
||||
if value_type == "list":
|
||||
repl_string = ""
|
||||
for litem in value_repl:
|
||||
repl_string += f'<li>{litem}</li>\n'
|
||||
self.file = self.file.replace(key, repl_string)
|
||||
|
||||
if value_type == "radio":
|
||||
repl_string = ""
|
||||
for litem in value_repl:
|
||||
repl_string += f'<input type="radio" id="1" name="{key[1:-1]}" value="{litem}">\n'
|
||||
repl_string += f'<label for="{litem}">{litem}</label><br>\n'
|
||||
self.file = self.file.replace(key, repl_string)
|
||||
|
||||
return self.__to_binary()
|
||||
|
||||
def __to_binary(self) -> bytes:
|
||||
return self.file.encode()
|
||||
|
8
lib/httpserver/__init__.py
Normal file
8
lib/httpserver/__init__.py
Normal file
@ -0,0 +1,8 @@
|
||||
# Minimal HTTP server
|
||||
#
|
||||
# Copyright 2021 (c) Erik de Lange
|
||||
# Released under MIT license
|
||||
|
||||
from .response import HTTPResponse
|
||||
from .sendfile import sendfile
|
||||
from .server import CONNECTION_CLOSE, CONNECTION_KEEP_ALIVE, HTTPServer
|
96
lib/httpserver/response.py
Normal file
96
lib/httpserver/response.py
Normal file
@ -0,0 +1,96 @@
|
||||
# Components of HTTP/1.1 responses
|
||||
#
|
||||
# Use when manually composing an HTTP response
|
||||
# Expand as required for your use
|
||||
#
|
||||
# For HTTP/1.1 specification see: https://www.ietf.org/rfc/rfc2616.txt
|
||||
# For MIME types see: https://www.iana.org/assignments/media-types/media-types.xhtml
|
||||
#
|
||||
# Copyright 2021 (c) Erik de Lange
|
||||
# Released under MIT license
|
||||
|
||||
from lib.httpserver.sendfile import sendfile
|
||||
|
||||
|
||||
reason = {
|
||||
200: "OK",
|
||||
301: "Moved Permanently",
|
||||
302: "Found",
|
||||
400: "Bad Request",
|
||||
404: "Not Found",
|
||||
418: "I'm a teapot"
|
||||
}
|
||||
|
||||
mimetype = {
|
||||
"HTML": b"Content-Type: text/html\r\n",
|
||||
"EVENT_STREAM": b"Content-Type: text/event-stream\r\n",
|
||||
"X_ICON": b"Content-Type: image/x-icon\r\n",
|
||||
"JSON": b"Content-Type: application/json\r\n"
|
||||
}
|
||||
|
||||
response_header = {
|
||||
"CLOSE": b"Connection: close\r\n",
|
||||
"KEEP_ALIVE": b"Connection: keep-alive\r\n"
|
||||
}
|
||||
|
||||
|
||||
class HTTPResponse:
|
||||
def __init__(self, con, status, mimetype=None, close=True, header=None):
|
||||
""" Create a response object
|
||||
|
||||
:param int status: HTTP status code
|
||||
:param socket con: socket
|
||||
:param str mimetype: HTTP mime type
|
||||
:param bool close: if true close connection else keep alive
|
||||
:param dict header: key,value pairs for HTTP response header fields
|
||||
"""
|
||||
self.status = status
|
||||
self.con = con
|
||||
self.mimetype = mimetype
|
||||
self.close = close
|
||||
|
||||
if header is None:
|
||||
self.header = {}
|
||||
else:
|
||||
self.header = header
|
||||
|
||||
def redirect(self, location):
|
||||
""" Redirect client """
|
||||
self.con.write(f"HTTP/1.1 {self.status} {reason.get(self.status, 'NA')}\nLocation: {location}\n")
|
||||
self.con.write(response_header.get("CLOSE"))
|
||||
|
||||
def send_file(self, filepath: str = ""):
|
||||
""" Send response to stream writer """
|
||||
self.con.write(f"HTTP/1.1 {self.status} {reason.get(self.status, 'NA')}\n")
|
||||
if self.mimetype:
|
||||
self.con.write(mimetype.get(self.mimetype))
|
||||
|
||||
if self.close:
|
||||
self.con.write(response_header.get("CLOSE"))
|
||||
else:
|
||||
self.con.write(response_header.get("KEEP_ALIVE"))
|
||||
|
||||
if len(self.header) > 0:
|
||||
for key, value in self.header.items():
|
||||
self.con.write(f"{key}: {value}\n")
|
||||
|
||||
sendfile(self.con, filepath)
|
||||
|
||||
def send_raw(self, raw: bytes):
|
||||
""" Send response to stream writer """
|
||||
self.con.write(f"HTTP/1.1 {self.status} {reason.get(self.status, 'NA')}\n")
|
||||
if self.mimetype:
|
||||
self.con.write(mimetype.get(self.mimetype))
|
||||
|
||||
if self.close:
|
||||
self.con.write(response_header.get("CLOSE"))
|
||||
else:
|
||||
self.con.write(response_header.get("KEEP_ALIVE"))
|
||||
|
||||
if len(self.header) > 0:
|
||||
for key, value in self.header.items():
|
||||
self.con.write(f"{key}: {value};\n")
|
||||
|
||||
self.con.write(b'\r\n\r\n')
|
||||
self.con.write(raw)
|
||||
|
25
lib/httpserver/sendfile.py
Normal file
25
lib/httpserver/sendfile.py
Normal file
@ -0,0 +1,25 @@
|
||||
# Memory efficient file transfer
|
||||
#
|
||||
# Copyright 2021 (c) Erik de Lange
|
||||
# Released under MIT license
|
||||
|
||||
_buffer = bytearray(512) # adjust size to your systems available memory
|
||||
_bmview = memoryview(_buffer) # reuse pre-allocated _buffer
|
||||
|
||||
|
||||
def sendfile(conn, filename):
|
||||
""" Send a file to a connection in chunks - lowering memory usage.
|
||||
|
||||
:param socket conn: connection to send the file content to
|
||||
:param str filename: name of file the send
|
||||
"""
|
||||
try:
|
||||
with open(filename, "rb") as fp:
|
||||
while True:
|
||||
n = fp.readinto(_buffer)
|
||||
if n == 0:
|
||||
break
|
||||
conn.write(_bmview[:n])
|
||||
except:
|
||||
print(f"WEB:File {filename} not found")
|
||||
|
145
lib/httpserver/server.py
Normal file
145
lib/httpserver/server.py
Normal file
@ -0,0 +1,145 @@
|
||||
# Minimal HTTP server
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# from httpserver import HTTPServer, sendfile, CONNECTION_CLOSE
|
||||
#
|
||||
# app = HTTPServer()
|
||||
|
||||
# @app.route("GET", "/")
|
||||
# def root(conn, request):
|
||||
# response = HTTPResponse(200, "text/html", close=True)
|
||||
# response.send(conn)
|
||||
# sendfile(conn, "index.html")
|
||||
#
|
||||
# app.start()
|
||||
#
|
||||
# Handlers for the (method, path) combinations must be decorated with @route,
|
||||
# and declared before the server is started (via a call to start).
|
||||
# Every handler receives the connection socket and an object with all the
|
||||
# details from the request (see url.py for exact content). The handler must
|
||||
# construct and send a correct HTTP response. To avoid typos use the
|
||||
# HTTPResponse component from response.py.
|
||||
# When leaving the handler the connection will be closed, unless the return
|
||||
# code of the handler is CONNECTION_KEEP_ALIVE.
|
||||
# Any (method, path) combination which has not been declared using @route
|
||||
# will, when received by the server, result in a 404 HTTP error.
|
||||
# The server cannot be stopped unless an alert is raised. A KeyboardInterrupt
|
||||
# will cause a controlled exit.
|
||||
#
|
||||
# Copyright 2021 (c) Erik de Lange
|
||||
# Released under MIT license
|
||||
|
||||
import errno
|
||||
import socket
|
||||
from micropython import const
|
||||
|
||||
from .response import HTTPResponse
|
||||
from .url import HTTPRequest, InvalidRequest
|
||||
|
||||
CONNECTION_CLOSE = const(0)
|
||||
CONNECTION_KEEP_ALIVE = const(1)
|
||||
|
||||
|
||||
class HTTPServerError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class HTTPServer:
|
||||
|
||||
def __init__(self, host="0.0.0.0", port=80, backlog=5, timeout=30):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.backlog = backlog
|
||||
self.timeout = timeout
|
||||
self._routes = dict() # stores link between (method, path) and function to execute
|
||||
|
||||
def route(self, method="GET", path="/"):
|
||||
""" Decorator which connects method and path to the decorated function. """
|
||||
|
||||
if (method, path) in self._routes:
|
||||
raise HTTPServerError(f"route{(method, path)} already registered")
|
||||
|
||||
def wrapper(function):
|
||||
self._routes[(method, path)] = function
|
||||
|
||||
return wrapper
|
||||
|
||||
def start(self):
|
||||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
|
||||
server.bind((self.host, self.port))
|
||||
server.listen(self.backlog)
|
||||
|
||||
print(f"HTTP server started on {self.host}:{self.port}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
conn, addr = server.accept()
|
||||
conn.settimeout(self.timeout)
|
||||
|
||||
request_line = conn.readline()
|
||||
if request_line is None:
|
||||
raise OSError(errno.ETIMEDOUT)
|
||||
|
||||
if request_line in [b"", b"\r\n"]:
|
||||
print(f"empty request line from {addr[0]}")
|
||||
conn.close()
|
||||
continue
|
||||
|
||||
print(f"request line {request_line} from {addr[0]}")
|
||||
|
||||
try:
|
||||
request = HTTPRequest(request_line)
|
||||
except InvalidRequest as e:
|
||||
while True:
|
||||
# read and discard header fields
|
||||
line = conn.readline()
|
||||
if line is None:
|
||||
raise OSError(errno.ETIMEDOUT)
|
||||
if line in [b"", b"\r\n"]:
|
||||
break
|
||||
HTTPResponse(conn, 400, "HTML", close=True).send_file()
|
||||
conn.write(repr(e).encode("utf-8"))
|
||||
conn.close()
|
||||
continue
|
||||
|
||||
while True:
|
||||
# read header fields and add name / value to dict 'header'
|
||||
line = conn.readline()
|
||||
if line is None:
|
||||
raise OSError(errno.ETIMEDOUT)
|
||||
|
||||
if line in [b"", b"\r\n"]:
|
||||
break
|
||||
else:
|
||||
if line.find(b":") != 1:
|
||||
name, value = line.split(b':', 1)
|
||||
request.header[name] = value.strip()
|
||||
|
||||
# search function which is connected to (method, path)
|
||||
func = self._routes.get((request.method, request.path))
|
||||
if func:
|
||||
if func(conn, request) != CONNECTION_KEEP_ALIVE:
|
||||
# close connection unless explicitly kept alive
|
||||
conn.close()
|
||||
else: # no function found for (method, path) combination
|
||||
response = HTTPResponse(conn, 404).send_file()
|
||||
conn.close()
|
||||
|
||||
except KeyboardInterrupt: # will stop the server
|
||||
conn.close()
|
||||
break
|
||||
except Exception as e:
|
||||
conn.close()
|
||||
if type(e) is OSError and e.errno == errno.ETIMEDOUT: # communication timeout
|
||||
pass
|
||||
elif type(e) is OSError and e.errno == errno.ECONNRESET: # client reset the connection
|
||||
pass
|
||||
else:
|
||||
server.close()
|
||||
raise e
|
||||
|
||||
server.close()
|
||||
print("HTTP server stopped")
|
54
lib/httpserver/sse.py
Normal file
54
lib/httpserver/sse.py
Normal file
@ -0,0 +1,54 @@
|
||||
# Server-sent event support
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# import _thread, time
|
||||
#
|
||||
# from httpserver.sse import EventSource
|
||||
#
|
||||
# @app.route("GET", "/api/greeting")
|
||||
# def api_greeting(conn, request):
|
||||
# """ Say hello every 5 seconds """
|
||||
#
|
||||
# def greeting_task(conn, eventsource):
|
||||
# while True:
|
||||
# time.sleep(5)
|
||||
# try:
|
||||
# eventsource.send(event="greeting", data="hello")
|
||||
# except Exception: # catch (a.o.) ECONNRESET when the client has disappeared
|
||||
# conn.close()
|
||||
# break # exit thread
|
||||
#
|
||||
# _thread.start_new_thread(greeting_task, (conn, EventSource(conn)))
|
||||
#
|
||||
# Copyright 2022 (c) Erik de Lange
|
||||
# Released under MIT license
|
||||
|
||||
from .response import HTTPResponse
|
||||
|
||||
|
||||
class EventSource:
|
||||
""" Open and use an event stream connection to the client """
|
||||
|
||||
def __init__(self, conn):
|
||||
""" Set up an event stream connection """
|
||||
self.conn = conn
|
||||
|
||||
HTTPResponse(conn, 200, "EVENT_STREAM", close=False, header={"Cache-Control": "no-cache"}).send_file()
|
||||
|
||||
def send(self, data=":", id=None, event=None, retry=None):
|
||||
""" Send event to client following the event stream format
|
||||
|
||||
:param str data: event data to send to client. mandatory
|
||||
:param int id: optional event id
|
||||
:param str event: optional event type, used for dispatching at client
|
||||
:param int retry: retry interval in milliseconds
|
||||
"""
|
||||
writer = self.conn
|
||||
if id is not None:
|
||||
writer.write(f"id: {id}\n")
|
||||
if event is not None:
|
||||
writer.write(f"event: {event}\n")
|
||||
if retry is not None:
|
||||
writer.write(f"retry: {retry}\n")
|
||||
writer.write(f"data: {data}\n\n")
|
120
lib/httpserver/url.py
Normal file
120
lib/httpserver/url.py
Normal file
@ -0,0 +1,120 @@
|
||||
# Routines for decoding an HTTP request line.
|
||||
#
|
||||
# HTTP request line as understood by this package:
|
||||
#
|
||||
# Request line: Method SP Request-URL SP HTTP-Version CRLF
|
||||
# Request URL: Path ? Query
|
||||
# Query: key=value&key=value
|
||||
#
|
||||
# Example: b"GET /page?key1=0.07&key2=0.03&key3=0.13 HTTP/1.1\r\n"
|
||||
#
|
||||
# Method: GET
|
||||
# Request URL: /page?key1=0.07&key2=0.03&key3=0.13
|
||||
# HTTP version: HTTP/1.1
|
||||
# Path: /page
|
||||
# Query: key1=0.07&key2=0.03&key3=0.13
|
||||
#
|
||||
# See also: https://www.tutorialspoint.com/http/http_requests.htm
|
||||
# https://en.wikipedia.org/wiki/Uniform_Resource_Identifier
|
||||
#
|
||||
# For MicroPython applications which process HTTP requests.
|
||||
#
|
||||
# Copyright 2021,2022 (c) Erik de Lange
|
||||
# Released under MIT license
|
||||
|
||||
|
||||
class InvalidRequest(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class HTTPRequest:
|
||||
|
||||
def __init__(self, request_line) -> None:
|
||||
""" Separate an HTTP request line in its elements.
|
||||
|
||||
:param bytes request_line: the complete HTTP request line
|
||||
:return Request: instance containing
|
||||
method the request method ("GET", "PUT", ...)
|
||||
url the request URL, including the query string (if any)
|
||||
path the request path from the URL
|
||||
query the query string from the URL (if any, else "")
|
||||
version the HTTP version
|
||||
parameters dictionary with key-value pairs from the query string
|
||||
header empty dict, placeholder for key-value pairs from request header fields
|
||||
:raises InvalidRequest: if line does not contain exactly 3 components separated by spaces
|
||||
if method is not in IETF standardized set
|
||||
aside from these no other checks done here
|
||||
"""
|
||||
try:
|
||||
self.method, self.url, self.version = request_line.decode("utf-8").split()
|
||||
# note that method, url and version are str, not bytes
|
||||
except ValueError:
|
||||
raise InvalidRequest(f"Expected 3 elements in {request_line}")
|
||||
|
||||
if self.version.find("/") != -1:
|
||||
self.version = self.version.split("/", 1)[1]
|
||||
|
||||
if self.method not in ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE"]:
|
||||
raise InvalidRequest(f"Invalid method {self.method} in {request_line}")
|
||||
|
||||
if self.url.find("?") != -1:
|
||||
self.path, self.query = self.url.split("?", 1)
|
||||
self.parameters = query(self.query)
|
||||
else:
|
||||
self.path = self.url
|
||||
self.query = ""
|
||||
self.parameters = dict()
|
||||
|
||||
self.header = dict()
|
||||
|
||||
|
||||
def query(query):
|
||||
""" Place all key-value pairs from a request URLs query string into a dict.
|
||||
|
||||
Example: request b"GET /page?key1=0.07&key2=0.03&key3=0.13 HTTP/1.1\r\n"
|
||||
yields dictionary {'key1': '0.07', 'key2': '0.03', 'key3': '0.13'}.
|
||||
|
||||
:param str query: the query part (everything after the '?') from an HTTP request line
|
||||
:return dict: dictionary with zero or more entries
|
||||
"""
|
||||
d = dict()
|
||||
if len(query) > 0:
|
||||
for pair in query.split("&"):
|
||||
try:
|
||||
key, value = pair.split("=", 1)
|
||||
if key not in d: # only first occurrence taken into account
|
||||
d[key] = value
|
||||
except ValueError: # skip malformed parameter (like missing '=')
|
||||
pass
|
||||
|
||||
return d
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# request_lines = [b"GET / HTTP/1.1\r\n",
|
||||
# b"GET /page/sub HTTP/1.1\r\n",
|
||||
# b"GET /page?key1=0.07&key2=0.03 HTTP/1.1\r\n",
|
||||
# b"GET /page?key1=0.07&key1=0.03 HTTP/1.1\r\n",
|
||||
# b"GET /page?key1=0.07& HTTP/1.1\r\n",
|
||||
# b"GET /page?key1=0.07 HTTP/1.1\r\n",
|
||||
# b"GET /page?key1= HTTP/1.1\r\n",
|
||||
# b"GET /page?key1 HTTP/1.1\r\n",
|
||||
# b"GET /page? HTTP/1.1\r\n",
|
||||
# b"GET HTTP/1.1\r\n",
|
||||
# b"GET / HTTP/1.1 one_too_much\r\n",
|
||||
# b"UNKNOWN / HTTP/1.1\r\n"]
|
||||
#
|
||||
# for line in request_lines:
|
||||
# print("request line", line)
|
||||
# try:
|
||||
# request = HTTPRequest(line)
|
||||
# print("method:", request.method)
|
||||
# print("url:", request.url)
|
||||
# print("version:", request.version)
|
||||
# print("path:", request.path)
|
||||
# print("query:", request.query)
|
||||
# print("parameters:", request.parameters)
|
||||
# except Exception as e:
|
||||
# print("exception", repr(e))
|
||||
# finally:
|
||||
# print()
|
305
lib/sh1106.py
Normal file
305
lib/sh1106.py
Normal file
@ -0,0 +1,305 @@
|
||||
#
|
||||
# MicroPython SH1106 OLED driver, I2C and SPI interfaces
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2016 Radomir Dopieralski (@deshipu),
|
||||
# 2017-2021 Robert Hammelrath (@robert-hh)
|
||||
# 2021 Tim Weber (@scy)
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Sample code sections for ESP8266 pin assignments
|
||||
# ------------ SPI ------------------
|
||||
# Pin Map SPI
|
||||
# - 3v - xxxxxx - Vcc
|
||||
# - G - xxxxxx - Gnd
|
||||
# - D7 - GPIO 13 - Din / MOSI fixed
|
||||
# - D5 - GPIO 14 - Clk / Sck fixed
|
||||
# - D8 - GPIO 4 - CS (optional, if the only connected device)
|
||||
# - D2 - GPIO 5 - D/C
|
||||
# - D1 - GPIO 2 - Res
|
||||
#
|
||||
# for CS, D/C and Res other ports may be chosen.
|
||||
#
|
||||
# from machine import Pin, SPI
|
||||
# import sh1106
|
||||
|
||||
# spi = SPI(1, baudrate=1000000)
|
||||
# display = sh1106.SH1106_SPI(128, 64, spi, Pin(5), Pin(2), Pin(4))
|
||||
# display.sleep(False)
|
||||
# display.fill(0)
|
||||
# display.text('Testing 1', 0, 0, 1)
|
||||
# display.show()
|
||||
#
|
||||
# --------------- I2C ------------------
|
||||
#
|
||||
# Pin Map I2C
|
||||
# - 3v - xxxxxx - Vcc
|
||||
# - G - xxxxxx - Gnd
|
||||
# - D2 - GPIO 5 - SCK / SCL
|
||||
# - D1 - GPIO 4 - DIN / SDA
|
||||
# - D0 - GPIO 16 - Res
|
||||
# - G - xxxxxx CS
|
||||
# - G - xxxxxx D/C
|
||||
#
|
||||
# Pin's for I2C can be set almost arbitrary
|
||||
#
|
||||
# from machine import Pin, I2C
|
||||
# import sh1106
|
||||
#
|
||||
# i2c = I2C(scl=Pin(5), sda=Pin(4), freq=400000)
|
||||
# display = sh1106.SH1106_I2C(128, 64, i2c, Pin(16), 0x3c)
|
||||
# display.sleep(False)
|
||||
# display.fill(0)
|
||||
# display.text('Testing 1', 0, 0, 1)
|
||||
# display.show()
|
||||
|
||||
from micropython import const
|
||||
import utime as time
|
||||
import framebuf
|
||||
|
||||
|
||||
# a few register definitions
|
||||
_SET_CONTRAST = const(0x81)
|
||||
_SET_NORM_INV = const(0xa6)
|
||||
_SET_DISP = const(0xae)
|
||||
_SET_SCAN_DIR = const(0xc0)
|
||||
_SET_SEG_REMAP = const(0xa0)
|
||||
_LOW_COLUMN_ADDRESS = const(0x00)
|
||||
_HIGH_COLUMN_ADDRESS = const(0x10)
|
||||
_SET_PAGE_ADDRESS = const(0xB0)
|
||||
|
||||
|
||||
class SH1106(framebuf.FrameBuffer):
|
||||
|
||||
def __init__(self, width, height, external_vcc, rotate=0):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.external_vcc = external_vcc
|
||||
self.flip_en = rotate == 180 or rotate == 270
|
||||
self.rotate90 = rotate == 90 or rotate == 270
|
||||
self.pages = self.height // 8
|
||||
self.bufsize = self.pages * self.width
|
||||
self.renderbuf = bytearray(self.bufsize)
|
||||
self.pages_to_update = 0
|
||||
|
||||
if self.rotate90:
|
||||
self.displaybuf = bytearray(self.bufsize)
|
||||
# HMSB is required to keep the bit order in the render buffer
|
||||
# compatible with byte-for-byte remapping to the display buffer,
|
||||
# which is in VLSB. Else we'd have to copy bit-by-bit!
|
||||
super().__init__(self.renderbuf, self.height, self.width,
|
||||
framebuf.MONO_HMSB)
|
||||
else:
|
||||
self.displaybuf = self.renderbuf
|
||||
super().__init__(self.renderbuf, self.width, self.height,
|
||||
framebuf.MONO_VLSB)
|
||||
|
||||
# flip() was called rotate() once, provide backwards compatibility.
|
||||
self.rotate = self.flip
|
||||
self.init_display()
|
||||
|
||||
def init_display(self):
|
||||
self.reset()
|
||||
self.fill(0)
|
||||
self.show()
|
||||
self.poweron()
|
||||
# rotate90 requires a call to flip() for setting up.
|
||||
self.flip(self.flip_en)
|
||||
|
||||
def poweroff(self):
|
||||
self.write_cmd(_SET_DISP | 0x00)
|
||||
|
||||
def poweron(self):
|
||||
self.write_cmd(_SET_DISP | 0x01)
|
||||
if self.delay:
|
||||
time.sleep_ms(self.delay)
|
||||
|
||||
def flip(self, flag=None, update=True):
|
||||
if flag is None:
|
||||
flag = not self.flip_en
|
||||
mir_v = flag ^ self.rotate90
|
||||
mir_h = flag
|
||||
self.write_cmd(_SET_SEG_REMAP | (0x01 if mir_v else 0x00))
|
||||
self.write_cmd(_SET_SCAN_DIR | (0x08 if mir_h else 0x00))
|
||||
self.flip_en = flag
|
||||
if update:
|
||||
self.show(True) # full update
|
||||
|
||||
def sleep(self, value):
|
||||
self.write_cmd(_SET_DISP | (not value))
|
||||
|
||||
def contrast(self, contrast):
|
||||
self.write_cmd(_SET_CONTRAST)
|
||||
self.write_cmd(contrast)
|
||||
|
||||
def invert(self, invert):
|
||||
self.write_cmd(_SET_NORM_INV | (invert & 1))
|
||||
|
||||
def show(self, full_update = False):
|
||||
# self.* lookups in loops take significant time (~4fps).
|
||||
(w, p, db, rb) = (self.width, self.pages,
|
||||
self.displaybuf, self.renderbuf)
|
||||
if self.rotate90:
|
||||
for i in range(self.bufsize):
|
||||
db[w * (i % p) + (i // p)] = rb[i]
|
||||
if full_update:
|
||||
pages_to_update = (1 << self.pages) - 1
|
||||
else:
|
||||
pages_to_update = self.pages_to_update
|
||||
#print("Updating pages: {:08b}".format(pages_to_update))
|
||||
for page in range(self.pages):
|
||||
if (pages_to_update & (1 << page)):
|
||||
self.write_cmd(_SET_PAGE_ADDRESS | page)
|
||||
self.write_cmd(_LOW_COLUMN_ADDRESS | 2)
|
||||
self.write_cmd(_HIGH_COLUMN_ADDRESS | 0)
|
||||
self.write_data(db[(w*page):(w*page+w)])
|
||||
self.pages_to_update = 0
|
||||
|
||||
def pixel(self, x, y, color=None):
|
||||
if color is None:
|
||||
return super().pixel(x, y)
|
||||
else:
|
||||
super().pixel(x, y , color)
|
||||
page = y // 8
|
||||
self.pages_to_update |= 1 << page
|
||||
|
||||
def text(self, text, x, y, color=1):
|
||||
super().text(text, x, y, color)
|
||||
self.register_updates(y, y+7)
|
||||
|
||||
def line(self, x0, y0, x1, y1, color):
|
||||
super().line(x0, y0, x1, y1, color)
|
||||
self.register_updates(y0, y1)
|
||||
|
||||
def hline(self, x, y, w, color):
|
||||
super().hline(x, y, w, color)
|
||||
self.register_updates(y)
|
||||
|
||||
def vline(self, x, y, h, color):
|
||||
super().vline(x, y, h, color)
|
||||
self.register_updates(y, y+h-1)
|
||||
|
||||
def fill(self, color):
|
||||
super().fill(color)
|
||||
self.pages_to_update = (1 << self.pages) - 1
|
||||
|
||||
def blit(self, fbuf, x, y, key=-1, palette=None):
|
||||
super().blit(fbuf, x, y, key, palette)
|
||||
self.register_updates(y, y+self.height)
|
||||
|
||||
def scroll(self, x, y):
|
||||
# my understanding is that scroll() does a full screen change
|
||||
super().scroll(x, y)
|
||||
self.pages_to_update = (1 << self.pages) - 1
|
||||
|
||||
def fill_rect(self, x, y, w, h, color):
|
||||
super().fill_rect(x, y, w, h, color)
|
||||
self.register_updates(y, y+h-1)
|
||||
|
||||
def rect(self, x, y, w, h, color):
|
||||
super().rect(x, y, w, h, color)
|
||||
self.register_updates(y, y+h-1)
|
||||
|
||||
def register_updates(self, y0, y1=None):
|
||||
# this function takes the top and optional bottom address of the changes made
|
||||
# and updates the pages_to_change list with any changed pages
|
||||
# that are not yet on the list
|
||||
start_page = max(0, y0 // 8)
|
||||
end_page = max(0, y1 // 8) if y1 is not None else start_page
|
||||
# rearrange start_page and end_page if coordinates were given from bottom to top
|
||||
if start_page > end_page:
|
||||
start_page, end_page = end_page, start_page
|
||||
for page in range(start_page, end_page+1):
|
||||
self.pages_to_update |= 1 << page
|
||||
|
||||
def reset(self, res):
|
||||
if res is not None:
|
||||
res(1)
|
||||
time.sleep_ms(1)
|
||||
res(0)
|
||||
time.sleep_ms(20)
|
||||
res(1)
|
||||
time.sleep_ms(20)
|
||||
|
||||
|
||||
class SH1106_I2C(SH1106):
|
||||
def __init__(self, width, height, i2c, res=None, addr=0x3c,
|
||||
rotate=0, external_vcc=False, delay=0):
|
||||
self.i2c = i2c
|
||||
self.addr = addr
|
||||
self.res = res
|
||||
self.temp = bytearray(2)
|
||||
self.delay = delay
|
||||
if res is not None:
|
||||
res.init(res.OUT, value=1)
|
||||
super().__init__(width, height, external_vcc, rotate)
|
||||
|
||||
def write_cmd(self, cmd):
|
||||
self.temp[0] = 0x80 # Co=1, D/C#=0
|
||||
self.temp[1] = cmd
|
||||
self.i2c.writeto(self.addr, self.temp)
|
||||
|
||||
def write_data(self, buf):
|
||||
self.i2c.writeto(self.addr, b'\x40'+buf)
|
||||
|
||||
def reset(self):
|
||||
super().reset(self.res)
|
||||
|
||||
|
||||
class SH1106_SPI(SH1106):
|
||||
def __init__(self, width, height, spi, dc, res=None, cs=None,
|
||||
rotate=0, external_vcc=False, delay=0):
|
||||
dc.init(dc.OUT, value=0)
|
||||
if res is not None:
|
||||
res.init(res.OUT, value=0)
|
||||
if cs is not None:
|
||||
cs.init(cs.OUT, value=1)
|
||||
self.spi = spi
|
||||
self.dc = dc
|
||||
self.res = res
|
||||
self.cs = cs
|
||||
self.delay = delay
|
||||
super().__init__(width, height, external_vcc, rotate)
|
||||
|
||||
def write_cmd(self, cmd):
|
||||
if self.cs is not None:
|
||||
self.cs(1)
|
||||
self.dc(0)
|
||||
self.cs(0)
|
||||
self.spi.write(bytearray([cmd]))
|
||||
self.cs(1)
|
||||
else:
|
||||
self.dc(0)
|
||||
self.spi.write(bytearray([cmd]))
|
||||
|
||||
def write_data(self, buf):
|
||||
if self.cs is not None:
|
||||
self.cs(1)
|
||||
self.dc(1)
|
||||
self.cs(0)
|
||||
self.spi.write(buf)
|
||||
self.cs(1)
|
||||
else:
|
||||
self.dc(1)
|
||||
self.spi.write(buf)
|
||||
|
||||
def reset(self):
|
||||
super().reset(self.res)
|
188
lib/tm_sim.py
Normal file
188
lib/tm_sim.py
Normal file
@ -0,0 +1,188 @@
|
||||
class TuringMachine():
|
||||
def __init__(self, turing_machine_def: str) -> None:
|
||||
# if a placeholder is needed, create it with empty string
|
||||
if not turing_machine_def:
|
||||
return
|
||||
|
||||
# STATE VARIABLES
|
||||
self.state_start: str = ''
|
||||
self.state_accept: str = ''
|
||||
self.state_reject: str = ''
|
||||
self.state_current: str = ''
|
||||
|
||||
# BAND VARIABLES
|
||||
self.band: list = []
|
||||
self.band_num: int = 0
|
||||
self.band_current: int = 0
|
||||
|
||||
# RULE VARIABLES
|
||||
self.rules: list = []
|
||||
self.rule_used: str = ''
|
||||
|
||||
# ERR Variables
|
||||
self.errored: bool = False
|
||||
|
||||
# PARSE BANDS
|
||||
def_split = turing_machine_def.replace(' ', '').replace('\n', '').lower().split(';')
|
||||
self.band_num = int(def_split[0])
|
||||
|
||||
# PARSE STATES
|
||||
self.state_start = def_split[1]
|
||||
self.state_current = def_split[1]
|
||||
self.state_accept = def_split[2]
|
||||
self.state_reject = def_split[3]
|
||||
|
||||
def_split_truncated = def_split[4:]
|
||||
|
||||
# PARSE RULES
|
||||
for x in def_split_truncated:
|
||||
if not x: continue
|
||||
|
||||
rule_current = x.split(',')
|
||||
tmp = rule_current[1].split('->')
|
||||
rule_current[1] = tmp[0]
|
||||
rule_current.insert(2, tmp[1])
|
||||
|
||||
self.rules.append(rule_current)
|
||||
|
||||
|
||||
def turing_run(self) -> None:
|
||||
'''Runs the TM without giving the option to step through manually.'''
|
||||
while not self.turing_finished():
|
||||
self.turing_step()
|
||||
if self.errored: return
|
||||
|
||||
|
||||
def turing_step(self) -> bool:
|
||||
'''Steps through the TM one rule at a time. A step is changing
|
||||
the value of the current element and moving the band by one.
|
||||
Returns whether the TM has finished running or has an empty band.'''
|
||||
if len(self.band) == 0:
|
||||
return True
|
||||
|
||||
# Add blank if current location is out of bounds and adjust location
|
||||
if self.band_current < 0:
|
||||
self.band.insert(0, '_')
|
||||
self.band_current = 0
|
||||
|
||||
if self.band_current == len(self.band):
|
||||
self.band.append('_')
|
||||
|
||||
for rule in self.rules:
|
||||
# Skip if rule does not apply to current state
|
||||
if not rule[0] == self.state_current: continue
|
||||
# Skip if rule does not apply to current band element
|
||||
if not rule[1] == self.band[self.band_current]: continue
|
||||
|
||||
# Set band element according to rule
|
||||
self.band[self.band_current] = rule[3]
|
||||
# Set current state according to rule
|
||||
self.state_current = rule[2]
|
||||
|
||||
# Move band according to rule
|
||||
if rule[4] == '<':
|
||||
self.band_current -= 1;
|
||||
if rule[4] == '>':
|
||||
self.band_current += 1;
|
||||
|
||||
self.rule_used = ( rule[0] + ', ' +
|
||||
rule[1] + ' -> ' +
|
||||
rule[2] + ', ' +
|
||||
rule[3] + ', ' +
|
||||
rule[4])
|
||||
|
||||
break
|
||||
|
||||
return self.turing_finished()
|
||||
|
||||
|
||||
def turing_finished(self) -> bool:
|
||||
'''Returns whether the TM is in accepting state, rejecting state or has
|
||||
halted because no applicable rules are left.'''
|
||||
return ( self.state_current == self.state_accept or
|
||||
self.state_current == self.state_reject or
|
||||
self.errored )
|
||||
|
||||
|
||||
def turing_accepted(self) -> bool:
|
||||
'''Returns whether the TM, with the given input, has reached accepted state.'''
|
||||
return self.state_current == self.state_accept
|
||||
|
||||
|
||||
def turing_reset(self) -> None:
|
||||
'''Resets current state to start state and band location to zero.'''
|
||||
self.state_current = self.state_start
|
||||
self.band_current = 0
|
||||
|
||||
|
||||
def get_rules(self) -> list:
|
||||
return self.rules
|
||||
|
||||
def get_rule_used(self) -> str:
|
||||
return self.rule_used
|
||||
|
||||
def get_band(self) -> str:
|
||||
return (''.join(self.band)).replace('_', '')
|
||||
|
||||
def get_band_context(self, context: int) -> str:
|
||||
'''Returns context chars to the left and right of the current element as well as the element.'''
|
||||
ret = ""
|
||||
for i in range(self.band_current - context, self.band_current + context + 1):
|
||||
if i < 0:
|
||||
ret += "_ "
|
||||
elif i+1 > len(self.band):
|
||||
ret += "_ "
|
||||
else:
|
||||
ret += f"{self.band[i]} "
|
||||
|
||||
return ret.rstrip()
|
||||
|
||||
def get_band_current_element(self) -> str:
|
||||
if self.band_current < 0 or self.band_current == len(self.band):
|
||||
return '_'
|
||||
return self.band[self.band_current]
|
||||
|
||||
def get_state_start(self) -> str:
|
||||
return self.state_start
|
||||
|
||||
def get_state_current(self) -> str:
|
||||
return self.state_current
|
||||
|
||||
def get_state_accept(self) -> str:
|
||||
return self.state_accept
|
||||
|
||||
def get_state_reject(self) -> str:
|
||||
return self.state_reject
|
||||
|
||||
def get_has_errored(self) -> bool:
|
||||
return self.errored
|
||||
|
||||
|
||||
def set_band(self, input: str):
|
||||
self.band = list(input)
|
||||
|
||||
def redefine(self, turing_machine_def: str):
|
||||
self.__init__(turing_machine_def)
|
||||
|
||||
|
||||
|
||||
def test():
|
||||
tm = TuringMachine(''' 1; q0; q3; q4;
|
||||
q0, 0 -> q0, 0, > ;
|
||||
q0, 1 -> q0, 1, > ;
|
||||
q0, _ -> q1, _, < ;
|
||||
q1, 0 -> q2, 1, < ;
|
||||
q1, 1 -> q1, 0, < ;
|
||||
q1, _ -> q3, 1, - ;
|
||||
q2, 1 -> q2, 1, < ;
|
||||
q2, 0 -> q2, 0, < ;
|
||||
q2, _ -> q3, _, >
|
||||
''')
|
||||
|
||||
tm.set_band("1011")
|
||||
tm.turing_run()
|
||||
if tm.get_band() == "1100":
|
||||
print("Successfull")
|
||||
else:
|
||||
print("Failed")
|
||||
|
16
main.py
Normal file
16
main.py
Normal file
@ -0,0 +1,16 @@
|
||||
from machine import Pin
|
||||
import src.slave as slave
|
||||
import src.wireless as wireless
|
||||
import src.data as data
|
||||
|
||||
|
||||
wireless.espnow_setup()
|
||||
|
||||
if data.master:
|
||||
# server has autostart in another _thread
|
||||
import src.server as server
|
||||
Pin(2, Pin.OUT).value(1)
|
||||
else:
|
||||
Pin(2, Pin.OUT).value(1)
|
||||
slave.loop()
|
||||
|
6
makefile
Normal file
6
makefile
Normal file
@ -0,0 +1,6 @@
|
||||
default:
|
||||
ampy --port /dev/ttyUSB0 put main.py
|
||||
ampy --port /dev/ttyUSB0 put src
|
||||
ampy --port /dev/ttyUSB0 put lib
|
||||
ampy --port /dev/ttyUSB0 put html
|
||||
|
6
src/data.py
Normal file
6
src/data.py
Normal file
@ -0,0 +1,6 @@
|
||||
slave_list: list[bytes] = []
|
||||
master = False
|
||||
master_mac = b''
|
||||
slave_num = 0
|
||||
result_dict: dict[int, list[str]] = {}
|
||||
|
42
src/display.py
Normal file
42
src/display.py
Normal file
@ -0,0 +1,42 @@
|
||||
from machine import Pin, SoftI2C
|
||||
from lib.sh1106 import SH1106_I2C as OLED
|
||||
import src.data as data
|
||||
|
||||
def setup():
|
||||
global display, i2c_display
|
||||
|
||||
i2c = SoftI2C(scl=Pin(22), sda=Pin(21))
|
||||
display = OLED(128, 64, i2c)
|
||||
|
||||
def draw():
|
||||
global display
|
||||
display.show()
|
||||
|
||||
def text(text: str, x: int, y:int):
|
||||
display.text(text, x, y)
|
||||
display.show()
|
||||
|
||||
def slave():
|
||||
global display
|
||||
display.text(str(data.slave_num), 0, 0)
|
||||
|
||||
def arrow():
|
||||
global display
|
||||
display.line(50, 0, 50, 15, 1)
|
||||
|
||||
def tm(band: str, rule_used: str, state_current: str):
|
||||
global display
|
||||
reset()
|
||||
|
||||
slave()
|
||||
arrow()
|
||||
display.text(state_current, 100, 0)
|
||||
display.text(band, 0, 20)
|
||||
display.text(rule_used, 0, 40)
|
||||
|
||||
draw()
|
||||
|
||||
|
||||
def reset():
|
||||
global display
|
||||
display.fill_rect(0, 0, 128, 64, 0)
|
194
src/server.py
Normal file
194
src/server.py
Normal file
@ -0,0 +1,194 @@
|
||||
import _thread
|
||||
from lib.httpserver import (HTTPServer, HTTPResponse)
|
||||
from lib.httpserver.url import HTTPRequest
|
||||
from lib.html_builder import HTMLBuilder
|
||||
import src.data as data
|
||||
from src.wireless import espnow_recv, espnow_send, espnow_send_raw
|
||||
|
||||
|
||||
app = HTTPServer(timeout=10)
|
||||
|
||||
status = {
|
||||
"page_title": {
|
||||
"type": "item",
|
||||
"value": "TM-SIM Home"
|
||||
},
|
||||
"slave_selected": {
|
||||
"type": "item",
|
||||
"value": "Slave 1"
|
||||
},
|
||||
"select_slave": {
|
||||
"type": "radio",
|
||||
"value": [f'Slave {x}' for x in range(1, len(data.slave_list)+1)]
|
||||
},
|
||||
"to_slave": {
|
||||
"type": "radio",
|
||||
"value": [f'Slave {x}' for x in range(1, len(data.slave_list)+1)]
|
||||
},
|
||||
"rule_used": {
|
||||
"type": "item",
|
||||
"value": ""
|
||||
},
|
||||
"band_current": {
|
||||
"type": "item",
|
||||
"value": ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def unescape(string: str) -> str:
|
||||
# unescapes the chars needed for tms
|
||||
# namely ; , - _ < >
|
||||
return (string
|
||||
.replace('%3B',';')
|
||||
.replace('%2C',',')
|
||||
.replace('%2D','-')
|
||||
.replace('%5F','_')
|
||||
.replace('%3C','<')
|
||||
.replace('%3E','>')
|
||||
.replace('+', ' ')
|
||||
)
|
||||
|
||||
|
||||
@app.route("GET", "/")
|
||||
def root(con, _):
|
||||
global status
|
||||
|
||||
HTTPResponse(con, 200, "HTML").send_raw(
|
||||
HTMLBuilder(["html/head.html","html/home.html"], status).build()
|
||||
)
|
||||
|
||||
|
||||
@app.route("GET", "/docs")
|
||||
def docs(con, _):
|
||||
HTTPResponse(con, 200, "HTML").send_file("html/docs.html")
|
||||
|
||||
|
||||
@app.route("GET", "/definition")
|
||||
def definition(con, request: HTTPRequest):
|
||||
# Get slave and tm definition from request and send off
|
||||
tm = "DEFINE:" + unescape(str(request.parameters.get("tm")))
|
||||
slave_num = int(str(request.parameters.get("select_slave"))[6:]) -1
|
||||
slave = data.slave_list[slave_num]
|
||||
|
||||
print(tm)
|
||||
espnow_send(slave, tm)
|
||||
HTTPResponse(con, 302).redirect("/")
|
||||
|
||||
|
||||
@app.route("GET", "/input")
|
||||
def input(con, request: HTTPRequest):
|
||||
# Get slave and input band from request and send off
|
||||
band = "BAND:" + str(request.parameters.get("band"))
|
||||
slave_num = int(str(request.parameters.get("select_slave"))[6:]) -1
|
||||
slave = data.slave_list[slave_num]
|
||||
|
||||
espnow_send(slave, band)
|
||||
HTTPResponse(con, 302).redirect("/")
|
||||
|
||||
|
||||
@app.route("GET", "/run")
|
||||
def run(con, request: HTTPRequest):
|
||||
# use builder to incorporate that into response
|
||||
global status
|
||||
|
||||
select_slave = str(request.parameters.get("select_slave"))
|
||||
if "Slave" in select_slave:
|
||||
status["slave_selected"]["value"] = unescape(select_slave)
|
||||
|
||||
# Go to simulator page where you can run all or step through
|
||||
HTTPResponse(con, 200, "HTML").send_raw(
|
||||
HTMLBuilder(["html/head.html", "html/run.html"], status).build()
|
||||
)
|
||||
|
||||
|
||||
@app.route("GET", "/run_all")
|
||||
def run_all(con, _):
|
||||
global status
|
||||
# Run the tm on the current controller to completion
|
||||
slave_num = int(status["slave_selected"]["value"][6:]) -1
|
||||
espnow_send(data.slave_list[slave_num], "RUN")
|
||||
status["band_current"]["value"] = espnow_recv()
|
||||
|
||||
HTTPResponse(con, 302).redirect("/run")
|
||||
|
||||
|
||||
@app.route("GET", "/run_step")
|
||||
def run_step(con, _):
|
||||
global status
|
||||
slave_num = int(status["slave_selected"]["value"][6:]) -1
|
||||
|
||||
espnow_send(data.slave_list[slave_num], "STEP")
|
||||
rule_used, band_current = espnow_recv().split(':')
|
||||
|
||||
status["rule_used"]["value"] = rule_used
|
||||
status["band_current"]["value"] = band_current
|
||||
|
||||
HTTPResponse(con, 302).redirect("/run")
|
||||
|
||||
|
||||
@app.route("GET", "/pass")
|
||||
def pass_band(con, request: HTTPRequest):
|
||||
try:
|
||||
slave_from = int(str(request.parameters.get("select_slave"))[6:]) -1
|
||||
slave_to = int(str(request.parameters.get("to_slave"))[6:]) -1
|
||||
|
||||
slave_from = data.slave_list[slave_from]
|
||||
slave_to = data.slave_list[slave_to]
|
||||
|
||||
# Give PASS command and send peer to pass to
|
||||
espnow_send(slave_from, "PASS")
|
||||
espnow_send_raw(slave_from, slave_to)
|
||||
except:
|
||||
print(f"WEB:ERR:PASS:Slaves not selected")
|
||||
|
||||
|
||||
HTTPResponse(con, 302).redirect("/run")
|
||||
|
||||
|
||||
@app.route("GET", "/result")
|
||||
def result(con, _):
|
||||
# htmlbuilder dat hoe to show result of every tm
|
||||
# and also endresult of computation chain
|
||||
result_list = []
|
||||
for i in range(0, len(data.slave_list)):
|
||||
print("test: ", i)
|
||||
# todo: parse result_dict of src.data to display res
|
||||
while not espnow_send(data.slave_list[i], "RESULT"):
|
||||
pass
|
||||
|
||||
recv = espnow_recv().split(":")
|
||||
if len(recv) > 1:
|
||||
result_list.append(espnow_recv()[1])
|
||||
else:
|
||||
result_list.append(espnow_recv())
|
||||
|
||||
results = {
|
||||
"results": {
|
||||
"type": "list",
|
||||
"value": result_list
|
||||
}
|
||||
}
|
||||
|
||||
HTTPResponse(con, 200, "HTML").send_raw(
|
||||
HTMLBuilder(["html/head.html", "html/result.html"], results).build()
|
||||
)
|
||||
|
||||
|
||||
@app.route("GET", "/reset")
|
||||
def reset(con, _):
|
||||
global status
|
||||
# rule_used and band_current are reset on slave side also
|
||||
status["rule_used"]["value"] = ""
|
||||
status["band_current"]["value"] = ""
|
||||
slave_num = int(status["slave_selected"]["value"][6:]) -1
|
||||
|
||||
# send reset
|
||||
espnow_send(data.slave_list[slave_num], "RESET")
|
||||
|
||||
HTTPResponse(con, 302).redirect("/run")
|
||||
|
||||
|
||||
|
||||
_thread.start_new_thread(app.start, ())
|
||||
|
98
src/slave.py
Normal file
98
src/slave.py
Normal file
@ -0,0 +1,98 @@
|
||||
from src.wireless import espnow_send, espnow_recv, espnow_recv_raw
|
||||
from lib.tm_sim import TuringMachine as TM
|
||||
import src.data as data
|
||||
import src.display as display
|
||||
|
||||
# placeholder in case somethin goes fucky wucky
|
||||
tm: TM = TM("")
|
||||
|
||||
def __define(definition: str) -> None:
|
||||
global tm
|
||||
# its alive!
|
||||
tm = TM(definition)
|
||||
display.tm("", "", tm.get_state_current())
|
||||
display.draw()
|
||||
|
||||
print(f"TM:CONFIG:Definition set")
|
||||
|
||||
def __band(band: str) -> None:
|
||||
global tm
|
||||
# insert band here
|
||||
tm.set_band(band)
|
||||
display.tm(tm.get_band_context(3), "", tm.get_state_current())
|
||||
display.draw()
|
||||
print(f"TM:CONFIG:Band set")
|
||||
|
||||
def __step() -> None:
|
||||
global tm
|
||||
# step forth by one
|
||||
fin = tm.turing_step()
|
||||
display.tm(tm.get_band_context(3), tm.get_rule_used().replace(' ',''), tm.get_state_current())
|
||||
|
||||
# master expects the used rule and current band
|
||||
send = tm.get_rule_used() + ":" + tm.get_band()
|
||||
espnow_send(data.master_mac, send)
|
||||
|
||||
if fin:
|
||||
print(f"TM:STEP:Finished")
|
||||
else:
|
||||
print(f"TM:STEP:Step")
|
||||
|
||||
def __run() -> None:
|
||||
global tm
|
||||
# crank dat
|
||||
tm.turing_run()
|
||||
display.tm(tm.get_band_context(3), tm.get_rule_used().replace(' ',''), tm.get_state_current())
|
||||
|
||||
espnow_send(data.master_mac, tm.get_band())
|
||||
print(f"TM:RUN:FINISHED")
|
||||
|
||||
def __pass() -> None:
|
||||
global tm
|
||||
# receive the address to which the band should be passed
|
||||
peer = espnow_recv_raw()
|
||||
# and do it
|
||||
espnow_send(peer, f'BAND:{tm.get_band()}')
|
||||
|
||||
def __result() -> None:
|
||||
global tm
|
||||
espnow_send(data.master_mac, tm.get_band())
|
||||
|
||||
def __reset() -> None:
|
||||
global tm
|
||||
# reset that sucker
|
||||
tm.turing_reset()
|
||||
display.tm("", "", tm.get_state_current())
|
||||
|
||||
print(f"TM:CONFIG:RESET")
|
||||
|
||||
|
||||
def loop():
|
||||
display.setup()
|
||||
display.slave()
|
||||
display.draw()
|
||||
|
||||
while True:
|
||||
msg = espnow_recv()
|
||||
|
||||
if "DEFINE" in msg:
|
||||
__define(msg.split(':')[1])
|
||||
|
||||
if "BAND" in msg:
|
||||
__band(msg.split(':')[1])
|
||||
|
||||
if "STEP" in msg:
|
||||
__step()
|
||||
|
||||
if "RUN" in msg:
|
||||
__run()
|
||||
|
||||
if "PASS" in msg:
|
||||
__pass()
|
||||
|
||||
if "RESULT" in msg:
|
||||
__result()
|
||||
|
||||
if "RESET" in msg:
|
||||
__reset()
|
||||
|
144
src/wireless.py
Normal file
144
src/wireless.py
Normal file
@ -0,0 +1,144 @@
|
||||
from network import WLAN, AP_IF, STA_IF
|
||||
from espnow import ESPNow
|
||||
import src.data as data
|
||||
|
||||
|
||||
def espnow_setup_master():
|
||||
global esp, wifi
|
||||
|
||||
wifi = WLAN(AP_IF)
|
||||
wifi.active(True)
|
||||
wifi.config(ssid="wifiap", key="password")
|
||||
|
||||
print(f"ESP:INIT:MASTER:Looking for Slaves")
|
||||
|
||||
broadcast_mac = b'\xff' * 6
|
||||
esp.add_peer(broadcast_mac)
|
||||
|
||||
slave_count = 1
|
||||
smsg = "ping"
|
||||
|
||||
# ~30s max broadcasting to slaves
|
||||
# espnow limit is 20 peers
|
||||
for _ in range(0,30):
|
||||
espnow_send(broadcast_mac, smsg)
|
||||
print(f"MSG:SEND:STR:{smsg}")
|
||||
|
||||
peer, rmsg = esp.recv(timeout_ms=1000)
|
||||
if peer:
|
||||
if type(rmsg) is bytes:
|
||||
print(f"MSG:RECV:STR:{rmsg.decode()}")
|
||||
|
||||
espnow_send(peer, f"{slave_count}")
|
||||
data.slave_list.append(peer)
|
||||
slave_count += 1
|
||||
|
||||
print(f"ESP:INIT:MASTER:Found slave")
|
||||
|
||||
dedup = []
|
||||
[dedup.append(x) for x in data.slave_list if x not in dedup]
|
||||
data.slave_list = dedup
|
||||
|
||||
print(f"ESP:INIT:MASTER:Found {len(data.slave_list)} slaves")
|
||||
|
||||
|
||||
def espnow_setup():
|
||||
global esp
|
||||
|
||||
wifi = WLAN(STA_IF)
|
||||
wifi.active(True)
|
||||
|
||||
esp = ESPNow()
|
||||
esp.active(True)
|
||||
|
||||
print(f"ESP:INIT:SLAVE:Looking for Master")
|
||||
|
||||
# 20s wait time for message
|
||||
host, msg = esp.recv(timeout_ms=20000)
|
||||
if host:
|
||||
data.master = False
|
||||
data.master_mac = host
|
||||
|
||||
# has to be, still need this
|
||||
if type(msg) is bytes:
|
||||
print(f"MSG:RECV:STR:{msg.decode()}")
|
||||
|
||||
espnow_send(data.master_mac, "pong")
|
||||
# ignore repeat pings and wait for correct package
|
||||
while True:
|
||||
try:
|
||||
data.slave_num = int(espnow_recv())
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
print(f"ESP:INIT:SLAVE:Found Master, registered as slave {data.slave_num}")
|
||||
|
||||
else:
|
||||
data.master = True
|
||||
espnow_setup_master()
|
||||
|
||||
|
||||
def espnow_send(peer: bytes, msg: str) -> bool:
|
||||
"""
|
||||
Returns True if peer has received the message.
|
||||
Intended to be used with espnow_recv.
|
||||
"""
|
||||
print(f"MSG:SEND:STR:{msg}")
|
||||
if not peer in [x[0] for x in esp.get_peers()] and not b'\xff'*6 in peer:
|
||||
esp.add_peer(peer)
|
||||
return esp.send(peer, msg.encode())
|
||||
|
||||
|
||||
def espnow_send_raw(peer: bytes, msg: bytes) -> bool:
|
||||
"""
|
||||
Returns True if peer has received the message.
|
||||
Intended to be used with espnow_recv_raw.
|
||||
"""
|
||||
print(f"MSG:SEND:RAW:{msg}")
|
||||
if not peer in [x[0] for x in esp.get_peers()]:
|
||||
esp.add_peer(peer)
|
||||
return esp.send(peer, msg)
|
||||
|
||||
|
||||
def espnow_recv() -> str:
|
||||
"""
|
||||
Returns the received, decoded message.
|
||||
Locks up further execution until a message is received.
|
||||
"""
|
||||
timeout = 5000 # 5s
|
||||
|
||||
host, msg = esp.recv(timeout)
|
||||
while not host:
|
||||
host, msg = esp.recv(timeout)
|
||||
|
||||
if type(msg) is bytes:
|
||||
print(f"MSG:RECV:STR:{msg.decode()}")
|
||||
return msg.decode()
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
def espnow_recv_raw() -> bytes:
|
||||
"""
|
||||
Returns the received message without decoding.
|
||||
Locks up further execution until a message from master is received.
|
||||
"""
|
||||
timeout = 5000 # 5s
|
||||
|
||||
host, msg = esp.recv(timeout)
|
||||
while not host:
|
||||
host, msg = esp.recv(timeout)
|
||||
|
||||
if host != data.master_mac:
|
||||
print(f"MSG:RECV:ERR:Message not from Master")
|
||||
host = None
|
||||
|
||||
print(f"MSG:RECV:RAW:{msg}")
|
||||
|
||||
# msg has to be bytes but still needs to be checked here
|
||||
if type(msg) is bytes:
|
||||
return msg
|
||||
else:
|
||||
return b''
|
||||
|
Loading…
x
Reference in New Issue
Block a user