+
+
+
-
@@ -408,6 +638,12 @@ const char transmit_html[] PROGMEM = R"rawliteral(
let ws;
let messages = [];
let txCount = 0;
+ let currentMode = 'periodic';
+
+ // Sequence Mode Variables
+ let sequenceSteps = [];
+ let sequenceRunning = false;
+ let sequenceAbort = false;
function initWebSocket() {
ws = new WebSocket('ws://' + window.location.hostname + ':81');
@@ -432,7 +668,302 @@ const char transmit_html[] PROGMEM = R"rawliteral(
};
}
- // 프리셋 관리
+ // Mode Switching
+ function switchMode(mode) {
+ currentMode = mode;
+
+ document.querySelectorAll('.mode-tab').forEach(tab => tab.classList.remove('active'));
+ document.querySelectorAll('.mode-content').forEach(content => content.classList.remove('active'));
+
+ if (mode === 'periodic') {
+ document.querySelectorAll('.mode-tab')[0].classList.add('active');
+ document.getElementById('periodic-mode').classList.add('active');
+ } else {
+ document.querySelectorAll('.mode-tab')[1].classList.add('active');
+ document.getElementById('sequence-mode').classList.add('active');
+ }
+ }
+
+ // Sequence Mode Functions
+ function updateStepForm() {
+ const stepType = document.getElementById('seq-step-type').value;
+
+ if (stepType === 'message') {
+ document.getElementById('message-step-form').style.display = 'block';
+ document.getElementById('delay-step-form').style.display = 'none';
+ } else {
+ document.getElementById('message-step-form').style.display = 'none';
+ document.getElementById('delay-step-form').style.display = 'block';
+ }
+ }
+
+ document.getElementById('repeat-mode').addEventListener('change', function() {
+ const mode = this.value;
+ const countGroup = document.getElementById('repeat-count-group');
+
+ if (mode === 'count') {
+ countGroup.style.display = 'flex';
+ } else {
+ countGroup.style.display = 'none';
+ }
+ });
+
+ function addSequenceStep() {
+ const stepType = document.getElementById('seq-step-type').value;
+
+ if (stepType === 'message') {
+ const id = document.getElementById('seq-can-id').value.toUpperCase();
+ const type = document.getElementById('seq-msg-type').value;
+ const dlc = parseInt(document.getElementById('seq-dlc').value);
+
+ if (!id || !/^[0-9A-F]+$/.test(id)) {
+ alert('Invalid CAN ID!');
+ return;
+ }
+
+ const data = [];
+ for (let i = 0; i < 8; i++) {
+ const val = document.getElementById('seq-d' + i).value.toUpperCase();
+ if (!/^[0-9A-F]{0,2}$/.test(val)) {
+ alert('Invalid data byte D' + i + '!');
+ return;
+ }
+ data.push(val.padStart(2, '0'));
+ }
+
+ sequenceSteps.push({
+ type: 'message',
+ id: id,
+ msgType: type,
+ dlc: dlc,
+ data: data
+ });
+ } else {
+ const delay = parseInt(document.getElementById('seq-delay').value);
+
+ if (delay < 1) {
+ alert('Delay must be at least 1ms!');
+ return;
+ }
+
+ sequenceSteps.push({
+ type: 'delay',
+ delay: delay
+ });
+ }
+
+ updateSequenceList();
+ }
+
+ function updateSequenceList() {
+ const list = document.getElementById('sequence-list');
+
+ if (sequenceSteps.length === 0) {
+ list.innerHTML = '
No steps added yet
';
+ return;
+ }
+
+ list.innerHTML = '';
+ sequenceSteps.forEach((step, index) => {
+ const stepDiv = document.createElement('div');
+ stepDiv.className = 'sequence-step';
+
+ let content = '';
+ let stepTypeClass = '';
+ let stepTypeName = '';
+
+ if (step.type === 'message') {
+ stepTypeClass = 'step-message';
+ stepTypeName = 'MSG';
+ content = 'ID: 0x' + step.id + ' | ' + step.msgType.toUpperCase() + ' | DLC: ' + step.dlc + ' | Data: ' + step.data.join(' ');
+ } else {
+ stepTypeClass = 'step-delay';
+ stepTypeName = 'DELAY';
+ content = 'Wait ' + step.delay + ' ms';
+ }
+
+ stepDiv.innerHTML =
+ '
' + (index + 1) + '
' +
+ '
' + stepTypeName + '
' +
+ '
' + content + '
' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ '
';
+
+ list.appendChild(stepDiv);
+ });
+ }
+
+ function moveStepUp(index) {
+ if (index > 0) {
+ [sequenceSteps[index], sequenceSteps[index - 1]] = [sequenceSteps[index - 1], sequenceSteps[index]];
+ updateSequenceList();
+ }
+ }
+
+ function moveStepDown(index) {
+ if (index < sequenceSteps.length - 1) {
+ [sequenceSteps[index], sequenceSteps[index + 1]] = [sequenceSteps[index + 1], sequenceSteps[index]];
+ updateSequenceList();
+ }
+ }
+
+ function deleteStep(index) {
+ sequenceSteps.splice(index, 1);
+ updateSequenceList();
+ }
+
+ function clearSequence() {
+ if (confirm('Clear all sequence steps?')) {
+ sequenceSteps = [];
+ updateSequenceList();
+ }
+ }
+
+ async function startSequence() {
+ if (sequenceSteps.length === 0) {
+ alert('No sequence steps added!');
+ return;
+ }
+
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
+ alert('WebSocket not connected!');
+ return;
+ }
+
+ sequenceRunning = true;
+ sequenceAbort = false;
+
+ document.getElementById('start-seq-btn').style.display = 'none';
+ document.getElementById('stop-seq-btn').style.display = 'inline-block';
+
+ const repeatMode = document.getElementById('repeat-mode').value;
+ const repeatCount = parseInt(document.getElementById('repeat-count').value) || 1;
+
+ let totalIterations = 0;
+
+ if (repeatMode === 'once') {
+ totalIterations = 1;
+ } else if (repeatMode === 'count') {
+ totalIterations = repeatCount;
+ } else {
+ totalIterations = -1; // Infinite
+ }
+
+ let currentIteration = 0;
+
+ while (sequenceRunning && (totalIterations === -1 || currentIteration < totalIterations)) {
+ if (sequenceAbort) break;
+
+ currentIteration++;
+
+ if (totalIterations === -1) {
+ document.getElementById('seq-status').textContent = 'Running... (Iteration: ' + currentIteration + ')';
+ } else {
+ document.getElementById('seq-status').textContent = 'Running... (' + currentIteration + ' / ' + totalIterations + ')';
+ }
+
+ for (let i = 0; i < sequenceSteps.length; i++) {
+ if (sequenceAbort || !sequenceRunning) break;
+
+ const step = sequenceSteps[i];
+
+ if (step.type === 'message') {
+ sendCanMessage(step.id, step.msgType, step.dlc, step.data);
+ } else if (step.type === 'delay') {
+ await sleep(step.delay);
+ }
+ }
+
+ if (sequenceAbort || !sequenceRunning) break;
+ }
+
+ stopSequence();
+ }
+
+ function stopSequence() {
+ sequenceRunning = false;
+ sequenceAbort = true;
+
+ document.getElementById('start-seq-btn').style.display = 'inline-block';
+ document.getElementById('stop-seq-btn').style.display = 'none';
+ document.getElementById('seq-status').textContent = 'Stopped';
+
+ setTimeout(() => {
+ document.getElementById('seq-status').textContent = '';
+ }, 3000);
+ }
+
+ function sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+
+ function saveSequencePreset() {
+ if (sequenceSteps.length === 0) {
+ alert('No sequence steps to save!');
+ return;
+ }
+
+ const name = prompt('Enter sequence preset name:');
+ if (!name) return;
+
+ try {
+ let presets = JSON.parse(localStorage.getItem('sequence_presets') || '{}');
+
+ presets[name] = {
+ steps: JSON.parse(JSON.stringify(sequenceSteps)),
+ savedAt: new Date().toISOString()
+ };
+
+ localStorage.setItem('sequence_presets', JSON.stringify(presets));
+ alert('Sequence preset "' + name + '" saved!');
+ } catch(e) {
+ console.error('Failed to save sequence preset:', e);
+ alert('Failed to save sequence preset!');
+ }
+ }
+
+ function loadSequencePreset() {
+ try {
+ const presets = JSON.parse(localStorage.getItem('sequence_presets') || '{}');
+ const names = Object.keys(presets);
+
+ if (names.length === 0) {
+ alert('No saved sequence presets!');
+ return;
+ }
+
+ let options = 'Select a sequence preset:\n\n';
+ names.forEach((name, index) => {
+ options += (index + 1) + '. ' + name + '\n';
+ });
+
+ const selection = prompt(options + '\nEnter number:');
+ if (!selection) return;
+
+ const index = parseInt(selection) - 1;
+ if (index >= 0 && index < names.length) {
+ const name = names[index];
+
+ if (sequenceSteps.length > 0) {
+ if (!confirm('Current sequence will be replaced. Continue?')) {
+ return;
+ }
+ }
+
+ sequenceSteps = JSON.parse(JSON.stringify(presets[name].steps));
+ updateSequenceList();
+ alert('Loaded sequence preset "' + name + '"');
+ }
+ } catch(e) {
+ console.error('Failed to load sequence preset:', e);
+ alert('Failed to load sequence preset!');
+ }
+ }
+
+ // Periodic Mode Functions (기존 함수들)
function savePreset() {
const presetName = document.getElementById('preset-name').value.trim();
@@ -698,7 +1229,6 @@ const char transmit_html[] PROGMEM = R"rawliteral(
const delayMs = parseInt(document.getElementById('send-all-delay').value) || 10;
- // 버튼 비활성화 및 시각적 피드백
const btn = event.target;
const originalText = btn.innerHTML;
btn.disabled = true;
@@ -706,10 +1236,8 @@ const char transmit_html[] PROGMEM = R"rawliteral(
let sentCount = 0;
- // 순차적으로 메시지 전송 (딜레이 포함)
function sendNext(index) {
if (index >= messages.length) {
- // 모든 메시지 전송 완료
console.log('Sent all messages once: ' + sentCount + ' messages');
btn.innerHTML = '✓ Sent ' + sentCount + ' msgs';
@@ -727,16 +1255,13 @@ const char transmit_html[] PROGMEM = R"rawliteral(
sendCanMessage(msg.id, msg.type, msg.dlc, msg.data);
sentCount++;
- // 진행 상황 표시
btn.innerHTML = '⏳ Sending ' + (index + 1) + '/' + messages.length;
- // 다음 메시지 전송 (딜레이 후)
setTimeout(() => {
sendNext(index + 1);
}, delayMs);
}
- // 첫 번째 메시지부터 시작
sendNext(0);
}
@@ -748,7 +1273,8 @@ const char transmit_html[] PROGMEM = R"rawliteral(
}
}
- document.querySelectorAll('input[id^="d"]').forEach(input => {
+ // Input validation
+ document.querySelectorAll('input[id^="d"], input[id^="seq-d"]').forEach(input => {
input.addEventListener('input', function() {
this.value = this.value.toUpperCase().replace(/[^0-9A-F]/g, '');
});
@@ -758,7 +1284,10 @@ const char transmit_html[] PROGMEM = R"rawliteral(
this.value = this.value.toUpperCase().replace(/[^0-9A-F]/g, '');
});
- // 페이지 로드 시 프리셋 목록 불러오기
+ document.getElementById('seq-can-id').addEventListener('input', function() {
+ this.value = this.value.toUpperCase().replace(/[^0-9A-F]/g, '');
+ });
+
window.addEventListener('load', function() {
loadPresetList();
});