Teachable Machine > P5.js > Device (web serial) (Remix)

10 views · Jan 13, 2026

Simple utility to connect tm + p5.js + microbit

🔀 Remix This 📖 View Docs ⛶ Full Screen ← Back to Gallery
📋 Embed Code

Copy this code to embed this sketch on your website:

<iframe src="https://p5js.ai/embed/1ec6921c-d91b-4ac1-8570-de5eb6c36aea" width="400" height="400" frameborder="0" allow="autoplay" title="Teachable Machine > P5.js > Device (web serial) (Remix)"></iframe>

Adjust width and height as needed. Preview embed →

❓ About This Sketch 3 questions

What visual elements are featured in the Teachable Machine p5.js sketch?

The sketch visually displays webcam video capture alongside real-time classification results based on user-defined models from Teachable Machine.

How can users interact with the p5.js sketch for device connectivity?

Users can input a Teachable Machine model URL, connect to a serial or Bluetooth device, and view live classification results as well as data received from the connected device.

What key creative coding concepts are demonstrated in this sketch?

This sketch showcases machine learning integration with ml5.js, real-time video processing, and device communication using web serial and Bluetooth APIs.

💬 AI Chat History 10 messages
👤 You
Lisää ohjelmaan myös web-bluetooth toiminnallisuus. Sen kautta tulee lähettää sama sisältö kuin web-serialissa eli numerot 1, 2 tai 3 luokittelutuloksen mukaan
🤖 AI
Okay, I understand. You want to add Web Bluetooth functionality to your existing p5.js sketch, allowing it to send the same classification numbers (1, 2, or 3) as the Web Serial connection.

This is a great addition for more flexible device communication! Web Bluetooth, like Web Serial, is a browser API, so no new p5.js libraries are needed. The main task is to handle the Bluetooth connection, characteristic writing, and integrate it with your existing classification logic.

**Important Note on Bluetooth UUIDs:**
For Web Bluetooth to work, your microcontroller (e.g., Arduino with an ESP32 or nRF module) *must* be programmed to expose a Bluetooth Low Energy (BLE) service with a specific UUID, and within that service, a characteristic with another specific UUID that allows writing.

I will use the following **example custom UUIDs**. You **must update your microcontroller's BLE code** to use these exact UUIDs (or generate your own and update the p5.js sketch accordingly).

* **Bluetooth Service UUID:** `19B10000-E8F2-537E-4F6C-D104768A1214`
* **Bluetooth Characteristic UUID (for writing):** `19B10001-E8F2-537E-4F6C-D104768A1214`

Here's the updated code for `sketch.js`, `index.html`, and `style.css`.

---

### `sketch.js` (Updated)

```javascript
// Remixed from: Anonymous/teachable-machine-p5-js-device-web-serial
// Original: https://p5js.ai/gallery/s/teachable-machine-p5-js-device-web-serial
// ----------------------------------------

// Remixed from: Larux75/teachable-machine-web-serial2
// Original: https://p5js.ai/gallery/u/Larux75-p5js/teachable-machine-web-serial2
// ----------------------------------------

//====================================================
// This code was written on the p5js.ai IDE
// Using the xelsed-alpha6-flash tuned model
// Visit the site and support Dyslexic Research
//===================================================

let classifier; // The ml5.js image classifier
let video; // The webcam video capture
let modelURLInput; // Input field for Teachable Machine model URL
let modelURLLoadButton; // Button to load the model from the URL
let modelURL = ''; // Your Teachable Machine model URL (initially empty)
let currentClassification = { label: 'Please enter model URL and click OK', confidence: 0 }; // Stores the latest classification result

// Web Serial variables
let port; // The connected serial port
let reader; // For reading data from the port
let writer; // For writing data from the port
let serialConnectButton; // Button to connect/disconnect serial
let serialStatusP; // Paragraph to display serial connection status
let receivedSerialDataP; // Paragraph to display data received from serial (renamed for clarity)

// Web Bluetooth variables
let bluetoothDevice; // The connected Bluetooth device
let bluetoothService; // The specific BLE service
let bluetoothCharacteristic; // The characteristic for writing data
let bluetoothConnectButton; // Button to connect/disconnect Bluetooth
let bluetoothStatusP; // Paragraph to display Bluetooth connection status
const bluetoothServiceUUID = '19B10000-E8F2-537E-4F6C-D104768A1214'; // CUSTOM UUID: Must match your microcontroller's BLE service UUID
const bluetoothCharacteristicUUID = '19B10001-E8F2-537E-4F6C-D104768A1214'; // CUSTOM UUID: Must match your microcontroller's BLE characteristic UUID

// Common timing and state variables
let classificationSendInterval = 3000; // Send classification every 3 seconds
let lastClassificationSendTime = 0;
let lastSentLabel = ''; // To avoid sending the same label repeatedly (stores the string label)

// Application state: 'input' or 'classifier'
let appState = 'input';

// FIX: Move videomargin declaration to the top so it's defined before setup() uses it.
let videoMargin = 20; // Margin around the video preview and UI elements

function preload() {
console.log('preload() started.');
// We won't load the model here anymore.
// Model loading will happen when the user enters a URL and clicks OK.
console.log('preload() finished.');
}

function setup() {
console.log('setup() started.');
createCanvas(windowWidth, windowHeight); // Create a responsive canvas

// Create the video capture from the webcam
video = createCapture(VIDEO, videoReady); // Add a callback for video readiness
video.hide(); // Hide the HTML video element as we'll draw it on the canvas
console.log('Video capture object created in setup().');
// Initialize custom ready flag to false
video.ready = false;

// Initial display message asking for model URL
textSize(32);
textAlign(CENTER, CENTER);
fill(0);
text('Please enter your Teachable Machine model URL below', width / 2, height / 2);

// --- Model URL Input Setup ---
modelURLInput = createInput();
modelURLInput.attribute('placeholder', 'Enter Teachable Machine model URL here');
modelURLInput.style('font-size', '16px');
modelURLInput.style('padding', '8px');
// FIX: videomargin is now defined, so this line will work.
modelURLInput.style('width', 'calc(100% - 100px - 2 * ' + videoMargin + 'px)'); // Adjust width
modelURLInput.style('border', '1px solid #ccc');
modelURLInput.style('border-radius', '4px');

modelURLLoadButton = createButton('OK');
modelURLLoadButton.mousePressed(loadModelFromInput);
modelURLLoadButton.style('font-size', '16px');
modelURLLoadButton.style('padding', '10px 15px');
modelURLLoadButton.style('background-color', '#007BFF');
modelURLLoadButton.style('color', 'white');
modelURLLoadButton.style('border', 'none');
modelURLLoadButton.style('border-radius', '5px');
modelURLLoadButton.style('cursor', 'pointer');

// --- Web Serial Setup ---
// Create p5 DOM elements for serial status and received data
serialStatusP = createP('Web Serial: Not connected.');
serialStatusP.style('font-size', '18px');
serialStatusP.style('color', '#333');
serialStatusP.style('font-family', 'sans-serif'); // Add a standard font for consistency

receivedSerialDataP = createP('Received: '); // Renamed for clarity
receivedSerialDataP.style('font-size', '18px');
receivedSerialDataP.style('color', '#333');
receivedSerialDataP.style('font-family', 'sans-serif'); // Add a standard font for consistency

// Check if Web Serial API is supported
if ('serial' in navigator) {
// Create the "Connect Serial Port" button
serialConnectButton = createButton('Connect Serial Port');
serialConnectButton.mousePressed(connectSerial); // Call connectSerial() when button is pressed
serialConnectButton.style('font-size', '16px');
serialConnectButton.style('padding', '10px 15px');
serialConnectButton.style('background-color', '#4CAF50');
serialConnectButton.style('color', 'white');
serialConnectButton.style('border', 'none');
serialConnectButton.style('border-radius', '5px');
serialConnectButton.style('cursor', 'pointer');

// Add event listener for port disconnection (e.g., USB cable unplugged)
navigator.serial.addEventListener('disconnect', portDisconnected);

} else {
// Display an error message if Web Serial is not supported
serialStatusP.html('Web Serial API not supported in this browser. Try Chrome/Edge.');
serialStatusP.style('color', 'red');
}

// --- Web Bluetooth Setup ---
bluetoothStatusP = createP('Web Bluetooth: Not connected.');
bluetoothStatusP.style('font-size', '18px');
bluetoothStatusP.style('color', '#333');
bluetoothStatusP.style('font-family', 'sans-serif');

if ('bluetooth' in navigator) {
bluetoothConnectButton = createButton('Connect Bluetooth Device');
bluetoothConnectButton.mousePressed(connectBluetooth);
bluetoothConnectButton.style('font-size', '16px');
bluetoothConnectButton.style('padding', '10px 15px');
bluetoothConnectButton.style('background-color', '#6A5ACD'); // A nice purple
bluetoothConnectButton.style('color', 'white');
bluetoothConnectButton.style('border', 'none');
bluetoothConnectButton.style('border-radius', '5px');
bluetoothConnectButton.style('cursor', 'pointer');
} else {
bluetoothStatusP.html('Web Bluetooth API not supported in this browser. Try Chrome/Edge.');
bluetoothStatusP.style('color', 'red');
}

// Set initial visibility for UI elements
setClassifierUIVisibility(false); // Hide classifier elements initially

// Call windowResized once to set initial positions correctly
windowResized();
console.log('setup() finished. appState:', appState);
}

function videoReady() {
console.log('Video capture is ready! (videoReady() callback executed)'); // Critical log
video.ready = true; // Custom flag to indicate video is ready
console.log('Video element readyState:', video.elt.readyState); // Check HTML video element state
console.log('Video dimensions: ', video.width, 'x', video.height); // Log actual video dimensions

// If model is already loaded and we're in classifier state, start classifying
if (appState === 'classifier' && classifier && classifier.ready) {
console.log('Video ready and classifier ready. Starting classification from videoReady().');
classifyVideo();
}
}

/**
* Hides/shows UI elements belonging to the classifier view.
* @param {boolean} isVisible - True to show, false to hide.
*/
function setClassifierUIVisibility(isVisible) {
const displayStyle = isVisible ? 'block' : 'none';

if (serialStatusP) serialStatusP.style('display', displayStyle);
if (receivedSerialDataP) receivedSerialDataP.style('display', displayStyle);
if (serialConnectButton) serialConnectButton.style('display', displayStyle);

if (bluetoothStatusP) bluetoothStatusP.style('display', displayStyle);
if (bluetoothConnectButton) bluetoothConnectButton.style('display', displayStyle);

// Input view elements are visible when classifier elements are hidden
if (modelURLInput) modelURLInput.style('display', isVisible ? 'none' : 'block');
if (modelURLLoadButton) modelURLLoadButton.style('display', isVisible ? 'none' : 'block');
console.log('Classifier UI visibility set to:', isVisible);
}


function loadModelFromInput() {
console.log('loadModelFromInput() called.');
const newModelURL = modelURLInput.value().trim();
if (newModelURL) {
modelURL = newModelURL;
console.log('Attempting to load model from:', modelURL); // Log attempt
currentClassification.label = 'Loading model...';
classifier = ml5.imageClassifier(modelURL, modelLoaded); // Re-initialize classifier
classifier.ready = false; // Reset ready flag
modelURLInput.attribute('disabled', ''); // Disable input during loading
modelURLLoadButton.attribute('disabled', ''); // Disable button during loading
} else {
alert('Please enter a valid Teachable Machine model URL.');
console.warn('No model URL entered.');
}
}

// Modified modelLoaded function to handle errors
function modelLoaded(error) {
console.log('modelLoaded() callback received.');
if (error) {
console.error('Error loading model:', error); // Log the error
currentClassification.label = 'Model loading failed: ' + error.message;
// Re-enable input and button so user can try again
modelURLInput.removeAttribute('disabled');
modelURLLoadButton.removeAttribute('disabled');
// Keep appState as 'input'
console.log('Model loading failed, staying in input state.');
return; // Stop here if loading failed
}
console.log('Model Loaded successfully!'); // Confirm model is loaded
classifier.ready = true; // Set a custom flag to indicate the model is ready
currentClassification.label = 'Model Loaded. Starting classification...';
// Check if video is also ready before starting classification
if (video && video.ready) {
console.log('Classifier ready and video ready. Starting classification from modelLoaded().');
classifyVideo(); // Start classifying once the model is loaded AND video is ready
} else {
console.warn('Model loaded, but video not yet ready. Will start classification when video is ready.');
}
modelURLInput.removeAttribute('disabled'); // Enable input after loading
modelURLLoadButton.removeAttribute('disabled'); // Enable button after loading
appState = 'classifier'; // Switch to classifier view
setClassifierUIVisibility(true); // Show classifier elements
windowResized(); // Re-position elements for the classifier view
console.log('modelLoaded() finished. appState:', appState);
}

function classifyVideo() {
// Ensure the video is loaded and the model is ready before attempting to classify
if (video && video.ready && classifier && classifier.ready) {
console.log('Classifying video frame.');
// Classify the current video frame and call gotResult when done
// Reference: https://learn.ml5js.org/docs/#/reference/image-classifier?id=classify
classifier.classify(video, gotResult);
} else {
// If not ready, try again after a short delay
// This is important if video or classifier isn't ready simultaneously
console.warn('classifyVideo() called, but video or classifier not ready. Retrying in 100ms.');
setTimeout(classifyVideo, 100);
}
}

function gotResult(error, results) {
// If there's an error during classification, log it
if (error) {
console.error('Error during classification:', error);
currentClassification.label = 'Error: ' + error.message;
currentClassification.confidence = 0;
// Don't stop classifying, try again
classifyVideo();
return;
}

// If no error, results will be an array of objects
// Each object has a 'label' and 'confidence'
// Sort results by confidence in descending order to get the top prediction
results.sort((a, b) => b.confidence - a.confidence);
const topResult = results[0];

// Update the current classification result
currentClassification.label = topResult.label;
currentClassification.confidence = topResult.confidence;

// Keep classifying the video feed recursively
classifyVideo();
}

function draw() {
background(220); // Clear the canvas each frame

if (appState === 'input') {
// Display input view message
textSize(32);
textAlign(CENTER, CENTER);
fill(0);
text('Please enter your Teachable Machine model URL below', width / 2, height / 2 - 50); // Slightly above input field
} else if (appState === 'classifier') {
if (video && video.ready && classifier && classifier.ready) { // Check video.ready here too
console.log('Attempting to draw video. Video ready:', video.ready, 'Classifier ready:', classifier.ready); // Critical log
// Calculate preview size while maintaining aspect ratio
// Let's make the preview height about half the canvas height, minus margins.
let previewHeight = height / 2 - 2 * videoMargin;
let previewWidth = (previewHeight / video.height) * video.width;

// Ensure previewWidth doesn't exceed available space if canvas is very narrow
// (e.g., if canvas width is less than previewWidth + videoMargin * 2)
let availableWidthForVideo = width - 2 * videoMargin;
if (previewWidth > availableWidthForVideo) {
previewWidth = availableWidthForVideo;
previewHeight = (previewWidth / video.width) * video.height;
}

// Draw the video feed at the top-left with margin
image(video, videoMargin, videoMargin, previewWidth, previewHeight);

// Classification text and background rectangle to the right of the video preview.
let textRectX = videoMargin + previewWidth + videoMargin;
let textRectWidth = width - textRectX - videoMargin;
let textRectHeight = previewHeight; // Make it the same height as the video preview
let textRectY = videoMargin; // Align with the top of the video

fill(255, 200); // White with 200 alpha
noStroke();
rect(textRectX, textRectY, textRectWidth, textRectHeight);

// Display the current classification result
fill(0);
textSize(24);
textAlign(LEFT, CENTER);
// Center text vertically within its rectangle
let textYOffset = textRectY + textRectHeight / 2;
text(`Label: ${currentClassification.label}`, textRectX + 10, textYOffset - 20); // Adjust y for two lines
// Use nf() to format confidence as a percentage with 2 decimal places
text(`Confidence: ${nf(currentClassification.confidence * 100, 0, 2)}%`, textRectX + 10, textYOffset + 20);

// --- Determine number to send based on classification ---
let numberToSend = -1; // Default to -1 for unknown labels
const currentLabel = currentClassification.label.toLowerCase(); // Use lowercase for comparison
switch (currentLabel) {
case 'ihminen':
numberToSend = 1;
break;
case 'koira':
numberToSend = 2;
break;
case 'kissa': // Assuming 'kissa' for number 3 based on common classification tasks
numberToSend = 3;
break;
default:
numberToSend = -1; // For any other label, or if label is empty/null
break;
}

// --- Send classification data via Web Serial and Web Bluetooth ---
const shouldSend = millis() - lastClassificationSendTime > classificationSendInterval && currentLabel !== lastSentLabel.toLowerCase();

if (shouldSend) {
if (numberToSend !== -1) {
// Send via Web Serial
if (port && writer) {
const serialDataToSend = numberToSend + '\n'; // Add newline for microcontroller parsing
writeSerial(serialDataToSend);
}

// Send via Web Bluetooth
if (bluetoothCharacteristic) {
// Web Bluetooth typically sends bytes directly, no newline needed
writeBluetooth(numberToSend);
}

// Update last sent info if any data was attempted to be sent
if (port || bluetoothCharacteristic) {
lastSentLabel = currentLabel;
lastClassificationSendTime = millis();
}
} else {
// Only log if no valid number was mapped and we were supposed to send
if (port || bluetoothCharacteristic) {
console.warn(`Unrecognized or default label "${currentLabel}". Not sending data.`);
}
}
}
} else if (!classifier.ready) {
// Display loading message if the model is not yet ready (should only happen briefly after OK)
textSize(32);
textAlign(CENTER, CENTER);
fill(0);
text('Loading model...', width / 2, height / 2);
}
}
}

function windowResized() {
resizeCanvas(windowWidth, windowHeight); // Adjust canvas size on window resize
console.log('windowResized() called. appState:', appState);

if (appState === 'input') {
// When in input state, position input/button centrally below the initial text
let inputWidth = min(width - 2 * videoMargin, 400); // Max 400px wide, or less if canvas is narrow
if (modelURLInput) modelURLInput.style('width', `${inputWidth - modelURLLoadButton.width - 20}px`); // Adjust for button width and spacing
if (modelURLInput) modelURLInput.position(width / 2 - inputWidth / 2, height / 2 + 50); // Below initial text
if (modelURLLoadButton) modelURLLoadButton.position(width / 2 + inputWidth / 2 - modelURLLoadButton.width, height / 2 + 50); // To the right of input
} else { // In classifier state, use the standard layout
// Recalculate video preview size based on new window dimensions
let previewHeight = height / 2 - 2 * videoMargin;
let previewWidth = (previewHeight / video.height) * video.width;
let availableWidthForVideo = width - 2 * videoMargin;
if (previewWidth > availableWidthForVideo) {
previewWidth = availableWidthForVideo;
previewHeight = (previewWidth / video.width) * video.height;
}

// Calculate the new starting y-position for the DOM elements below the video
// It's the bottom of the video (videoMargin + previewHeight) + an additional videoMargin for spacing
let domElementsStartY = videoMargin + previewHeight + videoMargin;

// Re-position model URL input and button (now below video, not centered)
if (modelURLInput) modelURLInput.style('width', 'calc(100% - 100px - 2 * ' + videoMargin + 'px)'); // Reset width
if (modelURLInput) modelURLInput.position(videoMargin, domElementsStartY);
if (modelURLLoadButton) modelURLLoadButton.position(videoMargin + modelURLInput.width + 10, domElementsStartY); // 10px spacing

// Re-position other p5 DOM elements
// Adjust starting Y for other elements based on the model URL input/button height
let otherDomElementsStartY = domElementsStartY + 40; // Assuming button height is around 30-35px + 10px spacing

// Serial elements
if (serialStatusP) serialStatusP.position(videoMargin, otherDomElementsStartY); // Adjusted y
if (receivedSerialDataP) receivedSerialDataP.position(videoMargin, otherDomElementsStartY + 30); // Adjusted y
if (serialConnectButton) serialConnectButton.position(videoMargin, otherDomElementsStartY + 70); // Adjusted y

// Bluetooth elements (below Serial elements)
let bluetoothDomElementsStartY = otherDomElementsStartY + 110; // 70 + buttonHeight (approx 40)
if (bluetoothStatusP) bluetoothStatusP.position(videoMargin, bluetoothDomElementsStartY);
if (bluetoothConnectButton) bluetoothConnectButton.position(videoMargin, bluetoothDomElementsStartY + 30);
}
}

// --- Web Serial Functions ---

/**
* Connects to a serial port selected by the user.
*/
async function connectSerial() {
try {
console.log('connectSerial() called.');
// Request a serial port from the user
port = await navigator.serial.requestPort();
serialStatusP.html('Web Serial: Connecting...');
console.log('Serial port requested and granted.');

// Open the port with a specified baud rate
// IMPORTANT: This baud rate (9600) must match the one used by your microcontroller (e.g., Arduino Serial.begin(9600);)
await port.open({ baudRate: 9600 });
serialStatusP.html('Web Serial: Connected!');
serialConnectButton.html('Disconnect Serial Port'); // Change button text
serialConnectButton.mousePressed(disconnectSerial); // Change button action to disconnect
console.log('Serial port opened with baudRate 9600.');

// Create TextDecoderStream and TextEncoderStream for easy text handling
const textDecoder = new TextDecoderStream();
const textEncoder = new TextEncoderStream();
reader = textDecoder.readable.getReader(); // Reader for incoming text data
writer = textEncoder.writable.getWriter(); // Writer for outgoing text data
console.log('TextDecoderStream and TextEncoderStream created.');

// Pipe the port's readable stream through the decoder
port.readable.pipeTo(textDecoder.writable);
// TÄMÄ ON OIKEIN: Pipe the encoder's readable stream (containing encoded bytes) to the port's writable stream
textEncoder.readable.pipeTo(port.writable); // THIS IS THE CORRECTION
console.log('Serial streams piped.');

readSerial(); // Start continuously reading data from the port
} catch (error) {
console.error('Error connecting to serial port:', error);
serialStatusP.html('Web Serial: Connection failed. ' + error.message);
}
}

/**
* Disconnects from the currently connected serial port.
*/
async function disconnectSerial() {
try {
console.log('disconnectSerial() called.');
if (reader) await reader.cancel(); // Cancel any pending read operations
if (writer) await writer.close(); // Close the writer stream
if (port) await port.close(); // Close the serial port

portDisconnected(); // Reset variables and UI
console.log('Serial port successfully disconnected.');
} catch (error) {
console.error('Error disconnecting serial port:', error);
serialStatusP.html('Web Serial: Disconnection failed. ' + error.message);
}
}

/**
* Continuously reads data from the serial port.
*/
async function readSerial() {
console.log('readSerial() started.');
while (port && port.readable) {
try {
const { value, done } = await reader.read();
if (done) {
console.log('Reader closed.');
break; // Exit loop if reader is closed
}

// Check if the received value is "1" (and trim whitespace for robustness)
if (value.trim() === '1') {
receivedSerialDataP.html('Received: remote device connected');
// You might want to update serialStatusP too, but currentStatusP is already "Connected!"
} else {
receivedSerialDataP.html(`Received: ${value}`); // Display received data
}

// For more complex data, you might parse 'value' here (e.g., JSON.parse(value))
} catch (error) {
console.error('Error reading from serial port:', error);
serialStatusP.html('Web Serial: Read error. ' + error.message);
break; // Exit loop on error
}
}
portDisconnected(); // Assume disconnected if loop exits (e.g., due to error or port closure)
console.log('readSerial() stopped.');
}

/**
* Writes data to the serial port.
* @param {string} data - The string data to send.
*/
async function writeSerial(data) {
if (port && writer) {
try {
await writer.write(data);
console.log('Sent to Serial:', data.trim()); // Log sent data (the number) without the trailing newline
} catch (error) {
console.error('Error writing to serial port:', error);
serialStatusP.html('Web Serial: Write error. ' + error.message);
portDisconnected(); // Assume disconnected on write error
}
} else {
console.warn('Cannot write: Serial port not connected.');
serialStatusP.html('Web Serial: Cannot write, not connected.');
}
}

/**
* Resets serial variables and UI when the port disconnects.
*/
function portDisconnected() {
console.log('Serial port disconnected.');
port = undefined;
reader = undefined;
writer = undefined;
serialStatusP.html('Web Serial: Disconnected.');
receivedSerialDataP.html('Received: ');
if (serialConnectButton) {
serialConnectButton.html('Connect Serial Port');
serialConnectButton.mousePressed(connectSerial); // Change button action back to connect
}
// Reset lastSentLabel and timer to allow re-sending on reconnect
lastSentLabel = '';
lastClassificationSendTime = 0;
}


// --- Web Bluetooth Functions ---

/**
* Connects to a Bluetooth device with the specified service and characteristic.
*/
async function connectBluetooth() {
try {
console.log('connectBluetooth() called.');
// Request a Bluetooth device from the user with the specified service
bluetoothStatusP.html('Web Bluetooth: Requesting device...');
bluetoothDevice = await navigator.bluetooth.requestDevice({
filters: [{ services: [bluetoothServiceUUID] }], // Filter for our custom service
optionalServices: [] // No optional services needed for this example
});
console.log('Bluetooth device requested and granted:', bluetoothDevice.name);

// Add event listener for GATT server disconnection
bluetoothDevice.addEventListener('gattserverdisconnected', bluetoothDisconnected);

// Connect to the GATT server
bluetoothStatusP.html('Web Bluetooth: Connecting to GATT server...');
const server = await bluetoothDevice.gatt.connect();
console.log('Connected to GATT server.');

// Get the primary service
bluetoothStatusP.html('Web Bluetooth: Getting service...');
bluetoothService = await server.getPrimaryService(bluetoothServiceUUID);
console.log('Got Bluetooth service:', bluetoothServiceUUID);

// Get the characteristic for writing
bluetoothStatusP.html('Web Bluetooth: Getting characteristic...');
bluetoothCharacteristic = await bluetoothService.getCharacteristic(bluetoothCharacteristicUUID);
console.log('Got Bluetooth characteristic:', bluetoothCharacteristicUUID);

bluetoothStatusP.html(`Web Bluetooth: Connected to ${bluetoothDevice.name}!`);
bluetoothConnectButton.html('Disconnect Bluetooth Device'); // Change button text
bluetoothConnectButton.mousePressed(disconnectBluetooth); // Change button action
console.log('Bluetooth connection established successfully.');

} catch (error) {
console.error('Error connecting to Bluetooth device:', error);
bluetoothStatusP.html('Web Bluetooth: Connection failed. ' + error.message);
bluetoothDisconnected(); // Reset UI on failure
}
}

/**
* Disconnects from the currently connected Bluetooth device.
*/
async function disconnectBluetooth() {
try {
console.log('disconnectBluetooth() called.');
if (bluetoothDevice && bluetoothDevice.gatt.connected) {
bluetoothStatusP.html('Web Bluetooth: Disconnecting...');
bluetoothDevice.gatt.disconnect();
console.log('Bluetooth GATT server disconnected.');
}
bluetoothDisconnected(); // Reset variables and UI
} catch (error) {
console.error('Error disconnecting Bluetooth device:', error);
bluetoothStatusP.html('Web Bluetooth: Disconnection failed. ' + error.message);
}
}

/**
* Writes a single number (byte) to the Bluetooth characteristic.
* @param {number} number - The number (1, 2, or 3) to send.
*/
async function writeBluetooth(number) {
if (bluetoothCharacteristic) {
try {
// Create a DataView to send a single byte
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/DataView
const dataView = new DataView(new ArrayBuffer(1));
dataView.setUint8(0, number); // Write the number as an 8-bit unsigned integer

await bluetoothCharacteristic.writeValue(dataView);
console.log('Sent to Bluetooth:', number);
} catch (error) {
console.error('Error writing to Bluetooth characteristic:', error);
bluetoothStatusP.html('Web Bluetooth: Write error. ' + error.message);
bluetoothDisconnected(); // Assume disconnected on write error
}
} else {
console.warn('Cannot write: Bluetooth device not connected.');
bluetoothStatusP.html('Web Bluetooth: Cannot write, not connected.');
}
}

/**
* Resets Bluetooth variables and UI when the device disconnects (or fails to connect).
*/
function bluetoothDisconnected() {
console.log('Bluetooth device disconnected or connection failed.');
bluetoothDevice = undefined;
bluetoothService = undefined;
bluetoothCharacteristic = undefined;
bluetoothStatusP.html('Web Bluetooth: Disconnected.');
if (bluetoothConnectButton) {
bluetoothConnectButton.html('Connect Bluetooth Device');
bluetoothConnectButton.mousePressed(connectBluetooth); // Change button action back to connect
}
// Reset lastSentLabel and timer to allow re-sending on reconnect
lastSentLabel = '';
lastClassificationSendTime = 0;
}
```

