diff --git a/proxy/Dockerfile b/proxy/Dockerfile new file mode 100644 index 0000000..814d9c6 --- /dev/null +++ b/proxy/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.11-slim-bookworm + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install -r requirements.txt + +EXPOSE 5009 + +COPY . . + +CMD [ "python", "app.py" ] diff --git a/proxy/app.py b/proxy/app.py new file mode 100644 index 0000000..4dec679 --- /dev/null +++ b/proxy/app.py @@ -0,0 +1,57 @@ +import json +import time + +import cherrypy +import requests + +from mock_data import get_mock_data + +# тестовый режим с возможностью заказа питсы с гравием(!) +MOCK = False + +API_URL = 'https://api.papajohns.ru' + + +class PapaJohnsProxy(object): + @cherrypy.expose + def default(self, *args, **kwargs): + path = cherrypy.request.path_info + method = cherrypy.request.method + headers = dict(cherrypy.request.headers) + query_string = cherrypy.request.query_string + data = cherrypy.request.body.read() if cherrypy.request.body.length else None + + if MOCK: + time.sleep(1) + return json.dumps(get_mock_data(path)) + + url = f'{API_URL}{path}' + + if query_string: + url += f'?{query_string}' + + headers_to_forward = { + k: v for k, v in headers.items() + if k.lower() not in ['host', 'content-length'] + } + + try: + response = requests.request( + method=method, + url=url, + headers=headers_to_forward, + data=data, + timeout=10 + ) + + cherrypy.response.status = response.status_code + return response.text + + except requests.exceptions.RequestException as e: + cherrypy.response.status = 500 + return json.dumps({"error": str(e)}) + + +if __name__ == '__main__': + cherrypy.config.update({'server.socket_host': '0.0.0.0', 'server.socket_port': 5009}) + cherrypy.quickstart(PapaJohnsProxy()) diff --git a/proxy/docker-compose.yml b/proxy/docker-compose.yml new file mode 100644 index 0000000..600527e --- /dev/null +++ b/proxy/docker-compose.yml @@ -0,0 +1,7 @@ +services: + pizza-proxy: + build: . + container_name: pizza-proxy + ports: + - 5009:5009 + restart: unless-stopped diff --git a/proxy/mock_data.py b/proxy/mock_data.py new file mode 100644 index 0000000..ef72882 --- /dev/null +++ b/proxy/mock_data.py @@ -0,0 +1,222 @@ +def get_mock_data(path: str) -> dict: + if '/order/save' in path: + return ORDER_SUCCESS + return ORDER_FAIL + if '/order/status' in path: + if ORDER_STATUS["order_status"] < 5: + ORDER_STATUS["order_status"] += 1 + return ORDER_STATUS + if '/cart/add' in path: + ORDER_STATUS["order_status"] = -1 + return CART_ADD + if '/catalog/category-goods' in path: + return PIZZAS + + +CART_ADD = { + "unauthorized_token": "7904e2634334760a642a169c0f7c67f0", + "cart_id": 123456789, + "composition": [ + { + "item": { + "name": "С ананасами" + } + } + ] +} + + +ORDER_FAIL = { + "success": False, + "message": { + "composition": [ + "Минимальная стоимость заказа на доставку 99999 ₽" + ] + }, + "status": 400 +} + +ORDER_SUCCESS = { + "order_id": 69420 +} + + +ORDER_STATUS = { + "order_status": -1 +} + + +PIZZAS = [ + { + "goods": [ + { + "id": 1234, + "name": "С ананасами", + "variations": [ + { + "id": 123430, + "price": 599, + "kind": { + "id": 3 + }, + "stuffed_crust": "none", + "size": { + "value": 23 + } + }, + { + "id": 123431, + "price": 879, + "kind": { + "id": 3 + }, + "stuffed_crust": "none", + "size": { + "value": 30 + } + }, + { + "id": 123432, + "price": 1079, + "kind": { + "id": 3 + }, + "stuffed_crust": "none", + "size": { + "value": 35 + } + }, + { + "id": 123433, + "price": 1379, + "kind": { + "id": 3 + }, + "stuffed_crust": "none", + "size": { + "value": 40 + } + }, + { + "id": 123410, + "price": 879, + "kind": { + "id": 1 + }, + "stuffed_crust": "none", + "size": { + "value": 30 + } + }, + { + "id": 123411, + "price": 1079, + "kind": { + "id": 1 + }, + "stuffed_crust": "none", + "size": { + "value": 35 + } + }, + { + "id": 123412, + "price": 1379, + "kind": { + "id": 1 + }, + "stuffed_crust": "none", + "size": { + "value": 40 + } + } + ], + "good_type": "promotional" + }, + { + "id": 4321, + "name": "С гравием", + "variations": [ + { + "id": 223430, + "price": 429, + "kind": { + "id": 3 + }, + "stuffed_crust": "none", + "size": { + "value": 23 + } + }, + { + "id": 223431, + "price": 429, + "kind": { + "id": 3 + }, + "stuffed_crust": "none", + "size": { + "value": 30 + } + }, + { + "id": 223432, + "price": 4279, + "kind": { + "id": 3 + }, + "stuffed_crust": "none", + "size": { + "value": 35 + } + }, + { + "id": 223433, + "price": 4279, + "kind": { + "id": 3 + }, + "stuffed_crust": "none", + "size": { + "value": 40 + } + }, + { + "id": 223410, + "price": 429, + "kind": { + "id": 1 + }, + "stuffed_crust": "none", + "size": { + "value": 30 + } + }, + { + "id": 223411, + "price": 4279, + "kind": { + "id": 1 + }, + "stuffed_crust": "none", + "size": { + "value": 35 + } + }, + { + "id": 223412, + "price": 4279, + "kind": { + "id": 1 + }, + "stuffed_crust": "none", + "size": { + "value": 40 + } + } + ], + "good_type": "promotional" + } + ] + } +] diff --git a/proxy/requirements.txt b/proxy/requirements.txt new file mode 100644 index 0000000..4f13e18 --- /dev/null +++ b/proxy/requirements.txt @@ -0,0 +1,2 @@ +CherryPy==18.9.0 +requests==2.31.0 diff --git a/scripts/config_copy.py b/scripts/config_copy.py new file mode 100644 index 0000000..3b91bb1 --- /dev/null +++ b/scripts/config_copy.py @@ -0,0 +1,6 @@ +Import('env') +import os +import shutil + +if not os.path.isfile("include/config.h"): + shutil.copy("config.example", "include/config.h") diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..5c2bffb --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,158 @@ +#include +#include +#include + +#include "parser.h" +#include "requests.h" +#include "config.h" +#include "menu.h" + + +ArudinoStreamParser parser; +PizzaHandler handler; + +Request r; +uint32_t order_id; +uint32_t trackLastUpdate; +char unauthorized_token[33]; + + +void setup() { + WiFi.disconnect(); + delay(100); + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); + Menu &menu = Menu::getInstance(); + while (WiFi.status() != WL_CONNECTED) { + menu.tick(); + delay(100); + } + + menu.setMenuPage(Menu::PRELOAD); + parser.setHandler(&handler); + char path[70]; + sprintf(path, "/catalog/category-goods?category=pizza&page_size=100&city_id=%d", CITY_ID); + r.request("GET", path, parser); +} + +bool addToCart(uint32_t pizzaId) { + JsonDocument add_pizza; + + JsonObject composition = add_pizza["composition"].add(); + composition["good_id"] = pizzaId; + composition["type"] = "good"; + add_pizza["city_id"] = CITY_ID; + + char payload[350]; + + serializeJson(add_pizza, payload); + char* responce = r.request("POST", "/cart/add", payload); + + + JsonDocument pizza_add_responce; + + DeserializationError error = deserializeJson(pizza_add_responce, responce); + free(responce); + + if (error) { + pizza_add_responce.clear(); + return 1; + } + + strcpy(unauthorized_token, pizza_add_responce["unauthorized_token"]); + + pizza_add_responce.clear(); + return 0; +} + +bool placeOrder() { + JsonDocument order; + + + JsonObject user_data = order["user_data"].to(); + user_data["username"] = ORDER_NAME; + user_data["phone"] = ORDER_PHONE; + user_data["email"] = ORDER_EMAIL; + user_data["subscription_state"] = false; + user_data["sms_state"] = false; + + JsonObject address_coordinates = order["address_coordinates"]["coordinates"].to(); + address_coordinates["latitude"] = ORDER_GPS_LAT; + address_coordinates["longitude"] = ORDER_GPS_LON; + + order["unauthorized_token"] = unauthorized_token; + order["pay_type"] = "card_to_courier"; + order["city_id"] = CITY_ID; + #ifdef ORDER_RESTAURANT_ID + order["restaurant_id"] = ORDER_RESTAURANT_ID; + #endif + + + char payload[350]; + + serializeJson(order, payload); + + char* responce = r.request("POST", "/order/save", payload); + JsonDocument order_place_responce; + DeserializationError error = deserializeJson(order_place_responce, responce); + free(responce); + if (error) { + order_place_responce.clear(); + return 1; + } else if (order_place_responce["success"] == false) { + order_place_responce.clear(); + return 1; + } + order_id = order_place_responce["order_id"]; + order_place_responce.clear(); + return 0; +} + +Menu::TrackingStatus getStatus() { + char path[100]; + sprintf(path, "/order/status?city_id=%d&unauthorized_token=%s&order_id=%d", CITY_ID, unauthorized_token, order_id); + char* responce = r.request("GET", path); + JsonDocument orderStatus; + + DeserializationError error = deserializeJson(orderStatus, responce); + free(responce); + + if (error) { + return Menu::UNKNOWN; + } + + Menu::TrackingStatus status = static_cast(orderStatus["order_status"]); + orderStatus.clear(); + return status; +} + +void loop() { + Menu &menu = Menu::getInstance(); + menu.tick(); + + if (menu.getMenuPage() == Menu::PLACING_ORDER) { + // питса выбрана, можно заказывать + uint32_t selectedPizzaId = menu.getSelectedPizzaId(); + + bool error = addToCart(selectedPizzaId); + if (error) { + menu.setMenuPage(Menu::PIZZA_SELECT); + return; + } + + error = placeOrder(); + if (error) { + menu.setMenuPage(Menu::PIZZA_SELECT); + return; + } + menu.setMenuPage(Menu::TRACKING); + + } else if (menu.getMenuPage() == Menu::TRACKING) { + if (millis() - trackLastUpdate > TRACK_UPDATE_INTERVAL) { + Menu::TrackingStatus status = getStatus(); + if (status != Menu::UNKNOWN) { + menu.setTrackingStatus(status); + } + trackLastUpdate = millis(); + } + } +} \ No newline at end of file diff --git a/src/menu.cpp b/src/menu.cpp new file mode 100644 index 0000000..849d6cf --- /dev/null +++ b/src/menu.cpp @@ -0,0 +1,349 @@ +#include "menu.h" + + +Menu::Menu() : lcd(0x3F, 16, 2), enc(ENCPIN1, ENCPIN2, ENCBTNPIN) { + Wire.begin(4, 5); + lcd.init(); + lcd.backlight(); + pinMode(BRBPIN, INPUT_PULLUP); + pinMode(LEDPIN, OUTPUT); +} + + +void Menu::setMenuPage(Page page) { + switch (page) { + case PRELOAD: + case PIZZA_SELECT: + lcd.cursor_off(); + menuPage = page; + break; + case DOUGH_SELECT: + if (menuPage != PIZZA_SELECT && menuPage != SIZE_SELECT) return; + selectedDough = Pizzas::TRADITIONAL; + pizzaSizesIndex = 0; + menuPage = page; + initVariations(); + break; + case SIZE_SELECT: + if (menuPage != DOUGH_SELECT) return; + menuPage = page; + break; + case CONFIRMATION: + if (menuPage != SIZE_SELECT) return; + menuPage = page; + break; + case PLACING_ORDER: { + if (menuPage != CONFIRMATION) return; + lcd.cursor_off(); + Pizzas::Variation var = pizzaSizes[pizzaSizesIndex]; + selectedPizzaId = var.id; + menuPage = page; + break; + } + case TRACKING: + if (menuPage != PLACING_ORDER) return; + menuPage = page; + break; + } + draw(); +}; + +void Menu::setTrackingStatus(TrackingStatus new_status) { + if (trackingStatus == new_status) return; + trackingStatus = new_status; + draw(); +} + +void Menu::updateIndex() { + if (menuPage != PIZZA_SELECT) return; + drawIndex(); +} + +void Menu::initVariations() { + Pizzas::Pizza pizza = pizzas.get(curentPizzaIndex); + for (int i = 0; i < 4; i++) pizzaSizes[i] = {}; + pizzaSizesIndex = 0; + uint8_t sizesCount; + for (int i=0; i= len) { + new_str[i+1] = '\0'; + break; + } + } + } + return new_str; +} + + +void Menu::drawPrice(uint16_t price) { + lcd.setCursor(0, 1); + lcd.print(price, DEC); + lcd.print("RUB "); +} + +void Menu::drawSizes() { + lcd.setCursor(0, 1); + for (int i=0; i<4; i++) { + Pizzas::Variation pizza = pizzaSizes[i]; + if (pizza.id) { + lcd.print(pizza.size, DEC); + } else { + lcd.print(" "); + } + lcd.print(" "); + } +} + +void Menu::drawIndex() { + uint8_t curentPizza = curentPizzaIndex + 1; + uint8_t totalPizzas = pizzas.get_count(); + lcd.setCursor(11, 1); + if (curentPizza < 10) lcd.print(" "); + if (totalPizzas < 10) lcd.print(" "); + lcd.print(curentPizza, DEC); + lcd.print("/"); + lcd.print(totalPizzas, DEC); +} + +void Menu::drawTrackingStatus() { + lcd.setCursor(0, 1); + if (trackingStatus == UNKNOWN) { + lcd.print("Получение..."); + } else { + lcd.print(trackingStatusStrings[trackingStatus]); + } +} + +void Menu::drawDough(Pizzas::Dough dough) { + switch (dough) { + case Pizzas::TRADITIONAL: + lcd.print("обычн"); + break; + case Pizzas::THIN: + lcd.print("тонк"); + break; + } + lcd.print(" "); +} + +void Menu::drawPriceShort(uint16_t price) { + lcd.setCursor(11, 0); + for (int d=1000; price 500) { + animationState = !animationState; + animationTick = millis(); + draw(); + } + break; + case DOUGH_SELECT: + case SIZE_SELECT: + if (millis() - animationTick > 300) { + if (animationState) { + lcd.cursor_on(); + } else { + lcd.cursor_off(); + } + animationState = !animationState; + animationTick = millis(); + } + break; + } +} + + +void Menu::draw() { + switch (menuPage) { + case WIFI_CONNECT: + lcd.setCursor(0, 0); + if (animationState) { + lcd.print("WIFI CONNECTING "); + } else { + lcd.print("WIFI CONNECTING."); + } + break; + case PRELOAD: + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("Загрузка питс..."); + break; + case PIZZA_SELECT: { + Pizzas::Pizza pizza = pizzas.get(curentPizzaIndex); + lcd.clear(); + drawPizzaName(pizza.name); + drawPrice(pizza.price); + drawIndex(); + digitalWrite(LEDPIN, LOW); + brbflag = false; + break; + } + case DOUGH_SELECT: { + lcd.clear(); + lcd.setCursor(0, 0); + drawDough(Pizzas::TRADITIONAL); + drawDough(Pizzas::THIN); + Pizzas::Variation var = pizzaSizes[pizzaSizesIndex]; + drawPriceShort(var.price); + drawSizes(); + uint8_t cursor = (selectedDough == Pizzas::TRADITIONAL) ? 0 : 6; + lcd.setCursor(cursor, 0); + break; + } + case SIZE_SELECT: + lcd.clear(); + lcd.setCursor(0, 0); + drawDough(selectedDough); + drawSizes(); + drawSizeSelectUpdate(); + break; + case CONFIRMATION: { + Pizzas::Pizza pizza = pizzas.get(curentPizzaIndex); + Pizzas::Variation var = pizzaSizes[pizzaSizesIndex]; + lcd.clear(); + drawPizzaName(pizza.name); + drawVariation(var); + digitalWrite(LEDPIN, HIGH); + delay(3000); + brbflag = false; + break; + } + case PLACING_ORDER: + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("Заказываем..."); + digitalWrite(LEDPIN, LOW); + break; + case TRACKING: + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("Статус:"); + drawTrackingStatus(); + break; + } +} diff --git a/src/parser.cpp b/src/parser.cpp new file mode 100644 index 0000000..d220622 --- /dev/null +++ b/src/parser.cpp @@ -0,0 +1,102 @@ +#include "parser.h" + + +void PizzaHandler::startDocument() { +} + +void PizzaHandler::startArray(ElementPath path) { +} + +void PizzaHandler::startObject(ElementPath path) { +} + +void PizzaHandler::value(ElementPath path, ElementValue value) { + char fullPath[200] = ""; + const char* currentKey = path.getKey(); + Menu &menu = Menu::getInstance(); + menu.tick(); + if (path.getCount() > 2) { + char grand[50] = ""; + path.get(-2)->toString(grand); + char parent[50] = ""; + path.get(-1)->toString(parent); + if (currentKey[0] == '\0') { + return; + } + if (!strcmp(grand, "goods")) { + if(!strcmp(currentKey, "name")) { + value.toString(last_pizza.name); + uint8_t len = strlen(last_pizza.name); + + for (int i = 0; i < len - 1; ++i) { + last_pizza.name[i] = last_pizza.name[i + 1]; + } + last_pizza.name[len - 2] = '\0'; + + } else if (!strcmp(currentKey, "id")) { + last_pizza.id = value.getInt(); + ElementSelector* parent_selector = path.getParent(); + + char key[50] = ""; + parent_selector->toString(key); + } else if (!strcmp(currentKey, "good_type")) { + if (strlen(last_pizza.name) && last_pizza.id && last_pizza.variations_count) { + pizzas.add_pizza(last_pizza); + if (menu.getMenuPage() == Menu::PRELOAD) { + menu.setMenuPage(Menu::PIZZA_SELECT); + } else { + menu.updateIndex(); + } + } + last_pizza = {}; + } + } else if (!strcmp(grand, "variations")) { + if(!strcmp(currentKey, "id")) { + char var_str[10]; + value.toString(var_str); + new_variation.id = value.getInt(); + } else if(!strcmp(currentKey, "price")) { + new_variation.price = value.getInt(); + #ifdef MINPRICE + if (new_variation.price < MINPRICE) { + new_variation = {}; + } + #endif + if (new_variation.price < last_pizza.price || last_pizza.price == 0) { + last_pizza.price = new_variation.price; + } + } else if (!strcmp(currentKey, "stuffed_crust")) { + char crust[10]; + value.toString(crust); + if(strcmp(crust, "\"none\"")) { + new_variation = {}; + } + } + } else if (!strcmp(parent, "kind")) { + if(!strcmp(currentKey, "id")) { + new_variation.dough = static_cast(value.getInt()); + } + } else if (!strcmp(parent, "size")) { + if(!strcmp(currentKey, "value")) { + new_variation.size = value.getInt(); + if (new_variation.id) { + last_pizza.variations[last_pizza.variations_count] = new_variation; + last_pizza.variations_count++; + } + new_variation = {}; + } + } + } +} + +void PizzaHandler::endArray(ElementPath path) { +} + +void PizzaHandler::endObject(ElementPath path) { +} + +void PizzaHandler::endDocument() { +} + +void PizzaHandler::whitespace(char c) { +} \ No newline at end of file diff --git a/src/requests.cpp b/src/requests.cpp new file mode 100644 index 0000000..c2a0b17 --- /dev/null +++ b/src/requests.cpp @@ -0,0 +1,67 @@ +#include "requests.h" + + +char* Request::_get_url(const char* path) { + size_t len = strlen(API_URL) + strlen(path) + 1; + char* url = (char*)malloc(len); + strcpy(url, API_URL); + strcat(url, path); + return url; +} + +HTTPClient* Request::_get_client(const char* method, char* url) { + HTTPClient* http = new HTTPClient(); + http->begin(url); + if (!strcmp(method, "POST")) { + http->addHeader("Content-Type", "application/json"); + http->addHeader("Accept", "application/json"); + } else { + http->addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"); + } + http->addHeader("Accept-Language", "en-US,en;q=0.5"); + http->setUserAgent("Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0"); + return http; +} + +void Request::request(const char* method, char* path, ArudinoStreamParser& parser) { + char* url = _get_url(path); + HTTPClient* http = _get_client(method, url); + + int httpCode = http->GET(); + if (httpCode == HTTP_CODE_OK) { + http->writeToStream(&parser); + } + http->end(); + delete http; + free(url); +} + +char* Request::request(const char* method, const char* path, const char* payload) { + char* url = _get_url(path); + HTTPClient* http = _get_client(method, url); + + int httpCode = http->POST(payload); + + const size_t len = http->getSize() + 1; + char* response = (char*)malloc(len); + http->getString().toCharArray(response, len); + http->end(); + delete http; + free(url); + return response; +} + +char* Request::request(const char* method, const char* path) { + char* url = _get_url(path); + HTTPClient* http = _get_client(method, url); + + int httpCode = http->GET(); + + const size_t len = http->getSize() + 1; + char* response = (char*)malloc(len); + http->getString().toCharArray(response, len); + http->end(); + delete http; + free(url); + return response; +}