mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-05 23:27:38 +03:00
Merge 3532f90f1e into 3ce11f14ce
This commit is contained in:
commit
90e93abff9
12
docs/emulation.md
Normal file
12
docs/emulation.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Emulation (development)
|
||||||
|
|
||||||
|
TODO: write this
|
||||||
|
|
||||||
|
Build the program using `env:emulation`, this is required to re-route `Serial.print` to qemu's stdout.
|
||||||
|
|
||||||
|
To run it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd scripts/emulation
|
||||||
|
docker compose up
|
||||||
|
```
|
||||||
@ -3,7 +3,7 @@
|
|||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <JpegToBmpConverter.h>
|
#include <JpegToBmpConverter.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <ZipFile.h>
|
#include <ZipFile.h>
|
||||||
|
|
||||||
#include "Epub/parsers/ContainerParser.h"
|
#include "Epub/parsers/ContainerParser.h"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
#include "Section.h"
|
#include "Section.h"
|
||||||
|
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include "Page.h"
|
#include "Page.h"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <EpdFontFamily.h>
|
#include <EpdFontFamily.h>
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <expat.h>
|
#include <expat.h>
|
||||||
|
|
||||||
#include "../Page.h"
|
#include "../Page.h"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
|
|||||||
@ -10,19 +10,19 @@ void GfxRenderer::rotateCoordinates(const int x, const int y, int* rotatedX, int
|
|||||||
// Logical portrait (480x800) → panel (800x480)
|
// Logical portrait (480x800) → panel (800x480)
|
||||||
// Rotation: 90 degrees clockwise
|
// Rotation: 90 degrees clockwise
|
||||||
*rotatedX = y;
|
*rotatedX = y;
|
||||||
*rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x;
|
*rotatedY = HalDisplay::DISPLAY_HEIGHT - 1 - x;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case LandscapeClockwise: {
|
case LandscapeClockwise: {
|
||||||
// Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right)
|
// Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right)
|
||||||
*rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - x;
|
*rotatedX = HalDisplay::DISPLAY_WIDTH - 1 - x;
|
||||||
*rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - y;
|
*rotatedY = HalDisplay::DISPLAY_HEIGHT - 1 - y;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PortraitInverted: {
|
case PortraitInverted: {
|
||||||
// Logical portrait (480x800) → panel (800x480)
|
// Logical portrait (480x800) → panel (800x480)
|
||||||
// Rotation: 90 degrees counter-clockwise
|
// Rotation: 90 degrees counter-clockwise
|
||||||
*rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - y;
|
*rotatedX = HalDisplay::DISPLAY_WIDTH - 1 - y;
|
||||||
*rotatedY = x;
|
*rotatedY = x;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -49,14 +49,14 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
|||||||
rotateCoordinates(x, y, &rotatedX, &rotatedY);
|
rotateCoordinates(x, y, &rotatedX, &rotatedY);
|
||||||
|
|
||||||
// Bounds checking against physical panel dimensions
|
// Bounds checking against physical panel dimensions
|
||||||
if (rotatedX < 0 || rotatedX >= EInkDisplay::DISPLAY_WIDTH || rotatedY < 0 ||
|
if (rotatedX < 0 || rotatedX >= HalDisplay::DISPLAY_WIDTH || rotatedY < 0 ||
|
||||||
rotatedY >= EInkDisplay::DISPLAY_HEIGHT) {
|
rotatedY >= HalDisplay::DISPLAY_HEIGHT) {
|
||||||
Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, rotatedX, rotatedY);
|
Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, rotatedX, rotatedY);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate byte position and bit position
|
// Calculate byte position and bit position
|
||||||
const uint16_t byteIndex = rotatedY * EInkDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8);
|
const uint16_t byteIndex = rotatedY * HalDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8);
|
||||||
const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first
|
const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first
|
||||||
|
|
||||||
if (state) {
|
if (state) {
|
||||||
@ -392,12 +392,12 @@ void GfxRenderer::invertScreen() const {
|
|||||||
Serial.printf("[%lu] [GFX] !! No framebuffer in invertScreen\n", millis());
|
Serial.printf("[%lu] [GFX] !! No framebuffer in invertScreen\n", millis());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) {
|
for (int i = 0; i < HalDisplay::BUFFER_SIZE; i++) {
|
||||||
buffer[i] = ~buffer[i];
|
buffer[i] = ~buffer[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) const {
|
void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const {
|
||||||
einkDisplay.displayBuffer(refreshMode);
|
einkDisplay.displayBuffer(refreshMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,13 +418,13 @@ int GfxRenderer::getScreenWidth() const {
|
|||||||
case Portrait:
|
case Portrait:
|
||||||
case PortraitInverted:
|
case PortraitInverted:
|
||||||
// 480px wide in portrait logical coordinates
|
// 480px wide in portrait logical coordinates
|
||||||
return EInkDisplay::DISPLAY_HEIGHT;
|
return HalDisplay::DISPLAY_HEIGHT;
|
||||||
case LandscapeClockwise:
|
case LandscapeClockwise:
|
||||||
case LandscapeCounterClockwise:
|
case LandscapeCounterClockwise:
|
||||||
// 800px wide in landscape logical coordinates
|
// 800px wide in landscape logical coordinates
|
||||||
return EInkDisplay::DISPLAY_WIDTH;
|
return HalDisplay::DISPLAY_WIDTH;
|
||||||
}
|
}
|
||||||
return EInkDisplay::DISPLAY_HEIGHT;
|
return HalDisplay::DISPLAY_HEIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getScreenHeight() const {
|
int GfxRenderer::getScreenHeight() const {
|
||||||
@ -432,13 +432,13 @@ int GfxRenderer::getScreenHeight() const {
|
|||||||
case Portrait:
|
case Portrait:
|
||||||
case PortraitInverted:
|
case PortraitInverted:
|
||||||
// 800px tall in portrait logical coordinates
|
// 800px tall in portrait logical coordinates
|
||||||
return EInkDisplay::DISPLAY_WIDTH;
|
return HalDisplay::DISPLAY_WIDTH;
|
||||||
case LandscapeClockwise:
|
case LandscapeClockwise:
|
||||||
case LandscapeCounterClockwise:
|
case LandscapeCounterClockwise:
|
||||||
// 480px tall in landscape logical coordinates
|
// 480px tall in landscape logical coordinates
|
||||||
return EInkDisplay::DISPLAY_HEIGHT;
|
return HalDisplay::DISPLAY_HEIGHT;
|
||||||
}
|
}
|
||||||
return EInkDisplay::DISPLAY_WIDTH;
|
return HalDisplay::DISPLAY_WIDTH;
|
||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getSpaceWidth(const int fontId) const {
|
int GfxRenderer::getSpaceWidth(const int fontId) const {
|
||||||
@ -640,9 +640,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
|||||||
|
|
||||||
uint8_t* GfxRenderer::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); }
|
uint8_t* GfxRenderer::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); }
|
||||||
|
|
||||||
size_t GfxRenderer::getBufferSize() { return EInkDisplay::BUFFER_SIZE; }
|
size_t GfxRenderer::getBufferSize() { return HalDisplay::BUFFER_SIZE; }
|
||||||
|
|
||||||
void GfxRenderer::grayscaleRevert() const { einkDisplay.grayscaleRevert(); }
|
|
||||||
|
|
||||||
void GfxRenderer::copyGrayscaleLsbBuffers() const { einkDisplay.copyGrayscaleLsbBuffers(einkDisplay.getFrameBuffer()); }
|
void GfxRenderer::copyGrayscaleLsbBuffers() const { einkDisplay.copyGrayscaleLsbBuffers(einkDisplay.getFrameBuffer()); }
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <EInkDisplay.h>
|
#include <HalDisplay.h>
|
||||||
#include <EpdFontFamily.h>
|
#include <EpdFontFamily.h>
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
@ -21,11 +21,11 @@ class GfxRenderer {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory
|
static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory
|
||||||
static constexpr size_t BW_BUFFER_NUM_CHUNKS = EInkDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE;
|
static constexpr size_t BW_BUFFER_NUM_CHUNKS = HalDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE;
|
||||||
static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == EInkDisplay::BUFFER_SIZE,
|
static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == HalDisplay::BUFFER_SIZE,
|
||||||
"BW buffer chunking does not line up with display buffer size");
|
"BW buffer chunking does not line up with display buffer size");
|
||||||
|
|
||||||
EInkDisplay& einkDisplay;
|
HalDisplay& einkDisplay;
|
||||||
RenderMode renderMode;
|
RenderMode renderMode;
|
||||||
Orientation orientation;
|
Orientation orientation;
|
||||||
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
|
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
|
||||||
@ -36,7 +36,7 @@ class GfxRenderer {
|
|||||||
void rotateCoordinates(int x, int y, int* rotatedX, int* rotatedY) const;
|
void rotateCoordinates(int x, int y, int* rotatedX, int* rotatedY) const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW), orientation(Portrait) {}
|
explicit GfxRenderer(HalDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW), orientation(Portrait) {}
|
||||||
~GfxRenderer() { freeBwBufferChunks(); }
|
~GfxRenderer() { freeBwBufferChunks(); }
|
||||||
|
|
||||||
static constexpr int VIEWABLE_MARGIN_TOP = 9;
|
static constexpr int VIEWABLE_MARGIN_TOP = 9;
|
||||||
@ -54,7 +54,7 @@ class GfxRenderer {
|
|||||||
// Screen ops
|
// Screen ops
|
||||||
int getScreenWidth() const;
|
int getScreenWidth() const;
|
||||||
int getScreenHeight() const;
|
int getScreenHeight() const;
|
||||||
void displayBuffer(EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const;
|
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
||||||
// EXPERIMENTAL: Windowed update - display only a rectangular region
|
// EXPERIMENTAL: Windowed update - display only a rectangular region
|
||||||
void displayWindow(int x, int y, int width, int height) const;
|
void displayWindow(int x, int y, int width, int height) const;
|
||||||
void invertScreen() const;
|
void invertScreen() const;
|
||||||
@ -106,6 +106,6 @@ class GfxRenderer {
|
|||||||
// Low level functions
|
// Low level functions
|
||||||
uint8_t* getFrameBuffer() const;
|
uint8_t* getFrameBuffer() const;
|
||||||
static size_t getBufferSize();
|
static size_t getBufferSize();
|
||||||
void grayscaleRevert() const;
|
// void grayscaleRevert() const; // unused
|
||||||
void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const;
|
void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
#include "JpegToBmpConverter.h"
|
#include "JpegToBmpConverter.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
#include <picojpeg.h>
|
#include <picojpeg.h>
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <MD5Builder.h>
|
#include <MD5Builder.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
// Initialize the static instance
|
// Initialize the static instance
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <MD5Builder.h>
|
#include <MD5Builder.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Extract filename from path (everything after last '/')
|
// Extract filename from path (everything after last '/')
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
bool Xtc::load() {
|
bool Xtc::load() {
|
||||||
Serial.printf("[%lu] [XTC] Loading XTC: %s\n", millis(), filepath.c_str());
|
Serial.printf("[%lu] [XTC] Loading XTC: %s\n", millis(), filepath.c_str());
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
#include "ZipFile.h"
|
#include "ZipFile.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <miniz.h>
|
#include <miniz.h>
|
||||||
|
|
||||||
bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t* outputBuf, const size_t inflatedSize) {
|
bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t* outputBuf, const size_t inflatedSize) {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|||||||
186
lib/hal/EmulationUtils.h
Normal file
186
lib/hal/EmulationUtils.h
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace EmulationUtils {
|
||||||
|
|
||||||
|
// Packet format (from device to host):
|
||||||
|
// $$CMD_(COMMAND):[(ARG0)][:(ARG1)][:(ARG2)]$$\n
|
||||||
|
// Host response with either a base64-encoded payload or int64, terminated by newline:
|
||||||
|
// (BASE64_ENCODED_PAYLOAD)\n
|
||||||
|
// 123\n
|
||||||
|
|
||||||
|
static const char* CMD_DISPLAY = "DISPLAY"; // arg0: base64-encoded buffer -- no return value
|
||||||
|
static const char* CMD_FS_LIST = "FS_LIST"; // arg0: path, arg1: max files -- returns list of filenames, one per line, terminated by empty line
|
||||||
|
static const char* CMD_FS_READ = "FS_READ"; // arg0: path, arg1: offset, arg2: length (-1 means read all) -- returns base64-encoded file contents
|
||||||
|
static const char* CMD_FS_STAT = "FS_STAT"; // arg0: path -- return file size int64_t; -1 means not found; -2 means is a directory
|
||||||
|
static const char* CMD_FS_WRITE = "FS_WRITE"; // arg0: path, arg1: base64-encoded data, arg2: offset, arg3: is inplace (0/1) -- return int64_t bytes written
|
||||||
|
static const char* CMD_FS_MKDIR = "FS_MKDIR"; // arg0: path -- return int64_t: 0=success
|
||||||
|
static const char* CMD_FS_RM = "FS_RM"; // arg0: path -- return int64_t: 0=success
|
||||||
|
static const char* CMD_BUTTON = "BUTTON"; // arg0: action ("read") -- return int64_t: HalInput state bitmask
|
||||||
|
|
||||||
|
static std::string base64_encode(const char* buf, unsigned int bufLen);
|
||||||
|
static std::vector<uint8_t> base64_decode(const char* encoded_string, unsigned int in_len);
|
||||||
|
|
||||||
|
static SemaphoreHandle_t sendMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
|
static void sendCmd(const char* cmd, const char* arg0 = nullptr, const char* arg1 = nullptr, const char* arg2 = nullptr, const char* arg3 = nullptr) {
|
||||||
|
xSemaphoreTake(sendMutex, portMAX_DELAY);
|
||||||
|
if (cmd != CMD_BUTTON) {
|
||||||
|
Serial.printf("[%lu] [EMU] Sending command: %s\n", millis(), cmd);
|
||||||
|
}
|
||||||
|
Serial.print("$$CMD_");
|
||||||
|
Serial.print(cmd);
|
||||||
|
if (arg0 != nullptr) {
|
||||||
|
Serial.print(":");
|
||||||
|
Serial.print(arg0);
|
||||||
|
} else {
|
||||||
|
Serial.print(":"); // ensure at least one colon
|
||||||
|
}
|
||||||
|
if (arg1 != nullptr) {
|
||||||
|
Serial.print(":");
|
||||||
|
Serial.print(arg1);
|
||||||
|
}
|
||||||
|
if (arg2 != nullptr) {
|
||||||
|
Serial.print(":");
|
||||||
|
Serial.print(arg2);
|
||||||
|
}
|
||||||
|
if (arg3 != nullptr) {
|
||||||
|
Serial.print(":");
|
||||||
|
Serial.print(arg3);
|
||||||
|
}
|
||||||
|
Serial.print("$$\n");
|
||||||
|
xSemaphoreGive(sendMutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String recvRespStr() {
|
||||||
|
unsigned long startMillis = millis();
|
||||||
|
String line;
|
||||||
|
const unsigned long timeoutMs = 5000;
|
||||||
|
while (millis() - startMillis < (unsigned long)timeoutMs) {
|
||||||
|
if (Serial.available()) {
|
||||||
|
char c = Serial.read();
|
||||||
|
if (c == '\n') {
|
||||||
|
return line;
|
||||||
|
} else {
|
||||||
|
line += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// should never reach here
|
||||||
|
Serial.println("FATAL: Timeout waiting for response");
|
||||||
|
return String();
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<uint8_t> recvRespBuf() {
|
||||||
|
String respStr = recvRespStr();
|
||||||
|
return base64_decode(respStr.c_str(), respStr.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
static int64_t recvRespInt64() {
|
||||||
|
String respStr = recvRespStr();
|
||||||
|
return respStr.toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
static const std::string base64_chars =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
"abcdefghijklmnopqrstuvwxyz"
|
||||||
|
"0123456789+/";
|
||||||
|
|
||||||
|
static inline bool is_base64(char c) {
|
||||||
|
return (isalnum(c) || (c == '+') || (c == '/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string base64_encode(const char* buf, unsigned int bufLen) {
|
||||||
|
std::string ret;
|
||||||
|
ret.reserve(bufLen * 4 / 3 + 4); // reserve enough space
|
||||||
|
int i = 0;
|
||||||
|
int j = 0;
|
||||||
|
char char_array_3[3];
|
||||||
|
char char_array_4[4];
|
||||||
|
|
||||||
|
while (bufLen--) {
|
||||||
|
char_array_3[i++] = *(buf++);
|
||||||
|
if (i == 3) {
|
||||||
|
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
|
||||||
|
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
|
||||||
|
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
|
||||||
|
char_array_4[3] = char_array_3[2] & 0x3f;
|
||||||
|
|
||||||
|
for(i = 0; (i < 4) ; i++) {
|
||||||
|
ret += base64_chars[char_array_4[i]];
|
||||||
|
}
|
||||||
|
i = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i) {
|
||||||
|
for(j = i; j < 3; j++) {
|
||||||
|
char_array_3[j] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
|
||||||
|
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
|
||||||
|
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
|
||||||
|
char_array_4[3] = char_array_3[2] & 0x3f;
|
||||||
|
|
||||||
|
for (j = 0; (j < i + 1); j++) {
|
||||||
|
ret += base64_chars[char_array_4[j]];
|
||||||
|
}
|
||||||
|
|
||||||
|
while((i++ < 3)) {
|
||||||
|
ret += '=';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<uint8_t> base64_decode(const char* encoded_string, unsigned int in_len) {
|
||||||
|
int i = 0;
|
||||||
|
int j = 0;
|
||||||
|
int in_ = 0;
|
||||||
|
char char_array_4[4], char_array_3[3];
|
||||||
|
std::vector<uint8_t> ret;
|
||||||
|
|
||||||
|
while (in_len-- && ( encoded_string[in_] != '=') && is_base64(encoded_string[in_])) {
|
||||||
|
char_array_4[i++] = encoded_string[in_]; in_++;
|
||||||
|
if (i ==4) {
|
||||||
|
for (i = 0; i < 4; i++) {
|
||||||
|
char_array_4[i] = base64_chars.find(char_array_4[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
|
||||||
|
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
|
||||||
|
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
|
||||||
|
|
||||||
|
for (i = 0; (i < 3); i++) {
|
||||||
|
ret.push_back(char_array_3[i]);
|
||||||
|
}
|
||||||
|
i = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i) {
|
||||||
|
for (j = i; j < 4; j++) {
|
||||||
|
char_array_4[j] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (j = 0; j < 4; j++) {
|
||||||
|
char_array_4[j] = base64_chars.find(char_array_4[j]);
|
||||||
|
}
|
||||||
|
|
||||||
|
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
|
||||||
|
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
|
||||||
|
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
|
||||||
|
|
||||||
|
for (j = 0; (j < i - 1); j++) {
|
||||||
|
ret.push_back(char_array_3[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace EmulationUtils
|
||||||
155
lib/hal/HalDisplay.cpp
Normal file
155
lib/hal/HalDisplay.cpp
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
#include <HalDisplay.h>
|
||||||
|
#include <EmulationUtils.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
HalDisplay::HalDisplay(int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, int8_t rst, int8_t busy) : einkDisplay(sclk, mosi, cs, dc, rst, busy) {
|
||||||
|
if (is_emulated) {
|
||||||
|
emuFramebuffer0 = new uint8_t[BUFFER_SIZE];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HalDisplay::~HalDisplay() {
|
||||||
|
if (emuFramebuffer0) {
|
||||||
|
delete[] emuFramebuffer0;
|
||||||
|
emuFramebuffer0 = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HalDisplay::begin() {
|
||||||
|
if (!is_emulated) {
|
||||||
|
einkDisplay.begin();
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [ ] Emulated display initialized\n", millis());
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HalDisplay::clearScreen(uint8_t color) const {
|
||||||
|
if (!is_emulated) {
|
||||||
|
einkDisplay.clearScreen(color);
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [ ] Emulated clear screen with color 0x%02X\n", millis(), color);
|
||||||
|
memset(emuFramebuffer0, color, BUFFER_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HalDisplay::drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool fromProgmem) const {
|
||||||
|
if (!is_emulated) {
|
||||||
|
einkDisplay.drawImage(imageData, x, y, w, h, fromProgmem);
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [ ] Emulated draw image at (%u, %u) with size %ux%u\n", millis(), x, y, w, h);
|
||||||
|
|
||||||
|
// Calculate bytes per line for the image
|
||||||
|
const uint16_t imageWidthBytes = w / 8;
|
||||||
|
|
||||||
|
// Copy image data to frame buffer
|
||||||
|
for (uint16_t row = 0; row < h; row++) {
|
||||||
|
const uint16_t destY = y + row;
|
||||||
|
if (destY >= DISPLAY_HEIGHT)
|
||||||
|
break;
|
||||||
|
|
||||||
|
const uint16_t destOffset = destY * DISPLAY_WIDTH_BYTES + (x / 8);
|
||||||
|
const uint16_t srcOffset = row * imageWidthBytes;
|
||||||
|
|
||||||
|
for (uint16_t col = 0; col < imageWidthBytes; col++) {
|
||||||
|
if ((x / 8 + col) >= DISPLAY_WIDTH_BYTES)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (fromProgmem) {
|
||||||
|
emuFramebuffer0[destOffset + col] = pgm_read_byte(&imageData[srcOffset + col]);
|
||||||
|
} else {
|
||||||
|
emuFramebuffer0[destOffset + col] = imageData[srcOffset + col];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HalDisplay::displayBuffer(RefreshMode mode) {
|
||||||
|
if (!is_emulated) {
|
||||||
|
einkDisplay.displayBuffer(mode);
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [ ] Emulated display buffer with mode %d\n", millis(), static_cast<int>(mode));
|
||||||
|
std::string b64 = EmulationUtils::base64_encode(reinterpret_cast<char*>(emuFramebuffer0), BUFFER_SIZE);
|
||||||
|
EmulationUtils::sendCmd(EmulationUtils::CMD_DISPLAY, b64.c_str());
|
||||||
|
// no response expected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HalDisplay::refreshDisplay(RefreshMode mode, bool turnOffScreen) {
|
||||||
|
if (!is_emulated) {
|
||||||
|
einkDisplay.refreshDisplay(mode, turnOffScreen);
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [ ] Emulated refresh display with mode %d, turnOffScreen %d\n", millis(), static_cast<int>(mode), turnOffScreen);
|
||||||
|
// emulated delay
|
||||||
|
if (mode == RefreshMode::FAST_REFRESH) {
|
||||||
|
delay(500);
|
||||||
|
} else if (mode == RefreshMode::HALF_REFRESH) {
|
||||||
|
delay(1000);
|
||||||
|
} else if (mode == RefreshMode::FULL_REFRESH) {
|
||||||
|
delay(2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HalDisplay::deepSleep() {
|
||||||
|
if (!is_emulated) {
|
||||||
|
einkDisplay.deepSleep();
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [ ] Emulated deep sleep\n", millis());
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* HalDisplay::getFrameBuffer() const {
|
||||||
|
if (!is_emulated) {
|
||||||
|
return einkDisplay.getFrameBuffer();
|
||||||
|
} else {
|
||||||
|
return emuFramebuffer0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HalDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer) {
|
||||||
|
if (!is_emulated) {
|
||||||
|
einkDisplay.copyGrayscaleBuffers(lsbBuffer, msbBuffer);
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [ ] Emulated copy grayscale buffers\n", millis());
|
||||||
|
// TODO: not sure what this does
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HalDisplay::copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer) {
|
||||||
|
if (!is_emulated) {
|
||||||
|
einkDisplay.copyGrayscaleLsbBuffers(lsbBuffer);
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [ ] Emulated copy grayscale LSB buffers\n", millis());
|
||||||
|
// TODO: not sure what this does
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HalDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) {
|
||||||
|
if (!is_emulated) {
|
||||||
|
einkDisplay.copyGrayscaleMsbBuffers(msbBuffer);
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [ ] Emulated copy grayscale MSB buffers\n", millis());
|
||||||
|
// TODO: not sure what this does
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HalDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) {
|
||||||
|
if (!is_emulated) {
|
||||||
|
einkDisplay.cleanupGrayscaleBuffers(bwBuffer);
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [ ] Emulated cleanup grayscale buffers\n", millis());
|
||||||
|
// TODO: not sure what this does
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HalDisplay::displayGrayBuffer() {
|
||||||
|
if (!is_emulated) {
|
||||||
|
einkDisplay.displayGrayBuffer();
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [ ] Emulated display gray buffer\n", millis());
|
||||||
|
// TODO: not sure what this does
|
||||||
|
}
|
||||||
|
}
|
||||||
57
lib/hal/HalDisplay.h
Normal file
57
lib/hal/HalDisplay.h
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <EInkDisplay.h>
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <SPI.h>
|
||||||
|
|
||||||
|
class HalDisplay {
|
||||||
|
public:
|
||||||
|
// Constructor with pin configuration
|
||||||
|
HalDisplay(int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, int8_t rst, int8_t busy);
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
~HalDisplay();
|
||||||
|
|
||||||
|
// Refresh modes (guarded to avoid redefinition in test builds)
|
||||||
|
using RefreshMode = EInkDisplay::RefreshMode;
|
||||||
|
static constexpr EInkDisplay::RefreshMode FULL_REFRESH = EInkDisplay::FULL_REFRESH;
|
||||||
|
static constexpr EInkDisplay::RefreshMode HALF_REFRESH = EInkDisplay::HALF_REFRESH;
|
||||||
|
static constexpr EInkDisplay::RefreshMode FAST_REFRESH = EInkDisplay::FAST_REFRESH;
|
||||||
|
|
||||||
|
// Initialize the display hardware and driver
|
||||||
|
void begin();
|
||||||
|
|
||||||
|
// Display dimensions
|
||||||
|
static constexpr uint16_t DISPLAY_WIDTH = 800;
|
||||||
|
static constexpr uint16_t DISPLAY_HEIGHT = 480;
|
||||||
|
static constexpr uint16_t DISPLAY_WIDTH_BYTES = DISPLAY_WIDTH / 8;
|
||||||
|
static constexpr uint32_t BUFFER_SIZE = DISPLAY_WIDTH_BYTES * DISPLAY_HEIGHT;
|
||||||
|
|
||||||
|
// Frame buffer operations
|
||||||
|
void clearScreen(uint8_t color = 0xFF) const;
|
||||||
|
void drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool fromProgmem = false) const;
|
||||||
|
|
||||||
|
void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH);
|
||||||
|
void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
|
||||||
|
|
||||||
|
// Power management
|
||||||
|
void deepSleep();
|
||||||
|
|
||||||
|
// Access to frame buffer
|
||||||
|
uint8_t* getFrameBuffer() const;
|
||||||
|
|
||||||
|
void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer);
|
||||||
|
void copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer);
|
||||||
|
void copyGrayscaleMsbBuffers(const uint8_t* msbBuffer);
|
||||||
|
void cleanupGrayscaleBuffers(const uint8_t* bwBuffer);
|
||||||
|
|
||||||
|
void displayGrayBuffer();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool is_emulated = CROSSPOINT_EMULATED;
|
||||||
|
|
||||||
|
// real display implementation
|
||||||
|
EInkDisplay einkDisplay;
|
||||||
|
|
||||||
|
// emulated display implementation
|
||||||
|
uint8_t* emuFramebuffer0 = nullptr;
|
||||||
|
};
|
||||||
122
lib/hal/HalInput.cpp
Normal file
122
lib/hal/HalInput.cpp
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
#include "HalInput.h"
|
||||||
|
#include "EmulationUtils.h"
|
||||||
|
#include <esp_sleep.h>
|
||||||
|
|
||||||
|
void HalInput::begin() {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
inputMgr.begin();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void HalInput::update() {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
inputMgr.update();
|
||||||
|
#else
|
||||||
|
const unsigned long currentTime = millis();
|
||||||
|
|
||||||
|
EmulationUtils::sendCmd(EmulationUtils::CMD_BUTTON, "read");
|
||||||
|
auto res = EmulationUtils::recvRespInt64();
|
||||||
|
assert(res >= 0);
|
||||||
|
|
||||||
|
const uint8_t state = static_cast<uint8_t>(res);
|
||||||
|
|
||||||
|
// Always clear events first
|
||||||
|
pressedEvents = 0;
|
||||||
|
releasedEvents = 0;
|
||||||
|
|
||||||
|
// Debounce
|
||||||
|
if (state != lastState) {
|
||||||
|
lastDebounceTime = currentTime;
|
||||||
|
lastState = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr unsigned long DEBOUNCE_DELAY = 5;
|
||||||
|
if ((currentTime - lastDebounceTime) > DEBOUNCE_DELAY) {
|
||||||
|
if (state != currentState) {
|
||||||
|
// Calculate pressed and released events
|
||||||
|
pressedEvents = state & ~currentState;
|
||||||
|
releasedEvents = currentState & ~state;
|
||||||
|
|
||||||
|
// If pressing buttons and wasn't before, start recording time
|
||||||
|
if (pressedEvents > 0 && currentState == 0) {
|
||||||
|
buttonPressStart = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If releasing a button and no other buttons being pressed, record finish time
|
||||||
|
if (releasedEvents > 0 && state == 0) {
|
||||||
|
buttonPressFinish = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentState = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalInput::isPressed(uint8_t buttonIndex) const {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return inputMgr.isPressed(buttonIndex);
|
||||||
|
#else
|
||||||
|
return currentState & (1 << buttonIndex);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalInput::wasPressed(uint8_t buttonIndex) const {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return inputMgr.wasPressed(buttonIndex);
|
||||||
|
#else
|
||||||
|
return currentState & (1 << buttonIndex);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalInput::wasAnyPressed() const {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return inputMgr.wasAnyPressed();
|
||||||
|
#else
|
||||||
|
return pressedEvents > 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalInput::wasReleased(uint8_t buttonIndex) const {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return inputMgr.wasReleased(buttonIndex);
|
||||||
|
#else
|
||||||
|
return releasedEvents & (1 << buttonIndex);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalInput::wasAnyReleased() const {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return inputMgr.wasAnyReleased();
|
||||||
|
#else
|
||||||
|
return releasedEvents > 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long HalInput::getHeldTime() const {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return inputMgr.getHeldTime();
|
||||||
|
#else
|
||||||
|
// Still hold a button
|
||||||
|
if (currentState > 0) {
|
||||||
|
return millis() - buttonPressStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buttonPressFinish - buttonPressStart;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void startDeepSleep(InputManager& inputMgr) {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||||
|
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
|
||||||
|
while (inputMgr.isPressed(InputManager::BTN_POWER)) {
|
||||||
|
delay(50);
|
||||||
|
inputMgr.update();
|
||||||
|
}
|
||||||
|
// Enter Deep Sleep
|
||||||
|
esp_deep_sleep_start();
|
||||||
|
#else
|
||||||
|
Serial.println("[ ] GPIO wakeup setup skipped in emulation mode.");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
52
lib/hal/HalInput.h
Normal file
52
lib/hal/HalInput.h
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
#include <InputManager.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class HalInput {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
InputManager inputMgr;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public:
|
||||||
|
HalInput() = default;
|
||||||
|
void begin();
|
||||||
|
|
||||||
|
void update();
|
||||||
|
bool isPressed(uint8_t buttonIndex) const;
|
||||||
|
bool wasPressed(uint8_t buttonIndex) const;
|
||||||
|
bool wasAnyPressed() const;
|
||||||
|
bool wasReleased(uint8_t buttonIndex) const;
|
||||||
|
bool wasAnyReleased() const;
|
||||||
|
unsigned long getHeldTime() const;
|
||||||
|
|
||||||
|
// Button indices
|
||||||
|
static constexpr uint8_t BTN_BACK = 0;
|
||||||
|
static constexpr uint8_t BTN_CONFIRM = 1;
|
||||||
|
static constexpr uint8_t BTN_LEFT = 2;
|
||||||
|
static constexpr uint8_t BTN_RIGHT = 3;
|
||||||
|
static constexpr uint8_t BTN_UP = 4;
|
||||||
|
static constexpr uint8_t BTN_DOWN = 5;
|
||||||
|
static constexpr uint8_t BTN_POWER = 6;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// emulation state
|
||||||
|
uint8_t currentState;
|
||||||
|
uint8_t lastState;
|
||||||
|
uint8_t pressedEvents;
|
||||||
|
uint8_t releasedEvents;
|
||||||
|
unsigned long lastDebounceTime;
|
||||||
|
unsigned long buttonPressStart;
|
||||||
|
unsigned long buttonPressFinish;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO @ngxson : this is a trick to avoid changing too many files at once.
|
||||||
|
// consider refactoring in a dedicated PR later.
|
||||||
|
#if CROSSPOINT_EMULATED == 1
|
||||||
|
using InputManager = HalInput;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void startDeepSleep(InputManager& inputMgr);
|
||||||
327
lib/hal/HalStorage.cpp
Normal file
327
lib/hal/HalStorage.cpp
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
#include "HalStorage.h"
|
||||||
|
#include "EmulationUtils.h"
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
#include <SDCardManager.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
HalStorage HalStorage::instance;
|
||||||
|
|
||||||
|
HalStorage::HalStorage() {}
|
||||||
|
|
||||||
|
bool HalStorage::begin() {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return SdMan.begin();
|
||||||
|
#else
|
||||||
|
// no-op
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::ready() const {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return SdMan.ready();
|
||||||
|
#else
|
||||||
|
// no-op
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<String> HalStorage::listFiles(const char* path, int maxFiles) {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return SdMan.listFiles(path, maxFiles);
|
||||||
|
#else
|
||||||
|
Serial.printf("[%lu] [FS ] Emulated listFiles: %s\n", millis(), path);
|
||||||
|
EmulationUtils::sendCmd(EmulationUtils::CMD_FS_LIST, path);
|
||||||
|
std::vector<String> output;
|
||||||
|
for (int i = 0; i < maxFiles; ++i) {
|
||||||
|
auto resp = EmulationUtils::recvRespStr();
|
||||||
|
if (resp.length() == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
output.push_back(resp);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
String HalStorage::readFile(const char* path) {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return SdMan.readFile(path);
|
||||||
|
#else
|
||||||
|
Serial.printf("[%lu] [FS ] Emulated readFile: %s\n", millis(), path);
|
||||||
|
EmulationUtils::sendCmd(EmulationUtils::CMD_FS_READ, path, "0", "-1");
|
||||||
|
return EmulationUtils::recvRespStr();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static int64_t getFileSizeEmulated(const char* path) {
|
||||||
|
EmulationUtils::sendCmd(EmulationUtils::CMD_FS_STAT, path);
|
||||||
|
return EmulationUtils::recvRespInt64();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::readFileToStream(const char* path, Print& out, size_t chunkSize) {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return SdMan.readFileToStream(path, out, chunkSize);
|
||||||
|
#else
|
||||||
|
Serial.printf("[%lu] [FS ] Emulated readFileToStream: %s\n", millis(), path);
|
||||||
|
auto size = getFileSizeEmulated(path);
|
||||||
|
if (size == -1) {
|
||||||
|
Serial.printf("[%lu] [FS ] File not found: %s\n", millis(), path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (size == -2) {
|
||||||
|
Serial.printf("[%lu] [FS ] Path is a directory, not a file: %s\n", millis(), path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
size_t bytesRead = 0;
|
||||||
|
while (bytesRead < static_cast<size_t>(size)) {
|
||||||
|
size_t toRead = std::min(chunkSize, static_cast<size_t>(size) - bytesRead);
|
||||||
|
EmulationUtils::sendCmd(EmulationUtils::CMD_FS_READ, path, String(bytesRead).c_str(), String(toRead).c_str());
|
||||||
|
auto buf = EmulationUtils::recvRespBuf();
|
||||||
|
out.write(buf.data(), buf.size());
|
||||||
|
bytesRead += buf.size();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t HalStorage::readFileToBuffer(const char* path, char* buffer, size_t bufferSize, size_t maxBytes) {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return SdMan.readFileToBuffer(path, buffer, bufferSize, maxBytes);
|
||||||
|
#else
|
||||||
|
Serial.printf("[%lu] [FS ] Emulated readFileToBuffer: %s\n", millis(), path);
|
||||||
|
auto size = getFileSizeEmulated(path);
|
||||||
|
if (size == -1) {
|
||||||
|
Serial.printf("[%lu] [FS ] File not found: %s\n", millis(), path);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (size == -2) {
|
||||||
|
Serial.printf("[%lu] [FS ] Path is a directory, not a file: %s\n", millis(), path);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
size_t toRead = static_cast<size_t>(size);
|
||||||
|
if (maxBytes > 0 && maxBytes < toRead) {
|
||||||
|
toRead = maxBytes;
|
||||||
|
}
|
||||||
|
if (toRead >= bufferSize) {
|
||||||
|
toRead = bufferSize - 1; // leave space for null terminator
|
||||||
|
}
|
||||||
|
EmulationUtils::sendCmd(EmulationUtils::CMD_FS_READ, path, "0", String(toRead).c_str());
|
||||||
|
auto buf = EmulationUtils::recvRespBuf();
|
||||||
|
size_t bytesRead = buf.size();
|
||||||
|
memcpy(buffer, buf.data(), bytesRead);
|
||||||
|
buffer[bytesRead] = '\0'; // null-terminate
|
||||||
|
return bytesRead;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::writeFile(const char* path, const String& content) {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return SdMan.writeFile(path, content);
|
||||||
|
#else
|
||||||
|
Serial.printf("[%lu] [FS ] Emulated writeFile: %s\n", millis(), path);
|
||||||
|
std::string b64 = EmulationUtils::base64_encode((char*)content.c_str(), content.length());
|
||||||
|
EmulationUtils::sendCmd(EmulationUtils::CMD_FS_WRITE, path, b64.c_str(), "0", "0");
|
||||||
|
EmulationUtils::recvRespInt64(); // unused for now
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::ensureDirectoryExists(const char* path) {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return SdMan.ensureDirectoryExists(path);
|
||||||
|
#else
|
||||||
|
Serial.printf("[%lu] [FS ] Emulated ensureDirectoryExists: %s\n", millis(), path);
|
||||||
|
EmulationUtils::sendCmd(EmulationUtils::CMD_FS_MKDIR, path);
|
||||||
|
EmulationUtils::recvRespInt64(); // unused for now
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile HalStorage::open(const char* path, const oflag_t oflag) {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return SdMan.open(path, oflag);
|
||||||
|
#else
|
||||||
|
// TODO: do we need to check existence or create the file?
|
||||||
|
return FsFile(path, oflag);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::mkdir(const char* path, const bool pFlag) {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return SdMan.mkdir(path, pFlag);
|
||||||
|
#else
|
||||||
|
EmulationUtils::sendCmd(EmulationUtils::CMD_FS_MKDIR, path);
|
||||||
|
EmulationUtils::recvRespInt64(); // unused for now
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::exists(const char* path) {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return SdMan.exists(path);
|
||||||
|
#else
|
||||||
|
Serial.printf("[%lu] [FS ] Emulated exists: %s\n", millis(), path);
|
||||||
|
auto size = getFileSizeEmulated(path);
|
||||||
|
return size != -1;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::remove(const char* path) {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return SdMan.remove(path);
|
||||||
|
#else
|
||||||
|
Serial.printf("[%lu] [FS ] Emulated remove: %s\n", millis(), path);
|
||||||
|
EmulationUtils::sendCmd(EmulationUtils::CMD_FS_RM, path);
|
||||||
|
EmulationUtils::recvRespInt64(); // unused for now
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::rmdir(const char* path) {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return SdMan.rmdir(path);
|
||||||
|
#else
|
||||||
|
Serial.printf("[%lu] [FS ] Emulated rmdir: %s\n", millis(), path);
|
||||||
|
EmulationUtils::sendCmd(EmulationUtils::CMD_FS_RM, path);
|
||||||
|
EmulationUtils::recvRespInt64(); // unused for now
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::openFileForRead(const char* moduleName, const char* path, FsFile& file) {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return SdMan.openFileForRead(moduleName, path, file);
|
||||||
|
#else
|
||||||
|
Serial.printf("[%lu] [FS ] Emulated openFileForRead: %s\n", millis(), path);
|
||||||
|
auto size = getFileSizeEmulated(path);
|
||||||
|
if (size == -1) {
|
||||||
|
Serial.printf("[%lu] [FS ] File not found: %s\n", millis(), path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (size == -2) {
|
||||||
|
Serial.printf("[%lu] [FS ] Path is a directory, not a file: %s\n", millis(), path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::openFileForRead(const char* moduleName, const std::string& path, FsFile& file) {
|
||||||
|
return openFileForRead(moduleName, path.c_str(), file);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::openFileForRead(const char* moduleName, const String& path, FsFile& file) {
|
||||||
|
return openFileForRead(moduleName, path.c_str(), file);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::openFileForWrite(const char* moduleName, const char* path, FsFile& file) {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return SdMan.openFileForWrite(moduleName, path, file);
|
||||||
|
#else
|
||||||
|
Serial.printf("[%lu] [FS ] Emulated openFileForWrite: %s\n", millis(), path);
|
||||||
|
auto size = getFileSizeEmulated(path);
|
||||||
|
if (size == -1) {
|
||||||
|
Serial.printf("[%lu] [FS ] File not found: %s\n", millis(), path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (size == -2) {
|
||||||
|
Serial.printf("[%lu] [FS ] Path is a directory, not a file: %s\n", millis(), path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::openFileForWrite(const char* moduleName, const std::string& path, FsFile& file) {
|
||||||
|
return openFileForWrite(moduleName, path.c_str(), file);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::openFileForWrite(const char* moduleName, const String& path, FsFile& file) {
|
||||||
|
return openFileForWrite(moduleName, path.c_str(), file);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::removeDir(const char* path) {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
return SdMan.removeDir(path);
|
||||||
|
#else
|
||||||
|
// to be implemented
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#if CROSSPOINT_EMULATED == 1
|
||||||
|
//
|
||||||
|
// FsFile emulation methods
|
||||||
|
//
|
||||||
|
|
||||||
|
FsFile::FsFile(const char* path, oflag_t oflag) : path(path), oflag(oflag) {
|
||||||
|
Serial.printf("[%lu] [FS ] Emulated FsFile open: %s\n", millis(), path);
|
||||||
|
auto size = getFileSizeEmulated(path);
|
||||||
|
if (size == -1) {
|
||||||
|
Serial.printf("[%lu] [FS ] File not found: %s\n", millis(), path);
|
||||||
|
open = false;
|
||||||
|
fileSizeBytes = 0;
|
||||||
|
} else if (isDir) {
|
||||||
|
Serial.printf("[%lu] [FS ] Path is a directory: %s\n", millis(), path);
|
||||||
|
open = true;
|
||||||
|
// get directory entries
|
||||||
|
EmulationUtils::sendCmd(EmulationUtils::CMD_FS_LIST, path);
|
||||||
|
dirEntries.clear();
|
||||||
|
while (true) {
|
||||||
|
auto resp = EmulationUtils::recvRespStr();
|
||||||
|
if (resp.length() == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
dirEntries.push_back(resp);
|
||||||
|
}
|
||||||
|
Serial.printf("[%lu] [FS ] Directory has %u entries\n", millis(), (unsigned)dirEntries.size());
|
||||||
|
dirIndex = 0;
|
||||||
|
fileSizeBytes = 0;
|
||||||
|
} else {
|
||||||
|
open = true;
|
||||||
|
fileSizeBytes = static_cast<size_t>(size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int FsFile::read(void* buf, size_t count) {
|
||||||
|
if (!open || isDir) return -1;
|
||||||
|
size_t bytesAvailable = (fileSizeBytes > filePos) ? (fileSizeBytes - filePos) : 0;
|
||||||
|
if (bytesAvailable == 0) return 0;
|
||||||
|
size_t toRead = std::min(count, bytesAvailable);
|
||||||
|
EmulationUtils::sendCmd(EmulationUtils::CMD_FS_READ, path.c_str(), String(filePos).c_str(), String(toRead).c_str());
|
||||||
|
auto data = EmulationUtils::recvRespBuf();
|
||||||
|
size_t bytesRead = data.size();
|
||||||
|
memcpy(buf, data.data(), bytesRead);
|
||||||
|
filePos += bytesRead;
|
||||||
|
return static_cast<int>(bytesRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
int FsFile::read() {
|
||||||
|
uint8_t b;
|
||||||
|
int result = read(&b, 1);
|
||||||
|
if (result <= 0) return -1;
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t FsFile::write(const uint8_t* buffer, size_t size) {
|
||||||
|
if (!open || isDir) return 0;
|
||||||
|
std::string b64 = EmulationUtils::base64_encode((const char*)buffer, size);
|
||||||
|
EmulationUtils::sendCmd(EmulationUtils::CMD_FS_WRITE, path.c_str(), b64.c_str(), String(filePos).c_str(), "1");
|
||||||
|
EmulationUtils::recvRespInt64(); // unused for now
|
||||||
|
filePos += size;
|
||||||
|
if (filePos > fileSizeBytes) {
|
||||||
|
fileSizeBytes = filePos;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t FsFile::write(uint8_t b) {
|
||||||
|
return write(&b, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
120
lib/hal/HalStorage.h
Normal file
120
lib/hal/HalStorage.h
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
#include <SDCardManager.h>
|
||||||
|
#else
|
||||||
|
typedef int oflag_t;
|
||||||
|
#define O_RDONLY 0 /* +1 == FREAD */
|
||||||
|
#define O_WRONLY 1 /* +1 == FWRITE */
|
||||||
|
#define O_RDWR 2 /* +1 == FREAD|FWRITE */
|
||||||
|
|
||||||
|
class FsFile : public Print {
|
||||||
|
String path;
|
||||||
|
String name;
|
||||||
|
oflag_t oflag; // unused for now
|
||||||
|
bool open = false;
|
||||||
|
// directory state
|
||||||
|
bool isDir = false;
|
||||||
|
std::vector<String> dirEntries;
|
||||||
|
size_t dirIndex = 0;
|
||||||
|
// file state
|
||||||
|
size_t filePos = 0;
|
||||||
|
size_t fileSizeBytes = 0;
|
||||||
|
public:
|
||||||
|
FsFile() = default;
|
||||||
|
FsFile(const char* path, oflag_t oflag);
|
||||||
|
~FsFile() = default;
|
||||||
|
|
||||||
|
void flush() { /* no-op */ }
|
||||||
|
size_t getName(char* name, size_t len) {
|
||||||
|
String n = path;
|
||||||
|
if (n.length() >= len) {
|
||||||
|
n = n.substring(0, len - 1);
|
||||||
|
}
|
||||||
|
n.toCharArray(name, len);
|
||||||
|
return n.length();
|
||||||
|
}
|
||||||
|
size_t size() { return fileSizeBytes; }
|
||||||
|
size_t fileSize() { return size(); }
|
||||||
|
size_t seek(size_t pos) {
|
||||||
|
filePos = pos;
|
||||||
|
return filePos;
|
||||||
|
}
|
||||||
|
size_t seekCur(int64_t offset) { return seek(filePos + offset); }
|
||||||
|
size_t seekSet(size_t offset) { return seek(offset); }
|
||||||
|
int available() const { return (fileSizeBytes > filePos) ? (fileSizeBytes - filePos) : 0; }
|
||||||
|
size_t position() const { return filePos; }
|
||||||
|
int read(void* buf, size_t count);
|
||||||
|
int read(); // read a single byte
|
||||||
|
size_t write(const uint8_t* buffer, size_t size);
|
||||||
|
size_t write(uint8_t b) override;
|
||||||
|
bool isDirectory() const { return isDir; }
|
||||||
|
int rewindDirectory() {
|
||||||
|
if (!isDir) return -1;
|
||||||
|
dirIndex = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
bool close() { open = false; return true; }
|
||||||
|
FsFile openNextFile() {
|
||||||
|
if (!isDir || dirIndex >= dirEntries.size()) {
|
||||||
|
return FsFile();
|
||||||
|
}
|
||||||
|
FsFile f(dirEntries[dirIndex].c_str(), O_RDONLY);
|
||||||
|
dirIndex++;
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
bool isOpen() const { return open; }
|
||||||
|
operator bool() const { return isOpen(); }
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class HalStorage {
|
||||||
|
public:
|
||||||
|
HalStorage();
|
||||||
|
bool begin();
|
||||||
|
bool ready() const;
|
||||||
|
std::vector<String> listFiles(const char* path = "/", int maxFiles = 200);
|
||||||
|
// Read the entire file at `path` into a String. Returns empty string on failure.
|
||||||
|
String readFile(const char* path);
|
||||||
|
// Low-memory helpers:
|
||||||
|
// Stream the file contents to a `Print` (e.g. `Serial`, or any `Print`-derived object).
|
||||||
|
// Returns true on success, false on failure.
|
||||||
|
bool readFileToStream(const char* path, Print& out, size_t chunkSize = 256);
|
||||||
|
// Read up to `bufferSize-1` bytes into `buffer`, null-terminating it. Returns bytes read.
|
||||||
|
size_t readFileToBuffer(const char* path, char* buffer, size_t bufferSize, size_t maxBytes = 0);
|
||||||
|
// Write a string to `path` on the SD card. Overwrites existing file.
|
||||||
|
// Returns true on success.
|
||||||
|
bool writeFile(const char* path, const String& content);
|
||||||
|
// Ensure a directory exists, creating it if necessary. Returns true on success.
|
||||||
|
bool ensureDirectoryExists(const char* path);
|
||||||
|
|
||||||
|
FsFile open(const char* path, const oflag_t oflag = O_RDONLY);
|
||||||
|
bool mkdir(const char* path, const bool pFlag = true);
|
||||||
|
bool exists(const char* path);
|
||||||
|
bool remove(const char* path);
|
||||||
|
bool rmdir(const char* path);
|
||||||
|
|
||||||
|
bool openFileForRead(const char* moduleName, const char* path, FsFile& file);
|
||||||
|
bool openFileForRead(const char* moduleName, const std::string& path, FsFile& file);
|
||||||
|
bool openFileForRead(const char* moduleName, const String& path, FsFile& file);
|
||||||
|
bool openFileForWrite(const char* moduleName, const char* path, FsFile& file);
|
||||||
|
bool openFileForWrite(const char* moduleName, const std::string& path, FsFile& file);
|
||||||
|
bool openFileForWrite(const char* moduleName, const String& path, FsFile& file);
|
||||||
|
bool removeDir(const char* path);
|
||||||
|
|
||||||
|
static HalStorage& getInstance() { return instance; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static HalStorage instance;
|
||||||
|
|
||||||
|
bool is_emulated = CROSSPOINT_EMULATED;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO @ngxson : this is a trick to avoid changing too many files at once.
|
||||||
|
// consider refactoring in a dedicated PR later.
|
||||||
|
#if CROSSPOINT_EMULATED == 1
|
||||||
|
#define SdMan HalStorage::getInstance()
|
||||||
|
#endif
|
||||||
@ -20,7 +20,6 @@ board_upload.offset_address = 0x10000
|
|||||||
|
|
||||||
build_flags =
|
build_flags =
|
||||||
-DARDUINO_USB_MODE=1
|
-DARDUINO_USB_MODE=1
|
||||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
|
||||||
-DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1
|
-DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1
|
||||||
-DEINK_DISPLAY_SINGLE_BUFFER_MODE=1
|
-DEINK_DISPLAY_SINGLE_BUFFER_MODE=1
|
||||||
-DDISABLE_FS_H_WARNING=1
|
-DDISABLE_FS_H_WARNING=1
|
||||||
@ -54,9 +53,21 @@ extends = base
|
|||||||
build_flags =
|
build_flags =
|
||||||
${base.build_flags}
|
${base.build_flags}
|
||||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}-dev\"
|
-DCROSSPOINT_VERSION=\"${crosspoint.version}-dev\"
|
||||||
|
-DCROSSPOINT_EMULATED=0
|
||||||
|
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
|
||||||
|
[env:emulation]
|
||||||
|
extends = base
|
||||||
|
build_flags =
|
||||||
|
${base.build_flags}
|
||||||
|
-DCROSSPOINT_VERSION=\"${crosspoint.version}-emu\"
|
||||||
|
-DCROSSPOINT_EMULATED=1
|
||||||
|
-DARDUINO_USB_CDC_ON_BOOT=0 # Disable USB CDC on boot for Serial output via UART
|
||||||
|
|
||||||
[env:gh_release]
|
[env:gh_release]
|
||||||
extends = base
|
extends = base
|
||||||
build_flags =
|
build_flags =
|
||||||
${base.build_flags}
|
${base.build_flags}
|
||||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}\"
|
-DCROSSPOINT_VERSION=\"${crosspoint.version}\"
|
||||||
|
-DCROSSPOINT_EMULATED=0
|
||||||
|
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
|||||||
46
scripts/emulation/Dockerfile
Normal file
46
scripts/emulation/Dockerfile
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
FROM debian:12
|
||||||
|
|
||||||
|
WORKDIR /root
|
||||||
|
|
||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
ARG ESP_IDF_VERSION=v5.5.2
|
||||||
|
ARG ESP_QEMU_VERSION=esp-develop-9.2.2-20250817
|
||||||
|
|
||||||
|
# Update container
|
||||||
|
RUN apt-get update && apt-get upgrade -y
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN apt-get install -y git wget flex bison gperf python3 python3-pip python3-setuptools python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0 \
|
||||||
|
libgcrypt20 libglib2.0-0 libpixman-1-0 libsdl2-2.0-0 libslirp0 \
|
||||||
|
libglib2.0-dev libpixman-1-dev libgcrypt20-dev libslirp-dev libsdl2-dev
|
||||||
|
|
||||||
|
# Install ESP-IDF
|
||||||
|
ENV PATH="~/.local/bin:${PATH}"
|
||||||
|
RUN mkdir -p ~/esp && \
|
||||||
|
cd ~/esp && git clone --recursive https://github.com/espressif/esp-idf.git -b ${ESP_IDF_VERSION} --depth 1 -j $(nproc) && \
|
||||||
|
ln -s /usr/bin/python3 /usr/bin/python
|
||||||
|
RUN cd ~/esp/esp-idf && export IDF_GITHUB_ASSETS="dl.espressif.com/github_assets" && ./install.sh && /bin/bash -c "source ./export.sh"
|
||||||
|
|
||||||
|
# Install qemu
|
||||||
|
RUN cd ~/esp/esp-idf && python tools/idf_tools.py install qemu-riscv32 qemu-xtensa
|
||||||
|
|
||||||
|
# Or, build qemu from source
|
||||||
|
# RUN git clone https://github.com/espressif/qemu.git -b ${ESP_QEMU_VERSION} --depth 1 \
|
||||||
|
# && cd qemu \
|
||||||
|
# && mkdir -p build \
|
||||||
|
# && cd build \
|
||||||
|
# && ../configure --target-list=riscv32-softmmu \
|
||||||
|
# --enable-gcrypt \
|
||||||
|
# --enable-slirp \
|
||||||
|
# --enable-debug \
|
||||||
|
# --enable-sdl \
|
||||||
|
# --disable-strip --disable-user \
|
||||||
|
# --disable-capstone --disable-vnc \
|
||||||
|
# --disable-gtk \
|
||||||
|
# && make -j $(nproc) vga=no \
|
||||||
|
# && make install
|
||||||
|
|
||||||
|
RUN echo "alias idf='source /root/esp/esp-idf/export.sh'" >> .bashrc
|
||||||
|
|
||||||
|
# Start from a Bash prompt
|
||||||
|
CMD [ "/bin/bash" ]
|
||||||
35
scripts/emulation/docker-compose.yml
Normal file
35
scripts/emulation/docker-compose.yml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
services:
|
||||||
|
crosspoint-emulator:
|
||||||
|
build: .
|
||||||
|
container_name: crosspoint-emulator
|
||||||
|
volumes:
|
||||||
|
- ../..:/app:ro
|
||||||
|
- ../../.sdcard:/sdcard:rw
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
ports:
|
||||||
|
- "8090:8090"
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
stop_signal: SIGKILL
|
||||||
|
entrypoint:
|
||||||
|
- /bin/bash
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -x;
|
||||||
|
set -e;
|
||||||
|
source /root/esp/esp-idf/export.sh
|
||||||
|
|
||||||
|
cd /tmp
|
||||||
|
|
||||||
|
cp /app/.pio/build/emulation/*.bin .
|
||||||
|
dd if=/dev/zero bs=1M count=16 of=./flash.bin
|
||||||
|
dd if="bootloader.bin" bs=1 count=$(stat -c%s "bootloader.bin") seek=0 conv=notrunc of=./flash.bin
|
||||||
|
dd if="partitions.bin" bs=1 count=$(stat -c%s "partitions.bin") seek=$((16#8000)) conv=notrunc of=./flash.bin
|
||||||
|
dd if="firmware.bin" bs=1 count=$(stat -c%s "firmware.bin") seek=$((16#10000)) conv=notrunc of=./flash.bin
|
||||||
|
|
||||||
|
# TODO: why pip install doesn't work in Dockerfile?
|
||||||
|
python3 -m pip install websockets --break-system-packages
|
||||||
|
|
||||||
|
cp /app/scripts/emulation/web_ui.html .
|
||||||
|
python3 /app/scripts/emulation/server.py
|
||||||
549
scripts/emulation/server.py
Normal file
549
scripts/emulation/server.py
Normal file
@ -0,0 +1,549 @@
|
|||||||
|
import asyncio
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
from http import HTTPStatus
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Set
|
||||||
|
|
||||||
|
import websockets
|
||||||
|
from websockets.http11 import Response
|
||||||
|
from websockets.server import WebSocketServerProtocol
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
This script spawns a QEMU process, then forward its stdout and stderr to WebSocket clients.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# SD card emulation directory
|
||||||
|
sdcard_path = os.environ.get("SDCARD_PATH", "/sdcard")
|
||||||
|
if not os.path.exists(sdcard_path):
|
||||||
|
os.makedirs(sdcard_path)
|
||||||
|
|
||||||
|
|
||||||
|
class SdCardHandler:
|
||||||
|
"""Handle SD card filesystem commands from the emulated device."""
|
||||||
|
|
||||||
|
def __init__(self, base_path: str):
|
||||||
|
self.base_path = base_path
|
||||||
|
|
||||||
|
def _resolve_path(self, path: str) -> str:
|
||||||
|
"""Resolve a device path to a host filesystem path."""
|
||||||
|
# Remove leading slash and join with base path
|
||||||
|
if path.startswith("/"):
|
||||||
|
path = path[1:]
|
||||||
|
return os.path.join(self.base_path, path)
|
||||||
|
|
||||||
|
def handle_fs_list(self, path: str, max_files: str = "200") -> list[str]:
|
||||||
|
"""List files in a directory. Returns list of filenames."""
|
||||||
|
resolved = self._resolve_path(path)
|
||||||
|
max_count = int(max_files) if max_files else 200
|
||||||
|
|
||||||
|
if not os.path.exists(resolved):
|
||||||
|
return []
|
||||||
|
|
||||||
|
if not os.path.isdir(resolved):
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
entries = []
|
||||||
|
for entry in os.listdir(resolved):
|
||||||
|
if len(entries) >= max_count:
|
||||||
|
break
|
||||||
|
full_path = os.path.join(resolved, entry)
|
||||||
|
# Append "/" for directories to match device behavior
|
||||||
|
if os.path.isdir(full_path):
|
||||||
|
entries.append(entry + "/")
|
||||||
|
else:
|
||||||
|
entries.append(entry)
|
||||||
|
return entries
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[SdCard] Error listing {path}: {e}", file=sys.stderr)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def handle_fs_read(self, path: str, offset: str = "0", length: str = "-1") -> str:
|
||||||
|
"""Read file contents. Returns base64-encoded data."""
|
||||||
|
resolved = self._resolve_path(path)
|
||||||
|
offset_int = int(offset) if offset else 0
|
||||||
|
length_int = int(length) if length else -1
|
||||||
|
|
||||||
|
if not os.path.exists(resolved) or os.path.isdir(resolved):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(resolved, "rb") as f:
|
||||||
|
if offset_int > 0:
|
||||||
|
f.seek(offset_int)
|
||||||
|
if length_int == -1:
|
||||||
|
data = f.read()
|
||||||
|
else:
|
||||||
|
data = f.read(length_int)
|
||||||
|
return base64.b64encode(data).decode("ascii")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[SdCard] Error reading {path}: {e}", file=sys.stderr)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def handle_fs_stat(self, path: str) -> int:
|
||||||
|
"""Get file size. Returns -1 if not found, -2 if directory."""
|
||||||
|
resolved = self._resolve_path(path)
|
||||||
|
|
||||||
|
if not os.path.exists(resolved):
|
||||||
|
return -1
|
||||||
|
|
||||||
|
if os.path.isdir(resolved):
|
||||||
|
return -2
|
||||||
|
|
||||||
|
try:
|
||||||
|
return os.path.getsize(resolved)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[SdCard] Error stat {path}: {e}", file=sys.stderr)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def handle_fs_write(self, path: str, b64_data: str, offset: str = "0", inplace: str = "0") -> int:
|
||||||
|
"""Write data to file. Returns bytes written."""
|
||||||
|
resolved = self._resolve_path(path)
|
||||||
|
offset_int = int(offset) if offset else 0
|
||||||
|
is_inplace = inplace == "1"
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = base64.b64decode(b64_data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[SdCard] Error decoding base64 for {path}: {e}", file=sys.stderr)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Ensure parent directory exists
|
||||||
|
parent = os.path.dirname(resolved)
|
||||||
|
if parent and not os.path.exists(parent):
|
||||||
|
os.makedirs(parent)
|
||||||
|
|
||||||
|
if is_inplace and os.path.exists(resolved):
|
||||||
|
# In-place write at offset
|
||||||
|
with open(resolved, "r+b") as f:
|
||||||
|
f.seek(offset_int)
|
||||||
|
f.write(data)
|
||||||
|
else:
|
||||||
|
# Overwrite or create new file
|
||||||
|
mode = "r+b" if os.path.exists(resolved) and offset_int > 0 else "wb"
|
||||||
|
with open(resolved, mode) as f:
|
||||||
|
if offset_int > 0:
|
||||||
|
f.seek(offset_int)
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
return len(data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[SdCard] Error writing {path}: {e}", file=sys.stderr)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def handle_fs_mkdir(self, path: str) -> int:
|
||||||
|
"""Create directory. Returns 0 on success."""
|
||||||
|
resolved = self._resolve_path(path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(resolved, exist_ok=True)
|
||||||
|
return 0
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[SdCard] Error mkdir {path}: {e}", file=sys.stderr)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def handle_fs_rm(self, path: str) -> int:
|
||||||
|
"""Remove file or directory. Returns 0 on success."""
|
||||||
|
resolved = self._resolve_path(path)
|
||||||
|
|
||||||
|
if not os.path.exists(resolved):
|
||||||
|
return -1
|
||||||
|
|
||||||
|
try:
|
||||||
|
if os.path.isdir(resolved):
|
||||||
|
shutil.rmtree(resolved)
|
||||||
|
else:
|
||||||
|
os.remove(resolved)
|
||||||
|
return 0
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[SdCard] Error removing {path}: {e}", file=sys.stderr)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
# Global SD card handler instance
|
||||||
|
sdcard_handler = SdCardHandler(sdcard_path)
|
||||||
|
|
||||||
|
|
||||||
|
# WebSocket clients
|
||||||
|
connected_clients: Set[WebSocketServerProtocol] = set()
|
||||||
|
|
||||||
|
# QEMU process
|
||||||
|
qemu_process: asyncio.subprocess.Process | None = None
|
||||||
|
|
||||||
|
# Command pattern: $$CMD_(COMMAND):(ARG0)[:(ARG1)][:(ARG2)][:(ARG3)]$$
|
||||||
|
CMD_PATTERN = re.compile(r'^\$\$CMD_([A-Z_]+):(.+)\$\$$')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_command(message: str) -> tuple[str, list[str]] | None:
|
||||||
|
"""Parse a command message. Returns (command, args) or None if not a command."""
|
||||||
|
match = CMD_PATTERN.match(message)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
command = match.group(1)
|
||||||
|
args_str = match.group(2)
|
||||||
|
# Split args by ':' but the last arg might contain ':'
|
||||||
|
args = args_str.split(':')
|
||||||
|
return (command, args)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_response(response: str):
|
||||||
|
"""Send a response back to the QEMU process via stdin."""
|
||||||
|
global qemu_process
|
||||||
|
if qemu_process and qemu_process.stdin:
|
||||||
|
qemu_process.stdin.write((response + "\n").encode())
|
||||||
|
await qemu_process.stdin.drain()
|
||||||
|
|
||||||
|
|
||||||
|
LAST_DISPLAY_BUFFER = None
|
||||||
|
BUTTON_STATE = 0
|
||||||
|
|
||||||
|
async def handle_command(command: str, args: list[str]) -> bool:
|
||||||
|
"""Handle a command from the device. Returns True if handled."""
|
||||||
|
global sdcard_handler, LAST_DISPLAY_BUFFER, BUTTON_STATE
|
||||||
|
|
||||||
|
try:
|
||||||
|
if command == "DISPLAY":
|
||||||
|
# Display command - no response needed
|
||||||
|
# arg0: base64-encoded buffer
|
||||||
|
print(f"[Emulator] DISPLAY command received (buffer size: {len(args[0]) if args else 0})", flush=True)
|
||||||
|
LAST_DISPLAY_BUFFER = args[0] if args else None
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif command == "FS_LIST":
|
||||||
|
# arg0: path, arg1: max files (optional)
|
||||||
|
path = args[0] if len(args) > 0 else "/"
|
||||||
|
max_files = args[1] if len(args) > 1 else "200"
|
||||||
|
entries = sdcard_handler.handle_fs_list(path, max_files)
|
||||||
|
print(f"[Emulator] FS_LIST {path} -> {len(entries)} entries", flush=True)
|
||||||
|
# Send each entry as a line, then empty line to terminate
|
||||||
|
for entry in entries:
|
||||||
|
await send_response(entry)
|
||||||
|
await send_response("") # Empty line terminates the list
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif command == "FS_READ":
|
||||||
|
# arg0: path, arg1: offset, arg2: length
|
||||||
|
path = args[0] if len(args) > 0 else ""
|
||||||
|
offset = args[1] if len(args) > 1 else "0"
|
||||||
|
length = args[2] if len(args) > 2 else "-1"
|
||||||
|
result = sdcard_handler.handle_fs_read(path, offset, length)
|
||||||
|
print(f"[Emulator] FS_READ {path} offset={offset} len={length} -> {len(result)} bytes (b64)", flush=True)
|
||||||
|
await send_response(result)
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif command == "FS_STAT":
|
||||||
|
# arg0: path
|
||||||
|
path = args[0] if len(args) > 0 else ""
|
||||||
|
result = sdcard_handler.handle_fs_stat(path)
|
||||||
|
print(f"[Emulator] FS_STAT {path} -> {result}", flush=True)
|
||||||
|
await send_response(str(result))
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif command == "FS_WRITE":
|
||||||
|
# arg0: path, arg1: base64-encoded data, arg2: offset, arg3: is inplace
|
||||||
|
path = args[0] if len(args) > 0 else ""
|
||||||
|
b64_data = args[1] if len(args) > 1 else ""
|
||||||
|
offset = args[2] if len(args) > 2 else "0"
|
||||||
|
inplace = args[3] if len(args) > 3 else "0"
|
||||||
|
result = sdcard_handler.handle_fs_write(path, b64_data, offset, inplace)
|
||||||
|
print(f"[Emulator] FS_WRITE {path} offset={offset} inplace={inplace} -> {result} bytes", flush=True)
|
||||||
|
await send_response(str(result))
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif command == "FS_MKDIR":
|
||||||
|
# arg0: path
|
||||||
|
path = args[0] if len(args) > 0 else ""
|
||||||
|
result = sdcard_handler.handle_fs_mkdir(path)
|
||||||
|
print(f"[Emulator] FS_MKDIR {path} -> {result}", flush=True)
|
||||||
|
await send_response(str(result))
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif command == "FS_RM":
|
||||||
|
# arg0: path
|
||||||
|
path = args[0] if len(args) > 0 else ""
|
||||||
|
result = sdcard_handler.handle_fs_rm(path)
|
||||||
|
print(f"[Emulator] FS_RM {path} -> {result}", flush=True)
|
||||||
|
await send_response(str(result))
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif command == "BUTTON":
|
||||||
|
# arg0: action
|
||||||
|
action = args[0]
|
||||||
|
if action != "read":
|
||||||
|
raise ValueError("Only read action is supported")
|
||||||
|
# no log (too frequent)
|
||||||
|
# print(f"[Emulator] BUTTON command received: {action}", flush=True)
|
||||||
|
await send_response(str(BUTTON_STATE))
|
||||||
|
return True
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"[Emulator] Unknown command: {command}", flush=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Emulator] Error handling command {command}: {e}", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def process_message(message: str, for_ui: bool) -> str | None:
|
||||||
|
if message.startswith("$$CMD_BUTTON:"):
|
||||||
|
return None # Too frequent, ignore
|
||||||
|
if message.startswith("$$CMD_"):
|
||||||
|
if for_ui:
|
||||||
|
return message
|
||||||
|
else:
|
||||||
|
# $$CMD_(COMMAND)[:(ARG0)][:(ARG1)][:(ARG2)]$$
|
||||||
|
command = message[2:].split(':')[0]
|
||||||
|
return "[Emulator] Received command: " + command
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
|
async def broadcast_message(msg_type: str, data: str):
|
||||||
|
"""Broadcast a message to all connected WebSocket clients."""
|
||||||
|
if not connected_clients:
|
||||||
|
return
|
||||||
|
|
||||||
|
message = json.dumps({"type": msg_type, "data": data})
|
||||||
|
|
||||||
|
# Send to all clients, remove disconnected ones
|
||||||
|
disconnected = set()
|
||||||
|
for client in connected_clients:
|
||||||
|
try:
|
||||||
|
await client.send(message)
|
||||||
|
except websockets.exceptions.ConnectionClosed:
|
||||||
|
disconnected.add(client)
|
||||||
|
|
||||||
|
connected_clients.difference_update(disconnected)
|
||||||
|
|
||||||
|
|
||||||
|
async def read_stream(stream: asyncio.StreamReader, stream_type: str):
|
||||||
|
"""Read from a stream line by line and broadcast to clients."""
|
||||||
|
buffer = b""
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
chunk = await stream.read(1024)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
|
||||||
|
buffer += chunk
|
||||||
|
|
||||||
|
# Process complete lines
|
||||||
|
while b"\n" in buffer:
|
||||||
|
line, buffer = buffer.split(b"\n", 1)
|
||||||
|
try:
|
||||||
|
decoded_line = line.decode("utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
decoded_line = line.decode("latin-1", errors="replace")
|
||||||
|
|
||||||
|
# Check if this is a command that needs handling
|
||||||
|
parsed = parse_command(decoded_line)
|
||||||
|
if parsed:
|
||||||
|
command, args = parsed
|
||||||
|
await handle_command(command, args)
|
||||||
|
if command == "DISPLAY":
|
||||||
|
await broadcast_message(stream_type, decoded_line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
to_stdio = process_message(decoded_line, for_ui=False)
|
||||||
|
to_ws = process_message(decoded_line, for_ui=True)
|
||||||
|
|
||||||
|
# Forward to parent process
|
||||||
|
if to_stdio is not None:
|
||||||
|
if stream_type == "stdout":
|
||||||
|
print(to_stdio, flush=True)
|
||||||
|
else:
|
||||||
|
print(to_stdio, file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
# Broadcast to WebSocket clients
|
||||||
|
if to_ws is not None:
|
||||||
|
await broadcast_message(stream_type, to_ws)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading {stream_type}: {e}", file=sys.stderr)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Process remaining buffer
|
||||||
|
if buffer:
|
||||||
|
try:
|
||||||
|
decoded_line = buffer.decode("utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
decoded_line = buffer.decode("latin-1", errors="replace")
|
||||||
|
|
||||||
|
if stream_type == "stdout":
|
||||||
|
print(decoded_line, flush=True)
|
||||||
|
else:
|
||||||
|
print(decoded_line, file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
await broadcast_message(stream_type, decoded_line)
|
||||||
|
|
||||||
|
|
||||||
|
async def spawn_qemu():
|
||||||
|
"""Spawn the QEMU process and capture its output."""
|
||||||
|
global qemu_process
|
||||||
|
|
||||||
|
# Build the command
|
||||||
|
cmd = [
|
||||||
|
"qemu-system-riscv32",
|
||||||
|
"-nographic",
|
||||||
|
"-M", "esp32c3",
|
||||||
|
"-drive", "file=flash.bin,if=mtd,format=raw",
|
||||||
|
# "-global", "driver=timer.esp32c3.timg,property=wdt_disable,value=true", # got panic if we don't disable WDT, why?
|
||||||
|
]
|
||||||
|
|
||||||
|
# Get working directory from environment or use /tmp
|
||||||
|
work_dir = os.getcwd()
|
||||||
|
|
||||||
|
print(f"Starting QEMU in {work_dir}...", flush=True)
|
||||||
|
print(f"Command: {' '.join(cmd)}", flush=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
qemu_process = await asyncio.create_subprocess_exec(
|
||||||
|
*cmd,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
stdin=asyncio.subprocess.PIPE,
|
||||||
|
cwd=work_dir,
|
||||||
|
env=os.environ.copy()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Read stdout and stderr concurrently
|
||||||
|
await asyncio.gather(
|
||||||
|
read_stream(qemu_process.stdout, "stdout"),
|
||||||
|
read_stream(qemu_process.stderr, "stderr")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait for process to complete
|
||||||
|
await qemu_process.wait()
|
||||||
|
print(f"QEMU process exited with code {qemu_process.returncode}", flush=True)
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("Error: qemu-system-riscv32 not found. Make sure it's in PATH.", file=sys.stderr)
|
||||||
|
await broadcast_message("stderr", "Error: qemu-system-riscv32 not found")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error spawning QEMU: {e}", file=sys.stderr)
|
||||||
|
await broadcast_message("stderr", f"Error spawning QEMU: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def websocket_handler(websocket: WebSocketServerProtocol):
|
||||||
|
"""Handle a WebSocket connection."""
|
||||||
|
connected_clients.add(websocket)
|
||||||
|
print(f"Client connected. Total clients: {len(connected_clients)}", flush=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Send a welcome message
|
||||||
|
await websocket.send(json.dumps({
|
||||||
|
"type": "info",
|
||||||
|
"data": "Connected to CrossPoint emulator"
|
||||||
|
}))
|
||||||
|
|
||||||
|
# Send the last display buffer if available
|
||||||
|
if LAST_DISPLAY_BUFFER is not None:
|
||||||
|
await websocket.send(json.dumps({
|
||||||
|
"type": "stdout",
|
||||||
|
"data": f"$$CMD_DISPLAY:{LAST_DISPLAY_BUFFER}$$"
|
||||||
|
}))
|
||||||
|
|
||||||
|
# Handle incoming messages (for stdin forwarding and events)
|
||||||
|
async for message in websocket:
|
||||||
|
try:
|
||||||
|
data = json.loads(message)
|
||||||
|
msg_type = data.get("type")
|
||||||
|
|
||||||
|
if msg_type == "stdin" and qemu_process and qemu_process.stdin:
|
||||||
|
input_data = data.get("data", "")
|
||||||
|
qemu_process.stdin.write((input_data + "\n").encode())
|
||||||
|
await qemu_process.stdin.drain()
|
||||||
|
elif msg_type == "button_state":
|
||||||
|
global BUTTON_STATE
|
||||||
|
BUTTON_STATE = int(data.get("state", "0"))
|
||||||
|
print(f"[WebUI] Button state updated to {BUTTON_STATE}", flush=True)
|
||||||
|
else:
|
||||||
|
# Print any other messages for debugging
|
||||||
|
print(f"[WebUI Message] {message}", flush=True)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"[WebUI] Invalid JSON received: {message}", file=sys.stderr)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error handling client message: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
except websockets.exceptions.ConnectionClosed:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
connected_clients.discard(websocket)
|
||||||
|
print(f"Client disconnected. Total clients: {len(connected_clients)}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Main entry point."""
|
||||||
|
host = os.environ.get("HOST", "0.0.0.0")
|
||||||
|
port = int(os.environ.get("PORT", "8090"))
|
||||||
|
|
||||||
|
print(f"Starting WebSocket server on {host}:{port}", flush=True)
|
||||||
|
|
||||||
|
# Start WebSocket server
|
||||||
|
async with websockets.serve(
|
||||||
|
websocket_handler, host, port,
|
||||||
|
process_request=process_request,
|
||||||
|
origins=None,
|
||||||
|
):
|
||||||
|
print(f"WebSocket server running on ws://{host}:{port}/ws", flush=True)
|
||||||
|
print(f"Web UI available at http://{host}:{port}/", flush=True)
|
||||||
|
|
||||||
|
# Spawn QEMU process
|
||||||
|
await spawn_qemu()
|
||||||
|
|
||||||
|
|
||||||
|
def signal_handler(signum, frame):
|
||||||
|
"""Handle shutdown signals."""
|
||||||
|
print("\nShutting down...", flush=True)
|
||||||
|
if qemu_process:
|
||||||
|
qemu_process.terminate()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def process_request(connection, request):
|
||||||
|
"""Handle HTTP requests for serving static files."""
|
||||||
|
path = request.path
|
||||||
|
|
||||||
|
if path == "/" or path == "/web_ui.html":
|
||||||
|
# Serve the web_ui.html file
|
||||||
|
html_path = Path(__file__).parent / "web_ui.html"
|
||||||
|
try:
|
||||||
|
content = html_path.read_bytes()
|
||||||
|
return Response(
|
||||||
|
HTTPStatus.OK,
|
||||||
|
"OK",
|
||||||
|
websockets.Headers([
|
||||||
|
("Content-Type", "text/html; charset=utf-8"),
|
||||||
|
("Content-Length", str(len(content))),
|
||||||
|
]),
|
||||||
|
content
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return Response(HTTPStatus.NOT_FOUND, "Not Found", websockets.Headers(), b"web_ui.html not found")
|
||||||
|
|
||||||
|
if path == "/ws":
|
||||||
|
# Return None to continue with WebSocket handshake
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Return 404 for other paths
|
||||||
|
return Response(HTTPStatus.NOT_FOUND, "Not Found", websockets.Headers(), b"Not found")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Set up signal handlers
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
|
# Run the main loop
|
||||||
|
asyncio.run(main())
|
||||||
242
scripts/emulation/web_ui.html
Normal file
242
scripts/emulation/web_ui.html
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>CrossPoint Emulator</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: monospace;
|
||||||
|
margin: 0;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #bbb;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-size: 1.2em;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
margin: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display, .control {
|
||||||
|
width: 50%;
|
||||||
|
height: 95vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 2em;
|
||||||
|
padding-right: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display > canvas {
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control > .ctrl {
|
||||||
|
padding-top: 2em;
|
||||||
|
padding-bottom: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control > #output {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
border: 1px solid #555;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="display">
|
||||||
|
<!-- Display -->
|
||||||
|
<canvas id="screen" width="480" height="800" style="border:1px solid #555;"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control">
|
||||||
|
<!-- Control + Log -->
|
||||||
|
<div class="ctrl">
|
||||||
|
<button id="btn0">Btn 0</button>
|
||||||
|
<button id="btn1">Btn 1</button>
|
||||||
|
|
||||||
|
<button id="btn2">Btn 2</button>
|
||||||
|
<button id="btn3">Btn 3</button>
|
||||||
|
|
||||||
|
<button id="btnU">Btn Up</button>
|
||||||
|
<button id="btnD">Btn Down</button>
|
||||||
|
|
||||||
|
<button id="btnP">Btn Power</button>
|
||||||
|
</div>
|
||||||
|
<div id="output"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const output = document.getElementById('output');
|
||||||
|
const screen = document.getElementById('screen');
|
||||||
|
const ctx = screen.getContext('2d');
|
||||||
|
|
||||||
|
let ws = null;
|
||||||
|
|
||||||
|
const MAX_LOG_LINES = 1000;
|
||||||
|
function appendLog(type, message) {
|
||||||
|
const line = document.createElement('div');
|
||||||
|
line.className = type;
|
||||||
|
line.textContent = message;
|
||||||
|
output.appendChild(line);
|
||||||
|
output.scrollTop = output.scrollHeight; // TODO: only scroll if already at bottom
|
||||||
|
while (output.childElementCount > MAX_LOG_LINES) {
|
||||||
|
output.removeChild(output.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawScreen(b64Data) {
|
||||||
|
// b64Data is a base64-encoded 1-bit per pixel framebuffer
|
||||||
|
// Source buffer is 800x480, with 100 bytes per row (800/8 = 100)
|
||||||
|
// We rotate 90 degrees clockwise to display as 480x800
|
||||||
|
const binaryString = atob(b64Data);
|
||||||
|
const srcWidth = 800;
|
||||||
|
const srcHeight = 480;
|
||||||
|
const srcWidthBytes = srcWidth / 8; // 100 bytes per row
|
||||||
|
|
||||||
|
// After 90° clockwise rotation: new width = srcHeight, new height = srcWidth
|
||||||
|
const dstWidth = srcHeight; // 480
|
||||||
|
const dstHeight = srcWidth; // 800
|
||||||
|
|
||||||
|
const imageData = ctx.createImageData(dstWidth, dstHeight);
|
||||||
|
const pixels = imageData.data;
|
||||||
|
|
||||||
|
for (let srcY = 0; srcY < srcHeight; srcY++) {
|
||||||
|
for (let xByte = 0; xByte < srcWidthBytes; xByte++) {
|
||||||
|
const byteIndex = srcY * srcWidthBytes + xByte;
|
||||||
|
const byte = binaryString.charCodeAt(byteIndex);
|
||||||
|
|
||||||
|
// Each byte contains 8 pixels (MSB first)
|
||||||
|
for (let bit = 0; bit < 8; bit++) {
|
||||||
|
const srcX = xByte * 8 + bit;
|
||||||
|
|
||||||
|
// 90° clockwise rotation: (srcX, srcY) -> (srcHeight - 1 - srcY, srcX)
|
||||||
|
const dstX = srcHeight - 1 - srcY;
|
||||||
|
const dstY = srcX;
|
||||||
|
|
||||||
|
const pixelIndex = (dstY * dstWidth + dstX) * 4;
|
||||||
|
|
||||||
|
// Bit 1 = white (0xFF), Bit 0 = black (0x00)
|
||||||
|
const isWhite = (byte >> (7 - bit)) & 1;
|
||||||
|
const color = isWhite ? 255 : 0;
|
||||||
|
|
||||||
|
pixels[pixelIndex] = color; // R
|
||||||
|
pixels[pixelIndex + 1] = color; // G
|
||||||
|
pixels[pixelIndex + 2] = color; // B
|
||||||
|
pixels[pixelIndex + 3] = 255; // A
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let btn0Pressed = false;
|
||||||
|
let btn1Pressed = false;
|
||||||
|
let btn2Pressed = false;
|
||||||
|
let btn3Pressed = false;
|
||||||
|
let btnUpPressed = false;
|
||||||
|
let btnDownPressed = false;
|
||||||
|
let btnPowerPressed = false;
|
||||||
|
|
||||||
|
let buttonState = 0;
|
||||||
|
function sendButtonState() {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'button_state',
|
||||||
|
state: buttonState
|
||||||
|
}));
|
||||||
|
appendLog('info', `Sent button state: ${buttonState}`);
|
||||||
|
} else {
|
||||||
|
appendLog('error', 'WebSocket not connected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupButtonHandler(buttonId, bitIndex) {
|
||||||
|
const button = document.getElementById(buttonId);
|
||||||
|
button.onmousedown = () => {
|
||||||
|
buttonState ^= (1 << bitIndex);
|
||||||
|
sendButtonState();
|
||||||
|
};
|
||||||
|
button.onmouseup = () => {
|
||||||
|
buttonState &= ~(1 << bitIndex);
|
||||||
|
sendButtonState();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
setupButtonHandler('btn0', 0);
|
||||||
|
setupButtonHandler('btn1', 1);
|
||||||
|
setupButtonHandler('btn2', 2);
|
||||||
|
setupButtonHandler('btn3', 3);
|
||||||
|
setupButtonHandler('btnU', 4);
|
||||||
|
setupButtonHandler('btnD', 5);
|
||||||
|
setupButtonHandler('btnP', 6);
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
||||||
|
|
||||||
|
appendLog('info', `Connecting to ${wsUrl}...`);
|
||||||
|
|
||||||
|
ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
appendLog('info', 'Connected');
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(event.data);
|
||||||
|
if (msg.data.startsWith('$$CMD_')) {
|
||||||
|
// $$CMD_(COMMAND)[:(ARG0)][:(ARG1)][:(ARG2)]$$
|
||||||
|
const parts = msg.data.slice(2, -2).split(':');
|
||||||
|
const command = parts[0];
|
||||||
|
appendLog('cmd', `Received command: ${command}`);
|
||||||
|
if (command === 'CMD_DISPLAY') {
|
||||||
|
drawScreen(parts[1]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
appendLog(msg.type, msg.data);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
appendLog('stdout', event.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
appendLog('info', 'Disconnected');
|
||||||
|
setTimeout(connect, 3000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onload = () => {
|
||||||
|
connect();
|
||||||
|
|
||||||
|
// fill canvas with black initially
|
||||||
|
ctx.fillStyle = 'white';
|
||||||
|
ctx.fillRect(0, 0, screen.width, screen.height);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -3,4 +3,16 @@
|
|||||||
|
|
||||||
#define BAT_GPIO0 0 // Battery voltage
|
#define BAT_GPIO0 0 // Battery voltage
|
||||||
|
|
||||||
static BatteryMonitor battery(BAT_GPIO0);
|
class Battery {
|
||||||
|
public:
|
||||||
|
uint16_t readPercentage() const {
|
||||||
|
#if CROSSPOINT_EMULATED == 0
|
||||||
|
static const BatteryMonitor hwBattery = BatteryMonitor(BAT_GPIO0);
|
||||||
|
return hwBattery.readPercentage();
|
||||||
|
#else
|
||||||
|
return 100;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static Battery battery;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <InputManager.h>
|
#include <HalInput.h>
|
||||||
|
|
||||||
class MappedInputManager {
|
class MappedInputManager {
|
||||||
public:
|
public:
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
#include "WifiCredentialStore.h"
|
#include "WifiCredentialStore.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
// Initialize the static instance
|
// Initialize the static instance
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <Txt.h>
|
#include <Txt.h>
|
||||||
#include <Xtc.h>
|
#include <Xtc.h>
|
||||||
|
|
||||||
@ -133,7 +133,7 @@ void SleepActivity::renderDefaultSleepScreen() const {
|
|||||||
renderer.invertScreen();
|
renderer.invertScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
|
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
|
||||||
@ -180,7 +180,7 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
|
|||||||
Serial.printf("[%lu] [SLP] drawing to %d x %d\n", millis(), x, y);
|
Serial.printf("[%lu] [SLP] drawing to %d x %d\n", millis(), x, y);
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
|
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
|
||||||
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||||
|
|
||||||
if (bitmap.hasGreyscale()) {
|
if (bitmap.hasGreyscale()) {
|
||||||
bitmap.rewindToData();
|
bitmap.rewindToData();
|
||||||
@ -270,5 +270,5 @@ void SleepActivity::renderCoverSleepScreen() const {
|
|||||||
|
|
||||||
void SleepActivity::renderBlankSleepScreen() const {
|
void SleepActivity::renderBlankSleepScreen() const {
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
#include <Bitmap.h>
|
#include <Bitmap.h>
|
||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <Xtc.h>
|
#include <Xtc.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
#include "MyLibraryActivity.h"
|
#include "MyLibraryActivity.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <WiFiClient.h>
|
#include <WiFiClient.h>
|
||||||
#include <WiFiUdp.h>
|
#include <WiFiUdp.h>
|
||||||
#include <freertos/FreeRTOS.h>
|
#include <freertos/FreeRTOS.h>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
#include <Epub/Page.h>
|
#include <Epub/Page.h>
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
@ -322,7 +322,7 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
auto progressCallback = [this, barX, barY, barWidth, barHeight](int progress) {
|
auto progressCallback = [this, barX, barY, barWidth, barHeight](int progress) {
|
||||||
const int fillWidth = (barWidth - 2) * progress / 100;
|
const int fillWidth = (barWidth - 2) * progress / 100;
|
||||||
renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true);
|
renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true);
|
||||||
renderer.displayBuffer(EInkDisplay::FAST_REFRESH);
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||||
@ -392,7 +392,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
||||||
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||||
if (pagesUntilFullRefresh <= 1) {
|
if (pagesUntilFullRefresh <= 1) {
|
||||||
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||||
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
||||||
} else {
|
} else {
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
#include "TxtReaderActivity.h"
|
#include "TxtReaderActivity.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
#include <Utf8.h>
|
#include <Utf8.h>
|
||||||
|
|
||||||
@ -237,7 +237,7 @@ void TxtReaderActivity::buildPageIndex() {
|
|||||||
// Fill progress bar
|
// Fill progress bar
|
||||||
const int fillWidth = (barWidth - 2) * progressPercent / 100;
|
const int fillWidth = (barWidth - 2) * progressPercent / 100;
|
||||||
renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true);
|
renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true);
|
||||||
renderer.displayBuffer(EInkDisplay::FAST_REFRESH);
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Yield to other tasks periodically
|
// Yield to other tasks periodically
|
||||||
@ -465,7 +465,7 @@ void TxtReaderActivity::renderPage() {
|
|||||||
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||||
|
|
||||||
if (pagesUntilFullRefresh <= 1) {
|
if (pagesUntilFullRefresh <= 1) {
|
||||||
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||||
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
||||||
} else {
|
} else {
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
@ -269,7 +269,7 @@ void XtcReaderActivity::renderPage() {
|
|||||||
|
|
||||||
// Display BW with conditional refresh based on pagesUntilFullRefresh
|
// Display BW with conditional refresh based on pagesUntilFullRefresh
|
||||||
if (pagesUntilFullRefresh <= 1) {
|
if (pagesUntilFullRefresh <= 1) {
|
||||||
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||||
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
||||||
} else {
|
} else {
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
@ -349,7 +349,7 @@ void XtcReaderActivity::renderPage() {
|
|||||||
|
|
||||||
// Display with appropriate refresh
|
// Display with appropriate refresh
|
||||||
if (pagesUntilFullRefresh <= 1) {
|
if (pagesUntilFullRefresh <= 1) {
|
||||||
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||||
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
||||||
} else {
|
} else {
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <EInkDisplay.h>
|
#include <HalDisplay.h>
|
||||||
#include <EpdFontFamily.h>
|
#include <EpdFontFamily.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
@ -10,12 +10,12 @@
|
|||||||
class FullScreenMessageActivity final : public Activity {
|
class FullScreenMessageActivity final : public Activity {
|
||||||
std::string text;
|
std::string text;
|
||||||
EpdFontFamily::Style style;
|
EpdFontFamily::Style style;
|
||||||
EInkDisplay::RefreshMode refreshMode;
|
HalDisplay::RefreshMode refreshMode;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit FullScreenMessageActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string text,
|
explicit FullScreenMessageActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string text,
|
||||||
const EpdFontFamily::Style style = EpdFontFamily::REGULAR,
|
const EpdFontFamily::Style style = EpdFontFamily::REGULAR,
|
||||||
const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH)
|
const HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH)
|
||||||
: Activity("FullScreenMessage", renderer, mappedInput),
|
: Activity("FullScreenMessage", renderer, mappedInput),
|
||||||
text(std::move(text)),
|
text(std::move(text)),
|
||||||
style(style),
|
style(style),
|
||||||
|
|||||||
25
src/main.cpp
25
src/main.cpp
@ -1,9 +1,9 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <EInkDisplay.h>
|
#include <HalDisplay.h>
|
||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <InputManager.h>
|
#include <HalInput.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
#include <builtinFonts/all.h>
|
#include <builtinFonts/all.h>
|
||||||
|
|
||||||
@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
#define SD_SPI_MISO 7
|
#define SD_SPI_MISO 7
|
||||||
|
|
||||||
EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);
|
HalDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);
|
||||||
InputManager inputManager;
|
InputManager inputManager;
|
||||||
MappedInputManager mappedInputManager(inputManager);
|
MappedInputManager mappedInputManager(inputManager);
|
||||||
GfxRenderer renderer(einkDisplay);
|
GfxRenderer renderer(einkDisplay);
|
||||||
@ -184,12 +184,14 @@ void verifyWakeupLongPress() {
|
|||||||
if (abort) {
|
if (abort) {
|
||||||
// Button released too early. Returning to sleep.
|
// Button released too early. Returning to sleep.
|
||||||
// IMPORTANT: Re-arm the wakeup trigger before sleeping again
|
// IMPORTANT: Re-arm the wakeup trigger before sleeping again
|
||||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
startDeepSleep(inputManager);
|
||||||
esp_deep_sleep_start();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void waitForPowerRelease() {
|
void waitForPowerRelease() {
|
||||||
|
if (CROSSPOINT_EMULATED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
inputManager.update();
|
inputManager.update();
|
||||||
while (inputManager.isPressed(InputManager::BTN_POWER)) {
|
while (inputManager.isPressed(InputManager::BTN_POWER)) {
|
||||||
delay(50);
|
delay(50);
|
||||||
@ -205,11 +207,7 @@ void enterDeepSleep() {
|
|||||||
einkDisplay.deepSleep();
|
einkDisplay.deepSleep();
|
||||||
Serial.printf("[%lu] [ ] Power button press calibration value: %lu ms\n", millis(), t2 - t1);
|
Serial.printf("[%lu] [ ] Power button press calibration value: %lu ms\n", millis(), t2 - t1);
|
||||||
Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis());
|
Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis());
|
||||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
startDeepSleep(inputManager);
|
||||||
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
|
|
||||||
waitForPowerRelease();
|
|
||||||
// Enter Deep Sleep
|
|
||||||
esp_deep_sleep_start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onGoHome();
|
void onGoHome();
|
||||||
@ -278,10 +276,13 @@ void setupDisplayAndFonts() {
|
|||||||
|
|
||||||
bool isUsbConnected() {
|
bool isUsbConnected() {
|
||||||
// U0RXD/GPIO20 reads HIGH when USB is connected
|
// U0RXD/GPIO20 reads HIGH when USB is connected
|
||||||
return digitalRead(UART0_RXD) == HIGH;
|
return CROSSPOINT_EMULATED || digitalRead(UART0_RXD) == HIGH;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isWakeupAfterFlashing() {
|
bool isWakeupAfterFlashing() {
|
||||||
|
if (CROSSPOINT_EMULATED) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const auto wakeupCause = esp_sleep_get_wakeup_cause();
|
const auto wakeupCause = esp_sleep_get_wakeup_cause();
|
||||||
const auto resetReason = esp_reset_reason();
|
const auto resetReason = esp_reset_reason();
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <esp_task_wdt.h>
|
#include <esp_task_wdt.h>
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user