---

### `index.html` (No Changes Needed)

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>p5.js Sketch</title>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.3/lib/p5.min.js"></script>
<script src="https://unpkg.com/ml5@0.12.2/dist/ml5.min.js"></script> <!-- Added ml5.js library -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="sketch.js"></script>
</body>
</html>
```

---

### `style.css` (No Changes Needed)

```css
body {
margin: 0;
padding: 0;
overflow: hidden;
}

canvas {
display: block;
}
```

---

**To use this new functionality:**

1. **Update your microcontroller's BLE code:** Ensure it exposes a service with the UUID `19B10000-E8F2-537E-4F6C-D104768A1214` and within that service, a characteristic that allows writing, with the UUID `19B10001-E8F2-537E-4F6C-D104768A1214`. The characteristic should expect a single 8-bit unsigned integer (byte).
2. **Run the sketch:**
* Enter your Teachable Machine model URL and click "OK".
* Once the model is loaded and classification starts, you will see a new "Connect Bluetooth Device" button below the Web Serial button.
* Click "Connect Bluetooth Device". Your browser will prompt you to select a Bluetooth device. Choose your microcontroller.
* Once connected, the classification numbers (1, 2, or 3) will be sent via both Web Serial (if connected) and Web Bluetooth whenever the classification label changes and the 3-second interval has passed.

Let me know if you have any questions or run into issues!
👤 You
Bluetooth painike ei tee mitään
🤖 AI
Aivan, tämä on yleinen ongelma Web Bluetoothin kanssa, ja se voi johtua monesta syystä. Koska minulla ei ole pääsyä laitteeseesi tai selaimesi konsoliin reaaliaikaisesti, meidän on lisättävä lokiviestejä koodiin, jotta saamme paremman kuvan siitä, mitä tapahtuu, kun painat Bluetooth-painiketta.

Tarkistan seuraavat asiat:
1. **Kutsutaanko `connectBluetooth()`-funktiota ylipäätään?**
2. **Onko Web Bluetooth API tuettu selaimessasi?** (Koodi jo tarkistaa tämän, mutta on hyvä varmistaa.)
3. **Tuleeko virheitä konsoliin?**
4. **Näyttääkö selain Bluetooth-laitevalintaikkunaa?**
5. **Mainostaako mikrokontrollerisi oikeita UUID-tunnuksia?** (Tämä on kriittinen!)

Lisään nyt `console.log`-lauseita `connectBluetooth()`-funktioon ja sen ympärille, jotta näemme paremmin, missä vaiheessa ohjelma mahdollisesti pysähtyy.

---

### `sketch.js` (Updated with more logs)

```javascript
// Remixed from: Anonymous/teachable-machine-p5-js-device-web-serial
// Original: https://p5js.ai/gallery/s/teachable-machine-p5-js-device-web-serial
// ----------------------------------------

// Remixed from: Larux75/teachable-machine-web-serial2
// Original: https://p5js.ai/gallery/u/Larux75-p5js/teachable-machine-web-serial2
// ----------------------------------------

//====================================================
// This code was written on the p5js.ai IDE
// Using the xelsed-alpha6-flash tuned model
// Visit the site and support Dyslexic Research
//===================================================

let classifier; // The ml5.js image classifier
let video; // The webcam video capture
let modelURLInput; // Input field for Teachable Machine model URL
let modelURLLoadButton; // Button to load the model from the URL
let modelURL = ''; // Your Teachable Machine model URL (initially empty)
let currentClassification = { label: 'Please enter model URL and click OK', confidence: 0 }; // Stores the latest classification result

// Web Serial variables
let port; // The connected serial port
let reader; // For reading data from the port
let writer; // For writing data from the port
let serialConnectButton; // Button to connect/disconnect serial
let serialStatusP; // Paragraph to display serial connection status
let receivedSerialDataP; // Paragraph to display data received from serial (renamed for clarity)

// Web Bluetooth variables
let bluetoothDevice; // The connected Bluetooth device
let bluetoothService; // The specific BLE service
let bluetoothCharacteristic; // The characteristic for writing data
let bluetoothConnectButton; // Button to connect/disconnect Bluetooth
let bluetoothStatusP; // Paragraph to display Bluetooth connection status
const bluetoothServiceUUID = '19B10000-E8F2-537E-4F6C-D104768A1214'; // CUSTOM UUID: Must match your microcontroller's BLE service UUID
const bluetoothCharacteristicUUID = '19B10001-E8F2-537E-4F6C-D104768A1214'; // CUSTOM UUID: Must match your microcontroller's BLE characteristic UUID

// Common timing and state variables
let classificationSendInterval = 3000; // Send classification every 3 seconds
let lastClassificationSendTime = 0;
let lastSentLabel = ''; // To avoid sending the same label repeatedly (stores the string label)

// Application state: 'input' or 'classifier'
let appState = 'input';

// FIX: Move videomargin declaration to the top so it's defined before setup() uses it.
let videoMargin = 20; // Margin around the video preview and UI elements

function preload() {
console.log('preload() started.');
// We won't load the model here anymore.
// Model loading will happen when the user enters a URL and clicks OK.
console.log('preload() finished.');
}

function setup() {
console.log('setup() started.');
createCanvas(windowWidth, windowHeight); // Create a responsive canvas

// Create the video capture from the webcam
video = createCapture(VIDEO, videoReady); // Add a callback for video readiness
video.hide(); // Hide the HTML video element as we'll draw it on the canvas
console.log('Video capture object created in setup().');
// Initialize custom ready flag to false
video.ready = false;

// Initial display message asking for model URL
textSize(32);
textAlign(CENTER, CENTER);
fill(0);
text('Please enter your Teachable Machine model URL below', width / 2, height / 2);

// --- Model URL Input Setup ---
modelURLInput = createInput();
modelURLInput.attribute('placeholder', 'Enter Teachable Machine model URL here');
modelURLInput.style('font-size', '16px');
modelURLInput.style('padding', '8px');
// FIX: videomargin is now defined, so this line will work.
modelURLInput.style('width', 'calc(100% - 100px - 2 * ' + videoMargin + 'px)'); // Adjust width
modelURLInput.style('border', '1px solid #ccc');
modelURLInput.style('border-radius', '4px');

modelURLLoadButton = createButton('OK');
modelURLLoadButton.mousePressed(loadModelFromInput);
modelURLLoadButton.style('font-size', '16px');
modelURLLoadButton.style('padding', '10px 15px');
modelURLLoadButton.style('background-color', '#007BFF');
modelURLLoadButton.style('color', 'white');
modelURLLoadButton.style('border', 'none');
modelURLLoadButton.style('border-radius', '5px');
modelURLLoadButton.style('cursor', 'pointer');

// --- Web Serial Setup ---
// Create p5 DOM elements for serial status and received data
serialStatusP = createP('Web Serial: Not connected.');
serialStatusP.style('font-size', '18px');
serialStatusP.style('color', '#333');
serialStatusP.style('font-family', 'sans-serif'); // Add a standard font for consistency

receivedSerialDataP = createP('Received: '); // Renamed for clarity
receivedSerialDataP.style('font-size', '18px');
receivedSerialDataP.style('color', '#333');
receivedSerialDataP.style('font-family', 'sans-serif'); // Add a standard font for consistency

// Check if Web Serial API is supported
if ('serial' in navigator) {
// Create the "Connect Serial Port" button
serialConnectButton = createButton('Connect Serial Port');
serialConnectButton.mousePressed(connectSerial); // Call connectSerial() when button is pressed
serialConnectButton.style('font-size', '16px');
serialConnectButton.style('padding', '10px 15px');
serialConnectButton.style('background-color', '#4CAF50');
serialConnectButton.style('color', 'white');
serialConnectButton.style('border', 'none');
serialConnectButton.style('border-radius', '5px');
serialConnectButton.style('cursor', 'pointer');

// Add event listener for port disconnection (e.g., USB cable unplugged)
navigator.serial.addEventListener('disconnect', portDisconnected);

} else {
// Display an error message if Web Serial is not supported
serialStatusP.html('Web Serial API not supported in this browser. Try Chrome/Edge.');
serialStatusP.style('color', 'red');
}

// --- Web Bluetooth Setup ---
bluetoothStatusP = createP('Web Bluetooth: Not connected.');
bluetoothStatusP.style('font-size', '18px');
bluetoothStatusP.style('color', '#333');
bluetoothStatusP.style('font-family', 'sans-serif');

if ('bluetooth' in navigator) {
console.log('Web Bluetooth API is supported in this browser.');
bluetoothConnectButton = createButton('Connect Bluetooth Device');
bluetoothConnectButton.mousePressed(connectBluetooth);
bluetoothConnectButton.style('font-size', '16px');
bluetoothConnectButton.style('padding', '10px 15px');
bluetoothConnectButton.style('background-color', '#6A5ACD'); // A nice purple
bluetoothConnectButton.style('color', 'white');
bluetoothConnectButton.style('border', 'none');
bluetoothConnectButton.style('border-radius', '5px');
bluetoothConnectButton.style('cursor', 'pointer');
} else {
console.warn('Web Bluetooth API is NOT supported in this browser.');
bluetoothStatusP.html('Web Bluetooth API not supported in this browser. Try Chrome/Edge.');
bluetoothStatusP.style('color', 'red');
}

// Set initial visibility for UI elements
setClassifierUIVisibility(false); // Hide classifier elements initially

// Call windowResized once to set initial positions correctly
windowResized();
console.log('setup() finished. appState:', appState);
}

