Compare commits

..
Author SHA1 Message Date
Thomas Göttgens 5a3afc756a Merge branch 'master' into 128-redux 2026-02-12 17:09:56 +01:00
Ben Meadors 9d222c9a55 Merge branch 'master' into 128-redux 2025-08-20 10:10:19 -05:00
Ben Meadors 2dd4ca8c52 Line 2025-08-20 06:01:21 -05:00
Ben Meadors 2a50ae05dd Account for 128x128 SH1107 displays 2025-08-20 06:00:24 -05:00
193 changed files with 1047 additions and 8060 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ ENV PIP_ROOT_USER_ACTION=ignore
RUN apt-get update && apt-get install --no-install-recommends -y \
cmake git zip libgpiod-dev libbluetooth-dev libi2c-dev \
libunistring-dev libmicrohttpd-dev libgnutls28-dev libgcrypt20-dev \
libusb-1.0-0-dev libssl-dev pkg-config libsqlite3-dev libsdl2-dev && \
libusb-1.0-0-dev libssl-dev pkg-config libsqlite3-dev && \
apt-get clean && rm -rf /var/lib/apt/lists/* && \
pip install --no-cache-dir -U \
platformio==6.1.16 \
+2 -2
View File
@@ -9,14 +9,14 @@ plugins:
lint:
enabled:
- checkov@3.2.501
- renovate@43.10.3
- renovate@43.8.5
- prettier@3.8.1
- trufflehog@3.93.3
- yamllint@1.38.0
- bandit@1.9.3
- trivy@0.69.1
- taplo@0.10.0
- ruff@0.15.1
- ruff@0.15.0
- isort@7.0.0
- markdownlint@0.47.0
- oxipng@10.1.0
+2 -2
View File
@@ -14,7 +14,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
curl wget g++ zip git ca-certificates pkg-config \
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
&& pip install --no-cache-dir -U platformio \
&& mkdir /tmp/firmware
@@ -53,7 +53,7 @@ USER root
RUN apt-get update && apt-get --no-install-recommends -y install \
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \
liborcania2.3 libulfius2.7t64 libssl3t64 \
libx11-6 libinput10 libxkbcommon-x11-0 libsdl2-2.0-0 \
libx11-6 libinput10 libxkbcommon-x11-0 \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
&& mkdir -p /var/lib/meshtasticd \
&& mkdir -p /etc/meshtasticd/config.d \
+2 -2
View File
@@ -11,7 +11,7 @@ RUN apk --no-cache add \
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
libgpiod-dev yaml-cpp-dev bluez-dev \
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev \
&& rm -rf /var/cache/apk/* \
&& pip install --no-cache-dir -U platformio \
&& mkdir /tmp/firmware
@@ -42,7 +42,7 @@ USER root
RUN apk --no-cache add \
shadow libstdc++ libbsd libgpiod yaml-cpp libusb \
i2c-tools libuv libx11 libinput libxkbcommon sdl2 \
i2c-tools libuv libx11 libinput libxkbcommon \
&& rm -rf /var/cache/apk/* \
&& mkdir -p /var/lib/meshtasticd \
&& mkdir -p /etc/meshtasticd/config.d \
+1 -2
View File
@@ -26,8 +26,7 @@ Build-Depends: debhelper-compat (= 13),
libx11-dev,
libinput-dev,
libxkbcommon-x11-dev,
libsqlite3-dev,
libsdl2-dev
libsqlite3-dev
Standards-Version: 4.6.2
Homepage: https://github.com/meshtastic/firmware
Rules-Requires-Root: no
+1 -9
View File
@@ -49,7 +49,6 @@ BuildRequires: pkgconfig(libulfius)
BuildRequires: pkgconfig(x11)
BuildRequires: pkgconfig(libinput)
BuildRequires: pkgconfig(xkbcommon-x11)
BuildRequires: pkgconfig(sdl2)
# libbsd is needed on older Fedora/RHEL to provide 'strlcpy'
%if 0%{?fedora} >= 39 || 0%{?rhel} >= 10
@@ -60,14 +59,8 @@ BuildRequires: pkgconfig(libbsd-overlay)
Requires: systemd-udev
# Declare that this package provides the user/group it creates in %pre
# Required for Fedora 43+ which tracks users/groups as RPM dependencies
Provides: user(%{meshtasticd_user})
Provides: group(%{meshtasticd_user})
Provides: group(spi)
%description
Meshtastic daemon. Meshtastic is an off-grid
Meshtastic daemon for controlling Meshtastic devices. Meshtastic is an off-grid
text communication platform that uses inexpensive LoRa radios.
%prep
@@ -158,7 +151,6 @@ fi
%license LICENSE
%doc README.md
%{_bindir}/meshtasticd
%{_bindir}/meshtasticd-start.sh
%dir %{_localstatedir}/lib/meshtasticd
%{_udevrulesdir}/99-meshtasticd-udev.rules
%dir %{_sysconfdir}/meshtasticd
-30
View File
@@ -1,30 +0,0 @@
# 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
-149
View File
@@ -1,149 +0,0 @@
# 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
-22
View File
@@ -1,22 +0,0 @@
<!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>
-2211
View File
File diff suppressed because it is too large Load Diff
-54
View File
@@ -1,54 +0,0 @@
{
"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
-82
View File
@@ -1,82 +0,0 @@
/**
* 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,
},
};
-557
View File
@@ -1,557 +0,0 @@
/**
* 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";
-327
View File
@@ -1,327 +0,0 @@
<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>
-467
View File
@@ -1,467 +0,0 @@
/**
* 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),
};
}
-255
View File
@@ -1,255 +0,0 @@
/**
* 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);
}
-627
View File
@@ -1,627 +0,0 @@
/**
* 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);
}
-421
View File
@@ -1,421 +0,0 @@
/**
* 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;
-50
View File
@@ -1,50 +0,0 @@
/**
* 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";
-4
View File
@@ -1,4 +0,0 @@
import { createApp } from "vue";
import Demo from "./Demo.vue";
createApp(Demo).mount("#app");
-28
View File
@@ -1,28 +0,0 @@
{
"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"]
}
-35
View File
@@ -1,35 +0,0 @@
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"),
},
},
});
-6
View File
@@ -50,13 +50,11 @@ build_flags = -Wno-missing-field-initializers
-DRADIOLIB_EXCLUDE_APRS=1
-DRADIOLIB_EXCLUDE_LORAWAN=1
-DMESHTASTIC_EXCLUDE_DROPZONE=1
-DMESHTASTIC_EXCLUDE_REPLYBOT=1
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
-DMESHTASTIC_EXCLUDE_POWERMON=1
-DMESHTASTIC_EXCLUDE_STATUS=1
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
-DLED_BUILTIN=-1
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
@@ -215,8 +213,6 @@ 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
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]
@@ -245,5 +241,3 @@ 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
sensirion/Sensirion I2C SFA3x@1.0.0
+59 -66
View File
@@ -1,21 +1,19 @@
#ifndef AMBIENTLIGHTINGTHREAD_H
#define AMBIENTLIGHTINGTHREAD_H
#include "Observer.h"
#include "configuration.h"
#include "detect/ScanI2C.h"
#include "sleep.h"
#ifdef HAS_NCP5623
#include <NCP5623.h>
#include <graphics/RAKled.h>
NCP5623 rgb;
#endif
#ifdef HAS_LP5562
#include <graphics/NomadStarLED.h>
LP5562 rgbw;
#endif
#ifdef HAS_NEOPIXEL
#include <Adafruit_NeoPixel.h>
#include <graphics/NeoPixel.h>
Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE);
#endif
#ifdef UNPHONE
@@ -23,24 +21,10 @@
extern unPhone unphone;
#endif
namespace concurrency
{
class AmbientLightingThread : public concurrency::OSThread
{
friend class StatusLEDModule; // Let the LEDStatusModule trigger the ambient lighting for notifications and battery status.
friend class ExternalNotificationModule; // Let the ExternalNotificationModule trigger the ambient lighting for notifications.
private:
#ifdef HAS_NCP5623
NCP5623 rgb;
#endif
#ifdef HAS_LP5562
LP5562 rgbw;
#endif
#ifdef HAS_NEOPIXEL
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE);
#endif
public:
explicit AmbientLightingThread(ScanI2C::DeviceType type) : OSThread("AmbientLighting")
{
@@ -52,15 +36,14 @@ class AmbientLightingThread : public concurrency::OSThread
moduleConfig.ambient_lighting.led_state = true;
#endif
#endif
#if AMBIENT_LIGHTING_TEST
// define to enable test
moduleConfig.ambient_lighting.led_state = true;
moduleConfig.ambient_lighting.current = 10;
// Uncomment to test module
// moduleConfig.ambient_lighting.led_state = true;
// moduleConfig.ambient_lighting.current = 10;
// Default to a color based on our node number
moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16;
moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8;
moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF;
#endif
// moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16;
// moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8;
// moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF;
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
_type = type;
if (_type == ScanI2C::DeviceType::NONE) {
@@ -70,6 +53,11 @@ class AmbientLightingThread : public concurrency::OSThread
}
#endif
#ifdef HAS_RGB_LED
if (!moduleConfig.ambient_lighting.led_state) {
LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF");
disable();
return;
}
LOG_DEBUG("AmbientLighting init");
#ifdef HAS_NCP5623
if (_type == ScanI2C::NCP5623) {
@@ -89,13 +77,7 @@ class AmbientLightingThread : public concurrency::OSThread
pixels.clear(); // Set all pixel colors to 'off'
pixels.setBrightness(moduleConfig.ambient_lighting.current);
#endif
if (!moduleConfig.ambient_lighting.led_state) {
LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF");
disable();
return;
}
setLighting(moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
setLighting();
#endif
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
}
@@ -109,8 +91,7 @@ class AmbientLightingThread : public concurrency::OSThread
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) {
#endif
setLighting(moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
setLighting();
return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
}
@@ -167,53 +148,65 @@ class AmbientLightingThread : public concurrency::OSThread
return 0;
}
protected:
void setLighting(float current, uint8_t red, uint8_t green, uint8_t blue)
void setLighting()
{
#ifdef HAS_NCP5623
rgb.setCurrent(current);
rgb.setRed(red);
rgb.setGreen(green);
rgb.setBlue(blue);
LOG_DEBUG("Init NCP5623 Ambient light w/ current=%f, red=%d, green=%d, blue=%d", current, red, green, blue);
rgb.setCurrent(moduleConfig.ambient_lighting.current);
rgb.setRed(moduleConfig.ambient_lighting.red);
rgb.setGreen(moduleConfig.ambient_lighting.green);
rgb.setBlue(moduleConfig.ambient_lighting.blue);
LOG_DEBUG("Init NCP5623 Ambient light w/ current=%d, red=%d, green=%d, blue=%d",
moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
#endif
#ifdef HAS_LP5562
rgbw.setCurrent(current);
rgbw.setRed(red);
rgbw.setGreen(green);
rgbw.setBlue(blue);
LOG_DEBUG("Init LP5562 Ambient light w/ current=%f, red=%d, green=%d, blue=%d", current, red, green, blue);
rgbw.setCurrent(moduleConfig.ambient_lighting.current);
rgbw.setRed(moduleConfig.ambient_lighting.red);
rgbw.setGreen(moduleConfig.ambient_lighting.green);
rgbw.setBlue(moduleConfig.ambient_lighting.blue);
LOG_DEBUG("Init LP5562 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current,
moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
#endif
#ifdef HAS_NEOPIXEL
pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT);
pixels.fill(pixels.Color(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green,
moduleConfig.ambient_lighting.blue),
0, NEOPIXEL_COUNT);
// RadioMaster Bandit has addressable LED at the two buttons
// this allow us to set different lighting for them in variant.h file.
#ifdef RADIOMASTER_900_BANDIT
#if defined(BUTTON1_COLOR) && defined(BUTTON1_COLOR_INDEX)
pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1);
#endif
#if defined(BUTTON2_COLOR) && defined(BUTTON2_COLOR_INDEX)
pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1);
#endif
#endif
pixels.show();
// LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%f, red=%d, green=%d, blue=%d",
// current, red, green, blue);
// LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d",
// moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
// moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
#endif
#ifdef RGBLED_CA
analogWrite(RGBLED_RED, 255 - red);
analogWrite(RGBLED_GREEN, 255 - green);
analogWrite(RGBLED_BLUE, 255 - blue);
LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", red, green, blue);
analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red);
analogWrite(RGBLED_GREEN, 255 - moduleConfig.ambient_lighting.green);
analogWrite(RGBLED_BLUE, 255 - moduleConfig.ambient_lighting.blue);
LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red,
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
#elif defined(RGBLED_RED)
analogWrite(RGBLED_RED, red);
analogWrite(RGBLED_GREEN, green);
analogWrite(RGBLED_BLUE, blue);
LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", red, green, blue);
analogWrite(RGBLED_RED, moduleConfig.ambient_lighting.red);
analogWrite(RGBLED_GREEN, moduleConfig.ambient_lighting.green);
analogWrite(RGBLED_BLUE, moduleConfig.ambient_lighting.blue);
LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red,
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
#endif
#ifdef UNPHONE
unphone.rgb(red, green, blue);
LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", red, green, blue);
unphone.rgb(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green,
moduleConfig.ambient_lighting.blue);
LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red,
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
#endif
}
};
#endif // AMBIENTLIGHTINGTHREAD_H
} // namespace concurrency
+10 -2
View File
@@ -89,14 +89,22 @@ class BluetoothStatus : public Status
case ConnectionState::CONNECTED:
LOG_DEBUG("BluetoothStatus CONNECTED");
#ifdef BLE_LED
digitalWrite(BLE_LED, LED_STATE_ON);
#ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, LOW);
#else
digitalWrite(BLE_LED, HIGH);
#endif
#endif
break;
case ConnectionState::DISCONNECTED:
LOG_DEBUG("BluetoothStatus DISCONNECTED");
#ifdef BLE_LED
digitalWrite(BLE_LED, LED_STATE_OFF);
#ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, HIGH);
#else
digitalWrite(BLE_LED, LOW);
#endif
#endif
break;
}
+66
View File
@@ -0,0 +1,66 @@
#include "Led.h"
#include "PowerMon.h"
#include "main.h"
#include "power.h"
GpioVirtPin ledForceOn, ledBlink;
#if defined(LED_PIN)
// Most boards have a GPIO for LED control
static GpioHwPin ledRawHwPin(LED_PIN);
#else
static GpioVirtPin ledRawHwPin; // Dummy pin for no hardware
#endif
#if LED_STATE_ON == 0
static GpioVirtPin ledHwPin;
static GpioNotTransformer ledInverter(&ledHwPin, &ledRawHwPin);
#else
static GpioPin &ledHwPin = ledRawHwPin;
#endif
#if defined(HAS_PMU)
/**
* A GPIO controlled by the PMU
*/
class GpioPmuPin : public GpioPin
{
public:
void set(bool value)
{
if (pmu_found && PMU) {
// blink the axp led
PMU->setChargingLedMode(value ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF);
}
}
} ledPmuHwPin;
// In some cases we need to drive a PMU LED and a normal LED
static GpioSplitter ledFinalPin(&ledHwPin, &ledPmuHwPin);
#else
static GpioPin &ledFinalPin = ledHwPin;
#endif
#ifdef USE_POWERMON
/**
* We monitor changes to the LED drive output because we use that as a sanity test in our power monitor stuff.
*/
class MonitoredLedPin : public GpioPin
{
public:
void set(bool value)
{
if (powerMon) {
if (value)
powerMon->setState(meshtastic_PowerMon_State_LED_On);
else
powerMon->clearState(meshtastic_PowerMon_State_LED_On);
}
ledFinalPin.set(value);
}
} monitoredLedPin;
#else
static GpioPin &monitoredLedPin = ledFinalPin;
#endif
static GpioBinaryTransformer ledForcer(&ledForceOn, &ledBlink, &monitoredLedPin, GpioBinaryTransformer::Or);
+7
View File
@@ -0,0 +1,7 @@
#include "GpioLogic.h"
#include "configuration.h"
/**
* ledForceOn and ledForceOff both override the normal ledBlinker behavior (which is controlled by main)
*/
extern GpioVirtPin ledForceOn, ledBlink;
+8 -93
View File
@@ -459,8 +459,6 @@ class AnalogBatteryLevel : public HasBatteryLevel
}
// if it's not HIGH - check the battery
#endif
// If we have an EXT_PWR_DETECT pin and it indicates no external power, believe it.
return false;
// technically speaking this should work for all(?) NRF52 boards
// but needs testing across multiple devices. NRF52 USB would not even work if
@@ -692,9 +690,7 @@ bool Power::setup()
bool found = false;
if (axpChipInit()) {
found = true;
} else if (cw2015Init()) {
found = true;
} else if (max17048Init()) {
} else if (lipoInit()) {
found = true;
} else if (lipoChargerInit()) {
found = true;
@@ -1323,7 +1319,7 @@ bool Power::axpChipInit()
/**
* Wrapper class for an I2C MAX17048 Lipo battery sensor.
*/
class MAX17048BatteryLevel : public HasBatteryLevel
class LipoBatteryLevel : public HasBatteryLevel
{
private:
MAX17048Singleton *max17048 = nullptr;
@@ -1371,18 +1367,18 @@ class MAX17048BatteryLevel : public HasBatteryLevel
virtual bool isCharging() override { return max17048->isBatteryCharging(); }
};
MAX17048BatteryLevel max17048Level;
LipoBatteryLevel lipoLevel;
/**
* Init the Lipo battery level sensor
*/
bool Power::max17048Init()
bool Power::lipoInit()
{
bool result = max17048Level.runOnce();
LOG_DEBUG("Power::max17048Init lipo sensor is %s", result ? "ready" : "not ready yet");
bool result = lipoLevel.runOnce();
LOG_DEBUG("Power::lipoInit lipo sensor is %s", result ? "ready" : "not ready yet");
if (!result)
return false;
batteryLevel = &max17048Level;
batteryLevel = &lipoLevel;
return true;
}
@@ -1390,88 +1386,7 @@ bool Power::max17048Init()
/**
* The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel
*/
bool Power::max17048Init()
{
return false;
}
#endif
#if !MESHTASTIC_EXCLUDE_I2C && HAS_CW2015
class CW2015BatteryLevel : public AnalogBatteryLevel
{
public:
/**
* Battery state of charge, from 0 to 100 or -1 for unknown
*/
virtual int getBatteryPercent() override
{
int data = -1;
Wire.beginTransmission(CW2015_ADDR);
Wire.write(0x04);
if (Wire.endTransmission() == 0) {
if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) {
data = Wire.read();
}
}
return data;
}
/**
* The raw voltage of the battery in millivolts, or NAN if unknown
*/
virtual uint16_t getBattVoltage() override
{
uint16_t mv = 0;
Wire.beginTransmission(CW2015_ADDR);
Wire.write(0x02);
if (Wire.endTransmission() == 0) {
if (Wire.requestFrom(CW2015_ADDR, (uint8_t)2)) {
mv = Wire.read();
mv <<= 8;
mv |= Wire.read();
// Voltage is read in 305uV units, convert to mV
mv = mv * 305 / 1000;
}
}
return mv;
}
};
CW2015BatteryLevel cw2015Level;
/**
* Init the CW2015 battery level sensor
*/
bool Power::cw2015Init()
{
Wire.beginTransmission(CW2015_ADDR);
uint8_t getInfo[] = {0x0a, 0x00};
Wire.write(getInfo, 2);
Wire.endTransmission();
delay(10);
Wire.beginTransmission(CW2015_ADDR);
Wire.write(0x00);
bool result = false;
if (Wire.endTransmission() == 0) {
if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) {
uint8_t data = Wire.read();
LOG_DEBUG("CW2015 init read data: 0x%x", data);
if (data == 0x73) {
result = true;
batteryLevel = &cw2015Level;
}
}
}
return result;
}
#else
/**
* The CW2015 battery level sensor is unavailable - default to AnalogBatteryLevel
*/
bool Power::cw2015Init()
bool Power::lipoInit()
{
return false;
}
+4 -4
View File
@@ -9,13 +9,13 @@
*/
#include "PowerFSM.h"
#include "Default.h"
#include "Led.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "PowerMon.h"
#include "configuration.h"
#include "graphics/Screen.h"
#include "main.h"
#include "modules/StatusLEDModule.h"
#include "sleep.h"
#include "target_specific.h"
@@ -103,7 +103,7 @@ static void lsIdle()
uint32_t sleepTime = SLEEP_TIME;
powerMon->setState(meshtastic_PowerMon_State_CPU_LightSleep);
statusLEDModule->setPowerLED(false);
ledBlink.set(false); // Never leave led on while in light sleep
esp_sleep_source_t wakeCause2 = doLightSleep(sleepTime * 1000LL);
powerMon->clearState(meshtastic_PowerMon_State_CPU_LightSleep);
@@ -111,7 +111,7 @@ static void lsIdle()
case ESP_SLEEP_WAKEUP_TIMER:
// Normal case: timer expired, we should just go back to sleep ASAP
statusLEDModule->setPowerLED(true);
ledBlink.set(true); // briefly turn on led
wakeCause2 = doLightSleep(100); // leave led on for 1ms
secsSlept += sleepTime;
@@ -146,7 +146,7 @@ static void lsIdle()
}
} else {
// Time to stop sleeping!
statusLEDModule->setPowerLED(false);
ledBlink.set(false);
LOG_INFO("Reached ls_secs, service loop()");
powerFSM.trigger(EVENT_WAKE_TIMER);
}
-2
View File
@@ -217,7 +217,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define SHTC3_ADDR 0x70
#define LPS22HB_ADDR 0x5C
#define LPS22HB_ADDR_ALT 0x5D
#define SFA30_ADDR 0x5D
#define SHT31_4x_ADDR 0x44
#define SHT31_4x_ADDR_ALT 0x45
#define PMSA003I_ADDR 0x12
@@ -234,7 +233,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define NAU7802_ADDR 0x2A
#define MAX30102_ADDR 0x57
#define SCD4X_ADDR 0x62
#define CW2015_ADDR 0x62
#define MLX90614_ADDR_DEF 0x5A
#define CGRADSENS_ADDR 0x66
#define LTR390UV_ADDR 0x53
+2 -2
View File
@@ -43,8 +43,8 @@ ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
ScanI2C::FoundDevice ScanI2C::firstAQI() const
{
ScanI2C::DeviceType types[] = {PMSA003I, SEN5X, SCD4X, SFA30};
return firstOfOrNONE(4, types);
ScanI2C::DeviceType types[] = {PMSA003I, SEN5X, SCD4X};
return firstOfOrNONE(2, types);
}
ScanI2C::FoundDevice ScanI2C::firstRGBLED() const
+1 -3
View File
@@ -89,9 +89,7 @@ class ScanI2C
DA217,
CHSC6X,
CST226SE,
SEN5X,
SFA30,
CW2015
SEN5X
} DeviceType;
// typedef uint8_t DeviceAddress;
+1 -20
View File
@@ -1,6 +1,4 @@
#include "ScanI2CTwoWire.h"
#include "configuration.h"
#include "detect/ScanI2C.h"
#if !MESHTASTIC_EXCLUDE_I2C
@@ -458,13 +456,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
break;
case LPS22HB_ADDR_ALT:
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD060), 48); // get device marking
if (registerValue != 0) {
type = SFA30;
logFoundDevice("SFA30", (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)
@@ -590,17 +581,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
break;
SCAN_SIMPLE_CASE(BHI260AP_ADDR, BHI260AP, "BHI260AP", (uint8_t)addr.address);
case SCD4X_ADDR: {
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x8), 1);
if (registerValue == 0x18) {
logFoundDevice("CW2015", (uint8_t)addr.address);
type = CW2015;
} else {
logFoundDevice("SCD4X", (uint8_t)addr.address);
type = SCD4X;
}
break;
}
SCAN_SIMPLE_CASE(SCD4X_ADDR, SCD4X, "SCD4X", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address);
#ifdef HAS_TPS65233
SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address);
+4
View File
@@ -0,0 +1,4 @@
#ifdef HAS_NEOPIXEL
#include <Adafruit_NeoPixel.h>
extern Adafruit_NeoPixel pixels;
#endif
+5
View File
@@ -0,0 +1,5 @@
#ifdef HAS_NCP5623
#include <NCP5623.h>
extern NCP5623 rgb;
#endif
+29 -22
View File
@@ -221,6 +221,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
if (rtc_sec > 0) {
// === Build Time String ===
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
int hour, minute, second;
graphics::decomposeTime(rtc_sec, hour, minute, second);
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
@@ -427,33 +428,39 @@ const int *getTextPositions(OLEDDisplay *display)
// *************************
void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
{
if (!isAPIConnected(service->api_state))
return;
bool drawConnectionState = false;
if (service->api_state == service->STATE_BLE || service->api_state == service->STATE_WIFI ||
service->api_state == service->STATE_SERIAL || service->api_state == service->STATE_PACKET ||
service->api_state == service->STATE_HTTP || service->api_state == service->STATE_ETH) {
drawConnectionState = true;
}
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
display->setColor(BLACK);
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
(connection_icon_height * scale) + (2 * scale));
display->setColor(WHITE);
if (currentResolution == ScreenResolution::High) {
const int bytesPerRow = (connection_icon_width + 7) / 8;
int iconX = 0;
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
if (drawConnectionState) {
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
display->setColor(BLACK);
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
(connection_icon_height * scale) + (2 * scale));
display->setColor(WHITE);
if (currentResolution == ScreenResolution::High) {
const int bytesPerRow = (connection_icon_width + 7) / 8;
int iconX = 0;
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
for (int yy = 0; yy < connection_icon_height; ++yy) {
const uint8_t *rowPtr = connection_icon + yy * bytesPerRow;
for (int xx = 0; xx < connection_icon_width; ++xx) {
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first
if (byteVal & bitMask) {
display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale);
for (int yy = 0; yy < connection_icon_height; ++yy) {
const uint8_t *rowPtr = connection_icon + yy * bytesPerRow;
for (int xx = 0; xx < connection_icon_width; ++xx) {
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first
if (byteVal & bitMask) {
display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale);
}
}
}
}
} else {
display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height,
connection_icon);
} else {
display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height,
connection_icon);
}
}
}
-14
View File
@@ -63,18 +63,4 @@ bool isAllowedPunctuation(char c);
std::string sanitizeString(const std::string &input);
static inline bool isAPIConnected(uint8_t state)
{
static constexpr bool connectedStates[] = {
/* STATE_NONE */ false,
/* STATE_BLE */ true,
/* STATE_WIFI */ true,
/* STATE_SERIAL */ true,
/* STATE_PACKET */ true,
/* STATE_HTTP */ true,
/* STATE_ETH */ true,
};
return state < sizeof(connectedStates) ? connectedStates[state] : false;
}
} // namespace graphics
+6 -6
View File
@@ -110,14 +110,14 @@ void getUptimeStr(uint32_t uptimeMillis, const char *prefix, char *uptimeStr, ui
uint32_t secs = (uptimeMillis % 60000) / 1000;
if (days) {
snprintf(uptimeStr, maxLength, "%s%ud %uh", prefix, days, hours);
snprintf(uptimeStr, maxLength, "%s: %ud %uh", prefix, days, hours);
} else if (hours) {
snprintf(uptimeStr, maxLength, "%s%uh %um", prefix, hours, mins);
snprintf(uptimeStr, maxLength, "%s: %uh %um", prefix, hours, mins);
} else if (!includeSecs) {
snprintf(uptimeStr, maxLength, "%s%um", prefix, mins);
snprintf(uptimeStr, maxLength, "%s: %um", prefix, mins);
} else if (mins) {
snprintf(uptimeStr, maxLength, "%s%um %us", prefix, mins, secs);
snprintf(uptimeStr, maxLength, "%s: %um %us", prefix, mins, secs);
} else {
snprintf(uptimeStr, maxLength, "%s%us", prefix, secs);
snprintf(uptimeStr, maxLength, "%s: %us", prefix, secs);
}
}
}
+3 -11
View File
@@ -429,10 +429,6 @@ void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool
c = c - 'a' + 'A';
}
keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c);
// Show the common "/" pairing next to "?" like on a real keyboard
if (key.type == VK_CHAR && key.character == '?') {
keyText = "?/";
}
}
int textWidth = display->getStringWidth(keyText.c_str());
@@ -522,13 +518,9 @@ char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress)
char c = key.character;
// Long-press: letters become uppercase; for "?" provide "/" like a typical keyboard
if (isLongPress) {
if (c >= 'a' && c <= 'z') {
c = (char)(c - 'a' + 'A');
} else if (c == '?') {
c = '/';
}
// Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings
if (isLongPress && c >= 'a' && c <= 'z') {
c = (char)(c - 'a' + 'A');
}
return c;
+1 -1
View File
@@ -663,7 +663,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5)) { // Only show uptime if the screen can show it
char uptimeStr[32] = "";
getUptimeStr(millis(), "Up: ", uptimeStr, sizeof(uptimeStr));
getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr));
textWidth = display->getStringWidth(uptimeStr);
nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], uptimeStr);
+33 -211
View File
@@ -2,7 +2,6 @@
#if HAS_SCREEN
#include "CompassRenderer.h"
#include "GPSStatus.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "NodeListRenderer.h"
#include "UIRenderer.h"
@@ -314,7 +313,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
// === Create the shortName and title string ===
const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node";
char titlestr[32] = {0};
snprintf(titlestr, sizeof(titlestr), "*%s*", shortName);
snprintf(titlestr, sizeof(titlestr), "Fav: %s", shortName);
// === Draw battery/time/mail header (common across screens) ===
graphics::drawCommonHeader(display, x, y, titlestr);
@@ -343,162 +342,34 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
}
// === 2. Signal and Hops (combined on one line, if available) ===
// If both are present: "Sig: 97% [2hops]"
// If only one: show only that one
char signalHopsStr[32] = "";
bool haveSignal = false;
int bars = 0;
int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100);
// Helper to get SNR limit based on modem preset
auto getSnrLimit = [](meshtastic_Config_LoRaConfig_ModemPreset preset) -> float {
switch (preset) {
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW:
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST:
return -6.0f;
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW:
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST:
return -5.5f;
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW:
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST:
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
return -4.5f;
default:
return -6.0f;
}
};
// Calculate signal grade using modem preset and SNR only
float snrLimit = getSnrLimit(config.lora.modem_preset);
float snr = node->snr;
// Determine signal quality label and bars using SNR-only grading
const char *qualityLabel = nullptr;
if (snr > snrLimit + 10) {
qualityLabel = "Good";
bars = 4;
} else if (snr > snrLimit + 6) {
qualityLabel = "Good";
bars = 3;
} else if (snr > snrLimit + 2) {
qualityLabel = "Good";
bars = 2;
} else if (snr > snrLimit - 4) {
qualityLabel = "Fair";
bars = 1;
} else {
qualityLabel = "Bad";
bars = 1;
}
// Add extra spacing on the left if we have an API connection to account for the common footer icons
const char *leftSideSpacing =
graphics::isAPIConnected(service->api_state) ? (currentResolution == ScreenResolution::High ? " " : " ") : " ";
// Always use "Sig" for the label
const char *signalLabel = " Sig";
// --- Build the Signal/Hops line ---
// Only show signal if we have valid SNR
if (snr > -100 && snr != 0) {
snprintf(signalHopsStr, sizeof(signalHopsStr), "%sSig:%s", leftSideSpacing, qualityLabel);
// If SNR looks reasonable, show signal
if ((int)((node->snr + 10) * 5) >= 0 && node->snr > -100) {
snprintf(signalHopsStr, sizeof(signalHopsStr), "%s: %d%%", signalLabel, percentSignal);
haveSignal = true;
}
// If hops is valid (>0), show right after signal
if (node->hops_away > 0) {
size_t len = strlen(signalHopsStr);
// Decide between "1 Hop" and "N Hops"
if (haveSignal) {
snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [#]");
snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away,
(node->hops_away == 1 ? "Hop" : "Hops"));
} else {
snprintf(signalHopsStr, sizeof(signalHopsStr), "[#]");
snprintf(signalHopsStr, sizeof(signalHopsStr), "[%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops"));
}
}
if (signalHopsStr[0]) {
int yPos = getTextPositions(display)[line++];
int curX = x;
// Split combined string into signal text and hop suffix
char sigPart[20] = "";
const char *hopPart = nullptr;
char *bracket = strchr(signalHopsStr, '[');
if (bracket) {
size_t n = (size_t)(bracket - signalHopsStr);
if (n >= sizeof(sigPart))
n = sizeof(sigPart) - 1;
memcpy(sigPart, signalHopsStr, n);
sigPart[n] = '\0';
// Trim trailing spaces
while (strlen(sigPart) && sigPart[strlen(sigPart) - 1] == ' ') {
sigPart[strlen(sigPart) - 1] = '\0';
}
hopPart = bracket; // "[n Hop(s)]"
} else {
strncpy(sigPart, signalHopsStr, sizeof(sigPart) - 1);
sigPart[sizeof(sigPart) - 1] = '\0';
}
// Draw signal quality text
display->drawString(curX, yPos, sigPart);
curX += display->getStringWidth(sigPart) + 4;
// Draw signal bars (skip on UltraLow, text only)
if (currentResolution != ScreenResolution::UltraLow && haveSignal && bars > 0) {
const int kMaxBars = 4;
if (bars < 1)
bars = 1;
if (bars > kMaxBars)
bars = kMaxBars;
int barX = curX;
const bool hi = (currentResolution == ScreenResolution::High);
int barWidth = hi ? 2 : 1;
int barGap = hi ? 2 : 1;
int maxBarHeight = FONT_HEIGHT_SMALL - 7;
if (!hi)
maxBarHeight -= 1;
int barY = yPos + (FONT_HEIGHT_SMALL - maxBarHeight) / 2;
for (int bi = 0; bi < kMaxBars; bi++) {
int barHeight = maxBarHeight * (bi + 1) / kMaxBars;
if (barHeight < 2)
barHeight = 2;
int bx = barX + bi * (barWidth + barGap);
int by = barY + maxBarHeight - barHeight;
if (bi < bars) {
display->fillRect(bx, by, barWidth, barHeight);
} else {
int baseY = barY + maxBarHeight - 1;
display->drawHorizontalLine(bx, baseY, barWidth);
}
}
curX += (kMaxBars * barWidth) + ((kMaxBars - 1) * barGap) + 2;
}
// Draw hops AFTER the bars as: [ number + hop icon ]
if (hopPart && node->hops_away > 0) {
// open bracket
display->drawString(curX, yPos, "[");
curX += display->getStringWidth("[") + 1;
// hop count
char hopCount[6];
snprintf(hopCount, sizeof(hopCount), "%d", node->hops_away);
display->drawString(curX, yPos, hopCount);
curX += display->getStringWidth(hopCount) + 2;
// hop icon
const int iconY = yPos + (FONT_HEIGHT_SMALL - hop_height) / 2;
display->drawXbm(curX, iconY, hop_width, hop_height, hop);
curX += hop_width + 1;
// closing bracket
display->drawString(curX, yPos, "]");
}
if (signalHopsStr[0] && line < 5) {
display->drawString(x, getTextPositions(display)[line++], signalHopsStr);
}
// === 3. Heard (last seen, skip if node never seen) ===
@@ -506,8 +377,8 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
uint32_t seconds = sinceLastSeen(node);
if (seconds != 0 && seconds != UINT32_MAX) {
uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24;
// Format as "Heard:Xm ago", "Heard:Xh ago", or "Heard:Xd ago"
snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard:?" : "%sHeard:%d%c ago"), leftSideSpacing,
// Format as "Heard: Xm ago", "Heard: Xh ago", or "Heard: Xd ago"
snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard: ?" : " Heard: %d%c ago"),
(days ? days
: hours ? hours
: minutes),
@@ -515,18 +386,16 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
: hours ? 'h'
: 'm'));
}
if (seenStr[0]) {
if (seenStr[0] && line < 5) {
display->drawString(x, getTextPositions(display)[line++], seenStr);
}
#if !defined(M5STACK_UNITC6L)
// === 4. Uptime (only show if metric is present) ===
char uptimeStr[32] = "";
if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) {
char upPrefix[12]; // enough for leftSideSpacing + "Up:"
snprintf(upPrefix, sizeof(upPrefix), "%sUp:", leftSideSpacing);
getUptimeStr(node->device_metrics.uptime_seconds * 1000, upPrefix, uptimeStr, sizeof(uptimeStr));
getUptimeStr(node->device_metrics.uptime_seconds * 1000, " Up", uptimeStr, sizeof(uptimeStr));
}
if (uptimeStr[0]) {
if (uptimeStr[0] && line < 5) {
display->drawString(x, getTextPositions(display)[line++], uptimeStr);
}
@@ -553,16 +422,16 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
if (miles < 0.1) {
int feet = (int)(miles * 5280);
if (feet > 0 && feet < 1000) {
snprintf(distStr, sizeof(distStr), "%sDistance:%dft", leftSideSpacing, feet);
snprintf(distStr, sizeof(distStr), " Distance: %dft", feet);
haveDistance = true;
} else if (feet >= 1000) {
snprintf(distStr, sizeof(distStr), "%sDistance:¼mi", leftSideSpacing);
snprintf(distStr, sizeof(distStr), " Distance: ¼mi");
haveDistance = true;
}
} else {
int roundedMiles = (int)(miles + 0.5);
if (roundedMiles > 0 && roundedMiles < 1000) {
snprintf(distStr, sizeof(distStr), "%sDistance:%dmi", leftSideSpacing, roundedMiles);
snprintf(distStr, sizeof(distStr), " Distance: %dmi", roundedMiles);
haveDistance = true;
}
}
@@ -570,74 +439,26 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
if (distanceKm < 1.0) {
int meters = (int)(distanceKm * 1000);
if (meters > 0 && meters < 1000) {
snprintf(distStr, sizeof(distStr), "%sDistance:%dm", leftSideSpacing, meters);
snprintf(distStr, sizeof(distStr), " Distance: %dm", meters);
haveDistance = true;
} else if (meters >= 1000) {
snprintf(distStr, sizeof(distStr), "%sDistance:1km", leftSideSpacing);
snprintf(distStr, sizeof(distStr), " Distance: 1km");
haveDistance = true;
}
} else {
int km = (int)(distanceKm + 0.5);
if (km > 0 && km < 1000) {
snprintf(distStr, sizeof(distStr), "%sDistance:%dkm", leftSideSpacing, km);
snprintf(distStr, sizeof(distStr), " Distance: %dkm", km);
haveDistance = true;
}
}
}
}
if (haveDistance && distStr[0]) {
// Only display if we actually have a value!
if (haveDistance && distStr[0] && line < 5) {
display->drawString(x, getTextPositions(display)[line++], distStr);
}
// === 6. Battery after Distance line, otherwise next available line ===
char batLine[32] = "";
bool haveBatLine = false;
if (node->has_device_metrics) {
bool hasPct = node->device_metrics.has_battery_level;
bool hasVolt = node->device_metrics.has_voltage && node->device_metrics.voltage > 0.001f;
int pct = 0;
float volt = 0.0f;
if (hasPct) {
pct = (int)node->device_metrics.battery_level;
}
if (hasVolt) {
volt = node->device_metrics.voltage;
}
if (hasPct && pct > 0 && pct <= 100) {
// Normal battery percentage
if (hasVolt) {
snprintf(batLine, sizeof(batLine), "%sBat:%d%% (%.2fV)", leftSideSpacing, pct, volt);
} else {
snprintf(batLine, sizeof(batLine), "%sBat:%d%%", leftSideSpacing, pct);
}
haveBatLine = true;
} else if (hasPct && pct > 100) {
// Plugged in
if (hasVolt) {
snprintf(batLine, sizeof(batLine), "%sPlugged In (%.2fV)", leftSideSpacing, volt);
} else {
snprintf(batLine, sizeof(batLine), "%sPlugged In", leftSideSpacing);
}
haveBatLine = true;
} else if (!hasPct && hasVolt) {
// Voltage only
snprintf(batLine, sizeof(batLine), "%sBat:%.2fV", leftSideSpacing, volt);
haveBatLine = true;
}
}
const int maxTextLines = (currentResolution == ScreenResolution::High) ? 6 : 5;
// Only draw battery if it fits within the allowed lines
if (haveBatLine && line <= maxTextLines) {
display->drawString(x, getTextPositions(display)[line++], batLine);
}
// --- Compass Rendering: landscape (wide) screens use the original side-aligned logic ---
if (SCREEN_WIDTH > SCREEN_HEIGHT) {
bool showCompass = false;
@@ -772,7 +593,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
}
char uptimeStr[32] = "";
if (currentResolution != ScreenResolution::UltraLow) {
getUptimeStr(millis(), "Up: ", uptimeStr, sizeof(uptimeStr));
getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr));
}
display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr);
@@ -1163,6 +984,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
config.display.heading_bold = false;
const char *displayLine = ""; // Initialize to empty string by default
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
if (config.position.fixed_position) {
@@ -1207,10 +1029,10 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
char uptimeStr[32];
#if defined(USE_EINK)
// E-Ink: skip seconds, show only days/hours/mins
getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), false);
getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), false);
#else
// Non E-Ink: include seconds where useful
getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), true);
getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), true);
#endif
display->drawString(0, getTextPositions(display)[line++], uptimeStr);
-6
View File
@@ -83,12 +83,6 @@ static const unsigned char mail[] PROGMEM = {
0b11111111, 0b00 // Bottom line
};
// Hop icon (9x10)
#define hop_width 9
#define hop_height 10
const uint8_t hop[] PROGMEM = {0x05, 0x00, 0x07, 0x00, 0x05, 0x00, 0x38, 0x00, 0x28, 0x00,
0x38, 0x00, 0xC0, 0x01, 0x40, 0x01, 0xC0, 0x01, 0x40, 0x00};
// 📬 Mail / Message
const uint8_t icon_mail[] PROGMEM = {
0b11111111, // ████████ top border
-12
View File
@@ -1,5 +1,3 @@
#include "graphics/niche/InkHUD/Tile.h"
#include <cstdint>
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./Applet.h"
@@ -787,16 +785,6 @@ void InkHUD::Applet::drawHeader(std::string text)
drawPixel(x, 0, BLACK);
drawPixel(x, headerDivY, BLACK); // Dotted 50%
}
// Dither near battery
if (settings->optionalFeatures.batteryIcon) {
constexpr uint16_t ditherSizePx = 4;
Tile *batteryTile = ((Applet *)inkhud->getSystemApplet("BatteryIcon"))->getTile();
const uint16_t batteryTileLeft = batteryTile->getLeft();
const uint16_t batteryTileTop = batteryTile->getTop();
const uint16_t batteryTileHeight = batteryTile->getHeight();
hatchRegion(batteryTileLeft - ditherSizePx, batteryTileTop, ditherSizePx, batteryTileHeight, 2, WHITE);
}
}
// Get the height of the standard applet header
@@ -48,27 +48,37 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
void InkHUD::BatteryIconApplet::onRender(bool full)
{
// Clear the region beneath the tile, including the border
// Fill entire tile
// - size of icon controlled by size of tile
int16_t l = 0;
int16_t t = 0;
uint16_t w = width();
int16_t h = height();
// Clear the region beneath the tile
// Most applets are drawing onto an empty frame buffer and don't need to do this
// We do need to do this with the battery though, as it is an "overlay"
fillRect(0, 0, width(), height(), WHITE);
fillRect(l, t, w, h, WHITE);
// Vertical centerline
const int16_t m = t + (h / 2);
// =====================
// Draw battery outline
// =====================
// Positive terminal "bump"
const int16_t &bumpL = l;
const uint16_t bumpH = h / 2;
const int16_t bumpT = m - (bumpH / 2);
constexpr uint16_t bumpW = 2;
const int16_t &bumpL = 1;
const uint16_t bumpH = (height() - 2) / 2;
const int16_t bumpT = (1 + ((height() - 2) / 2)) - (bumpH / 2);
fillRect(bumpL, bumpT, bumpW, bumpH, BLACK);
// Main body of battery
const int16_t bodyL = 1 + bumpW;
const int16_t &bodyT = 1;
const int16_t &bodyH = height() - 2; // Handle top/bottom padding
const int16_t bodyW = (width() - 1) - bumpW; // Handle 1px left pad
const int16_t bodyL = bumpL + bumpW;
const int16_t &bodyT = t;
const int16_t &bodyH = h;
const int16_t bodyW = w - bumpW;
drawRect(bodyL, bodyT, bodyW, bodyH, BLACK);
// Erase join between bump and body
@@ -79,13 +89,12 @@ void InkHUD::BatteryIconApplet::onRender(bool full)
// ===================
constexpr int16_t slicePad = 2;
int16_t sliceL = bodyL + slicePad;
const int16_t sliceL = bodyL + slicePad;
const int16_t sliceT = bodyT + slicePad;
const uint16_t sliceH = bodyH - (slicePad * 2);
uint16_t sliceW = bodyW - (slicePad * 2);
sliceW = (sliceW * socRounded) / 100; // Apply percentage
sliceL += ((bodyW - (slicePad * 2)) - sliceW); // Shift slice to the battery's negative terminal, correcting drain direction
sliceW = (sliceW * socRounded) / 100; // Apply percentage
hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK);
drawRect(sliceL, sliceT, sliceW, sliceH, BLACK);
+4 -4
View File
@@ -510,10 +510,10 @@ void InkHUD::WindowManager::placeSystemTiles()
const uint16_t batteryIconWidth = batteryIconHeight * 1.8;
inkhud->getSystemApplet("BatteryIcon")
->getTile()
->setRegion(inkhud->width() - batteryIconWidth - 1, // x
1, // y
batteryIconWidth + 1, // width
batteryIconHeight + 2); // height
->setRegion(inkhud->width() - batteryIconWidth, // x
2, // y
batteryIconWidth, // width
batteryIconHeight); // height
// Note: the tiles of placeholder and menu applets are manipulated specially
// - menuApplet borrows user tiles
+70 -25
View File
@@ -7,13 +7,13 @@
#include "NodeDB.h"
#include "PowerFSM.h"
#include "PowerMon.h"
#include "RadioLibInterface.h"
#include "ReliableRouter.h"
#include "airtime.h"
#include "buzz.h"
#include "power/PowerHAL.h"
#include "FSCommon.h"
#include "Led.h"
#include "RTC.h"
#include "SPILock.h"
#include "Throttle.h"
@@ -29,6 +29,7 @@
#include <Wire.h>
#endif
#include "detect/einkScan.h"
#include "graphics/RAKled.h"
#include "graphics/Screen.h"
#include "main.h"
#include "mesh/generated/meshtastic/config.pb.h"
@@ -192,8 +193,6 @@ 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)
@@ -243,10 +242,28 @@ const char *getDeviceName()
return name;
}
// TODO remove from main.cpp
static int32_t ledBlinker()
{
// Still set up the blinking (heartbeat) interval but skip code path below, so LED will blink if
// config.device.led_heartbeat_disabled is changed
if (config.device.led_heartbeat_disabled)
return 1000;
static bool ledOn;
ledOn ^= 1;
ledBlink.set(ledOn);
// have a very sparse duty cycle of LED being on, unless charging, then blink 0.5Hz square wave rate to indicate that
return powerStatus->getIsCharging() ? 1000 : (ledOn ? 1 : 1000);
}
uint32_t timeLastPowered = 0;
static Periodic *ledPeriodic;
static OSThread *powerFSMthread;
OSThread *ambientLightingThread;
static OSThread *ambientLightingThread;
RadioInterface *rIf = NULL;
#ifdef ARCH_PORTDUINO
@@ -282,16 +299,21 @@ void earlyInitVariant() {}
// blink user led in 3 flashes sequence to indicate what is happening
void waitUntilPowerLevelSafe()
{
#ifdef LED_PIN
pinMode(LED_PIN, OUTPUT);
#endif
while (powerHAL_isPowerLevelSafe() == false) {
#ifdef LED_POWER
#ifdef LED_PIN
// 3x: blink for 300 ms, pause for 300 ms
for (int i = 0; i < 3; i++) {
digitalWrite(LED_POWER, LED_STATE_ON);
digitalWrite(LED_PIN, LED_STATE_ON);
delay(300);
digitalWrite(LED_POWER, LED_STATE_OFF);
digitalWrite(LED_PIN, LED_STATE_OFF);
delay(300);
}
#endif
@@ -315,11 +337,6 @@ void setup()
// initialize power HAL layer as early as possible
powerHAL_init();
#ifdef LED_POWER
pinMode(LED_POWER, OUTPUT);
digitalWrite(LED_POWER, LED_STATE_ON);
#endif
// prevent booting if device is in power failure mode
// boot sequence will follow when battery level raises to safe mode
waitUntilPowerLevelSafe();
@@ -332,6 +349,11 @@ void setup()
digitalWrite(PIN_POWER_EN, HIGH);
#endif
#ifdef LED_POWER
pinMode(LED_POWER, OUTPUT);
digitalWrite(LED_POWER, LED_STATE_ON);
#endif
#ifdef LED_NOTIFICATION
pinMode(LED_NOTIFICATION, OUTPUT);
digitalWrite(LED_NOTIFICATION, HIGH ^ LED_STATE_ON);
@@ -344,7 +366,11 @@ void setup()
#ifdef BLE_LED
pinMode(BLE_LED, OUTPUT);
digitalWrite(BLE_LED, LED_STATE_OFF);
#ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, HIGH);
#else
digitalWrite(BLE_LED, LOW);
#endif
#endif
concurrency::hasBeenSetup = true;
@@ -463,6 +489,14 @@ void setup()
OSThread::setup();
// TODO make this ifdef based on defined pins and move from main.cpp
#if defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2)
// The ThinkNodes have their own blink logic
// ledPeriodic = new Periodic("Blink", elecrowLedBlinker);
#else
ledPeriodic = new Periodic("Blink", ledBlinker);
#endif
fsInit();
#if !MESHTASTIC_EXCLUDE_I2C
@@ -681,12 +715,20 @@ void setup()
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X);
#endif
#ifdef HAS_SDCARD
setupSDCard();
#endif
// LED init
#ifdef LED_PIN
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LED_STATE_ON); // turn on for now
#endif
// Hello
printInfo();
#ifdef BUILD_EPOCH
@@ -729,16 +771,19 @@ void setup()
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;
#endif
#if defined(USE_SH1107_128_64)
screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // keep dimension of 128x64
screen_geometry = GEOMETRY_128_64;
#endif
#endif
// if we have one of the fixed overrides in the settings, adjust display type accordingly.
if (screen_model == meshtastic_Config_DisplayConfig_OledType_OLED_SH1107) {
screen_geometry = GEOMETRY_128_64;
} else if (screen_model == meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128) {
screen_geometry = GEOMETRY_128_128;
}
#if !MESHTASTIC_EXCLUDE_I2C
#if !defined(ARCH_STM32WL)
if (acc_info.type != ScanI2C::DeviceType::NONE) {
@@ -796,7 +841,7 @@ void setup()
SPI.begin();
#endif
#else
// ESP32
// ESP32
#if defined(HW_SPI1_DEVICE)
SPI1.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
LOG_DEBUG("SPI1.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
@@ -929,6 +974,12 @@ void setup()
setupNicheGraphics();
#endif
#ifdef LED_PIN
// Turn LED off after boot, if heartbeat by config
if (config.device.led_heartbeat_disabled)
digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON);
#endif
// Do this after service.init (because that clears error_code)
#ifdef HAS_PMU
if (!pmu_found)
@@ -1125,12 +1176,6 @@ void loop()
#endif
power->powerCommandsCheck();
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
static uint32_t lastPrint = 0;
if (!Throttle::isWithinTimespanMs(lastPrint, 10 * 1000L)) {
-1
View File
@@ -33,7 +33,6 @@ 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 -11
View File
@@ -1408,15 +1408,6 @@ void NodeDB::loadFromDisk()
if (portduino_config.has_configDisplayMode) {
config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode;
}
if (portduino_config.has_statusMessage) {
moduleConfig.has_statusmessage = true;
strncpy(moduleConfig.statusmessage.node_status, portduino_config.statusMessage.c_str(),
sizeof(moduleConfig.statusmessage.node_status));
moduleConfig.statusmessage.node_status[sizeof(moduleConfig.statusmessage.node_status) - 1] = '\0';
}
if (portduino_config.enable_UDP) {
config.network.enabled_protocols = meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST;
}
#endif
}
@@ -1557,7 +1548,6 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat)
moduleConfig.has_ambient_lighting = true;
moduleConfig.has_audio = true;
moduleConfig.has_paxcounter = true;
moduleConfig.has_statusmessage = true;
success &=
saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig);
@@ -1772,7 +1762,7 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
info->has_device_metrics = false;
info->has_position = false;
info->user.public_key.size = 0;
memset(info->user.public_key.bytes, 0, sizeof(info->user.public_key.bytes));
info->user.public_key.bytes[0] = 0;
} else {
/* Clients are sending add_contact before every text message DM (because clients may hold a larger node database with
* public keys than the radio holds). However, we don't want to update last_heard just because we sent someone a DM!
+1 -2
View File
@@ -82,6 +82,7 @@ template <class T> class ProtobufModule : protected SinglePortModule
// it would be better to update even if the message was destined to others.
auto &p = mp.decoded;
LOG_INFO("Received %s from=0x%0x, id=0x%x, portnum=%d, payloadlen=%d", name, mp.from, mp.id, p.portnum, p.payload.size);
T scratch;
T *decoded = NULL;
@@ -89,8 +90,6 @@ template <class T> class ProtobufModule : protected SinglePortModule
memset(&scratch, 0, sizeof(scratch));
if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, fields, &scratch)) {
decoded = &scratch;
LOG_INFO("Received %s from=0x%0x, id=0x%x, portnum=%d, payloadlen=%d", name, mp.from, mp.id, p.portnum,
p.payload.size);
} else {
LOG_ERROR("Error decoding proto module!");
// if we can't decode it, nobody can process it!
+2 -7
View File
@@ -453,11 +453,8 @@ void RadioLibInterface::handleReceiveInterrupt()
}
#endif
if (state != RADIOLIB_ERR_NONE) {
// Log PacketHeader similar to RadioInterface::printPacket so we can try to match RX errors to other packets in the logs.
LOG_ERROR("Ignore received packet due to error=%d (maybe id=0x%08x fr=0x%08x to=0x%08x flags=0x%02x rxSNR=%g rxRSSI=%i "
"nextHop=0x%x relay=0x%x)",
state, radioBuffer.header.id, radioBuffer.header.from, radioBuffer.header.to, radioBuffer.header.flags,
iface->getSNR(), lround(iface->getRSSI()), radioBuffer.header.next_hop, radioBuffer.header.relay_node);
LOG_ERROR("Ignore received packet due to error=%d (maybe to=0x%08x, from=0x%08x, flags=0x%02x)", state,
radioBuffer.header.to, radioBuffer.header.from, radioBuffer.header.flags);
rxBad++;
airTime->logAirtime(RX_ALL_LOG, rxMsec);
@@ -517,8 +514,6 @@ 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);
}
@@ -36,9 +36,6 @@ PB_BIND(meshtastic_SCD4X_config, meshtastic_SCD4X_config, AUTO)
PB_BIND(meshtastic_SEN5X_config, meshtastic_SEN5X_config, AUTO)
PB_BIND(meshtastic_SCD30_config, meshtastic_SCD30_config, AUTO)
+4 -53
View File
@@ -206,27 +206,6 @@ typedef struct _meshtastic_SEN5X_config {
bool set_one_shot_mode;
} meshtastic_SEN5X_config;
typedef struct _meshtastic_SCD30_config {
/* Set Automatic self-calibration enabled */
bool has_set_asc;
bool set_asc;
/* Recalibration target CO2 concentration in ppm (FRC or ASC) */
bool has_set_target_co2_conc;
uint32_t set_target_co2_conc;
/* Reference temperature in degC */
bool has_set_temperature;
float set_temperature;
/* Altitude of sensor in meters above sea level. 0 - 3000m (overrides ambient pressure) */
bool has_set_altitude;
uint32_t set_altitude;
/* Power mode for sensor (true for low power, false for normal) */
bool has_set_measurement_interval;
uint32_t set_measurement_interval;
/* Perform a factory reset of the sensor */
bool has_soft_reset;
bool soft_reset;
} meshtastic_SCD30_config;
typedef struct _meshtastic_SensorConfig {
/* SCD4X CO2 Sensor configuration */
bool has_scd4x_config;
@@ -234,9 +213,6 @@ typedef struct _meshtastic_SensorConfig {
/* SEN5X PM Sensor configuration */
bool has_sen5x_config;
meshtastic_SEN5X_config sen5x_config;
/* SCD30 CO2 Sensor configuration */
bool has_scd30_config;
meshtastic_SCD30_config scd30_config;
} meshtastic_SensorConfig;
typedef PB_BYTES_ARRAY_T(8) meshtastic_AdminMessage_session_passkey_t;
@@ -424,7 +400,6 @@ extern "C" {
/* Initializer values for message structs */
#define meshtastic_AdminMessage_init_default {0, {0}, {0, {0}}}
#define meshtastic_AdminMessage_InputEvent_init_default {0, 0, 0, 0}
@@ -433,10 +408,9 @@ extern "C" {
#define meshtastic_NodeRemoteHardwarePinsResponse_init_default {0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}}
#define meshtastic_SharedContact_init_default {0, false, meshtastic_User_init_default, 0, 0}
#define meshtastic_KeyVerificationAdmin_init_default {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0}
#define meshtastic_SensorConfig_init_default {false, meshtastic_SCD4X_config_init_default, false, meshtastic_SEN5X_config_init_default, false, meshtastic_SCD30_config_init_default}
#define meshtastic_SensorConfig_init_default {false, meshtastic_SCD4X_config_init_default, false, meshtastic_SEN5X_config_init_default}
#define meshtastic_SCD4X_config_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_SEN5X_config_init_default {false, 0, false, 0}
#define meshtastic_SCD30_config_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_AdminMessage_init_zero {0, {0}, {0, {0}}}
#define meshtastic_AdminMessage_InputEvent_init_zero {0, 0, 0, 0}
#define meshtastic_AdminMessage_OTAEvent_init_zero {_meshtastic_OTAMode_MIN, {0, {0}}}
@@ -444,10 +418,9 @@ extern "C" {
#define meshtastic_NodeRemoteHardwarePinsResponse_init_zero {0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}}
#define meshtastic_SharedContact_init_zero {0, false, meshtastic_User_init_zero, 0, 0}
#define meshtastic_KeyVerificationAdmin_init_zero {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0}
#define meshtastic_SensorConfig_init_zero {false, meshtastic_SCD4X_config_init_zero, false, meshtastic_SEN5X_config_init_zero, false, meshtastic_SCD30_config_init_zero}
#define meshtastic_SensorConfig_init_zero {false, meshtastic_SCD4X_config_init_zero, false, meshtastic_SEN5X_config_init_zero}
#define meshtastic_SCD4X_config_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_SEN5X_config_init_zero {false, 0, false, 0}
#define meshtastic_SCD30_config_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
/* Field tags (for use in manual encoding/decoding) */
#define meshtastic_AdminMessage_InputEvent_event_code_tag 1
@@ -478,15 +451,8 @@ extern "C" {
#define meshtastic_SCD4X_config_set_power_mode_tag 7
#define meshtastic_SEN5X_config_set_temperature_tag 1
#define meshtastic_SEN5X_config_set_one_shot_mode_tag 2
#define meshtastic_SCD30_config_set_asc_tag 1
#define meshtastic_SCD30_config_set_target_co2_conc_tag 2
#define meshtastic_SCD30_config_set_temperature_tag 3
#define meshtastic_SCD30_config_set_altitude_tag 4
#define meshtastic_SCD30_config_set_measurement_interval_tag 5
#define meshtastic_SCD30_config_soft_reset_tag 6
#define meshtastic_SensorConfig_scd4x_config_tag 1
#define meshtastic_SensorConfig_sen5x_config_tag 2
#define meshtastic_SensorConfig_scd30_config_tag 3
#define meshtastic_AdminMessage_get_channel_request_tag 1
#define meshtastic_AdminMessage_get_channel_response_tag 2
#define meshtastic_AdminMessage_get_owner_request_tag 3
@@ -676,13 +642,11 @@ X(a, STATIC, OPTIONAL, UINT32, security_number, 4)
#define meshtastic_SensorConfig_FIELDLIST(X, a) \
X(a, STATIC, OPTIONAL, MESSAGE, scd4x_config, 1) \
X(a, STATIC, OPTIONAL, MESSAGE, sen5x_config, 2) \
X(a, STATIC, OPTIONAL, MESSAGE, scd30_config, 3)
X(a, STATIC, OPTIONAL, MESSAGE, sen5x_config, 2)
#define meshtastic_SensorConfig_CALLBACK NULL
#define meshtastic_SensorConfig_DEFAULT NULL
#define meshtastic_SensorConfig_scd4x_config_MSGTYPE meshtastic_SCD4X_config
#define meshtastic_SensorConfig_sen5x_config_MSGTYPE meshtastic_SEN5X_config
#define meshtastic_SensorConfig_scd30_config_MSGTYPE meshtastic_SCD30_config
#define meshtastic_SCD4X_config_FIELDLIST(X, a) \
X(a, STATIC, OPTIONAL, BOOL, set_asc, 1) \
@@ -701,16 +665,6 @@ X(a, STATIC, OPTIONAL, BOOL, set_one_shot_mode, 2)
#define meshtastic_SEN5X_config_CALLBACK NULL
#define meshtastic_SEN5X_config_DEFAULT NULL
#define meshtastic_SCD30_config_FIELDLIST(X, a) \
X(a, STATIC, OPTIONAL, BOOL, set_asc, 1) \
X(a, STATIC, OPTIONAL, UINT32, set_target_co2_conc, 2) \
X(a, STATIC, OPTIONAL, FLOAT, set_temperature, 3) \
X(a, STATIC, OPTIONAL, UINT32, set_altitude, 4) \
X(a, STATIC, OPTIONAL, UINT32, set_measurement_interval, 5) \
X(a, STATIC, OPTIONAL, BOOL, soft_reset, 6)
#define meshtastic_SCD30_config_CALLBACK NULL
#define meshtastic_SCD30_config_DEFAULT NULL
extern const pb_msgdesc_t meshtastic_AdminMessage_msg;
extern const pb_msgdesc_t meshtastic_AdminMessage_InputEvent_msg;
extern const pb_msgdesc_t meshtastic_AdminMessage_OTAEvent_msg;
@@ -721,7 +675,6 @@ extern const pb_msgdesc_t meshtastic_KeyVerificationAdmin_msg;
extern const pb_msgdesc_t meshtastic_SensorConfig_msg;
extern const pb_msgdesc_t meshtastic_SCD4X_config_msg;
extern const pb_msgdesc_t meshtastic_SEN5X_config_msg;
extern const pb_msgdesc_t meshtastic_SCD30_config_msg;
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
#define meshtastic_AdminMessage_fields &meshtastic_AdminMessage_msg
@@ -734,7 +687,6 @@ extern const pb_msgdesc_t meshtastic_SCD30_config_msg;
#define meshtastic_SensorConfig_fields &meshtastic_SensorConfig_msg
#define meshtastic_SCD4X_config_fields &meshtastic_SCD4X_config_msg
#define meshtastic_SEN5X_config_fields &meshtastic_SEN5X_config_msg
#define meshtastic_SCD30_config_fields &meshtastic_SCD30_config_msg
/* Maximum encoded size of messages (where known) */
#define MESHTASTIC_MESHTASTIC_ADMIN_PB_H_MAX_SIZE meshtastic_AdminMessage_size
@@ -744,10 +696,9 @@ extern const pb_msgdesc_t meshtastic_SCD30_config_msg;
#define meshtastic_HamParameters_size 31
#define meshtastic_KeyVerificationAdmin_size 25
#define meshtastic_NodeRemoteHardwarePinsResponse_size 496
#define meshtastic_SCD30_config_size 27
#define meshtastic_SCD4X_config_size 29
#define meshtastic_SEN5X_config_size 7
#define meshtastic_SensorConfig_size 69
#define meshtastic_SensorConfig_size 40
#define meshtastic_SharedContact_size 127
#ifdef __cplusplus
+3 -5
View File
@@ -109,9 +109,7 @@ typedef enum _meshtastic_TelemetrySensorType {
/* STH21 Temperature and R. Humidity sensor */
meshtastic_TelemetrySensorType_SHT21 = 47,
/* Sensirion STC31 CO2 sensor */
meshtastic_TelemetrySensorType_STC31 = 48,
/* SCD30 CO2, humidity, temperature sensor */
meshtastic_TelemetrySensorType_SCD30 = 49
meshtastic_TelemetrySensorType_STC31 = 48
} meshtastic_TelemetrySensorType;
/* Struct definitions */
@@ -489,8 +487,8 @@ extern "C" {
/* Helper constants for enums */
#define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_SCD30
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_SCD30+1))
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_STC31
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_STC31+1))
+43
View File
@@ -9,6 +9,7 @@
#if HAS_WIFI
#include "mesh/wifi/WiFiAPClient.h"
#endif
#include "Led.h"
#include "SPILock.h"
#include "power.h"
#include "serialization/JSON.h"
@@ -91,6 +92,7 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
ResourceNode *nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload);
ResourceNode *nodeJsonScanNetworks = new ResourceNode("/json/scanNetworks", "GET", &handleScanNetworks);
ResourceNode *nodeJsonBlinkLED = new ResourceNode("/json/blink", "POST", &handleBlinkLED);
ResourceNode *nodeJsonReport = new ResourceNode("/json/report", "GET", &handleReport);
ResourceNode *nodeJsonNodes = new ResourceNode("/json/nodes", "GET", &handleNodes);
ResourceNode *nodeJsonFsBrowseStatic = new ResourceNode("/json/fs/browse/static", "GET", &handleFsBrowseStatic);
@@ -108,6 +110,7 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
secureServer->registerNode(nodeRestart);
secureServer->registerNode(nodeFormUpload);
secureServer->registerNode(nodeJsonScanNetworks);
secureServer->registerNode(nodeJsonBlinkLED);
secureServer->registerNode(nodeJsonFsBrowseStatic);
secureServer->registerNode(nodeJsonDelete);
secureServer->registerNode(nodeJsonReport);
@@ -130,6 +133,7 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
insecureServer->registerNode(nodeRestart);
insecureServer->registerNode(nodeFormUpload);
insecureServer->registerNode(nodeJsonScanNetworks);
insecureServer->registerNode(nodeJsonBlinkLED);
insecureServer->registerNode(nodeJsonFsBrowseStatic);
insecureServer->registerNode(nodeJsonDelete);
insecureServer->registerNode(nodeJsonReport);
@@ -900,6 +904,45 @@ void handleRestart(HTTPRequest *req, HTTPResponse *res)
webServerThread->requestRestart = (millis() / 1000) + 5;
}
void handleBlinkLED(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "application/json");
res->setHeader("Access-Control-Allow-Origin", "*");
res->setHeader("Access-Control-Allow-Methods", "POST");
ResourceParameters *params = req->getParams();
std::string blink_target;
if (!params->getQueryParameter("blink_target", blink_target)) {
// if no blink_target was supplied in the URL parameters of the
// POST request, then assume we should blink the LED
blink_target = "LED";
}
if (blink_target == "LED") {
uint8_t count = 10;
while (count > 0) {
ledBlink.set(true);
delay(50);
ledBlink.set(false);
delay(50);
count = count - 1;
}
} else {
#if HAS_SCREEN
if (screen)
screen->blink();
#endif
}
JSONObject jsonObjOuter;
jsonObjOuter["status"] = new JSONValue("ok");
JSONValue *value = new JSONValue(jsonObjOuter);
std::string jsonString = value->Stringify();
res->print(jsonString.c_str());
delete value;
}
void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "application/json");
+1
View File
@@ -11,6 +11,7 @@ void handleFormUpload(HTTPRequest *req, HTTPResponse *res);
void handleScanNetworks(HTTPRequest *req, HTTPResponse *res);
void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res);
void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res);
void handleBlinkLED(HTTPRequest *req, HTTPResponse *res);
void handleReport(HTTPRequest *req, HTTPResponse *res);
void handleNodes(HTTPRequest *req, HTTPResponse *res);
void handleUpdateFs(HTTPRequest *req, HTTPResponse *res);
+10 -16
View File
@@ -391,7 +391,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
node->has_device_metrics = false;
node->has_position = false;
node->user.public_key.size = 0;
memset(node->user.public_key.bytes, 0, sizeof(node->user.public_key.bytes));
node->user.public_key.bytes[0] = 0;
saveChanges(SEGMENT_NODEDATABASE, false);
}
break;
@@ -643,6 +643,12 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
accelerometerThread->enabled = true;
accelerometerThread->start();
}
#endif
#ifdef LED_PIN
// Turn LED off if heartbeat by config
if (c.payload_variant.device.led_heartbeat_disabled) {
digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON);
}
#endif
if (config.device.button_gpio == c.payload_variant.device.button_gpio &&
config.device.buzzer_gpio == c.payload_variant.device.buzzer_gpio &&
@@ -899,11 +905,10 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
{
bool shouldReboot = true;
// If we are in an open transaction or configuring MQTT or Serial (which have validation), defer disabling Bluetooth
// Otherwise, disable Bluetooth to prevent the phone from interfering with the config
if (!hasOpenEditTransaction && !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag,
meshtastic_ModuleConfig_serial_tag, meshtastic_ModuleConfig_statusmessage_tag)) {
if (!hasOpenEditTransaction &&
!IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag)) {
disableBluetooth();
}
@@ -995,14 +1000,8 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
moduleConfig.has_paxcounter = true;
moduleConfig.paxcounter = c.payload_variant.paxcounter;
break;
case meshtastic_ModuleConfig_statusmessage_tag:
LOG_INFO("Set module config: StatusMessage");
moduleConfig.has_statusmessage = true;
moduleConfig.statusmessage = c.payload_variant.statusmessage;
shouldReboot = false;
break;
}
saveChanges(SEGMENT_MODULECONFIG, shouldReboot);
saveChanges(SEGMENT_MODULECONFIG);
return true;
}
@@ -1181,11 +1180,6 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag;
res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter;
break;
case meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG:
LOG_INFO("Get module config: StatusMessage");
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_statusmessage_tag;
res.get_module_config_response.payload_variant.statusmessage = moduleConfig.statusmessage;
break;
}
// NOTE: The phone app needs to know the ls_secsvalue so it can properly expect sleep behavior.
+96 -2
View File
@@ -24,8 +24,24 @@
#include "mesh/generated/meshtastic/rtttl.pb.h"
#include <Arduino.h>
#ifdef HAS_NCP5623
#include <graphics/RAKled.h>
#endif
#ifdef HAS_LP5562
#include <graphics/NomadStarLED.h>
#endif
#ifdef HAS_NEOPIXEL
#include <graphics/NeoPixel.h>
#endif
#ifdef UNPHONE
#include "unPhone.h"
extern unPhone unphone;
#endif
#if defined(HAS_RGB_LED)
#include "AmbientLightingThread.h"
uint8_t red = 0;
uint8_t green = 0;
uint8_t blue = 0;
@@ -107,6 +123,32 @@ int32_t ExternalNotificationModule::runOnce()
green = (colorState & 2) ? brightnessValues[brightnessIndex] : 0; // Green enabled on colorState = 2,3,6,7
blue = (colorState & 1) ? (brightnessValues[brightnessIndex] * 1.5) : 0; // Blue enabled on colorState = 1,3,5,7
white = (colorState & 12) ? brightnessValues[brightnessIndex] : 0;
#ifdef HAS_NCP5623
if (rgb_found.type == ScanI2C::NCP5623) {
rgb.setColor(red, green, blue);
}
#endif
#ifdef HAS_LP5562
if (rgb_found.type == ScanI2C::LP5562) {
rgbw.setColor(red, green, blue, white);
}
#endif
#ifdef RGBLED_CA
analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic
analogWrite(RGBLED_GREEN, 255 - green);
analogWrite(RGBLED_BLUE, 255 - blue);
#elif defined(RGBLED_RED)
analogWrite(RGBLED_RED, red);
analogWrite(RGBLED_GREEN, green);
analogWrite(RGBLED_BLUE, blue);
#endif
#ifdef HAS_NEOPIXEL
pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT);
pixels.show();
#endif
#ifdef UNPHONE
unphone.rgb(red, green, blue);
#endif
if (ascending) { // fade in
brightnessIndex++;
if (brightnessIndex == (sizeof(brightnessValues) - 1)) {
@@ -213,9 +255,34 @@ void ExternalNotificationModule::setExternalState(uint8_t index, bool on)
blue = 0;
white = 0;
}
ambientLightingThread->setLighting(moduleConfig.ambient_lighting.current, red, green, blue);
#endif
#ifdef HAS_NCP5623
if (rgb_found.type == ScanI2C::NCP5623) {
rgb.setColor(red, green, blue);
}
#endif
#ifdef HAS_LP5562
if (rgb_found.type == ScanI2C::LP5562) {
rgbw.setColor(red, green, blue, white);
}
#endif
#ifdef RGBLED_CA
analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic
analogWrite(RGBLED_GREEN, 255 - green);
analogWrite(RGBLED_BLUE, 255 - blue);
#elif defined(RGBLED_RED)
analogWrite(RGBLED_RED, red);
analogWrite(RGBLED_GREEN, green);
analogWrite(RGBLED_BLUE, blue);
#endif
#ifdef HAS_NEOPIXEL
pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT);
pixels.show();
#endif
#ifdef UNPHONE
unphone.rgb(red, green, blue);
#endif
#ifdef HAS_DRV2605
if (on) {
drv.go();
@@ -340,6 +407,33 @@ ExternalNotificationModule::ExternalNotificationModule()
LOG_INFO("Use Pin %i in PWM mode", config.device.buzzer_gpio);
}
}
#ifdef HAS_NCP5623
if (rgb_found.type == ScanI2C::NCP5623) {
rgb.begin();
rgb.setCurrent(10);
}
#endif
#ifdef HAS_LP5562
if (rgb_found.type == ScanI2C::LP5562) {
rgbw.begin();
rgbw.setCurrent(20);
}
#endif
#ifdef RGBLED_RED
pinMode(RGBLED_RED, OUTPUT); // set up the RGB led pins
pinMode(RGBLED_GREEN, OUTPUT);
pinMode(RGBLED_BLUE, OUTPUT);
#endif
#ifdef RGBLED_CA
analogWrite(RGBLED_RED, 255); // with a common anode type, logic is reversed
analogWrite(RGBLED_GREEN, 255); // so we want to initialise with lights off
analogWrite(RGBLED_BLUE, 255);
#endif
#ifdef HAS_NEOPIXEL
pixels.begin(); // Initialise the pixel(s)
pixels.clear(); // Set all pixel colors to 'off'
pixels.setBrightness(moduleConfig.ambient_lighting.current);
#endif
} else {
LOG_INFO("External Notification Module Disabled");
disable();
-5
View File
@@ -5,11 +5,6 @@
#include "configuration.h"
#include "input/InputBroker.h"
#ifdef HAS_RGB_LED
#include "AmbientLightingThread.h"
extern AmbientLightingThread *ambientLightingThread;
#endif
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !defined(CONFIG_IDF_TARGET_ESP32C6)
#include <NonBlockingRtttl.h>
#else
+3 -12
View File
@@ -1,11 +1,8 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
#include "buzz/BuzzerFeedbackThread.h"
#include "modules/SystemCommandsModule.h"
#endif
#include "modules/StatusLEDModule.h"
#if !MESHTASTIC_EXCLUDE_REPLYBOT
#include "ReplyBotModule.h"
#include "modules/SystemCommandsModule.h"
#endif
#if !MESHTASTIC_EXCLUDE_PKI
#include "KeyVerificationModule.h"
@@ -93,9 +90,6 @@
#if !MESHTASTIC_EXCLUDE_DROPZONE
#include "modules/DropzoneModule.h"
#endif
#if !MESHTASTIC_EXCLUDE_STATUS
#include "modules/StatusMessageModule.h"
#endif
#if defined(HAS_HARDWARE_WATCHDOG)
#include "watchdog/watchdogThread.h"
@@ -112,10 +106,10 @@ void setupModules()
buzzerFeedbackThread = new BuzzerFeedbackThread();
}
#endif
#if defined(LED_CHARGE) || defined(LED_PAIRING)
statusLEDModule = new StatusLEDModule();
#if !MESHTASTIC_EXCLUDE_REPLYBOT
new ReplyBotModule();
#endif
#if !MESHTASTIC_EXCLUDE_ADMIN
adminModule = new AdminModule();
#endif
@@ -156,9 +150,6 @@ void setupModules()
#if !MESHTASTIC_EXCLUDE_DROPZONE
dropzoneModule = new DropzoneModule();
#endif
#if !MESHTASTIC_EXCLUDE_STATUS
statusMessageModule = new StatusMessageModule();
#endif
#if !MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE
new GenericThreadModule();
#endif
+1 -1
View File
@@ -148,7 +148,7 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply()
// Strip the public key if the user is licensed
if (u.is_licensed && u.public_key.size > 0) {
memset(u.public_key.bytes, 0, sizeof(u.public_key.bytes));
u.public_key.bytes[0] = 0;
u.public_key.size = 0;
}
+3 -4
View File
@@ -1,4 +1,5 @@
#include "PowerStressModule.h"
#include "Led.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "PowerMon.h"
@@ -77,12 +78,10 @@ int32_t PowerStressModule::runOnce()
switch (p.cmd) {
case meshtastic_PowerStressMessage_Opcode_LED_ON:
// FIXME - implement
// ledForceOn.set(true);
ledForceOn.set(true);
break;
case meshtastic_PowerStressMessage_Opcode_LED_OFF:
// FIXME - implement
// ledForceOn.set(false);
ledForceOn.set(false);
break;
case meshtastic_PowerStressMessage_Opcode_GPS_ON:
// FIXME - implement
-183
View File
@@ -1,183 +0,0 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_REPLYBOT
/*
* ReplyBotModule.cpp
*
* This module implements a simple reply bot for the Meshtastic firmware. It listens for
* specific text commands ("/ping", "/hello" and "/test") delivered either via a direct
* message (DM) or a broadcast on the primary channel. When a supported command is
* received the bot responds with a short status message that includes the hop count
* (minimum number of relays), RSSI and SNR of the received packet. To avoid spamming
* the network it enforces a per‑sender cooldown between responses. By default the
* module is disabled. See the official firmware documentation for guidance on adding modules.
* To enable this module, set `#undef MESHTASTIC_EXCLUDE_REPLYBOT` in your variant.h file.
*/
#include "Channels.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "ReplyBotModule.h"
#include "mesh/MeshTypes.h"
#include <Arduino.h>
#include <cctype>
#include <cstring>
//
// Rate limiting data structures
//
// Each sender is tracked in a small ring buffer. When a message arrives from a
// sender we check the last time we responded to them. If the difference is
// less than the configured cooldown (different values for DM vs broadcast)
// the message is ignored; otherwise we update the last response time and
// proceed with replying.
struct ReplyBotCooldownEntry {
uint32_t from = 0;
uint32_t lastMs = 0;
};
static constexpr uint8_t REPLYBOT_COOLDOWN_SLOTS = 8; // ring buffer size
static constexpr uint32_t REPLYBOT_DM_COOLDOWN_MS = 15 * 1000; // 15 seconds for DMs
static constexpr uint32_t REPLYBOT_LF_COOLDOWN_MS = 60 * 1000; // 60 seconds for LongFast broadcasts
static ReplyBotCooldownEntry replybotCooldown[REPLYBOT_COOLDOWN_SLOTS];
static uint8_t replybotCooldownIdx = 0;
// Return true if a reply should be rate‑limited for this sender, updating the
// entry table as needed.
static bool replybotRateLimited(uint32_t from, uint32_t cooldownMs)
{
const uint32_t now = millis();
for (auto &e : replybotCooldown) {
if (e.from == from) {
// Found existing entry; check if cooldown expired
if ((uint32_t)(now - e.lastMs) < cooldownMs) {
return true;
}
e.lastMs = now;
return false;
}
}
// No entry found – insert new sender into the ring
replybotCooldown[replybotCooldownIdx].from = from;
replybotCooldown[replybotCooldownIdx].lastMs = now;
replybotCooldownIdx = (replybotCooldownIdx + 1) % REPLYBOT_COOLDOWN_SLOTS;
return false;
}
// Constructor – registers a single text port and marks the module promiscuous
// so that broadcast messages on the primary channel are visible.
ReplyBotModule::ReplyBotModule() : SinglePortModule("replybot", meshtastic_PortNum_TEXT_MESSAGE_APP)
{
isPromiscuous = true;
}
void ReplyBotModule::setup()
{
// In future we may add a protobuf configuration; for now the module is
// always enabled when compiled in.
}
// Determine whether we want to process this packet. We only care about
// plain text messages addressed to our port.
bool ReplyBotModule::wantPacket(const meshtastic_MeshPacket *p)
{
return (p && p->decoded.portnum == ourPortNum);
}
ProcessMessage ReplyBotModule::handleReceived(const meshtastic_MeshPacket &mp)
{
// Accept only direct messages to us or broadcasts on the Primary channel
// (regardless of modem preset: LongFast, MediumFast, etc).
const uint32_t ourNode = nodeDB->getNodeNum();
const bool isDM = (mp.to == ourNode);
const bool isPrimaryChannel = (mp.channel == channels.getPrimaryIndex()) && isBroadcast(mp.to);
if (!isDM && !isPrimaryChannel) {
return ProcessMessage::CONTINUE;
}
// Ignore empty payloads
if (mp.decoded.payload.size == 0) {
return ProcessMessage::CONTINUE;
}
// Copy payload into a null‑terminated buffer
char buf[260];
memset(buf, 0, sizeof(buf));
size_t n = mp.decoded.payload.size;
if (n > sizeof(buf) - 1)
n = sizeof(buf) - 1;
memcpy(buf, mp.decoded.payload.bytes, n);
// React only to supported slash commands
if (!isCommand(buf)) {
return ProcessMessage::CONTINUE;
}
// Apply rate limiting per sender depending on DM/broadcast
const uint32_t cooldownMs = isDM ? REPLYBOT_DM_COOLDOWN_MS : REPLYBOT_LF_COOLDOWN_MS;
if (replybotRateLimited(mp.from, cooldownMs)) {
return ProcessMessage::CONTINUE;
}
// Compute hop count indicator – if the relay_node is non‑zero we know
// there was at least one relay. Some firmware builds support a hop_start
// field which could be used for more accurate counts, but here we use
// the available relay_node flag only.
// int hopsAway = mp.hop_start - mp.hop_limit;
int hopsAway = getHopsAway(mp);
// Normalize RSSI: if positive adjust down by 200 to align with typical values
int rssi = mp.rx_rssi;
if (rssi > 0) {
rssi -= 200;
}
float snr = mp.rx_snr;
// Build the reply message and send it back via DM
char reply[96];
snprintf(reply, sizeof(reply), "🎙️ Mic Check : %d Hops away | RSSI %d | SNR %.1f", hopsAway, rssi, snr);
sendDm(mp, reply);
return ProcessMessage::CONTINUE;
}
// Check if the message starts with one of the supported commands. Leading
// whitespace is skipped and commands must be followed by end‑of‑string or
// whitespace.
bool ReplyBotModule::isCommand(const char *msg) const
{
if (!msg)
return false;
while (*msg == ' ' || *msg == '\t')
msg++;
auto isEndOrSpace = [](char c) { return c == '\0' || std::isspace(static_cast<unsigned char>(c)); };
if (strncmp(msg, "/ping", 5) == 0 && isEndOrSpace(msg[5]))
return true;
if (strncmp(msg, "/hello", 6) == 0 && isEndOrSpace(msg[6]))
return true;
if (strncmp(msg, "/test", 5) == 0 && isEndOrSpace(msg[5]))
return true;
return false;
}
// Send a direct message back to the originating node.
void ReplyBotModule::sendDm(const meshtastic_MeshPacket &rx, const char *text)
{
if (!text)
return;
meshtastic_MeshPacket *p = allocDataPacket();
p->to = rx.from;
p->channel = rx.channel;
p->want_ack = false;
p->decoded.want_response = false;
size_t len = strlen(text);
if (len > sizeof(p->decoded.payload.bytes)) {
len = sizeof(p->decoded.payload.bytes);
}
p->decoded.payload.size = len;
memcpy(p->decoded.payload.bytes, text, len);
service->sendToMesh(p);
}
#endif // MESHTASTIC_EXCLUDE_REPLYBOT
-19
View File
@@ -1,19 +0,0 @@
#pragma once
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_REPLYBOT
#include "SinglePortModule.h"
#include "mesh/generated/meshtastic/mesh.pb.h"
class ReplyBotModule : public SinglePortModule
{
public:
ReplyBotModule();
void setup() override;
bool wantPacket(const meshtastic_MeshPacket *p) override;
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
protected:
bool isCommand(const char *msg) const;
void sendDm(const meshtastic_MeshPacket &rx, const char *text);
};
#endif // MESHTASTIC_EXCLUDE_REPLYBOT
+7 -87
View File
@@ -13,10 +13,8 @@ StatusLEDModule::StatusLEDModule() : concurrency::OSThread("StatusLEDModule")
{
bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus);
powerStatusObserver.observe(&powerStatus->onNewStatus);
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
if (inputBroker)
inputObserver.observe(inputBroker);
#endif
}
int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg)
@@ -64,22 +62,19 @@ int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg)
}
return 0;
};
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
int StatusLEDModule::handleInputEvent(const InputEvent *event)
{
lastUserbuttonTime = millis();
return 0;
}
#endif
int32_t StatusLEDModule::runOnce()
{
my_interval = 1000;
if (power_state == charging) {
#ifndef POWER_LED_HARDWARE_BLINKS_WHILE_CHARGING
CHARGE_LED_state = !CHARGE_LED_state;
#endif
} else if (power_state == charged) {
CHARGE_LED_state = LED_STATE_ON;
} else if (power_state == critical) {
@@ -93,19 +88,13 @@ int32_t StatusLEDModule::runOnce()
my_interval = 250;
if (POWER_LED_starttime + 2000 < millis()) {
doing_fast_blink = false;
CHARGE_LED_state = LED_STATE_OFF;
}
}
}
if (power_state != charging && power_state != charged && !doing_fast_blink) {
if (CHARGE_LED_state == LED_STATE_ON) {
CHARGE_LED_state = LED_STATE_OFF;
my_interval = 999;
} else {
CHARGE_LED_state = LED_STATE_ON;
my_interval = 1;
CHARGE_LED_state = LED_STATE_OFF;
}
} else {
CHARGE_LED_state = LED_STATE_OFF;
}
if (!config.bluetooth.enabled || PAIRING_LED_starttime + 30 * 1000 < millis() || doing_fast_blink) {
@@ -123,11 +112,6 @@ int32_t StatusLEDModule::runOnce()
PAIRING_LED_state = LED_STATE_ON;
}
// Override if disabled in config
if (config.device.led_heartbeat_disabled) {
CHARGE_LED_state = LED_STATE_OFF;
}
#ifdef Battery_LED_1
bool chargeIndicatorLED1 = LED_STATE_OFF;
bool chargeIndicatorLED2 = LED_STATE_OFF;
bool chargeIndicatorLED3 = LED_STATE_OFF;
@@ -142,38 +126,14 @@ int32_t StatusLEDModule::runOnce()
if (powerStatus && powerStatus->getBatteryChargePercent() >= 75)
chargeIndicatorLED4 = LED_STATE_ON;
}
#endif
#if defined(HAS_PMU)
if (pmu_found && PMU) {
// blink the axp led
PMU->setChargingLedMode(CHARGE_LED_state ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF);
}
#endif
#ifdef PCA_LED_POWER
io.digitalWrite(PCA_LED_POWER, CHARGE_LED_state);
#endif
#ifdef PCA_LED_ENABLE
io.digitalWrite(PCA_LED_ENABLE, CHARGE_LED_state);
#endif
#ifdef LED_POWER
digitalWrite(LED_POWER, CHARGE_LED_state);
#ifdef LED_CHARGE
digitalWrite(LED_CHARGE, CHARGE_LED_state);
#endif
#ifdef LED_PAIRING
digitalWrite(LED_PAIRING, PAIRING_LED_state);
#endif
#ifdef RGB_LED_POWER
if (!config.device.led_heartbeat_disabled) {
if (CHARGE_LED_state == LED_STATE_ON) {
ambientLightingThread->setLighting(10, 255, 0, 0);
} else {
ambientLightingThread->setLighting(0, 0, 0, 0);
}
}
#endif
#ifdef Battery_LED_1
digitalWrite(Battery_LED_1, chargeIndicatorLED1);
#endif
@@ -189,43 +149,3 @@ int32_t StatusLEDModule::runOnce()
return (my_interval);
}
void StatusLEDModule::setPowerLED(bool LEDon)
{
#if defined(HAS_PMU)
if (pmu_found && PMU) {
// blink the axp led
PMU->setChargingLedMode(LEDon ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF);
}
#endif
if (LEDon)
LEDon = LED_STATE_ON;
else
LEDon = LED_STATE_OFF;
#ifdef PCA_LED_POWER
io.digitalWrite(PCA_LED_POWER, LEDon);
#endif
#ifdef PCA_LED_ENABLE
io.digitalWrite(PCA_LED_ENABLE, LEDon);
#endif
#ifdef LED_POWER
digitalWrite(LED_POWER, LEDon);
#endif
#ifdef LED_PAIRING
digitalWrite(LED_PAIRING, LEDon);
#endif
#ifdef Battery_LED_1
digitalWrite(Battery_LED_1, LEDon);
#endif
#ifdef Battery_LED_2
digitalWrite(Battery_LED_2, LEDon);
#endif
#ifdef Battery_LED_3
digitalWrite(Battery_LED_3, LEDon);
#endif
#ifdef Battery_LED_4
digitalWrite(Battery_LED_4, LEDon);
#endif
}
+2 -15
View File
@@ -5,14 +5,10 @@
#include "PowerStatus.h"
#include "concurrency/OSThread.h"
#include "configuration.h"
#include "main.h"
#include "input/InputBroker.h"
#include <Arduino.h>
#include <functional>
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
#include "input/InputBroker.h"
#endif
class StatusLEDModule : private concurrency::OSThread
{
bool slowTrack = false;
@@ -21,11 +17,8 @@ class StatusLEDModule : private concurrency::OSThread
StatusLEDModule();
int handleStatusUpdate(const meshtastic::Status *);
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
int handleInputEvent(const InputEvent *arg);
#endif
void setPowerLED(bool);
int handleInputEvent(const InputEvent *arg);
protected:
unsigned int my_interval = 1000; // interval in millisconds
@@ -35,10 +28,8 @@ class StatusLEDModule : private concurrency::OSThread
CallbackObserver<StatusLEDModule, const meshtastic::Status *>(this, &StatusLEDModule::handleStatusUpdate);
CallbackObserver<StatusLEDModule, const meshtastic::Status *> powerStatusObserver =
CallbackObserver<StatusLEDModule, const meshtastic::Status *>(this, &StatusLEDModule::handleStatusUpdate);
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
CallbackObserver<StatusLEDModule, const InputEvent *> inputObserver =
CallbackObserver<StatusLEDModule, const InputEvent *>(this, &StatusLEDModule::handleInputEvent);
#endif
private:
bool CHARGE_LED_state = LED_STATE_OFF;
@@ -59,7 +50,3 @@ class StatusLEDModule : private concurrency::OSThread
};
extern StatusLEDModule *statusLEDModule;
#ifdef RGB_LED_POWER
#include "AmbientLightingThread.h"
extern AmbientLightingThread *ambientLightingThread;
#endif
-41
View File
@@ -1,41 +0,0 @@
#if !MESHTASTIC_EXCLUDE_STATUS
#include "StatusMessageModule.h"
#include "MeshService.h"
#include "ProtobufModule.h"
StatusMessageModule *statusMessageModule;
int32_t StatusMessageModule::runOnce()
{
if (moduleConfig.has_statusmessage && moduleConfig.statusmessage.node_status[0] != '\0') {
// create and send message with the status message set
meshtastic_StatusMessage ourStatus = meshtastic_StatusMessage_init_zero;
strncpy(ourStatus.status, moduleConfig.statusmessage.node_status, sizeof(ourStatus.status));
ourStatus.status[sizeof(ourStatus.status) - 1] = '\0'; // ensure null termination
meshtastic_MeshPacket *p = allocDataPacket();
p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes),
meshtastic_StatusMessage_fields, &ourStatus);
p->to = NODENUM_BROADCAST;
p->decoded.want_response = false;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
p->channel = 0;
service->sendToMesh(p);
}
return 1000 * 12 * 60 * 60;
}
ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
{
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
meshtastic_StatusMessage incomingMessage;
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_StatusMessage_fields,
&incomingMessage)) {
LOG_INFO("Received a NodeStatus message %s", incomingMessage.status);
}
}
return ProcessMessage::CONTINUE;
}
#endif
-35
View File
@@ -1,35 +0,0 @@
#pragma once
#if !MESHTASTIC_EXCLUDE_STATUS
#include "SinglePortModule.h"
#include "configuration.h"
class StatusMessageModule : public SinglePortModule, private concurrency::OSThread
{
public:
/** Constructor
* name is for debugging output
*/
StatusMessageModule()
: SinglePortModule("statusMessage", meshtastic_PortNum_NODE_STATUS_APP), concurrency::OSThread("StatusMessage")
{
if (moduleConfig.has_statusmessage && moduleConfig.statusmessage.node_status[0] != '\0') {
this->setInterval(2 * 60 * 1000);
} else {
this->setInterval(1000 * 12 * 60 * 60);
}
// TODO: If we have a string, set the initial delay (15 minutes maybe)
}
virtual int32_t runOnce() override;
protected:
/** Called to handle a particular incoming message
*/
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
private:
};
extern StatusMessageModule *statusMessageModule;
#endif
+12 -32
View File
@@ -1,4 +1,3 @@
#include "DebugConfiguration.h"
#include "configuration.h"
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
@@ -26,9 +25,6 @@
#if __has_include(<SensirionI2cScd4x.h>)
#include "Sensor/SCD4XSensor.h"
#endif
#if __has_include(<SensirionI2cSfa3x.h>)
#include "Sensor/SFA30Sensor.h"
#endif
void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
{
@@ -54,9 +50,6 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
#if __has_include(<SensirionI2cScd4x.h>)
addSensor<SCD4XSensor>(i2cScanner, ScanI2C::DeviceType::SCD4X);
#endif
#if __has_include(<SensirionI2cSfa3x.h>)
addSensor<SFA30Sensor>(i2cScanner, ScanI2C::DeviceType::SFA30);
#endif
}
int32_t AirQualityTelemetryModule::runOnce()
@@ -78,8 +71,7 @@ int32_t AirQualityTelemetryModule::runOnce()
}
if (firstTime) {
// This is the first time the OSThread library has called this function, so
// do some setup
// This is the first time the OSThread library has called this function, so do some setup
firstTime = false;
if (moduleConfig.telemetry.air_quality_enabled) {
@@ -229,8 +221,6 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta
entries.push_back("PM10: " + String(m.pm100_standard) + "ug/m3");
if (m.has_co2)
entries.push_back("CO2: " + String(m.co2) + "ppm");
if (m.has_form_formaldehyde)
entries.push_back("HCHO: " + String(m.form_formaldehyde) + "ppb");
// === Show first available metric on top-right of first line ===
if (!entries.empty()) {
@@ -266,19 +256,17 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
const char *sender = getSenderShortName(mp);
if (t->variant.air_quality_metrics.has_pm10_standard)
LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, "
"pm100_standard=%i",
sender, t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard,
t->variant.air_quality_metrics.pm100_standard);
LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, pm100_standard=%i", sender,
t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard,
t->variant.air_quality_metrics.pm100_standard);
if (t->variant.air_quality_metrics.has_co2)
LOG_INFO("CO2=%i, CO2_T=%.2f, CO2_H=%.2f", t->variant.air_quality_metrics.co2,
t->variant.air_quality_metrics.co2_temperature, t->variant.air_quality_metrics.co2_humidity);
// TODO - Decide what to do with these
// LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
// t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental,
// t->variant.air_quality_metrics.pm100_environmental);
if (t->variant.air_quality_metrics.has_form_formaldehyde)
LOG_INFO("HCHO=%.2f, HCHO_T=%.2f, HCHO_H=%.2f", t->variant.air_quality_metrics.form_formaldehyde,
t->variant.air_quality_metrics.form_temperature, t->variant.air_quality_metrics.form_humidity);
LOG_INFO(" | CO2=%i, CO2_T=%f, CO2_H=%f", t->variant.air_quality_metrics.co2,
t->variant.air_quality_metrics.co2_temperature, t->variant.air_quality_metrics.co2_humidity);
#endif
// release previous packet before occupying a new spot
if (lastMeasurementPacket != nullptr)
@@ -366,18 +354,10 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
m.variant.air_quality_metrics.has_co2_humidity;
if (hasAnyCO2) {
LOG_INFO("Send: co2=%i, co2_t=%.2f, co2_rh=%.2f", m.variant.air_quality_metrics.co2,
LOG_INFO("Send: co2=%i, co2_t=%f, co2_rh=%f", m.variant.air_quality_metrics.co2,
m.variant.air_quality_metrics.co2_temperature, m.variant.air_quality_metrics.co2_humidity);
}
bool hasAnyHCHO = m.variant.air_quality_metrics.has_form_formaldehyde ||
m.variant.air_quality_metrics.has_form_temperature || m.variant.air_quality_metrics.has_form_humidity;
if (hasAnyHCHO) {
LOG_INFO("Send: hcho=%.2f, hcho_t=%.2f, hcho_rh=%.2f", m.variant.air_quality_metrics.form_formaldehyde,
m.variant.air_quality_metrics.form_temperature, m.variant.air_quality_metrics.form_humidity);
}
meshtastic_MeshPacket *p = allocDataProtobuf(m);
p->to = dest;
p->decoded.want_response = false;
@@ -446,4 +426,4 @@ AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule(
return result;
}
#endif
#endif
+1 -2
View File
@@ -64,9 +64,8 @@ class AirQualityTelemetryModule : private concurrency::OSThread,
bool firstTime = true;
meshtastic_MeshPacket *lastMeasurementPacket;
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
// uint32_t sendToPhoneIntervalMs = 1000; // Send to phone every minute
uint32_t lastSentToMesh = 0;
uint32_t lastSentToPhone = 0;
};
#endif
#endif
@@ -138,10 +138,6 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
measurement->variant.air_quality_metrics.has_particles_100um = true;
measurement->variant.air_quality_metrics.particles_100um = read16(buffer, 26);
LOG_DEBUG("Got %s readings: pM1p0_standard=%u, pM2p5_standard=%u, pM10p0_standard=%u", sensorName,
measurement->variant.air_quality_metrics.pm10_standard, measurement->variant.air_quality_metrics.pm25_standard,
measurement->variant.air_quality_metrics.pm100_standard);
return true;
}
+29 -62
View File
@@ -125,7 +125,7 @@ bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement)
reClockI2C(currentClock, _bus, false);
#endif
LOG_DEBUG("Got %s readings: co2=%u, co2_temp=%.2f, co2_hum%.2f", sensorName, co2, temperature, humidity);
LOG_DEBUG("%s readings: %u ppm, %.2f degC, %.2f %rh", sensorName, co2, temperature, humidity);
if (error != SCD4X_NO_ERROR) {
LOG_DEBUG("%s: Error while getting measurements: %u", sensorName, error);
if (co2 == 0) {
@@ -333,6 +333,12 @@ bool SCD4XSensor::setASC(bool ascEnabled)
return false;
}
if (ascActive) {
LOG_INFO("%s: ASC is enabled", sensorName);
} else {
LOG_INFO("%s: ASC is disabled", sensorName);
}
return true;
}
@@ -352,7 +358,7 @@ bool SCD4XSensor::setASC(bool ascEnabled)
bool SCD4XSensor::setASCBaseline(uint32_t targetCO2)
{
// TODO - Remove?
// Available in library, but not described in datasheet.
// Available in library, but not described in datasheet.
uint16_t error;
LOG_INFO("%s: Setting ASC baseline to: %u", sensorName, targetCO2);
@@ -534,7 +540,6 @@ bool SCD4XSensor::setAltitude(uint32_t altitude)
if (!stopMeasurement()) {
return false;
}
LOG_INFO("%s: setting altitude at %um", sensorName, altitude);
error = scd4x.setSensorAltitude(altitude);
@@ -543,15 +548,11 @@ bool SCD4XSensor::setAltitude(uint32_t altitude)
return false;
}
// NOTE: this gives an error if issued. Sensirion's library
// doesn't indicate it's needed.
// error = scd4x.persistSettings();
// if (error != SCD4X_NO_ERROR) {
// LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error);
// return false;
// }
LOG_INFO("%s: altitude set", sensorName);
error = scd4x.persistSettings();
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error);
return false;
}
return true;
}
@@ -574,8 +575,6 @@ bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure)
{
uint16_t error;
LOG_INFO("%s: setting ambient pressure at %u Pa", sensorName, ambientPressure);
error = scd4x.setAmbientPressure(ambientPressure);
if (error != SCD4X_NO_ERROR) {
@@ -590,8 +589,6 @@ bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure)
return false;
}
LOG_INFO("%s: ambient pressure set set", sensorName);
return true;
}
@@ -827,28 +824,15 @@ AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPa
if (request->sensor_config.scd4x_config.has_factory_reset) {
LOG_DEBUG("%s: Requested factory reset", sensorName);
if (!this->factoryReset()) {
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
this->factoryReset();
} else {
if (request->sensor_config.scd4x_config.has_set_asc) {
getASC(ascActive);
bool currentASC = ascActive;
this->setASC(request->sensor_config.scd4x_config.set_asc);
if (request->sensor_config.scd4x_config.set_asc == false) {
LOG_DEBUG("%s: Request for FRC", sensorName);
if (request->sensor_config.scd4x_config.has_set_target_co2_conc) {
if (this->setASC(request->sensor_config.scd4x_config.set_asc)) {
if (!this->performFRC(request->sensor_config.scd4x_config.set_target_co2_conc)) {
result = AdminMessageHandleResult::NOT_HANDLED;
// Set it back to ASC if failed
setASC(currentASC);
break;
};
} else {
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
this->performFRC(request->sensor_config.scd4x_config.set_target_co2_conc);
} else {
// FRC requested but no target CO2 provided
LOG_ERROR("%s: target CO2 not provided", sensorName);
@@ -857,17 +841,12 @@ AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPa
}
} else {
LOG_DEBUG("%s: Request for ASC", sensorName);
if (this->setASC(request->sensor_config.scd4x_config.set_asc)) {
if (request->sensor_config.scd4x_config.has_set_target_co2_conc) {
LOG_DEBUG("%s: Request has target CO2", sensorName);
this->setASCBaseline(request->sensor_config.scd4x_config.set_target_co2_conc);
// NOTE - in this situation, if we set ASC, but baseline set fails, we stay on ASC
} else {
LOG_DEBUG("%s: Request doesn't have target CO2", sensorName);
}
if (request->sensor_config.scd4x_config.has_set_target_co2_conc) {
LOG_DEBUG("%s: Request has target CO2", sensorName);
// TODO - Remove? see setASCBaseline function
this->setASCBaseline(request->sensor_config.scd4x_config.set_target_co2_conc);
} else {
result = AdminMessageHandleResult::NOT_HANDLED;
break;
LOG_DEBUG("%s: Request doesn't have target CO2", sensorName);
}
}
}
@@ -876,36 +855,27 @@ AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPa
// NOTE: this requires to have a sensor working on stable environment
// And to make it between readings
if (request->sensor_config.scd4x_config.has_set_temperature) {
if (!this->setTemperature(request->sensor_config.scd4x_config.set_temperature)) {
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
this->setTemperature(request->sensor_config.scd4x_config.set_temperature);
}
// Check for altitude or pressure offset
if (request->sensor_config.scd4x_config.has_set_altitude) {
if (!this->setAltitude(request->sensor_config.scd4x_config.set_altitude)) {
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
this->setAltitude(request->sensor_config.scd4x_config.set_altitude);
} else if (request->sensor_config.scd4x_config.has_set_ambient_pressure) {
if (!this->setAmbientPressure(request->sensor_config.scd4x_config.set_ambient_pressure)) {
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
this->setAmbientPressure(request->sensor_config.scd4x_config.set_ambient_pressure);
}
// Check for low power mode
// NOTE: to switch from one mode to another do:
// setPowerMode -> startMeasurement
if (request->sensor_config.scd4x_config.has_set_power_mode) {
if (!this->setPowerMode(request->sensor_config.scd4x_config.set_power_mode)) {
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
this->setPowerMode(request->sensor_config.scd4x_config.set_power_mode);
}
}
// Start measurement mode
this->startMeasurement();
result = AdminMessageHandleResult::HANDLED;
break;
@@ -913,9 +883,6 @@ AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPa
result = AdminMessageHandleResult::NOT_HANDLED;
}
// Start measurement mode
this->startMeasurement();
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
+12 -23
View File
@@ -665,16 +665,16 @@ bool SEN5XSensor::readValues()
sen5xmeasurement.vocIndex = !isnan(int_vocIndex) ? int_vocIndex / 10.0f : FLT_MAX;
sen5xmeasurement.noxIndex = !isnan(int_noxIndex) ? int_noxIndex / 10.0f : FLT_MAX;
LOG_DEBUG("Got %s readings: pM1p0=%u, pM2p5=%u, pM4p0=%u, pM10p0=%u", sensorName, sen5xmeasurement.pM1p0,
sen5xmeasurement.pM2p5, sen5xmeasurement.pM4p0, sen5xmeasurement.pM10p0);
LOG_DEBUG("Got: pM1p0=%u, pM2p5=%u, pM4p0=%u, pM10p0=%u", sen5xmeasurement.pM1p0, sen5xmeasurement.pM2p5,
sen5xmeasurement.pM4p0, sen5xmeasurement.pM10p0);
if (model != SEN50) {
LOG_DEBUG("Got %s readings: humidity=%.2f, temperature=%.2f, vocIndex=%.2f", sensorName, sen5xmeasurement.humidity,
sen5xmeasurement.temperature, sen5xmeasurement.vocIndex);
LOG_DEBUG("Got: humidity=%.2f, temperature=%.2f, vocIndex=%.2f", sen5xmeasurement.humidity, sen5xmeasurement.temperature,
sen5xmeasurement.vocIndex);
}
if (model == SEN55) {
LOG_DEBUG("Got %s readings: noxIndex=%.2f", sensorName, sen5xmeasurement.noxIndex);
LOG_DEBUG("Got: noxIndex=%.2f", sen5xmeasurement.noxIndex);
}
return true;
@@ -727,9 +727,9 @@ bool SEN5XSensor::readPNValues(bool cumulative)
sen5xmeasurement.pN1p0 -= sen5xmeasurement.pN0p5;
}
LOG_DEBUG("Got %s readings: pN0p5=%u, pN1p0=%u, pN2p5=%u, pN4p0=%u, pN10p0=%u, tSize=%.2f", sensorName,
sen5xmeasurement.pN0p5, sen5xmeasurement.pN1p0, sen5xmeasurement.pN2p5, sen5xmeasurement.pN4p0,
sen5xmeasurement.pN10p0, sen5xmeasurement.tSize);
LOG_DEBUG("Got: pN0p5=%u, pN1p0=%u, pN2p5=%u, pN4p0=%u, pN10p0=%u, tSize=%.2f", sen5xmeasurement.pN0p5,
sen5xmeasurement.pN1p0, sen5xmeasurement.pN2p5, sen5xmeasurement.pN4p0, sen5xmeasurement.pN10p0,
sen5xmeasurement.tSize);
return true;
}
@@ -919,11 +919,6 @@ bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement)
void SEN5XSensor::setMode(bool setOneShot)
{
oneShotMode = setOneShot;
if (oneShotMode) {
LOG_INFO("%s setting mode to one shot mode", sensorName);
} else {
LOG_INFO("%s setting mode to continuous mode", sensorName);
}
}
AdminMessageHandleResult SEN5XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
@@ -939,22 +934,16 @@ AdminMessageHandleResult SEN5XSensor::handleAdminMessage(const meshtastic_MeshPa
break;
}
// Check for one-shot/continuous mode request
if (request->sensor_config.sen5x_config.has_set_one_shot_mode) {
this->setMode(request->sensor_config.sen5x_config.set_one_shot_mode);
}
// TODO - Add admin command to set temperature offset?
// TODO - Add admin command to set temperature offset
// Check for temperature offset
// if (request->sensor_config.sen5x_config.has_set_temperature) {
// this->setTemperature(request->sensor_config.sen5x_config.set_temperature);
// }
// TODO - Add admin command to trigger fan cleaning?
// Check for one-shot/continuous mode request
// if (request->sensor_config.sen5x_config.has_fan_cleaning && request->sensor_config.sen5x_config.fan_cleaning) {
// this->startCleaning();
// }
if (request->sensor_config.sen5x_config.has_set_one_shot_mode) {
this->setMode(request->sensor_config.sen5x_config.set_one_shot_mode);
}
result = AdminMessageHandleResult::HANDLED;
break;
@@ -1,198 +0,0 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include(<SensirionI2cSfa3x.h>)
#include "../detect/reClockI2C.h"
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "SFA30Sensor.h"
SFA30Sensor::SFA30Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SFA30, "SFA30"){};
bool SFA30Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
{
LOG_INFO("Init sensor: %s", sensorName);
_bus = bus;
_address = dev->address.address;
#ifdef SFA30_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return false;
#endif /* CAN_RECLOCK_I2C */
#endif /* SFA30_I2C_CLOCK_SPEED */
sfa30.begin(*_bus, _address);
delay(20);
if (this->isError(sfa30.deviceReset())) {
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return false;
}
state = State::IDLE;
if (this->isError(sfa30.startContinuousMeasurement())) {
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return false;
}
LOG_INFO("%s starting measurement", sensorName);
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
status = 1;
state = State::ACTIVE;
measureStarted = getTime();
LOG_INFO("%s Enabled", sensorName);
initI2CSensor();
return true;
};
bool SFA30Sensor::isError(uint16_t response)
{
if (response == SFA30_NO_ERROR)
return false;
// TODO - Check error to char conversion
LOG_ERROR("%s: %u", sensorName, response);
return true;
}
void SFA30Sensor::sleep()
{
#ifdef SFA30_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return;
#endif /* CAN_RECLOCK_I2C */
#endif /* SFA30_I2C_CLOCK_SPEED */
// Note - not recommended for this sensor on a periodic basis
if (this->isError(sfa30.stopMeasurement())) {
LOG_ERROR("%s: can't stop measurement", sensorName);
};
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
LOG_INFO("%s: stop measurement", sensorName);
state = State::IDLE;
measureStarted = 0;
}
uint32_t SFA30Sensor::wakeUp()
{
#ifdef SFA30_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return false;
#endif /* CAN_RECLOCK_I2C */
#endif /* SFA30_I2C_CLOCK_SPEED */
LOG_INFO("Waking up %s", sensorName);
if (this->isError(sfa30.startContinuousMeasurement())) {
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return 0;
}
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
state = State::ACTIVE;
measureStarted = getTime();
return SFA30_WARMUP_MS;
}
int32_t SFA30Sensor::wakeUpTimeMs()
{
return SFA30_WARMUP_MS;
}
bool SFA30Sensor::canSleep()
{
// Sleep is disabled in this sensor because readings are not tested with periodic sleep
// with such low power consumption, prefered to keep it active
return false;
}
bool SFA30Sensor::isActive()
{
return state == State::ACTIVE;
}
int32_t SFA30Sensor::pendingForReadyMs()
{
uint32_t now;
now = getTime();
uint32_t sinceHchoMeasureStarted = (now - measureStarted) * 1000;
LOG_DEBUG("%s: Since measure started: %ums", sensorName, sinceHchoMeasureStarted);
if (sinceHchoMeasureStarted < SFA30_WARMUP_MS) {
LOG_INFO("%s: not enough time passed since starting measurement", sensorName);
return SFA30_WARMUP_MS - sinceHchoMeasureStarted;
}
return 0;
}
bool SFA30Sensor::getMetrics(meshtastic_Telemetry *measurement)
{
float hcho = 0.0;
float humidity = 0.0;
float temperature = 0.0;
#ifdef SFA30_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return false;
#endif /* CAN_RECLOCK_I2C */
#endif /* SFA30_I2C_CLOCK_SPEED */
if (this->isError(sfa30.readMeasuredValues(hcho, humidity, temperature))) {
LOG_WARN("%s: No values", sensorName);
return false;
}
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
measurement->variant.air_quality_metrics.has_form_temperature = true;
measurement->variant.air_quality_metrics.has_form_humidity = true;
measurement->variant.air_quality_metrics.has_form_formaldehyde = true;
measurement->variant.air_quality_metrics.form_temperature = temperature;
measurement->variant.air_quality_metrics.form_humidity = humidity;
measurement->variant.air_quality_metrics.form_formaldehyde = hcho;
LOG_DEBUG("Got %s readings: hcho=%.2f, hcho_temp=%.2f, hcho_hum=%.2f", sensorName, hcho, temperature, humidity);
return true;
}
#endif
@@ -1,39 +0,0 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include(<SensirionI2cSfa3x.h>)
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "RTC.h"
#include "TelemetrySensor.h"
#include <SensirionI2cSfa3x.h>
#define SFA30_I2C_CLOCK_SPEED 100000
#define SFA30_WARMUP_MS 10000
#define SFA30_NO_ERROR 0
class SFA30Sensor : public TelemetrySensor
{
private:
enum class State { IDLE, ACTIVE };
State state = State::IDLE;
uint32_t measureStarted = 0;
SensirionI2cSfa3x sfa30;
TwoWire *_bus{};
uint8_t _address{};
bool isError(uint16_t response);
public:
SFA30Sensor();
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
virtual bool isActive() override;
virtual void sleep() override;
virtual uint32_t wakeUp() override;
virtual bool canSleep() override;
virtual int32_t wakeUpTimeMs() override;
virtual int32_t pendingForReadyMs() override;
};
#endif
+5 -1
View File
@@ -757,7 +757,11 @@ void NimbleBluetooth::deinit()
isDeInit = true;
#ifdef BLE_LED
digitalWrite(BLE_LED, LED_STATE_OFF);
#ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, HIGH);
#else
digitalWrite(BLE_LED, LOW);
#endif
#endif
#ifndef NIMBLE_TWO
NimBLEDevice::deinit();
+1 -1
View File
@@ -158,7 +158,7 @@
#endif
#ifdef PIN_LED1
#define LED_POWER PIN_LED1 // LED1 on nrf52840-DK
#define LED_PIN PIN_LED1 // LED1 on nrf52840-DK
#endif
#ifdef PIN_BUTTON1
-8
View File
@@ -872,7 +872,6 @@ bool loadConfig(const char *configPath)
}
if (yamlConfig["Config"]) {
portduino_config.has_config_overrides = true;
if (yamlConfig["Config"]["DisplayMode"]) {
portduino_config.has_configDisplayMode = true;
if ((yamlConfig["Config"]["DisplayMode"]).as<std::string>("") == "TWOCOLOR") {
@@ -885,13 +884,6 @@ bool loadConfig(const char *configPath)
portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT;
}
}
if (yamlConfig["Config"]["StatusMessage"]) {
portduino_config.has_statusMessage = true;
portduino_config.statusMessage = (yamlConfig["Config"]["StatusMessage"]).as<std::string>("");
}
if ((yamlConfig["Config"]["EnableUDP"]).as<bool>(false)) {
portduino_config.enable_UDP = true;
}
}
if (yamlConfig["General"]) {
+14 -27
View File
@@ -177,12 +177,8 @@ extern struct portduino_config_struct {
int hostMetrics_channel = 0;
// config
bool has_config_overrides = false;
int configDisplayMode = 0;
bool has_configDisplayMode = false;
std::string statusMessage = "";
bool has_statusMessage = false;
bool enable_UDP = false;
// General
std::string mac_address = "";
@@ -509,30 +505,21 @@ extern struct portduino_config_struct {
}
// config
if (has_config_overrides) {
if (has_configDisplayMode) {
out << YAML::Key << "Config" << YAML::Value << YAML::BeginMap;
if (has_configDisplayMode) {
switch (configDisplayMode) {
case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR:
out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR";
break;
case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED:
out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED";
break;
case meshtastic_Config_DisplayConfig_DisplayMode_COLOR:
out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR";
break;
case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT:
out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT";
break;
}
}
if (has_statusMessage) {
out << YAML::Key << "StatusMessage" << YAML::Value << statusMessage;
}
if (enable_UDP) {
out << YAML::Key << "EnableUDP" << YAML::Value << true;
switch (configDisplayMode) {
case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR:
out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR";
break;
case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED:
out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED";
break;
case meshtastic_Config_DisplayConfig_DisplayMode_COLOR:
out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR";
break;
case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT:
out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT";
break;
}
out << YAML::EndMap; // Config
+2 -4
View File
@@ -103,10 +103,8 @@ class Power : private concurrency::OSThread
bool axpChipInit();
/// Setup a simple ADC input based battery sensor
bool analogInit();
/// Setup cw2015 battery level sensor
bool cw2015Init();
/// Setup a 17048 battery level sensor
bool max17048Init();
/// Setup a Lipo battery level sensor
bool lipoInit();
/// Setup a Lipo charger
bool lipoChargerInit();
/// Setup a meshSolar battery sensor
@@ -158,15 +158,6 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
if (decoded->variant.air_quality_metrics.has_co2_humidity) {
msgPayload["co2_humidity"] = new JSONValue(decoded->variant.air_quality_metrics.co2_humidity);
}
if (decoded->variant.air_quality_metrics.has_form_formaldehyde) {
msgPayload["form_formaldehyde"] = new JSONValue(decoded->variant.air_quality_metrics.form_formaldehyde);
}
if (decoded->variant.air_quality_metrics.has_form_temperature) {
msgPayload["form_temperature"] = new JSONValue(decoded->variant.air_quality_metrics.form_temperature);
}
if (decoded->variant.air_quality_metrics.has_form_humidity) {
msgPayload["form_humidity"] = new JSONValue(decoded->variant.air_quality_metrics.form_humidity);
}
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
if (decoded->variant.power_metrics.has_ch1_voltage) {
msgPayload["voltage_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_voltage);
@@ -129,15 +129,6 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
if (decoded->variant.air_quality_metrics.has_co2_humidity) {
jsonObj["payload"]["co2_humidity"] = decoded->variant.air_quality_metrics.co2_humidity;
}
if (decoded->variant.air_quality_metrics.has_form_formaldehyde) {
jsonObj["payload"]["form_formaldehyde"] = decoded->variant.air_quality_metrics.form_formaldehyde;
}
if (decoded->variant.air_quality_metrics.has_form_temperature) {
jsonObj["payload"]["form_temperature"] = decoded->variant.air_quality_metrics.form_temperature;
}
if (decoded->variant.air_quality_metrics.has_form_humidity) {
jsonObj["payload"]["form_humidity"] = decoded->variant.air_quality_metrics.form_humidity;
}
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
if (decoded->variant.power_metrics.has_ch1_voltage) {
jsonObj["payload"]["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage;
+3 -2
View File
@@ -5,6 +5,7 @@
#endif
#include "Default.h"
#include "Led.h"
#include "MeshRadio.h"
#include "MeshService.h"
#include "NodeDB.h"
@@ -12,7 +13,6 @@
#include "detect/LoRaRadioType.h"
#include "error.h"
#include "main.h"
#include "modules/StatusLEDModule.h"
#include "sleep.h"
#include "target_specific.h"
@@ -268,7 +268,8 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN
digitalWrite(PIN_WD_EN, LOW);
#endif
#endif
statusLEDModule->setPowerLED(false);
ledBlink.set(false);
#ifdef RESET_OLED
digitalWrite(RESET_OLED, 1); // put the display in reset before killing its power
#endif
@@ -14,7 +14,7 @@
#define LORA_CS 5
#define RF95_FAN_EN 17
// This is a LED_WS2812 not a standard LED
// #define LED_PIN 16 // This is a LED_WS2812 not a standard LED
#define HAS_NEOPIXEL // Enable the use of neopixels
#define NEOPIXEL_COUNT 1 // How many neopixels are connected
#define NEOPIXEL_DATA 16 // gpio pin used to send data to the neopixels
+1 -1
View File
@@ -20,7 +20,7 @@
#define LORA_DIO2
#define LORA_DIO3
#define LED_POWER 16 // green - blue is at 17
#define LED_PIN 16 // green - blue is at 17
#define BUTTON_PIN 25
#define BUTTON_NEED_PULLUP
+2
View File
@@ -23,6 +23,8 @@
#define SX126X_TXEN RADIOLIB_NC
#define SX126X_RXEN RADIOLIB_NC
// Status
// #define LED_PIN 1
// External notification
// FIXME: Check if EXT_NOTIFY_OUT actualy has any effect and removes the need for setting the external notication pin in the
// app/preferences
@@ -21,8 +21,8 @@
#define BUTTON_PIN 15 // Right side button - if not available, set device.button_gpio to 0 from Meshtastic client
// LEDs
#define LED_POWER 13 // Tx LED
#define USER_LED 2 // Rx LED
#define LED_PIN 13 // Tx LED
#define USER_LED 2 // Rx LED
// Buzzer
#define PIN_BUZZER 33
+1 -1
View File
@@ -15,7 +15,7 @@
#define ADC_MULTIPLIER 1.85 // (R1 = 470k, R2 = 680k)
#define EXT_PWR_DETECT 4 // Pin to detect connected external power source for LILYGO® TTGO T-Energy T18 and other DIY boards
#define EXT_NOTIFY_OUT 12 // Overridden default pin to use for Ext Notify Module (#975).
#define LED_POWER 2 // add status LED (compatible with core-pcb and DIY targets)
#define LED_PIN 2 // add status LED (compatible with core-pcb and DIY targets)
// Radio
#define USE_SX1262 // E22-900M30S uses SX1262
+1 -1
View File
@@ -15,7 +15,7 @@
#define ADC_MULTIPLIER 1.85 // (R1 = 470k, R2 = 680k)
#define EXT_PWR_DETECT 4 // Pin to detect connected external power source for LILYGO® TTGO T-Energy T18 and other DIY boards
#define EXT_NOTIFY_OUT 12 // Overridden default pin to use for Ext Notify Module (#975).
#define LED_POWER 2 // add status LED (compatible with core-pcb and DIY targets)
#define LED_PIN 2 // add status LED (compatible with core-pcb and DIY targets)
#define LORA_DIO0 26 // a No connect on the SX1262/SX1268 module
#define LORA_RESET 23 // RST for SX1276, and for SX1262/SX1268
@@ -3,7 +3,7 @@
// HACKBOX LoRa IO Kit
// Uses a ESP-32-WROOM and a RA-01SH (SX1262) LoRa Board
#define LED_POWER 2 // LED
#define LED_PIN 2 // LED
#define LED_STATE_ON 1 // State when LED is lit
#define HAS_SCREEN 0
+1 -1
View File
@@ -12,7 +12,7 @@
#define RESET_OLED 16 // If defined, this pin will be used to reset the display controller
#define LED_POWER 25 // If defined we will blink this LED
#define LED_PIN 25 // If defined we will blink this LED
#define BUTTON_PIN 0 // If defined, this will be used for user button presses
#define USE_RF95
+1 -1
View File
@@ -18,7 +18,7 @@
#define RESET_OLED 16 // If defined, this pin will be used to reset the display controller
#define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost
#define LED_POWER 25 // If defined we will blink this LED
#define LED_PIN 25 // If defined we will blink this LED
#define BUTTON_PIN 0 // If defined, this will be used for user button presses
#define USE_RF95
+1 -1
View File
@@ -13,7 +13,7 @@
#define RESET_OLED 16 // If defined, this pin will be used to reset the display controller
#define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost
#define LED_POWER 25 // If defined we will blink this LED
#define LED_PIN 25 // If defined we will blink this LED
#define BUTTON_PIN 0 // If defined, this will be used for user button presses
#define USE_RF95
@@ -15,7 +15,7 @@
#undef GPS_TX_PIN
// Green / Lora = PIN 22 / GPIO2, Yellow / Wifi = PIN 23 / GPIO0, Blue / BLE = PIN 25 / GPIO16
#define LED_POWER 22
#define LED_PIN 22
#define WIFI_LED 23
#define BLE_LED 25
+1 -1
View File
@@ -1,7 +1,7 @@
#define I2C_SCL SCL
#define I2C_SDA SDA
#define LED_POWER LED
#define LED_PIN LED
// active low, powers the Battery reader, but no lora antenna boost (?)
// #define VEXT_ENABLE Vext
+1 -1
View File
@@ -11,7 +11,7 @@
// Green LED
#define LED_STATE_ON 1 // State when LED is lit
#define LED_POWER 10
#define LED_PIN 10
// PCF8563 RTC Module
#define PCF8563_RTC 0x51
@@ -37,7 +37,7 @@
/*
LED PIN setup.
*/
#define LED_POWER 15
#define LED_PIN 15
/*
Five way button when using ADC.
+1 -1
View File
@@ -43,7 +43,7 @@ static const uint8_t SCK = 33;
#undef GPS_TX_PIN
#define GPS_TX_PIN (TX1)
#define LED_POWER LED_BLUE
#define LED_PIN LED_BLUE
#define PIN_VBAT WB_A0
#define BATTERY_PIN PIN_VBAT
+2 -2
View File
@@ -9,7 +9,7 @@
#define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module.
#define LED_STATE_ON 0 // State when LED is lit
#define LED_POWER 4 // Newer tbeams (1.1) have an extra led on GPIO4
#define LED_PIN 4 // Newer tbeams (1.1) have an extra led on GPIO4
// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if
// not found then probe for SX1262
@@ -49,7 +49,7 @@
#undef EXT_NOTIFY_OUT
#undef LED_STATE_ON
#undef LED_POWER
#undef LED_PIN
#define HAS_CST226SE 1
#define HAS_TOUCHSCREEN 1

Some files were not shown because too many files have changed in this diff Show More