это разъемчик только под экран и кнопки, колечко там без надобности
А вот и нет - там стоит китайская копия стм, именно она передает весь код в 328 для ГРБЛ и именно по этому проводу.
Там шлейф плоский. Нет колец. И как его натянуть? Ну диаметр большой я найду ![]()
А почему? читать код с файла и в отрезки его на дисплей.
…я так же сделал на игрушечной рисовалке, правда там координаты концов векторов, а не грбл код.
…и файлы это массивы в самом МК.
Можно все, вопрос в возможностях МК. Обычно для таких задач ставят уже нечто помощнее чем 328 мега и его аналоги. Конечно тот клон стм может и неплох, но сомневаюсь что его ресурсов хватит для быстрой отрисовки. И да, придется файл 3 раза прочитать - сначала определить границы и коэффициенты для отрисовки, затем отрисовать и наконец отправить в станок.
Ну у кого как, я про станок lilik, там вижу GD32F103.
ну ОК, там передача по UART, низкочастотная, зачем там колечки
У меня по подобному кабелю 20 МГц бегает и проблемы начинаются только при длине провода более полуметра
Хорошо, а что будет если из-за помех будет передано не G1 x5 y5, а что-то другое? Там ведь нет никакого контроля четности и достаточно один бит изменить и строка обработана не будет, интерпретатор ее скорее всего проигнорирует. Кстати в теории может быть передано и другое число тогда и хорошо если это будет не Z ось.
Кстати не стоит исключать что проблема в самом пульте. Действительно стоит наверное попробовать управление с ПК.
я все таки думаю не правильно вы рисуете)))
рисовать надо одной линией!
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Рисование одной линией из координат TXT</title>
<style>
* {
box-sizing: border-box;
user-select: none; /* небольшая помощь при перетаскивании, но не блокирует выделение текста в файле */
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #1a2a32;
margin: 0;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
max-width: 1300px;
width: 100%;
background: #2c3e2f;
border-radius: 2rem;
box-shadow: 0 20px 35px rgba(0,0,0,0.4);
overflow: hidden;
backdrop-filter: blur(0px);
transition: all 0.2s;
}
.header {
background: #1e2a23;
padding: 1rem 2rem;
color: #f0f3e8;
border-bottom: 2px solid #5b8c5a;
}
.header h1 {
margin: 0;
font-size: 1.6rem;
font-weight: 500;
letter-spacing: -0.3px;
}
.header p {
margin: 0.3rem 0 0;
font-size: 0.85rem;
opacity: 0.8;
font-family: monospace;
}
.upload-area {
background: #24342b;
padding: 1.5rem 2rem;
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #3f5842;
}
.file-label {
background: #5f8b6f;
padding: 0.6rem 1.4rem;
border-radius: 40px;
color: white;
font-weight: bold;
cursor: pointer;
transition: 0.2s;
font-size: 0.9rem;
display: inline-flex;
align-items: center;
gap: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.file-label:hover {
background: #7aa77a;
transform: scale(0.97);
}
#fileInput {
display: none;
}
.info-panel {
background: #1f2e26;
padding: 0.5rem 1.5rem;
border-radius: 30px;
font-family: monospace;
font-size: 0.9rem;
color: #cfe6cf;
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
}
.info-panel span {
font-weight: bold;
color: #ffe5a3;
}
.canvas-wrapper {
background: #eef4ea;
padding: 1.5rem;
display: flex;
justify-content: center;
align-items: center;
min-height: 550px;
}
canvas {
background: #ffffff;
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
border-radius: 20px;
max-width: 100%;
height: auto;
border: 2px solid #cbdcc2;
cursor: crosshair;
}
.controls {
background: #1e2a23;
padding: 1rem 2rem;
display: flex;
gap: 1.2rem;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
border-top: 1px solid #3f5842;
}
button {
background: #41644a;
border: none;
color: white;
padding: 0.5rem 1.3rem;
border-radius: 40px;
font-weight: bold;
font-size: 0.85rem;
cursor: pointer;
transition: 0.15s;
font-family: inherit;
box-shadow: 0 1px 3px black;
}
button:hover {
background: #5e8868;
transform: translateY(-1px);
}
button:active {
transform: translateY(1px);
}
.status {
background: #2a3b30;
padding: 0.4rem 1rem;
border-radius: 30px;
font-size: 0.8rem;
font-family: monospace;
color: #c7e9c7;
}
.error {
color: #ffbc9a;
background: #4a2a2a;
}
footer {
font-size: 0.7rem;
text-align: center;
background: #142018;
color: #8aa889;
padding: 0.6rem;
}
@media (max-width: 700px) {
.upload-area {
flex-direction: column;
align-items: stretch;
}
.info-panel {
justify-content: center;
}
.controls {
flex-direction: column;
align-items: stretch;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>✍️ Рисование одной линией</h1>
<p>Формат TXT: первая строка заголовок X Y (или сразу числа) / далее пары X Y через пробел или запятую</p>
</div>
<div class="upload-area">
<label class="file-label">
📂 Выбрать файл .txt
<input type="file" id="fileInput" accept=".txt, .csv, .text/plain">
</label>
<div class="info-panel" id="infoPanel">
<div>📊 Точек: <span id="pointCount">0</span></div>
<div>📏 Диапазон X: <span id="xRange">—</span></div>
<div>📐 Диапазон Y: <span id="yRange">—</span></div>
</div>
</div>
<div class="canvas-wrapper">
<canvas id="drawCanvas" width="900" height="600" style="width:100%; height:auto; max-width:900px; aspect-ratio:900/600"></canvas>
</div>
<div class="controls">
<div class="status" id="statusMsg">⚡ Ожидание загрузки TXT с координатами</div>
<div>
<button id="btnZoomFit">🔍 Подогнать под холст</button>
<button id="btnResetView">🔄 Сброс вида</button>
</div>
</div>
<footer>Поддерживаются форматы: "X Y" или "X,Y" в каждой строке. Линия рисуется в порядке следования точек.</footer>
</div>
<script>
(function(){
// ------ элементы ------
const canvas = document.getElementById('drawCanvas');
const ctx = canvas.getContext('2d');
const fileInput = document.getElementById('fileInput');
const pointCountSpan = document.getElementById('pointCount');
const xRangeSpan = document.getElementById('xRange');
const yRangeSpan = document.getElementById('yRange');
const statusDiv = document.getElementById('statusMsg');
const zoomFitBtn = document.getElementById('btnZoomFit');
const resetViewBtn = document.getElementById('btnResetView');
// ------ данные ------
let points = []; // массив объектов {x, y} (исходные координаты, числа)
let originalMinX = 0, originalMaxX = 0, originalMinY = 0, originalMaxY = 0;
// параметры отображения (панорамирование и масштаб)
let offsetX = 0, offsetY = 0; // смещение в пикселях (в координатах canvas)
let scale = 1.0; // масштаб (1px = 1 единица пользователя? нет, динамический)
// но для удобства: мы будем хранить transform: смещение (offsetX, offsetY) и масштаб,
// но масштаб будет относительно изначальной проекции world -> canvas.
// Чтобы универсально: определяем функцию worldToCanvas(x, y)
let viewInitialized = false; // флаг что сделали auto-fit при первой загрузке
// ----- вспомогательные функции для парсинга -----
function parseNumber(str) {
// удаляем лишние пробелы, заменяем запятую на точку? но у нас целые/дробные с точкой?
// пример: "289,390" — но тут запятая разделитель координат, но возможна дробная часть через точку.
// На всякий случай: если внутри числа есть запятая как десятичный разделитель - преобразуем.
let cleaned = str.trim();
// если есть запятая и она не является разделителем пар (т.е. внутри числа), но у нас пары разделены пробелом или запятой.
// Но по условию пример: X Y потом 0,0 - но это видимо "0,0" как два числа? на самом деле строка "0,0" => разделитель запятая.
// В задании: "X Y" затем строки вида "289,390" - это координаты через запятую.
// Лучше: если видим запятую в строке после обрезания, пробуем сплитнуть её.
if (cleaned.includes(',')) {
// замена десятичной запятой на точку? если это десятичное, но у нас похоже целые.
// Но если это число типа "289,390" целиком? Неправильно. Лучше: в функции разбора строки.
// оставим стандартно: просто заменяем запятую на точку для чисел с плавающей точкой, если это одно число.
// но тут число скорее всего без десятичных. Просто замена запятой на точку для безопасности.
cleaned = cleaned.replace(',', '.');
}
const num = parseFloat(cleaned);
return isNaN(num) ? null : num;
}
// парсинг строки вида "289 390" или "289,390" или " 289 , 390 "
function parseCoordLine(line) {
// удаляем возможные лишние пробелы
line = line.trim();
if (line === "") return null;
// сначала пробуем разделить пробельными символами
let parts = line.trim().split(/\s+/);
if (parts.length >= 2) {
let xRaw = parts[0];
let yRaw = parts[1];
// в xRaw или yRaw может быть запятая в конце? например "289," ? мало вероятно.
let x = parseNumber(xRaw);
let y = parseNumber(yRaw);
if (x !== null && y !== null && !isNaN(x) && !isNaN(y)) return {x, y};
}
// иначе пробуем разделить запятыми
if (line.includes(',')) {
let commaParts = line.split(',').map(s => s.trim());
if (commaParts.length >= 2) {
let x = parseNumber(commaParts[0]);
let y = parseNumber(commaParts[1]);
if (x !== null && y !== null && !isNaN(x) && !isNaN(y)) return {x, y};
}
}
return null;
}
// основной парсер файла: из текста извлекает массив точек
function parseTxtToPoints(text) {
const lines = text.split(/\r?\n/);
let coords = [];
let headerSkipped = false;
for(let i=0; i<lines.length; i++) {
let line = lines[i].trim();
if (line === "") continue;
// проверяем, возможно ли это строка заголовка "X Y" или "X,Y" (игнорируем регистр)
let lowerLine = line.toLowerCase();
if (!headerSkipped && (lowerLine === "x y" || lowerLine === "x,y" || lowerLine === "x\ty" ||
lowerLine.startsWith("x") && lowerLine.includes("y") && lowerLine.length < 10)) {
// пропускаем строку заголовка если она содержит только x и y
headerSkipped = true;
continue;
}
const point = parseCoordLine(line);
if (point !== null) {
coords.push(point);
headerSkipped = true; // после первой успешной точки считаем что заголовка дальше нет
} else {
// возможно это мусорная строка или неверный формат, но не прерываем, просто игнорируем
if (coords.length === 0 && i < 3) {
// не ругаемся сильно, просто пропускаем первые строки если они не распознаны
}
}
}
return coords;
}
// обновление статистики и границ
function updateStats(pointsArray) {
if (!pointsArray.length) {
pointCountSpan.innerText = '0';
xRangeSpan.innerText = '—';
yRangeSpan.innerText = '—';
return;
}
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
for(let p of pointsArray) {
if(p.x < minX) minX = p.x;
if(p.x > maxX) maxX = p.x;
if(p.y < minY) minY = p.y;
if(p.y > maxY) maxY = p.y;
}
originalMinX = minX; originalMaxX = maxX; originalMinY = minY; originalMaxY = maxY;
pointCountSpan.innerText = pointsArray.length;
xRangeSpan.innerText = `${minX.toFixed(2)} … ${maxX.toFixed(2)}`;
yRangeSpan.innerText = `${minY.toFixed(2)} … ${maxY.toFixed(2)}`;
}
// функция пересчета world координат (исходные x,y) в canvas координаты (px)
function worldToCanvas(xWorld, yWorld) {
// применяем текущий масштаб scale и смещение offsetX, offsetY
// стратегия: сначала масштабируем координаты относительно начала координат (0,0) world
// но для удобства панорамирования обычно: canvasX = (xWorld - viewCenterWorldX)*scale + canvasCenterX + offsetPixels
// Но мы сделаем проще: offsetX и offsetY это сдвиг в пикселях после масштаба, а начальное отображение задаётся функцией fitToCanvas.
// Базовая формула: canvasX = xWorld * scale + offsetX; canvasY = yWorld * scale + offsetY;
// однако из-за того что ось Y в canvas направлена вниз, а world Y растет вверх? но координаты из файла могут быть любыми.
// Для рисования линии в точности как задано, просто отображаем Y зеркально? нет, не зеркалим, иначе рисунок перевернётся.
// Будем использовать прямое отображение: Y canvas = yWorld * scale + offsetY. Чтобы не инвертировать, потому что пользователь ожидает как в файле.
// но в SVG/обычной графике ось Y направлена вниз, и если файл содержит координаты как на плоскости, то рисунок может быть перевернут.
// Для линий без привязки к реальному миру - не страшно, пользователь может повернуть мысленно. Но часто дают координаты как есть.
// добавим опцию? нет, оставим прямое отображение (Y увеличивается вниз). Можно переключить checkbox? но для простоты не будем усложнять.
// Если потребуется зеркалирование по Y, то изменим: canvasY = canvas.height - (yWorld * scale + offsetY). Но тогда настройки собьются.
// Чтобы рисунок выглядел "правильно" относительно привычного просмотра, лучше не инвертировать. Оставим как есть.
// при необходимости подгонки и панорамирования будет комфортно.
return {
x: xWorld * scale + offsetX,
y: yWorld * scale + offsetY
};
}
// рисование одной линии по точкам
function drawLine() {
if (!canvas || !ctx) return;
if (!points.length) {
// очищаем холст с белым фоном и показываем сообщение
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = "16px 'Segoe UI'";
ctx.fillStyle = "#7f8c6d";
ctx.textAlign = "center";
ctx.fillText("Нет данных. Загрузите TXT файл с координатами.", canvas.width/2, canvas.height/2);
return;
}
// очистка
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// рисуем линию
ctx.beginPath();
ctx.lineWidth = 2.2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = "#2c5a2e";
ctx.shadowBlur = 0;
let first = true;
for(let i=0; i<points.length; i++) {
const {x, y} = points[i];
const canvasPos = worldToCanvas(x, y);
if (first) {
ctx.moveTo(canvasPos.x, canvasPos.y);
first = false;
} else {
ctx.lineTo(canvasPos.x, canvasPos.y);
}
}
ctx.stroke();
// опционально: рисуем начальную и конечную точки для наглядности (небольшие маркеры)
if(points.length > 0) {
const start = worldToCanvas(points[0].x, points[0].y);
const end = worldToCanvas(points[points.length-1].x, points[points.length-1].y);
ctx.fillStyle = "#c44b3c";
ctx.beginPath();
ctx.arc(start.x, start.y, 4, 0, 2*Math.PI);
ctx.fill();
ctx.fillStyle = "#2b7a3e";
ctx.beginPath();
ctx.arc(end.x, end.y, 4, 0, 2*Math.PI);
ctx.fill();
ctx.fillStyle = "#1f2e1c";
ctx.font = "bold 12px monospace";
ctx.shadowBlur = 0;
ctx.fillText("Старт", start.x+5, start.y-3);
ctx.fillText("Финиш", end.x+5, end.y-3);
}
}
// автоматическое масштабирование и центрирование (fit)
function fitToCanvas(padding = 40) {
if (!points.length) return;
// вычисляем мировые границы
let minX = originalMinX, maxX = originalMaxX, minY = originalMinY, maxY = originalMaxY;
if (minX === maxX) { minX -= 0.5; maxX += 0.5; }
if (minY === maxY) { minY -= 0.5; maxY += 0.5; }
const worldWidth = maxX - minX;
const worldHeight = maxY - minY;
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
// вычисляем масштаб так, чтобы влезло с отступами
const scaleX = (canvasWidth - padding*2) / worldWidth;
const scaleY = (canvasHeight - padding*2) / worldHeight;
let newScale = Math.min(scaleX, scaleY);
if (!isFinite(newScale) || newScale <= 0) newScale = 1;
scale = newScale;
// смещение: центрируем bounding box
const worldCenterX = (minX + maxX) / 2;
const worldCenterY = (minY + maxY) / 2;
// координаты центра мира в холсте
const canvasCenterX = canvasWidth / 2;
const canvasCenterY = canvasHeight / 2;
offsetX = canvasCenterX - worldCenterX * scale;
offsetY = canvasCenterY - worldCenterY * scale;
drawLine();
}
// сброс вида: просто заново fit
function resetView() {
if (!points.length) {
statusDiv.innerText = "⚠️ Нет точек для сброса вида";
return;
}
fitToCanvas(45);
statusDiv.innerText = "🔄 Вид сброшен (fit)";
setTimeout(() => {
if(statusDiv.innerText.includes("сброшен")) statusDiv.innerText = "✅ Готово";
}, 1200);
}
// загрузка и отрисовка файла
function loadAndDraw(file) {
const reader = new FileReader();
reader.onload = function(e) {
const content = e.target.result;
const parsedPoints = parseTxtToPoints(content);
if (!parsedPoints.length) {
statusDiv.innerText = "❌ Ошибка: не найдены координаты. Проверьте формат (X Y или X,Y)";
statusDiv.classList.add("error");
points = [];
updateStats([]);
drawLine(); // отрисует пустой холст с сообщением
return;
}
statusDiv.classList.remove("error");
points = parsedPoints;
updateStats(points);
// при новой загрузке делаем автофит
fitToCanvas(45);
statusDiv.innerText = `✅ Загружено ${points.length} точек. Линия построена.`;
viewInitialized = true;
};
reader.onerror = function() {
statusDiv.innerText = "❌ Ошибка чтения файла";
statusDiv.classList.add("error");
};
reader.readAsText(file, "UTF-8");
}
// обработчик выбора файла
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) return;
if (!file.name.toLowerCase().endsWith('.txt') && !file.type.includes('text')) {
statusDiv.innerText = "⚠️ Рекомендуется .txt файл, но попробуем прочитать...";
}
loadAndDraw(file);
});
// дополнительная возможность перетаскивания? (опционально, но можно)
// панорамирование мышкой для удобства
let isPanning = false;
let panStartX = 0, panStartY = 0;
let startOffsetX = 0, startOffsetY = 0;
canvas.addEventListener('mousedown', (e) => {
if (!points.length) return;
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const mouseX = (e.clientX - rect.left) * (canvas.width / rect.width);
const mouseY = (e.clientY - rect.top) * (canvas.height / rect.height);
isPanning = true;
panStartX = mouseX;
panStartY = mouseY;
startOffsetX = offsetX;
startOffsetY = offsetY;
canvas.style.cursor = 'grabbing';
});
window.addEventListener('mousemove', (e) => {
if (!isPanning || !points.length) return;
const rect = canvas.getBoundingClientRect();
const mouseX = (e.clientX - rect.left) * (canvas.width / rect.width);
const mouseY = (e.clientY - rect.top) * (canvas.height / rect.height);
const dx = mouseX - panStartX;
const dy = mouseY - panStartY;
offsetX = startOffsetX + dx;
offsetY = startOffsetY + dy;
drawLine();
});
window.addEventListener('mouseup', () => {
if (isPanning) {
isPanning = false;
canvas.style.cursor = 'crosshair';
}
});
// колесико для зума
canvas.addEventListener('wheel', (e) => {
if (!points.length) return;
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const mouseXcanvas = (e.clientX - rect.left) * (canvas.width / rect.width);
const mouseYcanvas = (e.clientY - rect.top) * (canvas.height / rect.height);
// находим мировые координаты под курсором до зума
const worldBefore = (mx, my) => {
// xWorld = (mx - offsetX) / scale
return { x: (mx - offsetX) / scale, y: (my - offsetY) / scale };
};
const before = worldBefore(mouseXcanvas, mouseYcanvas);
const delta = e.deltaY > 0 ? 0.9 : 1.1;
let newScale = scale * delta;
newScale = Math.min(Math.max(newScale, 0.05), 50);
scale = newScale;
// после изменения масштаба корректируем смещение, чтобы точка под курсором осталась на месте
const after = worldBefore(mouseXcanvas, mouseYcanvas);
offsetX += (before.x - after.x) * scale;
offsetY += (before.y - after.y) * scale;
drawLine();
}, { passive: false });
zoomFitBtn.addEventListener('click', () => {
if (points.length) fitToCanvas(45);
else statusDiv.innerText = "Нет данных для подгонки";
});
resetViewBtn.addEventListener('click', resetView);
// начальная отрисовка пустого холста
drawLine();
// если canvas изначально имеет размер, то хорошо, но при ресайзе окна сохраняем масштаб? не трогаем, но перерисовываем
window.addEventListener('resize', () => {
if (points.length) drawLine();
});
// подсказка про drag & zoom
const styleStatus = document.createElement('style');
styleStatus.textContent = `canvas { transition: cursor 0.1s; }`;
document.head.appendChild(styleStatus);
})();
</script>
</body>
</html>
вот код для рисования
https://ru.files.fm/u/eqrkvz7m39
только так наверное сверло будет ломаться))) по этому рисовать надо на песке!)))
А сколько лет это рисоваться будет?
очень долго, особенно если медленная скорость, в этом и прелесть, наблюдать как робот пыхтит и рисует на песке это….
но итоговый результат можно посмотреть в html)))
https://aliexpress.ru/item/1005010152320218.html вот на этом, переделав ему мозги управления, надо запускать, на это вроде можно смотреть и любоваться
Может выложишь в проектах хтмл файл на узоры? А потом по подобию сделаешь стол с песком виртуальный, с красивой имитацией катания шарика на экране ПК по генерируемым узорам. На песке ни баба, ни машинка не проскочат из-за грубости инструмента. Мышка как вершина возможностей и всякие орнаменты - всё.
Что касается одной линии. Тут надо понимать принципиальную идею Гайвера. Уход от контурного рисования … к многоконтурному закрашиванию ![]()
Вот его подход на рисовалке.
файлы - примеры не скачиваются.
Он залочил скачивание с веб сервера..Решил проблему с другой темы.
А зачем? Как мы тогда посмотрим работу хтмл страницы из поста 249? Это надо идти в конвертор Гайвера со своей картинкой.
спс, у меня такое часто, выкладываю а потом что то происходит и не качаются…
получилось ?)))
или да, проще посмотреть через MagicGyver я от туда тырил(заимствовал частично) код)))
А кто сказал что там на уровне обмена чистый UART ?
Как минимум паркинг строк есть , т.к. системные запросы начинаются с ‘$’, вполне возможно что и crc16 прикручен. Для МК не критично у stm32f103 он аппаратный.
В 3D принтере Элегу эта проблема кое-как решена. По крайней мере что-то вроде эскиза принтер на экране показывает. Подозреваю, что картинку добавляет слайсер как комментарии в G-файл, но проверить это руки не доходят.
nevkon:
картинку выводить не так просто
А почему? читать код с файла и в отрезки его на дисплей.
Если картинка двумерная - можно, а с 3D есть существенные проблемы.
Пришёл пиксельный дисплей, совсем не в тему. Наврал продавец про ws2812b
А что там на самом деле?
А то я уже к нему присматривался.
что там на самом деле?
Там, на обороте ряд микросхем, наверное это типа Р6.