function videoReady() {
console.log('Video capture is ready! (videoReady() callback executed)'); // Critical log
video.ready = true; // Custom flag to indicate video is ready
console.log('Video element readyState:', video.elt.readyState); // Check HTML video element state
console.log('Video dimensions: ', video.width, 'x', video.height); // Log actual video dimensions

// If model is already loaded and we're in classifier state, start classifying
if (appState === 'classifier' && classifier && classifier.ready) {
console.log('Video ready and classifier ready. Starting classification from videoReady().');
classifyVideo();
}
}

/**
* Hides/shows UI elements belonging to the classifier view.
* @param {boolean} isVisible - True to show, false to hide.
*/
function setClassifierUIVisibility(isVisible) {
const displayStyle = isVisible ? 'block' : 'none';

if (serialStatusP) serialStatusP.style('display', displayStyle);
if (receivedSerialDataP) receivedSerialDataP.style('display', displayStyle);
if (serialConnectButton) serialConnectButton.style('display', displayStyle);

if (bluetoothStatusP) bluetoothStatusP.style('display', displayStyle);
if (bluetoothConnectButton) bluetoothConnectButton.style('display', displayStyle);

// Input view elements are visible when classifier elements are hidden
if (modelURLInput) modelURLInput.style('display', isVisible ? 'none' : 'block');
if (modelURLLoadButton) modelURLLoadButton.style('display', isVisible ? 'none' : 'block');
console.log('Classifier UI visibility set to:', isVisible);
}


function loadModelFromInput() {
console.log('loadModelFromInput() called.');
const newModelURL = modelURLInput.value().trim();
if (newModelURL) {
modelURL = newModelURL;
console.log('Attempting to load model from:', modelURL); // Log attempt
currentClassification.label = 'Loading model...';
classifier = ml5.imageClassifier(modelURL, modelLoaded); // Re-initialize classifier
classifier.ready = false; // Reset ready flag
modelURLInput.attribute('disabled', ''); // Disable input during loading
modelURLLoadButton.attribute('disabled', ''); // Disable button during loading
} else {
alert('Please enter a valid Teachable Machine model URL.');
console.warn('No model URL entered.');
}
}

// Modified modelLoaded function to handle errors
function modelLoaded(error) {
console.log('modelLoaded() callback received.');
if (error) {
console.error('Error loading model:', error); // Log the error
currentClassification.label = 'Model loading failed: ' + error.message;
// Re-enable input and button so user can try again
modelURLInput.removeAttribute('disabled');
modelURLLoadButton.removeAttribute('disabled');
// Keep appState as 'input'
console.log('Model loading failed, staying in input state.');
return; // Stop here if loading failed
}
console.log('Model Loaded successfully!'); // Confirm model is loaded
classifier.ready = true; // Set a custom flag to indicate the model is ready
currentClassification.label = 'Model Loaded. Starting classification...';
// Check if video is also ready before starting classification
if (video && video.ready) {
console.log('Classifier ready and video ready. Starting classification from modelLoaded().');
classifyVideo(); // Start classifying once the model is loaded AND video is ready
} else {
console.warn('Model loaded, but video not yet ready. Will start classification when video is ready.');
}
modelURLInput.removeAttribute('disabled'); // Enable input after loading
modelURLLoadButton.removeAttribute('disabled'); // Enable button after loading
appState = 'classifier'; // Switch to classifier view
setClassifierUIVisibility(true); // Show classifier elements
windowResized(); // Re-position elements for the classifier view
console.log('modelLoaded() finished. appState:', appState);
}

function classifyVideo() {
// Ensure the video is loaded and the model is ready before attempting to classify
if (video && video.ready && classifier && classifier.ready) {
console.log('Classifying video frame.');
// Classify the current video frame and call gotResult when done
// Reference: https://learn.ml5js.org/docs/#/reference/image-classifier?id=classify
classifier.classify(video, gotResult);
} else {
// If not ready, try again after a short delay
// This is important if video or classifier isn't ready simultaneously
console.warn('classifyVideo() called, but video or classifier not ready. Retrying in 100ms.');
setTimeout(classifyVideo, 100);
}
}

function gotResult(error, results) {
// If there's an error during classification, log it
if (error) {
console.error('Error during classification:', error);
currentClassification.label = 'Error: ' + error.message;
currentClassification.confidence = 0;
// Don't stop classifying, try again
classifyVideo();
return;
}

// If no error, results will be an array of objects
// Each object has a 'label' and 'confidence'
// Sort results by confidence in descending order to get the top prediction
results.sort((a, b) => b.confidence - a.confidence);
const topResult = results[0];

// Update the current classification result
currentClassification.label = topResult.label;
currentClassification.confidence = topResult.confidence;

// Keep classifying the video feed recursively
classifyVideo();
}

function draw() {
background(220); // Clear the canvas each frame

if (appState === 'input') {
// Display input view message
textSize(32);
textAlign(CENTER, CENTER);
fill(0);
text('Please enter your Teachable Machine model URL below', width / 2, height / 2 - 50); // Slightly above input field
} else if (appState === 'classifier') {
if (video && video.ready && classifier && classifier.ready) { // Check video.ready here too
console.log('Attempting to draw video. Video ready:', video.ready, 'Classifier ready:', classifier.ready); // Critical log
// Calculate preview size while maintaining aspect ratio
// Let's make the preview height about half the canvas height, minus margins.
let previewHeight = height / 2 - 2 * videoMargin;
let previewWidth = (previewHeight / video.height) * video.width;

// Ensure previewWidth doesn't exceed available space if canvas is very narrow
// (e.g., if canvas width is less than previewWidth + videoMargin * 2)
let availableWidthForVideo = width - 2 * videoMargin;
if (previewWidth > availableWidthForVideo) {
previewWidth = availableWidthForVideo;
previewHeight = (previewWidth / video.width) * video.height;
}

// Draw the video feed at the top-left with margin
image(video, videoMargin, videoMargin, previewWidth, previewHeight);

// Classification text and background rectangle to the right of the video preview.
let textRectX = videoMargin + previewWidth + videoMargin;
let textRectWidth = width - textRectX - videoMargin;
let textRectHeight = previewHeight; // Make it the same height as the video preview
let textRectY = videoMargin; // Align with the top of the video

fill(255, 200); // White with 200 alpha
noStroke();
rect(textRectX, textRectY, textRectWidth, textRectHeight);

// Display the current classification result
fill(0);
textSize(24);
textAlign(LEFT, CENTER);
// Center text vertically within its rectangle
let textYOffset = textRectY + textRectHeight / 2;
text(`Label: ${currentClassification.label}`, textRectX + 10, textYOffset - 20); // Adjust y for two lines
// Use nf() to format confidence as a percentage with 2 decimal places
text(`Confidence: ${nf(currentClassification.confidence * 100, 0, 2)}%`, textRectX + 10, textYOffset + 20);

// --- Determine number to send based on classification ---
let numberToSend = -1; // Default to -1 for unknown labels
const currentLabel = currentClassification.label.toLowerCase(); // Use lowercase for comparison
switch (currentLabel) {
case 'ihminen':
numberToSend = 1;
break;
case 'koira':
numberToSend = 2;
break;
case 'kissa': // Assuming 'kissa' for number 3 based on common classification tasks
numberToSend = 3;
break;
default:
numberToSend = -1; // For any other label, or if label is empty/null
break;
}

// --- Send classification data via Web Serial and Web Bluetooth ---
const shouldSend = millis() - lastClassificationSendTime > classificationSendInterval && currentLabel !== lastSentLabel.toLowerCase();

if (shouldSend) {
if (numberToSend !== -1) {
// Send via Web Serial
if (port && writer) {
const serialDataToSend = numberToSend + '\n'; // Add newline for microcontroller parsing
writeSerial(serialDataToSend);
}

// Send via Web Bluetooth
if (bluetoothCharacteristic) {
// Web Bluetooth typically sends bytes directly, no newline needed
writeBluetooth(numberToSend);
}

// Update last sent info if any data was attempted to be sent
if (port || bluetoothCharacteristic) {
lastSentLabel = currentLabel;
lastClassificationSendTime = millis();
}
} else {
// Only log if no valid number was mapped and we were supposed to send
if (port || bluetoothCharacteristic) {
console.warn(`Unrecognized or default label "${currentLabel}". Not sending data.`);
}
}
}
} else if (!classifier.ready) {
// Display loading message if the model is not yet ready (should only happen briefly after OK)
textSize(32);
textAlign(CENTER, CENTER);
fill(0);
text('Loading model...', width / 2, height / 2);
}
}
}

function windowResized() {
resizeCanvas(windowWidth, windowHeight); // Adjust canvas size on window resize
console.log('windowResized() called. appState:', appState);

if (appState === 'input') {
// When in input state, position input/button centrally below the initial text
let inputWidth = min(width - 2 * videoMargin, 400); // Max 400px wide, or less if canvas is narrow
if (modelURLInput) modelURLInput.style('width', `${inputWidth - modelURLLoadButton.width - 20}px`); // Adjust for button width and spacing
if (modelURLInput) modelURLInput.position(width / 2 - inputWidth / 2, height / 2 + 50); // Below initial text
if (modelURLLoadButton) modelURLLoadButton.position(width / 2 + inputWidth / 2 - modelURLLoadButton.width, height / 2 + 50); // To the right of input
} else { // In classifier state, use the standard layout
// Recalculate video preview size based on new window dimensions
let previewHeight = height / 2 - 2 * videoMargin;
let previewWidth = (previewHeight / video.height) * video.width;
let availableWidthForVideo = width - 2 * videoMargin;
if (previewWidth > availableWidthForVideo) {
previewWidth = availableWidthForVideo;
previewHeight = (previewWidth / video.width) * video.height;
}

// Calculate the new starting y-position for the DOM elements below the video
// It's the bottom of the video (videoMargin + previewHeight) + an additional videoMargin for spacing
let domElementsStartY = videoMargin + previewHeight + videoMargin;

// Re-position model URL input and button (now below video, not centered)
if (modelURLInput) modelURLInput.style('width', 'calc(100% - 100px - 2 * ' + videoMargin + 'px)'); // Reset width
if (modelURLInput) modelURLInput.position(videoMargin, domElementsStartY);
if (modelURLLoadButton) modelURLLoadButton.position(videoMargin + modelURLInput.width + 10, domElementsStartY); // 10px spacing

// Re-position other p5 DOM elements
// Adjust starting Y for other elements based on the model URL input/button height
let otherDomElementsStartY = domElementsStartY + 40; // Assuming button height is around 30-35px + 10px spacing

// Serial elements
if (serialStatusP) serialStatusP.position(videoMargin, otherDomElementsStartY); // Adjusted y
if (receivedSerialDataP) receivedSerialDataP.position(videoMargin, otherDomElementsStartY + 30); // Adjusted y
if (serialConnectButton) serialConnectButton.position(videoMargin, otherDomElementsStartY + 70); // Adjusted y

// Bluetooth elements (below Serial elements)
let bluetoothDomElementsStartY = otherDomElementsStartY + 110; // 70 + buttonHeight (approx 40)
if (bluetoothStatusP) bluetoothStatusP.position(videoMargin, bluetoothDomElementsStartY);
if (bluetoothConnectButton) bluetoothConnectButton.position(videoMargin, bluetoothDomElementsStartY + 30);
}
}

// --- Web Serial Functions ---

/**
* Connects to a serial port selected by the user.
*/
async function connectSerial() {
try {
console.log('connectSerial() called.');
// Request a serial port from the user
port = await navigator.serial.requestPort();
serialStatusP.html('Web Serial: Connecting...');
console.log('Serial port requested and granted.');

// Open the port with a specified baud rate
// IMPORTANT: This baud rate (9600) must match the one used by your microcontroller (e.g., Arduino Serial.begin(9600);)
await port.open({ baudRate: 9600 });
serialStatusP.html('Web Serial: Connected!');
serialConnectButton.html('Disconnect Serial Port'); // Change button text
serialConnectButton.mousePressed(disconnectSerial); // Change button action to disconnect
console.log('Serial port opened with baudRate 9600.');

// Create TextDecoderStream and TextEncoderStream for easy text handling
const textDecoder = new TextDecoderStream();
const textEncoder = new TextEncoderStream();
reader = textDecoder.readable.getReader(); // Reader for incoming text data
writer = textEncoder.writable.getWriter(); // Writer for outgoing text data
console.log('TextDecoderStream and TextEncoderStream created.');

// Pipe the port's readable stream through the decoder
port.readable.pipeTo(textDecoder.writable);
// TÄMÄ ON OIKEIN: Pipe the encoder's readable stream (containing encoded bytes) to the port's writable stream
textEncoder.readable.pipeTo(port.writable); // THIS IS THE CORRECTION
console.log('Serial streams piped.');

readSerial(); // Start continuously reading data from the port
} catch (error) {
console.error('Error connecting to serial port:', error);
serialStatusP.html('Web Serial: Connection failed. ' + error.message);
}
}

/**
* Disconnects from the currently connected serial port.
*/
async function disconnectSerial() {
try {
console.log('disconnectSerial() called.');
if (reader) await reader.cancel(); // Cancel any pending read operations
if (writer) await writer.close(); // Close the writer stream
if (port) await port.close(); // Close the serial port

portDisconnected(); // Reset variables and UI
console.log('Serial port successfully disconnected.');
} catch (error) {
console.error('Error disconnecting serial port:', error);
serialStatusP.html('Web Serial: Disconnection failed. ' + error.message);
}
}

/**
* Continuously reads data from the serial port.
*/
async function readSerial() {
console.log('readSerial() started.');
while (port && port.readable) {
try {
const { value, done } = await reader.read();
if (done) {
console.log('Reader closed.');
break; // Exit loop if reader is closed
}

// Check if the received value is "1" (and trim whitespace for robustness)
if (value.trim() === '1') {
receivedSerialDataP.html('Received: remote device connected');
// You might want to update serialStatusP too, but currentStatusP is already "Connected!"
} else {
receivedSerialDataP.html(`Received: ${value}`); // Display received data
}

// For more complex data, you might parse 'value' here (e.g., JSON.parse(value))
} catch (error) {
console.error('Error reading from serial port:', error);
serialStatusP.html('Web Serial: Read error. ' + error.message);
break; // Exit loop on error
}
}
portDisconnected(); // Assume disconnected if loop exits (e.g., due to error or port closure)
console.log('readSerial() stopped.');
}

/**
* Writes data to the serial port.
* @param {string} data - The string data to send.
*/
async function writeSerial(data) {
if (port && writer) {
try {
await writer.write(data);
console.log('Sent to Serial:', data.trim()); // Log sent data (the number) without the trailing newline
} catch (error) {
console.error('Error writing to serial port:', error);
serialStatusP.html('Web Serial: Write error. ' + error.message);
portDisconnected(); // Assume disconnected on write error
}
} else {
console.warn('Cannot write: Serial port not connected.');
serialStatusP.html('Web Serial: Cannot write, not connected.');
}
}

/**
* Resets serial variables and UI when the port disconnects.
*/
function portDisconnected() {
console.log('Serial port disconnected.');
port = undefined;
reader = undefined;
writer = undefined;
serialStatusP.html('Web Serial: Disconnected.');
receivedSerialDataP.html('Received: ');
if (serialConnectButton) {
serialConnectButton.html('Connect Serial Port');
serialConnectButton.mousePressed(connectSerial); // Change button action back to connect
}
// Reset lastSentLabel and timer to allow re-sending on reconnect
lastSentLabel = '';
lastClassificationSendTime = 0;
}


// --- Web Bluetooth Functions ---

/**
* Connects to a Bluetooth device with the specified service and characteristic.
*/
async function connectBluetooth() {
console.log('connectBluetooth() called from button press.'); // NEW LOG
bluetoothStatusP.html('Web Bluetooth: Requesting device...');
try {
// Request a Bluetooth device from the user with the specified service
console.log('Attempting to request Bluetooth device with service UUID:', bluetoothServiceUUID); // NEW LOG
bluetoothDevice = await navigator.bluetooth.requestDevice({
filters: [{ services: [bluetoothServiceUUID] }], // Filter for our custom service
optionalServices: [] // No optional services needed for this example
});
console.log('Bluetooth device requested and granted:', bluetoothDevice.name);

// Add event listener for GATT server disconnection
bluetoothDevice.addEventListener('gattserverdisconnected', bluetoothDisconnected);

// Connect to the GATT server
bluetoothStatusP.html('Web Bluetooth: Connecting to GATT server...');
console.log('Attempting to connect to GATT server.'); // NEW LOG
const server = await bluetoothDevice.gatt.connect();
console.log('Connected to GATT server.');

// Get the primary service
bluetoothStatusP.html('Web Bluetooth: Getting service...');
console.log('Attempting to get Bluetooth service with UUID:', bluetoothServiceUUID); // NEW LOG
bluetoothService = await server.getPrimaryService(bluetoothServiceUUID);
console.log('Got Bluetooth service:', bluetoothServiceUUID);

// Get the characteristic for writing
bluetoothStatusP.html('Web Bluetooth: Getting characteristic...');
console.log('Attempting to get Bluetooth characteristic with UUID:', bluetoothCharacteristicUUID); // NEW LOG
bluetoothCharacteristic = await bluetoothService.getCharacteristic(bluetoothCharacteristicUUID);
console.log('Got Bluetooth characteristic:', bluetoothCharacteristicUUID);

bluetoothStatusP.html(`Web Bluetooth: Connected to ${bluetoothDevice.name}!`);
bluetoothConnectButton.html('Disconnect Bluetooth Device'); // Change button text
bluetoothConnectButton.mousePressed(disconnectBluetooth); // Change button action
console.log('Bluetooth connection established successfully.');

} catch (error) {
console.error('Error connecting to Bluetooth device:', error);
bluetoothStatusP.html('Web Bluetooth: Connection failed. ' + error.message);
bluetoothDisconnected(); // Reset UI on failure
}
}

/**
* Disconnects from the currently connected Bluetooth device.
*/
async function disconnectBluetooth() {
try {
console.log('disconnectBluetooth() called.');
if (bluetoothDevice && bluetoothDevice.gatt.connected) {
bluetoothStatusP.html('Web Bluetooth: Disconnecting...');
bluetoothDevice.gatt.disconnect();
console.log('Bluetooth GATT server disconnected.');
}
bluetoothDisconnected(); // Reset variables and UI
} catch (error) {
console.error('Error disconnecting Bluetooth device:', error);
bluetoothStatusP.html('Web Bluetooth: Disconnection failed. ' + error.message);
}
}

