Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40ef34aa23 |
@@ -96,7 +96,6 @@ jobs:
|
||||
pio_platform: ${{ matrix.check.platform }}
|
||||
pio_env: ${{ matrix.check.board }}
|
||||
pio_target: check
|
||||
pio_opts: --fail-on-defect=low
|
||||
|
||||
build:
|
||||
needs: [setup, version]
|
||||
|
||||
@@ -33,7 +33,6 @@ __pycache__
|
||||
*~
|
||||
|
||||
venv/
|
||||
.venv/
|
||||
release/
|
||||
.vscode/extensions.json
|
||||
/compile_commands.json
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=0"
|
||||
],
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
|
||||
# Vite cache
|
||||
.vite/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
@@ -0,0 +1,149 @@
|
||||
# Meshtastic OLED Emulator
|
||||
|
||||
A Vue.js component and TypeScript library for emulating SSD1306/SH1106 OLED displays used in Meshtastic devices. This allows firmware UI development and testing directly in the browser.
|
||||
|
||||
## Features
|
||||
|
||||
- **Same API as firmware**: Uses the same `OLEDDisplay` API as esp8266-oled-ssd1306 library
|
||||
- **Multiple display sizes**: 128x64, 128x32, 64x48, 128x128
|
||||
- **Realistic rendering**: Pixel glow, OLED color tints (blue, white, yellow/blue dual)
|
||||
- **XBM image support**: Render firmware icons and images
|
||||
- **Font rendering**: Bitmap font support with text alignment
|
||||
- **Vue 3 component**: Easy integration with Vue applications
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @meshtastic/oled-emulator
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import {
|
||||
OLEDDisplay,
|
||||
OLEDEmulator,
|
||||
ArialMT_Plain_10,
|
||||
} from "@meshtastic/oled-emulator";
|
||||
|
||||
// Create display matching firmware
|
||||
const display = ref(new OLEDDisplay("128x64"));
|
||||
|
||||
onMounted(() => {
|
||||
display.value.setFont(ArialMT_Plain_10);
|
||||
display.value.clear();
|
||||
display.value.drawString(10, 10, "Hello Mesh!");
|
||||
display.value.drawRect(5, 5, 118, 54);
|
||||
display.value.fillCircle(64, 32, 15);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OLEDEmulator
|
||||
:display="display"
|
||||
:pixel-scale="4"
|
||||
preset="ssd1306-blue"
|
||||
:enable-glow="true"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
## OLEDDisplay API
|
||||
|
||||
The `OLEDDisplay` class provides the same methods as the firmware's display library:
|
||||
|
||||
```typescript
|
||||
const display = new OLEDDisplay("128x64");
|
||||
|
||||
// Basic drawing
|
||||
display.clear();
|
||||
display.setPixel(x, y);
|
||||
display.drawLine(x0, y0, x1, y1);
|
||||
display.drawRect(x, y, width, height);
|
||||
display.fillRect(x, y, width, height);
|
||||
display.drawCircle(x, y, radius);
|
||||
display.fillCircle(x, y, radius);
|
||||
|
||||
// Text
|
||||
display.setFont(ArialMT_Plain_10);
|
||||
display.setTextAlignment("TEXT_ALIGN_CENTER");
|
||||
display.drawString(x, y, "Hello");
|
||||
display.drawStringMaxWidth(x, y, maxWidth, "Long text...");
|
||||
const width = display.getStringWidth("text");
|
||||
|
||||
// Images (XBM format)
|
||||
display.drawXbm(x, y, width, height, imageData);
|
||||
|
||||
// Colors
|
||||
display.setColor("WHITE"); // or 'BLACK', 'INVERSE'
|
||||
|
||||
// Buffer access (for custom rendering)
|
||||
const buffer = display.getBuffer(); // Uint8Array
|
||||
```
|
||||
|
||||
## Display Presets
|
||||
|
||||
The emulator includes several realistic display presets:
|
||||
|
||||
| Preset | Description |
|
||||
| --------------------- | ------------------------------- |
|
||||
| `ssd1306-blue` | Classic blue OLED (default) |
|
||||
| `ssd1306-white` | White OLED |
|
||||
| `ssd1306-yellow-blue` | Dual-color (top 16 rows yellow) |
|
||||
| `sh1106` | SH1106 style |
|
||||
| `sh1107` | SH1107 128x128 |
|
||||
| `ssd1306-64x48` | Small 64x48 display |
|
||||
| `ssd1306-128x32` | Wide 128x32 display |
|
||||
|
||||
## Screen Renderers
|
||||
|
||||
Pre-built screen renderers matching firmware UI:
|
||||
|
||||
```typescript
|
||||
import { ScreenRenderers } from "@meshtastic/oled-emulator";
|
||||
|
||||
// Boot screen
|
||||
ScreenRenderers.drawBootScreen(display, "2.5.0");
|
||||
|
||||
// Node info screen
|
||||
ScreenRenderers.drawNodeInfoScreen(display, {
|
||||
shortName: "MESH",
|
||||
longName: "My Node",
|
||||
nodeId: "!abcd1234",
|
||||
lastHeard: "2m ago",
|
||||
snr: 12.5,
|
||||
});
|
||||
|
||||
// Message screen
|
||||
ScreenRenderers.drawMessageScreen(display, {
|
||||
from: "Alice",
|
||||
text: "Hello from the mesh!",
|
||||
time: "14:32",
|
||||
channel: "LongFast",
|
||||
});
|
||||
|
||||
// And more: drawGPSScreen, drawNodeListScreen, drawSystemScreen, drawCompassScreen
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run development server
|
||||
npm run dev
|
||||
|
||||
# Build library
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Future: Protocol Bridge
|
||||
|
||||
A future enhancement will allow connecting this emulator to a real Meshtastic firmware instance (running on native/portduino), enabling live framebuffer streaming over WebSocket for real-time display mirroring.
|
||||
|
||||
## License
|
||||
|
||||
GPL-3.0 - Same as Meshtastic firmware
|
||||
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Meshtastic OLED Emulator</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #121212;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+2211
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "@meshtastic/oled-emulator",
|
||||
"version": "0.1.0",
|
||||
"description": "Vue.js OLED display emulator for Meshtastic firmware UI development",
|
||||
"keywords": [
|
||||
"meshtastic",
|
||||
"oled",
|
||||
"ssd1306",
|
||||
"sh1106",
|
||||
"display",
|
||||
"emulator",
|
||||
"vue",
|
||||
"vue3"
|
||||
],
|
||||
"author": "Meshtastic",
|
||||
"license": "GPL-3.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/meshtastic/firmware.git",
|
||||
"directory": "oled-emulator"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./style.css": "./dist/style.css"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"typescript": "^5.3.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-dts": "^3.7.0",
|
||||
"vue": "^3.4.0",
|
||||
"vue-tsc": "^2.0.0"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* DisplayPresets.ts - OLED display presets for emulator
|
||||
*/
|
||||
|
||||
import type { OLEDDISPLAY_GEOMETRY } from "./OLEDDisplay";
|
||||
|
||||
// Display type presets with realistic OLED colors
|
||||
export interface DisplayPreset {
|
||||
name: string;
|
||||
geometry: OLEDDISPLAY_GEOMETRY;
|
||||
pixelOn: { r: number; g: number; b: number };
|
||||
pixelOff: { r: number; g: number; b: number };
|
||||
glow: number;
|
||||
contrast: number;
|
||||
}
|
||||
|
||||
export const DISPLAY_PRESETS: Record<string, DisplayPreset> = {
|
||||
// Classic blue OLED (SSD1306)
|
||||
"ssd1306-blue": {
|
||||
name: "SSD1306 Blue",
|
||||
geometry: "128x64",
|
||||
pixelOn: { r: 100, g: 180, b: 255 },
|
||||
pixelOff: { r: 2, g: 3, b: 8 },
|
||||
glow: 0.4,
|
||||
contrast: 1.0,
|
||||
},
|
||||
// White OLED (SSD1306)
|
||||
"ssd1306-white": {
|
||||
name: "SSD1306 White",
|
||||
geometry: "128x64",
|
||||
pixelOn: { r: 255, g: 255, b: 245 },
|
||||
pixelOff: { r: 5, g: 5, b: 5 },
|
||||
glow: 0.3,
|
||||
contrast: 1.0,
|
||||
},
|
||||
// Yellow-blue dual color OLED
|
||||
"ssd1306-yellow-blue": {
|
||||
name: "SSD1306 Yellow/Blue",
|
||||
geometry: "128x64",
|
||||
// Yellow section is top 16 rows, blue is rest
|
||||
pixelOn: { r: 255, g: 220, b: 50 }, // Will be overridden per-pixel
|
||||
pixelOff: { r: 3, g: 3, b: 5 },
|
||||
glow: 0.35,
|
||||
contrast: 1.0,
|
||||
},
|
||||
// SH1106 (slightly different characteristics)
|
||||
sh1106: {
|
||||
name: "SH1106",
|
||||
geometry: "128x64",
|
||||
pixelOn: { r: 120, g: 200, b: 255 },
|
||||
pixelOff: { r: 2, g: 2, b: 6 },
|
||||
glow: 0.45,
|
||||
contrast: 0.95,
|
||||
},
|
||||
// SH1107 128x128
|
||||
sh1107: {
|
||||
name: "SH1107 128x128",
|
||||
geometry: "128x128",
|
||||
pixelOn: { r: 255, g: 255, b: 240 },
|
||||
pixelOff: { r: 4, g: 4, b: 6 },
|
||||
glow: 0.3,
|
||||
contrast: 1.0,
|
||||
},
|
||||
// Small 64x48 OLED
|
||||
"ssd1306-64x48": {
|
||||
name: "SSD1306 64x48",
|
||||
geometry: "64x48",
|
||||
pixelOn: { r: 100, g: 180, b: 255 },
|
||||
pixelOff: { r: 2, g: 3, b: 8 },
|
||||
glow: 0.4,
|
||||
contrast: 1.0,
|
||||
},
|
||||
// Mini 128x32
|
||||
"ssd1306-128x32": {
|
||||
name: "SSD1306 128x32",
|
||||
geometry: "128x32",
|
||||
pixelOn: { r: 100, g: 180, b: 255 },
|
||||
pixelOff: { r: 2, g: 3, b: 8 },
|
||||
glow: 0.4,
|
||||
contrast: 1.0,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,557 @@
|
||||
/**
|
||||
* OLEDDisplay.ts - TypeScript port of esp8266-oled-ssd1306 OLEDDisplay API
|
||||
*
|
||||
* This provides the same drawing API as the firmware uses, allowing
|
||||
* Meshtastic screen rendering code to be ported to the web.
|
||||
*
|
||||
* Buffer format: Vertical byte packing (8 vertical pixels per byte)
|
||||
* Same as SSD1306/SH1106 native format.
|
||||
*/
|
||||
|
||||
export type OLEDDISPLAY_GEOMETRY =
|
||||
| "128x64"
|
||||
| "128x32"
|
||||
| "64x48"
|
||||
| "64x32"
|
||||
| "128x128"
|
||||
| "96x16";
|
||||
|
||||
export type OLEDDISPLAY_COLOR = "WHITE" | "BLACK" | "INVERSE";
|
||||
|
||||
export type OLEDDISPLAY_TEXT_ALIGNMENT =
|
||||
| "TEXT_ALIGN_LEFT"
|
||||
| "TEXT_ALIGN_CENTER"
|
||||
| "TEXT_ALIGN_RIGHT";
|
||||
|
||||
export interface OLEDFont {
|
||||
height: number;
|
||||
firstChar: number;
|
||||
charCount: number;
|
||||
widths: number[];
|
||||
data: Uint8Array;
|
||||
}
|
||||
|
||||
// Resolution info based on geometry
|
||||
const GEOMETRY_SIZES: Record<
|
||||
OLEDDISPLAY_GEOMETRY,
|
||||
{ width: number; height: number }
|
||||
> = {
|
||||
"128x64": { width: 128, height: 64 },
|
||||
"128x32": { width: 128, height: 32 },
|
||||
"64x48": { width: 64, height: 48 },
|
||||
"64x32": { width: 64, height: 32 },
|
||||
"128x128": { width: 128, height: 128 },
|
||||
"96x16": { width: 96, height: 16 },
|
||||
};
|
||||
|
||||
export class OLEDDisplay {
|
||||
private buffer: Uint8Array;
|
||||
private _width: number;
|
||||
private _height: number;
|
||||
private color: OLEDDISPLAY_COLOR = "WHITE";
|
||||
private textAlignment: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_LEFT";
|
||||
private font: OLEDFont | null = null;
|
||||
|
||||
constructor(geometry: OLEDDISPLAY_GEOMETRY = "128x64") {
|
||||
const size = GEOMETRY_SIZES[geometry];
|
||||
this._width = size.width;
|
||||
this._height = size.height;
|
||||
// Buffer size: width * (height / 8) - vertical byte packing
|
||||
this.buffer = new Uint8Array((this._width * this._height) / 8);
|
||||
}
|
||||
|
||||
get width(): number {
|
||||
return this._width;
|
||||
}
|
||||
|
||||
get height(): number {
|
||||
return this._height;
|
||||
}
|
||||
|
||||
getWidth(): number {
|
||||
return this._width;
|
||||
}
|
||||
|
||||
getHeight(): number {
|
||||
return this._height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw framebuffer (vertical byte packing format)
|
||||
*/
|
||||
getBuffer(): Uint8Array {
|
||||
return this.buffer;
|
||||
}
|
||||
|
||||
getBufferSize(): number {
|
||||
return this.buffer.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the display buffer
|
||||
*/
|
||||
clear(): void {
|
||||
this.buffer.fill(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the drawing color
|
||||
*/
|
||||
setColor(color: OLEDDISPLAY_COLOR): void {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set text alignment for drawString
|
||||
*/
|
||||
setTextAlignment(align: OLEDDISPLAY_TEXT_ALIGNMENT): void {
|
||||
this.textAlignment = align;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current font
|
||||
*/
|
||||
setFont(font: OLEDFont): void {
|
||||
this.font = font;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a single pixel
|
||||
*/
|
||||
setPixel(x: number, y: number): void {
|
||||
if (x < 0 || x >= this._width || y < 0 || y >= this._height) return;
|
||||
|
||||
// Vertical byte packing: each byte represents 8 vertical pixels
|
||||
const byteIndex = x + Math.floor(y / 8) * this._width;
|
||||
const bitMask = 1 << (y & 7);
|
||||
|
||||
switch (this.color) {
|
||||
case "WHITE":
|
||||
this.buffer[byteIndex] |= bitMask;
|
||||
break;
|
||||
case "BLACK":
|
||||
this.buffer[byteIndex] &= ~bitMask;
|
||||
break;
|
||||
case "INVERSE":
|
||||
this.buffer[byteIndex] ^= bitMask;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a single pixel (set to black)
|
||||
*/
|
||||
clearPixel(x: number, y: number): void {
|
||||
if (x < 0 || x >= this._width || y < 0 || y >= this._height) return;
|
||||
const byteIndex = x + Math.floor(y / 8) * this._width;
|
||||
const bitMask = 1 << (y & 7);
|
||||
this.buffer[byteIndex] &= ~bitMask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a pixel value (true = on, false = off)
|
||||
*/
|
||||
getPixel(x: number, y: number): boolean {
|
||||
if (x < 0 || x >= this._width || y < 0 || y >= this._height) return false;
|
||||
const byteIndex = x + Math.floor(y / 8) * this._width;
|
||||
const bitMask = 1 << (y & 7);
|
||||
return (this.buffer[byteIndex] & bitMask) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a line using Bresenham's algorithm
|
||||
*/
|
||||
drawLine(x0: number, y0: number, x1: number, y1: number): void {
|
||||
const dx = Math.abs(x1 - x0);
|
||||
const dy = Math.abs(y1 - y0);
|
||||
const sx = x0 < x1 ? 1 : -1;
|
||||
const sy = y0 < y1 ? 1 : -1;
|
||||
let err = dx - dy;
|
||||
|
||||
while (true) {
|
||||
this.setPixel(x0, y0);
|
||||
if (x0 === x1 && y0 === y1) break;
|
||||
const e2 = 2 * err;
|
||||
if (e2 > -dy) {
|
||||
err -= dy;
|
||||
x0 += sx;
|
||||
}
|
||||
if (e2 < dx) {
|
||||
err += dx;
|
||||
y0 += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw horizontal line (optimized)
|
||||
*/
|
||||
drawHorizontalLine(x: number, y: number, length: number): void {
|
||||
if (y < 0 || y >= this._height) return;
|
||||
for (let i = 0; i < length; i++) {
|
||||
this.setPixel(x + i, y);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw vertical line (optimized)
|
||||
*/
|
||||
drawVerticalLine(x: number, y: number, length: number): void {
|
||||
if (x < 0 || x >= this._width) return;
|
||||
for (let i = 0; i < length; i++) {
|
||||
this.setPixel(x, y + i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a rectangle outline
|
||||
*/
|
||||
drawRect(x: number, y: number, width: number, height: number): void {
|
||||
this.drawHorizontalLine(x, y, width);
|
||||
this.drawHorizontalLine(x, y + height - 1, width);
|
||||
this.drawVerticalLine(x, y, height);
|
||||
this.drawVerticalLine(x + width - 1, y, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a filled rectangle
|
||||
*/
|
||||
fillRect(x: number, y: number, width: number, height: number): void {
|
||||
for (let i = 0; i < width; i++) {
|
||||
for (let j = 0; j < height; j++) {
|
||||
this.setPixel(x + i, y + j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a circle outline using midpoint algorithm
|
||||
*/
|
||||
drawCircle(x0: number, y0: number, radius: number): void {
|
||||
let x = radius;
|
||||
let y = 0;
|
||||
let err = 0;
|
||||
|
||||
while (x >= y) {
|
||||
this.setPixel(x0 + x, y0 + y);
|
||||
this.setPixel(x0 + y, y0 + x);
|
||||
this.setPixel(x0 - y, y0 + x);
|
||||
this.setPixel(x0 - x, y0 + y);
|
||||
this.setPixel(x0 - x, y0 - y);
|
||||
this.setPixel(x0 - y, y0 - x);
|
||||
this.setPixel(x0 + y, y0 - x);
|
||||
this.setPixel(x0 + x, y0 - y);
|
||||
|
||||
y++;
|
||||
if (err <= 0) {
|
||||
err += 2 * y + 1;
|
||||
}
|
||||
if (err > 0) {
|
||||
x--;
|
||||
err -= 2 * x + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a filled circle
|
||||
*/
|
||||
fillCircle(x0: number, y0: number, radius: number): void {
|
||||
let x = radius;
|
||||
let y = 0;
|
||||
let err = 0;
|
||||
|
||||
while (x >= y) {
|
||||
this.drawHorizontalLine(x0 - x, y0 + y, 2 * x + 1);
|
||||
this.drawHorizontalLine(x0 - y, y0 + x, 2 * y + 1);
|
||||
this.drawHorizontalLine(x0 - x, y0 - y, 2 * x + 1);
|
||||
this.drawHorizontalLine(x0 - y, y0 - x, 2 * y + 1);
|
||||
|
||||
y++;
|
||||
if (err <= 0) {
|
||||
err += 2 * y + 1;
|
||||
}
|
||||
if (err > 0) {
|
||||
x--;
|
||||
err -= 2 * x + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a quarter circle (quadrant 0-3)
|
||||
*/
|
||||
drawCircleQuads(x0: number, y0: number, radius: number, quads: number): void {
|
||||
let x = radius;
|
||||
let y = 0;
|
||||
let err = 0;
|
||||
|
||||
while (x >= y) {
|
||||
if (quads & 0x1) {
|
||||
this.setPixel(x0 + x, y0 - y);
|
||||
this.setPixel(x0 + y, y0 - x);
|
||||
}
|
||||
if (quads & 0x2) {
|
||||
this.setPixel(x0 - y, y0 - x);
|
||||
this.setPixel(x0 - x, y0 - y);
|
||||
}
|
||||
if (quads & 0x4) {
|
||||
this.setPixel(x0 - x, y0 + y);
|
||||
this.setPixel(x0 - y, y0 + x);
|
||||
}
|
||||
if (quads & 0x8) {
|
||||
this.setPixel(x0 + y, y0 + x);
|
||||
this.setPixel(x0 + x, y0 + y);
|
||||
}
|
||||
|
||||
y++;
|
||||
if (err <= 0) {
|
||||
err += 2 * y + 1;
|
||||
}
|
||||
if (err > 0) {
|
||||
x--;
|
||||
err -= 2 * x + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw an XBM image (like firmware's drawXbm)
|
||||
* XBM format: LSB first, horizontal
|
||||
*/
|
||||
drawXbm(
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
xbm: Uint8Array | number[],
|
||||
): void {
|
||||
const widthBytes = Math.ceil(width / 8);
|
||||
for (let j = 0; j < height; j++) {
|
||||
for (let i = 0; i < width; i++) {
|
||||
const byteIndex = j * widthBytes + Math.floor(i / 8);
|
||||
const bitIndex = i % 8;
|
||||
if (xbm[byteIndex] & (1 << bitIndex)) {
|
||||
this.setPixel(x + i, y + j);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a PROGMEM-style image (vertical byte format, like some firmware icons)
|
||||
*/
|
||||
drawFastImage(
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
image: Uint8Array | number[],
|
||||
): void {
|
||||
const heightInBytes = Math.ceil(height / 8);
|
||||
for (let i = 0; i < width; i++) {
|
||||
for (let j = 0; j < heightInBytes; j++) {
|
||||
const imageByte = image[i + j * width];
|
||||
for (let bit = 0; bit < 8; bit++) {
|
||||
if (imageByte & (1 << bit)) {
|
||||
this.setPixel(x + i, y + j * 8 + bit);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the width of a string with the current font
|
||||
*/
|
||||
getStringWidth(text: string): number {
|
||||
if (!this.font) return 0;
|
||||
let width = 0;
|
||||
for (const char of text) {
|
||||
const charCode = char.charCodeAt(0);
|
||||
if (
|
||||
charCode >= this.font.firstChar &&
|
||||
charCode < this.font.firstChar + this.font.charCount
|
||||
) {
|
||||
width += this.font.widths[charCode - this.font.firstChar];
|
||||
} else {
|
||||
// Unknown char - use space width or default
|
||||
width += this.font.widths[0] || 4;
|
||||
}
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a string at the given position
|
||||
*/
|
||||
drawString(x: number, y: number, text: string): void {
|
||||
if (!this.font) return;
|
||||
|
||||
// Adjust x based on alignment
|
||||
let startX = x;
|
||||
if (this.textAlignment === "TEXT_ALIGN_CENTER") {
|
||||
startX = x - Math.floor(this.getStringWidth(text) / 2);
|
||||
} else if (this.textAlignment === "TEXT_ALIGN_RIGHT") {
|
||||
startX = x - this.getStringWidth(text);
|
||||
}
|
||||
|
||||
let cursorX = startX;
|
||||
for (const char of text) {
|
||||
const charCode = char.charCodeAt(0);
|
||||
if (
|
||||
charCode >= this.font.firstChar &&
|
||||
charCode < this.font.firstChar + this.font.charCount
|
||||
) {
|
||||
const charIndex = charCode - this.font.firstChar;
|
||||
const charWidth = this.font.widths[charIndex];
|
||||
|
||||
// Find the offset into the font data
|
||||
let offset = 0;
|
||||
const bytesPerColumn = Math.ceil(this.font.height / 8);
|
||||
for (let i = 0; i < charIndex; i++) {
|
||||
offset += this.font.widths[i] * bytesPerColumn;
|
||||
}
|
||||
|
||||
// Draw the character
|
||||
this.drawFontChar(
|
||||
cursorX,
|
||||
y,
|
||||
charWidth,
|
||||
this.font.height,
|
||||
this.font.data,
|
||||
offset,
|
||||
);
|
||||
cursorX += charWidth;
|
||||
} else {
|
||||
// Unknown character - advance by space
|
||||
cursorX += this.font.widths[0] || 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a single font character
|
||||
*/
|
||||
private drawFontChar(
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
data: Uint8Array,
|
||||
offset: number,
|
||||
): void {
|
||||
const bytesPerColumn = Math.ceil(height / 8);
|
||||
for (let col = 0; col < width; col++) {
|
||||
for (let byteRow = 0; byteRow < bytesPerColumn; byteRow++) {
|
||||
const dataByte = data[offset + col * bytesPerColumn + byteRow];
|
||||
for (let bit = 0; bit < 8; bit++) {
|
||||
const pixelY = byteRow * 8 + bit;
|
||||
if (pixelY < height && dataByte & (1 << bit)) {
|
||||
this.setPixel(x + col, y + pixelY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a string that wraps within maxWidth
|
||||
*/
|
||||
drawStringMaxWidth(
|
||||
x: number,
|
||||
y: number,
|
||||
maxWidth: number,
|
||||
text: string,
|
||||
): void {
|
||||
if (!this.font) return;
|
||||
|
||||
const words = text.split(" ");
|
||||
let line = "";
|
||||
let lineY = y;
|
||||
|
||||
for (const word of words) {
|
||||
const testLine = line + (line ? " " : "") + word;
|
||||
const testWidth = this.getStringWidth(testLine);
|
||||
|
||||
if (testWidth > maxWidth && line) {
|
||||
this.drawString(x, lineY, line);
|
||||
line = word;
|
||||
lineY += this.font.height;
|
||||
} else {
|
||||
line = testLine;
|
||||
}
|
||||
}
|
||||
|
||||
if (line) {
|
||||
this.drawString(x, lineY, line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a progress bar
|
||||
*/
|
||||
drawProgressBar(
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
progress: number,
|
||||
): void {
|
||||
// Clamp progress 0-100
|
||||
progress = Math.max(0, Math.min(100, progress));
|
||||
|
||||
// Draw outline
|
||||
this.drawRect(x, y, width, height);
|
||||
|
||||
// Draw fill
|
||||
const fillWidth = Math.floor(((width - 4) * progress) / 100);
|
||||
this.fillRect(x + 2, y + 2, fillWidth, height - 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display - no-op for emulator (would send buffer to hardware)
|
||||
*/
|
||||
display(): void {
|
||||
// In real hardware this sends the buffer to the display
|
||||
// For emulator, this is a no-op - we render from getBuffer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Flip the display (useful for some mounting orientations)
|
||||
*/
|
||||
flipScreenVertically(): void {
|
||||
// Rotate buffer 180 degrees
|
||||
const newBuffer = new Uint8Array(this.buffer.length);
|
||||
const heightBytes = this._height / 8;
|
||||
|
||||
for (let x = 0; x < this._width; x++) {
|
||||
for (let yByte = 0; yByte < heightBytes; yByte++) {
|
||||
let srcByte = this.buffer[x + yByte * this._width];
|
||||
// Reverse the bits
|
||||
let reversed = 0;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
if (srcByte & (1 << i)) {
|
||||
reversed |= 1 << (7 - i);
|
||||
}
|
||||
}
|
||||
// Place in mirrored position
|
||||
const newX = this._width - 1 - x;
|
||||
const newYByte = heightBytes - 1 - yByte;
|
||||
newBuffer[newX + newYByte * this._width] = reversed;
|
||||
}
|
||||
}
|
||||
|
||||
this.buffer = newBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
// Color constants for compatibility with firmware code style
|
||||
export const WHITE: OLEDDISPLAY_COLOR = "WHITE";
|
||||
export const BLACK: OLEDDISPLAY_COLOR = "BLACK";
|
||||
export const INVERSE: OLEDDISPLAY_COLOR = "INVERSE";
|
||||
|
||||
export const TEXT_ALIGN_LEFT: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_LEFT";
|
||||
export const TEXT_ALIGN_CENTER: OLEDDISPLAY_TEXT_ALIGNMENT =
|
||||
"TEXT_ALIGN_CENTER";
|
||||
export const TEXT_ALIGN_RIGHT: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_RIGHT";
|
||||
@@ -0,0 +1,327 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* OLEDEmulator.vue - Vue 3 component for OLED display emulation
|
||||
*
|
||||
* Renders an OLEDDisplay framebuffer with realistic OLED visual effects
|
||||
* including pixel glow, color tinting, and various display sizes.
|
||||
*/
|
||||
|
||||
import { ref, computed, watch, onMounted, onUnmounted, type PropType } from 'vue';
|
||||
import { OLEDDisplay } from './OLEDDisplay';
|
||||
import { DISPLAY_PRESETS, type DisplayPreset } from './DisplayPresets';
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* The OLEDDisplay instance to render
|
||||
*/
|
||||
display: {
|
||||
type: Object as PropType<OLEDDisplay>,
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* Pixel scale factor (how many canvas pixels per OLED pixel)
|
||||
*/
|
||||
pixelScale: {
|
||||
type: Number,
|
||||
default: 4,
|
||||
},
|
||||
/**
|
||||
* Display preset name or custom preset object
|
||||
*/
|
||||
preset: {
|
||||
type: [String, Object] as PropType<string | DisplayPreset>,
|
||||
default: 'ssd1306-blue',
|
||||
},
|
||||
/**
|
||||
* Enable pixel glow effect (slight bloom around lit pixels)
|
||||
*/
|
||||
enableGlow: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* Enable sub-pixel rendering for more realistic look
|
||||
*/
|
||||
enableSubPixel: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* Show pixel grid lines
|
||||
*/
|
||||
showGrid: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* Auto-refresh interval in ms (0 = manual refresh only)
|
||||
*/
|
||||
refreshInterval: {
|
||||
type: Number,
|
||||
default: 50,
|
||||
},
|
||||
/**
|
||||
* Yellow/blue split mode for dual-color OLEDs (rows 0-15 yellow)
|
||||
*/
|
||||
dualColor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click', x: number, y: number): void;
|
||||
(e: 'refresh'): void;
|
||||
}>();
|
||||
|
||||
const canvas = ref<HTMLCanvasElement | null>(null);
|
||||
const refreshTimer = ref<number | null>(null);
|
||||
|
||||
// Get the current display preset
|
||||
const currentPreset = computed<DisplayPreset>(() => {
|
||||
if (typeof props.preset === 'string') {
|
||||
return DISPLAY_PRESETS[props.preset] || DISPLAY_PRESETS['ssd1306-blue'];
|
||||
}
|
||||
return props.preset;
|
||||
});
|
||||
|
||||
// Canvas dimensions
|
||||
const canvasWidth = computed(() => props.display.width * props.pixelScale);
|
||||
const canvasHeight = computed(() => props.display.height * props.pixelScale);
|
||||
|
||||
/**
|
||||
* Render the framebuffer to canvas with OLED effects
|
||||
*/
|
||||
function render(): void {
|
||||
const cvs = canvas.value;
|
||||
if (!cvs) return;
|
||||
|
||||
const ctx = cvs.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const buffer = props.display.getBuffer();
|
||||
const width = props.display.width;
|
||||
const height = props.display.height;
|
||||
const scale = props.pixelScale;
|
||||
const preset = currentPreset.value;
|
||||
|
||||
// Create image data
|
||||
const imageData = ctx.createImageData(canvasWidth.value, canvasHeight.value);
|
||||
const data = imageData.data;
|
||||
|
||||
// Yellow color for dual-color mode (top 16 rows)
|
||||
const yellowOn = { r: 255, g: 220, b: 50 };
|
||||
const blueOn = { r: 100, g: 180, b: 255 };
|
||||
|
||||
// Render each OLED pixel
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
// Get pixel from framebuffer (vertical byte packing)
|
||||
const byteIndex = x + Math.floor(y / 8) * width;
|
||||
const bitMask = 1 << (y & 7);
|
||||
const isOn = (buffer[byteIndex] & bitMask) !== 0;
|
||||
|
||||
// Determine pixel color
|
||||
let pixelColor: { r: number; g: number; b: number };
|
||||
if (isOn) {
|
||||
if (props.dualColor && y < 16) {
|
||||
pixelColor = yellowOn;
|
||||
} else if (props.dualColor) {
|
||||
pixelColor = blueOn;
|
||||
} else {
|
||||
pixelColor = preset.pixelOn;
|
||||
}
|
||||
} else {
|
||||
pixelColor = preset.pixelOff;
|
||||
}
|
||||
|
||||
// Apply contrast
|
||||
pixelColor = {
|
||||
r: Math.min(255, pixelColor.r * preset.contrast),
|
||||
g: Math.min(255, pixelColor.g * preset.contrast),
|
||||
b: Math.min(255, pixelColor.b * preset.contrast),
|
||||
};
|
||||
|
||||
// Scale up pixel to canvas
|
||||
for (let sy = 0; sy < scale; sy++) {
|
||||
for (let sx = 0; sx < scale; sx++) {
|
||||
const px = x * scale + sx;
|
||||
const py = y * scale + sy;
|
||||
const idx = (py * canvasWidth.value + px) * 4;
|
||||
|
||||
// Apply glow effect (brighter center, dimmer edges)
|
||||
let intensity = 1.0;
|
||||
if (props.enableGlow && isOn) {
|
||||
const centerX = scale / 2;
|
||||
const centerY = scale / 2;
|
||||
const dist = Math.sqrt(
|
||||
Math.pow(sx - centerX, 2) + Math.pow(sy - centerY, 2)
|
||||
);
|
||||
const maxDist = Math.sqrt(2) * (scale / 2);
|
||||
intensity = 1.0 - (dist / maxDist) * preset.glow;
|
||||
}
|
||||
|
||||
// Apply sub-pixel rendering (RGB stripes)
|
||||
if (props.enableSubPixel && isOn) {
|
||||
const subPixelPos = sx % 3;
|
||||
if (subPixelPos === 0) {
|
||||
data[idx] = pixelColor.r * intensity * 1.2;
|
||||
data[idx + 1] = pixelColor.g * intensity * 0.8;
|
||||
data[idx + 2] = pixelColor.b * intensity * 0.8;
|
||||
} else if (subPixelPos === 1) {
|
||||
data[idx] = pixelColor.r * intensity * 0.8;
|
||||
data[idx + 1] = pixelColor.g * intensity * 1.2;
|
||||
data[idx + 2] = pixelColor.b * intensity * 0.8;
|
||||
} else {
|
||||
data[idx] = pixelColor.r * intensity * 0.8;
|
||||
data[idx + 1] = pixelColor.g * intensity * 0.8;
|
||||
data[idx + 2] = pixelColor.b * intensity * 1.2;
|
||||
}
|
||||
} else {
|
||||
data[idx] = pixelColor.r * intensity;
|
||||
data[idx + 1] = pixelColor.g * intensity;
|
||||
data[idx + 2] = pixelColor.b * intensity;
|
||||
}
|
||||
data[idx + 3] = 255; // Alpha
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw grid lines if enabled
|
||||
if (props.showGrid) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
// Draw right and bottom edge of each pixel
|
||||
// Right edge
|
||||
const rx = (x + 1) * scale - 1;
|
||||
for (let sy = 0; sy < scale; sy++) {
|
||||
const py = y * scale + sy;
|
||||
const idx = (py * canvasWidth.value + rx) * 4;
|
||||
data[idx] = Math.min(255, data[idx] + 20);
|
||||
data[idx + 1] = Math.min(255, data[idx + 1] + 20);
|
||||
data[idx + 2] = Math.min(255, data[idx + 2] + 20);
|
||||
}
|
||||
// Bottom edge
|
||||
const by = (y + 1) * scale - 1;
|
||||
for (let sx = 0; sx < scale; sx++) {
|
||||
const px = x * scale + sx;
|
||||
const idx = (by * canvasWidth.value + px) * 4;
|
||||
data[idx] = Math.min(255, data[idx] + 20);
|
||||
data[idx + 1] = Math.min(255, data[idx + 1] + 20);
|
||||
data[idx + 2] = Math.min(255, data[idx + 2] + 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
emit('refresh');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle canvas click
|
||||
*/
|
||||
function handleClick(event: MouseEvent): void {
|
||||
const cvs = canvas.value;
|
||||
if (!cvs) return;
|
||||
|
||||
const rect = cvs.getBoundingClientRect();
|
||||
const scaleX = cvs.width / rect.width;
|
||||
const scaleY = cvs.height / rect.height;
|
||||
|
||||
const canvasX = (event.clientX - rect.left) * scaleX;
|
||||
const canvasY = (event.clientY - rect.top) * scaleY;
|
||||
|
||||
const oledX = Math.floor(canvasX / props.pixelScale);
|
||||
const oledY = Math.floor(canvasY / props.pixelScale);
|
||||
|
||||
emit('click', oledX, oledY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start auto-refresh timer
|
||||
*/
|
||||
function startRefreshTimer(): void {
|
||||
stopRefreshTimer();
|
||||
if (props.refreshInterval > 0) {
|
||||
refreshTimer.value = window.setInterval(render, props.refreshInterval);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop auto-refresh timer
|
||||
*/
|
||||
function stopRefreshTimer(): void {
|
||||
if (refreshTimer.value !== null) {
|
||||
clearInterval(refreshTimer.value);
|
||||
refreshTimer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
render();
|
||||
startRefreshTimer();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopRefreshTimer();
|
||||
});
|
||||
|
||||
// Watch for changes
|
||||
watch(() => props.refreshInterval, startRefreshTimer);
|
||||
watch(() => props.display, render, { deep: true });
|
||||
watch(() => props.pixelScale, render);
|
||||
watch(() => props.preset, render);
|
||||
watch(() => props.enableGlow, render);
|
||||
watch(() => props.showGrid, render);
|
||||
watch(() => props.dualColor, render);
|
||||
|
||||
// Expose render method for manual refresh
|
||||
defineExpose({ render });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="oled-emulator-wrapper">
|
||||
<canvas
|
||||
ref="canvas"
|
||||
:width="canvasWidth"
|
||||
:height="canvasHeight"
|
||||
class="oled-canvas"
|
||||
@click="handleClick"
|
||||
/>
|
||||
<div class="oled-bezel" v-if="$slots.bezel">
|
||||
<slot name="bezel" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.oled-emulator-wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background: #1a1a1a;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.4),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.oled-canvas {
|
||||
display: block;
|
||||
border-radius: 2px;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
.oled-bezel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,467 @@
|
||||
/**
|
||||
* OLEDFonts.ts - Font data for OLED display emulator
|
||||
*
|
||||
* These are simplified bitmap fonts matching the style used in
|
||||
* Meshtastic firmware (based on ArialMT from esp8266-oled-ssd1306).
|
||||
*
|
||||
* Font format: Vertical byte packing, column-major order
|
||||
* Each character is drawn column by column, with each byte representing
|
||||
* 8 vertical pixels.
|
||||
*/
|
||||
|
||||
import type { OLEDFont } from "./OLEDDisplay";
|
||||
|
||||
/**
|
||||
* ArialMT Plain 10 - Small font (height ~13 pixels)
|
||||
* Covers ASCII 32-126 (space through ~)
|
||||
*/
|
||||
export const ArialMT_Plain_10: OLEDFont = {
|
||||
height: 13,
|
||||
firstChar: 32,
|
||||
charCount: 95,
|
||||
widths: [
|
||||
3, // space
|
||||
3, // !
|
||||
4, // "
|
||||
6, // #
|
||||
6, // $
|
||||
9, // %
|
||||
7, // &
|
||||
2, // '
|
||||
4, // (
|
||||
4, // )
|
||||
5, // *
|
||||
6, // +
|
||||
3, // ,
|
||||
4, // -
|
||||
3, // .
|
||||
4, // /
|
||||
6, // 0
|
||||
6, // 1
|
||||
6, // 2
|
||||
6, // 3
|
||||
6, // 4
|
||||
6, // 5
|
||||
6, // 6
|
||||
6, // 7
|
||||
6, // 8
|
||||
6, // 9
|
||||
3, // :
|
||||
3, // ;
|
||||
6, // <
|
||||
6, // =
|
||||
6, // >
|
||||
6, // ?
|
||||
11, // @
|
||||
8, // A
|
||||
7, // B
|
||||
7, // C
|
||||
7, // D
|
||||
6, // E
|
||||
6, // F
|
||||
8, // G
|
||||
7, // H
|
||||
3, // I
|
||||
5, // J
|
||||
7, // K
|
||||
6, // L
|
||||
9, // M
|
||||
7, // N
|
||||
8, // O
|
||||
6, // P
|
||||
8, // Q
|
||||
7, // R
|
||||
6, // S
|
||||
6, // T
|
||||
7, // U
|
||||
7, // V
|
||||
11, // W
|
||||
7, // X
|
||||
7, // Y
|
||||
6, // Z
|
||||
4, // [
|
||||
4, // \
|
||||
4, // ]
|
||||
6, // ^
|
||||
6, // _
|
||||
4, // `
|
||||
6, // a
|
||||
6, // b
|
||||
5, // c
|
||||
6, // d
|
||||
6, // e
|
||||
4, // f
|
||||
6, // g
|
||||
6, // h
|
||||
3, // i
|
||||
3, // j
|
||||
6, // k
|
||||
3, // l
|
||||
9, // m
|
||||
6, // n
|
||||
6, // o
|
||||
6, // p
|
||||
6, // q
|
||||
4, // r
|
||||
5, // s
|
||||
4, // t
|
||||
6, // u
|
||||
6, // v
|
||||
9, // w
|
||||
6, // x
|
||||
6, // y
|
||||
5, // z
|
||||
4, // {
|
||||
3, // |
|
||||
4, // }
|
||||
6, // ~
|
||||
],
|
||||
data: new Uint8Array([
|
||||
// Space (32)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// ! (33)
|
||||
0x00, 0x00, 0xbe, 0x00, 0x00, 0x00,
|
||||
// " (34)
|
||||
0x00, 0x0e, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00,
|
||||
// # (35)
|
||||
0x00, 0x48, 0xf8, 0x4e, 0xf8, 0x4e, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// $ (36)
|
||||
0x00, 0x9c, 0x92, 0xff, 0x92, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// % (37)
|
||||
0x0c, 0x12, 0x12, 0x8c, 0x60, 0x18, 0xc6, 0x20, 0x20, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// & (38)
|
||||
0x00, 0x60, 0x9c, 0x92, 0x6a, 0xa4, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
// ' (39)
|
||||
0x00, 0x0e, 0x00, 0x00,
|
||||
// ( (40)
|
||||
0x00, 0x7c, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// ) (41)
|
||||
0x00, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// * (42)
|
||||
0x14, 0x08, 0x3e, 0x08, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// + (43)
|
||||
0x10, 0x10, 0x7c, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// , (44)
|
||||
0x00, 0x00, 0xc0, 0x00, 0x00, 0x00,
|
||||
// - (45)
|
||||
0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00,
|
||||
// . (46)
|
||||
0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
|
||||
// / (47)
|
||||
0x00, 0xc0, 0x38, 0x06, 0x00, 0x00, 0x00, 0x00,
|
||||
// 0 (48)
|
||||
0x7c, 0x82, 0x82, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// 1 (49)
|
||||
0x00, 0x04, 0x02, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// 2 (50)
|
||||
0x84, 0xc2, 0xa2, 0x92, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// 3 (51)
|
||||
0x44, 0x82, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// 4 (52)
|
||||
0x30, 0x28, 0x24, 0xfe, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// 5 (53)
|
||||
0x4e, 0x8a, 0x8a, 0x8a, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// 6 (54)
|
||||
0x78, 0x94, 0x92, 0x92, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// 7 (55)
|
||||
0x02, 0xc2, 0x32, 0x0a, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// 8 (56)
|
||||
0x6c, 0x92, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// 9 (57)
|
||||
0x0c, 0x92, 0x92, 0x52, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// : (58)
|
||||
0x00, 0x00, 0x88, 0x00, 0x00, 0x00,
|
||||
// ; (59)
|
||||
0x00, 0x00, 0xc8, 0x00, 0x00, 0x00,
|
||||
// < (60)
|
||||
0x10, 0x28, 0x28, 0x44, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// = (61)
|
||||
0x28, 0x28, 0x28, 0x28, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// > (62)
|
||||
0x44, 0x44, 0x28, 0x28, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// ? (63)
|
||||
0x04, 0x02, 0xa2, 0x12, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// @ (64)
|
||||
0xf0, 0x08, 0xe4, 0x12, 0x12, 0x12, 0xe2, 0x14, 0x08, 0xf0, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// A (65)
|
||||
0x00, 0xc0, 0x38, 0x26, 0x26, 0x38, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
// B (66)
|
||||
0xfe, 0x92, 0x92, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
// C (67)
|
||||
0x7c, 0x82, 0x82, 0x82, 0x82, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
// D (68)
|
||||
0xfe, 0x82, 0x82, 0x82, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
// E (69)
|
||||
0xfe, 0x92, 0x92, 0x92, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// F (70)
|
||||
0xfe, 0x12, 0x12, 0x12, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// G (71)
|
||||
0x7c, 0x82, 0x82, 0x92, 0x92, 0x74, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
// H (72)
|
||||
0xfe, 0x10, 0x10, 0x10, 0x10, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
// I (73)
|
||||
0x00, 0xfe, 0x00, 0x00, 0x00, 0x00,
|
||||
// J (74)
|
||||
0x40, 0x80, 0x80, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// K (75)
|
||||
0xfe, 0x10, 0x28, 0x44, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
// L (76)
|
||||
0xfe, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// M (77)
|
||||
0xfe, 0x04, 0x18, 0x60, 0x60, 0x18, 0x04, 0xfe, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// N (78)
|
||||
0xfe, 0x04, 0x08, 0x30, 0x40, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
// O (79)
|
||||
0x7c, 0x82, 0x82, 0x82, 0x82, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
// P (80)
|
||||
0xfe, 0x12, 0x12, 0x12, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Q (81)
|
||||
0x7c, 0x82, 0x82, 0x82, 0x42, 0xc2, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
// R (82)
|
||||
0xfe, 0x12, 0x12, 0x32, 0x4c, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
// S (83)
|
||||
0x4c, 0x92, 0x92, 0x92, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// T (84)
|
||||
0x02, 0x02, 0xfe, 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// U (85)
|
||||
0x7e, 0x80, 0x80, 0x80, 0x80, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
// V (86)
|
||||
0x06, 0x18, 0x60, 0x80, 0x60, 0x18, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
// W (87)
|
||||
0x06, 0x38, 0xc0, 0x38, 0x06, 0x38, 0xc0, 0x38, 0x06, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// X (88)
|
||||
0x82, 0x44, 0x28, 0x10, 0x28, 0x44, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
// Y (89)
|
||||
0x02, 0x04, 0x08, 0xf0, 0x08, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
// Z (90)
|
||||
0xc2, 0xa2, 0x92, 0x8a, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// [ (91)
|
||||
0x00, 0xfe, 0x82, 0x82, 0x00, 0x00, 0x00, 0x00,
|
||||
// \ (92)
|
||||
0x06, 0x38, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// ] (93)
|
||||
0x82, 0x82, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// ^ (94)
|
||||
0x08, 0x04, 0x02, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// _ (95)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// ` (96)
|
||||
0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// a (97)
|
||||
0x40, 0xa8, 0xa8, 0xa8, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// b (98)
|
||||
0xfe, 0x88, 0x88, 0x88, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// c (99)
|
||||
0x70, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// d (100)
|
||||
0x70, 0x88, 0x88, 0x88, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// e (101)
|
||||
0x70, 0xa8, 0xa8, 0xa8, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// f (102)
|
||||
0x08, 0xfc, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// g (103)
|
||||
0x30, 0x48, 0x48, 0x48, 0xf8, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00,
|
||||
// h (104)
|
||||
0xfe, 0x08, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// i (105)
|
||||
0x00, 0xfa, 0x00, 0x00, 0x00, 0x00,
|
||||
// j (106)
|
||||
0x00, 0x00, 0xfa, 0x01, 0x00, 0x00,
|
||||
// k (107)
|
||||
0xfe, 0x20, 0x50, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// l (108)
|
||||
0x00, 0xfe, 0x00, 0x00, 0x00, 0x00,
|
||||
// m (109)
|
||||
0xf8, 0x08, 0x08, 0xf0, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// n (110)
|
||||
0xf8, 0x08, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// o (111)
|
||||
0x70, 0x88, 0x88, 0x88, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// p (112)
|
||||
0xf8, 0x48, 0x48, 0x48, 0x30, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// q (113)
|
||||
0x30, 0x48, 0x48, 0x48, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
|
||||
// r (114)
|
||||
0xf8, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// s (115)
|
||||
0x90, 0xa8, 0xa8, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// t (116)
|
||||
0x08, 0x7e, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// u (117)
|
||||
0x78, 0x80, 0x80, 0x80, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// v (118)
|
||||
0x18, 0x60, 0x80, 0x60, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// w (119)
|
||||
0x78, 0x80, 0x60, 0x18, 0x60, 0x80, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// x (120)
|
||||
0x88, 0x50, 0x20, 0x50, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// y (121)
|
||||
0x18, 0x60, 0x80, 0x60, 0x18, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// z (122)
|
||||
0xc8, 0xa8, 0x98, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// { (123)
|
||||
0x10, 0x6c, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// | (124)
|
||||
0x00, 0xfe, 0x00, 0x00, 0x00, 0x00,
|
||||
// } (125)
|
||||
0x82, 0x6c, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// ~ (126)
|
||||
0x10, 0x08, 0x10, 0x20, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
]),
|
||||
};
|
||||
|
||||
/**
|
||||
* ArialMT Plain 16 - Medium font (height ~19 pixels)
|
||||
* Simplified subset covering 0-9, A-Z, a-z, common punctuation
|
||||
*/
|
||||
export const ArialMT_Plain_16: OLEDFont = {
|
||||
height: 19,
|
||||
firstChar: 32,
|
||||
charCount: 95,
|
||||
widths: [
|
||||
4, // space
|
||||
4, // !
|
||||
5, // "
|
||||
8, // #
|
||||
8, // $
|
||||
12, // %
|
||||
10, // &
|
||||
2, // '
|
||||
5, // (
|
||||
5, // )
|
||||
6, // *
|
||||
8, // +
|
||||
4, // ,
|
||||
5, // -
|
||||
4, // .
|
||||
4, // /
|
||||
8, // 0
|
||||
8, // 1
|
||||
8, // 2
|
||||
8, // 3
|
||||
8, // 4
|
||||
8, // 5
|
||||
8, // 6
|
||||
8, // 7
|
||||
8, // 8
|
||||
8, // 9
|
||||
4, // :
|
||||
4, // ;
|
||||
8, // <
|
||||
8, // =
|
||||
8, // >
|
||||
8, // ?
|
||||
15, // @
|
||||
10, // A
|
||||
9, // B
|
||||
10, // C
|
||||
10, // D
|
||||
9, // E
|
||||
8, // F
|
||||
11, // G
|
||||
9, // H
|
||||
4, // I
|
||||
6, // J
|
||||
9, // K
|
||||
8, // L
|
||||
11, // M
|
||||
9, // N
|
||||
11, // O
|
||||
9, // P
|
||||
11, // Q
|
||||
10, // R
|
||||
9, // S
|
||||
8, // T
|
||||
9, // U
|
||||
10, // V
|
||||
14, // W
|
||||
10, // X
|
||||
10, // Y
|
||||
9, // Z
|
||||
4, // [
|
||||
4, // \
|
||||
4, // ]
|
||||
7, // ^
|
||||
8, // _
|
||||
5, // `
|
||||
8, // a
|
||||
8, // b
|
||||
7, // c
|
||||
8, // d
|
||||
8, // e
|
||||
4, // f
|
||||
8, // g
|
||||
8, // h
|
||||
4, // i
|
||||
4, // j
|
||||
8, // k
|
||||
4, // l
|
||||
12, // m
|
||||
8, // n
|
||||
8, // o
|
||||
8, // p
|
||||
8, // q
|
||||
5, // r
|
||||
7, // s
|
||||
4, // t
|
||||
8, // u
|
||||
8, // v
|
||||
12, // w
|
||||
8, // x
|
||||
8, // y
|
||||
7, // z
|
||||
5, // {
|
||||
4, // |
|
||||
5, // }
|
||||
8, // ~
|
||||
],
|
||||
// Font data would be similar pattern but larger - using placeholder
|
||||
data: new Uint8Array(2500).fill(0xff), // Placeholder - would contain actual font bitmap data
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to get FONT_HEIGHT_SMALL equivalent
|
||||
*/
|
||||
export const FONT_HEIGHT_SMALL = 13;
|
||||
export const FONT_HEIGHT_MEDIUM = 19;
|
||||
export const FONT_HEIGHT_LARGE = 28;
|
||||
|
||||
/**
|
||||
* Helper function to create a font from raw byte data (for importing firmware fonts)
|
||||
*/
|
||||
export function createFontFromBytes(
|
||||
height: number,
|
||||
firstChar: number,
|
||||
widths: number[],
|
||||
data: number[],
|
||||
): OLEDFont {
|
||||
return {
|
||||
height,
|
||||
firstChar,
|
||||
charCount: widths.length,
|
||||
widths,
|
||||
data: new Uint8Array(data),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* OLEDImages.ts - Icon and image data for OLED display emulator
|
||||
*
|
||||
* These are direct ports of the XBM/bitmap icons from the Meshtastic firmware.
|
||||
* Format: XBM style (LSB first, horizontal scanning)
|
||||
*/
|
||||
|
||||
// Satellite icon (8x8)
|
||||
export const imgSatellite_width = 8;
|
||||
export const imgSatellite_height = 8;
|
||||
export const imgSatellite = new Uint8Array([
|
||||
0b00000000, 0b00000000, 0b00000000, 0b00011000, 0b11011011, 0b11111111,
|
||||
0b11011011, 0b00011000,
|
||||
]);
|
||||
|
||||
// USB icon (10x8)
|
||||
export const imgUSB = new Uint8Array([
|
||||
0x00, 0xfc, 0xf0, 0xfc, 0x88, 0xff, 0x86, 0xfe, 0x85, 0xfe, 0x89, 0xff, 0xf1,
|
||||
0xfc, 0x00, 0xfc,
|
||||
]);
|
||||
|
||||
// Power icon (8x16)
|
||||
export const imgPower = new Uint8Array([
|
||||
0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08, 0x1c, 0x22, 0x22, 0x41, 0x7f,
|
||||
0x22, 0x22, 0x22,
|
||||
]);
|
||||
|
||||
// User icon (8x8)
|
||||
export const imgUser = new Uint8Array([
|
||||
0x3c, 0x42, 0x99, 0xa5, 0xa5, 0x99, 0x42, 0x3c,
|
||||
]);
|
||||
|
||||
// Position empty arrow (6x8)
|
||||
export const imgPositionEmpty = new Uint8Array([
|
||||
0x20, 0x30, 0x28, 0x24, 0x42, 0xff,
|
||||
]);
|
||||
|
||||
// Position solid arrow (6x8)
|
||||
export const imgPositionSolid = new Uint8Array([
|
||||
0x20, 0x30, 0x38, 0x3c, 0x7e, 0xff,
|
||||
]);
|
||||
|
||||
// Mail icon (10x7)
|
||||
export const mail_width = 10;
|
||||
export const mail_height = 7;
|
||||
export const mail = new Uint8Array([
|
||||
0b11111111,
|
||||
0b00, // Top line
|
||||
0b10000001,
|
||||
0b00, // Edges
|
||||
0b11000011,
|
||||
0b00, // Diagonals start
|
||||
0b10100101,
|
||||
0b00, // Inner M part
|
||||
0b10011001,
|
||||
0b00, // Inner M part
|
||||
0b10000001,
|
||||
0b00, // Edges
|
||||
0b11111111,
|
||||
0b00, // Bottom line
|
||||
]);
|
||||
|
||||
// Hop icon (9x10)
|
||||
export const hop_width = 9;
|
||||
export const hop_height = 10;
|
||||
export const hop = new Uint8Array([
|
||||
0x05, 0x00, 0x07, 0x00, 0x05, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, 0x00, 0xc0,
|
||||
0x01, 0x40, 0x01, 0xc0, 0x01, 0x40, 0x00,
|
||||
]);
|
||||
|
||||
// Mail icon (8x8)
|
||||
export const icon_mail = new Uint8Array([
|
||||
0b11111111, // ████████ top border
|
||||
0b10000001, // █ █ sides
|
||||
0b11000011, // ██ ██ diagonal
|
||||
0b10100101, // █ █ █ █ inner M
|
||||
0b10011001, // █ ██ █ inner M
|
||||
0b10000001, // █ █ sides
|
||||
0b10000001, // █ █ sides
|
||||
0b11111111, // ████████ bottom
|
||||
]);
|
||||
|
||||
// Compass icon (8x8)
|
||||
export const icon_compass = new Uint8Array([
|
||||
0x3c, // Row 0: ..####..
|
||||
0x52, // Row 1: .#..#.#.
|
||||
0x91, // Row 2: #...#..#
|
||||
0x91, // Row 3: #...#..#
|
||||
0x91, // Row 4: #...#..#
|
||||
0x81, // Row 5: #......#
|
||||
0x42, // Row 6: .#....#.
|
||||
0x3c, // Row 7: ..####..
|
||||
]);
|
||||
|
||||
// Radio icon (8x8)
|
||||
export const icon_radio = new Uint8Array([
|
||||
0x0f, // Row 0: ####....
|
||||
0x10, // Row 1: ....#...
|
||||
0x27, // Row 2: ###..#..
|
||||
0x48, // Row 3: ...#..#.
|
||||
0x93, // Row 4: ##..#..#
|
||||
0xa4, // Row 5: ..#..#.#
|
||||
0xa8, // Row 6: ...#.#.#
|
||||
0xa9, // Row 7: #..#.#.#
|
||||
]);
|
||||
|
||||
// System/settings icon (8x8)
|
||||
export const icon_system = new Uint8Array([
|
||||
0x24, // Row 0: ..#..#..
|
||||
0x3c, // Row 1: ..####..
|
||||
0xc3, // Row 2: ##....##
|
||||
0x5a, // Row 3: .#.##.#.
|
||||
0x5a, // Row 4: .#.##.#.
|
||||
0xc3, // Row 5: ##....##
|
||||
0x3c, // Row 6: ..####..
|
||||
0x24, // Row 7: ..#..#..
|
||||
]);
|
||||
|
||||
// WiFi icon (8x8)
|
||||
export const icon_wifi = new Uint8Array([
|
||||
0b00000000, 0b00011000, 0b00111100, 0b01111110, 0b11011011, 0b00011000,
|
||||
0b00011000, 0b00000000,
|
||||
]);
|
||||
|
||||
// Nodes icon (8x8)
|
||||
export const icon_nodes = new Uint8Array([
|
||||
0xf9, // Row 0: #..#####
|
||||
0x00, // Row 1
|
||||
0xf9, // Row 2: #..#####
|
||||
0x00, // Row 3
|
||||
0xf9, // Row 4: #..#####
|
||||
0x00, // Row 5
|
||||
0xf9, // Row 6: #..#####
|
||||
0x00, // Row 7
|
||||
]);
|
||||
|
||||
// Battery vertical (7x11)
|
||||
export const batteryBitmap_v = new Uint8Array([
|
||||
0b00011100, 0b00111110, 0b01000001, 0b01000001, 0b00000000, 0b00000000,
|
||||
0b00000000, 0b01000001, 0b01000001, 0b01000001, 0b00111110,
|
||||
]);
|
||||
|
||||
// Battery side gaps (8x3)
|
||||
export const batteryBitmap_sidegaps_v = new Uint8Array([
|
||||
0b10000010, 0b10000010, 0b10000010,
|
||||
]);
|
||||
|
||||
// Lightning bolt vertical (5x5)
|
||||
export const lightning_bolt_v = new Uint8Array([
|
||||
0b00000100, 0b00000110, 0b00011111, 0b00001100, 0b00000100,
|
||||
]);
|
||||
|
||||
// Horizontal battery bottom (13x9)
|
||||
export const batteryBitmap_h_bottom = new Uint8Array([
|
||||
0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
|
||||
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
|
||||
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
|
||||
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
|
||||
0b00011110, 0b00000000,
|
||||
]);
|
||||
|
||||
// Horizontal battery top (13x9)
|
||||
export const batteryBitmap_h_top = new Uint8Array([
|
||||
0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000,
|
||||
0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b11000000, 0b00000000,
|
||||
0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000,
|
||||
0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000,
|
||||
0b00111100, 0b00000000,
|
||||
]);
|
||||
|
||||
// Lightning bolt horizontal (13x9)
|
||||
export const lightning_bolt_h = new Uint8Array([
|
||||
0b00000000, 0b00000000, 0b00100000, 0b00000000, 0b00110000, 0b00000000,
|
||||
0b00111000, 0b00000000, 0b00111100, 0b00000000, 0b00011110, 0b00000000,
|
||||
0b11111111, 0b00000000, 0b01111000, 0b00000000, 0b00111100, 0b00000000,
|
||||
0b00011100, 0b00000000, 0b00001100, 0b00000000, 0b00000100, 0b00000000,
|
||||
0b00000000, 0b00000000,
|
||||
]);
|
||||
|
||||
// Signal bars icon (various signal strengths)
|
||||
export const signal_bars = {
|
||||
width: 12,
|
||||
height: 8,
|
||||
// 0 bars
|
||||
none: new Uint8Array([
|
||||
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
||||
]),
|
||||
// 1 bar
|
||||
low: new Uint8Array([
|
||||
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
||||
0b00000011, 0b00000000, 0b00000011, 0b00000000,
|
||||
]),
|
||||
// 2 bars
|
||||
medium: new Uint8Array([
|
||||
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000, 0b00001100, 0b00000000, 0b00001100, 0b00000000,
|
||||
0b00001111, 0b00000000, 0b00001111, 0b00000000,
|
||||
]),
|
||||
// 3 bars
|
||||
high: new Uint8Array([
|
||||
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00110000, 0b00000000,
|
||||
0b00110000, 0b00000000, 0b00111100, 0b00000000, 0b00111100, 0b00000000,
|
||||
0b00111111, 0b00000000, 0b00111111, 0b00000000,
|
||||
]),
|
||||
// 4 bars (full)
|
||||
full: new Uint8Array([
|
||||
0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11110000, 0b00000000,
|
||||
0b11110000, 0b00000000, 0b11111100, 0b00000000, 0b11111100, 0b00000000,
|
||||
0b11111111, 0b00000000, 0b11111111, 0b00000000,
|
||||
]),
|
||||
};
|
||||
|
||||
// Meshtastic logo (simplified 32x32)
|
||||
export const logo_width = 32;
|
||||
export const logo_height = 32;
|
||||
export const meshtastic_logo = new Uint8Array([
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00,
|
||||
0x03, 0xc0, 0x00, 0x80, 0x01, 0x80, 0x01, 0xc0, 0x00, 0x00, 0x03, 0x60, 0x00,
|
||||
0x00, 0x06, 0x30, 0x00, 0x00, 0x0c, 0x18, 0xf8, 0x1f, 0x18, 0x08, 0x04, 0x20,
|
||||
0x10, 0x0c, 0x02, 0x40, 0x30, 0x04, 0x01, 0x80, 0x20, 0x06, 0x01, 0x80, 0x60,
|
||||
0x02, 0xe1, 0x87, 0x40, 0x02, 0x11, 0x88, 0x40, 0x03, 0x09, 0x90, 0xc0, 0x03,
|
||||
0x09, 0x90, 0xc0, 0x02, 0x11, 0x88, 0x40, 0x02, 0xe1, 0x87, 0x40, 0x06, 0x01,
|
||||
0x80, 0x60, 0x04, 0x01, 0x80, 0x20, 0x0c, 0x02, 0x40, 0x30, 0x08, 0x04, 0x20,
|
||||
0x10, 0x18, 0xf8, 0x1f, 0x18, 0x30, 0x00, 0x00, 0x0c, 0x60, 0x00, 0x00, 0x06,
|
||||
0xc0, 0x00, 0x00, 0x03, 0x80, 0x01, 0x80, 0x01, 0x00, 0x03, 0xc0, 0x00, 0x00,
|
||||
0x06, 0x60, 0x00, 0x00, 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
]);
|
||||
|
||||
// Bluetooth icon (8x11)
|
||||
export const icon_bluetooth = new Uint8Array([
|
||||
0b00000000, 0b10000100, 0b01001010, 0b00101010, 0b00010100, 0b11111111,
|
||||
0b00010100, 0b00101010, 0b01001010, 0b10000100, 0b00000000,
|
||||
]);
|
||||
|
||||
// GPS/Location pin icon (8x12)
|
||||
export const icon_gps_pin = new Uint8Array([
|
||||
0b00111100, 0b01111110, 0b11111111, 0b11100111, 0b11100111, 0b11111111,
|
||||
0b01111110, 0b00111100, 0b00011000, 0b00011000, 0b00001000, 0b00000000,
|
||||
]);
|
||||
|
||||
// Message/chat bubble icon (10x8)
|
||||
export const icon_message = new Uint8Array([
|
||||
0b11111111, 0b00, 0b10000001, 0b00, 0b10000001, 0b00, 0b10000001, 0b00,
|
||||
0b10000001, 0b00, 0b11111111, 0b00, 0b00000110, 0b00, 0b00000010, 0b00,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Helper to convert icon data to XBM format if needed
|
||||
*/
|
||||
export function toXbm(data: Uint8Array | number[]): Uint8Array {
|
||||
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
||||
}
|
||||
@@ -0,0 +1,627 @@
|
||||
/**
|
||||
* ScreenRenderers.ts - Sample screen rendering functions
|
||||
*
|
||||
* These demonstrate how to port Meshtastic firmware UI screens
|
||||
* to the OLED emulator. The API matches the firmware's drawing code.
|
||||
*/
|
||||
|
||||
import {
|
||||
OLEDDisplay,
|
||||
WHITE,
|
||||
BLACK,
|
||||
TEXT_ALIGN_LEFT,
|
||||
TEXT_ALIGN_CENTER,
|
||||
TEXT_ALIGN_RIGHT,
|
||||
} from "./OLEDDisplay";
|
||||
import { Font_6x8 } from "./SimpleFonts";
|
||||
import * as images from "./OLEDImages";
|
||||
|
||||
// Font height for simple 6x8 font
|
||||
const FONT_HEIGHT_SMALL = 8;
|
||||
|
||||
// Screen resolution detection (matches SharedUIDisplay.cpp)
|
||||
export type ScreenResolution = "UltraLow" | "Low" | "High";
|
||||
|
||||
export function determineScreenResolution(
|
||||
width: number,
|
||||
height: number,
|
||||
): ScreenResolution {
|
||||
if (width <= 64 || height <= 48) {
|
||||
return "UltraLow";
|
||||
}
|
||||
if (width > 128) {
|
||||
return "High";
|
||||
}
|
||||
return "Low";
|
||||
}
|
||||
|
||||
// Helper for consistent line positioning
|
||||
export function getTextPositions(display: OLEDDisplay): number[] {
|
||||
const fontHeight = FONT_HEIGHT_SMALL;
|
||||
return [
|
||||
0, // textZeroLine
|
||||
fontHeight - 1, // textFirstLine
|
||||
fontHeight - 1 + (fontHeight - 5), // textSecondLine
|
||||
fontHeight - 1 + 2 * (fontHeight - 5), // textThirdLine
|
||||
fontHeight - 1 + 3 * (fontHeight - 5), // textFourthLine
|
||||
fontHeight - 1 + 4 * (fontHeight - 5), // textFifthLine
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a rounded highlight box (used for inverted headers)
|
||||
*/
|
||||
export function drawRoundedHighlight(
|
||||
display: OLEDDisplay,
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
r: number,
|
||||
): void {
|
||||
// Draw the center and side rectangles
|
||||
display.fillRect(x + r, y, w - 2 * r, h); // center bar
|
||||
display.fillRect(x, y + r, r, h - 2 * r); // left edge
|
||||
display.fillRect(x + w - r, y + r, r, h - 2 * r); // right edge
|
||||
|
||||
// Draw the rounded corners using filled circles
|
||||
display.fillCircle(x + r + 1, y + r, r); // top-left
|
||||
display.fillCircle(x + w - r - 1, y + r, r); // top-right
|
||||
display.fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left
|
||||
display.fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the common header bar (battery, title, icons)
|
||||
*/
|
||||
export function drawCommonHeader(
|
||||
display: OLEDDisplay,
|
||||
x: number,
|
||||
y: number,
|
||||
titleStr: string,
|
||||
options: {
|
||||
inverted?: boolean;
|
||||
batteryPercent?: number;
|
||||
isCharging?: boolean;
|
||||
hasUSB?: boolean;
|
||||
hasUnreadMessage?: boolean;
|
||||
} = {},
|
||||
): void {
|
||||
const {
|
||||
inverted = true,
|
||||
batteryPercent = 75,
|
||||
isCharging = false,
|
||||
hasUSB = false,
|
||||
hasUnreadMessage = false,
|
||||
} = options;
|
||||
|
||||
const HEADER_OFFSET_Y = 1;
|
||||
y += HEADER_OFFSET_Y;
|
||||
|
||||
display.setFont(Font_6x8);
|
||||
const highlightHeight = FONT_HEIGHT_SMALL - 1;
|
||||
const screenW = display.getWidth();
|
||||
|
||||
if (inverted) {
|
||||
// Draw inverted header background
|
||||
display.setColor(WHITE);
|
||||
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
|
||||
display.setColor(BLACK);
|
||||
} else {
|
||||
// Draw line under header
|
||||
display.setColor(WHITE);
|
||||
display.drawLine(0, 14, screenW, 14);
|
||||
}
|
||||
|
||||
// Draw title centered
|
||||
display.setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display.drawString(screenW / 2, y, titleStr);
|
||||
|
||||
// Reset color for remaining elements
|
||||
if (inverted) {
|
||||
display.setColor(WHITE);
|
||||
}
|
||||
|
||||
// Draw battery on left
|
||||
let batteryX = 1;
|
||||
const batteryY = HEADER_OFFSET_Y + 1;
|
||||
|
||||
if (hasUSB && !isCharging) {
|
||||
// Draw USB icon
|
||||
display.drawXbm(batteryX + 1, batteryY + 2, 10, 8, images.imgUSB);
|
||||
batteryX += 11;
|
||||
} else {
|
||||
// Draw battery outline
|
||||
display.drawXbm(batteryX, batteryY, 7, 11, images.batteryBitmap_v);
|
||||
|
||||
if (isCharging) {
|
||||
// Draw lightning bolt
|
||||
display.drawXbm(
|
||||
batteryX + 1,
|
||||
batteryY + 3,
|
||||
5,
|
||||
5,
|
||||
images.lightning_bolt_v,
|
||||
);
|
||||
} else {
|
||||
// Draw battery level
|
||||
display.drawXbm(
|
||||
batteryX - 1,
|
||||
batteryY + 4,
|
||||
8,
|
||||
3,
|
||||
images.batteryBitmap_sidegaps_v,
|
||||
);
|
||||
const fillHeight = Math.floor((8 * batteryPercent) / 100);
|
||||
const fillY = batteryY - fillHeight + 10;
|
||||
display.fillRect(batteryX + 1, fillY, 5, fillHeight);
|
||||
}
|
||||
batteryX += 9;
|
||||
}
|
||||
|
||||
// Draw mail icon on right if unread message
|
||||
if (hasUnreadMessage) {
|
||||
const mailX = screenW - images.mail_width - 2;
|
||||
display.drawXbm(
|
||||
mailX,
|
||||
y + 2,
|
||||
images.mail_width,
|
||||
images.mail_height,
|
||||
images.mail,
|
||||
);
|
||||
}
|
||||
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display.setColor(WHITE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a boot/splash screen with logo
|
||||
*/
|
||||
export function drawBootScreen(
|
||||
display: OLEDDisplay,
|
||||
appVersion: string = "2.5.0",
|
||||
): void {
|
||||
display.clear();
|
||||
display.setFont(Font_6x8);
|
||||
display.setColor(WHITE);
|
||||
|
||||
const centerX = display.getWidth() / 2;
|
||||
const centerY = display.getHeight() / 2;
|
||||
|
||||
// Draw logo centered
|
||||
const logoX = centerX - images.logo_width / 2;
|
||||
const logoY = centerY - images.logo_height / 2 - 8;
|
||||
display.drawXbm(
|
||||
logoX,
|
||||
logoY,
|
||||
images.logo_width,
|
||||
images.logo_height,
|
||||
images.meshtastic_logo,
|
||||
);
|
||||
|
||||
// Draw version below
|
||||
display.setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display.drawString(
|
||||
centerX,
|
||||
centerY + images.logo_height / 2,
|
||||
`v${appVersion}`,
|
||||
);
|
||||
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a node info screen
|
||||
*/
|
||||
export function drawNodeInfoScreen(
|
||||
display: OLEDDisplay,
|
||||
nodeInfo: {
|
||||
shortName: string;
|
||||
longName: string;
|
||||
nodeId: string;
|
||||
batteryLevel?: number;
|
||||
lastHeard?: string;
|
||||
snr?: number;
|
||||
hopsAway?: number;
|
||||
},
|
||||
): void {
|
||||
display.clear();
|
||||
|
||||
// Draw header
|
||||
drawCommonHeader(display, 0, 0, "Node Info", {
|
||||
inverted: true,
|
||||
batteryPercent: 85,
|
||||
});
|
||||
|
||||
display.setFont(Font_6x8);
|
||||
display.setColor(WHITE);
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
const lines = getTextPositions(display);
|
||||
const screenW = display.getWidth();
|
||||
|
||||
// Node name (bolded by drawing twice offset)
|
||||
display.drawString(0, lines[1], nodeInfo.longName);
|
||||
display.drawString(1, lines[1], nodeInfo.longName);
|
||||
|
||||
// Short name and ID
|
||||
display.drawString(0, lines[2], `${nodeInfo.shortName} • ${nodeInfo.nodeId}`);
|
||||
|
||||
// Last heard
|
||||
if (nodeInfo.lastHeard) {
|
||||
display.drawString(0, lines[3], `Heard: ${nodeInfo.lastHeard}`);
|
||||
}
|
||||
|
||||
// Signal info on right side
|
||||
if (nodeInfo.snr !== undefined) {
|
||||
const snrStr = `SNR: ${nodeInfo.snr}dB`;
|
||||
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
||||
display.drawString(screenW, lines[3], snrStr);
|
||||
}
|
||||
|
||||
// Hops
|
||||
if (nodeInfo.hopsAway !== undefined) {
|
||||
const hopStr =
|
||||
nodeInfo.hopsAway === 0 ? "Direct" : `${nodeInfo.hopsAway} hops`;
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display.drawString(0, lines[4], hopStr);
|
||||
}
|
||||
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a message screen
|
||||
*/
|
||||
export function drawMessageScreen(
|
||||
display: OLEDDisplay,
|
||||
message: {
|
||||
from: string;
|
||||
text: string;
|
||||
time: string;
|
||||
channel?: string;
|
||||
},
|
||||
): void {
|
||||
display.clear();
|
||||
|
||||
// Draw header with channel name if provided
|
||||
const headerTitle = message.channel || "Message";
|
||||
drawCommonHeader(display, 0, 0, headerTitle, {
|
||||
inverted: true,
|
||||
batteryPercent: 72,
|
||||
hasUnreadMessage: true,
|
||||
});
|
||||
|
||||
display.setFont(Font_6x8);
|
||||
display.setColor(WHITE);
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
const lines = getTextPositions(display);
|
||||
const screenW = display.getWidth();
|
||||
|
||||
// From line
|
||||
display.drawString(0, lines[1], `From: ${message.from}`);
|
||||
|
||||
// Time on right
|
||||
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
||||
display.drawString(screenW, lines[1], message.time);
|
||||
|
||||
// Message text (with word wrap)
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display.drawStringMaxWidth(0, lines[2], screenW, message.text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a GPS/location screen
|
||||
*/
|
||||
export function drawGPSScreen(
|
||||
display: OLEDDisplay,
|
||||
gpsInfo: {
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
altitude?: number;
|
||||
satellites?: number;
|
||||
hasLock: boolean;
|
||||
speed?: number;
|
||||
},
|
||||
): void {
|
||||
display.clear();
|
||||
|
||||
drawCommonHeader(display, 0, 0, "GPS", {
|
||||
inverted: true,
|
||||
batteryPercent: 90,
|
||||
});
|
||||
|
||||
display.setFont(Font_6x8);
|
||||
display.setColor(WHITE);
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
const lines = getTextPositions(display);
|
||||
|
||||
// Draw satellite icon and count
|
||||
display.drawXbm(
|
||||
0,
|
||||
lines[1],
|
||||
images.imgSatellite_width,
|
||||
images.imgSatellite_height,
|
||||
images.imgSatellite,
|
||||
);
|
||||
|
||||
if (!gpsInfo.hasLock) {
|
||||
display.drawString(12, lines[1], "No Lock");
|
||||
} else {
|
||||
display.drawString(12, lines[1], `${gpsInfo.satellites || 0} sats`);
|
||||
}
|
||||
|
||||
if (
|
||||
gpsInfo.hasLock &&
|
||||
gpsInfo.latitude !== undefined &&
|
||||
gpsInfo.longitude !== undefined
|
||||
) {
|
||||
// Coordinates
|
||||
display.drawString(0, lines[2], `Lat: ${gpsInfo.latitude.toFixed(6)}`);
|
||||
display.drawString(0, lines[3], `Lon: ${gpsInfo.longitude.toFixed(6)}`);
|
||||
|
||||
// Altitude
|
||||
if (gpsInfo.altitude !== undefined) {
|
||||
display.drawString(0, lines[4], `Alt: ${gpsInfo.altitude.toFixed(0)}m`);
|
||||
}
|
||||
|
||||
// Speed
|
||||
if (gpsInfo.speed !== undefined) {
|
||||
const screenW = display.getWidth();
|
||||
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
||||
display.drawString(screenW, lines[4], `${gpsInfo.speed.toFixed(1)} km/h`);
|
||||
}
|
||||
}
|
||||
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a node list screen
|
||||
*/
|
||||
export function drawNodeListScreen(
|
||||
display: OLEDDisplay,
|
||||
nodes: Array<{
|
||||
shortName: string;
|
||||
longName: string;
|
||||
lastHeard: string;
|
||||
snr?: number;
|
||||
}>,
|
||||
selectedIndex: number = 0,
|
||||
): void {
|
||||
display.clear();
|
||||
|
||||
drawCommonHeader(display, 0, 0, `Nodes (${nodes.length})`, {
|
||||
inverted: true,
|
||||
batteryPercent: 80,
|
||||
});
|
||||
|
||||
display.setFont(Font_6x8);
|
||||
display.setColor(WHITE);
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
const lines = getTextPositions(display);
|
||||
const screenW = display.getWidth();
|
||||
const maxVisibleNodes = 4;
|
||||
const startIndex = Math.max(
|
||||
0,
|
||||
selectedIndex - Math.floor(maxVisibleNodes / 2),
|
||||
);
|
||||
|
||||
for (let i = 0; i < maxVisibleNodes && startIndex + i < nodes.length; i++) {
|
||||
const node = nodes[startIndex + i];
|
||||
const lineY = lines[i + 1];
|
||||
const isSelected = startIndex + i === selectedIndex;
|
||||
|
||||
// Highlight selected row
|
||||
if (isSelected) {
|
||||
display.fillRect(0, lineY - 1, screenW, FONT_HEIGHT_SMALL);
|
||||
display.setColor(BLACK);
|
||||
}
|
||||
|
||||
// Node short name
|
||||
display.drawString(0, lineY, node.shortName);
|
||||
|
||||
// Last heard on right
|
||||
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
||||
display.drawString(screenW, lineY, node.lastHeard);
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
if (isSelected) {
|
||||
display.setColor(WHITE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a system info screen
|
||||
*/
|
||||
export function drawSystemScreen(
|
||||
display: OLEDDisplay,
|
||||
sysInfo: {
|
||||
uptime: string;
|
||||
channelUtil: number;
|
||||
airUtil: number;
|
||||
batteryVoltage?: number;
|
||||
nodes: number;
|
||||
freeMemory?: number;
|
||||
},
|
||||
): void {
|
||||
display.clear();
|
||||
|
||||
drawCommonHeader(display, 0, 0, "System", {
|
||||
inverted: true,
|
||||
batteryPercent: 95,
|
||||
});
|
||||
|
||||
display.setFont(Font_6x8);
|
||||
display.setColor(WHITE);
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
const lines = getTextPositions(display);
|
||||
const screenW = display.getWidth();
|
||||
|
||||
// Uptime
|
||||
display.drawString(0, lines[1], `Uptime: ${sysInfo.uptime}`);
|
||||
|
||||
// Channel utilization
|
||||
display.drawString(0, lines[2], `ChUtil: ${sysInfo.channelUtil.toFixed(1)}%`);
|
||||
|
||||
// Air utilization
|
||||
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
||||
display.drawString(
|
||||
screenW,
|
||||
lines[2],
|
||||
`AirTx: ${sysInfo.airUtil.toFixed(1)}%`,
|
||||
);
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
// Nodes
|
||||
display.drawString(0, lines[3], `Nodes: ${sysInfo.nodes}`);
|
||||
|
||||
// Battery voltage
|
||||
if (sysInfo.batteryVoltage !== undefined) {
|
||||
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
||||
display.drawString(
|
||||
screenW,
|
||||
lines[3],
|
||||
`${sysInfo.batteryVoltage.toFixed(2)}V`,
|
||||
);
|
||||
}
|
||||
|
||||
// Free memory
|
||||
if (sysInfo.freeMemory !== undefined) {
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display.drawString(
|
||||
0,
|
||||
lines[4],
|
||||
`Free: ${(sysInfo.freeMemory / 1024).toFixed(0)}KB`,
|
||||
);
|
||||
}
|
||||
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a compass screen with bearing arrow
|
||||
*/
|
||||
export function drawCompassScreen(
|
||||
display: OLEDDisplay,
|
||||
compassInfo: {
|
||||
heading: number; // Device heading in degrees
|
||||
bearing: number; // Bearing to target in degrees
|
||||
distance?: number; // Distance to target in meters
|
||||
targetName?: string;
|
||||
},
|
||||
): void {
|
||||
display.clear();
|
||||
|
||||
drawCommonHeader(display, 0, 0, compassInfo.targetName || "Compass", {
|
||||
inverted: true,
|
||||
batteryPercent: 70,
|
||||
});
|
||||
|
||||
display.setFont(Font_6x8);
|
||||
display.setColor(WHITE);
|
||||
|
||||
const screenW = display.getWidth();
|
||||
const screenH = display.getHeight();
|
||||
|
||||
// Compass center and radius
|
||||
const compassRadius = Math.min(screenW, screenH - 20) / 2 - 4;
|
||||
const compassX = screenW / 2;
|
||||
const compassY = (screenH + 16) / 2;
|
||||
|
||||
// Draw compass circle
|
||||
display.drawCircle(compassX, compassY, compassRadius);
|
||||
|
||||
// Draw cardinal directions
|
||||
display.setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display.drawString(compassX, compassY - compassRadius - 10, "N");
|
||||
|
||||
// Calculate relative bearing (bearing - heading)
|
||||
const relativeBearing =
|
||||
((compassInfo.bearing - compassInfo.heading) * Math.PI) / 180;
|
||||
|
||||
// Draw bearing arrow
|
||||
const arrowLength = compassRadius - 6;
|
||||
const arrowX = compassX + Math.sin(relativeBearing) * arrowLength;
|
||||
const arrowY = compassY - Math.cos(relativeBearing) * arrowLength;
|
||||
|
||||
display.drawLine(compassX, compassY, arrowX, arrowY);
|
||||
|
||||
// Draw arrowhead
|
||||
const headAngle = 0.4;
|
||||
const headLength = 8;
|
||||
const angle1 = relativeBearing + Math.PI - headAngle;
|
||||
const angle2 = relativeBearing + Math.PI + headAngle;
|
||||
display.drawLine(
|
||||
arrowX,
|
||||
arrowY,
|
||||
arrowX + Math.sin(angle1) * headLength,
|
||||
arrowY - Math.cos(angle1) * headLength,
|
||||
);
|
||||
display.drawLine(
|
||||
arrowX,
|
||||
arrowY,
|
||||
arrowX + Math.sin(angle2) * headLength,
|
||||
arrowY - Math.cos(angle2) * headLength,
|
||||
);
|
||||
|
||||
// Draw distance at bottom
|
||||
if (compassInfo.distance !== undefined) {
|
||||
let distStr: string;
|
||||
if (compassInfo.distance >= 1000) {
|
||||
distStr = `${(compassInfo.distance / 1000).toFixed(1)} km`;
|
||||
} else {
|
||||
distStr = `${Math.round(compassInfo.distance)} m`;
|
||||
}
|
||||
display.drawString(compassX, screenH - FONT_HEIGHT_SMALL, distStr);
|
||||
}
|
||||
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a progress/loading screen
|
||||
*/
|
||||
export function drawProgressScreen(
|
||||
display: OLEDDisplay,
|
||||
title: string,
|
||||
progress: number,
|
||||
statusText?: string,
|
||||
): void {
|
||||
display.clear();
|
||||
display.setFont(Font_6x8);
|
||||
display.setColor(WHITE);
|
||||
display.setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
|
||||
const screenW = display.getWidth();
|
||||
const screenH = display.getHeight();
|
||||
const centerX = screenW / 2;
|
||||
const centerY = screenH / 2;
|
||||
|
||||
// Title
|
||||
display.drawString(centerX, centerY - 20, title);
|
||||
|
||||
// Progress bar
|
||||
const barWidth = screenW - 20;
|
||||
const barHeight = 10;
|
||||
const barX = 10;
|
||||
const barY = centerY - barHeight / 2;
|
||||
|
||||
display.drawProgressBar(barX, barY, barWidth, barHeight, progress);
|
||||
|
||||
// Progress percentage
|
||||
display.drawString(centerX, barY + barHeight + 4, `${Math.round(progress)}%`);
|
||||
|
||||
// Status text
|
||||
if (statusText) {
|
||||
display.drawString(centerX, screenH - FONT_HEIGHT_SMALL - 2, statusText);
|
||||
}
|
||||
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
/**
|
||||
* SimpleFonts.ts - Simple working bitmap fonts for OLED emulator
|
||||
*
|
||||
* These are basic monospace bitmap fonts that are easy to render correctly.
|
||||
* Format: Each character is stored in column-major order, 1 byte per column.
|
||||
* For fonts taller than 8px, multiple bytes per column are used.
|
||||
*/
|
||||
|
||||
import type { OLEDFont } from "./OLEDDisplay";
|
||||
|
||||
/**
|
||||
* Simple 6x8 monospace font
|
||||
* Each character is 6 columns wide, 8 rows tall
|
||||
* 1 byte per column (8 vertical pixels)
|
||||
* Includes ASCII 32-126
|
||||
*/
|
||||
export const Font_6x8: OLEDFont = {
|
||||
height: 8,
|
||||
firstChar: 32,
|
||||
charCount: 95,
|
||||
widths: new Array(95).fill(6), // Fixed width font
|
||||
data: new Uint8Array([
|
||||
// Space (32)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// ! (33)
|
||||
0x00, 0x00, 0x5f, 0x00, 0x00, 0x00,
|
||||
// " (34)
|
||||
0x00, 0x07, 0x00, 0x07, 0x00, 0x00,
|
||||
// # (35)
|
||||
0x14, 0x7f, 0x14, 0x7f, 0x14, 0x00,
|
||||
// $ (36)
|
||||
0x24, 0x2a, 0x7f, 0x2a, 0x12, 0x00,
|
||||
// % (37)
|
||||
0x23, 0x13, 0x08, 0x64, 0x62, 0x00,
|
||||
// & (38)
|
||||
0x36, 0x49, 0x55, 0x22, 0x50, 0x00,
|
||||
// ' (39)
|
||||
0x00, 0x05, 0x03, 0x00, 0x00, 0x00,
|
||||
// ( (40)
|
||||
0x00, 0x1c, 0x22, 0x41, 0x00, 0x00,
|
||||
// ) (41)
|
||||
0x00, 0x41, 0x22, 0x1c, 0x00, 0x00,
|
||||
// * (42)
|
||||
0x08, 0x2a, 0x1c, 0x2a, 0x08, 0x00,
|
||||
// + (43)
|
||||
0x08, 0x08, 0x3e, 0x08, 0x08, 0x00,
|
||||
// , (44)
|
||||
0x00, 0x50, 0x30, 0x00, 0x00, 0x00,
|
||||
// - (45)
|
||||
0x08, 0x08, 0x08, 0x08, 0x08, 0x00,
|
||||
// . (46)
|
||||
0x00, 0x60, 0x60, 0x00, 0x00, 0x00,
|
||||
// / (47)
|
||||
0x20, 0x10, 0x08, 0x04, 0x02, 0x00,
|
||||
// 0 (48)
|
||||
0x3e, 0x51, 0x49, 0x45, 0x3e, 0x00,
|
||||
// 1 (49)
|
||||
0x00, 0x42, 0x7f, 0x40, 0x00, 0x00,
|
||||
// 2 (50)
|
||||
0x42, 0x61, 0x51, 0x49, 0x46, 0x00,
|
||||
// 3 (51)
|
||||
0x21, 0x41, 0x45, 0x4b, 0x31, 0x00,
|
||||
// 4 (52)
|
||||
0x18, 0x14, 0x12, 0x7f, 0x10, 0x00,
|
||||
// 5 (53)
|
||||
0x27, 0x45, 0x45, 0x45, 0x39, 0x00,
|
||||
// 6 (54)
|
||||
0x3c, 0x4a, 0x49, 0x49, 0x30, 0x00,
|
||||
// 7 (55)
|
||||
0x01, 0x71, 0x09, 0x05, 0x03, 0x00,
|
||||
// 8 (56)
|
||||
0x36, 0x49, 0x49, 0x49, 0x36, 0x00,
|
||||
// 9 (57)
|
||||
0x06, 0x49, 0x49, 0x29, 0x1e, 0x00,
|
||||
// : (58)
|
||||
0x00, 0x36, 0x36, 0x00, 0x00, 0x00,
|
||||
// ; (59)
|
||||
0x00, 0x56, 0x36, 0x00, 0x00, 0x00,
|
||||
// < (60)
|
||||
0x00, 0x08, 0x14, 0x22, 0x41, 0x00,
|
||||
// = (61)
|
||||
0x14, 0x14, 0x14, 0x14, 0x14, 0x00,
|
||||
// > (62)
|
||||
0x41, 0x22, 0x14, 0x08, 0x00, 0x00,
|
||||
// ? (63)
|
||||
0x02, 0x01, 0x51, 0x09, 0x06, 0x00,
|
||||
// @ (64)
|
||||
0x32, 0x49, 0x79, 0x41, 0x3e, 0x00,
|
||||
// A (65)
|
||||
0x7e, 0x11, 0x11, 0x11, 0x7e, 0x00,
|
||||
// B (66)
|
||||
0x7f, 0x49, 0x49, 0x49, 0x36, 0x00,
|
||||
// C (67)
|
||||
0x3e, 0x41, 0x41, 0x41, 0x22, 0x00,
|
||||
// D (68)
|
||||
0x7f, 0x41, 0x41, 0x22, 0x1c, 0x00,
|
||||
// E (69)
|
||||
0x7f, 0x49, 0x49, 0x49, 0x41, 0x00,
|
||||
// F (70)
|
||||
0x7f, 0x09, 0x09, 0x01, 0x01, 0x00,
|
||||
// G (71)
|
||||
0x3e, 0x41, 0x41, 0x51, 0x32, 0x00,
|
||||
// H (72)
|
||||
0x7f, 0x08, 0x08, 0x08, 0x7f, 0x00,
|
||||
// I (73)
|
||||
0x00, 0x41, 0x7f, 0x41, 0x00, 0x00,
|
||||
// J (74)
|
||||
0x20, 0x40, 0x41, 0x3f, 0x01, 0x00,
|
||||
// K (75)
|
||||
0x7f, 0x08, 0x14, 0x22, 0x41, 0x00,
|
||||
// L (76)
|
||||
0x7f, 0x40, 0x40, 0x40, 0x40, 0x00,
|
||||
// M (77)
|
||||
0x7f, 0x02, 0x04, 0x02, 0x7f, 0x00,
|
||||
// N (78)
|
||||
0x7f, 0x04, 0x08, 0x10, 0x7f, 0x00,
|
||||
// O (79)
|
||||
0x3e, 0x41, 0x41, 0x41, 0x3e, 0x00,
|
||||
// P (80)
|
||||
0x7f, 0x09, 0x09, 0x09, 0x06, 0x00,
|
||||
// Q (81)
|
||||
0x3e, 0x41, 0x51, 0x21, 0x5e, 0x00,
|
||||
// R (82)
|
||||
0x7f, 0x09, 0x19, 0x29, 0x46, 0x00,
|
||||
// S (83)
|
||||
0x46, 0x49, 0x49, 0x49, 0x31, 0x00,
|
||||
// T (84)
|
||||
0x01, 0x01, 0x7f, 0x01, 0x01, 0x00,
|
||||
// U (85)
|
||||
0x3f, 0x40, 0x40, 0x40, 0x3f, 0x00,
|
||||
// V (86)
|
||||
0x1f, 0x20, 0x40, 0x20, 0x1f, 0x00,
|
||||
// W (87)
|
||||
0x7f, 0x20, 0x18, 0x20, 0x7f, 0x00,
|
||||
// X (88)
|
||||
0x63, 0x14, 0x08, 0x14, 0x63, 0x00,
|
||||
// Y (89)
|
||||
0x03, 0x04, 0x78, 0x04, 0x03, 0x00,
|
||||
// Z (90)
|
||||
0x61, 0x51, 0x49, 0x45, 0x43, 0x00,
|
||||
// [ (91)
|
||||
0x00, 0x00, 0x7f, 0x41, 0x41, 0x00,
|
||||
// \ (92)
|
||||
0x02, 0x04, 0x08, 0x10, 0x20, 0x00,
|
||||
// ] (93)
|
||||
0x41, 0x41, 0x7f, 0x00, 0x00, 0x00,
|
||||
// ^ (94)
|
||||
0x04, 0x02, 0x01, 0x02, 0x04, 0x00,
|
||||
// _ (95)
|
||||
0x40, 0x40, 0x40, 0x40, 0x40, 0x00,
|
||||
// ` (96)
|
||||
0x00, 0x01, 0x02, 0x04, 0x00, 0x00,
|
||||
// a (97)
|
||||
0x20, 0x54, 0x54, 0x54, 0x78, 0x00,
|
||||
// b (98)
|
||||
0x7f, 0x48, 0x44, 0x44, 0x38, 0x00,
|
||||
// c (99)
|
||||
0x38, 0x44, 0x44, 0x44, 0x20, 0x00,
|
||||
// d (100)
|
||||
0x38, 0x44, 0x44, 0x48, 0x7f, 0x00,
|
||||
// e (101)
|
||||
0x38, 0x54, 0x54, 0x54, 0x18, 0x00,
|
||||
// f (102)
|
||||
0x08, 0x7e, 0x09, 0x01, 0x02, 0x00,
|
||||
// g (103)
|
||||
0x08, 0x14, 0x54, 0x54, 0x3c, 0x00,
|
||||
// h (104)
|
||||
0x7f, 0x08, 0x04, 0x04, 0x78, 0x00,
|
||||
// i (105)
|
||||
0x00, 0x44, 0x7d, 0x40, 0x00, 0x00,
|
||||
// j (106)
|
||||
0x20, 0x40, 0x44, 0x3d, 0x00, 0x00,
|
||||
// k (107)
|
||||
0x00, 0x7f, 0x10, 0x28, 0x44, 0x00,
|
||||
// l (108)
|
||||
0x00, 0x41, 0x7f, 0x40, 0x00, 0x00,
|
||||
// m (109)
|
||||
0x7c, 0x04, 0x18, 0x04, 0x78, 0x00,
|
||||
// n (110)
|
||||
0x7c, 0x08, 0x04, 0x04, 0x78, 0x00,
|
||||
// o (111)
|
||||
0x38, 0x44, 0x44, 0x44, 0x38, 0x00,
|
||||
// p (112)
|
||||
0x7c, 0x14, 0x14, 0x14, 0x08, 0x00,
|
||||
// q (113)
|
||||
0x08, 0x14, 0x14, 0x18, 0x7c, 0x00,
|
||||
// r (114)
|
||||
0x7c, 0x08, 0x04, 0x04, 0x08, 0x00,
|
||||
// s (115)
|
||||
0x48, 0x54, 0x54, 0x54, 0x20, 0x00,
|
||||
// t (116)
|
||||
0x04, 0x3f, 0x44, 0x40, 0x20, 0x00,
|
||||
// u (117)
|
||||
0x3c, 0x40, 0x40, 0x20, 0x7c, 0x00,
|
||||
// v (118)
|
||||
0x1c, 0x20, 0x40, 0x20, 0x1c, 0x00,
|
||||
// w (119)
|
||||
0x3c, 0x40, 0x30, 0x40, 0x3c, 0x00,
|
||||
// x (120)
|
||||
0x44, 0x28, 0x10, 0x28, 0x44, 0x00,
|
||||
// y (121)
|
||||
0x0c, 0x50, 0x50, 0x50, 0x3c, 0x00,
|
||||
// z (122)
|
||||
0x44, 0x64, 0x54, 0x4c, 0x44, 0x00,
|
||||
// { (123)
|
||||
0x00, 0x08, 0x36, 0x41, 0x00, 0x00,
|
||||
// | (124)
|
||||
0x00, 0x00, 0x7f, 0x00, 0x00, 0x00,
|
||||
// } (125)
|
||||
0x00, 0x41, 0x36, 0x08, 0x00, 0x00,
|
||||
// ~ (126)
|
||||
0x08, 0x08, 0x2a, 0x1c, 0x08, 0x00,
|
||||
]),
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple 5x7 monospace font (more compact)
|
||||
* Each character is 5 columns wide, 7 rows tall
|
||||
* 1 byte per column
|
||||
*/
|
||||
export const Font_5x7: OLEDFont = {
|
||||
height: 7,
|
||||
firstChar: 32,
|
||||
charCount: 95,
|
||||
widths: new Array(95).fill(5),
|
||||
data: new Uint8Array([
|
||||
// Space (32)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// ! (33)
|
||||
0x00, 0x00, 0x2f, 0x00, 0x00,
|
||||
// " (34)
|
||||
0x00, 0x07, 0x00, 0x07, 0x00,
|
||||
// # (35)
|
||||
0x14, 0x3e, 0x14, 0x3e, 0x14,
|
||||
// $ (36)
|
||||
0x24, 0x2a, 0x7f, 0x2a, 0x12,
|
||||
// % (37)
|
||||
0x23, 0x13, 0x08, 0x64, 0x62,
|
||||
// & (38)
|
||||
0x36, 0x49, 0x55, 0x22, 0x50,
|
||||
// ' (39)
|
||||
0x00, 0x05, 0x03, 0x00, 0x00,
|
||||
// ( (40)
|
||||
0x00, 0x1c, 0x22, 0x41, 0x00,
|
||||
// ) (41)
|
||||
0x00, 0x41, 0x22, 0x1c, 0x00,
|
||||
// * (42)
|
||||
0x14, 0x08, 0x3e, 0x08, 0x14,
|
||||
// + (43)
|
||||
0x08, 0x08, 0x3e, 0x08, 0x08,
|
||||
// , (44)
|
||||
0x00, 0x50, 0x30, 0x00, 0x00,
|
||||
// - (45)
|
||||
0x08, 0x08, 0x08, 0x08, 0x08,
|
||||
// . (46)
|
||||
0x00, 0x30, 0x30, 0x00, 0x00,
|
||||
// / (47)
|
||||
0x20, 0x10, 0x08, 0x04, 0x02,
|
||||
// 0 (48)
|
||||
0x3e, 0x51, 0x49, 0x45, 0x3e,
|
||||
// 1 (49)
|
||||
0x00, 0x42, 0x7f, 0x40, 0x00,
|
||||
// 2 (50)
|
||||
0x42, 0x61, 0x51, 0x49, 0x46,
|
||||
// 3 (51)
|
||||
0x21, 0x41, 0x45, 0x4b, 0x31,
|
||||
// 4 (52)
|
||||
0x18, 0x14, 0x12, 0x7f, 0x10,
|
||||
// 5 (53)
|
||||
0x27, 0x45, 0x45, 0x45, 0x39,
|
||||
// 6 (54)
|
||||
0x3c, 0x4a, 0x49, 0x49, 0x30,
|
||||
// 7 (55)
|
||||
0x01, 0x71, 0x09, 0x05, 0x03,
|
||||
// 8 (56)
|
||||
0x36, 0x49, 0x49, 0x49, 0x36,
|
||||
// 9 (57)
|
||||
0x06, 0x49, 0x49, 0x29, 0x1e,
|
||||
// : (58)
|
||||
0x00, 0x36, 0x36, 0x00, 0x00,
|
||||
// ; (59)
|
||||
0x00, 0x56, 0x36, 0x00, 0x00,
|
||||
// < (60)
|
||||
0x08, 0x14, 0x22, 0x41, 0x00,
|
||||
// = (61)
|
||||
0x14, 0x14, 0x14, 0x14, 0x14,
|
||||
// > (62)
|
||||
0x00, 0x41, 0x22, 0x14, 0x08,
|
||||
// ? (63)
|
||||
0x02, 0x01, 0x51, 0x09, 0x06,
|
||||
// @ (64)
|
||||
0x32, 0x49, 0x79, 0x41, 0x3e,
|
||||
// A (65)
|
||||
0x7e, 0x11, 0x11, 0x11, 0x7e,
|
||||
// B (66)
|
||||
0x7f, 0x49, 0x49, 0x49, 0x36,
|
||||
// C (67)
|
||||
0x3e, 0x41, 0x41, 0x41, 0x22,
|
||||
// D (68)
|
||||
0x7f, 0x41, 0x41, 0x22, 0x1c,
|
||||
// E (69)
|
||||
0x7f, 0x49, 0x49, 0x49, 0x41,
|
||||
// F (70)
|
||||
0x7f, 0x09, 0x09, 0x09, 0x01,
|
||||
// G (71)
|
||||
0x3e, 0x41, 0x49, 0x49, 0x7a,
|
||||
// H (72)
|
||||
0x7f, 0x08, 0x08, 0x08, 0x7f,
|
||||
// I (73)
|
||||
0x00, 0x41, 0x7f, 0x41, 0x00,
|
||||
// J (74)
|
||||
0x20, 0x40, 0x41, 0x3f, 0x01,
|
||||
// K (75)
|
||||
0x7f, 0x08, 0x14, 0x22, 0x41,
|
||||
// L (76)
|
||||
0x7f, 0x40, 0x40, 0x40, 0x40,
|
||||
// M (77)
|
||||
0x7f, 0x02, 0x0c, 0x02, 0x7f,
|
||||
// N (78)
|
||||
0x7f, 0x04, 0x08, 0x10, 0x7f,
|
||||
// O (79)
|
||||
0x3e, 0x41, 0x41, 0x41, 0x3e,
|
||||
// P (80)
|
||||
0x7f, 0x09, 0x09, 0x09, 0x06,
|
||||
// Q (81)
|
||||
0x3e, 0x41, 0x51, 0x21, 0x5e,
|
||||
// R (82)
|
||||
0x7f, 0x09, 0x19, 0x29, 0x46,
|
||||
// S (83)
|
||||
0x46, 0x49, 0x49, 0x49, 0x31,
|
||||
// T (84)
|
||||
0x01, 0x01, 0x7f, 0x01, 0x01,
|
||||
// U (85)
|
||||
0x3f, 0x40, 0x40, 0x40, 0x3f,
|
||||
// V (86)
|
||||
0x1f, 0x20, 0x40, 0x20, 0x1f,
|
||||
// W (87)
|
||||
0x3f, 0x40, 0x38, 0x40, 0x3f,
|
||||
// X (88)
|
||||
0x63, 0x14, 0x08, 0x14, 0x63,
|
||||
// Y (89)
|
||||
0x07, 0x08, 0x70, 0x08, 0x07,
|
||||
// Z (90)
|
||||
0x61, 0x51, 0x49, 0x45, 0x43,
|
||||
// [ (91)
|
||||
0x00, 0x7f, 0x41, 0x41, 0x00,
|
||||
// \ (92)
|
||||
0x02, 0x04, 0x08, 0x10, 0x20,
|
||||
// ] (93)
|
||||
0x00, 0x41, 0x41, 0x7f, 0x00,
|
||||
// ^ (94)
|
||||
0x04, 0x02, 0x01, 0x02, 0x04,
|
||||
// _ (95)
|
||||
0x40, 0x40, 0x40, 0x40, 0x40,
|
||||
// ` (96)
|
||||
0x00, 0x01, 0x02, 0x04, 0x00,
|
||||
// a (97)
|
||||
0x20, 0x54, 0x54, 0x54, 0x78,
|
||||
// b (98)
|
||||
0x7f, 0x48, 0x44, 0x44, 0x38,
|
||||
// c (99)
|
||||
0x38, 0x44, 0x44, 0x44, 0x20,
|
||||
// d (100)
|
||||
0x38, 0x44, 0x44, 0x48, 0x7f,
|
||||
// e (101)
|
||||
0x38, 0x54, 0x54, 0x54, 0x18,
|
||||
// f (102)
|
||||
0x08, 0x7e, 0x09, 0x01, 0x02,
|
||||
// g (103)
|
||||
0x0c, 0x52, 0x52, 0x52, 0x3e,
|
||||
// h (104)
|
||||
0x7f, 0x08, 0x04, 0x04, 0x78,
|
||||
// i (105)
|
||||
0x00, 0x44, 0x7d, 0x40, 0x00,
|
||||
// j (106)
|
||||
0x20, 0x40, 0x44, 0x3d, 0x00,
|
||||
// k (107)
|
||||
0x7f, 0x10, 0x28, 0x44, 0x00,
|
||||
// l (108)
|
||||
0x00, 0x41, 0x7f, 0x40, 0x00,
|
||||
// m (109)
|
||||
0x7c, 0x04, 0x18, 0x04, 0x78,
|
||||
// n (110)
|
||||
0x7c, 0x08, 0x04, 0x04, 0x78,
|
||||
// o (111)
|
||||
0x38, 0x44, 0x44, 0x44, 0x38,
|
||||
// p (112)
|
||||
0x7c, 0x14, 0x14, 0x14, 0x08,
|
||||
// q (113)
|
||||
0x08, 0x14, 0x14, 0x18, 0x7c,
|
||||
// r (114)
|
||||
0x7c, 0x08, 0x04, 0x04, 0x08,
|
||||
// s (115)
|
||||
0x48, 0x54, 0x54, 0x54, 0x20,
|
||||
// t (116)
|
||||
0x04, 0x3f, 0x44, 0x40, 0x20,
|
||||
// u (117)
|
||||
0x3c, 0x40, 0x40, 0x20, 0x7c,
|
||||
// v (118)
|
||||
0x1c, 0x20, 0x40, 0x20, 0x1c,
|
||||
// w (119)
|
||||
0x3c, 0x40, 0x30, 0x40, 0x3c,
|
||||
// x (120)
|
||||
0x44, 0x28, 0x10, 0x28, 0x44,
|
||||
// y (121)
|
||||
0x0c, 0x50, 0x50, 0x50, 0x3c,
|
||||
// z (122)
|
||||
0x44, 0x64, 0x54, 0x4c, 0x44,
|
||||
// { (123)
|
||||
0x00, 0x08, 0x36, 0x41, 0x00,
|
||||
// | (124)
|
||||
0x00, 0x00, 0x7f, 0x00, 0x00,
|
||||
// } (125)
|
||||
0x00, 0x41, 0x36, 0x08, 0x00,
|
||||
// ~ (126)
|
||||
0x10, 0x08, 0x08, 0x10, 0x08,
|
||||
]),
|
||||
};
|
||||
|
||||
// Alias for compatibility
|
||||
export const SimpleFont = Font_6x8;
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Meshtastic OLED Emulator
|
||||
*
|
||||
* A Vue.js component and TypeScript library for emulating SSD1306/SH1106 OLED displays.
|
||||
* Provides the same OLEDDisplay API as the Meshtastic firmware, enabling
|
||||
* screen development and testing in the browser.
|
||||
*/
|
||||
|
||||
// Core display class
|
||||
export {
|
||||
OLEDDisplay,
|
||||
type OLEDDISPLAY_GEOMETRY,
|
||||
type OLEDDISPLAY_COLOR,
|
||||
type OLEDDISPLAY_TEXT_ALIGNMENT,
|
||||
type OLEDFont,
|
||||
} from "./OLEDDisplay";
|
||||
export {
|
||||
WHITE,
|
||||
BLACK,
|
||||
INVERSE,
|
||||
TEXT_ALIGN_LEFT,
|
||||
TEXT_ALIGN_CENTER,
|
||||
TEXT_ALIGN_RIGHT,
|
||||
} from "./OLEDDisplay";
|
||||
|
||||
// Fonts
|
||||
export {
|
||||
ArialMT_Plain_10,
|
||||
ArialMT_Plain_16,
|
||||
FONT_HEIGHT_SMALL,
|
||||
FONT_HEIGHT_MEDIUM,
|
||||
FONT_HEIGHT_LARGE,
|
||||
createFontFromBytes,
|
||||
} from "./OLEDFonts";
|
||||
|
||||
// Simple working fonts (recommended)
|
||||
export { Font_6x8, Font_5x7, SimpleFont } from "./SimpleFonts";
|
||||
|
||||
// Images and icons
|
||||
export * as OLEDImages from "./OLEDImages";
|
||||
|
||||
// Screen renderers (firmware UI ports)
|
||||
export * as ScreenRenderers from "./ScreenRenderers";
|
||||
|
||||
// Vue component
|
||||
export { default as OLEDEmulator } from "./OLEDEmulator.vue";
|
||||
export { DISPLAY_PRESETS, type DisplayPreset } from "./DisplayPresets";
|
||||
|
||||
// Demo component
|
||||
export { default as OLEDEmulatorDemo } from "./Demo.vue";
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createApp } from "vue";
|
||||
import Demo from "./Demo.vue";
|
||||
|
||||
createApp(Demo).mount("#app");
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* Vue */
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import dts from "vite-plugin-dts";
|
||||
import { resolve } from "path";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
dts({
|
||||
insertTypesEntry: true,
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, "src/index.ts"),
|
||||
name: "MeshtasticOLEDEmulator",
|
||||
formats: ["es", "cjs"],
|
||||
fileName: (format) => `index.${format === "es" ? "mjs" : "js"}`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ["vue"],
|
||||
output: {
|
||||
globals: {
|
||||
vue: "Vue",
|
||||
},
|
||||
},
|
||||
},
|
||||
cssCodeSplit: false,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
+38
-22
@@ -117,7 +117,7 @@ lib_deps =
|
||||
[radiolib_base]
|
||||
lib_deps =
|
||||
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
|
||||
jgromes/RadioLib@7.6.0
|
||||
jgromes/RadioLib@7.5.0
|
||||
|
||||
[device-ui_base]
|
||||
lib_deps =
|
||||
@@ -184,8 +184,42 @@ lib_deps =
|
||||
# renovate: datasource=custom.pio depName=BH1750_WE packageName=wollewald/library/BH1750_WE
|
||||
wollewald/BH1750_WE@1.1.10
|
||||
|
||||
; Common environmental sensor libraries (not included in native / portduino)
|
||||
[environmental_extra_common]
|
||||
; (not included in native / portduino)
|
||||
[environmental_extra]
|
||||
lib_deps =
|
||||
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
|
||||
adafruit/Adafruit BMP3XX Library@2.1.6
|
||||
# renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X
|
||||
adafruit/Adafruit MAX1704X@1.0.3
|
||||
# renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library
|
||||
adafruit/Adafruit SHTC3 Library@1.0.2
|
||||
# renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X
|
||||
adafruit/Adafruit LPS2X@2.0.6
|
||||
# renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library
|
||||
adafruit/Adafruit SHT31 Library@2.2.2
|
||||
# renovate: datasource=custom.pio depName=Adafruit VEML7700 packageName=adafruit/library/Adafruit VEML7700 Library
|
||||
adafruit/Adafruit VEML7700 Library@2.1.6
|
||||
# renovate: datasource=custom.pio depName=Adafruit SHT4x packageName=adafruit/library/Adafruit SHT4x Library
|
||||
adafruit/Adafruit SHT4x Library@1.0.5
|
||||
# renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library
|
||||
sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6
|
||||
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
|
||||
closedcube/ClosedCube OPT3001@1.1.2
|
||||
# renovate: datasource=custom.pio depName=Bosch BSEC2 packageName=boschsensortec/library/bsec2
|
||||
boschsensortec/bsec2@1.10.2610
|
||||
# renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library
|
||||
boschsensortec/BME68x Sensor Library@1.3.40408
|
||||
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
|
||||
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
|
||||
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
|
||||
sensirion/Sensirion Core@0.7.3
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
|
||||
sensirion/Sensirion I2C SCD4x@1.1.0
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
|
||||
sensirion/Sensirion I2C SFA3x@1.0.0
|
||||
|
||||
; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
|
||||
[environmental_extra_no_bsec]
|
||||
lib_deps =
|
||||
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
|
||||
adafruit/Adafruit BMP3XX Library@2.1.6
|
||||
@@ -211,23 +245,5 @@ lib_deps =
|
||||
sensirion/Sensirion Core@0.7.3
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
|
||||
sensirion/Sensirion I2C SCD4x@1.1.0
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
|
||||
sensirion/Sensirion I2C SFA3x@1.0.0
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SCD30 packageName=sensirion/library/Sensirion I2C SCD30
|
||||
sensirion/Sensirion I2C SCD30@1.0.0
|
||||
|
||||
; Environmental sensors with BSEC2 (Bosch proprietary IAQ)
|
||||
[environmental_extra]
|
||||
lib_deps =
|
||||
${environmental_extra_common.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=Bosch BSEC2 packageName=boschsensortec/library/bsec2
|
||||
boschsensortec/bsec2@1.10.2610
|
||||
# renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library
|
||||
boschsensortec/BME68x Sensor Library@1.3.40408
|
||||
|
||||
; Environmental sensors without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
|
||||
[environmental_extra_no_bsec]
|
||||
lib_deps =
|
||||
${environmental_extra_common.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=adafruit/Adafruit BME680 Library packageName=adafruit/library/Adafruit BME680
|
||||
adafruit/Adafruit BME680 Library@^2.0.5
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
#include "sleep.h"
|
||||
|
||||
#ifdef HAS_NCP5623
|
||||
#include <Wire.h>
|
||||
|
||||
#include <NCP5623.h>
|
||||
#endif
|
||||
|
||||
|
||||
+13
-10
@@ -4,7 +4,6 @@
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include "sleep.h"
|
||||
#include <memory>
|
||||
|
||||
#ifdef HAS_I2S
|
||||
#include <AudioFileSourcePROGMEM.h>
|
||||
@@ -30,9 +29,9 @@ class AudioThread : public concurrency::OSThread
|
||||
io.digitalWrite(EXPANDS_AMP_EN, HIGH);
|
||||
#endif
|
||||
setCPUFast(true);
|
||||
rtttlFile = std::unique_ptr<AudioFileSourcePROGMEM>(new AudioFileSourcePROGMEM(data, len));
|
||||
i2sRtttl = std::unique_ptr<AudioGeneratorRTTTL>(new AudioGeneratorRTTTL());
|
||||
i2sRtttl->begin(rtttlFile.get(), audioOut.get());
|
||||
rtttlFile = new AudioFileSourcePROGMEM(data, len);
|
||||
i2sRtttl = new AudioGeneratorRTTTL();
|
||||
i2sRtttl->begin(rtttlFile, audioOut);
|
||||
}
|
||||
|
||||
// Also handles actually playing the RTTTL, needs to be called in loop
|
||||
@@ -48,10 +47,12 @@ class AudioThread : public concurrency::OSThread
|
||||
{
|
||||
if (i2sRtttl != nullptr) {
|
||||
i2sRtttl->stop();
|
||||
delete i2sRtttl;
|
||||
i2sRtttl = nullptr;
|
||||
}
|
||||
|
||||
if (rtttlFile != nullptr) {
|
||||
delete rtttlFile;
|
||||
rtttlFile = nullptr;
|
||||
}
|
||||
|
||||
@@ -65,14 +66,16 @@ class AudioThread : public concurrency::OSThread
|
||||
{
|
||||
if (i2sRtttl != nullptr) {
|
||||
i2sRtttl->stop();
|
||||
delete i2sRtttl;
|
||||
i2sRtttl = nullptr;
|
||||
}
|
||||
|
||||
#ifdef T_LORA_PAGER
|
||||
io.digitalWrite(EXPANDS_AMP_EN, HIGH);
|
||||
#endif
|
||||
auto sam = std::unique_ptr<ESP8266SAM>(new ESP8266SAM);
|
||||
sam->Say(audioOut.get(), text);
|
||||
ESP8266SAM *sam = new ESP8266SAM;
|
||||
sam->Say(audioOut, text);
|
||||
delete sam;
|
||||
setCPUFast(false);
|
||||
#ifdef T_LORA_PAGER
|
||||
io.digitalWrite(EXPANDS_AMP_EN, LOW);
|
||||
@@ -93,15 +96,15 @@ class AudioThread : public concurrency::OSThread
|
||||
private:
|
||||
void initOutput()
|
||||
{
|
||||
audioOut = std::unique_ptr<AudioOutputI2S>(new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S));
|
||||
audioOut = new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S);
|
||||
audioOut->SetPinout(DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_MCLK);
|
||||
audioOut->SetGain(0.2);
|
||||
};
|
||||
|
||||
std::unique_ptr<AudioGeneratorRTTTL> i2sRtttl = nullptr;
|
||||
std::unique_ptr<AudioOutputI2S> audioOut = nullptr;
|
||||
AudioGeneratorRTTTL *i2sRtttl = nullptr;
|
||||
AudioOutputI2S *audioOut = nullptr;
|
||||
|
||||
std::unique_ptr<AudioFileSourcePROGMEM> rtttlFile = nullptr;
|
||||
AudioFileSourcePROGMEM *rtttlFile = nullptr;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
+4
-6
@@ -704,11 +704,11 @@ bool Power::setup()
|
||||
found = true;
|
||||
} else if (analogInit()) {
|
||||
found = true;
|
||||
} else {
|
||||
#ifdef NRF_APM
|
||||
found = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef NRF_APM
|
||||
found = true;
|
||||
#endif
|
||||
#ifdef EXT_PWR_DETECT
|
||||
attachInterrupt(
|
||||
EXT_PWR_DETECT,
|
||||
@@ -846,10 +846,8 @@ void Power::readPowerStatus()
|
||||
|
||||
if (batteryLevel) {
|
||||
hasBattery = batteryLevel->isBatteryConnect() ? OptTrue : OptFalse;
|
||||
#ifndef NRF_APM
|
||||
usbPowered = batteryLevel->isVbusIn() ? OptTrue : OptFalse;
|
||||
isChargingNow = batteryLevel->isCharging() ? OptTrue : OptFalse;
|
||||
#endif
|
||||
if (hasBattery) {
|
||||
batteryVoltageMv = batteryLevel->getBattVoltage();
|
||||
// If the AXP192 returns a valid battery percentage, use it
|
||||
|
||||
+29
-18
@@ -227,21 +227,34 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_
|
||||
isBleConnected = nrf52Bluetooth != nullptr && nrf52Bluetooth->isConnected();
|
||||
#endif
|
||||
if (isBleConnected) {
|
||||
char *message;
|
||||
size_t initialLen;
|
||||
size_t len;
|
||||
initialLen = strlen(format);
|
||||
message = new char[initialLen + 1];
|
||||
len = vsnprintf(message, initialLen + 1, format, arg);
|
||||
if (len > initialLen) {
|
||||
delete[] message;
|
||||
message = new char[len + 1];
|
||||
vsnprintf(message, len + 1, format, arg);
|
||||
}
|
||||
auto thread = concurrency::OSThread::currentThread;
|
||||
meshtastic_LogRecord logRecord = meshtastic_LogRecord_init_zero;
|
||||
logRecord.level = getLogLevel(logLevel);
|
||||
vsprintf(logRecord.message, format, arg);
|
||||
strcpy(logRecord.message, message);
|
||||
if (thread)
|
||||
strcpy(logRecord.source, thread->ThreadName.c_str());
|
||||
logRecord.time = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||
|
||||
auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[meshtastic_LogRecord_size]);
|
||||
size_t size = pb_encode_to_bytes(buffer.get(), meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord);
|
||||
uint8_t *buffer = new uint8_t[meshtastic_LogRecord_size];
|
||||
size_t size = pb_encode_to_bytes(buffer, meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord);
|
||||
#ifdef ARCH_ESP32
|
||||
nimbleBluetooth->sendLog(buffer.get(), size);
|
||||
nimbleBluetooth->sendLog(buffer, size);
|
||||
#elif defined(ARCH_NRF52)
|
||||
nrf52Bluetooth->sendLog(buffer.get(), size);
|
||||
nrf52Bluetooth->sendLog(buffer, size);
|
||||
#endif
|
||||
delete[] message;
|
||||
delete[] buffer;
|
||||
}
|
||||
}
|
||||
#else
|
||||
@@ -279,8 +292,8 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
|
||||
|
||||
// append \n to format
|
||||
size_t len = strlen(format);
|
||||
auto newFormat = std::unique_ptr<char[]>(new char[len + 2]);
|
||||
strcpy(newFormat.get(), format);
|
||||
char *newFormat = new char[len + 2];
|
||||
strcpy(newFormat, format);
|
||||
newFormat[len] = '\n';
|
||||
newFormat[len + 1] = '\0';
|
||||
|
||||
@@ -297,18 +310,23 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
|
||||
va_end(arg);
|
||||
}
|
||||
if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) {
|
||||
delete[] newFormat;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (portduino_config.logoutputlevel < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) {
|
||||
delete[] newFormat;
|
||||
return;
|
||||
} else if (portduino_config.logoutputlevel < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) {
|
||||
delete[] newFormat;
|
||||
return;
|
||||
} else if (portduino_config.logoutputlevel < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) {
|
||||
delete[] newFormat;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (moduleConfig.serial.override_console_serial_port && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) {
|
||||
delete[] newFormat;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -320,19 +338,11 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
|
||||
#endif
|
||||
|
||||
va_list arg;
|
||||
va_list arg_copy;
|
||||
|
||||
va_start(arg, format);
|
||||
|
||||
va_copy(arg_copy, arg);
|
||||
log_to_serial(logLevel, newFormat.get(), arg_copy);
|
||||
va_end(arg_copy);
|
||||
|
||||
va_copy(arg_copy, arg);
|
||||
log_to_syslog(logLevel, newFormat.get(), arg_copy);
|
||||
va_end(arg_copy);
|
||||
|
||||
log_to_ble(logLevel, newFormat.get(), arg);
|
||||
log_to_serial(logLevel, newFormat, arg);
|
||||
log_to_syslog(logLevel, newFormat, arg);
|
||||
log_to_ble(logLevel, newFormat, arg);
|
||||
|
||||
va_end(arg);
|
||||
#ifdef HAS_FREE_RTOS
|
||||
@@ -342,6 +352,7 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
|
||||
#endif
|
||||
}
|
||||
|
||||
delete[] newFormat;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,10 +76,8 @@ bool NotifiedWorkerThread::notifyLater(uint32_t delay, uint32_t v, bool overwrit
|
||||
|
||||
void NotifiedWorkerThread::checkNotification()
|
||||
{
|
||||
// Atomically read and clear. (This avoids a potential race condition where an interrupt handler could set a new notification
|
||||
// after checkNotification reads but before it clears, which would cause us to miss that notification until the next one comes
|
||||
// in.)
|
||||
auto n = notification.exchange(0); // read+clear atomically: like `n = notification; notification = 0;` but interrupt-safe
|
||||
auto n = notification;
|
||||
notification = 0; // clear notification
|
||||
if (n) {
|
||||
onNotify(n);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "OSThread.h"
|
||||
#include <atomic>
|
||||
|
||||
namespace concurrency
|
||||
{
|
||||
@@ -14,7 +13,7 @@ class NotifiedWorkerThread : public OSThread
|
||||
/**
|
||||
* The notification that was most recently used to wake the thread. Read from runOnce()
|
||||
*/
|
||||
std::atomic<uint32_t> notification{0};
|
||||
uint32_t notification = 0;
|
||||
|
||||
public:
|
||||
NotifiedWorkerThread(const char *name) : OSThread(name) {}
|
||||
|
||||
@@ -244,7 +244,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define BQ25896_ADDR 0x6B
|
||||
#define LTR553ALS_ADDR 0x23
|
||||
#define SEN5X_ADDR 0x69
|
||||
#define SCD30_ADDR 0x61
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// ACCELEROMETER
|
||||
|
||||
@@ -91,8 +91,7 @@ class ScanI2C
|
||||
CST226SE,
|
||||
SEN5X,
|
||||
SFA30,
|
||||
CW2015,
|
||||
SCD30
|
||||
CW2015
|
||||
} DeviceType;
|
||||
|
||||
// typedef uint8_t DeviceAddress;
|
||||
|
||||
@@ -117,25 +117,6 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
|
||||
return value;
|
||||
}
|
||||
|
||||
bool ScanI2CTwoWire::i2cCommandResponseLength(ScanI2C::DeviceAddress addr, uint16_t command, uint8_t expectedLength) const
|
||||
{
|
||||
TwoWire *i2cBus = fetchI2CBus(addr);
|
||||
i2cBus->beginTransmission(addr.address);
|
||||
if (command > 0xFF) {
|
||||
i2cBus->write((uint8_t)(command >> 8));
|
||||
}
|
||||
i2cBus->write((uint8_t)(command & 0xFF));
|
||||
if (i2cBus->endTransmission() != 0) {
|
||||
return false;
|
||||
}
|
||||
delay(20);
|
||||
uint8_t received = i2cBus->requestFrom(addr.address, expectedLength);
|
||||
bool match = (received == expectedLength);
|
||||
while (i2cBus->available())
|
||||
i2cBus->read();
|
||||
return match;
|
||||
}
|
||||
|
||||
/// for SEN5X detection
|
||||
// Note, this code needs to be called before setting the I2C bus speed
|
||||
// for the screen at high speed. The speed needs to be at 100kHz, otherwise
|
||||
@@ -451,7 +432,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {
|
||||
type = OPT3001;
|
||||
logFoundDevice("OPT3001", (uint8_t)addr.address);
|
||||
} else if (i2cCommandResponseLength(addr, 0x89, 6)) { // SHT4x serial number (6 bytes inc. CRC)
|
||||
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 6) !=
|
||||
0) { // unique SHT4x serial number (6 bytes inc. CRC)
|
||||
type = SHT4X;
|
||||
logFoundDevice("SHT4X", (uint8_t)addr.address);
|
||||
} else {
|
||||
@@ -476,19 +458,13 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
break;
|
||||
|
||||
case LPS22HB_ADDR_ALT:
|
||||
// SFA30 detection: send 2-byte command 0xD060 (Get Device Marking) and check for 48-byte response
|
||||
if (i2cCommandResponseLength(addr, 0xD060, 48)) {
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD060), 48); // get device marking
|
||||
if (registerValue != 0) {
|
||||
type = SFA30;
|
||||
logFoundDevice("SFA30", (uint8_t)addr.address);
|
||||
break;
|
||||
}
|
||||
// Fallback: LPS22HB detection at alternate address using WHO_AM_I register (0x0F == 0xB1)
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1);
|
||||
if (registerValue == 0xB1) {
|
||||
type = LPS22HB;
|
||||
logFoundDevice("LPS22HB", (uint8_t)addr.address);
|
||||
}
|
||||
break;
|
||||
// TODO - What happens with these two?
|
||||
SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address)
|
||||
SCAN_SIMPLE_CASE(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address)
|
||||
|
||||
@@ -581,7 +557,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(SCD30_ADDR, SCD30, "SCD30", (uint8_t)addr.address);
|
||||
case CST328_ADDR:
|
||||
// Do we have the CST328 or the CST226SE
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xAB), 1);
|
||||
|
||||
@@ -55,8 +55,6 @@ class ScanI2CTwoWire : public ScanI2C
|
||||
|
||||
uint16_t getRegisterValue(const RegisterLocation &, ResponseWidth, bool) const;
|
||||
|
||||
bool i2cCommandResponseLength(DeviceAddress addr, uint16_t command, uint8_t expectedLength) const;
|
||||
|
||||
DeviceType probeOLED(ScanI2C::DeviceAddress) const;
|
||||
|
||||
static void logFoundDevice(const char *device, uint8_t address);
|
||||
|
||||
+12
-10
@@ -52,7 +52,7 @@ SerialUART *GPS::_serial_gps = &GPS_SERIAL_PORT;
|
||||
HardwareSerial *GPS::_serial_gps = nullptr;
|
||||
#endif
|
||||
|
||||
std::unique_ptr<GPS> gps = nullptr;
|
||||
GPS *gps = nullptr;
|
||||
|
||||
static GPSUpdateScheduling scheduling;
|
||||
|
||||
@@ -127,7 +127,7 @@ static int32_t gpsSwitch()
|
||||
return 1000;
|
||||
}
|
||||
|
||||
static std::unique_ptr<concurrency::Periodic> gpsPeriodic;
|
||||
static concurrency::Periodic *gpsPeriodic;
|
||||
#endif
|
||||
|
||||
static void UBXChecksum(uint8_t *message, size_t length)
|
||||
@@ -1485,7 +1485,7 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipI
|
||||
if (bufferSize > 2048)
|
||||
bufferSize = 2048;
|
||||
|
||||
auto response = std::unique_ptr<char[]>(new char[bufferSize]); // Dynamically allocate based on baud rate
|
||||
char *response = new char[bufferSize](); // Dynamically allocate based on baud rate
|
||||
uint16_t responseLen = 0;
|
||||
unsigned long start = millis();
|
||||
while (millis() - start < timeout) {
|
||||
@@ -1501,18 +1501,19 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipI
|
||||
if (c == ',' || (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n')) {
|
||||
// check if we can see our chips
|
||||
for (const auto &chipInfo : responseMap) {
|
||||
if (strstr(response.get(), chipInfo.detectionString.c_str()) != nullptr) {
|
||||
if (strstr(response, chipInfo.detectionString.c_str()) != nullptr) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(response.get());
|
||||
LOG_DEBUG(response);
|
||||
#endif
|
||||
LOG_INFO("%s detected", chipInfo.chipName.c_str());
|
||||
delete[] response; // Cleanup before return
|
||||
return chipInfo.driver;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(response.get());
|
||||
LOG_DEBUG(response);
|
||||
#endif
|
||||
// Reset the response buffer for the next potential message
|
||||
responseLen = 0;
|
||||
@@ -1521,12 +1522,13 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipI
|
||||
}
|
||||
}
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(response.get());
|
||||
LOG_DEBUG(response);
|
||||
#endif
|
||||
delete[] response; // Cleanup before return
|
||||
return GNSS_MODEL_UNKNOWN; // Return unknown on timeout
|
||||
}
|
||||
|
||||
std::unique_ptr<GPS> GPS::createGps()
|
||||
GPS *GPS::createGps()
|
||||
{
|
||||
int8_t _rx_gpio = config.position.rx_gpio;
|
||||
int8_t _tx_gpio = config.position.tx_gpio;
|
||||
@@ -1551,7 +1553,7 @@ std::unique_ptr<GPS> GPS::createGps()
|
||||
if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all
|
||||
return nullptr;
|
||||
|
||||
auto new_gps = std::unique_ptr<GPS>(new GPS());
|
||||
GPS *new_gps = new GPS;
|
||||
new_gps->rx_gpio = _rx_gpio;
|
||||
new_gps->tx_gpio = _tx_gpio;
|
||||
|
||||
@@ -1579,7 +1581,7 @@ std::unique_ptr<GPS> GPS::createGps()
|
||||
#ifdef PIN_GPS_SWITCH
|
||||
// toggle GPS via external GPIO switch
|
||||
pinMode(PIN_GPS_SWITCH, INPUT);
|
||||
gpsPeriodic = std::unique_ptr<concurrency::Periodic>(new concurrency::Periodic("GPSSwitch", gpsSwitch));
|
||||
gpsPeriodic = new concurrency::Periodic("GPSSwitch", gpsSwitch);
|
||||
#endif
|
||||
|
||||
// Currently disabled per issue #525 (TinyGPS++ crash bug)
|
||||
|
||||
+2
-4
@@ -2,8 +2,6 @@
|
||||
#include "configuration.h"
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "GPSStatus.h"
|
||||
#include "GpioLogic.h"
|
||||
#include "Observer.h"
|
||||
@@ -120,7 +118,7 @@ class GPS : private concurrency::OSThread
|
||||
|
||||
// Creates an instance of the GPS class.
|
||||
// Returns the new instance or null if the GPS is not present.
|
||||
static std::unique_ptr<GPS> createGps();
|
||||
static GPS *createGps();
|
||||
|
||||
// Wake the GPS hardware - ready for an update
|
||||
void up();
|
||||
@@ -258,5 +256,5 @@ class GPS : private concurrency::OSThread
|
||||
uint8_t fixeddelayCtr = 0;
|
||||
};
|
||||
|
||||
extern std::unique_ptr<GPS> gps;
|
||||
extern GPS *gps;
|
||||
#endif // Exclude GPS
|
||||
|
||||
+1
-1
@@ -312,7 +312,7 @@ const char *RtcName(RTCQuality quality)
|
||||
* @param t The time to potentially set the RTC to.
|
||||
* @return True if the RTC was set to the provided time, false otherwise.
|
||||
*/
|
||||
RTCSetResult perhapsSetRTC(RTCQuality q, const struct tm &t)
|
||||
RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t)
|
||||
{
|
||||
/* Convert to unix time
|
||||
The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, 1970
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ extern uint32_t lastSetFromPhoneNtpOrGps;
|
||||
|
||||
/// If we haven't yet set our RTC this boot, set it from a GPS derived time
|
||||
RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate = false);
|
||||
RTCSetResult perhapsSetRTC(RTCQuality q, const struct tm &t);
|
||||
RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t);
|
||||
|
||||
/// Return a string name for the quality
|
||||
const char *RtcName(RTCQuality quality);
|
||||
|
||||
@@ -10,7 +10,7 @@ EInkDynamicDisplay::EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDI
|
||||
{
|
||||
// If tracking ghost pixels, grab memory
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
dirtyPixels = std::unique_ptr<uint8_t[]>(new uint8_t[EInkDisplay::displayBufferSize]()); // Init with zeros
|
||||
dirtyPixels = new uint8_t[EInkDisplay::displayBufferSize](); // Init with zeros
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ EInkDynamicDisplay::~EInkDynamicDisplay()
|
||||
{
|
||||
// If we were tracking ghost pixels, free the memory
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
dirtyPixels = nullptr;
|
||||
delete[] dirtyPixels;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -454,7 +454,7 @@ void EInkDynamicDisplay::checkExcessiveGhosting()
|
||||
void EInkDynamicDisplay::resetGhostPixelTracking()
|
||||
{
|
||||
// Copy the current frame into dirtyPixels[] from the display buffer
|
||||
memcpy(dirtyPixels.get(), EInkDisplay::buffer, EInkDisplay::displayBufferSize);
|
||||
memcpy(dirtyPixels, EInkDisplay::buffer, EInkDisplay::displayBufferSize);
|
||||
}
|
||||
#endif // EINK_LIMIT_GHOSTING_PX
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
#include <memory>
|
||||
|
||||
#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY)
|
||||
|
||||
@@ -117,11 +116,11 @@ class EInkDynamicDisplay : public EInkDisplay, protected concurrency::NotifiedWo
|
||||
// Optional - track ghosting, pixel by pixel
|
||||
// May 2024: no longer used by any display. Kept for possible future use.
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
void countGhostPixels(); // Count any pixels which have moved from black to white since last full-refresh
|
||||
void checkExcessiveGhosting(); // Check if ghosting exceeds defined limit
|
||||
void resetGhostPixelTracking(); // Clear the dirty pixels array. Call when full-refresh cleans the display.
|
||||
std::unique_ptr<uint8_t[]> dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem)
|
||||
uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use
|
||||
void countGhostPixels(); // Count any pixels which have moved from black to white since last full-refresh
|
||||
void checkExcessiveGhosting(); // Check if ghosting exceeds defined limit
|
||||
void resetGhostPixelTracking(); // Clear the dirty pixels array. Call when full-refresh cleans the display.
|
||||
uint8_t *dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem)
|
||||
uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use
|
||||
#endif
|
||||
|
||||
// Conditional - async full refresh - only with modified meshtastic/GxEPD2
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#ifdef HAS_LP5562
|
||||
#include <Wire.h>
|
||||
|
||||
#include <LP5562.h>
|
||||
extern LP5562 rgbw;
|
||||
|
||||
|
||||
@@ -539,7 +539,7 @@ void menuHandler::messageResponseMenu()
|
||||
// If viewing ALL chats, hide “Mute Chat”
|
||||
if (mode != graphics::MessageRenderer::ThreadMode::ALL && mode != graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
const uint8_t chIndex = (threadChannel != 0) ? (uint8_t)threadChannel : channels.getPrimaryIndex();
|
||||
const auto &chan = channels.getByIndex(chIndex);
|
||||
auto &chan = channels.getByIndex(chIndex);
|
||||
|
||||
optionsArray[options] = chan.settings.module_settings.is_muted ? "Unmute Channel" : "Mute Channel";
|
||||
optionsEnumArray[options++] = MuteChannel;
|
||||
@@ -831,7 +831,7 @@ void menuHandler::messageViewModeMenu()
|
||||
// Gather unique peers
|
||||
auto dms = messageStore.getDirectMessages();
|
||||
std::vector<uint32_t> uniquePeers;
|
||||
for (const auto &m : dms) {
|
||||
for (auto &m : dms) {
|
||||
uint32_t peer = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender;
|
||||
if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end())
|
||||
uniquePeers.push_back(peer);
|
||||
@@ -1397,7 +1397,7 @@ void menuHandler::manageNodeMenu()
|
||||
}
|
||||
|
||||
if (selected == Favorite) {
|
||||
const auto *n = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
|
||||
auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
|
||||
if (!n) {
|
||||
return;
|
||||
}
|
||||
@@ -2292,13 +2292,14 @@ void menuHandler::wifiToggleMenu()
|
||||
void menuHandler::screenOptionsMenu()
|
||||
{
|
||||
// Check if brightness is supported
|
||||
bool hasSupportBrightness = false;
|
||||
#if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
|
||||
hasSupportBrightness = true;
|
||||
#endif
|
||||
|
||||
#if defined(T_DECK)
|
||||
// TDeck Doesn't seem to support brightness at all, at least not reliably
|
||||
bool hasSupportBrightness = false;
|
||||
#elif defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
|
||||
bool hasSupportBrightness = true;
|
||||
#else
|
||||
bool hasSupportBrightness = false;
|
||||
hasSupportBrightness = false;
|
||||
#endif
|
||||
|
||||
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles };
|
||||
@@ -2443,7 +2444,7 @@ void menuHandler::frameTogglesMenu()
|
||||
nodelist_hopsignal,
|
||||
nodelist_distance,
|
||||
nodelist_bearings,
|
||||
gps_position,
|
||||
gps,
|
||||
lora,
|
||||
clock,
|
||||
show_favorites,
|
||||
@@ -2481,7 +2482,7 @@ void menuHandler::frameTogglesMenu()
|
||||
#endif
|
||||
|
||||
optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position";
|
||||
optionsEnumArray[options++] = gps_position;
|
||||
optionsEnumArray[options++] = gps;
|
||||
#endif
|
||||
|
||||
optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa";
|
||||
@@ -2544,7 +2545,7 @@ void menuHandler::frameTogglesMenu()
|
||||
screen->toggleFrameVisibility("nodelist_bearings");
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
} else if (selected == gps_position) {
|
||||
} else if (selected == gps) {
|
||||
screen->toggleFrameVisibility("gps");
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
|
||||
@@ -171,7 +171,7 @@ unsigned long getModeCycleIntervalMs()
|
||||
|
||||
int calculateMaxScroll(int totalEntries, int visibleRows)
|
||||
{
|
||||
return max(0, (totalEntries - 1) / (visibleRows * 2));
|
||||
return std::max(0, (totalEntries - 1) / (visibleRows * 2));
|
||||
}
|
||||
|
||||
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
|
||||
@@ -187,12 +187,13 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
|
||||
if (totalEntries <= visibleNodeRows * columns)
|
||||
return;
|
||||
|
||||
int scrollbarHeight = display->getHeight() - scrollStartY - 10;
|
||||
int thumbHeight = max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries);
|
||||
int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) /
|
||||
max(1, max(0, (totalEntries - 1) / (visibleNodeRows * columns)));
|
||||
|
||||
int scrollbarX = display->getWidth() - 2;
|
||||
int scrollbarHeight = display->getHeight() - scrollStartY - 10;
|
||||
int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries);
|
||||
int perPage = visibleNodeRows * columns;
|
||||
int maxScroll = std::max(0, (totalEntries - 1) / perPage);
|
||||
int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll);
|
||||
|
||||
for (int i = 0; i < thumbHeight; i++) {
|
||||
display->setPixel(scrollbarX, thumbY + i);
|
||||
}
|
||||
@@ -555,13 +556,13 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
|
||||
int maxScroll = 0;
|
||||
if (perPage > 0) {
|
||||
maxScroll = max(0, (totalEntries - 1) / perPage);
|
||||
maxScroll = std::max(0, (totalEntries - 1) / perPage);
|
||||
}
|
||||
|
||||
if (scrollIndex > maxScroll)
|
||||
scrollIndex = maxScroll;
|
||||
int startIndex = scrollIndex * visibleNodeRows * totalColumns;
|
||||
int endIndex = min(startIndex + visibleNodeRows * totalColumns, totalEntries);
|
||||
int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries);
|
||||
int yOffset = 0;
|
||||
int col = 0;
|
||||
int lastNodeY = y;
|
||||
@@ -579,7 +580,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
if (extras)
|
||||
extras(display, node, xPos, yPos, columnWidth, heading, lat, lon);
|
||||
|
||||
lastNodeY = max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
|
||||
lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
|
||||
yOffset += rowYOffset;
|
||||
shownCount++;
|
||||
rowCount++;
|
||||
@@ -612,11 +613,13 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
if (millis() - popupTime < POPUP_DURATION_MS) {
|
||||
popupTotal = totalEntries;
|
||||
|
||||
int perPage = visibleNodeRows * totalColumns;
|
||||
|
||||
popupStart = startIndex + 1;
|
||||
popupEnd = min(startIndex + perPage, totalEntries);
|
||||
popupEnd = std::min(startIndex + perPage, totalEntries);
|
||||
|
||||
popupPage = (scrollIndex + 1);
|
||||
popupMaxPage = max(1, (totalEntries + perPage - 1) / perPage);
|
||||
popupMaxPage = std::max(1, (totalEntries + perPage - 1) / perPage);
|
||||
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%d-%d/%d Pg %d/%d", popupStart, popupEnd, popupTotal, popupPage, popupMaxPage);
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include "graphics/TimeFormatters.h"
|
||||
#include "graphics/images.h"
|
||||
#include "main.h"
|
||||
#include "modules/CannedMessageModule.h"
|
||||
#include "target_specific.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <RTC.h>
|
||||
@@ -289,8 +288,7 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
|
||||
// **********************
|
||||
// * Favorite Node Info *
|
||||
// **********************
|
||||
// cppcheck-suppress constParameterPointer; signature must match FrameCallback typedef from OLEDDisplayUi library
|
||||
void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
if (favoritedNodes.empty())
|
||||
return;
|
||||
@@ -1390,7 +1388,6 @@ static int8_t lastFrameIndex = -1;
|
||||
static uint32_t lastFrameChangeTime = 0;
|
||||
constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000;
|
||||
|
||||
// cppcheck-suppress constParameterPointer; signature must match OverlayCallback typedef from OLEDDisplayUi library
|
||||
void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
int currentFrame = state->currentFrame;
|
||||
@@ -1423,11 +1420,6 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing;
|
||||
const int xStart = (SCREEN_WIDTH - totalWidth) / 2;
|
||||
|
||||
// Suppress nav overlay entirely
|
||||
if (cannedMessageModule && cannedMessageModule->isFreeTextUIActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS;
|
||||
int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT;
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ class UIRenderer
|
||||
// Navigation bar overlay
|
||||
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
|
||||
static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ int LatchingBacklight::beforeDeepSleep(void *unused)
|
||||
{
|
||||
// Contingency only
|
||||
// - pin wasn't set
|
||||
if (pin != static_cast<uint8_t>(-1)) {
|
||||
if (pin != (uint8_t)-1) {
|
||||
off();
|
||||
pinMode(pin, INPUT); // High impedance - unnecessary?
|
||||
} else
|
||||
@@ -55,7 +55,7 @@ int LatchingBacklight::beforeDeepSleep(void *unused)
|
||||
// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling
|
||||
void LatchingBacklight::peek()
|
||||
{
|
||||
assert(pin != static_cast<uint8_t>(-1));
|
||||
assert(pin != (uint8_t)-1);
|
||||
digitalWrite(pin, logicActive); // On
|
||||
on = true;
|
||||
latched = false;
|
||||
@@ -67,7 +67,7 @@ void LatchingBacklight::peek()
|
||||
// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling
|
||||
void LatchingBacklight::latch()
|
||||
{
|
||||
assert(pin != static_cast<uint8_t>(-1));
|
||||
assert(pin != (uint8_t)-1);
|
||||
|
||||
// Blink if moving from peek to latch
|
||||
// Indicates to user that the transition has taken place
|
||||
@@ -89,7 +89,7 @@ void LatchingBacklight::latch()
|
||||
// Suitable for ending both peek and latch
|
||||
void LatchingBacklight::off()
|
||||
{
|
||||
assert(pin != static_cast<uint8_t>(-1));
|
||||
assert(pin != (uint8_t)-1);
|
||||
digitalWrite(pin, !logicActive); // Off
|
||||
on = false;
|
||||
latched = false;
|
||||
|
||||
@@ -40,7 +40,7 @@ class LatchingBacklight
|
||||
CallbackObserver<LatchingBacklight, void *> deepSleepObserver =
|
||||
CallbackObserver<LatchingBacklight, void *>(this, &LatchingBacklight::beforeDeepSleep);
|
||||
|
||||
uint8_t pin = static_cast<uint8_t>(-1);
|
||||
uint8_t pin = (uint8_t)-1;
|
||||
bool logicActive = HIGH; // Is light active HIGH or active LOW
|
||||
|
||||
bool on = false; // Is light on (either peek or latched)
|
||||
|
||||
@@ -34,7 +34,7 @@ void InkHUD::Applet::drawPixel(int16_t x, int16_t y, uint16_t color)
|
||||
{
|
||||
// Only render pixels if they fall within user's cropped region
|
||||
if (x >= cropLeft && x < (cropLeft + cropWidth) && y >= cropTop && y < (cropTop + cropHeight))
|
||||
assignedTile->handleAppletPixel(x, y, static_cast<Color>(color));
|
||||
assignedTile->handleAppletPixel(x, y, (Color)color);
|
||||
}
|
||||
|
||||
// Link our applet to a tile
|
||||
@@ -144,21 +144,6 @@ void InkHUD::Applet::resetDrawingSpace()
|
||||
setFont(fontSmall);
|
||||
}
|
||||
|
||||
// Sets one or more inputs to enabled/disabled for this applet and if they should be sent to it
|
||||
void InkHUD::Applet::setInputsSubscribed(uint8_t input, bool captured)
|
||||
{
|
||||
if (captured)
|
||||
subscribedInputs |= input;
|
||||
else
|
||||
subscribedInputs &= ~input;
|
||||
}
|
||||
|
||||
// Checks if a specific input is enabled for this applet and should be sent to it
|
||||
bool InkHUD::Applet::isInputSubscribed(InputMask input)
|
||||
{
|
||||
return (subscribedInputs & input) == input;
|
||||
}
|
||||
|
||||
// Tell InkHUD::Renderer that we want to render now
|
||||
// Applets should internally listen for events they are interested in, via MeshModule, CallbackObserver etc
|
||||
// When an applet decides it has heard something important, and wants to redraw, it calls this method
|
||||
@@ -327,7 +312,7 @@ void InkHUD::Applet::printAt(int16_t x, int16_t y, const char *text, HorizontalA
|
||||
}
|
||||
|
||||
// Print text, specifying the position of any edge / corner of the textbox
|
||||
void InkHUD::Applet::printAt(int16_t x, int16_t y, const std::string &text, HorizontalAlignment ha, VerticalAlignment va)
|
||||
void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha, VerticalAlignment va)
|
||||
{
|
||||
printAt(x, y, text.c_str(), ha, va);
|
||||
}
|
||||
@@ -349,7 +334,7 @@ InkHUD::AppletFont InkHUD::Applet::getFont()
|
||||
|
||||
// Parse any text which might have "special characters"
|
||||
// Re-encodes UTF-8 characters to match our 8-bit encoded fonts
|
||||
std::string InkHUD::Applet::parse(const std::string &text)
|
||||
std::string InkHUD::Applet::parse(std::string text)
|
||||
{
|
||||
return getFont().decodeUTF8(text);
|
||||
}
|
||||
@@ -376,10 +361,10 @@ std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node)
|
||||
}
|
||||
|
||||
// Determine if all characters of a string are printable using the current font
|
||||
bool InkHUD::Applet::isPrintable(const std::string &text)
|
||||
bool InkHUD::Applet::isPrintable(std::string text)
|
||||
{
|
||||
// Scan for SUB (0x1A), which is the value assigned by AppletFont::applyEncoding if a unicode character is not handled
|
||||
for (const char &c : text) {
|
||||
for (char &c : text) {
|
||||
if (c == '\x1A')
|
||||
return false;
|
||||
}
|
||||
@@ -402,7 +387,7 @@ uint16_t InkHUD::Applet::getTextWidth(const char *text)
|
||||
|
||||
// Gets rendered width of a string
|
||||
// Wrapper for getTextBounds
|
||||
uint16_t InkHUD::Applet::getTextWidth(const std::string &text)
|
||||
uint16_t InkHUD::Applet::getTextWidth(std::string text)
|
||||
{
|
||||
return getTextWidth(text.c_str());
|
||||
}
|
||||
@@ -450,7 +435,7 @@ std::string InkHUD::Applet::hexifyNodeNum(NodeNum num)
|
||||
|
||||
// Print text, with word wrapping
|
||||
// Avoids splitting words in half, instead moving the entire word to a new line wherever possible
|
||||
void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, const std::string &text)
|
||||
void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std::string text)
|
||||
{
|
||||
// Place the AdafruitGFX cursor to suit our "top" coord
|
||||
setCursor(left, top + getFont().heightAboveCursor());
|
||||
@@ -507,15 +492,15 @@ void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, con
|
||||
|
||||
// Todo: rewrite making use of AdafruitGFX native text wrapping
|
||||
char cstr[] = {0, 0};
|
||||
int16_t bx, by;
|
||||
uint16_t bw, bh;
|
||||
int16_t l, t;
|
||||
uint16_t w, h;
|
||||
for (uint16_t c = 0; c < word.length(); c++) {
|
||||
// Shove next char into a c string
|
||||
cstr[0] = word[c];
|
||||
getTextBounds(cstr, getCursorX(), getCursorY(), &bx, &by, &bw, &bh);
|
||||
getTextBounds(cstr, getCursorX(), getCursorY(), &l, &t, &w, &h);
|
||||
|
||||
// Manual newline, if next character will spill beyond screen edge
|
||||
if ((bx + bw) > left + width)
|
||||
if ((l + w) > left + width)
|
||||
setCursor(left, getCursorY() + getFont().lineHeight());
|
||||
|
||||
// Print next character
|
||||
@@ -534,7 +519,7 @@ void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, con
|
||||
|
||||
// Simulate running printWrapped, to determine how tall the block of text will be.
|
||||
// This is a wasteful way of handling things. Maybe some way to optimize in future?
|
||||
uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, const std::string &text)
|
||||
uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, std::string text)
|
||||
{
|
||||
// Cache the current crop region
|
||||
int16_t cL = cropLeft;
|
||||
@@ -664,7 +649,7 @@ uint16_t InkHUD::Applet::getActiveNodeCount()
|
||||
|
||||
// For each node in db
|
||||
for (uint16_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
|
||||
// Check if heard recently, and not our own node
|
||||
if (sinceLastSeen(node) < settings->recentlyActiveSeconds && node->num != nodeDB->getNodeNum())
|
||||
@@ -717,7 +702,7 @@ std::string InkHUD::Applet::localizeDistance(uint32_t meters)
|
||||
}
|
||||
|
||||
// Print text with a "faux bold" effect, by drawing it multiple times, offsetting slightly
|
||||
void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, const std::string &text, uint8_t thicknessX, uint8_t thicknessY)
|
||||
void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY)
|
||||
{
|
||||
// How many times to draw along x axis
|
||||
int16_t xStart;
|
||||
@@ -785,7 +770,7 @@ bool InkHUD::Applet::approveNotification(NicheGraphics::InkHUD::Notification &n)
|
||||
│ │
|
||||
└───────────────────────────────┘
|
||||
*/
|
||||
void InkHUD::Applet::drawHeader(const std::string &text)
|
||||
void InkHUD::Applet::drawHeader(std::string text)
|
||||
{
|
||||
// Y position for divider
|
||||
// - between header text and messages
|
||||
|
||||
@@ -89,9 +89,6 @@ class Applet : public GFX
|
||||
virtual void onForeground() {}
|
||||
virtual void onBackground() {}
|
||||
virtual void onShutdown() {}
|
||||
|
||||
// Input Events
|
||||
|
||||
virtual void onButtonShortPress() {}
|
||||
virtual void onButtonLongPress() {}
|
||||
virtual void onExitShort() {}
|
||||
@@ -103,18 +100,6 @@ class Applet : public GFX
|
||||
virtual void onFreeText(char c) {}
|
||||
virtual void onFreeTextDone() {}
|
||||
virtual void onFreeTextCancel() {}
|
||||
// List of inputs which can be subscribed to
|
||||
enum InputMask { // | No Joystick | With Joystick |
|
||||
BUTTON_SHORT = 1, // | Button Click | Joystick Center Click |
|
||||
BUTTON_LONG = 2, // | Button Hold | Joystick Center Hold |
|
||||
EXIT_SHORT = 4, // | no-op | Back Button Click |
|
||||
EXIT_LONG = 8, // | no-op | Back Button Hold |
|
||||
NAV_UP = 16, // | no-op | Joystick Up |
|
||||
NAV_DOWN = 32, // | no-op | Joystick Down |
|
||||
NAV_LEFT = 64, // | no-op | Joystick Left |
|
||||
NAV_RIGHT = 128 // | no-op | Joystick Right |
|
||||
};
|
||||
bool isInputSubscribed(InputMask input); // Check if input should be handled by applet, this should not be overloaded.
|
||||
|
||||
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
|
||||
|
||||
@@ -136,28 +121,20 @@ class Applet : public GFX
|
||||
void setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height); // Ignore pixels drawn outside a certain region
|
||||
void resetCrop(); // Removes setCrop()
|
||||
|
||||
// User Input Handling
|
||||
|
||||
uint8_t subscribedInputs = 0b00000000; // Maybe uint16_t for futureproofing? other devices may need more inputs
|
||||
void setInputsSubscribed(uint8_t input,
|
||||
bool captured); // Set if an input should be handled by applet or not, this should not be
|
||||
// overloaded. Can take multiple inputs at once if you OR/`|` them together
|
||||
|
||||
// Text
|
||||
|
||||
void setFont(AppletFont f);
|
||||
AppletFont getFont();
|
||||
uint16_t getTextWidth(const std::string &text);
|
||||
uint16_t getTextWidth(std::string text);
|
||||
uint16_t getTextWidth(const char *text);
|
||||
uint32_t getWrappedTextHeight(int16_t left, uint16_t width, const std::string &text); // Result of printWrapped
|
||||
uint32_t getWrappedTextHeight(int16_t left, uint16_t width, std::string text); // Result of printWrapped
|
||||
void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
|
||||
void printAt(int16_t x, int16_t y, const std::string &text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
|
||||
void printThick(int16_t xCenter, int16_t yCenter, const std::string &text, uint8_t thicknessX,
|
||||
uint8_t thicknessY); // Faux bold
|
||||
void printWrapped(int16_t left, int16_t top, uint16_t width, const std::string &text); // Per-word line wrapping
|
||||
void printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
|
||||
void printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY); // Faux bold
|
||||
void printWrapped(int16_t left, int16_t top, uint16_t width, std::string text); // Per-word line wrapping
|
||||
|
||||
void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color); // Fill with sparse lines
|
||||
void drawHeader(const std::string &text); // Draw the standard applet header
|
||||
void drawHeader(std::string text); // Draw the standard applet header
|
||||
|
||||
// Meshtastic Logo
|
||||
|
||||
@@ -173,9 +150,9 @@ class Applet : public GFX
|
||||
std::string getTimeString(); // Current time, human readable
|
||||
uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu
|
||||
std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric
|
||||
std::string parse(const std::string &text); // Handle text which might contain special chars
|
||||
std::string parse(std::string text); // Handle text which might contain special chars
|
||||
std::string parseShortName(meshtastic_NodeInfoLite *node); // Get the shortname, or a substitute if has unprintable chars
|
||||
bool isPrintable(const std::string &text); // Check for characters which the font can't print
|
||||
bool isPrintable(std::string); // Check for characters which the font can't print
|
||||
|
||||
// Convenient references
|
||||
|
||||
|
||||
@@ -39,11 +39,11 @@ InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding
|
||||
// Caution: signed and unsigned types
|
||||
int8_t glyphAscender = 0 - gfxFont->glyph[i].yOffset;
|
||||
if (glyphAscender > 0)
|
||||
this->ascenderHeight = max(this->ascenderHeight, static_cast<uint8_t>(glyphAscender));
|
||||
this->ascenderHeight = max(this->ascenderHeight, (uint8_t)glyphAscender);
|
||||
|
||||
int8_t glyphDescender = gfxFont->glyph[i].height + gfxFont->glyph[i].yOffset;
|
||||
if (glyphDescender > 0)
|
||||
this->descenderHeight = max(this->descenderHeight, static_cast<uint8_t>(glyphDescender));
|
||||
this->descenderHeight = max(this->descenderHeight, (uint8_t)glyphDescender);
|
||||
}
|
||||
|
||||
// Apply any manual padding to grow or shrink the line size
|
||||
@@ -52,7 +52,7 @@ InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding
|
||||
descenderHeight += paddingBottom;
|
||||
|
||||
// Find how far the cursor advances when we "print" a space character
|
||||
spaceCharWidth = gfxFont->glyph[static_cast<uint8_t>(' ') - gfxFont->first].xAdvance;
|
||||
spaceCharWidth = gfxFont->glyph[(uint8_t)' ' - gfxFont->first].xAdvance;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -98,7 +98,7 @@ uint8_t InkHUD::AppletFont::widthBetweenWords()
|
||||
|
||||
// Convert a unicode char from set of UTF-8 bytes to UTF-32
|
||||
// Used by AppletFont::applyEncoding, which remaps unicode chars for extended ASCII fonts, based on their UTF-32 value
|
||||
uint32_t InkHUD::AppletFont::toUtf32(const std::string &utf8)
|
||||
uint32_t InkHUD::AppletFont::toUtf32(std::string utf8)
|
||||
{
|
||||
uint32_t utf32 = 0;
|
||||
|
||||
@@ -132,7 +132,7 @@ uint32_t InkHUD::AppletFont::toUtf32(const std::string &utf8)
|
||||
|
||||
// Process a string, collating UTF-8 bytes, and sending them off for re-encoding to extended ASCII
|
||||
// Not all InkHUD text is passed through here, only text which could potentially contain non-ASCII chars
|
||||
std::string InkHUD::AppletFont::decodeUTF8(const std::string &encoded)
|
||||
std::string InkHUD::AppletFont::decodeUTF8(std::string encoded)
|
||||
{
|
||||
// Final processed output
|
||||
std::string decoded;
|
||||
@@ -141,7 +141,7 @@ std::string InkHUD::AppletFont::decodeUTF8(const std::string &encoded)
|
||||
std::string utf8Char;
|
||||
uint8_t utf8CharSize = 0;
|
||||
|
||||
for (const char &c : encoded) {
|
||||
for (char &c : encoded) {
|
||||
|
||||
// If first byte
|
||||
if (utf8Char.empty()) {
|
||||
@@ -178,7 +178,7 @@ std::string InkHUD::AppletFont::decodeUTF8(const std::string &encoded)
|
||||
|
||||
// Re-encode a single UTF-8 character to extended ASCII
|
||||
// Target encoding depends on the font
|
||||
char InkHUD::AppletFont::applyEncoding(const std::string &utf8)
|
||||
char InkHUD::AppletFont::applyEncoding(std::string utf8)
|
||||
{
|
||||
// ##################################################### Syntactic Sugar #####################################################
|
||||
#define REMAP(in, out) \
|
||||
|
||||
@@ -30,21 +30,20 @@ class AppletFont
|
||||
};
|
||||
|
||||
AppletFont();
|
||||
explicit AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0,
|
||||
int8_t paddingBottom = 0);
|
||||
AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0, int8_t paddingBottom = 0);
|
||||
|
||||
uint8_t lineHeight();
|
||||
uint8_t heightAboveCursor();
|
||||
uint8_t heightBelowCursor();
|
||||
uint8_t widthBetweenWords(); // Width of the space character
|
||||
|
||||
std::string decodeUTF8(const std::string &encoded);
|
||||
std::string decodeUTF8(std::string encoded);
|
||||
|
||||
const GFXfont *gfxFont = nullptr; // Default value: in-built AdafruitGFX font
|
||||
const GFXfont *gfxFont = NULL; // Default value: in-built AdafruitGFX font
|
||||
|
||||
private:
|
||||
uint32_t toUtf32(const std::string &utf8);
|
||||
char applyEncoding(const std::string &utf8);
|
||||
uint32_t toUtf32(std::string utf8);
|
||||
char applyEncoding(std::string utf8);
|
||||
|
||||
uint8_t height = 8; // Default value: in-built AdafruitGFX font
|
||||
uint8_t ascenderHeight = 0; // Default value: in-built AdafruitGFX font
|
||||
|
||||
@@ -81,25 +81,25 @@ ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacke
|
||||
uint8_t InkHUD::NodeListApplet::maxCards()
|
||||
{
|
||||
// Cache result. Shouldn't change during execution
|
||||
static uint8_t maxCardCount = 0;
|
||||
static uint8_t cards = 0;
|
||||
|
||||
if (!maxCardCount) {
|
||||
if (!cards) {
|
||||
const uint16_t height = Tile::maxDisplayDimension();
|
||||
|
||||
// Use a loop instead of arithmetic, because it's easier for my brain to follow
|
||||
// Add cards one by one, until the latest card extends below screen
|
||||
|
||||
uint16_t y = cardH; // First card: no margin above
|
||||
maxCardCount = 1;
|
||||
cards = 1;
|
||||
|
||||
while (y < height) {
|
||||
y += cardMarginH;
|
||||
y += cardH;
|
||||
maxCardCount++;
|
||||
cards++;
|
||||
}
|
||||
}
|
||||
|
||||
return maxCardCount;
|
||||
return cards;
|
||||
}
|
||||
|
||||
// Draw, using info which derived applet placed into NodeListApplet::cards for us
|
||||
@@ -137,12 +137,12 @@ void InkHUD::NodeListApplet::onRender(bool full)
|
||||
|
||||
// Gather info
|
||||
// ========================================
|
||||
const NodeNum &nodeNum = card->nodeNum;
|
||||
NodeNum &nodeNum = card->nodeNum;
|
||||
SignalStrength &signal = card->signal;
|
||||
std::string longName; // handled below
|
||||
std::string shortName; // handled below
|
||||
std::string distance; // handled below;
|
||||
const uint8_t &hopsAway = card->hopsAway;
|
||||
uint8_t &hopsAway = card->hopsAway;
|
||||
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum);
|
||||
|
||||
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
#include "./UserAppletInputExample.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::UserAppletInputExampleApplet::onActivate()
|
||||
{
|
||||
setGrabbed(false);
|
||||
}
|
||||
|
||||
void InkHUD::UserAppletInputExampleApplet::onRender(bool full)
|
||||
{
|
||||
drawHeader("Input Example");
|
||||
uint16_t headerHeight = getHeaderHeight();
|
||||
|
||||
std::string buttonName;
|
||||
if (settings->joystick.enabled)
|
||||
buttonName = "joystick center button";
|
||||
else
|
||||
buttonName = "user button";
|
||||
|
||||
std::string additional = " | Control is grabbed, long press " + buttonName + " to release controls";
|
||||
if (!isGrabbed)
|
||||
additional = " | Control is released, long press " + buttonName + " to grab controls";
|
||||
|
||||
printWrapped(0, headerHeight, width(), "Last button: " + lastInput + additional);
|
||||
}
|
||||
|
||||
void InkHUD::UserAppletInputExampleApplet::setGrabbed(bool grabbed)
|
||||
{
|
||||
isGrabbed = grabbed;
|
||||
setInputsSubscribed(BUTTON_SHORT | EXIT_SHORT | EXIT_LONG | NAV_UP | NAV_DOWN | NAV_LEFT | NAV_RIGHT,
|
||||
grabbed); // Enables/disables grabbing all inputs
|
||||
setInputsSubscribed(BUTTON_LONG, true); // Always grab this input
|
||||
}
|
||||
|
||||
void InkHUD::UserAppletInputExampleApplet::onButtonShortPress()
|
||||
{
|
||||
lastInput = "BUTTON_SHORT";
|
||||
requestUpdate();
|
||||
}
|
||||
void InkHUD::UserAppletInputExampleApplet::onButtonLongPress()
|
||||
{
|
||||
lastInput = "BUTTON_LONG";
|
||||
setGrabbed(!isGrabbed);
|
||||
requestUpdate();
|
||||
}
|
||||
void InkHUD::UserAppletInputExampleApplet::onExitShort()
|
||||
{
|
||||
lastInput = "EXIT_SHORT";
|
||||
requestUpdate();
|
||||
}
|
||||
void InkHUD::UserAppletInputExampleApplet::onExitLong()
|
||||
{
|
||||
lastInput = "EXIT_LONG";
|
||||
requestUpdate();
|
||||
}
|
||||
void InkHUD::UserAppletInputExampleApplet::onNavUp()
|
||||
{
|
||||
lastInput = "NAV_UP";
|
||||
requestUpdate();
|
||||
}
|
||||
void InkHUD::UserAppletInputExampleApplet::onNavDown()
|
||||
{
|
||||
lastInput = "NAV_DOWN";
|
||||
requestUpdate();
|
||||
}
|
||||
void InkHUD::UserAppletInputExampleApplet::onNavLeft()
|
||||
{
|
||||
lastInput = "NAV_LEFT";
|
||||
requestUpdate();
|
||||
}
|
||||
void InkHUD::UserAppletInputExampleApplet::onNavRight()
|
||||
{
|
||||
lastInput = "NAV_RIGHT";
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
#endif
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class UserAppletInputExampleApplet : public Applet
|
||||
{
|
||||
public:
|
||||
void onActivate() override;
|
||||
|
||||
void onRender(bool full) override;
|
||||
void onButtonShortPress() override;
|
||||
void onButtonLongPress() override;
|
||||
void onExitShort() override;
|
||||
void onExitLong() override;
|
||||
void onNavUp() override;
|
||||
void onNavDown() override;
|
||||
void onNavLeft() override;
|
||||
void onNavRight() override;
|
||||
|
||||
private:
|
||||
std::string lastInput = "None";
|
||||
bool isGrabbed = false;
|
||||
|
||||
void setGrabbed(bool grabbed);
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
@@ -29,10 +29,10 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
|
||||
// If we get a different type of status, something has gone weird elsewhere
|
||||
assert(status->getStatusType() == STATUS_TYPE_POWER);
|
||||
|
||||
const meshtastic::PowerStatus *pwrStatus = (const meshtastic::PowerStatus *)status;
|
||||
meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)status;
|
||||
|
||||
// Get the new state of charge %, and round to the nearest 10%
|
||||
uint8_t newSocRounded = ((pwrStatus->getBatteryChargePercent() + 5) / 10) * 10;
|
||||
uint8_t newSocRounded = ((powerStatus->getBatteryChargePercent() + 5) / 10) * 10;
|
||||
|
||||
// If rounded value has changed, trigger a display update
|
||||
// It's okay to requestUpdate before we store the new value, as the update won't run until next loop()
|
||||
|
||||
@@ -1176,7 +1176,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
|
||||
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
|
||||
const meshtastic_Channel &ch = channels.getByIndex(i);
|
||||
meshtastic_Channel &ch = channels.getByIndex(i);
|
||||
|
||||
if (!ch.has_settings)
|
||||
continue;
|
||||
@@ -1252,7 +1252,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
case NODE_CONFIG_CHANNEL_PRECISION: {
|
||||
previousPage = MenuPage::NODE_CONFIG_CHANNEL_DETAIL;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
const meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex);
|
||||
meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex);
|
||||
if (!ch.settings.has_module_settings || ch.settings.module_settings.position_precision == 0) {
|
||||
items.push_back(MenuItem("Position is Off", MenuPage::NODE_CONFIG_CHANNEL_DETAIL));
|
||||
break;
|
||||
@@ -1524,15 +1524,7 @@ void InkHUD::MenuApplet::onButtonShortPress()
|
||||
if (!settings->joystick.enabled) {
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
// Select the first item that isn't a header
|
||||
cursor = 0;
|
||||
while (cursor < items.size() && items.at(cursor).isHeader) {
|
||||
cursor++;
|
||||
}
|
||||
if (cursor >= items.size()) {
|
||||
cursorShown = false;
|
||||
cursor = 0;
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
cursor = (cursor + 1) % items.size();
|
||||
@@ -1584,15 +1576,7 @@ void InkHUD::MenuApplet::onNavUp()
|
||||
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
// Select the last item that isn't a header
|
||||
cursor = items.size() - 1;
|
||||
while (items.at(cursor).isHeader) {
|
||||
if (cursor == 0) {
|
||||
cursorShown = false;
|
||||
break;
|
||||
}
|
||||
cursor--;
|
||||
}
|
||||
cursor = 0;
|
||||
} else {
|
||||
do {
|
||||
if (cursor == 0)
|
||||
@@ -1613,15 +1597,7 @@ void InkHUD::MenuApplet::onNavDown()
|
||||
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
// Select the first item that isn't a header
|
||||
cursor = 0;
|
||||
while (cursor < items.size() && items.at(cursor).isHeader) {
|
||||
cursor++;
|
||||
}
|
||||
if (cursor >= items.size()) {
|
||||
cursorShown = false;
|
||||
cursor = 0;
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
cursor = (cursor + 1) % items.size();
|
||||
@@ -1783,7 +1759,7 @@ void InkHUD::MenuApplet::populateRecipientPage()
|
||||
|
||||
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
|
||||
// Get the channel, and check if it's enabled
|
||||
const meshtastic_Channel &channel = channels.getByIndex(i);
|
||||
meshtastic_Channel &channel = channels.getByIndex(i);
|
||||
if (!channel.has_settings || channel.role == meshtastic_Channel_Role_DISABLED)
|
||||
continue;
|
||||
|
||||
@@ -1853,7 +1829,7 @@ void InkHUD::MenuApplet::populateRecipientPage()
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, const std::string &text)
|
||||
void InkHUD::MenuApplet::drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, std::string text)
|
||||
{
|
||||
setFont(fontSmall);
|
||||
uint16_t wrapMaxH = 0;
|
||||
|
||||
@@ -55,7 +55,7 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
||||
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
|
||||
|
||||
void drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height,
|
||||
const std::string &text); // Draw input field for free text
|
||||
std::string text); // Draw input field for free text
|
||||
uint16_t getSystemInfoPanelHeight();
|
||||
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
|
||||
uint16_t *height = nullptr); // Info panel at top of root menu
|
||||
|
||||
@@ -228,17 +228,17 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
|
||||
Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)) {
|
||||
|
||||
// Although we are handling DM and broadcast notifications together, we do need to treat them slightly differently
|
||||
bool msgIsBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
|
||||
bool isBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
|
||||
|
||||
// Pick source of message
|
||||
const MessageStore::Message *message =
|
||||
msgIsBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm;
|
||||
MessageStore::Message *message =
|
||||
isBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm;
|
||||
|
||||
// Find info about the sender
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender);
|
||||
|
||||
// Leading tag (channel vs. DM)
|
||||
text += msgIsBroadcast ? "From:" : "DM: ";
|
||||
text += isBroadcast ? "From:" : "DM: ";
|
||||
|
||||
// Sender id
|
||||
if (node && node->has_user)
|
||||
@@ -252,7 +252,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
|
||||
text.clear();
|
||||
|
||||
// Leading tag (channel vs. DM)
|
||||
text += msgIsBroadcast ? "Msg from " : "DM from ";
|
||||
text += isBroadcast ? "Msg from " : "DM from ";
|
||||
|
||||
// Sender id
|
||||
if (node && node->has_user)
|
||||
|
||||
@@ -55,12 +55,12 @@ int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *sta
|
||||
// We'll mimic that behavior, just to keep in line with the other Statuses,
|
||||
// even though I'm not sure what the original reason for jumping through these extra hoops was.
|
||||
assert(status->getStatusType() == STATUS_TYPE_BLUETOOTH);
|
||||
const auto *btStatus = static_cast<const meshtastic::BluetoothStatus *>(status);
|
||||
meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)status;
|
||||
|
||||
// When pairing begins
|
||||
if (btStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) {
|
||||
if (bluetoothStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) {
|
||||
// Store the passkey for rendering
|
||||
passkey = btStatus->getPasskey();
|
||||
passkey = bluetoothStatus->getPasskey();
|
||||
|
||||
// Show pairing screen
|
||||
bringToForeground();
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./FavoritesMapApplet.h"
|
||||
#include "NodeDB.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
bool InkHUD::FavoritesMapApplet::shouldDrawNode(meshtastic_NodeInfoLite *node)
|
||||
{
|
||||
// Keep our own node available as map anchor/center; all others must be favorited.
|
||||
return node && (node->num == nodeDB->getNodeNum() || node->is_favorite);
|
||||
}
|
||||
|
||||
void InkHUD::FavoritesMapApplet::onRender(bool full)
|
||||
{
|
||||
// Custom empty state text for favorites-only map.
|
||||
if (!enoughMarkers()) {
|
||||
printAt(X(0.5), Y(0.5) - (getFont().lineHeight() / 2), "Favorite node position", CENTER, MIDDLE);
|
||||
printAt(X(0.5), Y(0.5) + (getFont().lineHeight() / 2), "will appear here", CENTER, MIDDLE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw the usual map applet first.
|
||||
MapApplet::onRender(full);
|
||||
|
||||
// Draw our latest "node of interest" as a special marker.
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom);
|
||||
if (node && node->is_favorite && nodeDB->hasValidPosition(node) && enoughMarkers())
|
||||
drawLabeledMarker(node);
|
||||
}
|
||||
|
||||
// Determine if we need to redraw the map, when we receive a new position packet.
|
||||
ProcessMessage InkHUD::FavoritesMapApplet::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
// If applet is not active, we shouldn't be handling any data.
|
||||
if (!isActive())
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Try decode a position from the packet.
|
||||
bool hasPosition = false;
|
||||
float lat;
|
||||
float lng;
|
||||
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) {
|
||||
meshtastic_Position position = meshtastic_Position_init_default;
|
||||
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &position)) {
|
||||
if (position.has_latitude_i && position.has_longitude_i // Actually has position
|
||||
&& (position.latitude_i != 0 || position.longitude_i != 0)) // Position isn't "null island"
|
||||
{
|
||||
hasPosition = true;
|
||||
lat = position.latitude_i * 1e-7; // Convert from Meshtastic's internal int32_t format
|
||||
lng = position.longitude_i * 1e-7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if we didn't get a valid position.
|
||||
if (!hasPosition)
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
const int8_t hopsAway = getHopsAway(mp);
|
||||
const bool hasHopsAway = hopsAway >= 0;
|
||||
|
||||
// Determine if the position packet would change anything on-screen.
|
||||
bool somethingChanged = false;
|
||||
|
||||
// If our own position.
|
||||
if (isFromUs(&mp)) {
|
||||
// Ignore tiny local movement to reduce update spam.
|
||||
if (GeoCoord::latLongToMeter(ourLastLat, ourLastLng, lat, lng) > 50) {
|
||||
somethingChanged = true;
|
||||
ourLastLat = lat;
|
||||
ourLastLng = lng;
|
||||
}
|
||||
} else {
|
||||
// For non-local packets, this applet only reacts to favorited nodes.
|
||||
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from);
|
||||
if (!sender || !sender->is_favorite)
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Check if this position is from someone different than our previous position packet.
|
||||
if (mp.from != lastFrom) {
|
||||
somethingChanged = true;
|
||||
lastFrom = mp.from;
|
||||
lastLat = lat;
|
||||
lastLng = lng;
|
||||
lastHopsAway = hopsAway;
|
||||
}
|
||||
|
||||
// Same sender: check if position changed.
|
||||
else if (GeoCoord::latLongToMeter(lastLat, lastLng, lat, lng) > 10) {
|
||||
somethingChanged = true;
|
||||
lastLat = lat;
|
||||
lastLng = lng;
|
||||
}
|
||||
|
||||
// Same sender, same position: check if hops changed.
|
||||
else if (hasHopsAway && (hopsAway != lastHopsAway)) {
|
||||
somethingChanged = true;
|
||||
lastHopsAway = hopsAway;
|
||||
}
|
||||
}
|
||||
|
||||
if (somethingChanged) {
|
||||
requestAutoshow();
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,44 +0,0 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Plots position of favorited nodes from DB, with North facing up.
|
||||
Scaled to fit the most distant node.
|
||||
Size of marker represents hops away.
|
||||
The favorite node which most recently sent a position will be labeled.
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h"
|
||||
|
||||
#include "SinglePortModule.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class FavoritesMapApplet : public MapApplet, public SinglePortModule
|
||||
{
|
||||
public:
|
||||
FavoritesMapApplet() : SinglePortModule("FavoritesMapApplet", meshtastic_PortNum_POSITION_APP) {}
|
||||
void onRender(bool full) override;
|
||||
|
||||
protected:
|
||||
bool shouldDrawNode(meshtastic_NodeInfoLite *node) override;
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
NodeNum lastFrom = 0; // Sender of most recent favorited (non-local) position packet
|
||||
float lastLat = 0.0;
|
||||
float lastLng = 0.0;
|
||||
float lastHopsAway = 0;
|
||||
|
||||
float ourLastLat = 0.0; // Info about most recent local position
|
||||
float ourLastLng = 0.0;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
@@ -69,10 +69,9 @@ void InkHUD::HeardApplet::populateFromNodeDB()
|
||||
}
|
||||
|
||||
// Sort the collection by age
|
||||
std::sort(ordered.begin(), ordered.end(),
|
||||
[](const meshtastic_NodeInfoLite *top, const meshtastic_NodeInfoLite *bottom) -> bool {
|
||||
return (top->last_heard > bottom->last_heard);
|
||||
});
|
||||
std::sort(ordered.begin(), ordered.end(), [](meshtastic_NodeInfoLite *top, meshtastic_NodeInfoLite *bottom) -> bool {
|
||||
return (top->last_heard > bottom->last_heard);
|
||||
});
|
||||
|
||||
// Keep the most recent entries only
|
||||
// Just enough to fill the screen
|
||||
|
||||
@@ -69,7 +69,7 @@ void InkHUD::ThreadedMessageApplet::onRender(bool full)
|
||||
while (msgB >= (0 - fontSmall.lineHeight()) && i < store->messages.size()) {
|
||||
|
||||
// Grab data for message
|
||||
const MessageStore::Message &m = store->messages.at(i);
|
||||
MessageStore::Message &m = store->messages.at(i);
|
||||
bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message
|
||||
std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message
|
||||
|
||||
|
||||
@@ -59,16 +59,10 @@ void InkHUD::Events::onButtonShort()
|
||||
if (consumer) {
|
||||
consumer->onButtonShortPress();
|
||||
} else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::BUTTON_SHORT))
|
||||
userConsumer->onButtonShortPress();
|
||||
else {
|
||||
if (!settings->joystick.enabled)
|
||||
inkhud->nextApplet();
|
||||
else
|
||||
inkhud->openMenu();
|
||||
}
|
||||
if (!settings->joystick.enabled)
|
||||
inkhud->nextApplet();
|
||||
else
|
||||
inkhud->openMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,14 +84,8 @@ void InkHUD::Events::onButtonLong()
|
||||
// If no system applet is handling input, default behavior instead is to open the menu
|
||||
if (consumer)
|
||||
consumer->onButtonLongPress();
|
||||
else {
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::BUTTON_LONG))
|
||||
userConsumer->onButtonLongPress();
|
||||
else
|
||||
inkhud->openMenu();
|
||||
}
|
||||
else
|
||||
inkhud->openMenu();
|
||||
}
|
||||
|
||||
void InkHUD::Events::onExitShort()
|
||||
@@ -122,14 +110,8 @@ void InkHUD::Events::onExitShort()
|
||||
// If no system applet is handling input, default behavior instead is change tiles
|
||||
if (consumer)
|
||||
consumer->onExitShort();
|
||||
else if (!dismissedExt) { // Don't change tile if this button press silenced the external notification module
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_SHORT))
|
||||
userConsumer->onExitShort();
|
||||
else
|
||||
inkhud->nextTile();
|
||||
}
|
||||
else if (!dismissedExt) // Don't change tile if this button press silenced the external notification module
|
||||
inkhud->nextTile();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,13 +133,6 @@ void InkHUD::Events::onExitLong()
|
||||
|
||||
if (consumer)
|
||||
consumer->onExitLong();
|
||||
else {
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_LONG))
|
||||
userConsumer->onExitLong();
|
||||
// Nothing uses exit long yet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,12 +157,6 @@ void InkHUD::Events::onNavUp()
|
||||
|
||||
if (consumer)
|
||||
consumer->onNavUp();
|
||||
else if (!dismissedExt) {
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_UP))
|
||||
userConsumer->onNavUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,12 +181,6 @@ void InkHUD::Events::onNavDown()
|
||||
|
||||
if (consumer)
|
||||
consumer->onNavDown();
|
||||
else if (!dismissedExt) {
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_DOWN))
|
||||
userConsumer->onNavDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,14 +206,8 @@ void InkHUD::Events::onNavLeft()
|
||||
// If no system applet is handling input, default behavior instead is to cycle applets
|
||||
if (consumer)
|
||||
consumer->onNavLeft();
|
||||
else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_LEFT))
|
||||
userConsumer->onNavLeft();
|
||||
else
|
||||
inkhud->prevApplet();
|
||||
}
|
||||
else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module
|
||||
inkhud->prevApplet();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,14 +233,8 @@ void InkHUD::Events::onNavRight()
|
||||
// If no system applet is handling input, default behavior instead is to cycle applets
|
||||
if (consumer)
|
||||
consumer->onNavRight();
|
||||
else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_RIGHT))
|
||||
userConsumer->onNavRight();
|
||||
else
|
||||
inkhud->nextApplet();
|
||||
}
|
||||
else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module
|
||||
inkhud->nextApplet();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -210,12 +210,6 @@ void InkHUD::InkHUD::prevApplet()
|
||||
windowManager->prevApplet();
|
||||
}
|
||||
|
||||
// Returns the currently active applet
|
||||
InkHUD::Applet *InkHUD::InkHUD::getActiveApplet()
|
||||
{
|
||||
return windowManager->getActiveApplet();
|
||||
}
|
||||
|
||||
// Show the menu (on the the focused tile)
|
||||
// The applet previously displayed there will be restored once the menu closes
|
||||
void InkHUD::InkHUD::openMenu()
|
||||
|
||||
@@ -74,7 +74,6 @@ class InkHUD
|
||||
|
||||
void nextApplet();
|
||||
void prevApplet();
|
||||
NicheGraphics::InkHUD::Applet *getActiveApplet();
|
||||
void openMenu();
|
||||
void openAlignStick();
|
||||
void openKeyboard();
|
||||
|
||||
@@ -12,7 +12,7 @@ using namespace NicheGraphics;
|
||||
constexpr uint8_t MAX_MESSAGES_SAVED = 10;
|
||||
constexpr uint32_t MAX_MESSAGE_SIZE = 250;
|
||||
|
||||
InkHUD::MessageStore::MessageStore(const std::string &label)
|
||||
InkHUD::MessageStore::MessageStore(std::string label)
|
||||
{
|
||||
filename = "";
|
||||
filename += "/NicheGraphics";
|
||||
@@ -50,13 +50,12 @@ void InkHUD::MessageStore::saveToFlash()
|
||||
// For each message
|
||||
for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) {
|
||||
Message &m = messages.at(i);
|
||||
f.write(reinterpret_cast<const uint8_t *>(&m.timestamp), sizeof(m.timestamp)); // Write timestamp. 4 bytes
|
||||
f.write(reinterpret_cast<const uint8_t *>(&m.sender), sizeof(m.sender)); // Write sender NodeId. 4 Bytes
|
||||
f.write(reinterpret_cast<const uint8_t *>(&m.channelIndex), sizeof(m.channelIndex)); // Write channel index. 1 Byte
|
||||
f.write(reinterpret_cast<const uint8_t *>(m.text.c_str()), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text
|
||||
f.write('\0'); // Append null term
|
||||
LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", static_cast<uint32_t>(i), min(MAX_MESSAGE_SIZE, m.text.size()),
|
||||
m.text.c_str());
|
||||
f.write((uint8_t *)&m.timestamp, sizeof(m.timestamp)); // Write timestamp. 4 bytes
|
||||
f.write((uint8_t *)&m.sender, sizeof(m.sender)); // Write sender NodeId. 4 Bytes
|
||||
f.write((uint8_t *)&m.channelIndex, sizeof(m.channelIndex)); // Write channel index. 1 Byte
|
||||
f.write((uint8_t *)m.text.c_str(), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text. Variable length
|
||||
f.write('\0'); // Append null term
|
||||
LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", (uint32_t)i, min(MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str());
|
||||
}
|
||||
|
||||
// Release firmware's SPI lock, because SafeFile::close needs it
|
||||
@@ -112,17 +111,17 @@ void InkHUD::MessageStore::loadFromFlash()
|
||||
|
||||
// First byte: how many messages are in the flash store
|
||||
uint8_t flashMessageCount = 0;
|
||||
f.readBytes(reinterpret_cast<char *>(&flashMessageCount), 1);
|
||||
LOG_DEBUG("Messages available: %u", static_cast<uint32_t>(flashMessageCount));
|
||||
f.readBytes((char *)&flashMessageCount, 1);
|
||||
LOG_DEBUG("Messages available: %u", (uint32_t)flashMessageCount);
|
||||
|
||||
// For each message
|
||||
for (uint8_t i = 0; i < flashMessageCount && i < MAX_MESSAGES_SAVED; i++) {
|
||||
Message m;
|
||||
|
||||
// Read meta data (fixed width)
|
||||
f.readBytes(reinterpret_cast<char *>(&m.timestamp), sizeof(m.timestamp));
|
||||
f.readBytes(reinterpret_cast<char *>(&m.sender), sizeof(m.sender));
|
||||
f.readBytes(reinterpret_cast<char *>(&m.channelIndex), sizeof(m.channelIndex));
|
||||
f.readBytes((char *)&m.timestamp, sizeof(m.timestamp));
|
||||
f.readBytes((char *)&m.sender, sizeof(m.sender));
|
||||
f.readBytes((char *)&m.channelIndex, sizeof(m.channelIndex));
|
||||
|
||||
// Read characters until we find a null term
|
||||
char c;
|
||||
@@ -137,8 +136,7 @@ void InkHUD::MessageStore::loadFromFlash()
|
||||
// Store in RAM
|
||||
messages.push_back(m);
|
||||
|
||||
LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", static_cast<uint32_t>(i), m.timestamp, m.sender,
|
||||
m.text.c_str());
|
||||
LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", (uint32_t)i, m.timestamp, m.sender, m.text.c_str());
|
||||
}
|
||||
|
||||
f.close();
|
||||
|
||||
@@ -31,7 +31,7 @@ class MessageStore
|
||||
};
|
||||
|
||||
MessageStore() = delete;
|
||||
explicit MessageStore(const std::string &label); // Label determines filename in flash
|
||||
explicit MessageStore(std::string label); // Label determines filename in flash
|
||||
|
||||
void saveToFlash();
|
||||
void loadFromFlash();
|
||||
|
||||
@@ -8,5 +8,5 @@ build_flags =
|
||||
-D MESHTASTIC_EXCLUDE_INPUTBROKER ; Suppress default input handling
|
||||
-D HAS_BUTTON=0 ; Suppress default ButtonThread
|
||||
lib_deps =
|
||||
# renovate: datasource=github-tags depName=GFX_Root packageName=ZinggJM/GFX_Root
|
||||
https://github.com/ZinggJM/GFX_Root/archive/3195764e352a0d2567c8d277ac408ca7293a99b0.zip ; Used by InkHUD as a "slimmer" version of AdafruitGFX
|
||||
# TODO renovate
|
||||
https://github.com/ZinggJM/GFX_Root#2.0.0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX
|
||||
|
||||
@@ -269,42 +269,42 @@ void InkHUD::Renderer::clearTile(Tile *t)
|
||||
// Rotate the tile dimensions
|
||||
int16_t left = 0;
|
||||
int16_t top = 0;
|
||||
uint16_t tileW = 0;
|
||||
uint16_t tileH = 0;
|
||||
uint16_t width = 0;
|
||||
uint16_t height = 0;
|
||||
switch (settings->rotation) {
|
||||
case 0:
|
||||
left = t->getLeft();
|
||||
top = t->getTop();
|
||||
tileW = t->getWidth();
|
||||
tileH = t->getHeight();
|
||||
width = t->getWidth();
|
||||
height = t->getHeight();
|
||||
break;
|
||||
case 1:
|
||||
left = driver->width - (t->getTop() + t->getHeight());
|
||||
top = t->getLeft();
|
||||
tileW = t->getHeight();
|
||||
tileH = t->getWidth();
|
||||
width = t->getHeight();
|
||||
height = t->getWidth();
|
||||
break;
|
||||
case 2:
|
||||
left = driver->width - (t->getLeft() + t->getWidth());
|
||||
top = driver->height - (t->getTop() + t->getHeight());
|
||||
tileW = t->getWidth();
|
||||
tileH = t->getHeight();
|
||||
width = t->getWidth();
|
||||
height = t->getHeight();
|
||||
break;
|
||||
case 3:
|
||||
left = t->getTop();
|
||||
top = driver->height - (t->getLeft() + t->getWidth());
|
||||
tileW = t->getHeight();
|
||||
tileH = t->getWidth();
|
||||
width = t->getHeight();
|
||||
height = t->getWidth();
|
||||
break;
|
||||
}
|
||||
|
||||
// Calculate the bounds to clear
|
||||
uint16_t xStart = (left < 0) ? 0 : left;
|
||||
uint16_t yStart = (top < 0) ? 0 : top;
|
||||
if (xStart >= driver->width || yStart >= driver->height || left + tileW < 0 || top + tileH < 0)
|
||||
if (xStart >= driver->width || yStart >= driver->height || left + width < 0 || top + height < 0)
|
||||
return; // the box is completely off the screen
|
||||
uint16_t xEnd = left + tileW;
|
||||
uint16_t yEnd = top + tileH;
|
||||
uint16_t xEnd = left + width;
|
||||
uint16_t yEnd = top + height;
|
||||
if (xEnd > driver->width)
|
||||
xEnd = driver->width;
|
||||
if (yEnd > driver->height)
|
||||
|
||||
@@ -273,12 +273,6 @@ void InkHUD::WindowManager::prevApplet()
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST
|
||||
}
|
||||
|
||||
// Returns active applet
|
||||
NicheGraphics::InkHUD::Applet *InkHUD::WindowManager::getActiveApplet()
|
||||
{
|
||||
return userTiles.at(settings->userTiles.focused)->getAssignedApplet();
|
||||
}
|
||||
|
||||
// Rotate the display image by 90 degrees
|
||||
void InkHUD::WindowManager::rotate()
|
||||
{
|
||||
@@ -402,7 +396,7 @@ void InkHUD::WindowManager::autoshow()
|
||||
{
|
||||
// Don't perform autoshow if a system applet has exclusive use of the display right now
|
||||
// Note: lockRequests prevents autoshow attempting to hide menuApplet
|
||||
for (const SystemApplet *sa : inkhud->systemApplets) {
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->lockRendering || sa->lockRequests)
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ class WindowManager
|
||||
|
||||
void nextTile();
|
||||
void prevTile();
|
||||
Applet *getActiveApplet();
|
||||
void openMenu();
|
||||
void openAlignStick();
|
||||
void openKeyboard();
|
||||
|
||||
@@ -146,7 +146,7 @@ void CannedMessageStore::handleGet(meshtastic_AdminMessage *response)
|
||||
std::string merged;
|
||||
if (!messages.empty()) { // Don't run if no messages: error on pop_back with size=0
|
||||
merged.reserve(201);
|
||||
for (const std::string &s : messages) {
|
||||
for (std::string &s : messages) {
|
||||
merged += s;
|
||||
merged += '|';
|
||||
}
|
||||
|
||||
@@ -106,8 +106,8 @@ static unsigned char HackadayCommunicatorTapMap[_TCA8418_NUM_KEYS][2] = {{},
|
||||
{}};
|
||||
|
||||
HackadayCommunicatorKeyboard::HackadayCommunicatorKeyboard()
|
||||
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX),
|
||||
next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0)
|
||||
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1),
|
||||
last_tap(0L), char_idx(0), tap_interval(0)
|
||||
{
|
||||
reset();
|
||||
}
|
||||
@@ -147,6 +147,7 @@ void HackadayCommunicatorKeyboard::pressed(uint8_t key)
|
||||
modifierFlag = 0;
|
||||
}
|
||||
|
||||
uint8_t next_key = 0;
|
||||
int row = (key - 1) / 10;
|
||||
int col = (key - 1) % 10;
|
||||
if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) {
|
||||
@@ -186,8 +187,8 @@ void HackadayCommunicatorKeyboard::released()
|
||||
return;
|
||||
}
|
||||
|
||||
if (last_key >= _TCA8418_NUM_KEYS) {
|
||||
last_key = UINT8_MAX;
|
||||
if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) {
|
||||
last_key = -1;
|
||||
state = Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ class HackadayCommunicatorKeyboard : public TCA8418KeyboardBase
|
||||
private:
|
||||
uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed
|
||||
uint32_t last_modifier_time; // Timestamp of the last modifier key press
|
||||
uint8_t last_key;
|
||||
uint8_t next_key;
|
||||
int8_t last_key;
|
||||
int8_t next_key;
|
||||
uint32_t last_tap;
|
||||
uint8_t char_idx;
|
||||
int32_t tap_interval;
|
||||
|
||||
@@ -91,7 +91,7 @@ MPR121Keyboard::MPR121Keyboard() : m_wire(nullptr), m_addr(0), readCallback(null
|
||||
{
|
||||
// LOG_DEBUG("MPR121 @ %02x", m_addr);
|
||||
state = Init;
|
||||
last_key = UINT8_MAX;
|
||||
last_key = -1;
|
||||
last_tap = 0L;
|
||||
char_idx = 0;
|
||||
queue = "";
|
||||
@@ -359,8 +359,8 @@ void MPR121Keyboard::released()
|
||||
return;
|
||||
}
|
||||
// would clear longpress callback... later.
|
||||
if (last_key >= _NUM_KEYS) { // reset to idle if last_key out of bounds
|
||||
last_key = UINT8_MAX;
|
||||
if (last_key < 0 || last_key > _NUM_KEYS) { // reset to idle if last_key out of bounds
|
||||
last_key = -1;
|
||||
state = Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class MPR121Keyboard
|
||||
|
||||
MPR121States state;
|
||||
|
||||
uint8_t last_key;
|
||||
int8_t last_key;
|
||||
uint32_t last_tap;
|
||||
uint8_t char_idx;
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ static unsigned char TCA8418LongPressMap[_TCA8418_NUM_KEYS] = {
|
||||
};
|
||||
|
||||
TCA8418Keyboard::TCA8418Keyboard()
|
||||
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), last_key(UINT8_MAX), next_key(UINT8_MAX), last_tap(0L), char_idx(0),
|
||||
tap_interval(0), should_backspace(false)
|
||||
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), last_key(-1), next_key(-1), last_tap(0L), char_idx(0), tap_interval(0),
|
||||
should_backspace(false)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ void TCA8418Keyboard::pressed(uint8_t key)
|
||||
if (state == Init || state == Busy) {
|
||||
return;
|
||||
}
|
||||
uint8_t next_key = 0;
|
||||
int row = (key - 1) / 10;
|
||||
int col = (key - 1) % 10;
|
||||
|
||||
@@ -71,7 +72,7 @@ void TCA8418Keyboard::pressed(uint8_t key)
|
||||
}
|
||||
|
||||
// Compute key index based on dynamic row/column
|
||||
next_key = (uint8_t)(row * _TCA8418_COLS + col);
|
||||
next_key = row * _TCA8418_COLS + col;
|
||||
|
||||
// LOG_DEBUG("TCA8418: Key %u -> Next Key %u", key, next_key);
|
||||
|
||||
@@ -105,8 +106,8 @@ void TCA8418Keyboard::released()
|
||||
return;
|
||||
}
|
||||
|
||||
if (last_key >= _TCA8418_NUM_KEYS) { // reset to idle if last_key out of bounds
|
||||
last_key = UINT8_MAX;
|
||||
if (last_key < 0 || last_key > _TCA8418_NUM_KEYS) { // reset to idle if last_key out of bounds
|
||||
last_key = -1;
|
||||
state = Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ class TCA8418Keyboard : public TCA8418KeyboardBase
|
||||
void pressed(uint8_t key) override;
|
||||
void released(void) override;
|
||||
|
||||
uint8_t last_key;
|
||||
uint8_t next_key;
|
||||
int8_t last_key;
|
||||
int8_t next_key;
|
||||
uint32_t last_tap;
|
||||
uint8_t char_idx;
|
||||
int32_t tap_interval;
|
||||
|
||||
@@ -62,8 +62,8 @@ static unsigned char TDeckProTapMap[_TCA8418_NUM_KEYS][5] = {
|
||||
};
|
||||
|
||||
TDeckProKeyboard::TDeckProKeyboard()
|
||||
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX),
|
||||
next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0)
|
||||
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1),
|
||||
last_tap(0L), char_idx(0), tap_interval(0)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ void TDeckProKeyboard::pressed(uint8_t key)
|
||||
modifierFlag = 0;
|
||||
}
|
||||
|
||||
uint8_t next_key = 0;
|
||||
int row = (key - 1) / 10;
|
||||
int col = (key - 1) % 10;
|
||||
|
||||
@@ -141,8 +142,8 @@ void TDeckProKeyboard::released()
|
||||
return;
|
||||
}
|
||||
|
||||
if (last_key >= _TCA8418_NUM_KEYS) {
|
||||
last_key = UINT8_MAX;
|
||||
if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) {
|
||||
last_key = -1;
|
||||
state = Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ class TDeckProKeyboard : public TCA8418KeyboardBase
|
||||
private:
|
||||
uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed
|
||||
uint32_t last_modifier_time; // Timestamp of the last modifier key press
|
||||
uint8_t last_key;
|
||||
uint8_t next_key;
|
||||
int8_t last_key;
|
||||
int8_t next_key;
|
||||
uint32_t last_tap;
|
||||
uint8_t char_idx;
|
||||
int32_t tap_interval;
|
||||
|
||||
@@ -65,8 +65,8 @@ static unsigned char TLoraPagerTapMap[_TCA8418_NUM_KEYS][3] = {{'q', 'Q', '1'},
|
||||
{' ', 0x00, Key::BL_TOGGLE}};
|
||||
|
||||
TLoraPagerKeyboard::TLoraPagerKeyboard()
|
||||
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX),
|
||||
next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0)
|
||||
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1),
|
||||
last_tap(0L), char_idx(0), tap_interval(0)
|
||||
{
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
ledcAttach(KB_BL_PIN, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH);
|
||||
@@ -129,6 +129,7 @@ void TLoraPagerKeyboard::pressed(uint8_t key)
|
||||
modifierFlag = 0;
|
||||
}
|
||||
|
||||
uint8_t next_key = 0;
|
||||
int row = (key - 1) / 10;
|
||||
int col = (key - 1) % 10;
|
||||
|
||||
@@ -169,8 +170,8 @@ void TLoraPagerKeyboard::released()
|
||||
return;
|
||||
}
|
||||
|
||||
if (last_key >= _TCA8418_NUM_KEYS) {
|
||||
last_key = UINT8_MAX;
|
||||
if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) {
|
||||
last_key = -1;
|
||||
state = Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ class TLoraPagerKeyboard : public TCA8418KeyboardBase
|
||||
private:
|
||||
uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed
|
||||
uint32_t last_modifier_time; // Timestamp of the last modifier key press
|
||||
uint8_t last_key;
|
||||
uint8_t next_key;
|
||||
int8_t last_key;
|
||||
int8_t next_key;
|
||||
uint32_t last_tap;
|
||||
uint8_t char_idx;
|
||||
int32_t tap_interval;
|
||||
|
||||
+24
-18
@@ -192,6 +192,8 @@ bool kb_found = false;
|
||||
// global bool to record that on-screen keyboard (OSK) is present
|
||||
bool osk_found = false;
|
||||
|
||||
unsigned long last_listen = 0;
|
||||
|
||||
// The I2C address of the RTC Module (if found)
|
||||
ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE;
|
||||
// The I2C address of the Accelerometer (if found)
|
||||
@@ -247,7 +249,9 @@ static OSThread *powerFSMthread;
|
||||
OSThread *ambientLightingThread;
|
||||
|
||||
RadioInterface *rIf = NULL;
|
||||
#ifdef ARCH_PORTDUINO
|
||||
RadioLibHal *RadioLibHAL = NULL;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Some platforms (nrf52) might provide an alterate version that suppresses calling delay from sleep.
|
||||
@@ -721,15 +725,17 @@ void setup()
|
||||
playStartMelody();
|
||||
|
||||
#if HAS_SCREEN
|
||||
// fixed screen override?
|
||||
// fixed screen override?
|
||||
if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO)
|
||||
screen_model = config.display.oled;
|
||||
|
||||
#if defined(USE_SH1107)
|
||||
screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // set dimension of 128x128
|
||||
screen_geometry = GEOMETRY_128_128;
|
||||
#elif defined(USE_SH1107_128_64)
|
||||
#endif
|
||||
|
||||
#if defined(USE_SH1107_128_64)
|
||||
screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // keep dimension of 128x64
|
||||
#else
|
||||
if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO)
|
||||
screen_model = config.display.oled;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -948,7 +954,7 @@ void setup()
|
||||
#endif
|
||||
#endif
|
||||
|
||||
auto rIf = initLoRa();
|
||||
initLoRa();
|
||||
|
||||
lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation)
|
||||
|
||||
@@ -997,12 +1003,12 @@ void setup()
|
||||
if (!rIf)
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_NO_RADIO);
|
||||
else {
|
||||
router->addInterface(rIf);
|
||||
|
||||
// Log bit rate to debug output
|
||||
LOG_DEBUG("LoRA bitrate = %f bytes / sec", (float(meshtastic_Constants_DATA_PAYLOAD_LEN) /
|
||||
(float(rIf->getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN)))) *
|
||||
1000);
|
||||
|
||||
router->addInterface(std::move(rIf));
|
||||
}
|
||||
|
||||
// This must be _after_ service.init because we need our preferences loaded from flash to have proper timeout values
|
||||
@@ -1119,12 +1125,10 @@ void loop()
|
||||
#endif
|
||||
power->powerCommandsCheck();
|
||||
|
||||
if (RadioLibInterface::instance != nullptr) {
|
||||
static uint32_t lastRadioMissedIrqPoll;
|
||||
if (!Throttle::isWithinTimespanMs(lastRadioMissedIrqPoll, 1000)) {
|
||||
lastRadioMissedIrqPoll = millis();
|
||||
RadioLibInterface::instance->pollMissedIrqs();
|
||||
}
|
||||
if (RadioLibInterface::instance != nullptr && !Throttle::isWithinTimespanMs(last_listen, 1000 * 60) &&
|
||||
!(RadioLibInterface::instance->isSending() || RadioLibInterface::instance->isActivelyReceiving())) {
|
||||
RadioLibInterface::instance->startReceive();
|
||||
LOG_DEBUG("attempting AGC reset");
|
||||
}
|
||||
|
||||
#ifdef DEBUG_STACK
|
||||
@@ -1146,7 +1150,10 @@ void loop()
|
||||
}
|
||||
if (portduino_status.LoRa_in_error && rebootAtMsec == 0) {
|
||||
LOG_ERROR("LoRa in error detected, attempting to recover");
|
||||
router->addInterface(nullptr);
|
||||
if (rIf != nullptr) {
|
||||
delete rIf;
|
||||
rIf = nullptr;
|
||||
}
|
||||
if (portduino_config.lora_spi_dev == "ch341") {
|
||||
if (ch341Hal != nullptr) {
|
||||
delete ch341Hal;
|
||||
@@ -1162,9 +1169,8 @@ void loop()
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
auto rIf = initLoRa();
|
||||
if (rIf) {
|
||||
router->addInterface(std::move(rIf));
|
||||
if (initLoRa()) {
|
||||
router->addInterface(rIf);
|
||||
portduino_status.LoRa_in_error = false;
|
||||
} else {
|
||||
LOG_WARN("Reconfigure failed, rebooting");
|
||||
|
||||
@@ -33,6 +33,7 @@ extern ScanI2C::DeviceAddress cardkb_found;
|
||||
extern uint8_t kb_model;
|
||||
extern bool kb_found;
|
||||
extern bool osk_found;
|
||||
extern unsigned long last_listen;
|
||||
extern ScanI2C::DeviceAddress rtc_found;
|
||||
extern ScanI2C::DeviceAddress accelerometer_found;
|
||||
extern ScanI2C::FoundDevice rgb_found;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "CryptoEngine.h"
|
||||
// #include "NodeDB.h"
|
||||
#include "architecture.h"
|
||||
#include <memory>
|
||||
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
#include "NodeDB.h"
|
||||
@@ -170,9 +169,10 @@ void CryptoEngine::hash(uint8_t *bytes, size_t numBytes)
|
||||
|
||||
void CryptoEngine::aesSetKey(const uint8_t *key_bytes, size_t key_len)
|
||||
{
|
||||
delete aes;
|
||||
aes = nullptr;
|
||||
if (key_len != 0) {
|
||||
aes = std::unique_ptr<AESSmall256>(new AESSmall256());
|
||||
aes = new AESSmall256();
|
||||
aes->setKey(key_bytes, key_len);
|
||||
}
|
||||
}
|
||||
@@ -231,11 +231,12 @@ void CryptoEngine::decrypt(uint32_t fromNode, uint64_t packetId, size_t numBytes
|
||||
// Generic implementation of AES-CTR encryption.
|
||||
void CryptoEngine::encryptAESCtr(CryptoKey _key, uint8_t *_nonce, size_t numBytes, uint8_t *bytes)
|
||||
{
|
||||
std::unique_ptr<CTRCommon> ctr;
|
||||
delete ctr;
|
||||
ctr = nullptr;
|
||||
if (_key.length == 16)
|
||||
ctr = std::unique_ptr<CTRCommon>(new CTR<AES128>());
|
||||
ctr = new CTR<AES128>();
|
||||
else
|
||||
ctr = std::unique_ptr<CTRCommon>(new CTR<AES256>());
|
||||
ctr = new CTR<AES256>();
|
||||
ctr->setKey(_key.bytes, _key.length);
|
||||
static uint8_t scratch[MAX_BLOCKSIZE];
|
||||
memcpy(scratch, bytes, numBytes);
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "configuration.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include <Arduino.h>
|
||||
#include <memory>
|
||||
|
||||
extern concurrency::Lock *cryptLock;
|
||||
|
||||
@@ -49,7 +48,7 @@ class CryptoEngine
|
||||
virtual void aesSetKey(const uint8_t *key, size_t key_len);
|
||||
|
||||
virtual void aesEncrypt(uint8_t *in, uint8_t *out);
|
||||
std::unique_ptr<AESSmall256> aes = nullptr;
|
||||
AESSmall256 *aes = NULL;
|
||||
|
||||
#endif
|
||||
|
||||
@@ -78,6 +77,7 @@ class CryptoEngine
|
||||
/** Our per packet nonce */
|
||||
uint8_t nonce[16] = {0};
|
||||
CryptoKey key = {};
|
||||
CTRCommon *ctr = NULL;
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
uint8_t shared_key[32] = {0};
|
||||
uint8_t private_key[32] = {0};
|
||||
|
||||
@@ -263,7 +263,6 @@ template <typename T> void LR11x0Interface<T>::startReceive()
|
||||
|
||||
// Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits
|
||||
enableInterrupt(isrRxLevel0);
|
||||
checkRxDoneIrqFlag();
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
+5
-7
@@ -814,28 +814,26 @@ void NodeDB::installDefaultModuleConfig()
|
||||
moduleConfig.has_store_forward = true;
|
||||
moduleConfig.has_telemetry = true;
|
||||
moduleConfig.has_external_notification = true;
|
||||
#if defined(PIN_BUZZER) || defined(PIN_VIBRATION) || defined(LED_NOTIFICATION)
|
||||
moduleConfig.external_notification.enabled = true;
|
||||
#endif
|
||||
#if defined(PIN_BUZZER)
|
||||
moduleConfig.external_notification.enabled = true;
|
||||
moduleConfig.external_notification.output_buzzer = PIN_BUZZER;
|
||||
moduleConfig.external_notification.use_pwm = true;
|
||||
moduleConfig.external_notification.alert_message_buzzer = true;
|
||||
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
|
||||
#endif
|
||||
#if defined(PIN_VIBRATION)
|
||||
moduleConfig.external_notification.enabled = true;
|
||||
moduleConfig.external_notification.output_vibra = PIN_VIBRATION;
|
||||
moduleConfig.external_notification.alert_message_vibra = true;
|
||||
moduleConfig.external_notification.output_ms = 500;
|
||||
moduleConfig.external_notification.nag_timeout = 2;
|
||||
#endif
|
||||
#if defined(LED_NOTIFICATION)
|
||||
moduleConfig.external_notification.enabled = true;
|
||||
moduleConfig.external_notification.output = LED_NOTIFICATION;
|
||||
moduleConfig.external_notification.active = LED_STATE_ON;
|
||||
moduleConfig.external_notification.alert_message = true;
|
||||
moduleConfig.external_notification.output_ms = 1000;
|
||||
#endif
|
||||
#if defined(PIN_VIBRATION)
|
||||
moduleConfig.external_notification.nag_timeout = 2;
|
||||
#elif defined(PIN_BUZZER) || defined(LED_NOTIFICATION)
|
||||
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
|
||||
#endif
|
||||
|
||||
|
||||
@@ -439,7 +439,7 @@ void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, cons
|
||||
}
|
||||
|
||||
// Getters and setters for hop limit fields packed in hop_limit
|
||||
inline uint8_t PacketHistory::getHighestHopLimit(const PacketRecord &r)
|
||||
inline uint8_t PacketHistory::getHighestHopLimit(PacketRecord &r)
|
||||
{
|
||||
return r.hop_limit & HOP_LIMIT_HIGHEST_MASK;
|
||||
}
|
||||
@@ -449,7 +449,7 @@ inline void PacketHistory::setHighestHopLimit(PacketRecord &r, uint8_t hopLimit)
|
||||
r.hop_limit = (r.hop_limit & ~HOP_LIMIT_HIGHEST_MASK) | (hopLimit & HOP_LIMIT_HIGHEST_MASK);
|
||||
}
|
||||
|
||||
inline uint8_t PacketHistory::getOurTxHopLimit(const PacketRecord &r)
|
||||
inline uint8_t PacketHistory::getOurTxHopLimit(PacketRecord &r)
|
||||
{
|
||||
return (r.hop_limit & HOP_LIMIT_OUR_TX_MASK) >> HOP_LIMIT_OUR_TX_SHIFT;
|
||||
}
|
||||
|
||||
@@ -43,9 +43,9 @@ class PacketHistory
|
||||
* @return true if node was indeed a relayer, false if not */
|
||||
bool wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole = nullptr);
|
||||
|
||||
uint8_t getHighestHopLimit(const PacketRecord &r);
|
||||
uint8_t getHighestHopLimit(PacketRecord &r);
|
||||
void setHighestHopLimit(PacketRecord &r, uint8_t hopLimit);
|
||||
uint8_t getOurTxHopLimit(const PacketRecord &r);
|
||||
uint8_t getOurTxHopLimit(PacketRecord &r);
|
||||
void setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit);
|
||||
|
||||
PacketHistory(const PacketHistory &); // non construction-copyable
|
||||
|
||||
@@ -301,7 +301,6 @@ void RF95Interface::startReceive()
|
||||
|
||||
// Must be done AFTER, starting receive, because startReceive clears (possibly stale) interrupt pending register bits
|
||||
enableInterrupt(isrRxLevel0);
|
||||
checkRxDoneIrqFlag();
|
||||
}
|
||||
|
||||
bool RF95Interface::isChannelActive()
|
||||
|
||||
+63
-55
@@ -227,19 +227,23 @@ static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1];
|
||||
// Global LoRa radio type
|
||||
LoRaRadioType radioType = NO_RADIO;
|
||||
|
||||
extern RadioInterface *rIf;
|
||||
extern RadioLibHal *RadioLibHAL;
|
||||
#if defined(HW_SPI1_DEVICE) && defined(ARCH_ESP32)
|
||||
extern SPIClass SPI1;
|
||||
#endif
|
||||
|
||||
std::unique_ptr<RadioInterface> initLoRa()
|
||||
bool initLoRa()
|
||||
{
|
||||
std::unique_ptr<RadioInterface> rIf = nullptr;
|
||||
if (rIf != nullptr) {
|
||||
delete rIf;
|
||||
rIf = nullptr;
|
||||
}
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
SPISettings loraSpiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0);
|
||||
SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0);
|
||||
#else
|
||||
SPISettings loraSpiSettings(4000000, MSBFIRST, SPI_MODE0);
|
||||
SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0);
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_PORTDUINO
|
||||
@@ -248,26 +252,26 @@ std::unique_ptr<RadioInterface> initLoRa()
|
||||
RADIOLIB_PIN_TYPE busy) {
|
||||
switch (portduino_config.lora_module) {
|
||||
case use_rf95:
|
||||
return std::unique_ptr<RadioInterface>(new RF95Interface(hal, cs, irq, rst, busy));
|
||||
return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy);
|
||||
case use_sx1262:
|
||||
return std::unique_ptr<RadioInterface>(new SX1262Interface(hal, cs, irq, rst, busy));
|
||||
return (RadioInterface *)new SX1262Interface(hal, cs, irq, rst, busy);
|
||||
case use_sx1268:
|
||||
return std::unique_ptr<RadioInterface>(new SX1268Interface(hal, cs, irq, rst, busy));
|
||||
return (RadioInterface *)new SX1268Interface(hal, cs, irq, rst, busy);
|
||||
case use_sx1280:
|
||||
return std::unique_ptr<RadioInterface>(new SX1280Interface(hal, cs, irq, rst, busy));
|
||||
return (RadioInterface *)new SX1280Interface(hal, cs, irq, rst, busy);
|
||||
case use_lr1110:
|
||||
return std::unique_ptr<RadioInterface>(new LR1110Interface(hal, cs, irq, rst, busy));
|
||||
return (RadioInterface *)new LR1110Interface(hal, cs, irq, rst, busy);
|
||||
case use_lr1120:
|
||||
return std::unique_ptr<RadioInterface>(new LR1120Interface(hal, cs, irq, rst, busy));
|
||||
return (RadioInterface *)new LR1120Interface(hal, cs, irq, rst, busy);
|
||||
case use_lr1121:
|
||||
return std::unique_ptr<RadioInterface>(new LR1121Interface(hal, cs, irq, rst, busy));
|
||||
return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy);
|
||||
case use_llcc68:
|
||||
return std::unique_ptr<RadioInterface>(new LLCC68Interface(hal, cs, irq, rst, busy));
|
||||
return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy);
|
||||
case use_simradio:
|
||||
return std::unique_ptr<RadioInterface>(new SimRadio);
|
||||
return (RadioInterface *)new SimRadio;
|
||||
default:
|
||||
assert(0); // shouldn't happen
|
||||
return std::unique_ptr<RadioInterface>(nullptr);
|
||||
return (RadioInterface *)nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -280,7 +284,7 @@ std::unique_ptr<RadioInterface> initLoRa()
|
||||
delete RadioLibHAL;
|
||||
RadioLibHAL = nullptr;
|
||||
}
|
||||
RadioLibHAL = new LockingArduinoHal(SPI, loraSpiSettings);
|
||||
RadioLibHAL = new LockingArduinoHal(SPI, spiSettings);
|
||||
}
|
||||
rIf =
|
||||
loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin,
|
||||
@@ -288,28 +292,27 @@ std::unique_ptr<RadioInterface> initLoRa()
|
||||
|
||||
if (!rIf->init()) {
|
||||
LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str());
|
||||
rIf = nullptr;
|
||||
delete rIf;
|
||||
rIf = NULL;
|
||||
exit(EXIT_FAILURE);
|
||||
} else {
|
||||
LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str());
|
||||
}
|
||||
|
||||
#elif defined(HW_SPI1_DEVICE)
|
||||
LockingArduinoHal *loraHal = new LockingArduinoHal(SPI1, loraSpiSettings);
|
||||
RadioLibHAL = loraHal;
|
||||
LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings);
|
||||
#else // HW_SPI1_DEVICE
|
||||
LockingArduinoHal *loraHal = new LockingArduinoHal(SPI, loraSpiSettings);
|
||||
RadioLibHAL = loraHal;
|
||||
LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings);
|
||||
#endif
|
||||
|
||||
// radio init MUST BE AFTER service.init, so we have our radio config settings (from nodedb init)
|
||||
#if defined(USE_STM32WLx)
|
||||
if (!rIf) {
|
||||
rIf = std::unique_ptr<STM32WLE5JCInterface>(
|
||||
new STM32WLE5JCInterface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
|
||||
rIf = new STM32WLE5JCInterface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
||||
if (!rIf->init()) {
|
||||
LOG_WARN("No STM32WL radio");
|
||||
rIf = nullptr;
|
||||
delete rIf;
|
||||
rIf = NULL;
|
||||
} else {
|
||||
LOG_INFO("STM32WL init success");
|
||||
radioType = STM32WLx_RADIO;
|
||||
@@ -319,10 +322,11 @@ std::unique_ptr<RadioInterface> initLoRa()
|
||||
|
||||
#if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1
|
||||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||||
rIf = std::unique_ptr<RF95Interface>(new RF95Interface(loraHal, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1));
|
||||
rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1);
|
||||
if (!rIf->init()) {
|
||||
LOG_WARN("No RF95 radio");
|
||||
rIf = nullptr;
|
||||
delete rIf;
|
||||
rIf = NULL;
|
||||
} else {
|
||||
LOG_INFO("RF95 init success");
|
||||
radioType = RF95_RADIO;
|
||||
@@ -332,17 +336,17 @@ std::unique_ptr<RadioInterface> initLoRa()
|
||||
|
||||
#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && !defined(TCXO_OPTIONAL) && RADIOLIB_EXCLUDE_SX126X != 1
|
||||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||||
auto sxIf =
|
||||
std::unique_ptr<SX1262Interface>(new SX1262Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
|
||||
auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
||||
#ifdef SX126X_DIO3_TCXO_VOLTAGE
|
||||
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
|
||||
#endif
|
||||
if (!sxIf->init()) {
|
||||
LOG_WARN("No SX1262 radio");
|
||||
rIf = nullptr;
|
||||
delete sxIf;
|
||||
rIf = NULL;
|
||||
} else {
|
||||
LOG_INFO("SX1262 init success");
|
||||
rIf = std::move(sxIf);
|
||||
rIf = sxIf;
|
||||
radioType = SX1262_RADIO;
|
||||
}
|
||||
}
|
||||
@@ -351,25 +355,26 @@ std::unique_ptr<RadioInterface> initLoRa()
|
||||
#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && defined(TCXO_OPTIONAL)
|
||||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||||
// try using the specified TCXO voltage
|
||||
auto sxIf =
|
||||
std::unique_ptr<SX1262Interface>(new SX1262Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
|
||||
auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
||||
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
|
||||
if (!sxIf->init()) {
|
||||
LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
|
||||
rIf = nullptr;
|
||||
delete sxIf;
|
||||
rIf = NULL;
|
||||
} else {
|
||||
LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
|
||||
rIf = std::move(sxIf);
|
||||
rIf = sxIf;
|
||||
radioType = SX1262_RADIO;
|
||||
}
|
||||
}
|
||||
|
||||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||||
// If specified TCXO voltage fails, attempt to use DIO3 as a reference instead
|
||||
rIf = std::unique_ptr<SX1262Interface>(new SX1262Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
|
||||
rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
||||
if (!rIf->init()) {
|
||||
LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V");
|
||||
rIf = nullptr;
|
||||
delete rIf;
|
||||
rIf = NULL;
|
||||
} else {
|
||||
LOG_INFO("SX1262 init success, XTAL, Vref 0.0V");
|
||||
radioType = SX1262_RADIO;
|
||||
@@ -381,24 +386,25 @@ std::unique_ptr<RadioInterface> initLoRa()
|
||||
#if defined(SX126X_DIO3_TCXO_VOLTAGE) && defined(TCXO_OPTIONAL)
|
||||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||||
// try using the specified TCXO voltage
|
||||
auto sxIf =
|
||||
std::unique_ptr<SX1268Interface>(new SX1268Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
|
||||
auto *sxIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
||||
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
|
||||
if (!sxIf->init()) {
|
||||
LOG_WARN("No SX1268 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
|
||||
rIf = nullptr;
|
||||
delete sxIf;
|
||||
rIf = NULL;
|
||||
} else {
|
||||
LOG_INFO("SX1268 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
|
||||
rIf = std::move(sxIf);
|
||||
rIf = sxIf;
|
||||
radioType = SX1268_RADIO;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||||
rIf = std::unique_ptr<SX1268Interface>(new SX1268Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
|
||||
rIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
||||
if (!rIf->init()) {
|
||||
LOG_WARN("No SX1268 radio");
|
||||
rIf = nullptr;
|
||||
delete rIf;
|
||||
rIf = NULL;
|
||||
} else {
|
||||
LOG_INFO("SX1268 init success");
|
||||
radioType = SX1268_RADIO;
|
||||
@@ -408,10 +414,11 @@ std::unique_ptr<RadioInterface> initLoRa()
|
||||
|
||||
#if defined(USE_LLCC68)
|
||||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||||
rIf = std::unique_ptr<LLCC68Interface>(new LLCC68Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
|
||||
rIf = new LLCC68Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
||||
if (!rIf->init()) {
|
||||
LOG_WARN("No LLCC68 radio");
|
||||
rIf = nullptr;
|
||||
delete rIf;
|
||||
rIf = NULL;
|
||||
} else {
|
||||
LOG_INFO("LLCC68 init success");
|
||||
radioType = LLCC68_RADIO;
|
||||
@@ -421,11 +428,11 @@ std::unique_ptr<RadioInterface> initLoRa()
|
||||
|
||||
#if defined(USE_LR1110) && RADIOLIB_EXCLUDE_LR11X0 != 1
|
||||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||||
rIf = std::unique_ptr<LR1110Interface>(
|
||||
new LR1110Interface(loraHal, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN));
|
||||
rIf = new LR1110Interface(RadioLibHAL, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN);
|
||||
if (!rIf->init()) {
|
||||
LOG_WARN("No LR1110 radio");
|
||||
rIf = nullptr;
|
||||
delete rIf;
|
||||
rIf = NULL;
|
||||
} else {
|
||||
LOG_INFO("LR1110 init success");
|
||||
radioType = LR1110_RADIO;
|
||||
@@ -435,11 +442,11 @@ std::unique_ptr<RadioInterface> initLoRa()
|
||||
|
||||
#if defined(USE_LR1120) && RADIOLIB_EXCLUDE_LR11X0 != 1
|
||||
if (!rIf) {
|
||||
rIf = std::unique_ptr<LR1120Interface>(
|
||||
new LR1120Interface(loraHal, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN));
|
||||
rIf = new LR1120Interface(RadioLibHAL, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN);
|
||||
if (!rIf->init()) {
|
||||
LOG_WARN("No LR1120 radio");
|
||||
rIf = nullptr;
|
||||
delete rIf;
|
||||
rIf = NULL;
|
||||
} else {
|
||||
LOG_INFO("LR1120 init success");
|
||||
radioType = LR1120_RADIO;
|
||||
@@ -449,11 +456,11 @@ std::unique_ptr<RadioInterface> initLoRa()
|
||||
|
||||
#if defined(USE_LR1121) && RADIOLIB_EXCLUDE_LR11X0 != 1
|
||||
if (!rIf) {
|
||||
rIf = std::unique_ptr<LR1121Interface>(
|
||||
new LR1121Interface(loraHal, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN));
|
||||
rIf = new LR1121Interface(RadioLibHAL, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN);
|
||||
if (!rIf->init()) {
|
||||
LOG_WARN("No LR1121 radio");
|
||||
rIf = nullptr;
|
||||
delete rIf;
|
||||
rIf = NULL;
|
||||
} else {
|
||||
LOG_INFO("LR1121 init success");
|
||||
radioType = LR1121_RADIO;
|
||||
@@ -463,10 +470,11 @@ std::unique_ptr<RadioInterface> initLoRa()
|
||||
|
||||
#if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1
|
||||
if (!rIf) {
|
||||
rIf = std::unique_ptr<SX1280Interface>(new SX1280Interface(loraHal, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY));
|
||||
rIf = new SX1280Interface(RadioLibHAL, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY);
|
||||
if (!rIf->init()) {
|
||||
LOG_WARN("No SX1280 radio");
|
||||
rIf = nullptr;
|
||||
delete rIf;
|
||||
rIf = NULL;
|
||||
} else {
|
||||
LOG_INFO("SX1280 init success");
|
||||
radioType = SX1280_RADIO;
|
||||
@@ -488,7 +496,7 @@ std::unique_ptr<RadioInterface> initLoRa()
|
||||
rebootAtMsec = millis() + 5000;
|
||||
}
|
||||
}
|
||||
return rIf;
|
||||
return rIf != nullptr;
|
||||
}
|
||||
|
||||
void initRegion()
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "PointerQueue.h"
|
||||
#include "airtime.h"
|
||||
#include "error.h"
|
||||
#include <memory>
|
||||
|
||||
// Forward decl to avoid a direct include of generated config headers / full LoRaConfig definition in this widely-included file.
|
||||
typedef struct _meshtastic_Config_LoRaConfig meshtastic_Config_LoRaConfig;
|
||||
@@ -280,7 +279,7 @@ class RadioInterface
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<RadioInterface> initLoRa();
|
||||
bool initLoRa();
|
||||
|
||||
/// Debug printing for packets
|
||||
void printPacket(const char *prefix, const meshtastic_MeshPacket *p);
|
||||
|
||||
@@ -517,26 +517,12 @@ void RadioLibInterface::handleReceiveInterrupt()
|
||||
|
||||
void RadioLibInterface::startReceive()
|
||||
{
|
||||
// Note the updated timestamp, to avoid unneeded AGC resets
|
||||
last_listen = millis();
|
||||
isReceiving = true;
|
||||
powerMon->setState(meshtastic_PowerMon_State_Lora_RXOn);
|
||||
}
|
||||
|
||||
void RadioLibInterface::pollMissedIrqs()
|
||||
{
|
||||
// RadioLibInterface::enableInterrupt uses EDGE-TRIGGERED interrupts. Poll as a backup to catch missed edges.
|
||||
if (isReceiving) {
|
||||
checkRxDoneIrqFlag();
|
||||
}
|
||||
}
|
||||
|
||||
void RadioLibInterface::checkRxDoneIrqFlag()
|
||||
{
|
||||
if (iface->checkIrq(RADIOLIB_IRQ_RX_DONE)) {
|
||||
LOG_WARN("caught missed RX_DONE");
|
||||
notify(ISR_RX, true);
|
||||
}
|
||||
}
|
||||
|
||||
void RadioLibInterface::configHardwareForSend()
|
||||
{
|
||||
powerMon->setState(meshtastic_PowerMon_State_Lora_TXOn);
|
||||
|
||||
@@ -112,11 +112,6 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
*/
|
||||
virtual void enableInterrupt(void (*)()) = 0;
|
||||
|
||||
/**
|
||||
* Poll as a backup to catch missed edge-triggered interrupts.
|
||||
*/
|
||||
void pollMissedIrqs();
|
||||
|
||||
/**
|
||||
* Debugging counts
|
||||
*/
|
||||
@@ -269,6 +264,4 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
*/
|
||||
|
||||
bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) override;
|
||||
|
||||
void checkRxDoneIrqFlag();
|
||||
};
|
||||
|
||||
+2
-3
@@ -8,7 +8,6 @@
|
||||
#include "PointerQueue.h"
|
||||
#include "RadioInterface.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include <memory>
|
||||
|
||||
/**
|
||||
* A mesh aware router that supports multiple interfaces.
|
||||
@@ -21,7 +20,7 @@ class Router : protected concurrency::OSThread, protected PacketHistory
|
||||
PointerQueue<meshtastic_MeshPacket> fromRadioQueue;
|
||||
|
||||
protected:
|
||||
std::unique_ptr<RadioInterface> iface = nullptr;
|
||||
RadioInterface *iface = NULL;
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -33,7 +32,7 @@ class Router : protected concurrency::OSThread, protected PacketHistory
|
||||
/**
|
||||
* Currently we only allow one interface, that may change in the future
|
||||
*/
|
||||
void addInterface(std::unique_ptr<RadioInterface> _iface) { iface = std::move(_iface); }
|
||||
void addInterface(RadioInterface *_iface) { iface = _iface; }
|
||||
|
||||
/**
|
||||
* do idle processing
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
#ifdef ARCH_PORTDUINO
|
||||
#include "PortduinoGlue.h"
|
||||
#endif
|
||||
#if defined(USE_GC1109_PA) && defined(ARCH_ESP32)
|
||||
#include <driver/rtc_io.h>
|
||||
#include <esp_sleep.h>
|
||||
#endif
|
||||
|
||||
#include "Throttle.h"
|
||||
|
||||
@@ -59,33 +55,14 @@ template <typename T> bool SX126xInterface<T>::init()
|
||||
#if defined(USE_GC1109_PA)
|
||||
// GC1109 FEM chip initialization
|
||||
// See variant.h for full pin mapping and control logic documentation
|
||||
//
|
||||
// On deep sleep wake, PA_POWER and PA_EN are held HIGH by RTC latch (set in
|
||||
// enableLoraInterrupt). We configure GPIO registers before releasing the hold
|
||||
// so the pad transitions atomically from held-HIGH to register-HIGH with no
|
||||
// power glitch. On cold boot the hold_dis is a harmless no-op.
|
||||
|
||||
// VFEM_Ctrl (LORA_PA_POWER): Power enable for GC1109 LDO (always on)
|
||||
pinMode(LORA_PA_POWER, OUTPUT);
|
||||
digitalWrite(LORA_PA_POWER, HIGH);
|
||||
rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER);
|
||||
|
||||
// TLV75733P LDO has ~550us startup time (datasheet tSTR). On cold boot, wait
|
||||
// for VBAT to stabilise before driving CSD/CPS, per GC1109 requirement:
|
||||
// "VBAT must be prior to CSD/CPS/CTX for the power on sequence"
|
||||
// On deep sleep wake the LDO was held on via RTC latch, so no delay needed.
|
||||
#if defined(ARCH_ESP32)
|
||||
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_UNDEFINED) {
|
||||
delayMicroseconds(1000);
|
||||
}
|
||||
#else
|
||||
delayMicroseconds(1000);
|
||||
#endif
|
||||
|
||||
// CSD (LORA_PA_EN): Chip enable - must be HIGH to enable GC1109 for both RX and TX
|
||||
pinMode(LORA_PA_EN, OUTPUT);
|
||||
digitalWrite(LORA_PA_EN, HIGH);
|
||||
rtc_gpio_hold_dis((gpio_num_t)LORA_PA_EN);
|
||||
|
||||
// CPS (LORA_PA_TX_EN): PA mode select - HIGH enables full PA during TX, LOW for RX (don't care)
|
||||
// Note: TX/RX path switching (CTX) is handled by DIO2 via SX126X_DIO2_AS_RF_SWITCH
|
||||
@@ -194,17 +171,6 @@ template <typename T> bool SX126xInterface<T>::init()
|
||||
LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", result);
|
||||
}
|
||||
|
||||
#ifdef USE_GC1109_PA
|
||||
// Undocumented SX1262 register patch recommended by Heltec/Semtech for improved RX sensitivity
|
||||
// on boards with the GC1109 FEM. Sets bit 0 of register 0x8B5.
|
||||
// Reference: https://github.com/meshcore-dev/MeshCore/pull/1398
|
||||
if (module.SPIsetRegValue(0x8B5, 0x01, 0, 0) == RADIOLIB_ERR_NONE) {
|
||||
LOG_INFO("Applied SX1262 register 0x8B5 patch for GC1109 RX improvement");
|
||||
} else {
|
||||
LOG_WARN("Failed to apply SX1262 register 0x8B5 patch for GC1109");
|
||||
}
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
// Read/write a register we are not using (only used for FSK mode) to test SPI comms
|
||||
uint8_t crcLSB = 0;
|
||||
@@ -362,7 +328,6 @@ template <typename T> void SX126xInterface<T>::startReceive()
|
||||
|
||||
// Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits
|
||||
enableInterrupt(isrRxLevel0);
|
||||
checkRxDoneIrqFlag();
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -271,7 +271,6 @@ template <typename T> void SX128xInterface<T>::startReceive()
|
||||
|
||||
// Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits
|
||||
enableInterrupt(isrRxLevel0);
|
||||
checkRxDoneIrqFlag();
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen)
|
||||
/**
|
||||
* Read any rx chars from the link and call handleRecStream
|
||||
*/
|
||||
int32_t StreamAPI::readStream(const char *buf, uint16_t bufLen)
|
||||
int32_t StreamAPI::readStream(char *buf, uint16_t bufLen)
|
||||
{
|
||||
if (bufLen < 1) {
|
||||
// Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time
|
||||
@@ -56,7 +56,7 @@ void StreamAPI::writeStream()
|
||||
}
|
||||
}
|
||||
|
||||
int32_t StreamAPI::handleRecStream(const char *buf, uint16_t bufLen)
|
||||
int32_t StreamAPI::handleRecStream(char *buf, uint16_t bufLen)
|
||||
{
|
||||
uint16_t index = 0;
|
||||
while (bufLen > index) { // Currently we never want to block
|
||||
|
||||
@@ -57,8 +57,8 @@ class StreamAPI : public PhoneAPI
|
||||
* Read any rx chars from the link and call handleToRadio
|
||||
*/
|
||||
int32_t readStream();
|
||||
int32_t readStream(const char *buf, uint16_t bufLen);
|
||||
int32_t handleRecStream(const char *buf, uint16_t bufLen);
|
||||
int32_t readStream(char *buf, uint16_t bufLen);
|
||||
int32_t handleRecStream(char *buf, uint16_t bufLen);
|
||||
|
||||
/**
|
||||
* call getFromRadio() and deliver encapsulated packets to the Stream
|
||||
|
||||
@@ -33,7 +33,7 @@ static int constant_time_compare(const void *a_, const void *b_, size_t len)
|
||||
d |= (a[i] ^ b[i]);
|
||||
}
|
||||
/* Constant time bit arithmetic to convert d > 0 to -1 and d = 0 to 0. */
|
||||
return (1 & (((unsigned int)d - 1) >> 8)) - 1;
|
||||
return (1 & ((d - 1) >> 8)) - 1;
|
||||
}
|
||||
|
||||
static void WPA_PUT_BE16(uint8_t *a, uint16_t val)
|
||||
|
||||
@@ -17,7 +17,7 @@ class PacketAPI : public PhoneAPI, public concurrency::OSThread
|
||||
virtual int32_t runOnce();
|
||||
|
||||
protected:
|
||||
explicit PacketAPI(PacketServer *_server);
|
||||
PacketAPI(PacketServer *_server);
|
||||
// Check the current underlying physical queue to see if the client is fetching packets
|
||||
bool checkIsConnected() override;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user