From 0e9809c8b798bbdd69f1a42a5280a3ebb79cb740 Mon Sep 17 00:00:00 2001 From: nopnop2002 Date: Wed, 30 Jun 2021 06:52:43 +0900 Subject: [PATCH] First Release --- CMakeLists.txt | 12 ++ Makefile | 16 ++ README.md | 174 ++++++++++++++++++- csv/can2http.csv | 11 ++ flask/can.py | 61 +++++++ main/CMakeLists.txt | 4 + main/Kconfig.projbuild | 110 ++++++++++++ main/component.mk | 4 + main/http_post.c | 188 ++++++++++++++++++++ main/main.c | 380 +++++++++++++++++++++++++++++++++++++++++ main/twai.c | 103 +++++++++++ main/twai.h | 17 ++ partitions.csv | 6 + sdkconfig.defaults | 17 ++ tornado/can.py | 70 ++++++++ 15 files changed, 1172 insertions(+), 1 deletion(-) create mode 100644 CMakeLists.txt create mode 100644 Makefile create mode 100644 csv/can2http.csv create mode 100644 flask/can.py create mode 100644 main/CMakeLists.txt create mode 100644 main/Kconfig.projbuild create mode 100644 main/component.mk create mode 100644 main/http_post.c create mode 100644 main/main.c create mode 100644 main/twai.c create mode 100644 main/twai.h create mode 100644 partitions.csv create mode 100644 sdkconfig.defaults create mode 100644 tornado/can.py diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..114213b --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,12 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(can2http) + +# Create a SPIFFS image from the contents of the 'font' directory +# that fits the partition named 'storage'. FLASH_IN_PROJECT indicates that +# the generated image should be flashed when the entire project is flashed to +# the target with 'idf.py -p PORT flash +spiffs_create_partition_image(storage csv FLASH_IN_PROJECT) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..38c808b --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# + +PROJECT_NAME := can2mqtt + +include $(IDF_PATH)/make/project.mk + +# Create a SPIFFS image from the contents of the 'spiffs_image' directory +# that fits the partition named 'storage'. FLASH_IN_PROJECT indicates that +# the generated image should be flashed when the entire project is flashed to +# the target with 'make flash'. +SPIFFS_IMAGE_FLASH_IN_PROJECT := 1 +$(eval $(call spiffs_create_partition_image,storage,csv)) + diff --git a/README.md b/README.md index 67c1c7c..8a800f3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,174 @@ # esp-idf-can2http -CANbus to http bridge using esp32 +CANbus to http bridge using esp32. +It's purpose is to be a bridge between a CAN-Bus and a HTTP-Server. + +I inspired from [here](https://github.com/c3re/can2mqtt). + +![esp-idf-can2http](https://user-images.githubusercontent.com/6020549/123870583-773bc300-d96d-11eb-8c07-27747faf7bb4.jpg) + + +# Software requirement +esp-idf v4.2-dev-2243 or later. +Use twai(Two-Wire Automotive Interface) driver instead of can driver. + +# Hardware requirements +1. SN65HVD23x CAN-BUS Transceiver + +|SN65HVD23x||ESP32|ESP32-S2|| +|:-:|:-:|:-:|:-:|:-:| +|D(CTX)|--|GPIO21|GPIO20|(*1)| +|GND|--|GND|GND|| +|Vcc|--|3.3V|3.3V|| +|R(CRX)|--|GPIO22|GPIO21|(*1)| +|Vref|--|N/C|N/C|| +|CANL|--|||To CAN Bus| +|CANH|--|||To CAN Bus| +|RS|--|GND|GND|(*2)| + +(*1) You can change using menuconfig. + +(*2) N/C for SN65HVD232 + +2. Termination resistance +I used 150 ohms. + + +# Test Circuit +``` + +-----------+ +-----------+ +-----------+ + | Atmega328 | | Atmega328 | | ESP32 | + | | | | | | + | Transmit | | Receive | | 21 22 | + +-----------+ +-----------+ +-----------+ + | | | | | | + +-----------+ +-----------+ | | + | | | | | | + | MCP2515 | | MCP2515 | | | + | | | | | | + +-----------+ +-----------+ | | + | | | | | | + +-----------+ +-----------+ +-----------+ + | | | | | D R | + | MCP2551 | | MCP2551 | | VP230 | + | H L | | H L | | H L | + +-----------+ +-----------+ +-----------+ + | | | | | | + +--^^^--+ | | +--^^^--+ + | R1 | | | | R2 | + |---+-------|-----+-------|-----+-------|---| BackBorn H + | | | + | | | + | | | + |-----------+-------------+-------------+---| BackBorn L + + +--^^^--+:Terminaror register + R1:120 ohms + R2:150 ohms(Not working at 120 ohms) +``` + +__NOTE__ +3V CAN Trasnceviers like VP230 are fully interoperable with 5V CAN trasnceviers like MCP2551. +Check [here](http://www.ti.com/lit/an/slla337/slla337.pdf). + + +# Installation for ESP32 +``` +git clone https://github.com/nopnop2002/esp-idf-can2http +cd esp-idf-can2htp +idf.py set-target esp32 +idf.py menuconfig +idf.py flash +``` + +# Installation for ESP32-S2 +``` +git clone https://github.com/nopnop2002/esp-idf-can2http +cd esp-idf-can2http +idf.py set-target esp32s2 +idf.py menuconfig +idf.py flash +``` + +# Configuration +![config-main](https://user-images.githubusercontent.com/6020549/123870635-92a6ce00-d96d-11eb-9b67-7b6a26e95fbd.jpg) +![config-app](https://user-images.githubusercontent.com/6020549/123870638-94709180-d96d-11eb-94da-b4860148be6a.jpg) + +## CAN Setting +![config-can](https://user-images.githubusercontent.com/6020549/123870665-a05c5380-d96d-11eb-89b1-78a274bfd957.jpg) + +## WiFi Setting +![config-wifi](https://user-images.githubusercontent.com/6020549/123870681-a81bf800-d96d-11eb-8637-66295408b055.jpg) + +## HTTP Server Setting +![config-http](https://user-images.githubusercontent.com/6020549/123870716-b702aa80-d96d-11eb-8954-6ca365e78639.jpg) + +# Definition from CANbus to HTTP +When CANbus data is received, it is sent by HTTP according to csv/can2http.csv. +The file can2http.csv has three columns. +In the first column you need to specify the CAN Frame type. +The CAN frame type is either S(Standard frame) or E(Extended frame). +In the second column you have to specify the CAN-ID as a __hexdecimal number__. +In the last column you have to specify the HTTP-Path. +Each CAN-ID and each HTTP-Path is allowed to appear only once in the whole file. + +``` +S,101,/post +E,101,/post +S,103,/post +E,103,/post +``` + +When a Standard CAN frame with ID 0x101 is received, it is POST by PATH of "/post". +When a Extended CAN frame with ID 0x101 is received, it is POST by PATH of "/post". +http://{HTTP-Server-IP}:{HTTP-Server-Port}/post/{CAN-Data} + +# HTTP Server Using Tornado +``` +sudo apt install python3-pip python3-setuptools python3-magic +python -m pip install -U pip +python -m pip install -U wheel +python -m pip install tornado +cd tornado +python can.py +``` +![can2http-tornado](https://user-images.githubusercontent.com/6020549/123871778-18774900-d96f-11eb-95b6-df9713047c30.jpg) + +# HTTP Server Using Flask +``` +sudo apt install python3-pip python3-setuptools python3-magic +python -m pip install -U pip +python -m pip install -U wheel +python -m pip install -U Werkzeug +python -m pip install flask +cd flask +python can.py +``` + +![can2http-flask](https://user-images.githubusercontent.com/6020549/123871850-35ac1780-d96f-11eb-9d03-c1a0b547e9c8.jpg) + + +# List data Using Brouser +Open your browser and put the IP address in the address bar. + +![can2http-browser](https://user-images.githubusercontent.com/6020549/123872025-71df7800-d96f-11eb-8832-8d9e1169c993.jpg) + +# Troubleshooting +There is a module of SN65HVD230 like this. +![SN65HVD230-1](https://user-images.githubusercontent.com/6020549/80897499-4d204e00-8d34-11ea-80c9-3dc41b1addab.JPG) + +There is a __120 ohms__ terminating resistor on the left side. +![SN65HVD230-22](https://user-images.githubusercontent.com/6020549/89281044-74185400-d684-11ea-9f55-830e0e9e6424.JPG) + +I have removed the terminating resistor. +And I used a external resistance of __150 ohms__. +A transmission fail is fixed. +![SN65HVD230-33](https://user-images.githubusercontent.com/6020549/89280710-f7857580-d683-11ea-9b36-12e36910e7d9.JPG) + +If the transmission fails, these are the possible causes. +- There is no receiving app on CanBus. +- The speed does not match the receiver. +- There is no terminating resistor on the CanBus. +- There are three terminating resistors on the CanBus. +- The resistance value of the terminating resistor is incorrect. +- Stub length in CAN bus is too long. See [here](https://e2e.ti.com/support/interface-group/interface/f/interface-forum/378932/iso1050-can-bus-stub-length). + diff --git a/csv/can2http.csv b/csv/can2http.csv new file mode 100644 index 0000000..df7422e --- /dev/null +++ b/csv/can2http.csv @@ -0,0 +1,11 @@ +#The file can2http.csv has three columns. +#In the first column you need to specify the CAN Frame type. +#The CAN frame type is either S(Standard frame) or E(Extended frame). +#In the second column you have to specify the CAN-ID as a __hexdecimal number__. +#In the last column you have to specify the HTTP-Path. +#Each CAN-ID and each HTTP-Path is allowed to appear only once in the whole file. + +S,101,/post +E,101,/post +S,103,/post +E,103,/post diff --git a/flask/can.py b/flask/can.py new file mode 100644 index 0000000..5b4c652 --- /dev/null +++ b/flask/can.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Simple REST Server + +import sys +import json +import datetime +from flask import Flask, request +app = Flask(__name__) + +database = [] + +@app.route("/") +def root(): + global database + res = "" + for data in database: + print("{}".format(data)) + #print("{}".format(len(data[2]))) + res = res + "{} {:x} {} {}".format(data[0],data[1],data[2],data[3]) + "
" + #return "Hello World!" + return res + +#curl -X POST -H "Content-Type: application/json" -d '{"canid":101, "frame":"standard" , "data": [1,2,3,4,5,6,7,8]}' http://192.168.10.43:8000/post +@app.route("/post", methods=["POST"]) +def post(): + function = sys._getframe().f_code.co_name + #print("{}: request={}".format(function, request)) + #print("{}: request.data={}".format(function, request.data)) + dict = json.loads(request.data) + print("{} dict={}".format(function, dict)) + + items = [] + now = datetime.datetime.now() + items.append(now.strftime("%Y/%m/%d %H:%M:%S")) + if ("canid" in dict): + print("{} canid=0x{:x}".format(function, dict['canid'])) + items.append(dict['canid']) + + if ("frame" in dict): + print("{} frame={}".format(function, dict['frame'])) + items.append(dict['frame']) + + if ("data" in dict): + print("{} data length={}".format(function, len(dict['data']))) + print("{} data={}".format(function, dict['data'])) + items.append(dict['data']) + + global database + #print("{} {} {}".format(type(database), len(database), database)) + if(len(database) >= 20): + database.pop(0) + database.append(items) + + data = json.dumps(['result', 'ok']) + return data + +if __name__ == "__main__": + #app.run() + app.run(host='0.0.0.0', port=8000, debug=True) + diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..2103cc0 --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,4 @@ +set(COMPONENT_SRCS "main.c" "http_post.c" "twai.c") +set(COMPONENT_ADD_INCLUDEDIRS ".") + +register_component() diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild new file mode 100644 index 0000000..83c068b --- /dev/null +++ b/main/Kconfig.projbuild @@ -0,0 +1,110 @@ +menu "Application Configuration" + + menu "CAN Setting" + + choice CAN_BITRATE + prompt "CAN Bitrate" + default CAN_BITRATE_500 + help + Select the CAN bitrate for the example. + config CAN_BITRATE_25 + bool "BITRATE_25" + help + CAN bitrate is 25 Kbit/s. + config CAN_BITRATE_50 + bool "BITRATE_50" + help + CAN bitrate is 50 Kbit/s. + config CAN_BITRATE_100 + bool "BITRATE_100" + help + CAN bitrate is 100 Kbit/s. + config CAN_BITRATE_125 + bool "BITRATE_125" + help + CAN bitrate is 125 Kbit/s. + config CAN_BITRATE_250 + bool "BITRATE_250" + help + CAN bitrate is 250 Kbit/s. + config CAN_BITRATE_500 + bool "BITRATE_500" + help + CAN bitrate is 500 Kbit/s. + config CAN_BITRATE_800 + bool "BITRATE_800" + help + CAN bitrate is 800 Kbit/s. + config CAN_BITRATE_1000 + bool "BITRATE_1000" + help + CAN bitrate is 1 Mbit/s. + endchoice + + config CTX_GPIO + int "CTX GPIO number" + range 0 34 + default 2 if IDF_TARGET_ESP32C3 + default 20 if IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 + default 21 if IDF_TARGET_ESP32 + help + GPIO number (IOxx) to CTX. + Some GPIOs are used for other purposes (flash connections, etc.). + GPIOs 35-39 are input-only so cannot be used as outputs. + + config CRX_GPIO + int "CRX GPIO number" + range 0 34 + default 3 if IDF_TARGET_ESP32C3 + default 21 if IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 + default 22 if IDF_TARGET_ESP32 + help + GPIO number (IOxx) to CRX. + Some GPIOs are used for other purposes (flash connections, etc.). + GPIOs 35-39 are input-only so cannot be used as outputs. + + config ENABLE_PRINT + bool "Output the received CAN FRAME to STDOUT" + help + Output the received CAN FRAME to STDOUT. + + endmenu + + menu "WiFi Setting" + + config ESP_WIFI_SSID + string "WiFi SSID" + default "myssid" + help + SSID (network name) to connect to. + + config ESP_WIFI_PASSWORD + string "WiFi Password" + default "mypassword" + help + WiFi password (WPA or WPA2) to connect to. + + config ESP_MAXIMUM_RETRY + int "Maximum retry" + default 5 + help + Set the Maximum retry to avoid station reconnecting to the AP unlimited when the AP is really inexistent. + + endmenu + + menu "HTTP Server Setting" + + config WEB_SERVER + string "HTTP Server" + default "myhttpserver" + help + The host name or IP address of the HTTP server to use. + + config WEB_PORT + int "HTTP Port" + default 8000 + help + HTTP server port to use. + + endmenu +endmenu diff --git a/main/component.mk b/main/component.mk new file mode 100644 index 0000000..b4fa727 --- /dev/null +++ b/main/component.mk @@ -0,0 +1,4 @@ +# +# Main Makefile. This is basically the same as a component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) diff --git a/main/http_post.c b/main/http_post.c new file mode 100644 index 0000000..a011e30 --- /dev/null +++ b/main/http_post.c @@ -0,0 +1,188 @@ +/* ESP HTTP Client Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "esp_system.h" +#include "esp_tls.h" +#include "esp_http_client.h" + +#include "cJSON.h" + +#include "twai.h" + +static const char *TAG = "HTTP"; + +extern QueueHandle_t xQueue_http; + +esp_err_t _http_event_handler(esp_http_client_event_t *evt) +{ + static char *output_buffer; // Buffer to store response of http request from event handler + static int output_len; // Stores number of bytes read + switch(evt->event_id) { + case HTTP_EVENT_ERROR: + ESP_LOGD(TAG, "HTTP_EVENT_ERROR"); + break; + case HTTP_EVENT_ON_CONNECTED: + ESP_LOGD(TAG, "HTTP_EVENT_ON_CONNECTED"); + break; + case HTTP_EVENT_HEADER_SENT: + ESP_LOGD(TAG, "HTTP_EVENT_HEADER_SENT"); + break; + case HTTP_EVENT_ON_HEADER: + ESP_LOGD(TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value); + break; + case HTTP_EVENT_ON_DATA: + ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len); + /* + * Check for chunked encoding is added as the URL for chunked encoding used in this example returns binary data. + * However, event handler can also be used in case chunked encoding is used. + */ + if (!esp_http_client_is_chunked_response(evt->client)) { + // If user_data buffer is configured, copy the response into the buffer + if (evt->user_data) { + memcpy(evt->user_data + output_len, evt->data, evt->data_len); + } else { + if (output_buffer == NULL) { + output_buffer = (char *) malloc(esp_http_client_get_content_length(evt->client)); + output_len = 0; + if (output_buffer == NULL) { + ESP_LOGE(TAG, "Failed to allocate memory for output buffer"); + return ESP_FAIL; + } + } + memcpy(output_buffer + output_len, evt->data, evt->data_len); + } + output_len += evt->data_len; + } + + break; + case HTTP_EVENT_ON_FINISH: + ESP_LOGD(TAG, "HTTP_EVENT_ON_FINISH"); + if (output_buffer != NULL) { + // Response is accumulated in output_buffer. Uncomment the below line to print the accumulated response + // ESP_LOG_BUFFER_HEX(TAG, output_buffer, output_len); + free(output_buffer); + output_buffer = NULL; + } + output_len = 0; + break; + case HTTP_EVENT_DISCONNECTED: + ESP_LOGD(TAG, "HTTP_EVENT_DISCONNECTED"); + int mbedtls_err = 0; + esp_err_t err = esp_tls_get_and_clear_last_error(evt->data, &mbedtls_err, NULL); + if (err != 0) { + if (output_buffer != NULL) { + free(output_buffer); + output_buffer = NULL; + } + output_len = 0; + ESP_LOGI(TAG, "Last esp error code: 0x%x", err); + ESP_LOGI(TAG, "Last mbedtls failure: 0x%x", mbedtls_err); + } + break; + } + return ESP_OK; +} + +#define MAX_HTTP_OUTPUT_BUFFER 2048 + +static void http_rest_with_url(char * path, char * post_data) +{ + char local_response_buffer[MAX_HTTP_OUTPUT_BUFFER] = {0}; + /** + * NOTE: All the configuration parameters for http_client must be spefied either in URL or as host and path parameters. + * If host and path parameters are not set, query parameter will be ignored. In such cases, + * query parameter should be specified in URL. + * + * If URL as well as host and path parameters are specified, values of host and path will be considered. + */ + esp_http_client_config_t config = { + .host = CONFIG_WEB_SERVER, + .port = CONFIG_WEB_PORT, + .path = path, + .event_handler = _http_event_handler, + .user_data = local_response_buffer, // Pass address of local buffer to get response + .disable_auto_redirect = true, + }; + +#if 0 + // Same as above + esp_http_client_config_t config = { + .url = "http://192.168.10.43:8000/post", + .event_handler = _http_event_handler, + .user_data = local_response_buffer, // Pass address of local buffer to get response + .disable_auto_redirect = true, + }; +#endif + esp_http_client_handle_t client = esp_http_client_init(&config); + + // POST + // no need to change url + //esp_http_client_set_url(client, "http://192.168.10.43:8000/post"); + esp_http_client_set_method(client, HTTP_METHOD_POST); + esp_http_client_set_header(client, "Content-Type", "application/json"); + esp_http_client_set_post_field(client, post_data, strlen(post_data)); + esp_err_t err = esp_http_client_perform(client); + if (err == ESP_OK) { + ESP_LOGI(TAG, "HTTP POST Status = %d, content_length = %d", + esp_http_client_get_status_code(client), + esp_http_client_get_content_length(client)); + ESP_LOGI(TAG, "local_response_buffer=[%s]", local_response_buffer); + } else { + ESP_LOGE(TAG, "HTTP POST request failed: %s", esp_err_to_name(err)); + } + + esp_http_client_cleanup(client); +} + + +void http_client_task(void *pvParameters) +{ + ESP_LOGI(TAG, "Start HTTP Client: http://%s:%d", CONFIG_WEB_SERVER, CONFIG_WEB_PORT); + FRAME_t frameBuf; + while (1) { + xQueueReceive(xQueue_http, &frameBuf, portMAX_DELAY); + ESP_LOGI(TAG, "canid=%x ext=%d topic=[%s]", frameBuf.canid, frameBuf.ext, frameBuf.topic); + for(int i=0;i +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "freertos/event_groups.h" +#include "freertos/semphr.h" +#include "esp_system.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include "esp_vfs.h" +#include "nvs_flash.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_spiffs.h" +#include "driver/twai.h" // Update from V4.2 + +#include "twai.h" + +#define TAG "MAIN" + +static const twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); + +#if CONFIG_CAN_BITRATE_25 +static const twai_timing_config_t t_config = TWAI_TIMING_CONFIG_25KBITS(); +#define BITRATE "Bitrate is 25 Kbit/s" +#elif CONFIG_CAN_BITRATE_50 +static const twai_timing_config_t t_config = TWAI_TIMING_CONFIG_50KBITS(); +#define BITRATE "Bitrate is 50 Kbit/s" +#elif CONFIG_CAN_BITRATE_100 +static const twai_timing_config_t t_config = TWAI_TIMING_CONFIG_100KBITS(); +#define BITRATE "Bitrate is 100 Kbit/s" +#elif CONFIG_CAN_BITRATE_125 +static const twai_timing_config_t t_config = TWAI_TIMING_CONFIG_125KBITS(); +#define BITRATE "Bitrate is 125 Kbit/s" +#elif CONFIG_CAN_BITRATE_250 +static const twai_timing_config_t t_config = TWAI_TIMING_CONFIG_250KBITS(); +#define BITRATE "Bitrate is 250 Kbit/s" +#elif CONFIG_CAN_BITRATE_500 +static const twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS(); +#define BITRATE "Bitrate is 500 Kbit/s" +#elif CONFIG_CAN_BITRATE_800 +static const twai_timing_config_t t_config = TWAI_TIMING_CONFIG_800KBITS(); +#define BITRATE "Bitrate is 800 Kbit/s" +#elif CONFIG_CAN_BITRATE_1000 +static const twai_timing_config_t t_config = TWAI_TIMING_CONFIG_1MBITS(); +#define BITRATE "Bitrate is 1 Mbit/s" +#endif + +/* FreeRTOS event group to signal when we are connected*/ +static EventGroupHandle_t s_wifi_event_group; + +/* The event group allows multiple bits for each event, but we only care about two events: + * - we are connected to the AP with an IP + * - we failed to connect after the maximum amount of retries */ +#define WIFI_CONNECTED_BIT BIT0 +#define WIFI_FAIL_BIT BIT1 + +static int s_retry_num = 0; + +QueueHandle_t xQueue_http; + +TOPIC_t *publish; +int16_t npublish; + +static void event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + esp_wifi_connect(); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + if (s_retry_num < CONFIG_ESP_MAXIMUM_RETRY) { + esp_wifi_connect(); + s_retry_num++; + ESP_LOGI(TAG, "retry to connect to the AP"); + } else { + xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); + } + ESP_LOGI(TAG,"connect to the AP fail"); + } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; + ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip)); + s_retry_num = 0; + xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); + } +} + +bool wifi_init_sta(void) +{ + bool ret = false; + s_wifi_event_group = xEventGroupCreate(); + + ESP_ERROR_CHECK(esp_netif_init()); + + ESP_ERROR_CHECK(esp_event_loop_create_default()); + esp_netif_create_default_wifi_sta(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + esp_event_handler_instance_t instance_any_id; + esp_event_handler_instance_t instance_got_ip; + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, + ESP_EVENT_ANY_ID, + &event_handler, + NULL, + &instance_any_id)); + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, + IP_EVENT_STA_GOT_IP, + &event_handler, + NULL, + &instance_got_ip)); + + wifi_config_t wifi_config = { + .sta = { + .ssid = CONFIG_ESP_WIFI_SSID, + .password = CONFIG_ESP_WIFI_PASSWORD, + /* Setting a password implies station will connect to all security modes including WEP/WPA. + * However these modes are deprecated and not advisable to be used. Incase your Access point + * doesn't support WPA2, these mode can be enabled by commenting below line */ + .threshold.authmode = WIFI_AUTH_WPA2_PSK, + + .pmf_cfg = { + .capable = true, + .required = false + }, + }, + }; + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) ); + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config) ); + ESP_ERROR_CHECK(esp_wifi_start() ); + + ESP_LOGI(TAG, "wifi_init_sta finished."); + + /* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum + * number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above) */ + EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, + WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, + pdFALSE, + pdFALSE, + portMAX_DELAY); + + /* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually + * happened. */ + if (bits & WIFI_CONNECTED_BIT) { + ESP_LOGI(TAG, "connected to ap SSID:%s password:%s", + CONFIG_ESP_WIFI_SSID, CONFIG_ESP_WIFI_PASSWORD); + ret = true; + } else if (bits & WIFI_FAIL_BIT) { + ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s", + CONFIG_ESP_WIFI_SSID, CONFIG_ESP_WIFI_PASSWORD); + } else { + ESP_LOGE(TAG, "UNEXPECTED EVENT"); + } + + /* The event will not be processed after unregister */ + ESP_ERROR_CHECK(esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, instance_got_ip)); + ESP_ERROR_CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_any_id)); + vEventGroupDelete(s_wifi_event_group); + return ret; +} + +esp_err_t mountSPIFFS(char * partition_label, char * base_path) { + ESP_LOGI(TAG, "Initializing SPIFFS file system"); + + esp_vfs_spiffs_conf_t conf = { + .base_path = base_path, + .partition_label = partition_label, + .max_files = 5, + .format_if_mount_failed = true + }; + + // Use settings defined above to initialize and mount SPIFFS filesystem. + // Note: esp_vfs_spiffs_register is an all-in-one convenience function. + esp_err_t ret = esp_vfs_spiffs_register(&conf); + + if (ret != ESP_OK) { + if (ret == ESP_FAIL) { + ESP_LOGE(TAG, "Failed to mount or format filesystem"); + } else if (ret == ESP_ERR_NOT_FOUND) { + ESP_LOGE(TAG, "Failed to find SPIFFS partition"); + } else { + ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret)); + } + return ret; + } + + size_t total = 0, used = 0; + ret = esp_spiffs_info(partition_label, &total, &used); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret)); + } else { + ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used); + DIR* dir = opendir(base_path); + assert(dir != NULL); + while (true) { + struct dirent*pe = readdir(dir); + if (!pe) break; + ESP_LOGI(TAG, "d_name=%s d_ino=%d d_type=%x", pe->d_name,pe->d_ino, pe->d_type); + } + closedir(dir); + } + ESP_LOGI(TAG, "Mount SPIFFS filesystem"); + return ret; +} + +esp_err_t build_table(TOPIC_t **topics, char *file, int16_t *ntopic) +{ + ESP_LOGI(TAG, "build_table file=%s", file); + char line[128]; + int _ntopic = 0; + + FILE* f = fopen(file, "r"); + if (f == NULL) { + ESP_LOGE(TAG, "Failed to open file for reading"); + return ESP_FAIL; + } + while (1){ + if ( fgets(line, sizeof(line) ,f) == 0 ) break; + // strip newline + char* pos = strchr(line, '\n'); + if (pos) { + *pos = '\0'; + } + ESP_LOGD(TAG, "line=[%s]", line); + if (strlen(line) == 0) continue; + if (line[0] == '#') continue; + _ntopic++; + } + fclose(f); + ESP_LOGI(TAG, "build_table _ntopic=%d", _ntopic); + + *topics = calloc(_ntopic, sizeof(TOPIC_t)); + if (*topics == NULL) { + ESP_LOGE(TAG, "Error allocating memory for topic"); + return ESP_ERR_NO_MEM; + } + + f = fopen(file, "r"); + if (f == NULL) { + ESP_LOGE(TAG, "Failed to open file for reading"); + return ESP_FAIL; + } + + char *ptr; + int index = 0; + while (1){ + if ( fgets(line, sizeof(line) ,f) == 0 ) break; + // strip newline + char* pos = strchr(line, '\n'); + if (pos) { + *pos = '\0'; + } + ESP_LOGD(TAG, "line=[%s]", line); + if (strlen(line) == 0) continue; + if (line[0] == '#') continue; + + // Frame type + ptr = strtok(line, ","); + ESP_LOGD(TAG, "ptr=%s", ptr); + if (strcmp(ptr, "S") == 0) { + (*topics+index)->frame = 0; + } else if (strcmp(ptr, "E") == 0) { + (*topics+index)->frame = 1; + } else { + ESP_LOGE(TAG, "This line is invalid [%s]", line); + continue; + } + + // CAN ID + uint32_t canid; + ptr = strtok(NULL, ","); + if(ptr == NULL) continue; + ESP_LOGD(TAG, "ptr=%s", ptr); + canid = strtol(ptr, NULL, 16); + if (canid == 0) { + ESP_LOGE(TAG, "This line is invalid [%s]", line); + continue; + } + (*topics+index)->canid = canid; + + // mqtt topic + char *sp; + ptr = strtok(NULL, ","); + if(ptr == NULL) { + ESP_LOGE(TAG, "This line is invalid [%s]", line); + continue; + } + ESP_LOGD(TAG, "ptr=[%s] strlen=%d", ptr, strlen(ptr)); + sp = strstr(ptr,"#"); + if (sp != NULL) { + ESP_LOGE(TAG, "This line is invalid [%s]", line); + continue; + } + sp = strstr(ptr,"+"); + if (sp != NULL) { + ESP_LOGE(TAG, "This line is invalid [%s]", line); + continue; + } + (*topics+index)->topic = (char *)malloc(strlen(ptr)+1); + strcpy((*topics+index)->topic, ptr); + (*topics+index)->topic_len = strlen(ptr); + index++; + } + fclose(f); + *ntopic = index; + return ESP_OK; +} + +void dump_table(TOPIC_t *topics, int16_t ntopic) +{ + for(int i=0;iframe, (topics+i)->canid, (topics+i)->topic, (topics+i)->topic_len); + } + +} + +void http_client_task(void *pvParameters); +void twai_task(void *pvParameters); + +void app_main() +{ + // Initialize NVS + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + // WiFi initialize + if (wifi_init_sta() == false) { + while(1) vTaskDelay(10); + } + + // Install and start TWAI driver + ESP_LOGI(TAG, "%s",BITRATE); + ESP_LOGI(TAG, "CTX_GPIO=%d",CONFIG_CTX_GPIO); + ESP_LOGI(TAG, "CRX_GPIO=%d",CONFIG_CRX_GPIO); + + static const twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(CONFIG_CTX_GPIO, CONFIG_CRX_GPIO, TWAI_MODE_NORMAL); + ESP_ERROR_CHECK(twai_driver_install(&g_config, &t_config, &f_config)); + ESP_LOGI(TAG, "Driver installed"); + ESP_ERROR_CHECK(twai_start()); + ESP_LOGI(TAG, "Driver started"); + + // Mount SPIFFS + char *partition_label = "storage"; + char *base_path = "/spiffs"; + ret = mountSPIFFS(partition_label, base_path); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "mountSPIFFS fail"); + while(1) { vTaskDelay(1); } + } + + // Create Queue + xQueue_http = xQueueCreate( 10, sizeof(FRAME_t) ); + configASSERT( xQueue_http ); + + // build publish table + ret = build_table(&publish, "/spiffs/can2http.csv", &npublish); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "build publish table fail"); + while(1) { vTaskDelay(1); } + } + dump_table(publish, npublish); + + xTaskCreate(http_client_task, "http", 1024*6, NULL, 2, NULL); + xTaskCreate(twai_task, "twai_rx", 1024*6, NULL, 2, NULL); +} diff --git a/main/twai.c b/main/twai.c new file mode 100644 index 0000000..b237cb3 --- /dev/null +++ b/main/twai.c @@ -0,0 +1,103 @@ +/* TWAI Network Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "freertos/semphr.h" +#include "esp_err.h" +#include "esp_log.h" +#include "driver/twai.h" // Update from V4.2 + +#include "twai.h" + +static const char *TAG = "TWAI"; + +extern QueueHandle_t xQueue_http; +//extern QueueHandle_t xQueue_twai_tx; + +extern TOPIC_t *publish; +extern int16_t npublish; + +void dump_table(TOPIC_t *topics, int16_t ntopic); + +void twai_task(void *pvParameters) +{ + ESP_LOGI(TAG,"task start"); + dump_table(publish, npublish); + + twai_message_t rx_msg; + FRAME_t frameBuf; + while (1) { + esp_err_t ret = twai_receive(&rx_msg, pdMS_TO_TICKS(1)); + if (ret == ESP_OK) { + ESP_LOGD(TAG,"twai_receive identifier=0x%x flags=0x%x data_length_code=%d", + rx_msg.identifier, rx_msg.flags, rx_msg.data_length_code); + + int ext = rx_msg.flags & 0x01; + int rtr = rx_msg.flags & 0x02; + ESP_LOGD(TAG, "ext=%x rtr=%x", ext, rtr); + +#if CONFIG_ENABLE_PRINT + if (ext == 0) { + printf("Standard ID: 0x%03x ", rx_msg.identifier); + } else { + printf("Extended ID: 0x%08x", rx_msg.identifier); + } + printf(" DLC: %d Data: ", rx_msg.data_length_code); + + if (rtr == 0) { + for (int i = 0; i < rx_msg.data_length_code; i++) { + printf("0x%02x ", rx_msg.data[i]); + } + } else { + printf("REMOTE REQUEST FRAME"); + + } + printf("\n"); +#endif + + for(int index=0;index" + self.write(res) + +#curl -X POST -H "Content-Type: application/json" -d '{"canid":101, "frame":"standard" , "data": [1,2,3,4,5,6,7,8]}' http://192.168.10.43:8000/post +class PostHandler(tornado.web.RequestHandler): + def post(self): + #print("{}".format(self.request.body)) + function = sys._getframe().f_code.co_name + dict = tornado.escape.json_decode(self.request.body) + print("{} dict={}".format(function, dict)) + + items = [] + now = datetime.datetime.now() + items.append(now.strftime("%Y/%m/%d %H:%M:%S")) + if ("canid" in dict): + print("{} canid=0x{:x}".format(function, dict['canid'])) + items.append(dict['canid']) + + if ("frame" in dict): + print("{} frame={}".format(function, dict['frame'])) + items.append(dict['frame']) + + if ("data" in dict): + print("{} data length={}".format(function, len(dict['data']))) + print("{} data={}".format(function, dict['data'])) + items.append(dict['data']) + + global database + #print("{} {} {}".format(type(database), len(database), database)) + if(len(database) >= 20): + database.pop(0) + database.append(items) + + self.write(json.dumps({"result":"ok"})) + +if __name__ == "__main__": + tornado.options.parse_command_line() + app = tornado.web.Application( + handlers=[ + (r"/", IndexHandler), + (r"/post", PostHandler), + ],debug=True + ) + http_server = tornado.httpserver.HTTPServer(app) + http_server.listen(options.port) + tornado.ioloop.IOLoop.current().start()