/**
* Writes a single number (byte) to the Bluetooth characteristic.
* @param {number} number - The number (1, 2, or 3) to send.
*/
async function writeBluetooth(number) {
if (bluetoothCharacteristic) {
try {
// Create a DataView to send a single byte
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/DataView
const dataView = new DataView(new ArrayBuffer(1));
dataView.setUint8(0, number); // Write the number as an 8-bit unsigned integer

await bluetoothCharacteristic.writeValue(dataView);
console.log('Sent to Bluetooth:', number);
} catch (error) {
console.error('Error writing to Bluetooth characteristic:', error);
bluetoothStatusP.html('Web Bluetooth: Write error. ' + error.message);
bluetoothDisconnected(); // Assume disconnected on write error
}
} else {
console.warn('Cannot write: Bluetooth device not connected.');
bluetoothStatusP.html('Web Bluetooth: Cannot write, not connected.');
}
}

/**
* Resets Bluetooth variables and UI when the device disconnects (or fails to connect).
*/
function bluetoothDisconnected() {
console.log('Bluetooth device disconnected or connection failed.');
bluetoothDevice = undefined;
bluetoothService = undefined;
bluetoothCharacteristic = undefined;
bluetoothStatusP.html('Web Bluetooth: Disconnected.');
if (bluetoothConnectButton) {
bluetoothConnectButton.html('Connect Bluetooth Device');
bluetoothConnectButton.mousePressed(connectBluetooth); // Change button action back to connect
}
// Reset lastSentLabel and timer to allow re-sending on reconnect
lastSentLabel = '';
lastClassificationSendTime = 0;
}
```

---

### `index.html` (No Changes Needed)

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>p5.js Sketch</title>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.3/lib/p5.min.js"></script>
<script src="https://unpkg.com/ml5@0.12.2/dist/ml5.min.js"></script> <!-- Added ml5.js library -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="sketch.js"></script>
</body>
</html>
```

---

### `style.css` (No Changes Needed)

```css
body {
margin: 0;
padding: 0;
overflow: hidden;
}

canvas {
display: block;
}
```

---

**Nyt, kun olet päivittänyt koodin, toimi näin:**

1. **Avaa selaimen kehittäjätyökalut:**
* Chrome/Edge: Paina `F12` tai napsauta hiiren kakkospainikkeella sivua ja valitse "Inspect" (Tarkasta) tai "Inspect Element" (Tarkasta elementti).
* Siirry "Console" (Konsoli) -välilehdelle.

2. **Lataa sketch uudelleen:** Varmista, että näet uudet lokiviestit konsolissa.

3. **Yritä uudelleen:**
* Anna Teachable Machine -mallisi URL ja paina "OK".
* Kun malli on ladattu, paina **"Connect Bluetooth Device"** -painiketta.

4. **Tarkista konsoli ja selain:**
* **Mitä lokiviestejä konsoliin ilmestyy painikkeen painamisen jälkeen?**
* **Näyttääkö selaimesi Bluetooth-laitteen valintaikkunaa?**
* **Jos valintaikkuna ilmestyy, näetkö siinä mikrokontrollerisi?**
* **Jos valintaikkuna ilmestyy, valitsetko laitteen ja painatko "Pair" (Parita) tai "Connect" (Yhdistä)?**

**Yleisiä syitä, miksi Bluetooth-painike ei tee mitään tai yhteys epäonnistuu:**

* **Selain ei tue Web Bluetoothia:** Vain Chrome, Edge, Opera ja muut Chromium-pohjaiset selaimet tukevat sitä. Firefox ja Safari eivät tue Web Bluetooth API:a.
* **Sivua ei tarjoilla HTTPS:n kautta:** Web Bluetooth edellyttää turvallista yhteyttä (HTTPS). Jos käytät `http://` -osoitetta, se ei toimi. P5.js-editori tarjoilee sketchit automaattisesti HTTPS:n kautta, joten tämän ei pitäisi olla ongelma tässä ympäristössä.
* **Mikrokontrolleri ei mainosta oikeaa service UUID:tä:** Tämä on **todennäköisin syy**. `navigator.bluetooth.requestDevice({ filters: [{ services: [bluetoothServiceUUID] }] })` etsii vain laitteita, jotka mainostavat täsmälleen tätä palvelun UUID:tä. Jos mikrokontrollerisi BLE-koodi ei mainosta `19B10000-E8F2-537E-4F6C-D104768A1214`-tunnusta, selain ei löydä sitä.
* **Käyttäjä peruuttaa laitevalintaikkunan:** Jos et valitse laitetta tai painat "Cancel" (Peruuta), `requestDevice()`-kutsu heittää virheen, joka käsitellään `catch`-lohossa.
* **GATT-palvelin ei ole käytettävissä tai palvelu/ominaisuus puuttuu:** Vaikka laite löytyisi ja yhteys muodostuisi, jos mikrokontrolleri ei tarjoa määriteltyä palvelua (`bluetoothServiceUUID`) tai ominaisuutta (`bluetoothCharacteristicUUID`), myöhemmät `getPrimaryService()`- tai `getCharacteristic()`-kutsut epäonnistuvat.

**Kun saat uusia lokiviestejä konsoliin, kopioi ne ja kerro minulle, niin voimme jatkaa vianmääritystä.**
👤 You
Bluetooth yhteys antaa tämän virheilmoituksen: ​
1768307716594
TypeError: Invalid Service name: '19B10000-E8F2-537E-4F6C-D104768A1214'. It must be a valid UUID alias (e.g. 0x1234), UUID (lowercase hex characters e.g. '00001234-0000-1000-8000-00805f9b34fb'), or recognized standard name from https://www.bluetooth.com/specifications/gatt/services e.g. 'alert_notification'.
🤖 AI
Ahaa! Tämä on erittäin selkeä virheilmoitus ja auttaa meitä paljon.

Virhe sanoo:
`TypeError: Invalid Service name: '19B10000-E8F2-537E-4F6C-D104768A1214'. It must be a valid UUID alias (e.g. 0x1234), UUID (lowercase hex characters e.g. '00001234-0000-1000-8000-00805f9b34fb'), or recognized standard name from https://www.bluetooth.com/specifications/gatt/services e.g. 'alert_notification'.`

Tämä tarkoittaa, että selain (Web Bluetooth API) odottaa UUID-tunnusten olevan **pienillä kirjaimilla** (`lowercase hex characters`). Sinun UUID-tunnuksesi `19B10000-E8F2-537E-4F6C-D104768A1214` sisältää isoja kirjaimia (B, E, F, C, D, A).

Meidän on muutettava molemmat UUID-vakiot pieniksi kirjaimiksi koodissa.

---

### `sketch.js` (Updated: UUIDs to lowercase)

