Files
Jakub 8f4c92a217 feat(sim): add variants/sim/ — real companion_radio firmware on native + Emscripten
New board variant compiling the unmodified MyMesh/UITask/DataStore app
logic against real mesh::Radio/MainBoard/RTCClock/RNG interfaces, for
running the actual firmware outside embedded hardware:

- Native (plain g++, platform = native): ASCII-art display over stdout,
  stdin-driven input, local-disk-backed DataStore/IdentityStore.
- Emscripten/WASM (variants/sim/build_wasm.sh, since PlatformIO's native
  platform force-overrides any CC/CXX toolchain override back to system
  clang++): canvas-backed display, IDBFS-backed persistence across page
  reloads, JS-callable input via sim_enqueue_key(), emscripten_set_main_loop.

Real rweather/Crypto (AES128/SHA256/Ed25519) vendored unmodified and
proven working on both targets. variants/sim/web/index.html is a bare
verification harness, not the polished website embed.
2026-09-03 00:46:47 +02:00

311 lines
9.7 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* CayenneLPP - CayenneLPP Polyline Codec
*
* Copyright (C) 2021 by Manuel Weichselbaumer <mincequi@web.de>
*
* Use of this source code is governed by the MIT license that can be found in the LICENSE file.
*
*/
#ifndef ARDUINO
#include "CayenneLPPPolyline.h"
#include <cmath>
#include <map>
const double scaleFactor = 10000.0;
const std::map<uint8_t, double> s_valueMap {
//{ 224, 0.0 }, // 0.00001, reserved
//{ 225, 0.0 }, // 0.000025, reserved
//{ 226, 0.0 }, // 0.00005, reserved
{ 227, 1.0 }, // 0.0001
{ 228, 2.0 }, // 0.0002
{ 229, 5.0 }, // 0.0005
{ 230, 10.0 }, // 0.001
{ 231, 20.0 }, // 0.002
{ 232, 50.0 }, // 0.005
{ 233, 100.0 }, // 0.01
{ 234, 200.0 }, // 0.02
{ 235, 500.0 }, // 0.05
{ 236, 1000.0 }, // 0.1
{ 237, 2000.0 }, // 0.2
{ 238, 5000.0 }, // 0.5
{ 239, 10000.0 }, // 1.0
//{ 240, 20000.0 }, // 2.0
//{ 241, 50000.0 }, // 5.0
//{ 255, 0.0 } // reserved
};
struct InitialCoord {
int32_t lat:24;
int32_t lon:24;
};
struct DeltaCoord {
int8_t dLat:4;
int8_t dLon:4;
};
CayenneLPPPolyline::CayenneLPPPolyline(uint32_t size) : m_maxSize(size) {
}
std::vector<uint8_t> CayenneLPPPolyline::encode(const std::vector<Point>& coords,
uint8_t factor,
Simplification simplification) {
reset();
if (coords.size() < 2) {
return {};
}
const double dFactor = getFactor(factor);
// Apply DouglasPeucker first
auto coords2 = simplification == DouglasPeucker ? douglasPeucker(coords, dFactor/scaleFactor * 0.5) : coords;
// Push initial item to init encoder
pushFirst(coords2.front().first * scaleFactor / dFactor, coords2.front().second * scaleFactor / dFactor, factor);
for (auto it = coords2.begin()+1; it != coords2.end() && m_buffer.size() < m_maxSize; ++it) {
// Latitude -/+ 90
// Longitude -/+ 180
if (abs(it->first) > 90.0 || abs(it->second) > 180.0) {
break;
}
// Push each item to encoder
push(it->first * scaleFactor / dFactor, it->second * scaleFactor / dFactor, simplification == PerpendicularDistance);
}
// Write final header
pushFirst(coords2.front().first * scaleFactor / dFactor, coords2.front().second * scaleFactor / dFactor, factor);
return m_buffer;
}
std::vector<uint8_t> CayenneLPPPolyline::encode(const std::vector<Point>& coords,
Precision precision,
Simplification simplification) {
return encode(coords, static_cast<uint8_t>(precision), simplification);
}
std::vector<std::pair<double, double>> CayenneLPPPolyline::decode(const std::vector<uint8_t>& buffer) {
if (buffer.size() < 7) {
return {};
}
const double dFactor = getFactor(buffer[1]);
if (dFactor == 0.0) {
return {};
}
std::vector<std::pair<double, double>> coords;
//uint8_t size = buffer[0];
int32_t prevLat = buffer[2] << 24 | buffer[3] << 16 | buffer[4] << 8;
prevLat /= 256;
prevLat *= dFactor;
int32_t prevLon = buffer[5] << 24 | buffer[6] << 16 | buffer[7] << 8;
prevLon /= 256;
prevLon *= dFactor;
coords.push_back( { prevLat / scaleFactor, prevLon / scaleFactor } );
if (buffer.size() == 7) {
return coords;
}
for (auto it = buffer.begin()+8; it != buffer.end(); ++it) {
const auto dc = *(DeltaCoord*)&*it;
const int dLat = dc.dLat * dFactor;
const int dLon = dc.dLon * dFactor;
coords.push_back( { (prevLat + dLat) / scaleFactor, (prevLon + dLon) / scaleFactor } );
prevLat += dLat;
prevLon += dLon;
}
return coords;
}
CayenneLPPPolyline::Stats CayenneLPPPolyline::getEncodeStats() const {
return m_stats;
}
void CayenneLPPPolyline::reset() {
m_buffer.clear();
m_prevLat = 0.0;
m_prevLon = 0.0;
m_errLat = 0.0;
m_errLon = 0.0;
m_stats = {};
}
double CayenneLPPPolyline::getFactor(uint8_t factor) {
if ( factor == 0) {
return 0.0;
} else if (factor < 200) {
return factor;
} else if (s_valueMap.count(factor)) {
return s_valueMap.at(factor);
} else {
return 0.0;
}
}
void CayenneLPPPolyline::push(double lat, double lon, bool optimize) {
// Compute delta and correct error from previous rounding
const double dLat = (lat - m_prevLat) + m_errLat;
const double dLon = (lon - m_prevLon) + m_errLon;
// Round values
const int32_t roundLat = round(dLat);
const int32_t roundLon = round(dLon);
// Ignore items with zero delta
if ((abs(roundLat) < 1) && (abs(roundLon) < 1)) {
++m_stats.removedCoords;
// Delta fits into one nibble, push it
} else if (abs(roundLat) < 8 && abs(roundLon) < 8) {
writeDelta(roundLat, roundLon, optimize);
// Delta is too big for one nibble. Compute intermediates.
} else {
++m_stats.addedCoords;
--m_stats.keptCoords;
// Push intermediate. This is a simplified solution.
// A more sophisticated computation can be found her: https://www.movable-type.co.uk/scripts/latlong.html
// Please check the Intermediate point section.
const double divisor = ceil(std::max(abs(dLat/7.0), abs(dLon/7.0)));
push(m_prevLat + dLat / divisor, m_prevLon + dLon / divisor, optimize);
// Push original lat/lon after intermediate.
push(lat, lon, optimize);
return;
}
// Compute error from rounding
m_errLat = dLat - roundLat;
m_errLon = dLon - roundLon;
m_prevLat = lat;
m_prevLon = lon;
}
void CayenneLPPPolyline::pushFirst(double lat, double lon, uint8_t factor) {
const int32_t roundLat = round(lat);
const int32_t roundLon = round(lon);
writeHeader(roundLat, roundLon, factor);
m_errLat = (lat - roundLat);
m_errLon = (lon - roundLon);
m_prevLat = lat;
m_prevLon = lon;
}
void CayenneLPPPolyline::writeHeader(int32_t lat, int32_t lon, uint8_t factor) {
// ESP-IDF framework
#if !defined(ARDUINO) && defined(IDF_VER)
m_buffer.resize(std::max(static_cast<long unsigned int>(m_buffer.size()), 8UL));
#else
m_buffer.resize(std::max(m_buffer.size(), 8UL));
#endif
m_buffer[0] = m_buffer.size();
m_buffer[1] = factor;
m_buffer[2] = (lat >> 16); m_buffer[3] = (lat >> 8); m_buffer[4] = (lat);
m_buffer[5] = (lon >> 16); m_buffer[6] = (lon >> 8); m_buffer[7] = (lon);
}
void CayenneLPPPolyline::writeDelta(int8_t lat, int8_t lon, bool optimize) {
const DeltaCoord& prevDelta = (DeltaCoord&)m_buffer.back();
const DeltaCoord currDelta { lat, lon };
// This is a cheap optimization as an alternative to Douglas-Peucker
// Check if the sum of this and next delta is within range
const int8_t dLat = prevDelta.dLat + currDelta.dLat;
const int8_t dLon = prevDelta.dLon + currDelta.dLon;
if (optimize && m_buffer.size() > 8 && abs(dLat) < 8 && abs(dLon) < 8) {
// Check if previous delta only differs slightly from straight line to current delta
const double distance = abs(dLat * -1.0 * prevDelta.dLon + prevDelta.dLat * dLon) / sqrt(dLat * dLat + dLon * dLon);
if (distance < 0.5) {
((DeltaCoord&)m_buffer.back()).dLat = dLat;
((DeltaCoord&)m_buffer.back()).dLon = dLon;
++m_stats.removedCoords;
return;
}
}
m_buffer.push_back(*(uint8_t*)(&currDelta));
++m_stats.keptCoords;
}
std::vector<CayenneLPPPolyline::Point> CayenneLPPPolyline::douglasPeucker(const std::vector<Point> &pointList, double epsilon) {
if (pointList.size() < 2) {
return {};
}
std::vector<Point> out;
// Find the point with the maximum distance from line between start and end
double dmax = 0.0;
size_t index = 0;
size_t end = pointList.size()-1;
for (size_t i = 1; i < end; i++) {
double d = distance(pointList[i], pointList[0], pointList[end]);
if (d > dmax) {
index = i;
dmax = d;
}
}
// If max distance is greater than epsilon, recursively simplify
if (dmax > epsilon) {
// Recursive call
std::vector<Point> recResults1;
std::vector<Point> recResults2;
std::vector<Point> firstLine(pointList.begin(), pointList.begin()+index+1);
std::vector<Point> lastLine(pointList.begin()+index, pointList.end());
recResults1 = douglasPeucker(firstLine, epsilon);
recResults2 = douglasPeucker(lastLine, epsilon);
// Build the result list
out.assign(recResults1.begin(), recResults1.end()-1);
out.insert(out.end(), recResults2.begin(), recResults2.end());
if (out.size() < 2) {
return {};
}
} else {
//Just return start and end points
out.clear();
out.push_back(pointList[0]);
out.push_back(pointList[end]);
}
return out;
}
double CayenneLPPPolyline::distance(const Point& point, const Point& lineStart, const Point& lineEnd) {
double dLat = lineEnd.first - lineStart.first;
double dLon = lineEnd.second - lineStart.second;
// Normalise
const double mag = sqrt(dLat * dLat + dLon * dLon);
if (mag > 0.0) {
dLat /= mag; dLon /= mag;
}
const double pvx = point.first - lineStart.first;
const double pvy = point.second - lineStart.second;
// Get dot product (project pv onto normalized direction)
const double pvdot = dLat * pvx + dLon * pvy;
// Scale line direction vector
const double dsx = pvdot * dLat;
const double dsy = pvdot * dLon;
// Subtract this from pv
const double ax = pvx - dsx;
const double ay = pvy - dsy;
return sqrt(ax * ax + ay * ay);
}
#endif