diff --git a/README.md b/README.md deleted file mode 100644 index 3a1ec59b1828d6b72f9f3d62e10690be04d1eda8..0000000000000000000000000000000000000000 --- a/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# CM2305 Group 17 Project - - - -## 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.cardiff.ac.uk/c23030734/cm2305-group-17-project.git -git branch -M main -git push -uf origin main -``` - -## Integrate with your tools - -- [ ] [Set up project integrations](https://git.cardiff.ac.uk/c23030734/cm2305-group-17-project/-/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. diff --git a/add_product.py b/add_product.py deleted file mode 100644 index cdbf567161d6b4b538241cf35216e52fb3daab4b..0000000000000000000000000000000000000000 --- a/add_product.py +++ /dev/null @@ -1,15 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -from models import Product, Base - -DATABASE_URL = 'sqlite:///local.db' - -engine = create_engine(DATABASE_URL) -Session = sessionmaker(bind=engine) -session = Session() - -new_product = Product(id="0765756931038", name="Mouse Box", recycling_type="Cardboard", reward_value=5) -session.add(new_product) -session.commit() - -print("Done") \ No newline at end of file diff --git a/add_user.py b/add_user.py deleted file mode 100644 index 2d939af7671b399cb61df5afd4779f76abed6eca..0000000000000000000000000000000000000000 --- a/add_user.py +++ /dev/null @@ -1,15 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -from models import Customer, Base - -DATABASE_URL = 'sqlite:///local.db' - -engine = create_engine(DATABASE_URL) -Session = sessionmaker(bind=engine) -session = Session() - -new_customer = Customer(id="0600077786", first_name="Jeyan", email="kanagaratnamj@cardiff.ac.uk") -session.add(new_customer) -session.commit() - -print("Done") \ No newline at end of file diff --git a/email_template.py b/email_template.py deleted file mode 100644 index 2b4486ff3e89aea8643fface55141b4b57a5c44c..0000000000000000000000000000000000000000 --- a/email_template.py +++ /dev/null @@ -1,95 +0,0 @@ -html = """\ -<style> - .page {{ - margin-left: 10%; - margin-right: 10%; - }} - .section {{ - font-family: system-ui, sans-serif; - text-align: center; - color: #1E1C59; - display: flex; - flex-direction: column; - margin-top: 20px; - }} - #points {{ - font-size: 64px; - font-weight: bold; - }} - .left {{ - text-align: left; - }} - .footer {{ - margin-top: 100px; - font-size: 14px; - }} -</style> -<div class="page section"> - <p> - Thank you, {name}, for recycling today. Every item returned is a step towards a cleaner planet.<br/><br/> - </p> - <div style="margin-top: 30px;margin-bottom: 20px;"> - <p> - You've earned - </p> - <span id="points"> - {points} - </span> - <p> - points from this return!<br><br> - You currently have {total_points} points. - </p> - </div> - <div class="section left"> - <h1 style="font-weight: bold"> - How to spend your points? - </h1> - <p> - Spending your points has never been easier. Simply visit your local store, scan your card on eligible purchases, and earn savings on your future purchases by redeeming the points you earn from returning used cosmetic packaging. - </p> - </div> - <div class="section left"> - <h1 style="font-weight: bold"> - Want another 5 points? - </h1> - <p> - Did you know that you can earn double points every time you complete the survey below? It won't take more than a minute of your time. - </p> - <div class="section left"> - <p style="font-size: 20px;"> - How was your in-store experience today? - </p> - <div style="display: flex; flex-direction: row; justify-content: space-between; margin-top: 20px;"> - <a href="#"> - <div style="background-color: #1E1C59; color: #FFFFFF; padding: 10px; border-radius: 5px; text-decoration: none;"> - Poor - </div> - </a> - <a href="#"> - <div style="background-color: #1E1C59; color: #FFFFFF; padding: 10px; border-radius: 5px; text-decoration: none;"> - Unsatisfactory - </div> - </a> - <a href="#"> - <div style="background-color: #1E1C59; color: #FFFFFF; padding: 10px; border-radius: 5px; text-decoration: none;"> - Neutral - </div> - </a> - <a href="#"> - <div style="background-color: #1E1C59; color: #FFFFFF; padding: 10px; border-radius: 5px; text-decoration: none;"> - Satisfactory - </div> - </a> - <a href="#"> - <div style="background-color: #1E1C59; color: #FFFFFF; padding: 10px; border-radius: 5px; text-decoration: none;"> - Excellent - </div> - </a> - </div> - </div> - </div> - <div class="footer"> - This email is factual and is being sent for demonstration purposes only. - </div> -</div> -""" \ No newline at end of file diff --git a/emails.py b/emails.py deleted file mode 100644 index a7185e310c6f1c0238352ffc046471ddaa9316dd..0000000000000000000000000000000000000000 --- a/emails.py +++ /dev/null @@ -1,27 +0,0 @@ -from requests import request -from email_template import html - -url = "https://api.useplunk.com/v1/send" - -# Change me to a Secret API Key from https://app.useplunk.com/settings/api -# Do not commit me -token = "" - -def send(to_email, name, points, total_points): - payload = { - "to": to_email, - "subject": "You've Earned Points Today", - "body": html.format(name=name, points=points, total_points=total_points), - "name": "Return&Earn", - } - headers = { - "Content-Type": "application/json", - "Authorization": f'Bearer {token}' - } - - response = request("POST", url, json=payload, headers=headers) - - print(response.text) - -# Test -send("CrowleE@cardiff.ac.uk", "Ewan", 20, 200) \ No newline at end of file diff --git a/flask-test/__pycache__/models.cpython-313.pyc b/flask-test/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..27404237b45e194a2189f56f7da4604dc574b1b1 Binary files /dev/null and b/flask-test/__pycache__/models.cpython-313.pyc differ diff --git a/flask-test/flasktest.py b/flask-test/flasktest.py index 6118d11012e62ce0551b9c8b662107211777c34f..760ac8df5f4bbadd64b498d7fd79ecb59bcbdb95 100644 --- a/flask-test/flasktest.py +++ b/flask-test/flasktest.py @@ -1,22 +1,74 @@ -from flask import Flask, render_template -import sqlalchemy as db +from flask import Flask, render_template, jsonify, request +from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -import models +from models import Base, Return, Customer, Product +from datetime import datetime app = Flask(__name__) -# Initialise the database engine -engine = db.create_engine('sqlite:///local.db', echo=True) -Session = sessionmaker(bind=engine) +# Database configuration +SQLALCHEMY_DATABASE_URL = "sqlite:///./local.db" +engine = create_engine(SQLALCHEMY_DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) @app.route('/') def index(): + return render_template('index.html') - session = Session() - returns = session.query(models.Return).all() #Queries the returns - - session.close() - return render_template('index.html', returns=returns) +@app.route('/returns') +def get_returns(): + start_date = request.args.get('start_date') + end_date = request.args.get('end_date') + + db = SessionLocal() + try: + # Build query + query = db.query(Return) + + # Add date filtering + if start_date: + query = query.filter(Return.return_date >= datetime.fromisoformat(start_date)) + if end_date: + query = query.filter(Return.return_date <= datetime.fromisoformat(end_date)) + + # Get return records + returns = query.all() + + # Calculate statistics + total_returns = len(returns) + total_points = sum(return_item.reward_value for return_item in returns) + active_users = len(set(return_item.customer_id for return_item in returns)) + + # Calculate product statistics + product_stats = {} + for return_item in returns: + product_stats[return_item.product_id] = product_stats.get(return_item.product_id, 0) + 1 + + # Calculate weekly statistics + weekly_stats = {} + for return_item in returns: + day_of_week = return_item.return_date.weekday() + weekly_stats[day_of_week] = weekly_stats.get(day_of_week, 0) + 1 + + return jsonify({ + 'returns': [ + { + 'id': r.id, + 'customer_id': r.customer_id, + 'product_id': r.product_id, + 'reward_value': r.reward_value, + 'return_date': r.return_date.isoformat() + } + for r in returns + ], + 'total_returns': total_returns, + 'total_points': total_points, + 'active_users': active_users, + 'product_stats': product_stats, + 'weekly_stats': weekly_stats + }) + finally: + db.close() if __name__ == '__main__': - app.run(debug=True, host='0.0.0.0', port=5000) + app.run(debug=True) diff --git a/flask-test/requirements.txt b/flask-test/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..b23a3ee5a72efe60c41080b882521d6a5bc7c573 --- /dev/null +++ b/flask-test/requirements.txt @@ -0,0 +1,9 @@ +Flask==3.1.0 +SQLAlchemy==2.0.39 +Werkzeug==3.1.3 +Jinja2==3.1.6 +MarkupSafe==3.0.2 +click==8.1.8 +colorama==0.4.6 +itsdangerous==2.2.0 +blinker==1.9.0 \ No newline at end of file diff --git a/flask-test/static/css/style.css b/flask-test/static/css/style.css index ab817ced7d62167432106c534c55f2116a7b8f36..17c96569ef2470dd1e742e8a8bc98b9e1701deb7 100644 --- a/flask-test/static/css/style.css +++ b/flask-test/static/css/style.css @@ -1,29 +1,472 @@ /* /static/css/style.css */ +/* Global styles */ +:root { + --primary-color: #2ecc71; + --secondary-color: #27ae60; + --background-color: #f0f9f4; + --text-color: #2c3e50; + --card-background: #ffffff; + --border-color: #b8e6c9; + --hover-color: #a8e6cf; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +body { + background-color: var(--background-color); + color: var(--text-color); + line-height: 1.6; +} + +/* Navbar styles */ +.navbar { + background-color: var(--primary-color); + padding: 1rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.navbar .logo { + height: 40px; + width: auto; +} + +/* Main content area */ .page { - margin-left: 10%; - margin-right: 10%; + max-width: 1200px; + margin: 2rem auto; + padding: 0 1rem; } .section { - font-family: system-ui, sans-serif; - text-align: center; - color: #1E1C59; + background-color: var(--card-background); + border-radius: 10px; + padding: 2rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +/* Header section styles */ +.header-section { display: flex; - flex-direction: column; - margin-top: 20px; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; } -#points { - font-size: 64px; +.header-section h1 { + margin: 0; + color: #333; + font-size: 1.8rem; font-weight: bold; } -.left { - text-align: left; +/* Refresh control styles */ +.refresh-controls { + display: flex; + align-items: center; + gap: 1rem; +} + +.refresh-button { + background-color: var(--primary-color); + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; +} + +.refresh-button:hover { + background-color: var(--hover-color); +} + +.countdown { + color: #666; + font-size: 0.9rem; +} + +/* Filter and Statistics Section */ +.filter-section { + margin-bottom: 2rem; +} + +.date-filter { + margin-bottom: 2.5rem; /* Increase space between filter and stats */ +} + +/* Statistics summary styles */ +.stats-summary { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-bottom: 2rem; +} + +.stat-box { + background-color: white; + padding: 1.5rem; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.1); + text-align: center; + transition: transform 0.2s ease; +} + +.stat-box:hover { + transform: translateY(-2px); + box-shadow: 0 6px 15px rgba(0,0,0,0.15); +} + +.stat-box h3 { + color: #666; + font-size: 1rem; + margin-bottom: 0.5rem; } +.stat-box p { + font-size: 1.5rem; + font-weight: bold; + color: #333; + margin: 0; +} + +/* Chart area styles */ +.charts-container { + margin-top: 20px; + width: 100%; +} + +.chart-row { + margin-bottom: 30px; + width: 100%; +} + +.chart-container { + background: white; + border-radius: 12px; + padding: 25px; + box-shadow: 0 4px 15px rgba(0,0,0,0.1); + width: 100%; + height: 400px; + transition: box-shadow 0.3s ease; +} + +.chart-container:hover { + box-shadow: 0 6px 20px rgba(0,0,0,0.15); +} + +.chart-container h3 { + margin: 0 0 20px 0; + color: #333; + font-size: 1.2em; +} + +#productChart, #hourlyChart { + width: 100% !important; + height: 300px !important; +} + +/* Detailed records list styles */ +.returns-list-container { + margin-top: 2rem; +} + +.returns-list-container h2 { + color: var(--secondary-color); + margin-bottom: 1rem; +} + +/* Footer styles */ .footer { - margin-top: 100px; - font-size: 14px; + text-align: center; + padding: 1rem; + background-color: var(--primary-color); + color: white; + position: fixed; + bottom: 0; + width: 100%; +} + +/* Responsive design */ +@media (max-width: 768px) { + .page { + margin: 1rem auto; + } + + .section { + padding: 1rem; + } + + .charts-container { + grid-template-columns: 1fr; + } + + .header-section { + flex-direction: column; + gap: 1rem; + text-align: center; + } + + .refresh-controls { + flex-direction: column; + } + + .chart-container { + height: 300px; + } + + #productChart, #hourlyChart { + height: 200px !important; + } +} + +/* Table styles */ +.table-container { + overflow-x: auto; + margin-top: 1rem; +} + +.returns-table { + width: 100%; + border-collapse: collapse; + background-color: white; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +.returns-table th, +.returns-table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +.returns-table th { + background-color: var(--primary-color); + color: white; + font-weight: 500; +} + +.returns-table tr:hover { + background-color: var(--hover-color); +} + +/* Action buttons */ +.action-btn { + padding: 6px 12px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + transition: background-color 0.2s; +} + +.view-btn { + background-color: var(--primary-color); + color: white; +} + +.details-btn { + background-color: var(--secondary-color); + color: white; + margin-left: 8px; +} + +.action-btn:hover { + opacity: 0.9; +} + +/* Details row */ +.details-row { + background-color: var(--background-color); +} + +.details-content { + padding: 1rem; +} + +.details-content p { + margin: 0.5rem 0; +} + +/* Modal styles */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.5); + z-index: 1000; +} + +.modal-content { + position: relative; + background-color: white; + margin: 10% auto; + padding: 2rem; + width: 80%; + max-width: 600px; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); +} + +.close { + position: absolute; + right: 1rem; + top: 1rem; + font-size: 1.5rem; + cursor: pointer; + color: var(--text-color); +} + +.close:hover { + color: var(--primary-color); +} + +/* Filter controls */ +.filter-controls { + display: flex; + gap: 2rem; + align-items: center; +} + +.filter-group { + display: flex; + align-items: center; + gap: 1rem; +} + +.filter-group label { + font-weight: 500; + color: var(--text-color); +} + +.filter-group select, +.filter-group input { + padding: 0.5rem; + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 0.9rem; +} + +.filter-group button { + padding: 0.5rem 1rem; + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; +} + +.filter-group button:hover { + background-color: var(--hover-color); +} + +/* No records message */ +.no-records-message { + text-align: center; + padding: 2rem; + background-color: var(--background-color); + border-radius: 8px; + margin: 1rem 0; + font-size: 1.1rem; + color: var(--text-color); +} + +/* Date picker modal styles */ +.date-picker-btn { + background-color: var(--secondary-color); + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; +} + +.date-picker-btn:hover { + background-color: var(--hover-color); +} + +.date-picker-content { + padding: 1rem 0; +} + +.date-range-group { + margin-bottom: 1.5rem; +} + +.date-range-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; +} + +.date-range-group input { + width: 100%; + padding: 0.5rem; + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 1rem; +} + +.preset-ranges { + margin: 1.5rem 0; +} + +.preset-ranges h3 { + margin-bottom: 1rem; + color: var(--text-color); +} + +.preset-buttons { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 0.5rem; +} + +.preset-buttons button { + padding: 0.5rem; + background-color: var(--background-color); + border: 1px solid var(--border-color); + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; +} + +.preset-buttons button:hover { + background-color: var(--hover-color); + border-color: var(--primary-color); +} + +.apply-date-range { + width: 100%; + padding: 0.75rem; + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.2s; +} + +.apply-date-range:hover { + background-color: var(--secondary-color); +} + +/* No records message in table */ +.no-records-message td { + text-align: center; + padding: 2rem; + background-color: var(--background-color); + color: var(--text-color); + font-size: 1.1rem; } diff --git a/flask-test/static/Final_LT_HD_Horizontal.png b/flask-test/static/images/logo.png similarity index 100% rename from flask-test/static/Final_LT_HD_Horizontal.png rename to flask-test/static/images/logo.png diff --git a/flask-test/static/js/script.js b/flask-test/static/js/script.js new file mode 100644 index 0000000000000000000000000000000000000000..365e9a39b8387db1d127728f35e99e6bfd2ab7bf --- /dev/null +++ b/flask-test/static/js/script.js @@ -0,0 +1,379 @@ +// 页面加载完成后执行 +document.addEventListener('DOMContentLoaded', function() { + // 获取所有产品图片 + const productImages = document.querySelectorAll('.product-image'); + + // 为每个图片添加错误处理 + productImages.forEach(img => { + img.onerror = function() { + this.src = '/static/products/default.jpg'; + }; + }); + + initCharts(); + filterByDate('today'); // 默认显示今日数据 + + // 添加刷新按钮事件监听 + document.getElementById('refreshBtn').addEventListener('click', function() { + refreshData(); + // 重置倒计时 + countdown = 30 * 60; + }); +}); + +// 设置倒计时刷新 +let countdown = 30 * 60; // 30分钟 +let currentFilter = 'today'; // 保存当前的筛选条件 + +function updateCountdown() { + const minutes = Math.floor(countdown / 60); + const seconds = countdown % 60; + const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + + const countdownElements = document.querySelectorAll('#countdown'); + countdownElements.forEach(element => { + element.textContent = timeString; + }); + + countdown--; + + if (countdown < 0) { + countdown = 30 * 60; // 重置为30分钟 + refreshData(); // 刷新数据 + } +} + +// 每秒更新倒计时 +setInterval(updateCountdown, 1000); + +let productChart, hourlyChart; + +function initCharts() { + const productCtx = document.getElementById('productChart').getContext('2d'); + const hourlyCtx = document.getElementById('hourlyChart').getContext('2d'); + + productChart = new Chart(productCtx, { + type: 'bar', + data: { + labels: [], + datasets: [{ + label: 'Returns by Product', + data: [], + backgroundColor: 'rgba(54, 162, 235, 0.5)', + borderColor: 'rgba(54, 162, 235, 1)', + borderWidth: 1 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + beginAtZero: true, + ticks: { + stepSize: 1 + } + } + } + } + }); + + hourlyChart = new Chart(hourlyCtx, { + type: 'line', + data: { + labels: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + datasets: [{ + label: 'Returns by Day of Week', + data: [0, 0, 0, 0, 0, 0, 0], + fill: false, + borderColor: 'rgba(75, 192, 192, 1)', + tension: 0.1 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + beginAtZero: true, + ticks: { + stepSize: 1 + } + } + } + } + }); +} + +// 更新统计数据 +function updateStats(data) { + document.getElementById('totalReturns').textContent = data.total_returns; + document.getElementById('totalPoints').textContent = data.total_points; + document.getElementById('activeUsers').textContent = data.active_users; +} + +// 更新图表数据 +function updateCharts(data) { + // Update product chart + const productLabels = Object.keys(data.product_stats); + const productData = Object.values(data.product_stats); + + productChart.data.labels = productLabels; + productChart.data.datasets[0].data = productData; + productChart.update(); + + // Update weekly trend chart + const weeklyData = [0, 0, 0, 0, 0, 0, 0]; // Initialize array for 7 days + data.returns.forEach(return_item => { + const date = new Date(return_item.return_date); + const dayOfWeek = date.getDay(); // 0 = Sunday, 1 = Monday, etc. + weeklyData[dayOfWeek]++; + }); + + hourlyChart.data.datasets[0].data = weeklyData; + hourlyChart.update(); +} + +// 根据日期范围筛选数据 +function filterByDate(range) { + const now = new Date(); + let startDate = new Date(); + let endDate = new Date(); + + switch(range) { + case 'today': + startDate.setHours(0, 0, 0, 0); + endDate.setHours(23, 59, 59, 999); + break; + case 'yesterday': + startDate.setDate(now.getDate() - 1); + startDate.setHours(0, 0, 0, 0); + endDate.setDate(now.getDate() - 1); + endDate.setHours(23, 59, 59, 999); + break; + case 'week': + // 获取本周一 + const day = now.getDay(); + const diff = now.getDate() - day + (day === 0 ? -6 : 1); + startDate = new Date(now.setDate(diff)); + startDate.setHours(0, 0, 0, 0); + // 获取本周日 + endDate = new Date(startDate); + endDate.setDate(startDate.getDate() + 6); + endDate.setHours(23, 59, 59, 999); + break; + case 'month': + // 获取本月第一天 + startDate = new Date(now.getFullYear(), now.getMonth(), 1); + startDate.setHours(0, 0, 0, 0); + // 获取本月最后一天 + endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0); + endDate.setHours(23, 59, 59, 999); + break; + case 'quarter': + const currentQuarter = Math.floor(now.getMonth() / 3); + startDate = new Date(now.getFullYear(), currentQuarter * 3, 1); + startDate.setHours(0, 0, 0, 0); + endDate = new Date(now.getFullYear(), (currentQuarter + 1) * 3, 0); + endDate.setHours(23, 59, 59, 999); + break; + case 'year': + startDate = new Date(now.getFullYear(), 0, 1); + startDate.setHours(0, 0, 0, 0); + endDate = new Date(now.getFullYear(), 11, 31); + endDate.setHours(23, 59, 59, 999); + break; + } + + fetchData(startDate, endDate); +} + +function filterByCustomRange() { + const startDate = new Date(document.getElementById('startDate').value); + const endDate = new Date(document.getElementById('endDate').value); + fetchData(startDate, endDate); +} + +function fetchData(startDate, endDate) { + fetch(`/returns?start_date=${startDate.toISOString()}&end_date=${endDate.toISOString()}`) + .then(response => response.json()) + .then(data => { + updateStats(data); + updateCharts(data); + updateTable(data.returns); + }) + .catch(error => console.error('Error:', error)); +} + +// Date picker modal functions +function showDatePickerModal() { + const modal = document.getElementById('datePickerModal'); + modal.style.display = 'block'; + + // Set default dates + const today = new Date(); + const lastMonth = new Date(today.getFullYear(), today.getMonth() - 1, today.getDate()); + + document.getElementById('modalStartDate').value = lastMonth.toISOString().split('T')[0]; + document.getElementById('modalEndDate').value = today.toISOString().split('T')[0]; +} + +function closeDatePickerModal() { + const modal = document.getElementById('datePickerModal'); + modal.style.display = 'none'; +} + +function selectPresetRange(preset) { + const today = new Date(); + let startDate = new Date(); + + switch(preset) { + case 'last7days': + startDate.setDate(today.getDate() - 7); + break; + case 'last30days': + startDate.setDate(today.getDate() - 30); + break; + case 'last90days': + startDate.setDate(today.getDate() - 90); + break; + case 'lastYear': + startDate.setFullYear(today.getFullYear() - 1); + break; + } + + document.getElementById('modalStartDate').value = startDate.toISOString().split('T')[0]; + document.getElementById('modalEndDate').value = today.toISOString().split('T')[0]; +} + +function applyDateRange() { + const startDate = new Date(document.getElementById('modalStartDate').value); + const endDate = new Date(document.getElementById('modalEndDate').value); + + // Set time to start and end of day + startDate.setHours(0, 0, 0, 0); + endDate.setHours(23, 59, 59, 999); + + fetchData(startDate, endDate); + closeDatePickerModal(); +} + +// Update table with data +function updateTable(returns) { + const tbody = document.querySelector('.returns-table tbody'); + const noRecordsMessage = document.getElementById('noRecordsMessage'); + + // Clear existing rows except the no records message + const rows = tbody.querySelectorAll('tr:not(#noRecordsMessage)'); + rows.forEach(row => row.remove()); + + if (!returns || returns.length === 0) { + noRecordsMessage.style.display = 'table-row'; + return; + } + + noRecordsMessage.style.display = 'none'; + + returns.forEach(return_item => { + const row = document.createElement('tr'); + row.innerHTML = ` + <td>${return_item.id}</td> + <td>${return_item.customer_id}</td> + <td>${return_item.product_id}</td> + <td>${return_item.reward_value}</td> + <td>${return_item.return_date}</td> + <td> + <button class="action-btn view-btn" onclick="toggleDetails(this)">View</button> + <button class="action-btn details-btn" onclick="showDetailsModal(this)" style="display: none;">Details</button> + </td> + `; + tbody.appendChild(row); + + const detailsRow = document.createElement('tr'); + detailsRow.className = 'details-row'; + detailsRow.style.display = 'none'; + detailsRow.innerHTML = ` + <td colspan="6"> + <div class="details-content"> + <p><strong>ID:</strong> ${return_item.id}</p> + <p><strong>Customer ID:</strong> ${return_item.customer_id}</p> + <p><strong>Product ID:</strong> ${return_item.product_id}</p> + <p><strong>Reward Value:</strong> ${return_item.reward_value}</p> + <p><strong>Return Date:</strong> ${return_item.return_date}</p> + </div> + </td> + `; + tbody.appendChild(detailsRow); + }); +} + +// 刷新数据 +function refreshData() { + // 根据当前的筛选条件重新获取数据 + switch(currentFilter) { + case 'today': + filterByDate('today'); + break; + case 'week': + filterByDate('week'); + break; + case 'month': + filterByDate('month'); + break; + case 'custom': + const customDate = document.getElementById('customDate').value; + if (customDate) { + filterByDate(customDate); + } + break; + } +} + +// Table interaction functions +function toggleDetails(button) { + const row = button.closest('tr'); + const detailsRow = row.nextElementSibling; + const detailsBtn = row.querySelector('.details-btn'); + + if (detailsRow.style.display === 'none') { + detailsRow.style.display = 'table-row'; + detailsBtn.style.display = 'inline-block'; + button.textContent = 'Hide'; + } else { + detailsRow.style.display = 'none'; + detailsBtn.style.display = 'none'; + button.textContent = 'View'; + } +} + +// Modal functions +const modal = document.getElementById('detailsModal'); +const closeBtn = document.querySelector('.close'); + +function showDetailsModal(button) { + const row = button.closest('tr'); + const detailsContent = row.nextElementSibling.querySelector('.details-content').innerHTML; + document.getElementById('modalContent').innerHTML = detailsContent; + modal.style.display = 'block'; +} + +// Close modal when clicking the close button +closeBtn.onclick = function() { + modal.style.display = 'none'; +} + +// Close modal when clicking outside +window.onclick = function(event) { + if (event.target == modal) { + modal.style.display = 'none'; + } +} + +// Add double-click handler for rows +document.querySelectorAll('.returns-table tbody tr').forEach(row => { + row.addEventListener('dblclick', function() { + const detailsContent = this.nextElementSibling.querySelector('.details-content').innerHTML; + document.getElementById('modalContent').innerHTML = detailsContent; + modal.style.display = 'block'; + }); +}); diff --git a/flask-test/static/script.js b/flask-test/static/script.js deleted file mode 100644 index e599ba1eccea0afa510934c45f95f9390989ad9c..0000000000000000000000000000000000000000 --- a/flask-test/static/script.js +++ /dev/null @@ -1,38 +0,0 @@ -document.addEventListener("DOMContentLoaded", function() { - let countdown = 5; // Needs to be updated if fetch time changes - let countdownElement = document.getElementById("countdown"); - - function fetchReturns() { - fetch('/returns') - .then(response => response.json()) - .then(data => { - let container = document.getElementById("returns-container"); - container.innerHTML = ""; - - data.returns.forEach(ret => { - let p = document.createElement("p"); - p.textContent = `${ret.id} - ${ret.timestamp}`; - container.appendChild(p); - }); - - countdown = 5; - }) - .catch(error => console.error("Error fetching returns:", error)); - } - - // Function to update countdown timer - function updateCountdown() { - countdownElement.textContent = countdown; - countdown--; - - if (countdown < 0) { - countdown = 5; // Reset - } - } - - // 5000ms > 5 seconds - setInterval(fetchReturns, 5000); - setInterval(updateCountdown, 1000); - // Initial fetch - fetchReturns(); -}); diff --git a/flask-test/templates/index.html b/flask-test/templates/index.html index 9804332022c79318f6862323715978810a40a6da..370fb82990447c20634d3773741287bfa138e3a5 100644 --- a/flask-test/templates/index.html +++ b/flask-test/templates/index.html @@ -3,44 +3,144 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>Return Dashboard</title> - <!-- css style sheet --> + <title>ReturnEarn Dashboard</title> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> + <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> </head> <body> + <div class="container-fluid"> + <div class="navbar"> + <a href="#"> + <img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo" class="logo"> + </a> + </div> - <div class="navbar"> - <a href="#"> - <img src="{{ url_for('static', filename='Final_LT_HD_Horizontal.png') }}" alt="Logo"> - </a> - </div> + <div class="page section"> + <div class="header-section"> + <h1>Returns List</h1> + <div class="refresh-controls"> + <button id="refreshBtn" class="refresh-button">Refresh Now</button> + <div id="countdown" class="countdown">Next auto-refresh: 30:00</div> + </div> + </div> + <!-- Filter and Statistics Section --> + <div class="filter-section"> + <div class="date-filter"> + <div class="filter-controls"> + <div class="filter-group"> + <label>Date Range:</label> + <select id="dateFilter" onchange="filterByDate(this.value)"> + <option value="year">This Year</option> + <option value="today">Current Day</option> + <option value="yesterday">Yesterday</option> + <option value="week">This Week</option> + <option value="month">This Month</option> + </select> + </div> + <div class="filter-group"> + <button onclick="showDatePickerModal()" class="date-picker-btn">Advanced Date Picker</button> + </div> + </div> + </div> + + <div class="stats-summary"> + <div class="stat-box"> + <h3>Total Returns</h3> + <p id="totalReturns">0</p> + </div> + <div class="stat-box"> + <h3>Total Points</h3> + <p id="totalPoints">0</p> + </div> + <div class="stat-box"> + <h3>Active Users</h3> + <p id="activeUsers">0</p> + </div> + </div> + </div> + <!-- Charts Section --> + <div class="charts-container"> + <div class="chart-row"> + <div class="chart-container"> + <h3>Product Statistics</h3> + <canvas id="productChart"></canvas> + </div> + </div> + <div class="chart-row"> + <div class="chart-container"> + <h3>Weekly Return Trends</h3> + <canvas id="hourlyChart"></canvas> + </div> + </div> + </div> - <div class="page section"> - <h1>Returns List</h1> - <p>Here are the latest returns:<br> - This page will reset in <span id="countdown">5</span> seconds</p> - <ul> - {% for return in returns | reverse %} - - <p><strong>ID:</strong> {{ return.id }}</p> - <p><strong>Customer ID:</strong> {{ return.customer_id }}</p> - <p><strong>Product ID:</strong> {{ return.product_id }}</p> - <p><strong>Reward Value:</strong> {{ return.reward_value }}</p> - <p><strong>Return Date:</strong> {{ return.return_date }}</p> - <!-- hr splits --> - <hr> - - {% endfor %} - </ul> - </div> + <!-- Detailed Records Section --> + <div class="returns-list-container"> + <h2>Detailed Records</h2> + <div class="table-container"> + <table class="returns-table"> + <thead> + <tr> + <th>ID</th> + <th>Customer ID</th> + <th>Product ID</th> + <th>Reward Value</th> + <th>Return Date</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + <tr id="noRecordsMessage" class="no-records-message" style="display: none;"> + <td colspan="6">No records found for the selected date range.</td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + + <!-- Modal for detailed view --> + <div id="detailsModal" class="modal"> + <div class="modal-content"> + <span class="close">×</span> + <h2>Return Details</h2> + <div id="modalContent"></div> + </div> + </div> - <div class="footer"> - This page will reset in <span id="countdown">5</span> seconds + <!-- Date Picker Modal --> + <div id="datePickerModal" class="modal"> + <div class="modal-content"> + <span class="close" onclick="closeDatePickerModal()">×</span> + <h2>Advanced Date Picker</h2> + <div class="date-picker-content"> + <div class="date-range-group"> + <label>Start Date:</label> + <input type="date" id="modalStartDate"> + </div> + <div class="date-range-group"> + <label>End Date:</label> + <input type="date" id="modalEndDate"> + </div> + <div class="preset-ranges"> + <h3>Preset Ranges</h3> + <div class="preset-buttons"> + <button onclick="selectPresetRange('last7days')">Last 7 Days</button> + <button onclick="selectPresetRange('last30days')">Last 30 Days</button> + <button onclick="selectPresetRange('last90days')">Last 90 Days</button> + <button onclick="selectPresetRange('lastYear')">Last Year</button> + </div> + </div> + <button class="apply-date-range" onclick="applyDateRange()">Apply Date Range</button> + </div> + </div> + </div> </div> - <script src="{{ url_for('static', filename='script.js') }}"></script> + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> + <script src="{{ url_for('static', filename='js/script.js') }}"></script> </body> </html> diff --git a/i2c.py b/i2c.py deleted file mode 100644 index 33319839dda48678f8f40a2eb1183f4c77cfeff6..0000000000000000000000000000000000000000 --- a/i2c.py +++ /dev/null @@ -1,88 +0,0 @@ -import sys -import time -import grovepi - -DISPLAY_RGB_ADDR = 0x30 -DISPLAY_TEXT_ADDR = 0x3e - -PIR_SENSOR_A_PIN = 3 -BUZZER_PIN = 4 - -if sys.platform == 'uwp': - import winrt_smbus as smbus - bus = smbus.SMBus(1) -else: - import smbus - import RPi.GPIO as GPIO - rev = GPIO.RPI_REVISION - if rev == 2 or rev == 3: - bus = smbus.SMBus(1) - else: - bus = smbus.SMBus(0) - - -def init_pins(): - grovepi.pinMode(PIR_SENSOR_A_PIN, "INPUT") - grovepi.pinMode(BUZZER_PIN, "OUTPUT") - - -def buzz(): - grovepi.digitalWrite(BUZZER_PIN, 1) - time.sleep(0.5) - grovepi.digitalWrite(BUZZER_PIN, 0) - - -def is_motion(): - return grovepi.digitalRead(PIR_SENSOR_A_PIN) - - -def setRGB(r, g, b): - """ - Controls the Grove LCD backlight color by writing to the - device at DISPLAY_RGB_ADDR. - """ - bus.write_byte_data(DISPLAY_RGB_ADDR, 0x04, 0x15) - - bus.write_byte_data(DISPLAY_RGB_ADDR, 0x06, r) - bus.write_byte_data(DISPLAY_RGB_ADDR, 0x07, g) - bus.write_byte_data(DISPLAY_RGB_ADDR, 0x08, b) - - -def textCommand(cmd): - """ - Sends a command byte to the LCD text command register at DISPLAY_TEXT_ADDR (0x80). - Used internally by setText() to configure display settings or move the cursor. - """ - bus.write_byte_data(DISPLAY_TEXT_ADDR, 0x80, cmd) - - -def setText(text): - """ - Clears the display, configures it for 2-line mode, then writes - up to 32 characters. If it hits 16 chars or a newline, - it moves to the second line. Excess text is ignored. - """ - # Clear display - textCommand(0x01) - time.sleep(0.05) - # Display on, no cursor - textCommand(0x08 | 0x04) - # 2-line mode - textCommand(0x28) - time.sleep(0.05) - - count = 0 - row = 0 - for c in text: - if c == '\n' or count == 16: - # Move to next line - count = 0 - row += 1 - if row == 2: # Only 2 lines available - break - textCommand(0xc0) # Move cursor to second line - if c == '\n': - continue - count += 1 - # Write character to the LCD data register (0x40) - bus.write_byte_data(DISPLAY_TEXT_ADDR, 0x40, ord(c)) diff --git a/main.py b/main.py deleted file mode 100644 index 9ae9e50a326675d1b76987d81c52951fa1c5add3..0000000000000000000000000000000000000000 --- a/main.py +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env python3 -import time -import i2c -import scan -import messages -import models -import emails -import numpy as np -from picamera import PiCamera -from pyzbar.pyzbar import decode -from PIL import Image -import sqlalchemy as db -from sqlalchemy.orm import sessionmaker - -# All in seconds -POLL_INTERVAL = 1 -MAX_IDLE_RESCANS = 6 -MAX_IDLE_DEPOSIT = 30 - - -def init_db_engine(): - engine = db.create_engine('sqlite:///local.db', echo=True) - models.Base.metadata.create_all(engine) - - return engine - - -def init_camera(): - """ - Initialises the camera. - """ - camera = PiCamera() - camera.resolution = (1920,1080) - camera.framerate = 24 - camera.start_preview(fullscreen=False,window=(100,200,640, 480)) - - return camera - - -def init_lcd(): - """ - Initialises the LCD screen. - """ - i2c.setRGB(0, 255, 0) - i2c.setText(messages.STATE_NO_CUSTOMER_MESSAGE) - - -def lookup_customer(engine, customer_id): - print(f'Locating CUSTOMER: {customer_id}') - - Session = sessionmaker(bind=engine) - session = Session() - - customer = session.query(models.Customer).filter_by(id = customer_id).first() - - return customer - - -def lookup_product(engine, product_id): - print(f'Locating PRODUCT: {product_id}') - - Session = sessionmaker(bind=engine) - session = Session() - - product = session.query(models.Product).filter_by(id = product_id).first() - - return product - - -def main(): - print('Initialising...') - - engine = init_db_engine() - camera = init_camera() - init_lcd() - i2c.init_pins() - - customer = None - - # Store the customer ID here to prevent double scanning. - customer_id = None - - product = None - returnObj = None - - rescan_count = 0 - - try: - while True: - if customer == None or product == None: - # In this state, we are missing one of two key pieces of information. - # This is the default state. - # - # We collect this information using the camera, so we must take a photo here - # while this state is true. - - # Capture the view of the camera. - camera.capture('tmp.jpg') - - # Open the image, - img = Image.open('tmp.jpg') - # and overwrite the type with the binary data of the image instead. - img = np.array(img) - - # Use pyzbar to extract a barcode from the image. - data = scan.detect_code(img) - - if customer == None: - # Default back to the initial state. - i2c.setRGB(0, 255, 0) - i2c.setText(messages.STATE_NO_CUSTOMER_MESSAGE) - - if data: - i2c.setText(messages.STATE_TRANSITION_MESSAGE) - - # Locate the customer's record - customer = lookup_customer(engine, data) - - if customer == None: - i2c.setRGB(255, 0, 0) - i2c.setText(messages.STATE_CUSTOMER_NOT_FOUND_MESSAGE) - - time.sleep(5) - else: - customer_id = data - rescan_count = 0 - i2c.buzz() - else: - # It must be the product we don't have then. - # Default back to the initial state. - i2c.setRGB(0, 255, 0) - i2c.setText(messages.STATE_NO_PRODUCT_MESSAGE) - - if rescan_count >= MAX_IDLE_RESCANS: - print("Product scan timed out after 6 attempted rescans.") - customer = None - customer_id = None - rescan_count = 0 - else: - rescan_count = rescan_count + 1 - - if data: - if data != customer_id: - i2c.setText(messages.STATE_TRANSITION_MESSAGE) - - # Locate the product's record. - product = lookup_product(engine, data) - - if product == None: - i2c.setRGB(255, 0, 0) - i2c.setText(messages.STATE_PRODUCT_NOT_FOUND_MESSAGE) - - time.sleep(5) - else: - i2c.buzz() - - # No other case needed, we wait for a barcode - time.sleep(POLL_INTERVAL) - else: - i2c.setRGB(0, 255, 0) - - if product.recycling_type == 0: - i2c.setText(messages.STATE_DEPOSIT_LEFT_BIN_MESSAGE) - else: - i2c.setText(messages.STATE_DEPOSIT_RIGHT_BIN_MESSAGE) - - # 30 seconds - deposit_window_end_time = time.time() + MAX_IDLE_DEPOSIT - while time.time() < deposit_window_end_time: - if i2c.is_motion(): - i2c.setText(messages.STATE_PRODUCT_DEPOSITED) - i2c.buzz() - time.sleep(5) - emails.send(customer.email, customer.first_name, 20, 200) - customer = None - customer_id = None - product = None - returnObj = None - rescan_count = 0 - - finally: - camera.stop_preview() - -if __name__ == '__main__': - main() diff --git a/messages.py b/messages.py deleted file mode 100644 index fcdb967085a51a9ab12a1b8c2e63bc241e058c33..0000000000000000000000000000000000000000 --- a/messages.py +++ /dev/null @@ -1,17 +0,0 @@ -STATE_TRANSITION_MESSAGE = 'One second...' -STATE_ERROR_MESSAGE = 'Sorry! Something went wrong!' - -STATE_NO_CUSTOMER_MESSAGE = 'Scan your Boots card to start.' -STATE_CUSTOMER_NOT_FOUND_MESSAGE = 'Card not found. Try again.' - -STATE_NO_PRODUCT_MESSAGE = 'Now, scan your empty product.' -STATE_PRODUCT_NOT_FOUND_MESSAGE = 'Product not found. Try again.' - -STATE_IDLE_MESSAGE = 'No activity detected. Signing out.' - -STATE_DEPOSIT_LEFT_BIN_MESSAGE = 'Put the item in the left bin.' -STATE_DEPOSIT_RIGHT_BIN_MESSAGE = 'Put the item in the left bin.' - -STATE_PRODUCT_DEPOSITED = 'Return complete. Check your email.' - -STATE_BIN_FULL_MESSAGE = "I'll be back! I need emptying!" \ No newline at end of file diff --git a/models.py b/models.py deleted file mode 100644 index 7d8841190ce2c7f5ba7661105104ee1b499053b2..0000000000000000000000000000000000000000 --- a/models.py +++ /dev/null @@ -1,35 +0,0 @@ -from datetime import datetime -from sqlalchemy import Integer, String, DateTime -from sqlalchemy.orm import DeclarativeBase -from sqlalchemy.orm import mapped_column - - -class Base(DeclarativeBase): - pass - - -class Customer(Base): - __tablename__ = "customers" - - id = mapped_column(String, primary_key=True) - first_name = mapped_column(String(50), nullable=False) - email = mapped_column(String, nullable=False) - - -class Product(Base): - __tablename__ = "products" - - id = mapped_column(String, primary_key=True) - name = mapped_column(String(50), nullable=False) - recycling_type = mapped_column(Integer, nullable=False) - reward_value = mapped_column(Integer, nullable=False) - - -class Return(Base): - __tablename__ = "returns" - - id = mapped_column(String, primary_key=True) - customer_id = mapped_column(String, nullable=False) - product_id = mapped_column(String, nullable=False) - return_date = mapped_column(DateTime, nullable=False, default=datetime.utcnow) - reward_value = mapped_column(Integer, nullable=False) diff --git a/scan.py b/scan.py deleted file mode 100644 index 2fa46379ae61fc79f688054f32e2e0c7458b4db6..0000000000000000000000000000000000000000 --- a/scan.py +++ /dev/null @@ -1,15 +0,0 @@ -from pyzbar.pyzbar import decode - - -def detect_code(frame): - """ - Detects and decodes QR codes and barcodes. - """ - decoded_objects = decode(frame) - for obj in decoded_objects: - barcode_data = obj.data.decode('utf-8') - barcode_type = obj.type - print(f"Saw {barcode_type}: {barcode_data}") - return barcode_data - - return None diff --git a/week5-work-fixed/.gitkeep b/week5-work-fixed/.gitkeep deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/week5-work-fixed/README.md b/week5-work-fixed/README.md deleted file mode 100644 index 3a1ec59b1828d6b72f9f3d62e10690be04d1eda8..0000000000000000000000000000000000000000 --- a/week5-work-fixed/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# CM2305 Group 17 Project - - - -## 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.cardiff.ac.uk/c23030734/cm2305-group-17-project.git -git branch -M main -git push -uf origin main -``` - -## Integrate with your tools - -- [ ] [Set up project integrations](https://git.cardiff.ac.uk/c23030734/cm2305-group-17-project/-/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. diff --git a/week5-work-fixed/addproduct.py b/week5-work-fixed/addproduct.py deleted file mode 100644 index cdbf567161d6b4b538241cf35216e52fb3daab4b..0000000000000000000000000000000000000000 --- a/week5-work-fixed/addproduct.py +++ /dev/null @@ -1,15 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -from models import Product, Base - -DATABASE_URL = 'sqlite:///local.db' - -engine = create_engine(DATABASE_URL) -Session = sessionmaker(bind=engine) -session = Session() - -new_product = Product(id="0765756931038", name="Mouse Box", recycling_type="Cardboard", reward_value=5) -session.add(new_product) -session.commit() - -print("Done") \ No newline at end of file diff --git a/week5-work-fixed/adduser.py b/week5-work-fixed/adduser.py deleted file mode 100644 index 2d939af7671b399cb61df5afd4779f76abed6eca..0000000000000000000000000000000000000000 --- a/week5-work-fixed/adduser.py +++ /dev/null @@ -1,15 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -from models import Customer, Base - -DATABASE_URL = 'sqlite:///local.db' - -engine = create_engine(DATABASE_URL) -Session = sessionmaker(bind=engine) -session = Session() - -new_customer = Customer(id="0600077786", first_name="Jeyan", email="kanagaratnamj@cardiff.ac.uk") -session.add(new_customer) -session.commit() - -print("Done") \ No newline at end of file diff --git a/week5-work-fixed/i2c.py b/week5-work-fixed/i2c.py deleted file mode 100644 index c930babdca5644558ab649822605352ad12d325f..0000000000000000000000000000000000000000 --- a/week5-work-fixed/i2c.py +++ /dev/null @@ -1,69 +0,0 @@ -import sys -import time - -DISPLAY_RGB_ADDR = 0x30 -DISPLAY_TEXT_ADDR = 0x3e - -if sys.platform == 'uwp': - import winrt_smbus as smbus - bus = smbus.SMBus(1) -else: - import smbus - import RPi.GPIO as GPIO - rev = GPIO.RPI_REVISION - if rev == 2 or rev == 3: - bus = smbus.SMBus(1) - else: - bus = smbus.SMBus(0) - - -def setRGB(r, g, b): - """ - Controls the Grove LCD backlight color by writing to the - device at DISPLAY_RGB_ADDR. - """ - bus.write_byte_data(DISPLAY_RGB_ADDR, 0x04, 0x15) - - bus.write_byte_data(DISPLAY_RGB_ADDR, 0x06, r) - bus.write_byte_data(DISPLAY_RGB_ADDR, 0x07, g) - bus.write_byte_data(DISPLAY_RGB_ADDR, 0x08, b) - - -def textCommand(cmd): - """ - Sends a command byte to the LCD text command register at DISPLAY_TEXT_ADDR (0x80). - Used internally by setText() to configure display settings or move the cursor. - """ - bus.write_byte_data(DISPLAY_TEXT_ADDR, 0x80, cmd) - - -def setText(text): - """ - Clears the display, configures it for 2-line mode, then writes - up to 32 characters. If it hits 16 chars or a newline, - it moves to the second line. Excess text is ignored. - """ - # Clear display - textCommand(0x01) - time.sleep(0.05) - # Display on, no cursor - textCommand(0x08 | 0x04) - # 2-line mode - textCommand(0x28) - time.sleep(0.05) - - count = 0 - row = 0 - for c in text: - if c == '\n' or count == 16: - # Move to next line - count = 0 - row += 1 - if row == 2: # Only 2 lines available - break - textCommand(0xc0) # Move cursor to second line - if c == '\n': - continue - count += 1 - # Write character to the LCD data register (0x40) - bus.write_byte_data(DISPLAY_TEXT_ADDR, 0x40, ord(c)) diff --git a/week5-work-fixed/local.db b/week5-work-fixed/local.db deleted file mode 100644 index 5107cccd7ff6dd0a2115a3d984ec1346160ec679..0000000000000000000000000000000000000000 Binary files a/week5-work-fixed/local.db and /dev/null differ diff --git a/week5-work-fixed/main.py b/week5-work-fixed/main.py deleted file mode 100644 index d9968b77a81bf254c59dd74214c8838f88141e21..0000000000000000000000000000000000000000 --- a/week5-work-fixed/main.py +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env python3 -import time -import grovepi -import i2c -import scan -import messages -import models -import numpy as np -from picamera import PiCamera -from pyzbar.pyzbar import decode -from PIL import Image -import sqlalchemy as db -from sqlalchemy.orm import sessionmaker - -# Sensor ---------- -pir_sensor = 3 -buzzer =4 - -grovepi.pinMode(pir_sensor, "INPUT") -grovepi.pinMode(buzzer, "OUTPUT") - -print("Starting sensors") -time.sleep(2) -# -------------------- - -POLL_INTERVAL = 1 # Screenshot every second - - -def init_db_engine(): - engine = db.create_engine('sqlite:///local.db', echo=True) - models.Base.metadata.create_all(engine) - - return engine - - -def init_camera(): - """ - Initialises the camera. - """ - camera = PiCamera() - camera.resolution = (1920,1080) - camera.framerate = 24 - camera.start_preview(fullscreen=False,window=(100,200,800,600)) - - return camera - -def beep(): - grovepi.digitalWrite(buzzer, 1) - time.sleep(0.5) - grovepi.digitalWrite(buzzer, 0) - - -def init_lcd(): - """ - Initialises the LCD screen. - """ - i2c.setRGB(0, 255, 0) - i2c.setText(messages.STATE_NO_CUSTOMER_MESSAGE) - - -def lookup_customer(engine, customer_id): - print(f'Locating CUSTOMER: {customer_id}') - - Session = sessionmaker(bind=engine) - session = Session() - - customer = session.query(models.Customer).filter_by(id = customer_id).first() - - # Name output (for testing adduser.py) - if customer: - print(f'Customer Found: {customer.first_name}') - else: - print('Customer not found') - - return customer - - -def lookup_product(engine, product_id): - print(f'Locating PRODUCT: {product_id}') - - Session = sessionmaker(bind=engine) - session = Session() - - product = session.query(models.Product).filter_by(id = product_id).first() - - if product: - print(f'Product Found: {product.name}, {product.recycling_type}') - else: - print('Customer not found') - - return product - - -def main(): - print('Initialising...') - - engine = init_db_engine() - camera = init_camera() - init_lcd() - - customer = None - - # Store the customer ID here to prevent double scanning. - customer_id = None - - product = None - - manualcount = 0 - - returnObj = None - - try: - while True: - if customer == None or product == None: - # In this state, we are missing one of two key pieces of information. - # This is the default state. - # - # We collect this information using the camera, so we must take a photo here - # while this state is true. - - # Capture the view of the camera. - camera.capture('tmp.jpg') - - # Open the image, - img = Image.open('tmp.jpg') - # and overwrite the type with the binary data of the image instead. - img = np.array(img) - - # Use pyzbar to extract a barcode from the image. - data = scan.detect_code(img) - - if customer == None: - # Default back to the initial state. - i2c.setRGB(0, 255, 0) - i2c.setText(messages.STATE_NO_CUSTOMER_MESSAGE) - - if data: - i2c.setText(messages.STATE_TRANSITION_MESSAGE) - - # Locate the customer's record - customer = lookup_customer(engine, data) - - if customer == None: - i2c.setRGB(255, 0, 0) - i2c.setText(messages.STATE_CUSTOMER_NOT_FOUND_MESSAGE) - - time.sleep(5) - else: - customer_id = data - beep() - manualcount = 0 - - else: - # It must be the product we don't have then. - # Default back to the initial state. - i2c.setRGB(0, 255, 0) - i2c.setText(messages.STATE_NO_PRODUCT_MESSAGE) - - # Capture the view of the camera. - manualcount = manualcount + 1 - camera.capture('tmp.jpg') - - # Open the image, - img = Image.open('tmp.jpg') - # and overwrite the type with the binary data of the image instead. - img = np.array(img) - - # Use pyzbar to extract a barcode from the image. - data = scan.detect_code(img) - - - - if manualcount > 6: - print("User time out") # Debug message for terminal - i2c.setRGB(255, 0, 0) - i2c.setText(messages.STATE_IDLE_MESSAGE) - time.sleep(3) - customer = None - customer_id = None - manualcount =0 - continue - - elif data and data != customer_id: - - i2c.setText(messages.STATE_TRANSITION_MESSAGE) - - # Locate the product's record. - product = lookup_product(engine, data) - - if product == None: - print("Not recognised") - STATE_PRODUCT_NOT_FOUND_MESSAGE = 'Product not found. Try again.' - - time.sleep(3) - else: - print("Product found") - i2c.setText(messages.STATE_PRODUCT_FOUND_MESSAGE) - beep() - product_id = data - manualcount = 0 - - try: - while True: - if grovepi.digitalRead(pir_sensor): - print("Detected") - i2c.setText(messages.STATE_PRODUCT_DEPOSITED) - beep() - else: - print("Not") - - time.sleep(1) - - except KeyboardInterrupt: - print("OOPS") - - - # No other case needed, we wait for a barcode - - time.sleep(POLL_INTERVAL) - finally: - camera.stop_preview() - -if __name__ == '__main__': - main() diff --git a/week5-work-fixed/messages.py b/week5-work-fixed/messages.py deleted file mode 100644 index ef87fe6ad4354b0c4a7e8a0058e55228c655d249..0000000000000000000000000000000000000000 --- a/week5-work-fixed/messages.py +++ /dev/null @@ -1,14 +0,0 @@ -STATE_TRANSITION_MESSAGE = 'One second...' -STATE_ERROR_MESSAGE = 'Sorry! Something went wrong!' - -STATE_NO_CUSTOMER_MESSAGE = 'Scan your Boots card to start.' -STATE_CUSTOMER_NOT_FOUND_MESSAGE = 'Card not found. Try again.' - -STATE_NO_PRODUCT_MESSAGE = 'Now, scan your empty product.' -STATE_IDLE_MESSAGE = 'No activity detected. Signing out.' -STATE_PRODUCT_FOUND_MESSAGE = 'Product found.' -STATE_PRODUCT_NOT_FOUND_MESSAGE = 'Product not found. Try again.' - -STATE_PRODUCT_DEPOSITED = 'Product successfully deposited!' - -STATE_BIN_FULL_MESSAGE = "I'll be back! I need emptying!" \ No newline at end of file diff --git a/week5-work-fixed/models.py b/week5-work-fixed/models.py deleted file mode 100644 index b6f50e72f9c57db676b1bb18077762e2d0e4a812..0000000000000000000000000000000000000000 --- a/week5-work-fixed/models.py +++ /dev/null @@ -1,35 +0,0 @@ -from datetime import datetime -from sqlalchemy import Integer, String, DateTime -from sqlalchemy.orm import DeclarativeBase -from sqlalchemy.orm import mapped_column - - -class Base(DeclarativeBase): - pass - - -class Customer(Base): - __tablename__ = "customers" - - id = mapped_column(String, primary_key=True) - first_name = mapped_column(String(50), nullable=False) - email = mapped_column(String, nullable=False) - - -class Product(Base): - __tablename__ = "products" - - id = mapped_column(String, primary_key=True) - name = mapped_column(String(50), nullable=False) - recycling_type = mapped_column(String, nullable=False) - reward_value = mapped_column(Integer, nullable=False) - - -class Return(Base): - __tablename__ = "returns" - - id = mapped_column(String, primary_key=True) - customer_id = mapped_column(String, nullable=False) - product_id = mapped_column(String, nullable=False) - return_date = mapped_column(DateTime, nullable=False, default=datetime.utcnow) - reward_value = mapped_column(Integer, nullable=False) diff --git a/week5-work-fixed/scan.py b/week5-work-fixed/scan.py deleted file mode 100644 index 2fa46379ae61fc79f688054f32e2e0c7458b4db6..0000000000000000000000000000000000000000 --- a/week5-work-fixed/scan.py +++ /dev/null @@ -1,15 +0,0 @@ -from pyzbar.pyzbar import decode - - -def detect_code(frame): - """ - Detects and decodes QR codes and barcodes. - """ - decoded_objects = decode(frame) - for obj in decoded_objects: - barcode_data = obj.data.decode('utf-8') - barcode_type = obj.type - print(f"Saw {barcode_type}: {barcode_data}") - return barcode_data - - return None diff --git a/week5-work-fixed/tmp.jpg b/week5-work-fixed/tmp.jpg deleted file mode 100644 index fb102fce3ad38388a2a5cdd1d51fdd343a70d84e..0000000000000000000000000000000000000000 Binary files a/week5-work-fixed/tmp.jpg and /dev/null differ diff --git a/week5-work/.gitkeep b/week5-work/.gitkeep deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/week5-work/addproduct.py b/week5-work/addproduct.py deleted file mode 100644 index cdbf567161d6b4b538241cf35216e52fb3daab4b..0000000000000000000000000000000000000000 --- a/week5-work/addproduct.py +++ /dev/null @@ -1,15 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -from models import Product, Base - -DATABASE_URL = 'sqlite:///local.db' - -engine = create_engine(DATABASE_URL) -Session = sessionmaker(bind=engine) -session = Session() - -new_product = Product(id="0765756931038", name="Mouse Box", recycling_type="Cardboard", reward_value=5) -session.add(new_product) -session.commit() - -print("Done") \ No newline at end of file diff --git a/week5-work/adduser.py b/week5-work/adduser.py deleted file mode 100644 index 2d939af7671b399cb61df5afd4779f76abed6eca..0000000000000000000000000000000000000000 --- a/week5-work/adduser.py +++ /dev/null @@ -1,15 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -from models import Customer, Base - -DATABASE_URL = 'sqlite:///local.db' - -engine = create_engine(DATABASE_URL) -Session = sessionmaker(bind=engine) -session = Session() - -new_customer = Customer(id="0600077786", first_name="Jeyan", email="kanagaratnamj@cardiff.ac.uk") -session.add(new_customer) -session.commit() - -print("Done") \ No newline at end of file diff --git a/week5-work/i2c.py b/week5-work/i2c.py deleted file mode 100644 index c930babdca5644558ab649822605352ad12d325f..0000000000000000000000000000000000000000 --- a/week5-work/i2c.py +++ /dev/null @@ -1,69 +0,0 @@ -import sys -import time - -DISPLAY_RGB_ADDR = 0x30 -DISPLAY_TEXT_ADDR = 0x3e - -if sys.platform == 'uwp': - import winrt_smbus as smbus - bus = smbus.SMBus(1) -else: - import smbus - import RPi.GPIO as GPIO - rev = GPIO.RPI_REVISION - if rev == 2 or rev == 3: - bus = smbus.SMBus(1) - else: - bus = smbus.SMBus(0) - - -def setRGB(r, g, b): - """ - Controls the Grove LCD backlight color by writing to the - device at DISPLAY_RGB_ADDR. - """ - bus.write_byte_data(DISPLAY_RGB_ADDR, 0x04, 0x15) - - bus.write_byte_data(DISPLAY_RGB_ADDR, 0x06, r) - bus.write_byte_data(DISPLAY_RGB_ADDR, 0x07, g) - bus.write_byte_data(DISPLAY_RGB_ADDR, 0x08, b) - - -def textCommand(cmd): - """ - Sends a command byte to the LCD text command register at DISPLAY_TEXT_ADDR (0x80). - Used internally by setText() to configure display settings or move the cursor. - """ - bus.write_byte_data(DISPLAY_TEXT_ADDR, 0x80, cmd) - - -def setText(text): - """ - Clears the display, configures it for 2-line mode, then writes - up to 32 characters. If it hits 16 chars or a newline, - it moves to the second line. Excess text is ignored. - """ - # Clear display - textCommand(0x01) - time.sleep(0.05) - # Display on, no cursor - textCommand(0x08 | 0x04) - # 2-line mode - textCommand(0x28) - time.sleep(0.05) - - count = 0 - row = 0 - for c in text: - if c == '\n' or count == 16: - # Move to next line - count = 0 - row += 1 - if row == 2: # Only 2 lines available - break - textCommand(0xc0) # Move cursor to second line - if c == '\n': - continue - count += 1 - # Write character to the LCD data register (0x40) - bus.write_byte_data(DISPLAY_TEXT_ADDR, 0x40, ord(c)) diff --git a/week5-work/local.db b/week5-work/local.db deleted file mode 100644 index 5107cccd7ff6dd0a2115a3d984ec1346160ec679..0000000000000000000000000000000000000000 Binary files a/week5-work/local.db and /dev/null differ diff --git a/week5-work/main.py b/week5-work/main.py deleted file mode 100644 index 70f894d03caa298741f1d6676461ca373ca86eac..0000000000000000000000000000000000000000 --- a/week5-work/main.py +++ /dev/null @@ -1,204 +0,0 @@ -#!/usr/bin/env python3 -import time -import grovepi -import i2c -import scan -import messages -import models -import numpy as np -from picamera import PiCamera -from pyzbar.pyzbar import decode -from PIL import Image -import sqlalchemy as db -from sqlalchemy.orm import sessionmaker - -POLL_INTERVAL = 1 # Screenshot every second - - -def init_db_engine(): - engine = db.create_engine('sqlite:///local.db', echo=True) - models.Base.metadata.create_all(engine) - - return engine - - -def init_camera(): - """ - Initialises the camera. - """ - camera = PiCamera() - camera.resolution = (1920,1080) - camera.framerate = 24 - camera.start_preview(fullscreen=False,window=(100,200,800,600)) - - return camera - - -def init_lcd(): - """ - Initialises the LCD screen. - """ - i2c.setRGB(0, 255, 0) - i2c.setText(messages.STATE_NO_CUSTOMER_MESSAGE) - - -def lookup_customer(engine, customer_id): - print(f'Locating CUSTOMER: {customer_id}') - - Session = sessionmaker(bind=engine) - session = Session() - - customer = session.query(models.Customer).filter_by(id = customer_id).first() - - # Name output (for testing adduser.py) - if customer: - print(f'Customer Found: {customer.first_name}') - else: - print('Customer not found') - - return customer - - -def lookup_product(engine, product_id): - print(f'Locating PRODUCT: {product_id}') - - Session = sessionmaker(bind=engine) - session = Session() - - product = session.query(models.Product).filter_by(id = product_id).first() - - if product: - print(f'Product Found: {product.name}, {product.recycling_type}') - else: - print('Customer not found') - - return product - - -def main(): - print('Initialising...') - - engine = init_db_engine() - camera = init_camera() - init_lcd() - - customer = None - - # Store the customer ID here to prevent double scanning. - customer_id = None - - product = None - - manualcount = 0 - prodfound = False - - returnObj = None - - try: - while True: - if customer == None or product == None: - # In this state, we are missing one of two key pieces of information. - # This is the default state. - # - # We collect this information using the camera, so we must take a photo here - # while this state is true. - - # Capture the view of the camera. - camera.capture('tmp.jpg') - - # Open the image, - img = Image.open('tmp.jpg') - # and overwrite the type with the binary data of the image instead. - img = np.array(img) - - # Use pyzbar to extract a barcode from the image. - data = scan.detect_code(img) - - if customer == None: - # Default back to the initial state. - i2c.setRGB(0, 255, 0) - i2c.setText(messages.STATE_NO_CUSTOMER_MESSAGE) - - if data: - i2c.setText(messages.STATE_TRANSITION_MESSAGE) - - # Locate the customer's record - customer = lookup_customer(engine, data) - - if customer == None: - i2c.setRGB(255, 0, 0) - i2c.setText(messages.STATE_CUSTOMER_NOT_FOUND_MESSAGE) - - time.sleep(5) - else: - customer_id = data - - else: - # It must be the product we don't have then. - # Default back to the initial state. - i2c.setRGB(0, 255, 0) - i2c.setText(messages.STATE_NO_PRODUCT_MESSAGE) - - # Capture the view of the camera. - camera.capture('tmp.jpg') - - # Open the image, - img = Image.open('tmp.jpg') - # and overwrite the type with the binary data of the image instead. - img = np.array(img) - - # Use pyzbar to extract a barcode from the image. - data = scan.detect_code(img) - - manualcount = manualcount + 1 - if manualcount > 20: - print("User time out") # Debug message for terminal - i2c.setRGB(255, 0, 0) - i2c.setText(messages.STATE_IDLE_MESSAGE) - time.sleep(3) - customer = None - customer_id = None - - elif data and data != customer_id: - - i2c.setText(messages.STATE_TRANSITION_MESSAGE) - - # Locate the product's record. - product = lookup_product(engine, data) - - if product == None: - print("Not recognised") - STATE_PRODUCT_NOT_FOUND_MESSAGE = 'Product not found. Try again.' - - time.sleep(3) - else: - print("Product found") - i2c.setText(messages.STATE_PRODUCT_FOUND_MESSAGE) - prodfound = True - product_id = data - - - """ - if (time.time() - start_time) > 20 and prodfound == False: - print("User time out") # Debug message for terminal - i2c.setRGB(255, 0, 0) - i2c.setText(messages.STATE_IDLE_MESSAGE) - time.sleep(3) - customer = None - customer_id = None - """ - - - - - - - - # No other case needed, we wait for a barcode - - time.sleep(POLL_INTERVAL) - finally: - camera.stop_preview() - -if __name__ == '__main__': - main() diff --git a/week5-work/messages.py b/week5-work/messages.py deleted file mode 100644 index 29be99bdc2f5c17709e0757de4bbf0a9983e0761..0000000000000000000000000000000000000000 --- a/week5-work/messages.py +++ /dev/null @@ -1,12 +0,0 @@ -STATE_TRANSITION_MESSAGE = 'One second...' -STATE_ERROR_MESSAGE = 'Sorry! Something went wrong!' - -STATE_NO_CUSTOMER_MESSAGE = 'Scan your Boots card to start.' -STATE_CUSTOMER_NOT_FOUND_MESSAGE = 'Card not found. Try again.' - -STATE_NO_PRODUCT_MESSAGE = 'Now, scan your empty product.' -STATE_IDLE_MESSAGE = 'No activity detected. Signing out.' -STATE_PRODUCT_FOUND_MESSAGE = 'Product found.' -STATE_PRODUCT_NOT_FOUND_MESSAGE = 'Product not found. Try again.' - -STATE_BIN_FULL_MESSAGE = "I'll be back! I need emptying!" \ No newline at end of file diff --git a/week5-work/models.py b/week5-work/models.py deleted file mode 100644 index b6f50e72f9c57db676b1bb18077762e2d0e4a812..0000000000000000000000000000000000000000 --- a/week5-work/models.py +++ /dev/null @@ -1,35 +0,0 @@ -from datetime import datetime -from sqlalchemy import Integer, String, DateTime -from sqlalchemy.orm import DeclarativeBase -from sqlalchemy.orm import mapped_column - - -class Base(DeclarativeBase): - pass - - -class Customer(Base): - __tablename__ = "customers" - - id = mapped_column(String, primary_key=True) - first_name = mapped_column(String(50), nullable=False) - email = mapped_column(String, nullable=False) - - -class Product(Base): - __tablename__ = "products" - - id = mapped_column(String, primary_key=True) - name = mapped_column(String(50), nullable=False) - recycling_type = mapped_column(String, nullable=False) - reward_value = mapped_column(Integer, nullable=False) - - -class Return(Base): - __tablename__ = "returns" - - id = mapped_column(String, primary_key=True) - customer_id = mapped_column(String, nullable=False) - product_id = mapped_column(String, nullable=False) - return_date = mapped_column(DateTime, nullable=False, default=datetime.utcnow) - reward_value = mapped_column(Integer, nullable=False) diff --git a/week5-work/scan.py b/week5-work/scan.py deleted file mode 100644 index 2fa46379ae61fc79f688054f32e2e0c7458b4db6..0000000000000000000000000000000000000000 --- a/week5-work/scan.py +++ /dev/null @@ -1,15 +0,0 @@ -from pyzbar.pyzbar import decode - - -def detect_code(frame): - """ - Detects and decodes QR codes and barcodes. - """ - decoded_objects = decode(frame) - for obj in decoded_objects: - barcode_data = obj.data.decode('utf-8') - barcode_type = obj.type - print(f"Saw {barcode_type}: {barcode_data}") - return barcode_data - - return None diff --git a/week5-work/tmp.jpg b/week5-work/tmp.jpg deleted file mode 100644 index 5352f2c327c8131cad135f2b57569b5fd8b82d58..0000000000000000000000000000000000000000 Binary files a/week5-work/tmp.jpg and /dev/null differ