```javascript
// Remixed from: Anonymous/teachable-machine-p5-js-device-web-serial
// Original: https://p5js.ai/gallery/s/teachable-machine-p5-js-device-web-serial
// ----------------------------------------

// Remixed from: Larux75/teachable-machine-web-serial2
// Original: https://p5js.ai/gallery/u/Larux75-p5js/teachable-machine-web-serial2
// ----------------------------------------

//====================================================
// This code was written on the p5js.ai IDE
// Using the xelsed-alpha6-flash tuned model
// Visit the site and support Dyslexic Research
//===================================================

let classifier; // The ml5.js image classifier
let video; // The webcam video capture
let modelURLInput; // Input field for Teachable Machine model URL
let modelURLLoadButton; // Button to load the model from the URL
let modelURL = ''; // Your Teachable Machine model URL (initially empty)
let currentClassification = { label: 'Please enter model URL and click OK', confidence: 0 }; // Stores the latest classification result

// Web Serial variables
let port; // The connected serial port
let reader; // For reading data from the port
let writer; // For writing data from the port
let serialConnectButton; // Button to connect/disconnect serial
let serialStatusP; // Paragraph to display serial connection status
let receivedSerialDataP; // Paragraph to display data received from serial (renamed for clarity)

// Web Bluetooth variables
let bluetoothDevice; // The connected Bluetooth device
let bluetoothService; // The specific BLE service
let bluetoothCharacteristic; // The characteristic for writing data
let bluetoothConnectButton; // Button to connect/disconnect Bluetooth
let bluetoothStatusP; // Paragraph to display Bluetooth connection status
// CUSTOM UUIDs: MUST BE LOWERCASE HEX CHARACTERS!
const bluetoothServiceUUID = '19b10000-e8f2-537e-4f6c-d104768a1214'; // CUSTOM UUID: Must match your microcontroller's BLE service UUID
const bluetoothCharacteristicUUID = '19b10001-e8f2-537e-4f6c-d104768a1214'; // CUSTOM UUID: Must match your microcontroller's BLE characteristic UUID

// Common timing and state variables
let classificationSendInterval = 3000; // Send classification every 3 seconds
let lastClassificationSendTime = 0;
let lastSentLabel = ''; // To avoid sending the same label repeatedly (stores the string label)

// Application state: 'input' or 'classifier'
let appState = 'input';

// FIX: Move videomargin declaration to the top so it's defined before setup() uses it.
let videoMargin = 20; // Margin around the video preview and UI elements

function preload() {
console.log('preload() started.');
// We won't load the model here anymore.
// Model loading will happen when the user enters a URL and clicks OK.
console.log('preload() finished.');
}

function setup() {
console.log('setup() started.');
createCanvas(windowWidth, windowHeight); // Create a responsive canvas

// Create the video capture from the webcam
video = createCapture(VIDEO, videoReady); // Add a callback for video readiness
video.hide(); // Hide the HTML video element as we'll draw it on the canvas
console.log('Video capture object created in setup().');
// Initialize custom ready flag to false
video.ready = false;

// Initial display message asking for model URL
textSize(32);
textAlign(CENTER, CENTER);
fill(0);
text('Please enter your Teachable Machine model URL below', width / 2, height / 2);

// --- Model URL Input Setup ---
modelURLInput = createInput();
modelURLInput.attribute('placeholder', 'Enter Teachable Machine model URL here');
modelURLInput.style('font-size', '16px');
modelURLInput.style('padding', '8px');
// FIX: videomargin is now defined, so this line will work.
modelURLInput.style('width', 'calc(100% - 100px - 2 * ' + videoMargin + 'px)'); // Adjust width
modelURLInput.style('border', '1px solid #ccc');
modelURLInput.style('border-radius', '4px');

modelURLLoadButton = createButton('OK');
modelURLLoadButton.mousePressed(loadModelFromInput);
modelURLLoadButton.style('font-size', '16px');
modelURLLoadButton.style('padding', '10px 15px');
modelURLLoadButton.style('background-color', '#007BFF');
modelURLLoadButton.style('color', 'white');
modelURLLoadButton.style('border', 'none');
modelURLLoadButton.style('border-radius', '5px');
modelURLLoadButton.style('cursor', 'pointer');

// --- Web Serial Setup ---
// Create p5 DOM elements for serial status and received data
serialStatusP = createP('Web Serial: Not connected.');
serialStatusP.style('font-size', '18px');
serialStatusP.style('color', '#333');
serialStatusP.style('font-family', 'sans-serif'); // Add a standard font for consistency

receivedSerialDataP = createP('Received: '); // Renamed for clarity
receivedSerialDataP.style('font-size', '18px');
receivedSerialDataP.style('color', '#333');
receivedSerialDataP.style('font-family', 'sans-serif'); // Add a standard font for consistency

// Check if Web Serial API is supported
if ('serial' in navigator) {
// Create the "Connect Serial Port" button
serialConnectButton = createButton('Connect Serial Port');
serialConnectButton.mousePressed(connectSerial); // Call connectSerial() when button is pressed
serialConnectButton.style('font-size', '16px');
serialConnectButton.style('padding', '10px 15px');
serialConnectButton.style('background-color', '#4CAF50');
serialConnectButton.style('color', 'white');
serialConnectButton.style('border', 'none');
serialConnectButton.style('border-radius', '5px');
serialConnectButton.style('cursor', 'pointer');

// Add event listener for port disconnection (e.g., USB cable unplugged)
navigator.serial.addEventListener('disconnect', portDisconnected);

} else {
// Display an error message if Web Serial is not supported
serialStatusP.html('Web Serial API not supported in this browser. Try Chrome/Edge.');
serialStatusP.style('color', 'red');
}

// --- Web Bluetooth Setup ---
bluetoothStatusP = createP('Web Bluetooth: Not connected.');
bluetoothStatusP.style('font-size', '18px');
bluetoothStatusP.style('color', '#333');
bluetoothStatusP.style('font-family', 'sans-serif');

if ('bluetooth' in navigator) {
console.log('Web Bluetooth API is supported in this browser.');
bluetoothConnectButton = createButton('Connect Bluetooth Device');
bluetoothConnectButton.mousePressed(connectBluetooth);
bluetoothConnectButton.style('font-size', '16px');
bluetoothConnectButton.style('padding', '10px 15px');
bluetoothConnectButton.style('background-color', '#6A5ACD'); // A nice purple
bluetoothConnectButton.style('color', 'white');
bluetoothConnectButton.style('border', 'none');
bluetoothConnectButton.style('border-radius', '5px');
bluetoothConnectButton.style('cursor', 'pointer');
} else {
console.warn('Web Bluetooth API is NOT supported in this browser.');
bluetoothStatusP.html('Web Bluetooth API not supported in this browser. Try Chrome/Edge.');
bluetoothStatusP.style('color', 'red');
}

// Set initial visibility for UI elements
setClassifierUIVisibility(false); // Hide classifier elements initially

// Call windowResized once to set initial positions correctly
windowResized();
console.log('setup() finished. appState:', appState);
}

function videoReady() {
console.log('Video capture is ready! (videoReady() callback executed)'); // Critical log
video.ready = true; // Custom flag to indicate video is ready
console.log('Video element readyState:', video.elt.readyState); // Check HTML video element state
console.log('Video dimensions: ', video.width, 'x', video.height); // Log actual video dimensions

// If model is already loaded and we're in classifier state, start classifying
if (appState === 'classifier' && classifier && classifier.ready) {
console.log('Video ready and classifier ready. Starting classification from videoReady().');
classifyVideo();
}
}

/**
* Hides/shows UI elements belonging to the classifier view.
* @param {boolean} isVisible - True to show, false to hide.
*/
function setClassifierUIVisibility(isVisible) {
const displayStyle = isVisible ? 'block' : 'none';

if (serialStatusP) serialStatusP.style('display', displayStyle);
if (receivedSerialDataP) receivedSerialDataP.style('display', displayStyle);
if (serialConnectButton) serialConnectButton.style('display', displayStyle);

if (bluetoothStatusP) bluetoothStatusP.style('display', displayStyle);
if (bluetoothConnectButton) bluetoothConnectButton.style('display', displayStyle);

// Input view elements are visible when classifier elements are hidden
if (modelURLInput) modelURLInput.style('display', isVisible ? 'none' : 'block');
if (modelURLLoadButton) modelURLLoadButton.style('display', isVisible ? 'none' : 'block');
console.log('Classifier UI visibility set to:', isVisible);
}


function loadModelFromInput() {
console.log('loadModelFromInput() called.');
const newModelURL = modelURLInput.value().trim();
if (newModelURL) {
modelURL = newModelURL;
console.log('Attempting to load model from:', modelURL); // Log attempt
currentClassification.label = 'Loading model...';
classifier = ml5.imageClassifier(modelURL, modelLoaded); // Re-initialize classifier
classifier.ready = false; // Reset ready flag
modelURLInput.attribute('disabled', ''); // Disable input during loading
modelURLLoadButton.attribute('disabled', ''); // Disable button during loading
} else {
alert('Please enter a valid Teachable Machine model URL.');
console.warn('No model URL entered.');
}
}

// Modified modelLoaded function to handle errors
function modelLoaded(error) {
console.log('modelLoaded() callback received.');
if (error) {
console.error('Error loading model:', error); // Log the error
currentClassification.label = 'Model loading failed: ' + error.message;
// Re-enable input and button so user can try again
modelURLInput.removeAttribute('disabled');
modelURLLoadButton.removeAttribute('disabled');
// Keep appState as 'input'
console.log('Model loading failed, staying in input state.');
return; // Stop here if loading failed
}
console.log('Model Loaded successfully!'); // Confirm model is loaded
classifier.ready = true; // Set a custom flag to indicate the model is ready
currentClassification.label = 'Model Loaded. Starting classification...';
// Check if video is also ready before starting classification
if (video && video.ready) {
console.log('Classifier ready and video ready. Starting classification from modelLoaded().');
classifyVideo(); // Start classifying once the model is loaded AND video is ready
} else {
console.warn('Model loaded, but video not yet ready. Will start classification when video is ready.');
}
modelURLInput.removeAttribute('disabled'); // Enable input after loading
modelURLLoadButton.removeAttribute('disabled'); // Enable button after loading
appState = 'classifier'; // Switch to classifier view
setClassifierUIVisibility(true); // Show classifier elements
windowResized(); // Re-position elements for the classifier view
console.log('modelLoaded() finished. appState:', appState);
}

function classifyVideo() {
// Ensure the video is loaded and the model is ready before attempting to classify
if (video && video.ready && classifier && classifier.ready) {
console.log('Classifying video frame.');
// Classify the current video frame and call gotResult when done
// Reference: https://learn.ml5js.org/docs/#/reference/image-classifier?id=classify
classifier.classify(video, gotResult);
} else {
// If not ready, try again after a short delay
// This is important if video or classifier isn't ready simultaneously
console.warn('classifyVideo() called, but video or classifier not ready. Retrying in 100ms.');
setTimeout(classifyVideo, 100);
}
}

function gotResult(error, results) {
// If there's an error during classification, log it
if (error) {
console.error('Error during classification:', error);
currentClassification.label = 'Error: ' + error.message;
currentClassification.confidence = 0;
// Don't stop classifying, try again
classifyVideo();
return;
}

// If no error, results will be an array of objects
// Each object has a 'label' and 'confidence'
// Sort results by confidence in descending order to get the top prediction
results.sort((a, b) => b.confidence - a.confidence);
const topResult = results[0];

// Update the current classification result
currentClassification.label = topResult.label;
currentClassification.confidence = topResult.confidence;

// Keep classifying the video feed recursively
classifyVideo();
}

function draw() {
background(220); // Clear the canvas each frame

if (appState === 'input') {
// Display input view message
textSize(32);
textAlign(CENTER, CENTER);
fill(0);
text('Please enter your Teachable Machine model URL below', width / 2, height / 2 - 50); // Slightly above input field
} else if (appState === 'classifier') {
if (video && video.ready && classifier && classifier.ready) { // Check video.ready here too
console.log('Attempting to draw video. Video ready:', video.ready, 'Classifier ready:', classifier.ready); // Critical log
// Calculate preview size while maintaining aspect ratio
// Let's make the preview height about half the canvas height, minus margins.
let previewHeight = height / 2 - 2 * videoMargin;
let previewWidth = (previewHeight / video.height) * video.width;

// Ensure previewWidth doesn't exceed available space if canvas is very narrow
// (e.g., if canvas width is less than previewWidth + videoMargin * 2)
let availableWidthForVideo = width - 2 * videoMargin;
if (previewWidth > availableWidthForVideo) {
previewWidth = availableWidthForVideo;
previewHeight = (previewWidth / video.width) * video.height;
}

// Draw the video feed at the top-left with margin
image(video, videoMargin, videoMargin, previewWidth, previewHeight);

// Classification text and background rectangle to the right of the video preview.
let textRectX = videoMargin + previewWidth + videoMargin;
let textRectWidth = width - textRectX - videoMargin;
let textRectHeight = previewHeight; // Make it the same height as the video preview
let textRectY = videoMargin; // Align with the top of the video

fill(255, 200); // White with 200 alpha
noStroke();
rect(textRectX, textRectY, textRectWidth, textRectHeight);

// Display the current classification result
fill(0);
textSize(24);
textAlign(LEFT, CENTER);
// Center text vertically within its rectangle
let textYOffset = textRectY + textRectHeight / 2;
text(`Label: ${currentClassification.label}`, textRectX + 10, textYOffset - 20); // Adjust y for two lines
// Use nf() to format confidence as a percentage with 2 decimal places
text(`Confidence: ${nf(currentClassification.confidence * 100, 0, 2)}%`, textRectX + 10, textYOffset + 20);

// --- Determine number to send based on classification ---
let numberToSend = -1; // Default to -1 for unknown labels
const currentLabel = currentClassification.label.toLowerCase(); // Use lowercase for comparison
switch (currentLabel) {
case 'ihminen':
numberToSend = 1;
break;
case 'koira':
numberToSend = 2;
break;
case 'kissa': // Assuming 'kissa' for number 3 based on common classification tasks
numberToSend = 3;
break;
default:
numberToSend = -1; // For any other label, or if label is empty/null
break;
}

// --- Send classification data via Web Serial and Web Bluetooth ---
const shouldSend = millis() - lastClassificationSendTime > classificationSendInterval && currentLabel !== lastSentLabel.toLowerCase();

if (shouldSend) {
if (numberToSend !== -1) {
// Send via Web Serial
if (port && writer) {
const serialDataToSend = numberToSend + '\n'; // Add newline for microcontroller parsing
writeSerial(serialDataToSend);
}

// Send via Web Bluetooth
if (bluetoothCharacteristic) {
// Web Bluetooth typically sends bytes directly, no newline needed
writeBluetooth(numberToSend);
}

// Update last sent info if any data was attempted to be sent
if (port || bluetoothCharacteristic) {
lastSentLabel = currentLabel;
lastClassificationSendTime = millis();
}
} else {
// Only log if no valid number was mapped and we were supposed to send
if (port || bluetoothCharacteristic) {
console.warn(`Unrecognized or default label "${currentLabel}". Not sending data.`);
}
}
}
} else if (!classifier.ready) {
// Display loading message if the model is not yet ready (should only happen briefly after OK)
textSize(32);
textAlign(CENTER, CENTER);
fill(0);
text('Loading model...', width / 2, height / 2);
}
}
}

function windowResized() {
resizeCanvas(windowWidth, windowHeight); // Adjust canvas size on window resize
console.log('windowResized() called. appState:', appState);

if (appState === 'input') {
// When in input state, position input/button centrally below the initial text
let inputWidth = min(width - 2 * videoMargin, 400); // Max 400px wide, or less if canvas is narrow
if (modelURLInput) modelURLInput.style('width', `${inputWidth - modelURLLoadButton.width - 20}px`); // Adjust for button width and spacing
if (modelURLInput) modelURLInput.position(width / 2 - inputWidth / 2, height / 2 + 50); // Below initial text
if (modelURLLoadButton) modelURLLoadButton.position(width / 2 + inputWidth / 2 - modelURLLoadButton.width, height / 2 + 50); // To the right of input
} else { // In classifier state, use the standard layout
// Recalculate video preview size based on new window dimensions
let previewHeight = height / 2 - 2 * videoMargin;
let previewWidth = (previewHeight / video.height) * video.width;
let availableWidthForVideo = width - 2 * videoMargin;
if (previewWidth > availableWidthForVideo) {
previewWidth = availableWidthForVideo;
previewHeight = (previewWidth / video.width) * video.height;
}

// Calculate the new starting y-position for the DOM elements below the video
// It's the bottom of the video (videoMargin + previewHeight) + an additional videoMargin for spacing
let domElementsStartY = videoMargin + previewHeight + videoMargin;

// Re-position model URL input and button (now below video, not centered)
if (modelURLInput) modelURLInput.style('width', 'calc(100% - 100px - 2 * ' + videoMargin + 'px)'); // Reset width
if (modelURLInput) modelURLInput.position(videoMargin, domElementsStartY);
if (modelURLLoadButton) modelURLLoadButton.position(videoMargin + modelURLInput.width + 10, domElementsStartY); // 10px spacing

// Re-position other p5 DOM elements
// Adjust starting Y for other elements based on the model URL input/button height
let otherDomElementsStartY = domElementsStartY + 40; // Assuming button height is around 30-35px + 10px spacing

// Serial elements
if (serialStatusP) serialStatusP.position(videoMargin, otherDomElementsStartY); // Adjusted y
if (receivedSerialDataP) receivedSerialDataP.position(videoMargin, otherDomElementsStartY + 30); // Adjusted y
if (serialConnectButton) serialConnectButton.position(videoMargin, otherDomElementsStartY + 70); // Adjusted y

// Bluetooth elements (below Serial elements)
let bluetoothDomElementsStartY = otherDomElementsStartY + 110; // 70 + buttonHeight (approx 40)
if (bluetoothStatusP) bluetoothStatusP.position(videoMargin, bluetoothDomElementsStartY);
if (bluetoothConnectButton) bluetoothConnectButton.position(videoMargin, bluetoothDomElementsStartY + 30);
}
}

// --- Web Serial Functions ---

/**
* Connects to a serial port selected by the user.
*/
async function connectSerial() {
try {
console.log('connectSerial() called.');
// Request a serial port from the user
port = await navigator.serial.requestPort();
serialStatusP.html('Web Serial: Connecting...');
console.log('Serial port requested and granted.');

// Open the port with a specified baud rate
// IMPORTANT: This baud rate (9600) must match the one used by your microcontroller (e.g., Arduino Serial.begin(9600);)
await port.open({ baudRate: 9600 });
serialStatusP.html('Web Serial: Connected!');
serialConnectButton.html('Disconnect Serial Port'); // Change button text
serialConnectButton.mousePressed(disconnectSerial); // Change button action to disconnect
console.log('Serial port opened with baudRate 9600.');

// Create TextDecoderStream and TextEncoderStream for easy text handling
const textDecoder = new TextDecoderStream();
const textEncoder = new TextEncoderStream();
reader = textDecoder.readable.getReader(); // Reader for incoming text data
writer = textEncoder.writable.getWriter(); // Writer for outgoing text data
console.log('TextDecoderStream and TextEncoderStream created.');

// Pipe the port's readable stream through the decoder
port.readable.pipeTo(textDecoder.writable);
// TÄMÄ ON OIKEIN: Pipe the encoder's readable stream (containing encoded bytes) to the port's writable stream
textEncoder.readable.pipeTo(port.writable); // THIS IS THE CORRECTION
console.log('Serial streams piped.');

readSerial(); // Start continuously reading data from the port
} catch (error) {
console.error('Error connecting to serial port:', error);
serialStatusP.html('Web Serial: Connection failed. ' + error.message);
}
}

/**
* Disconnects from the currently connected serial port.
*/
async function disconnectSerial() {
try {
console.log('disconnectSerial() called.');
if (reader) await reader.cancel(); // Cancel any pending read operations
if (writer) await writer.close(); // Close the writer stream
if (port) await port.close(); // Close the serial port

portDisconnected(); // Reset variables and UI
console.log('Serial port successfully disconnected.');
} catch (error) {
console.error('Error disconnecting serial port:', error);
serialStatusP.html('Web Serial: Disconnection failed. ' + error.message);
}
}

/**
* Continuously reads data from the serial port.
*/
async function readSerial() {
console.log('readSerial() started.');
while (port && port.readable) {
try {
const { value, done } = await reader.read();
if (done) {
console.log('Reader closed.');
break; // Exit loop if reader is closed
}

// Check if the received value is "1" (and trim whitespace for robustness)
if (value.trim() === '1') {
receivedSerialDataP.html('Received: remote device connected');
// You might want to update serialStatusP too, but currentStatusP is already "Connected!"
} else {
receivedSerialDataP.html(`Received: ${value}`); // Display received data
}

// For more complex data, you might parse 'value' here (e.g., JSON.parse(value))
} catch (error) {
console.error('Error reading from serial port:', error);
serialStatusP.html('Web Serial: Read error. ' + error.message);
break; // Exit loop on error
}
}
portDisconnected(); // Assume disconnected if loop exits (e.g., due to error or port closure)
console.log('readSerial() stopped.');
}

/**
* Writes data to the serial port.
* @param {string} data - The string data to send.
*/
async function writeSerial(data) {
if (port && writer) {
try {
await writer.write(data);
console.log('Sent to Serial:', data.trim()); // Log sent data (the number) without the trailing newline
} catch (error) {
console.error('Error writing to serial port:', error);
serialStatusP.html('Web Serial: Write error. ' + error.message);
portDisconnected(); // Assume disconnected on write error
}
} else {
console.warn('Cannot write: Serial port not connected.');
serialStatusP.html('Web Serial: Cannot write, not connected.');
}
}

/**
* Resets serial variables and UI when the port disconnects.
*/
function portDisconnected() {
console.log('Serial port disconnected.');
port = undefined;
reader = undefined;
writer = undefined;
serialStatusP.html('Web Serial: Disconnected.');
receivedSerialDataP.html('Received: ');
if (serialConnectButton) {
serialConnectButton.html('Connect Serial Port');
serialConnectButton.mousePressed(connectSerial); // Change button action back to connect
}
// Reset lastSentLabel and timer to allow re-sending on reconnect
lastSentLabel = '';
lastClassificationSendTime = 0;
}


// --- Web Bluetooth Functions ---

/**
* Connects to a Bluetooth device with the specified service and characteristic.
*/
async function connectBluetooth() {
console.log('connectBluetooth() called from button press.'); // NEW LOG
bluetoothStatusP.html('Web Bluetooth: Requesting device...');
try {
// Request a Bluetooth device from the user with the specified service
console.log('Attempting to request Bluetooth device with service UUID:', bluetoothServiceUUID); // NEW LOG
bluetoothDevice = await navigator.bluetooth.requestDevice({
filters: [{ services: [bluetoothServiceUUID] }], // Filter for our custom service
optionalServices: [] // No optional services needed for this example
});
console.log('Bluetooth device requested and granted:', bluetoothDevice.name);

// Add event listener for GATT server disconnection
bluetoothDevice.addEventListener('gattserverdisconnected', bluetoothDisconnected);

// Connect to the GATT server
bluetoothStatusP.html('Web Bluetooth: Connecting to GATT server...');
console.log('Attempting to connect to GATT server.'); // NEW LOG
const server = await bluetoothDevice.gatt.connect();
console.log('Connected to GATT server.');

// Get the primary service
bluetoothStatusP.html('Web Bluetooth: Getting service...');
console.log('Attempting to get Bluetooth service with UUID:', bluetoothServiceUUID); // NEW LOG
bluetoothService = await server.getPrimaryService(bluetoothServiceUUID);
console.log('Got Bluetooth service:', bluetoothServiceUUID);

// Get the characteristic for writing
bluetoothStatusP.html('Web Bluetooth: Getting characteristic...');
console.log('Attempting to get Bluetooth characteristic with UUID:', bluetoothCharacteristicUUID); // NEW LOG
bluetoothCharacteristic = await bluetoothService.getCharacteristic(bluetoothCharacteristicUUID);
console.log('Got Bluetooth characteristic:', bluetoothCharacteristicUUID);

bluetoothStatusP.html(`Web Bluetooth: Connected to ${bluetoothDevice.name}!`);
bluetoothConnectButton.html('Disconnect Bluetooth Device'); // Change button text
bluetoothConnectButton.mousePressed(disconnectBluetooth); // Change button action
console.log('Bluetooth connection established successfully.');

} catch (error) {
console.error('Error connecting to Bluetooth device:', error);
bluetoothStatusP.html('Web Bluetooth: Connection failed. ' + error.message);
bluetoothDisconnected(); // Reset UI on failure
}
}

/**
* Disconnects from the currently connected Bluetooth device.
*/
async function disconnectBluetooth() {
try {
console.log('disconnectBluetooth() called.');
if (bluetoothDevice && bluetoothDevice.gatt.connected) {
bluetoothStatusP.html('Web Bluetooth: Disconnecting...');
bluetoothDevice.gatt.disconnect();
console.log('Bluetooth GATT server disconnected.');
}
bluetoothDisconnected(); // Reset variables and UI
} catch (error) {
console.error('Error disconnecting Bluetooth device:', error);
bluetoothStatusP.html('Web Bluetooth: Disconnection failed. ' + error.message);
}
}

/**
* Writes a single number (byte) to the Bluetooth characteristic.
* @param {number} number - The number (1, 2, or 3) to send.
*/
async function writeBluetooth(number) {
if (bluetoothCharacteristic) {
try {
// Create a DataView to send a single byte
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/DataView
const dataView = new DataView(new ArrayBuffer(1));
dataView.setUint8(0, number); // Write the number as an 8-bit unsigned integer

await bluetoothCharacteristic.writeValue(dataView);
console.log('Sent to Bluetooth:', number);
} catch (error) {
console.error('Error writing to Bluetooth characteristic:', error);
bluetoothStatusP.html('Web Bluetooth: Write error. ' + error.message);
bluetoothDisconnected(); // Assume disconnected on write error
}
} else {
console.warn('Cannot write: Bluetooth device not connected.');
bluetoothStatusP.html('Web Bluetooth: Cannot write, not connected.');
}
}

/**
* Resets Bluetooth variables and UI when the device disconnects (or fails to connect).
*/
function bluetoothDisconnected() {
console.log('Bluetooth device disconnected or connection failed.');
bluetoothDevice = undefined;
bluetoothService = undefined;
bluetoothCharacteristic = undefined;
bluetoothStatusP.html('Web Bluetooth: Disconnected.');
if (bluetoothConnectButton) {
bluetoothConnectButton.html('Connect Bluetooth Device');
bluetoothConnectButton.mousePressed(connectBluetooth); // Change button action back to connect
}
// Reset lastSentLabel and timer to allow re-sending on reconnect
lastSentLabel = '';
lastClassificationSendTime = 0;
}
```

---

### `index.html` (No Changes Needed)

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>p5.js Sketch</title>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.3/lib/p5.min.js"></script>
<script src="https://unpkg.com/ml5@0.12.2/dist/ml5.min.js"></script> <!-- Added ml5.js library -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="sketch.js"></script>
</body>
</html>
```

---

### `style.css` (No Changes Needed)

```css
body {
margin: 0;
padding: 0;
overflow: hidden;
}

canvas {
display: block;
}
```

---

**Mitä nyt tapahtuu:**

Kun olet päivittänyt `sketch.js`-tiedoston ja suorittanut koodin uudelleen, Web Bluetooth API:n pitäisi hyväksyä UUID-tunnukset ja antaa selaimen etsiä laitteita, jotka mainostavat tätä palvelua.

Kokeile painiketta uudelleen ja tarkista konsoli. Nyt sinun pitäisi nähdä joko:
1. **Laitteen valintaikkuna ilmestyy.** (Tämä olisi hyvä merkki!)
2. **Uusi virhe, joka viittaa seuraavaan ongelmaan.**

Jos laitteen valintaikkuna ilmestyy, mutta et näe mikrokontrolleriasi, se tarkoittaa, että mikrokontrollerisi ei mainosta kyseistä service UUID:tä, vaikka olisit sen koodissa määrittänytkin. Tarkista silloin mikrokontrollerisi BLE-koodi erittäin huolellisesti.
👤 You
Korjaa nämä
// CUSTOM UUIDs: Nämä ovat viralliset micro:bit UART Service UUID:t
const bluetoothServiceUUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
const bluetoothCharacteristicUUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e'; // RX Characteristic (Write)

Lisää myös tämä:

Muuta lähetettävä numero tekstiksi ja lisää rivinvaihto
🤖 AI
Aivan, nyt käytämme micro:bitin virallisia UART-palvelun UUID-tunnuksia ja lähetämme tiedon tekstinä rivinvaihdon kera, mikä on yleinen tapa sarjaliikenteessä.

Tässä ovat korjaukset `sketch.js`-tiedostoon:

---

### `sketch.js` (Updated: micro:bit UART UUIDs and string + newline for Bluetooth)

```javascript
// Remixed from: Anonymous/teachable-machine-p5-js-device-web-serial
// Original: https://p5js.ai/gallery/s/teachable-machine-p5-js-device-web-serial
// ----------------------------------------

// Remixed from: Larux75/teachable-machine-web-serial2
// Original: https://p5js.ai/gallery/u/Larux75-p5js/teachable-machine-web-serial2
// ----------------------------------------

//====================================================
// This code was written on the p5js.ai IDE
// Using the xelsed-alpha6-flash tuned model
// Visit the site and support Dyslexic Research
//===================================================

let classifier; // The ml5.js image classifier
let video; // The webcam video capture
let modelURLInput; // Input field for Teachable Machine model URL
let modelURLLoadButton; // Button to load the model from the URL
let modelURL = ''; // Your Teachable Machine model URL (initially empty)
let currentClassification = { label: 'Please enter model URL and click OK', confidence: 0 }; // Stores the latest classification result

// Web Serial variables
let port; // The connected serial port
let reader; // For reading data from the port
let writer; // For writing data from the port
let serialConnectButton; // Button to connect/disconnect serial
let serialStatusP; // Paragraph to display serial connection status
let receivedSerialDataP; // Paragraph to display data received from serial (renamed for clarity)

// Web Bluetooth variables
let bluetoothDevice; // The connected Bluetooth device
let bluetoothService; // The specific BLE service
let bluetoothCharacteristic; // The characteristic for writing data
let bluetoothConnectButton; // Button to connect/disconnect Bluetooth
let bluetoothStatusP; // Paragraph to display Bluetooth connection status
// CUSTOM UUIDs: Nämä ovat viralliset micro:bit UART Service UUID:t
const bluetoothServiceUUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
const bluetoothCharacteristicUUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e'; // RX Characteristic (Write)

