diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..45faf75 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,18 @@ +name: Publish Release + +on: + push: + tags: + - 'v*.*.*' + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Publish release + uses: softprops/action-gh-release@v1 + with: + generate_release_notes: true diff --git a/hardware/BOM.csv b/hardware/BOM.csv new file mode 100644 index 0000000..6e4d1ab --- /dev/null +++ b/hardware/BOM.csv @@ -0,0 +1,8 @@ +Part Name, Total Quantity Required, Note +LilyGo TTGO T-Beam v1.1 SX1262 + GPS NEO-M8N + ESP32 + BMS, 1, +GPS + GLONASS Antenna, 1, +LoRa Antenna, 1, +u.FL to Waterproof SMA Adapter Cable, 2, +SMA Connector Cap, 2, +NCR 18650B Li-Ion Battery 3400mAh, 1, +LED Diode 3mm, 1, diff --git a/hardware/STL/case-bottom.stl b/hardware/STL/case-bottom.stl new file mode 100644 index 0000000..becff97 Binary files /dev/null and b/hardware/STL/case-bottom.stl differ diff --git a/hardware/STL/case-top.stl b/hardware/STL/case-top.stl new file mode 100644 index 0000000..1204e5a Binary files /dev/null and b/hardware/STL/case-top.stl differ diff --git a/hardware/STL/gasket.stl b/hardware/STL/gasket.stl new file mode 100644 index 0000000..4ee4a51 Binary files /dev/null and b/hardware/STL/gasket.stl differ diff --git a/hardware/wiring-diagram.svg b/hardware/wiring-diagram.svg new file mode 100644 index 0000000..1df55d3 --- /dev/null +++ b/hardware/wiring-diagram.svg @@ -0,0 +1,3 @@ + + +
ESP32 + GPS + LoRa + BMS
ESP32 + GPS + LoRa...
Li-Ion Battery
Li-Ion Battery
u.FL
u.FL
External GPS Antenna
External GPS Antenna
u.FL
u.FL
External LoRa Antenna
External LoRa Antenna
Status LED
Status LED
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/lib/E32-868T20D/E32-868T20D.cpp b/lib/E32-868T20D/E32-868T20D.cpp new file mode 100644 index 0000000..71d420d --- /dev/null +++ b/lib/E32-868T20D/E32-868T20D.cpp @@ -0,0 +1,45 @@ +#include "E32-868T20D.h" + +uint8_t E32_868T20D::checksum(uint8_t * data, size_t len) { + uint8_t cs = 0; + for (int i = 0; i < len; i++) + cs += data[i]; + return ~cs + 1; +} + +size_t E32_868T20D::encode(uint16_t address, const char * message, uint8_t * output, size_t * outputSize) { + size_t consumed = strnlen(message, 58); + *outputSize = consumed + 5; + output[0] = consumed; + output[1] = random(0xFF); + output[2] = address >> 8; + output[3] = address; + for (int i = 0; i < consumed; i++) + output[i+4] = message[i] ^ keys[output[1]]; + output[*outputSize-1] = checksum(output, *outputSize-1); + return consumed; +} + +uint16_t E32_868T20D::getAddress(uint8_t * packet) { + return (uint16_t)packet[2] << 8 | packet[3]; +} + +size_t E32_868T20D::decode(uint8_t * packet, char * output) { + size_t len = packet[0]; + if (checksum(packet, len+5)) return 0; + uint8_t index = packet[1]; + for (int i = 0; i < len; i++) + output[i] = packet[i+4] ^ keys[index]; + output[len] = 0; + return len; +} + +void E32_868T20D::generateKeyDumpPacket(uint16_t address, uint8_t keyIndex, uint8_t * output, size_t * outputSize) { + *outputSize = 6; + output[0] = 0x01; + output[1] = keyIndex; + output[2] = address >> 8; + output[3] = address; + output[4] = 0x00; + output[5] = checksum(output, *outputSize-1); +} diff --git a/lib/E32-868T20D/E32-868T20D.h b/lib/E32-868T20D/E32-868T20D.h new file mode 100644 index 0000000..37813c0 --- /dev/null +++ b/lib/E32-868T20D/E32-868T20D.h @@ -0,0 +1,34 @@ +#ifndef E32_868T20D_H +#define E32_868T20D_H + +#include + +class E32_868T20D { + const uint8_t keys[256] = { + 0x9A, 0x99, 0x98, 0x9F, 0x9E, 0x9D, 0x9C, 0xA3, 0xA2, 0xA1, 0xA0, 0xA7, 0xA6, 0xA5, 0xA4, 0xAB, + 0xAA, 0xA9, 0xA8, 0xAF, 0xAE, 0xAD, 0xAC, 0xB3, 0xB2, 0xB1, 0xB0, 0xB7, 0xB6, 0xB5, 0xB4, 0xBB, + 0xBA, 0xB9, 0xB8, 0xBF, 0xBE, 0xBD, 0xBC, 0xC3, 0xC2, 0xC1, 0xC0, 0xC7, 0xC6, 0xC5, 0xC4, 0xCB, + 0xCA, 0xC9, 0xC8, 0xCF, 0xCE, 0xCD, 0xCC, 0xD3, 0xD2, 0xD1, 0xD0, 0xD7, 0xD6, 0xD5, 0xD4, 0xDB, + 0xDA, 0xD9, 0xD8, 0xDF, 0xDE, 0xDD, 0xDC, 0xE3, 0xE2, 0xE1, 0xE0, 0xE7, 0xE6, 0xE5, 0xE4, 0xEB, + 0xEA, 0xE9, 0xE8, 0xEF, 0xEE, 0xED, 0xEC, 0xF3, 0xF2, 0xF1, 0xF0, 0xF7, 0xF6, 0xF5, 0xF4, 0xFB, + 0xFA, 0xF9, 0xF8, 0xFF, 0xFE, 0xFD, 0xFC, 0x03, 0x02, 0x01, 0x00, 0x07, 0x06, 0x05, 0x04, 0x0B, + 0x0A, 0x09, 0x08, 0x0F, 0x0E, 0x0D, 0x0C, 0x13, 0x12, 0x11, 0x10, 0x17, 0x16, 0x15, 0x14, 0x1B, + 0x1A, 0x19, 0x18, 0x1F, 0x1E, 0x1D, 0x1C, 0x23, 0x22, 0x21, 0x20, 0x27, 0x26, 0x25, 0x24, 0x2B, + 0x2A, 0x29, 0x28, 0x2F, 0x2E, 0x2D, 0x2C, 0x33, 0x32, 0x31, 0x30, 0x37, 0x36, 0x35, 0x34, 0x3B, + 0x3A, 0x39, 0x38, 0x3F, 0x3E, 0x3D, 0x3C, 0x43, 0x42, 0x41, 0x40, 0x47, 0x46, 0x45, 0x44, 0x4B, + 0x4A, 0x49, 0x48, 0x4F, 0x4E, 0x4D, 0x4C, 0x53, 0x52, 0x51, 0x50, 0x57, 0x56, 0x55, 0x54, 0x5B, + 0x5A, 0x59, 0x58, 0x5F, 0x5E, 0x5D, 0x5C, 0x63, 0x62, 0x61, 0x60, 0x67, 0x66, 0x65, 0x64, 0x6B, + 0x6A, 0x69, 0x68, 0x6F, 0x6E, 0x6D, 0x6C, 0x73, 0x72, 0x71, 0x70, 0x77, 0x76, 0x75, 0x74, 0x7B, + 0x7A, 0x79, 0x78, 0x7F, 0x7E, 0x7D, 0x7C, 0x83, 0x82, 0x81, 0x80, 0x87, 0x86, 0x85, 0x84, 0x8B, + 0x8A, 0x89, 0x88, 0x8F, 0x8E, 0x8D, 0x8C, 0x93, 0x92, 0x91, 0x90, 0x97, 0x96, 0x95, 0x94, 0x9B, + }; + + public: + uint8_t checksum(uint8_t * data, size_t len); + size_t encode(uint16_t address, const char * message, uint8_t * output, size_t * outputSize); + uint16_t getAddress(uint8_t * packet); + size_t decode(uint8_t * packet, char * output); + void generateKeyDumpPacket(uint16_t address, uint8_t keyIndex, uint8_t * output, size_t * outputSize); +}; + +#endif diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..58415f5 --- /dev/null +++ b/lib/README @@ -0,0 +1,45 @@ +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini index ffe8504..361ee82 100644 --- a/platformio.ini +++ b/platformio.ini @@ -8,18 +8,28 @@ ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html -[env:LilyGo TTGO T-Beam] +[env:TTGO T-Beam] platform = espressif32 board = ttgo-t-beam framework = arduino +monitor_speed = 115200 +src_filter = - + lib_deps = - bblanchon/ArduinoJson@^6.18.2 + metisvela/SailtrackModule@^1.6.0 lewisxhe/AXP202X_Library@^1.1.3 sparkfun/SparkFun u-blox GNSS Arduino Library@^2.0.9 - metisvela/SailTrack Module@^1.1.3 -monitor_speed = 115200 + jgromes/RadioLib@^5.1.2 +build_flags = + -D STM_NOTIFICATION_LED_PIN=4 + -D STM_NOTIFICATION_LED_ON_STATE=LOW + +; Uncomment to use OTA +; upload_protocol = espota +; upload_port = 192.168.42.101 -[env:LilyGo TTGO T-Beam OTA] -extends = env:LilyGo TTGO T-Beam -upload_protocol = espota -upload_port = sailtrack-radio.local +[env: TTGO T-Beam (KeyDump)] +extends = env:TTGO T-Beam +src_filter = + - +lib_deps = + lewisxhe/AXP202X_Library@^1.1.3 + jgromes/RadioLib@^5.1.2 diff --git a/src/keydump.cpp b/src/keydump.cpp new file mode 100644 index 0000000..8d84017 --- /dev/null +++ b/src/keydump.cpp @@ -0,0 +1,61 @@ +#include +#include +#include +#include + +// -------------------------- Configuration -------------------------- // + +#define LORA_CS_PIN 18 +#define LORA_DIO1_PIN 33 +#define LORA_RST_PIN 23 +#define LORA_BUSY_PIN 32 + +// EBYTE E32-868T20D parameters +#define E32_CHANNEL 0x09 +#define E32_ADDRESS 0x1310 +#define E32_BASE_FREQUENCY_MHZ 862 +#define E32_BANDWIDTH_KHZ 500 +#define E32_SPREADING_FACTOR 11 +#define E32_CODING_RATE_DENOM 5 + +// ------------------------------------------------------------------- // + +AXP20X_Class pmu; +SX1262 lora = new Module(LORA_CS_PIN, LORA_DIO1_PIN, LORA_RST_PIN, LORA_BUSY_PIN); +E32_868T20D e32; + +void beginPMU() { + Wire.begin(); + pmu.begin(Wire, AXP192_SLAVE_ADDRESS); + pmu.setPowerOutPut(AXP192_DCDC1, AXP202_OFF); // GPIO Pins Power Source + pmu.setPowerOutPut(AXP192_DCDC2, AXP202_OFF); // Unused + pmu.setPowerOutPut(AXP192_LDO2, AXP202_OFF); // LoRa Power Source + pmu.setPowerOutPut(AXP192_LDO3, AXP202_OFF); // GPS Power Source + pmu.setPowerOutPut(AXP192_EXTEN, AXP202_OFF); // External Connector Power Source +} + +void beginLora() { + pmu.setLDO2Voltage(3300); + pmu.setPowerOutPut(AXP192_LDO2, AXP202_ON); + lora.begin(E32_BASE_FREQUENCY_MHZ + E32_CHANNEL, E32_BANDWIDTH_KHZ, E32_SPREADING_FACTOR, E32_CODING_RATE_DENOM); +} + +void setup() { + Serial.begin(115200); + beginPMU(); + beginLora(); + + Serial.println("Dumping keys..."); + uint8_t packet[64]; + size_t len; + for (int i = 0; i <= 0xFF; i++) { + e32.generateKeyDumpPacket(E32_ADDRESS, (uint8_t)i, packet, &len); + lora.transmit(packet, len); + Serial.print("Completed: "); + Serial.print(i+1); Serial.print("/"); Serial.println(256); + delay(100); + } + Serial.println("Done!"); +} + +void loop() {} diff --git a/src/main.cpp b/src/main.cpp index 53e2836..d925769 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,87 +1,172 @@ #include -#include +#include #include #include -#include +#include +#include + +// -------------------------- Configuration -------------------------- // + +#define MQTT_PUBLISH_FREQ_HZ 5 +#define LORA_SEND_FREQ_HZ 1 + +#define GPS_BAUD_RATE 9600 +#define GPS_SERIAL_CONFIG SERIAL_8N1 +#define GPS_RX_PIN 34 +#define GPS_TX_PIN 12 +#define GPS_NAVIGATION_FREQ_HZ MQTT_PUBLISH_FREQ_HZ + +#define LORA_CS_PIN 18 +#define LORA_DIO1_PIN 33 +#define LORA_RST_PIN 23 +#define LORA_BUSY_PIN 32 +#define LORA_MESSAGE_BUFFER_SIZE 512 +#define LORA_PACKET_SIZE 64 + +// EBYTE E32-868T20D parameters +#define E32_CHANNEL 0x09 +#define E32_ADDRESS 0x1310 +#define E32_BASE_FREQUENCY_MHZ 862 +#define E32_BANDWIDTH_KHZ 500 +#define E32_SPREADING_FACTOR 11 +#define E32_CODING_RATE_DENOM 5 -#define I2C_SDA 21 -#define I2C_SCL 22 +#define LOOP_TASK_INTERVAL_MS 1000 / (2 * GPS_NAVIGATION_FREQ_HZ) +#define LORA_TASK_INTERVAL_MS 1000 / LORA_SEND_FREQ_HZ -#define GPS_RX_PIN 34 -#define GPS_TX_PIN 12 -#define GPS_BAND_RATE 9600 +struct LoraMetric { + char value[32]; + char topic[32]; + char name[32]; +} loraMetrics[] = { + { "0", "sensor/gps0", "fixType" }, + { "0", "sensor/gps0", "epoch" }, + { "0", "sensor/gps0", "lon" }, + { "0", "sensor/gps0", "lat" }, + { "0", "sensor/gps0", "gSpeed" }, + { "0", "sensor/gps0", "headMot" }, + { "0", "sensor/imu0", "euler.x"}, + { "0", "sensor/imu0", "euler.y" }, + { "0", "sensor/imu0", "euler.z" } +}; + +// ------------------------------------------------------------------- // -SFE_UBLOX_GNSS GPS; -AXP20X_Class PMU; +SailtrackModule stm; +SFE_UBLOX_GNSS gps; +AXP20X_Class pmu; +SX1262 lora = new Module(LORA_CS_PIN, LORA_DIO1_PIN, LORA_RST_PIN, LORA_BUSY_PIN); +E32_868T20D e32; + +size_t loraSentBytes = 0; class ModuleCallbacks: public SailtrackModuleCallbacks { - void onWifiConnectionBegin() { - // TODO: Notify user - } - - void onWifiConnectionResult(wl_status_t status) { - // TODO: Notify user + void onStatusPublish(JsonObject status) { + JsonObject battery = status.createNestedObject("battery"); + battery["voltage"] = pmu.getBattVoltage() / 1000; + JsonObject lora = status.createNestedObject("lora"); + lora["bitrate"] = loraSentBytes * 8 * STM_STATUS_PUBLISH_FREQ_HZ / 1000; + loraSentBytes = 0; } - DynamicJsonDocument getStatus() { - DynamicJsonDocument payload(300); - JsonObject battery = payload.createNestedObject("battery"); - JsonObject cpu = payload.createNestedObject("cpu"); - battery["voltage"] = PMU.getBattVoltage() / 1000; - battery["charging"] = PMU.isChargeing(); - cpu["temperature"] = temperatureRead(); - return payload; + void onMqttMessage(const char * topic, JsonObjectConst message) { + for (int i = 0; i < sizeof(loraMetrics)/sizeof(*loraMetrics); i++) { + LoraMetric & metric = loraMetrics[i]; + if (!strcmp(topic, metric.topic)) { + char metricName[strlen(metric.name)+1]; + strcpy(metricName, metric.name); + char * token = strtok(metricName, "."); + JsonVariantConst tmpVal = message; + while (token) { + if (!tmpVal.containsKey(token)) break; + tmpVal = tmpVal[token]; + token = strtok(NULL, "."); + } + if (!token) serializeJson(tmpVal, metric.value); + } + } } }; -void onGPSData(UBX_NAV_PVT_data_t ubxDataStruct) { - DynamicJsonDocument payload(300); - payload["latitude"] = ubxDataStruct.lat; - payload["longitude"] = ubxDataStruct.lon; - payload["speed"] = ubxDataStruct.gSpeed; - payload["heading"] = ubxDataStruct.headMot; - payload["vacc"] = ubxDataStruct.vAcc; - payload["hacc"] = ubxDataStruct.hAcc; - payload["sacc"] = ubxDataStruct.sAcc; - payload["headacc"] = ubxDataStruct.headAcc; - STModule.publish("sensor/gps0", "gps0", payload); +void loraTask(void * pvArguments) { + TickType_t lastWakeTime = xTaskGetTickCount(); + while (true) { + char message[LORA_MESSAGE_BUFFER_SIZE]; + strcpy(message, loraMetrics[0].value); + for (int i = 1; i < sizeof(loraMetrics)/sizeof(*loraMetrics); i++) { + strcat(message, " "); + strcat(message, loraMetrics[i].value); + } + strcat(message, "\n"); + + uint8_t packet[LORA_PACKET_SIZE]; + size_t len; + size_t consumed = 0; + size_t toConsume = strlen(message); + while (consumed < toConsume) { + consumed += e32.encode(E32_ADDRESS, message + consumed, packet, &len); + lora.transmit(packet, len); + loraSentBytes += len; + } + vTaskDelayUntil(&lastWakeTime, pdMS_TO_TICKS(LORA_TASK_INTERVAL_MS)); + } } void beginPMU() { - Wire.begin(I2C_SDA, I2C_SCL); - PMU.begin(Wire, AXP192_SLAVE_ADDRESS); - PMU.setPowerOutPut(AXP192_DCDC2, AXP202_OFF); - PMU.setPowerOutPut(AXP192_LDO2, AXP202_OFF); - PMU.setPowerOutPut(AXP192_LDO3, AXP202_OFF); - PMU.setPowerOutPut(AXP192_EXTEN, AXP202_OFF); + Wire.begin(); + pmu.begin(Wire, AXP192_SLAVE_ADDRESS); + pmu.setPowerOutPut(AXP192_DCDC1, AXP202_OFF); // GPIO Pins Power Source + pmu.setPowerOutPut(AXP192_DCDC2, AXP202_OFF); // Unused + pmu.setPowerOutPut(AXP192_LDO2, AXP202_OFF); // LoRa Power Source + pmu.setPowerOutPut(AXP192_LDO3, AXP202_OFF); // GPS Power Source + pmu.setPowerOutPut(AXP192_EXTEN, AXP202_OFF); // External Connector Power Source } void beginGPS() { - PMU.setLDO3Voltage(3300); - PMU.setPowerOutPut(AXP192_LDO3, AXP202_ON); - Serial1.begin(GPS_BAND_RATE, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); - GPS.begin(Serial1); - GPS.setUART1Output(COM_TYPE_UBX); - GPS.setMeasurementRate(200); - GPS.setAutoPVTcallback(&onGPSData); + pmu.setLDO3Voltage(3300); + pmu.setPowerOutPut(AXP192_LDO3, AXP202_ON); + Serial1.begin(GPS_BAUD_RATE, GPS_SERIAL_CONFIG, GPS_RX_PIN, GPS_TX_PIN); + gps.begin(Serial1); + gps.setUART1Output(COM_TYPE_UBX); + gps.setDynamicModel(DYN_MODEL_SEA); + gps.setNavigationFrequency(GPS_NAVIGATION_FREQ_HZ); + gps.setAutoPVT(true); } void beginLora() { - PMU.setLDO2Voltage(3300); - PMU.setPowerOutPut(AXP192_LDO2, AXP202_ON); - // TODO: Init LoRa + pmu.setLDO2Voltage(3300); + pmu.setPowerOutPut(AXP192_LDO2, AXP202_ON); + lora.begin(E32_BASE_FREQUENCY_MHZ + E32_CHANNEL, E32_BANDWIDTH_KHZ, E32_SPREADING_FACTOR, E32_CODING_RATE_DENOM); + for (auto metric : loraMetrics) stm.subscribe(metric.topic); + xTaskCreate(loraTask, "loraTask", STM_TASK_MEDIUM_STACK_SIZE, NULL, STM_TASK_MEDIUM_PRIORITY, NULL); } void setup() { beginPMU(); - STModule.begin("radio", "sailtrack-radio", IPAddress(192, 168, 42, 101)); - STModule.setCallbacks(new ModuleCallbacks()); + stm.begin("radio", IPAddress(192, 168, 42, 101), new ModuleCallbacks()); beginGPS(); - //beginLora(); + beginLora(); } void loop() { - GPS.checkUblox(); - GPS.checkCallbacks(); - delay(50); + TickType_t lastWakeTime = xTaskGetTickCount(); + if (gps.getPVT() && gps.getTimeValid()) { + StaticJsonDocument doc; + doc["fixType"] = gps.getFixType(); // GNSSfix Type: 0 = no fix, 1 = dead reckoning only, 2 = 2D-fix, 3 = 3D-fix + doc["epoch"] = gps.getUnixEpoch(); // Get the current Unix epoch time rounded to the nearest second + doc["lon"] = gps.getLongitude(); // Longitude: deg * 1e-7 + doc["lat"] = gps.getLatitude(); // Latitude: deg * 1e-7 + doc["hMSL"] = gps.getAltitudeMSL(); // Height above mean sea level: mm + doc["hAcc"] = gps.getHorizontalAccEst(); // Horizontal accuracy estimate: mm + doc["vAcc"] = gps.getVerticalAccEst(); // Vertical accuracy estimate: mm + doc["velN"] = gps.getNedNorthVel(); // NED north velocity: mm/s + doc["velE"] = gps.getNedEastVel(); // NED east velocity: mm/s + doc["velD"] = gps.getNedDownVel(); // NED down velocity: mm/s + doc["gSpeed"] = gps.getGroundSpeed(); // Ground Speed (2-D): mm/s + doc["headMot"] = gps.getHeading(); // Heading of motion (2-D): deg * 1e-5 + doc["sAcc"] = gps.getSpeedAccEst(); // Speed accuracy estimate: mm/s + doc["headAcc"] = gps.getHeadingAccEst(); // Heading accuracy estimate (both motion and vehicle): deg * 1e-5 + stm.publish("sensor/gps0", doc.as()); + } + vTaskDelayUntil(&lastWakeTime, pdMS_TO_TICKS(LOOP_TASK_INTERVAL_MS)); }