// Common timing and state variables
let classificationSendInterval = 3000; // Send classification every 3 seconds
let lastClassificationSendTime = 0;
let lastSentLabel = ''; // To avoid sending the same label repeatedly (stores the string label)

// Application state: 'input' or 'classifier'
let appState = 'input';

// FIX: Move videomargin declaration to the top so it's defined before setup() uses it.
let videoMargin = 20; // Margin around the video preview and UI elements

function preload() {
console.log('preload() started.');
// We won't load the model here anymore.
// Model loading will happen when the user enters a URL and clicks OK.
console.log('preload() finished.');
}

function setup() {
console.log('setup() started.');
createCanvas(windowWidth, windowHeight); // Create a responsive canvas

// Create the video capture from the webcam
video = createCapture(VIDEO, videoReady); // Add a callback for video readiness
video.hide(); // Hide the HTML video element as we'll draw it on the canvas
console.log('Video capture object created in setup().');
// Initialize custom ready flag to false
video.ready = false;

// Initial display message asking for model URL
textSize(32);
textAlign(CENTER, CENTER);
fill(0);
text('Please enter your Teachable Machine model URL below', width / 2, height / 2);

// --- Model URL Input Setup ---
modelURLInput = createInput();
modelURLInput.attribute('placeholder', 'Enter Teachable Machine model URL here');
modelURLInput.style('font-size', '16px');
modelURLInput.style('padding', '8px');
// FIX: videomargin is now defined, so this line will work.
modelURLInput.style('width', 'calc(100% - 100px - 2 * ' + videoMargin + 'px)'); // Adjust width
modelURLInput.style('border', '1px solid #ccc');
modelURLInput.style('border-radius', '4px');

modelURLLoadButton = createButton('OK');
modelURLLoadButton.mousePressed(loadModelFromInput);
modelURLLoadButton.style('font-size', '16px');
modelURLLoadButton.style('padding', '10px 15px');
modelURLLoadButton.style('background-color', '#007BFF');
modelURLLoadButton.style('color', 'white');
modelURLLoadButton.style('border', 'none');
modelURLLoadButton.style('border-radius', '5px');
modelURLLoadButton.style('cursor', 'pointer');

// --- Web Serial Setup ---
// Create p5 DOM elements for serial status and received data
serialStatusP = createP('Web Serial: Not connected.');
serialStatusP.style('font-size', '18px');
serialStatusP.style('color', '#333');
serialStatusP.style('font-family', 'sans-serif'); // Add a standard font for consistency

receivedSerialDataP = createP('Received: '); // Renamed for clarity
receivedSerialDataP.style('font-size', '18px');
receivedSerialDataP.style('color', '#333');
receivedSerialDataP.style('font-family', 'sans-serif'); // Add a standard font for consistency

// Check if Web Serial API is supported
if ('serial' in navigator) {
// Create the "Connect Serial Port" button
serialConnectButton = createButton('Connect Serial Port');
serialConnectButton.mousePressed(connectSerial); // Call connectSerial() when button is pressed
serialConnectButton.style('font-size', '16px');
serialConnectButton.style('padding', '10px 15px');
serialConnectButton.style('background-color', '#4CAF50');
serialConnectButton.style('color', 'white');
serialConnectButton.style('border', 'none');
serialConnectButton.style('border-radius', '5px');
serialConnectButton.style('cursor', 'pointer');

// Add event listener for port disconnection (e.g., USB cable unplugged)
navigator.serial.addEventListener('disconnect', portDisconnected);

} else {
// Display an error message if Web Serial is not supported
serialStatusP.html('Web Serial API not supported in this browser. Try Chrome/Edge.');
serialStatusP.style('color', 'red');
}

// --- Web Bluetooth Setup ---
bluetoothStatusP = createP('Web Bluetooth: Not connected.');
bluetoothStatusP.style('font-size', '18px');
bluetoothStatusP.style('color', '#333');
bluetoothStatusP.style('font-family', 'sans-serif');

if ('bluetooth' in navigator) {
console.log('Web Bluetooth API is supported in this browser.');
bluetoothConnectButton = createButton('Connect Bluetooth Device');
bluetoothConnectButton.mousePressed(connectBluetooth);
bluetoothConnectButton.style('font-size', '16px');
bluetoothConnectButton.style('padding', '10px 15px');
bluetoothConnectButton.style('background-color', '#6A5ACD'); // A nice purple
bluetoothConnectButton.style('color', 'white');
bluetoothConnectButton.style('border', 'none');
bluetoothConnectButton.style('border-radius', '5px');
bluetoothConnectButton.style('cursor', 'pointer');
} else {
console.warn('Web Bluetooth API is NOT supported in this browser.');
bluetoothStatusP.html('Web Bluetooth API not supported in this browser. Try Chrome/Edge.');
bluetoothStatusP.style('color', 'red');
}

// Set initial visibility for UI elements
setClassifierUIVisibility(false); // Hide classifier elements initially

// Call windowResized once to set initial positions correctly
windowResized();
console.log('setup() finished. appState:', appState);
}

function videoReady() {
console.log('Video capture is ready! (videoReady() callback executed)'); // Critical log
video.ready = true; // Custom flag to indicate video is ready
console.log('Video element readyState:', video.elt.readyState); // Check HTML video element state
console.log('Video dimensions: ', video.width, 'x', video.height); // Log actual video dimensions

// If model is already loaded and we're in classifier state, start classifying
if (appState === 'classifier' && classifier && classifier.ready) {
console.log('Video ready and classifier ready. Starting classification from videoReady().');
classifyVideo();
}
}

/**
* Hides/shows UI elements belonging to the classifier view.
* @param {boolean} isVisible - True to show, false to hide.
*/
function setClassifierUIVisibility(isVisible) {
const displayStyle = isVisible ? 'block' : 'none';

if (serialStatusP) serialStatusP.style('display', displayStyle);
if (receivedSerialDataP) receivedSerialDataP.style('display', displayStyle);
if (serialConnectButton) serialConnectButton.style('display', displayStyle);

if (bluetoothStatusP) bluetoothStatusP.style('display', displayStyle);
if (bluetoothConnectButton) bluetoothConnectButton.style('display', displayStyle);

// Input view elements are visible when classifier elements are hidden
if (modelURLInput) modelURLInput.style('display', isVisible ? 'none' : 'block');
if (modelURLLoadButton) modelURLLoadButton.style('display', isVisible ? 'none' : 'block');
console.log('Classifier UI visibility set to:', isVisible);
}


function loadModelFromInput() {
console.log('loadModelFromInput() called.');
const newModelURL = modelURLInput.value().trim();
if (newModelURL) {
modelURL = newModelURL;
console.log('Attempting to load model from:', modelURL); // Log attempt
currentClassification.label = 'Loading model...';
classifier = ml5.imageClassifier(modelURL, modelLoaded); // Re-initialize classifier
classifier.ready = false; // Reset ready flag
modelURLInput.attribute('disabled', ''); // Disable input during loading
modelURLLoadButton.attribute('disabled', ''); // Disable button during loading
} else {
alert('Please enter a valid Teachable Machine model URL.');
console.warn('No model URL entered.');
}
}

// Modified modelLoaded function to handle errors
function modelLoaded(error) {
console.log('modelLoaded() callback received.');
if (error) {
console.error('Error loading model:', error); // Log the error
currentClassification.label = 'Model loading failed: ' + error.message;
// Re-enable input and button so user can try again
modelURLInput.removeAttribute('disabled');
modelURLLoadButton.removeAttribute('disabled');
// Keep appState as 'input'
console.log('Model loading failed, staying in input state.');
return; // Stop here if loading failed
}
console.log('Model Loaded successfully!'); // Confirm model is loaded
classifier.ready = true; // Set a custom flag to indicate the model is ready
currentClassification.label = 'Model Loaded. Starting classification...';
// Check if video is also ready before starting classification
if (video && video.ready) {
console.log('Classifier ready and video ready. Starting classification from modelLoaded().');
classifyVideo(); // Start classifying once the model is loaded AND video is ready
} else {
console.warn('Model loaded, but video not yet ready. Will start classification when video is ready.');
}
modelURLInput.removeAttribute('disabled'); // Enable input after loading
modelURLLoadButton.removeAttribute('disabled'); // Enable button after loading
appState = 'classifier'; // Switch to classifier view
setClassifierUIVisibility(true); // Show classifier elements
windowResized(); // Re-position elements for the classifier view
console.log('modelLoaded() finished. appState:', appState);
}

function classifyVideo() {
// Ensure the video is loaded and the model is ready before attempting to classify
if (video && video.ready && classifier && classifier.ready) {
console.log('Classifying video frame.');
// Classify the current video frame and call gotResult when done
// Reference: https://learn.ml5js.org/docs/#/reference/image-classifier?id=classify
classifier.classify(video, gotResult);
} else {
// If not ready, try again after a short delay
// This is important if video or classifier isn't ready simultaneously
console.warn('classifyVideo() called, but video or classifier not ready. Retrying in 100ms.');
setTimeout(classifyVideo, 100);
}
}

function gotResult(error, results) {
// If there's an error during classification, log it
if (error) {
console.error('Error during classification:', error);
currentClassification.label = 'Error: ' + error.message;
currentClassification.confidence = 0;
// Don't stop classifying, try again
classifyVideo();
return;
}

// If no error, results will be an array of objects
// Each object has a 'label' and 'confidence'
// Sort results by confidence in descending order to get the top prediction
results.sort((a, b) => b.confidence - a.confidence);
const topResult = results[0];

// Update the current classification result
currentClassification.label = topResult.label;
currentClassification.confidence = topResult.confidence;

// Keep classifying the video feed recursively
classifyVideo();
}

function draw() {
background(220); // Clear the canvas each frame

if (appState === 'input') {
// Display input view message
textSize(32);
textAlign(CENTER, CENTER);
fill(0);
text('Please enter your Teachable Machine model URL below', width / 2, height / 2 - 50); // Slightly above input field
} else if (appState === 'classifier') {
if (video && video.ready && classifier && classifier.ready) { // Check video.ready here too
console.log('Attempting to draw video. Video ready:', video.ready, 'Classifier ready:', classifier.ready); // Critical log
// Calculate preview size while maintaining aspect ratio
// Let's make the preview height about half the canvas height, minus margins.
let previewHeight = height / 2 - 2 * videoMargin;
let previewWidth = (previewHeight / video.height) * video.width;

// Ensure previewWidth doesn't exceed available space if canvas is very narrow
// (e.g., if canvas width is less than previewWidth + videoMargin * 2)
let availableWidthForVideo = width - 2 * videoMargin;
if (previewWidth > availableWidthForVideo) {
previewWidth = availableWidthForVideo;
previewHeight = (previewWidth / video.width) * video.height;
}

// Draw the video feed at the top-left with margin
image(video, videoMargin, videoMargin, previewWidth, previewHeight);

// Classification text and background rectangle to the right of the video preview.
let textRectX = videoMargin + previewWidth + videoMargin;
let textRectWidth = width - textRectX - videoMargin;
let textRectHeight = previewHeight; // Make it the same height as the video preview
let textRectY = videoMargin; // Align with the top of the video

fill(255, 200); // White with 200 alpha
noStroke();
rect(textRectX, textRectY, textRectWidth, textRectHeight);

// Display the current classification result
fill(0);
textSize(24);
textAlign(LEFT, CENTER);
// Center text vertically within its rectangle
let textYOffset = textRectY + textRectHeight / 2;
text(`Label: ${currentClassification.label}`, textRectX + 10, textYOffset - 20); // Adjust y for two lines
// Use nf() to format confidence as a percentage with 2 decimal places
text(`Confidence: ${nf(currentClassification.confidence * 100, 0, 2)}%`, textRectX + 10, textYOffset + 20);

// --- Determine number to send based on classification ---
let numberToSend = -1; // Default to -1 for unknown labels
const currentLabel = currentClassification.label.toLowerCase(); // Use lowercase for comparison
switch (currentLabel) {
case 'ihminen':
numberToSend = 1;
break;
case 'koira':
numberToSend = 2;
break;
case 'kissa': // Assuming 'kissa' for number 3 based on common classification tasks
numberToSend = 3;
break;
default:
numberToSend = -1; // For any other label, or if label is empty/null
break;
}

// --- Send classification data via Web Serial and Web Bluetooth ---
const shouldSend = millis() - lastClassificationSendTime > classificationSendInterval && currentLabel !== lastSentLabel.toLowerCase();

if (shouldSend) {
if (numberToSend !== -1) {
// Send via Web Serial
if (port && writer) {
const serialDataToSend = numberToSend + '\n'; // Add newline for microcontroller parsing
writeSerial(serialDataToSend);
}

// Send via Web Bluetooth
if (bluetoothCharacteristic) {
// Web Bluetooth: Convert number to string and add newline for micro:bit UART
writeBluetooth(numberToSend);
}

// Update last sent info if any data was attempted to be sent
if (port || bluetoothCharacteristic) {
lastSentLabel = currentLabel;
lastClassificationSendTime = millis();
}
} else {
// Only log if no valid number was mapped and we were supposed to send
if (port || bluetoothCharacteristic) {
console.warn(`Unrecognized or default label "${currentLabel}". Not sending data.`);
}
}
}
} else if (!classifier.ready) {
// Display loading message if the model is not yet ready (should only happen briefly after OK)
textSize(32);
textAlign(CENTER, CENTER);
fill(0);
text('Loading model...', width / 2, height / 2);
}
}
}

function windowResized() {
resizeCanvas(windowWidth, windowHeight); // Adjust canvas size on window resize
console.log('windowResized() called. appState:', appState);

if (appState === 'input') {
// When in input state, position input/button centrally below the initial text
let inputWidth = min(width - 2 * videoMargin, 400); // Max 400px wide, or less if canvas is narrow
if (modelURLInput) modelURLInput.style('width', `${inputWidth - modelURLLoadButton.width - 20}px`); // Adjust for button width and spacing
if (modelURLInput) modelURLInput.position(width / 2 - inputWidth / 2, height / 2 + 50); // Below initial text
if (modelURLLoadButton) modelURLLoadButton.position(width / 2 + inputWidth / 2 - modelURLLoadButton.width, height / 2 + 50); // To the right of input
} else { // In classifier state, use the standard layout
// Recalculate video preview size based on new window dimensions
let previewHeight = height / 2 - 2 * videoMargin;
let previewWidth = (previewHeight / video.height) * video.width;
let availableWidthForVideo = width - 2 * videoMargin;
if (previewWidth > availableWidthForVideo) {
previewWidth = availableWidthForVideo;
previewHeight = (previewWidth / video.width) * video.height;
}

// Calculate the new starting y-position for the DOM elements below the video
// It's the bottom of the video (videoMargin + previewHeight) + an additional videoMargin for spacing
let domElementsStartY = videoMargin + previewHeight + videoMargin;

// Re-position model URL input and button (now below video, not centered)
if (modelURLInput) modelURLInput.style('width', 'calc(100% - 100px - 2 * ' + videoMargin + 'px)'); // Reset width
if (modelURLInput) modelURLInput.position(videoMargin, domElementsStartY);
if (modelURLLoadButton) modelURLLoadButton.position(videoMargin + modelURLInput.width + 10, domElementsStartY); // 10px spacing

// Re-position other p5 DOM elements
// Adjust starting Y for other elements based on the model URL input/button height
let otherDomElementsStartY = domElementsStartY + 40; // Assuming button height is around 30-35px + 10px spacing

// Serial elements
if (serialStatusP) serialStatusP.position(videoMargin, otherDomElementsStartY); // Adjusted y
if (receivedSerialDataP) receivedSerialDataP.position(videoMargin, otherDomElementsStartY + 30); // Adjusted y
if (serialConnectButton) serialConnectButton.position(videoMargin, otherDomElementsStartY + 70); // Adjusted y

// Bluetooth elements (below Serial elements)
let bluetoothDomElementsStartY = otherDomElementsStartY + 110; // 70 + buttonHeight (approx 40)
if (bluetoothStatusP) bluetoothStatusP.position(videoMargin, bluetoothDomElementsStartY);
if (bluetoothConnectButton) bluetoothConnectButton.position(videoMargin, bluetoothDomElementsStartY + 30);
}
}

// --- Web Serial Functions ---

/**
* Connects to a serial port selected by the user.
*/
async function connectSerial() {
try {
console.log('connectSerial() called.');
// Request a serial port from the user
port = await navigator.serial.requestPort();
serialStatusP.html('Web Serial: Connecting...');
console.log('Serial port requested and granted.');

// Open the port with a specified baud rate
// IMPORTANT: This baud rate (9600) must match the one used by your microcontroller (e.g., Arduino Serial.begin(9600);)
await port.open({ baudRate: 9600 });
serialStatusP.html('Web Serial: Connected!');
serialConnectButton.html('Disconnect Serial Port'); // Change button text
serialConnectButton.mousePressed(disconnectSerial); // Change button action to disconnect
console.log('Serial port opened with baudRate 9600.');

// Create TextDecoderStream and TextEncoderStream for easy text handling
const textDecoder = new TextDecoderStream();
const textEncoder = new TextEncoderStream();
reader = textDecoder.readable.getReader(); // Reader for incoming text data
writer = textEncoder.writable.getWriter(); // Writer for outgoing text data
console.log('TextDecoderStream and TextEncoderStream created.');

// Pipe the port's readable stream through the decoder
port.readable.pipeTo(textDecoder.writable);
// TÄMÄ ON OIKEIN: Pipe the encoder's readable stream (containing encoded bytes) to the port's writable stream
textEncoder.readable.pipeTo(port.writable); // THIS IS THE CORRECTION
console.log('Serial streams piped.');

readSerial(); // Start continuously reading data from the port
} catch (error) {
console.error('Error connecting to serial port:', error);
serialStatusP.html('Web Serial: Connection failed. ' + error.message);
}
}

/**
* Disconnects from the currently connected serial port.
*/
async function disconnectSerial() {
try {
console.log('disconnectSerial() called.');
if (reader) await reader.cancel(); // Cancel any pending read operations
if (writer) await writer.close(); // Close the writer stream
if (port) await port.close(); // Close the serial port

portDisconnected(); // Reset variables and UI
console.log('Serial port successfully disconnected.');
} catch (error) {
console.error('Error disconnecting serial port:', error);
serialStatusP.html('Web Serial: Disconnection failed. ' + error.message);
}
}

/**
* Continuously reads data from the serial port.
*/
async function readSerial() {
console.log('readSerial() started.');
while (port && port.readable) {
try {
const { value, done } = await reader.read();
if (done) {
console.log('Reader closed.');
break; // Exit loop if reader is closed
}

// Check if the received value is "1" (and trim whitespace for robustness)
if (value.trim() === '1') {
receivedSerialDataP.html('Received: remote device connected');
// You might want to update serialStatusP too, but currentStatusP is already "Connected!'
} else {
receivedSerialDataP.html(`Received: ${value}`); // Display received data
}

// For more complex data, you might parse 'value' here (e.g., JSON.parse(value))
} catch (error) {
console.error('Error reading from serial port:', error);
serialStatusP.html('Web Serial: Read error. ' + error.message);
break; // Exit loop on error
}
}
portDisconnected(); // Assume disconnected if loop exits (e.g., due to error or port closure)
console.log('readSerial() stopped.');
}

/**
* Writes data to the serial port.
* @param {string} data - The string data to send.
*/
async function writeSerial(data) {
if (port && writer) {
try {
await writer.write(data);
console.log('Sent to Serial:', data.trim()); // Log sent data (the number) without the trailing newline
} catch (error) {
console.error('Error writing to serial port:', error);
serialStatusP.html('Web Serial: Write error. ' + error.message);
portDisconnected(); // Assume disconnected on write error
}
} else {
console.warn('Cannot write: Serial port not connected.');
serialStatusP.html('Web Serial: Cannot write, not connected.');
}
}

/**
* Resets serial variables and UI when the port disconnects.
*/
function portDisconnected() {
console.log('Serial port disconnected.');
port = undefined;
reader = undefined;
writer = undefined;
serialStatusP.html('Web Serial: Disconnected.');
receivedSerialDataP.html('Received: ');
if (serialConnectButton) {
serialConnectButton.html('Connect Serial Port');
serialConnectButton.mousePressed(connectSerial); // Change button action back to connect
}
// Reset lastSentLabel and timer to allow re-sending on reconnect
lastSentLabel = '';
lastClassificationSendTime = 0;
}


// --- Web Bluetooth Functions ---

/**
* Connects to a Bluetooth device with the specified service and characteristic.
*/
async function connectBluetooth() {
console.log('connectBluetooth() called from button press.'); // NEW LOG
bluetoothStatusP.html('Web Bluetooth: Requesting device...');
try {
// Request a Bluetooth device from the user with the specified service
console.log('Attempting to request Bluetooth device with service UUID:', bluetoothServiceUUID); // NEW LOG
bluetoothDevice = await navigator.bluetooth.requestDevice({
filters: [{ services: [bluetoothServiceUUID] }], // Filter for our custom service
optionalServices: [] // No optional services needed for this example
});
console.log('Bluetooth device requested and granted:', bluetoothDevice.name);

// Add event listener for GATT server disconnection
bluetoothDevice.addEventListener('gattserverdisconnected', bluetoothDisconnected);

// Connect to the GATT server
bluetoothStatusP.html('Web Bluetooth: Connecting to GATT server...');
console.log('Attempting to connect to GATT server.'); // NEW LOG
const server = await bluetoothDevice.gatt.connect();
console.log('Connected to GATT server.');

// Get the primary service
bluetoothStatusP.html('Web Bluetooth: Getting service...');
console.log('Attempting to get Bluetooth service with UUID:', bluetoothServiceUUID); // NEW LOG
bluetoothService = await server.getPrimaryService(bluetoothServiceUUID);
console.log('Got Bluetooth service:', bluetoothServiceUUID);

// Get the characteristic for writing
bluetoothStatusP.html('Web Bluetooth: Getting characteristic...');
console.log('Attempting to get Bluetooth characteristic with UUID:', bluetoothCharacteristicUUID); // NEW LOG
bluetoothCharacteristic = await bluetoothService.getCharacteristic(bluetoothCharacteristicUUID);
console.log('Got Bluetooth characteristic:', bluetoothCharacteristicUUID);

bluetoothStatusP.html(`Web Bluetooth: Connected to ${bluetoothDevice.name}!`);
bluetoothConnectButton.html('Disconnect Bluetooth Device'); // Change button text
bluetoothConnectButton.mousePressed(disconnectBluetooth); // Change button action
console.log('Bluetooth connection established successfully.');

} catch (error) {
console.error('Error connecting to Bluetooth device:', error);
bluetoothStatusP.html('Web Bluetooth: Connection failed. ' + error.message);
bluetoothDisconnected(); // Reset UI on failure
}
}

/**
* Disconnects from the currently connected Bluetooth device.
*/
async function disconnectBluetooth() {
try {
console.log('disconnectBluetooth() called.');
if (bluetoothDevice && bluetoothDevice.gatt.connected) {
bluetoothStatusP.html('Web Bluetooth: Disconnecting...');
bluetoothDevice.gatt.disconnect();
console.log('Bluetooth GATT server disconnected.');
}
bluetoothDisconnected(); // Reset variables and UI
} catch (error) {
console.error('Error disconnecting Bluetooth device:', error);
bluetoothStatusP.html('Web Bluetooth: Disconnection failed. ' + error.message);
}
}

/**
* Writes a single number (byte) to the Bluetooth characteristic.
* @param {number} number - The number (1, 2, or 3) to send.
*/
async function writeBluetooth(number) {
if (bluetoothCharacteristic) {
try {
// Convert number to string and add newline character
const stringToSend = String(number) + '\n';
const encoder = new TextEncoder(); // Create a TextEncoder to convert string to bytes
const encodedData = encoder.encode(stringToSend); // Encode the string to a Uint8Array

await bluetoothCharacteristic.writeValue(encodedData); // Write the byte array to the characteristic
console.log('Sent to Bluetooth:', stringToSend.trim()); // Log sent data without the trailing newline
} catch (error) {
console.error('Error writing to Bluetooth characteristic:', error);
bluetoothStatusP.html('Web Bluetooth: Write error. ' + error.message);
bluetoothDisconnected(); // Assume disconnected on write error
}
} else {
console.warn('Cannot write: Bluetooth device not connected.');
bluetoothStatusP.html('Web Bluetooth: Cannot write, not connected.');
}
}

/**
* Resets Bluetooth variables and UI when the device disconnects (or fails to connect).
*/
function bluetoothDisconnected() {
console.log('Bluetooth device disconnected or connection failed.');
bluetoothDevice = undefined;
bluetoothService = undefined;
bluetoothCharacteristic = undefined;
bluetoothStatusP.html('Web Bluetooth: Disconnected.');
if (bluetoothConnectButton) {
bluetoothConnectButton.html('Connect Bluetooth Device');
bluetoothConnectButton.mousePressed(connectBluetooth); // Change button action back to connect
}
// Reset lastSentLabel and timer to allow re-sending on reconnect
lastSentLabel = '';
lastClassificationSendTime = 0;
}
```

---

### `index.html` (No Changes Needed)

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>p5.js Sketch</title>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.3/lib/p5.min.js"></script>
<script src="https://unpkg.com/ml5@0.12.2/dist/ml5.min.js"></script> <!-- Added ml5.js library -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="sketch.js"></script>
</body>
</html>
```

---

### `style.css` (No Changes Needed)

```css
body {
margin: 0;
padding: 0;
overflow: hidden;
}

canvas {
display: block;
}
```

---

**Tärkeää huomioitavaa micro:bitin kanssa:**

* **micro:bitin BLE-koodi:** Varmista, että micro:bitisi on ohjelmoitu käyttämään UART-palvelua. Esimerkiksi MakeCode-editorissa voit lisätä "Serial" (Sarja) -kategorian lohkoja ja asettaa ne käyttämään Bluetoothia.
* Yleensä sinun täytyy käynnistää micro:bitin UART-palvelu koodissasi.
* Kun lähetät dataa micro:bitiin (RX-ominaisuus), voit lukea sitä micro:bitin päässä käyttämällä sarjaliikennelohkoja, kuten `serial.readString()` tai `serial.readUntil()`, ja etsiä rivinvaihtoa (`\n`).
* **Selaimen tuki:** Muista, että Web Bluetooth toimii vain Chrome-, Edge- ja Opera-selaimissa (ei Firefoxissa tai Safarissa).
* **HTTPS:** Koodin on oltava turvallisen yhteyden (HTTPS) kautta, jotta Web Bluetooth toimii. P5.js-editori hoitaa tämän automaattisesti.

Kokeile nyt uudelleen ja tarkista konsoli. Nyt UUID-virheen pitäisi olla korjattu, ja Bluetooth-laitteen valintaikkunan pitäisi ilmestyä, jos micro:bit mainostaa UART-palvelua.
👤 You
Pakota näyttämään aivan kaikki lähistöllä olevat bluetooth laitteet
🤖 AI
Aivan, ymmärrän. Joskus voi olla hyödyllistä nähdä kaikki laitteet, jos esimerkiksi mikrokontrolleri ei mainosta palveluaan skannausdatassa tai jos haluat yhdistää laitteeseen, jonka palvelun UUID-tunnusta et tiedä etukäteen.

Voimme muuttaa `navigator.bluetooth.requestDevice()`-kutsua niin, että se pyytää selaimeen näyttämään kaikki lähellä olevat Bluetooth-laitteet.

**Tärkeää huomioitavaa:**
* Kun käytetään `acceptAllDevices: true`, selain näyttää *kaikki* löytämänsä Bluetooth LE -laitteet, mukaan lukien puhelimet, kuulokkeet jne. Käyttäjän on valittava oikea laite huolellisesti.
* Vaikka valitsisimme kaikki laitteet, meidän on silti ilmoitettava `optionalServices`-parametrissä, mitä palveluita aiomme käyttää. Jos palvelua ei ole `optionalServices`-listalla, selain ei anna meille lupaa käyttää sitä, vaikka laite löytyisikin. Tässä tapauksessa pidämme micro:bitin UART service UUID:n `optionalServices`-listalla.

Tässä on päivitetty `sketch.js`-tiedosto:

---

### `sketch.js` (Updated: `acceptAllDevices: true` for Bluetooth)

```javascript
// Remixed from: Anonymous/teachable-machine-p5-js-device-web-serial
// Original: https://p5js.ai/gallery/s/teachable-machine-p5-js-device-web-serial
// ----------------------------------------

// Remixed from: Larux75/teachable-machine-web-serial2
// Original: https://p5js.ai/gallery/u/Larux75-p5js/teachable-machine-web-serial2
// ----------------------------------------

//====================================================
// This code was written on the p5js.ai IDE
// Using the xelsed-alpha6-flash tuned model
// Visit the site and support Dyslexic Research
//===================================================

let classifier; // The ml5.js image classifier
let video; // The webcam video capture
let modelURLInput; // Input field for Teachable Machine model URL
let modelURLLoadButton; // Button to load the model from the URL
let modelURL = ''; // Your Teachable Machine model URL (initially empty)
let currentClassification = { label: 'Please enter model URL and click OK', confidence: 0 }; // Stores the latest classification result

// Web Serial variables
let port; // The connected serial port
let reader; // For reading data from the port
let writer; // For writing data from the port
let serialConnectButton; // Button to connect/disconnect serial
let serialStatusP; // Paragraph to display serial connection status
let receivedSerialDataP; // Paragraph to display data received from serial (renamed for clarity)

// Web Bluetooth variables
let bluetoothDevice; // The connected Bluetooth device
let bluetoothService; // The specific BLE service
let bluetoothCharacteristic; // The characteristic for writing data
let bluetoothConnectButton; // Button to connect/disconnect Bluetooth
let bluetoothStatusP; // Paragraph to display Bluetooth connection status
// CUSTOM UUIDs: Nämä ovat viralliset micro:bit UART Service UUID:t
const bluetoothServiceUUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
const bluetoothCharacteristicUUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e'; // RX Characteristic (Write)

// Common timing and state variables
let classificationSendInterval = 3000; // Send classification every 3 seconds
let lastClassificationSendTime = 0;
let lastSentLabel = ''; // To avoid sending the same label repeatedly (stores the string label)

// Application state: 'input' or 'classifier'
let appState = 'input';

// FIX: Move videomargin declaration to the top so it's defined before setup() uses it.
let videoMargin = 20; // Margin around the video preview and UI elements

function preload() {
console.log('preload() started.');
// We won't load the model here anymore.
// Model loading will happen when the user enters a URL and clicks OK.
console.log('preload() finished.');
}

function setup() {
console.log('setup() started.');
createCanvas(windowWidth, windowHeight); // Create a responsive canvas

// Create the video capture from the webcam
video = createCapture(VIDEO, videoReady); // Add a callback for video readiness
video.hide(); // Hide the HTML video element as we'll draw it on the canvas
console.log('Video capture object created in setup().');
// Initialize custom ready flag to false
video.ready = false;

// Initial display message asking for model URL
textSize(32);
textAlign(CENTER, CENTER);
fill(0);
text('Please enter your Teachable Machine model URL below', width / 2, height / 2);

// --- Model URL Input Setup ---
modelURLInput = createInput();
modelURLInput.attribute('placeholder', 'Enter Teachable Machine model URL here');
modelURLInput.style('font-size', '16px');
modelURLInput.style('padding', '8px');
// FIX: videomargin is now defined, so this line will work.
modelURLInput.style('width', 'calc(100% - 100px - 2 * ' + videoMargin + 'px)'); // Adjust width
modelURLInput.style('border', '1px solid #ccc');
modelURLInput.style('border-radius', '4px');

modelURLLoadButton = createButton('OK');
modelURLLoadButton.mousePressed(loadModelFromInput);
modelURLLoadButton.style('font-size', '16px');
modelURLLoadButton.style('padding', '10px 15px');
modelURLLoadButton.style('background-color', '#007BFF');
modelURLLoadButton.style('color', 'white');
modelURLLoadButton.style('border', 'none');
modelURLLoadButton.style('border-radius', '5px');
modelURLLoadButton.style('cursor', 'pointer');

// --- Web Serial Setup ---
// Create p5 DOM elements for serial status and received data
serialStatusP = createP('Web Serial: Not connected.');
serialStatusP.style('font-size', '18px');
serialStatusP.style('color', '#333');
serialStatusP.style('font-family', 'sans-serif'); // Add a standard font for consistency

receivedSerialDataP = createP('Received: '); // Renamed for clarity
receivedSerialDataP.style('font-size', '18px');
receivedSerialDataP.style('color', '#333');
receivedSerialDataP.style('font-family', 'sans-serif'); // Add a standard font for consistency

// Check if Web Serial API is supported
if ('serial' in navigator) {
// Create the "Connect Serial Port" button
serialConnectButton = createButton('Connect Serial Port');
serialConnectButton.mousePressed(connectSerial); // Call connectSerial() when button is pressed
serialConnectButton.style('font-size', '16px');
serialConnectButton.style('padding', '10px 15px');
serialConnectButton.style('background-color', '#4CAF50');
serialConnectButton.style('color', 'white');
serialConnectButton.style('border', 'none');
serialConnectButton.style('border-radius', '5px');
serialConnectButton.style('cursor', 'pointer');

// Add event listener for port disconnection (e.g., USB cable unplugged)
navigator.serial.addEventListener('disconnect', portDisconnected);

} else {
// Display an error message if Web Serial is not supported
serialStatusP.html('Web Serial API not supported in this browser. Try Chrome/Edge.');
serialStatusP.style('color', 'red');
}

// --- Web Bluetooth Setup ---
bluetoothStatusP = createP('Web Bluetooth: Not connected.');
bluetoothStatusP.style('font-size', '18px');
bluetoothStatusP.style('color', '#333');
bluetoothStatusP.style('font-family', 'sans-serif');

if ('bluetooth' in navigator) {
console.log('Web Bluetooth API is supported in this browser.');
bluetoothConnectButton = createButton('Connect Bluetooth Device');
bluetoothConnectButton.mousePressed(connectBluetooth);
bluetoothConnectButton.style('font-size', '16px');
bluetoothConnectButton.style('padding', '10px 15px');
bluetoothConnectButton.style('background-color', '#6A5ACD'); // A nice purple
bluetoothConnectButton.style('color', 'white');
bluetoothConnectButton.style('border', 'none');
bluetoothConnectButton.style('border-radius', '5px');
bluetoothConnectButton.style('cursor', 'pointer');
} else {
console.warn('Web Bluetooth API is NOT supported in this browser.');
bluetoothStatusP.html('Web Bluetooth API not supported in this browser. Try Chrome/Edge.');
bluetoothStatusP.style('color', 'red');
}

// Set initial visibility for UI elements
setClassifierUIVisibility(false); // Hide classifier elements initially

// Call windowResized once to set initial positions correctly
windowResized();
console.log('setup() finished. appState:', appState);
}

function videoReady() {
console.log('Video capture is ready! (videoReady() callback executed)'); // Critical log
video.ready = true; // Custom flag to indicate video is ready
console.log('Video element readyState:', video.elt.readyState); // Check HTML video element state
console.log('Video dimensions: ', video.width, 'x', video.height); // Log actual video dimensions

// If model is already loaded and we're in classifier state, start classifying
if (appState === 'classifier' && classifier && classifier.ready) {
console.log('Video ready and classifier ready. Starting classification from videoReady().');
classifyVideo();
}
}

/**
* Hides/shows UI elements belonging to the classifier view.
* @param {boolean} isVisible - True to show, false to hide.
*/
function setClassifierUIVisibility(isVisible) {
const displayStyle = isVisible ? 'block' : 'none';

if (serialStatusP) serialStatusP.style('display', displayStyle);
if (receivedSerialDataP) receivedSerialDataP.style('display', displayStyle);
if (serialConnectButton) serialConnectButton.style('display', displayStyle);

if (bluetoothStatusP) bluetoothStatusP.style('display', displayStyle);
if (bluetoothConnectButton) bluetoothConnectButton.style('display', displayStyle);

// Input view elements are visible when classifier elements are hidden
if (modelURLInput) modelURLInput.style('display', isVisible ? 'none' : 'block');
if (modelURLLoadButton) modelURLLoadButton.style('display', isVisible ? 'none' : 'block');
console.log('Classifier UI visibility set to:', isVisible);
}


function loadModelFromInput() {
console.log('loadModelFromInput() called.');
const newModelURL = modelURLInput.value().trim();
if (newModelURL) {
modelURL = newModelURL;
console.log('Attempting to load model from:', modelURL); // Log attempt
currentClassification.label = 'Loading model...';
classifier = ml5.imageClassifier(modelURL, modelLoaded); // Re-initialize classifier
classifier.ready = false; // Reset ready flag
modelURLInput.attribute('disabled', ''); // Disable input during loading
modelURLLoadButton.attribute('disabled', ''); // Disable button during loading
} else {
alert('Please enter a valid Teachable Machine model URL.');
console.warn('No model URL entered.');
}
}

// Modified modelLoaded function to handle errors
function modelLoaded(error) {
console.log('modelLoaded() callback received.');
if (error) {
console.error('Error loading model:', error); // Log the error
currentClassification.label = 'Model loading failed: ' + error.message;
// Re-enable input and button so user can try again
modelURLInput.removeAttribute('disabled');
modelURLLoadButton.removeAttribute('disabled');
// Keep appState as 'input'
console.log('Model loading failed, staying in input state.');
return; // Stop here if loading failed
}
console.log('Model Loaded successfully!'); // Confirm model is loaded
classifier.ready = true; // Set a custom flag to indicate the model is ready
currentClassification.label = 'Model Loaded. Starting classification...';
// Check if video is also ready before starting classification
if (video && video.ready) {
console.log('Classifier ready and video ready. Starting classification from modelLoaded().');
classifyVideo(); // Start classifying once the model is loaded AND video is ready
} else {
console.warn('Model loaded, but video not yet ready. Will start classification when video is ready.');
}
modelURLInput.removeAttribute('disabled'); // Enable input after loading
modelURLLoadButton.removeAttribute('disabled'); // Enable button after loading
appState = 'classifier'; // Switch to classifier view
setClassifierUIVisibility(true); // Show classifier elements
windowResized(); // Re-position elements for the classifier view
console.log('modelLoaded() finished. appState:', appState);
}

function classifyVideo() {
// Ensure the video is loaded and the model is ready before attempting to classify
if (video && video.ready && classifier && classifier.ready) {
console.log('Classifying video frame.');
// Classify the current video frame and call gotResult when done
// Reference: https://learn.ml5js.org/docs/#/reference/image-classifier?id=classify
classifier.classify(video, gotResult);
} else {
// If not ready, try again after a short delay
// This is important if video or classifier isn't ready simultaneously
console.warn('classifyVideo() called, but video or classifier not ready. Retrying in 100ms.');
setTimeout(classifyVideo, 100);
}
}

function gotResult(error, results) {
// If there's an error during classification, log it
if (error) {
console.error('Error during classification:', error);
currentClassification.label = 'Error: ' + error.message;
currentClassification.confidence = 0;
// Don't stop classifying, try again
classifyVideo();
return;
}

// If no error, results will be an array of objects
// Each object has a 'label' and 'confidence'
// Sort results by confidence in descending order to get the top prediction
results.sort((a, b) => b.confidence - a.confidence);
const topResult = results[0];

// Update the current classification result
currentClassification.label = topResult.label;
currentClassification.confidence = topResult.confidence;

// Keep classifying the video feed recursively
classifyVideo();
}

function draw() {
background(220); // Clear the canvas each frame

if (appState === 'input') {
// Display input view message
textSize(32);
textAlign(CENTER, CENTER);
fill(0);
text('Please enter your Teachable Machine model URL below', width / 2, height / 2 - 50); // Slightly above input field
} else if (appState === 'classifier') {
if (video && video.ready && classifier && classifier.ready) { // Check video.ready here too
console.log('Attempting to draw video. Video ready:', video.ready, 'Classifier ready:', classifier.ready); // Critical log
// Calculate preview size while maintaining aspect ratio
// Let's make the preview height about half the canvas height, minus margins.
let previewHeight = height / 2 - 2 * videoMargin;
let previewWidth = (previewHeight / video.height) * video.width;

// Ensure previewWidth doesn't exceed available space if canvas is very narrow
// (e.g., if canvas width is less than previewWidth + videoMargin * 2)
let availableWidthForVideo = width - 2 * videoMargin;
if (previewWidth > availableWidthForVideo) {
previewWidth = availableWidthForVideo;
previewHeight = (previewWidth / video.width) * video.height;
}

// Draw the video feed at the top-left with margin
image(video, videoMargin, videoMargin, previewWidth, previewHeight);

// Classification text and background rectangle to the right of the video preview.
let textRectX = videoMargin + previewWidth + videoMargin;
let textRectWidth = width - textRectX - videoMargin;
let textRectHeight = previewHeight; // Make it the same height as the video preview
let textRectY = videoMargin; // Align with the top of the video

fill(255, 200); // White with 200 alpha
noStroke();
rect(textRectX, textRectY, textRectWidth, textRectHeight);

// Display the current classification result
fill(0);
textSize(24);
textAlign(LEFT, CENTER);
// Center text vertically within its rectangle
let textYOffset = textRectY + textRectHeight / 2;
text(`Label: ${currentClassification.label}`, textRectX + 10, textYOffset - 20); // Adjust y for two lines
// Use nf() to format confidence as a percentage with 2 decimal places
text(`Confidence: ${nf(currentClassification.confidence * 100, 0, 2)}%`, textRectX + 10, textYOffset + 20);

// --- Determine number to send based on classification ---
let numberToSend = -1; // Default to -1 for unknown labels
const currentLabel = currentClassification.label.toLowerCase(); // Use lowercase for comparison
switch (currentLabel) {
case 'ihminen':
numberToSend = 1;
break;
case 'koira':
numberToSend = 2;
break;
case 'kissa': // Assuming 'kissa' for number 3 based on common classification tasks
numberToSend = 3;
break;
default:
numberToSend = -1; // For any other label, or if label is empty/null
break;
}

// --- Send classification data via Web Serial and Web Bluetooth ---
const shouldSend = millis() - lastClassificationSendTime > classificationSendInterval && currentLabel !== lastSentLabel.toLowerCase();

if (shouldSend) {
if (numberToSend !== -1) {
// Send via Web Serial
if (port && writer) {
const serialDataToSend = numberToSend + '\n'; // Add newline for microcontroller parsing
writeSerial(serialDataToSend);
}

// Send via Web Bluetooth
if (bluetoothCharacteristic) {
// Web Bluetooth: Convert number to string and add newline for micro:bit UART
writeBluetooth(numberToSend);
}

// Update last sent info if any data was attempted to be sent
if (port || bluetoothCharacteristic) {
lastSentLabel = currentLabel;
lastClassificationSendTime = millis();
}
} else {
// Only log if no valid number was mapped and we were supposed to send
if (port || bluetoothCharacteristic) {
console.warn(`Unrecognized or default label "${currentLabel}". Not sending data.`);
}
}
}
} else if (!classifier.ready) {
// Display loading message if the model is not yet ready (should only happen briefly after OK)
textSize(32);
textAlign(CENTER, CENTER);
fill(0);
text('Loading model...', width / 2, height / 2);
}
}
}

function windowResized() {
resizeCanvas(windowWidth, windowHeight); // Adjust canvas size on window resize
console.log('windowResized() called. appState:', appState);

if (appState === 'input') {
// When in input state, position input/button centrally below the initial text
let inputWidth = min(width - 2 * videoMargin, 400); // Max 400px wide, or less if canvas is narrow
if (modelURLInput) modelURLInput.style('width', `${inputWidth - modelURLLoadButton.width - 20}px`); // Adjust for button width and spacing
if (modelURLInput) modelURLInput.position(width / 2 - inputWidth / 2, height / 2 + 50); // Below initial text
if (modelURLLoadButton) modelURLLoadButton.position(width / 2 + inputWidth / 2 - modelURLLoadButton.width, height / 2 + 50); // To the right of input
} else { // In classifier state, use the standard layout
// Recalculate video preview size based on new window dimensions
let previewHeight = height / 2 - 2 * videoMargin;
let previewWidth = (previewHeight / video.height) * video.width;
let availableWidthForVideo = width - 2 * videoMargin;
if (previewWidth > availableWidthForVideo) {
previewWidth = availableWidthForVideo;
previewHeight = (previewWidth / video.width) * video.height;
}

// Calculate the new starting y-position for the DOM elements below the video
// It's the bottom of the video (videoMargin + previewHeight) + an additional videoMargin for spacing
let domElementsStartY = videoMargin + previewHeight + videoMargin;

// Re-position model URL input and button (now below video, not centered)
if (modelURLInput) modelURLInput.style('width', 'calc(100% - 100px - 2 * ' + videoMargin + 'px)'); // Reset width
if (modelURLInput) modelURLInput.position(videoMargin, domElementsStartY);
if (modelURLLoadButton) modelURLLoadButton.position(videoMargin + modelURLInput.width + 10, domElementsStartY); // 10px spacing

// Re-position other p5 DOM elements
// Adjust starting Y for other elements based on the model URL input/button height
let otherDomElementsStartY = domElementsStartY + 40; // Assuming button height is around 30-35px + 10px spacing

// Serial elements
if (serialStatusP) serialStatusP.position(videoMargin, otherDomElementsStartY); // Adjusted y
if (receivedSerialDataP) receivedSerialDataP.position(videoMargin, otherDomElementsStartY + 30); // Adjusted y
if (serialConnectButton) serialConnectButton.position(videoMargin, otherDomElementsStartY + 70); // Adjusted y

// Bluetooth elements (below Serial elements)
let bluetoothDomElementsStartY = otherDomElementsStartY + 110; // 70 + buttonHeight (approx 40)
if (bluetoothStatusP) bluetoothStatusP.position(videoMargin, bluetoothDomElementsStartY);
if (bluetoothConnectButton) bluetoothConnectButton.position(videoMargin, bluetoothDomElementsStartY + 30);
}
}

// --- Web Serial Functions ---

/**
* Connects to a serial port selected by the user.
*/
async function connectSerial() {
try {
console.log('connectSerial() called.');
// Request a serial port from the user
port = await navigator.serial.requestPort();
serialStatusP.html('Web Serial: Connecting...');
console.log('Serial port requested and granted.');

// Open the port with a specified baud rate
// IMPORTANT: This baud rate (9600) must match the one used by your microcontroller (e.g., Arduino Serial.begin(9600);)
await port.open({ baudRate: 9600 });
serialStatusP.html('Web Serial: Connected!');
serialConnectButton.html('Disconnect Serial Port'); // Change button text
serialConnectButton.mousePressed(disconnectSerial); // Change button action to disconnect
console.log('Serial port opened with baudRate 9600.');

// Create TextDecoderStream and TextEncoderStream for easy text handling
const textDecoder = new TextDecoderStream();
const textEncoder = new TextEncoderStream();
reader = textDecoder.readable.getReader(); // Reader for incoming text data
writer = textEncoder.writable.getWriter(); // Writer for outgoing text data
console.log('TextDecoderStream and TextEncoderStream created.');

// Pipe the port's readable stream through the decoder
port.readable.pipeTo(textDecoder.writable);
// TÄMÄ ON OIKEIN: Pipe the encoder's readable stream (containing encoded bytes) to the port's writable stream
textEncoder.readable.pipeTo(port.writable); // THIS IS THE CORRECTION
console.log('Serial streams piped.');

readSerial(); // Start continuously reading data from the port
} catch (error) {
console.error('Error connecting to serial port:', error);
serialStatusP.html('Web Serial: Connection failed. ' + error.message);
}
}

/**
* Disconnects from the currently connected serial port.
*/
async function disconnectSerial() {
try {
console.log('disconnectSerial() called.');
if (reader) await reader.cancel(); // Cancel any pending read operations
if (writer) await writer.close(); // Close the writer stream
if (port) await port.close(); // Close the serial port

portDisconnected(); // Reset variables and UI
console.log('Serial port successfully disconnected.');
} catch (error) {
console.error('Error disconnecting serial port:', error);
serialStatusP.html('Web Serial: Disconnection failed. ' + error.message);
}
}

/**
* Continuously reads data from the serial port.
*/
async function readSerial() {
console.log('readSerial() started.');
while (port && port.readable) {
try {
const { value, done } = await reader.read();
if (done) {
console.log('Reader closed.');
break; // Exit loop if reader is closed
}

// Check if the received value is "1" (and trim whitespace for robustness)
if (value.trim() === '1') {
receivedSerialDataP.html('Received: remote device connected');
// You might want to update serialStatusP too, but currentStatusP is already "Connected!'
} else {
receivedSerialDataP.html(`Received: ${value}`); // Display received data
}

// For more complex data, you might parse 'value' here (e.g., JSON.parse(value))
} catch (error) {
console.error('Error reading from serial port:', error);
serialStatusP.html('Web Serial: Read error. ' + error.message);
break; // Exit loop on error
}
}
portDisconnected(); // Assume disconnected if loop exits (e.g., due to error or port closure)
console.log('readSerial() stopped.');
}

/**
* Writes data to the serial port.
* @param {string} data - The string data to send.
*/
async function writeSerial(data) {
if (port && writer) {
try {
await writer.write(data);
console.log('Sent to Serial:', data.trim()); // Log sent data (the number) without the trailing newline
} catch (error) {
console.error('Error writing to serial port:', error);
serialStatusP.html('Web Serial: Write error. ' + error.message);
portDisconnected(); // Assume disconnected on write error
}
} else {
console.warn('Cannot write: Serial port not connected.');
serialStatusP.html('Web Serial: Cannot write, not connected.');
}
}

/**
* Resets serial variables and UI when the port disconnects.
*/
function portDisconnected() {
console.log('Serial port disconnected.');
port = undefined;
reader = undefined;
writer = undefined;
serialStatusP.html('Web Serial: Disconnected.');
receivedSerialDataP.html('Received: ');
if (serialConnectButton) {
serialConnectButton.html('Connect Serial Port');
serialConnectButton.mousePressed(connectSerial); // Change button action back to connect
}
// Reset lastSentLabel and timer to allow re-sending on reconnect
lastSentLabel = '';
lastClassificationSendTime = 0;
}


// --- Web Bluetooth Functions ---

/**
* Connects to a Bluetooth device with the specified service and characteristic.
*/
async function connectBluetooth() {
console.log('connectBluetooth() called from button press.'); // NEW LOG
bluetoothStatusP.html('Web Bluetooth: Requesting device...');
try {
// Request a Bluetooth device from the user with the specified service
console.log('Attempting to request ALL Bluetooth devices, but will only access specific service:', bluetoothServiceUUID); // NEW LOG
bluetoothDevice = await navigator.bluetooth.requestDevice({
acceptAllDevices: true, // This will show all nearby Bluetooth devices
optionalServices: [bluetoothServiceUUID] // We still need to declare the services we intend to access
});
console.log('Bluetooth device requested and granted:', bluetoothDevice.name);

// Add event listener for GATT server disconnection
bluetoothDevice.addEventListener('gattserverdisconnected', bluetoothDisconnected);

// Connect to the GATT server
bluetoothStatusP.html('Web Bluetooth: Connecting to GATT server...');
console.log('Attempting to connect to GATT server.'); // NEW LOG
const server = await bluetoothDevice.gatt.connect();
console.log('Connected to GATT server.');

// Get the primary service
bluetoothStatusP.html('Web Bluetooth: Getting service...');
console.log('Attempting to get Bluetooth service with UUID:', bluetoothServiceUUID); // NEW LOG
bluetoothService = await server.getPrimaryService(bluetoothServiceUUID);
console.log('Got Bluetooth service:', bluetoothServiceUUID);

// Get the characteristic for writing
bluetoothStatusP.html('Web Bluetooth: Getting characteristic...');
console.log('Attempting to get Bluetooth characteristic with UUID:', bluetoothCharacteristicUUID); // NEW LOG
bluetoothCharacteristic = await bluetoothService.getCharacteristic(bluetoothCharacteristicUUID);
console.log('Got Bluetooth characteristic:', bluetoothCharacteristicUUID);

bluetoothStatusP.html(`Web Bluetooth: Connected to ${bluetoothDevice.name}!`);
bluetoothConnectButton.html('Disconnect Bluetooth Device'); // Change button text
bluetoothConnectButton.mousePressed(disconnectBluetooth); // Change button action
console.log('Bluetooth connection established successfully.');

} catch (error) {
console.error('Error connecting to Bluetooth device:', error);
bluetoothStatusP.html('Web Bluetooth: Connection failed. ' + error.message);
bluetoothDisconnected(); // Reset UI on failure
}
}

/**
* Disconnects from the currently connected Bluetooth device.
*/
async function disconnectBluetooth() {
try {
console.log('disconnectBluetooth() called.');
if (bluetoothDevice && bluetoothDevice.gatt.connected) {
bluetoothStatusP.html('Web Bluetooth: Disconnecting...');
bluetoothDevice.gatt.disconnect();
console.log('Bluetooth GATT server disconnected.');
}
bluetoothDisconnected(); // Reset variables and UI
} catch (error) {
console.error('Error disconnecting Bluetooth device:', error);
bluetoothStatusP.html('Web Bluetooth: Disconnection failed. ' + error.message);
}
}

/**
* Writes a single number (byte) to the Bluetooth characteristic.
* @param {number} number - The number (1, 2, or 3) to send.
*/
async function writeBluetooth(number) {
if (bluetoothCharacteristic) {
try {
// Convert number to string and add newline character
const stringToSend = String(number) + '\n';
const encoder = new TextEncoder(); // Create a TextEncoder to convert string to bytes
const encodedData = encoder.encode(stringToSend); // Encode the string to a Uint8Array

await bluetoothCharacteristic.writeValue(encodedData); // Write the byte array to the characteristic
console.log('Sent to Bluetooth:', stringToSend.trim()); // Log sent data without the trailing newline
} catch (error) {
console.error('Error writing to Bluetooth characteristic:', error);
bluetoothStatusP.html('Web Bluetooth: Write error. ' + error.message);
bluetoothDisconnected(); // Assume disconnected on write error
}
} else {
console.warn('Cannot write: Bluetooth device not connected.');
bluetoothStatusP.html('Web Bluetooth: Cannot write, not connected.');
}
}

/**
* Resets Bluetooth variables and UI when the device disconnects (or fails to connect).
*/
function bluetoothDisconnected() {
console.log('Bluetooth device disconnected or connection failed.');
bluetoothDevice = undefined;
bluetoothService = undefined;
bluetoothCharacteristic = undefined;
bluetoothStatusP.html('Web Bluetooth: Disconnected.');
if (bluetoothConnectButton) {
bluetoothConnectButton.html('Connect Bluetooth Device');
bluetoothConnectButton.mousePressed(connectBluetooth); // Change button action back to connect
}
// Reset lastSentLabel and timer to allow re-sending on reconnect
lastSentLabel = '';
lastClassificationSendTime = 0;
}
```

---

### `index.html` (No Changes Needed)

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>p5.js Sketch</title>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.3/lib/p5.min.js"></script>
<script src="https://unpkg.com/ml5@0.12.2/dist/ml5.min.js"></script> <!-- Added ml5.js library -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="sketch.js"></script>
</body>
</html>
```

---

### `style.css` (No Changes Needed)

```css
body {
margin: 0;
padding: 0;
overflow: hidden;
}

canvas {
display: block;
}
```

---

Nyt, kun painat "Connect Bluetooth Device" -painiketta, selaimesi pitäisi näyttää valintaikkunan, jossa on lueteltu kaikki lähellä olevat Bluetooth LE -laitteet. Valitse micro:bit-laitteesi siitä listasta.

Muista, että micro:bitin on edelleen mainostettava UART-palvelua, jotta voimme onnistuneesti yhdistää siihen ja käyttää sen ominaisuuksia. `acceptAllDevices: true` vain laajentaa laitteiden löytämistä, mutta palvelujen käyttöoikeudet edellyttävät edelleen `optionalServices`-määritystä ja että laite todella tarjoaa kyseisen palvelun.
🎬 Video Preview