Hackster is hosting Hackster Holidays, Ep. 7: Livestream & Giveaway Drawing. Watch previous episodes or stream live on Friday!Stream Hackster Holidays, Ep. 7 on Friday!
Doctor Volt
Published © GPL3+

Laser Scanning Microscope From Blu-Ray Player

DIY Laser Scanning Microscope made from a defective Blu-Ray Player.

ExpertShowcase (no instructions)12,915
Laser Scanning Microscope From Blu-Ray Player

Things used in this project

Hardware components

ESP32 SBC-NodeMCU
×1
OPU BDP10G
×1
FFC/FPB adapter board 40 pin, 0.5mm
×1
Custom 3D printed parts
LEDO 6060 resin is a good choice.
×1

Software apps and online services

VS Code
Microsoft VS Code
PlatformIO IDE
PlatformIO IDE

Story

Read more

Custom parts and enclosures

CAD drawings

Open with FreeCAD

Schematics

lasermic_cJrHnGvi2x.kicad_sch

Library file for NodeMCU-ESP32

lasermic_rAmxgMPDqU.pdf

Code

src.ino

C/C++
/* Copyright (C) 2022  Doctor Volt

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <https://www.gnu.org/licenses></https:>.
*/

// #include <Arduino.h>
#include <ArduinoJson.h>
// #include "driver/ledc.h"

#define PINPWM_SCAN_X 26
#define PINTOGGLE_SCAN_X 25
#define PINPWN_FOCUS 22
#define PINSTEPY_EN 18
#define PINSTEPY_STP 19
#define PINSTEPY_DIR 21

#define PIN_CH1 32 
#define PIN_CH2 33 
#define ACTIVE LOW
#define PASSIVE HIGH
#define MAX_SAMPLES 16
enum CMD
{
  CMD_IDLE,
  SCAN,
  FOCUS
};

//WPA credentials
const char *ssid = "******";
const char *password = "*******";
// const int ledPin = LED_BUILTIN;
const int PWMFreq = 20000;
const int PWMChanScn = 0;
const int PWMChanFoc = 3;
int PWMResBits = 6;
int PWMRes; // = (1 << PWMResBits) - 1;
int XRes;   // = 2 * (1 << PWMResBits) - 1;
int YRes;
const int OCVChannels = 2;
int scan_speed = 1;
bool scan_back = false;

struct __attribute__((__packed__)) Point
{
  uint16_t C1;
  uint16_t C2;
};
typedef Point CVPoint;
CVPoint *linebuf;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
volatile SemaphoreHandle_t scanSemaphore;
TaskHandle_t taskHandle;

void web_init(const char *ssid, const char *password);
void web_write(const char *data);
void web_write_event(const char *event);
void web_cleanup();
void test();

void setXscan(int x)
{
  digitalWrite(PINTOGGLE_SCAN_X, (x > 0));
  int d = (x > 0) ? PWMRes - x : -x;
  // Serial.printf("x: %d, d: %d\r\n", x, d);
  ledcWrite(PWMChanScn, d);
}
// #define TWOWAY
void scanX(int speed, CVPoint *linebuf, bool back = false)
{
  const int nsamp = MAX_SAMPLES - speed;
  for (int scanX = -PWMRes; scanX <= PWMRes; scanX++)
  {
    if (!back)
    {
      setXscan(scanX);
      delayMicroseconds(100);
      int c1 = 0, c2 = 0;
      for (int i = 0; i < nsamp; i++)
      {
        c1 += analogRead(PIN_CH1);
        c2 += analogRead(PIN_CH2);
      }
      linebuf[PWMRes + scanX].C1 = 16*(c1 / nsamp); //Sum
      linebuf[PWMRes + scanX].C2 = 64*(c2 / nsamp); //FES
    }
    else
    {
      setXscan(-scanX);
      delayMicroseconds(1000);
    }
    // vTaskDelay(1);
  }
}

/* This must be started as task to prevent triggering the async_tcp watchdog*/
void scan(void *pvParameters)
{
  Serial.println("Start scanning");
  ledcAttachPin(PINPWM_SCAN_X, PWMChanScn);
  digitalWrite(PINSTEPY_EN, ACTIVE);
  digitalWrite(PINSTEPY_DIR, scan_back);
  for (int i = 0; i < 20; i++)
  {
    digitalWrite(PINSTEPY_STP, HIGH);
    vTaskDelay(10);
    digitalWrite(PINSTEPY_STP, LOW);
  }
  setXscan(-PWMRes);
  vTaskDelay(10);

  for (int i = 0; i < YRes; i++)
  {
    vTaskDelay(100);
    scanX(scan_speed, linebuf);       // Scan line Forwards
    scanX(scan_speed, linebuf, true); // Scan Backwards
    // digitalWrite(PINSTEPY_EN, ACTIVE); //);
    vTaskDelay(1);
    digitalWrite(PINSTEPY_STP, HIGH);
    digitalWrite(PINSTEPY_STP, LOW);
    vTaskDelay(1);
    xSemaphoreGive(scanSemaphore);
    vTaskSuspend(NULL); // Suspend task during data transfer
                        // digitalWrite(PINSTEPY_EN, PASSIVE);
  }
  // Move sled back to intitial y position
  digitalWrite(PINSTEPY_DIR, !scan_back);
  digitalWrite(PINSTEPY_EN, ACTIVE);
  vTaskDelay(1);
  for (int i = 0; i < (YRes + 20); i++)
  {
    digitalWrite(PINSTEPY_STP, HIGH);
    vTaskDelay(10);
    digitalWrite(PINSTEPY_STP, LOW);
  }
  digitalWrite(PINSTEPY_EN, PASSIVE);
  digitalWrite(PINPWM_SCAN_X, LOW);
  ledcDetachPin(PINPWM_SCAN_X);
  digitalWrite(PINTOGGLE_SCAN_X, LOW);

  Serial.println("Scan completed");
  vTaskDelete(NULL);
}

void handleWebMessage(const char *data, size_t len)
{
  // Serial.write((char *)data, len);
  // Serial.println();
  DynamicJsonDocument jdoc(1024);
  deserializeJson(jdoc, data);

  if (scan_speed != (int)jdoc["speed"])
  {
    scan_speed = jdoc["speed"];
    Serial.printf("speed: %d\n", scan_speed);
  }
  static int pwmbits;
  if (pwmbits != (int)jdoc["pwmbits"])
  {
    pwmbits = jdoc["pwmbits"];
    Serial.printf("pwmbits: %d\n", pwmbits);
    ledcSetup(PWMChanScn, PWMFreq, pwmbits);
    PWMRes = (1 << pwmbits) - 1;
    XRes = 2 * (1 << pwmbits) - 1;
    YRes = jdoc["yres"];
    linebuf = (CVPoint *)calloc(XRes, sizeof(CVPoint));
  }
  static int focus;
  if (focus != (int)jdoc["focus"])
  {
    focus = jdoc["focus"];
    //Serial.printf("focus: %d\n", focus);
    ledcWrite(PWMChanFoc, focus);
  }

  if (jdoc["cmd"] == (int)CMD::SCAN)
  {
    xTaskCreate(scan, "scan", 16000, NULL, 1, &taskHandle);
  }
}
/* Outputs PWM signal on GPIO_A and inverted pwm from GPIO_B. Used to drive L293*/
/*void pwmControl(uint8_t gpio_a, uint8_t gpio_b, uint8_t chan)
{
  uint8_t group = (chan / 8), channel = (chan % 8), timer = ((chan / 2) % 4);

  ledc_channel_config_t ledc_channel = {
      .gpio_num = gpio_a,
      .speed_mode = (ledc_mode_t)group,
      .channel = (ledc_channel_t)channel,
      .intr_type = LEDC_INTR_DISABLE,
      .timer_sel = (ledc_timer_t)timer,
      .duty = 0,
      .hpoint = 0,
  };
  ledc_channel_config(&ledc_channel); // Start first channel

  ledc_channel.gpio_num = gpio_b;
  ledc_channel.flags.output_invert = true;
  ledc_channel_config(&ledc_channel); // Start inverted channel
}*/

/*void ARDUINO_ISR_ATTR onTimer()
{
  portENTER_CRITICAL_ISR(&timerMux);
  portEXIT_CRITICAL_ISR(&timerMux);
  xSemaphoreGiveFromISR(timerSemaphore, NULL);
}*/

void setup()
{
  Serial.begin(115200);
  // digitalWrite(ledPin, LOW);
  scanSemaphore = xSemaphoreCreateBinary();
  // hw_timer_t *timer = timerBegin(0, 80, true);
  // timerAttachInterrupt(timer, &onTimer, true);
  // timerAlarmWrite(timer, 100000, true);
  // timerAlarmEnable(timer);

  pinMode(PINSTEPY_EN, OUTPUT);
  pinMode(PINSTEPY_DIR, OUTPUT);
  pinMode(PINSTEPY_STP, OUTPUT);
  digitalWrite(PINSTEPY_EN, PASSIVE);

  pinMode(PINTOGGLE_SCAN_X, OUTPUT);
  // analogReadResolution(8);
  adcAttachPin(PIN_CH1);
  adcAttachPin(PIN_CH2);

  ledcSetup(PWMChanFoc, PWMFreq, 10);
  ledcAttachPin(PINPWN_FOCUS, PWMChanFoc);
  // handleWebMessage("{\"speed\":2,\"pwmbits\":6,\"yres\":128,\"cmd\":1}", 35);
  web_init(ssid, password);
}

void loop()
{
  char buf[16];
  if (xSemaphoreTake(scanSemaphore, 500) == pdTRUE)
  {
    String line;
    for (int i = 0; i < XRes; i++)
    {
      //sprintf(buf, "\"%d\",\"%d\",", linebuf[i].C1, linebuf[i].C2);
      sprintf(buf, "%d,%d,", linebuf[i].C1, linebuf[i].C2);
      line += String(buf);
    }
    line[line.length() - 1] = 0; // Remove comma */
    web_write_event(line.c_str());
    // web_write(line.c_str());
    vTaskResume(taskHandle);
  }
  else
  {
    sprintf(buf, "%d,%d", analogRead(PIN_CH1), analogRead(PIN_CH2));
    web_write(buf);
  }
  // Serial.println("loop");
  web_cleanup();
}

web.cpp

C/C++
/* Copyright (C) 2020  Doctor Volt
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <https://www.gnu.org/licenses></https:>.
*/

#define LOCAL_WEB //Website located on PC

#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#ifndef LOCAL_WEB
#include <SPIFFS.h>
#endif
#include <mdns.h>
//#include <AsyncElegantOTA.h>

const char *hostname = "lasermic.local";

void handleWebMessage(const char *data, size_t len);

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
AsyncEventSource es("/es");

void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type,
             void *arg, uint8_t *data, size_t len)
{
  switch (type)
  {
  case WS_EVT_CONNECT:
    Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
    break;
  case WS_EVT_DISCONNECT:
    Serial.printf("WebSocket client #%u disconnected\n", client->id());
    break;
  case WS_EVT_DATA:
    // handleWebSocketMessage(arg, data, len);
    {
      AwsFrameInfo *info = (AwsFrameInfo *)arg;
      if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT)
      {
        handleWebMessage((const char *)data, len);
      }
    }
    break;
  case WS_EVT_PONG:
    Serial.println("WebSocket Pong:");
  case WS_EVT_ERROR:
    Serial.println("WebSocket Error");
  }
}

String processor(const String &var)
{
  Serial.print("processor");
  Serial.println(var);
  if (var == "STATE")
  {
  }
  return "Hello";
}

void web_init(const char *ssid, const char *password)
{
  // Serial port for debugging purposes

  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  Serial.print("Connecting to WiFi");
  while (WiFi.status() != WL_CONNECTED)
  {
    Serial.print(".");
    delay(500);
  }
  // Print ESP Local IP Address
  Serial.println();
  Serial.println("WiFi Connected");

  // Assign DNS name
  mdns_init();
  mdns_hostname_set(hostname);

  ws.onEvent(onEvent);
  server.addHandler(&ws);

  // Initialize SPIFFS
  #ifndef LOCAL_WEB
  Serial.printf("Open http://%s in browser\r\n", hostname);
  if (!SPIFFS.begin(true))
  {
    Serial.println("An Error has occurred while mounting SPIFFS");
    return;
  }
  server.serveStatic("/", SPIFFS, "/");
  #endif

  /*Event source initialization*/
  es.onConnect([](AsyncEventSourceClient *client)
               {
    Serial.println("es.onConnect");
    if(client->lastId()){
      Serial.printf("Event Client reconnected! Last message ID: %u\n", client->lastId());
    }
    if(client->connected()){
      Serial.println("Event source connected");

    } });

  // Magic to allow access to event server from anywhere
  DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Origin"), F("*"));
  // DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Headers"), F("content-type"));
  // Start server
  server.begin();
  server.addHandler(&es);
  // AsyncElegantOTA.begin(&server);
}

void web_write_event(const char *data)
{
  // ws.textAll((const char*)buffer, len);
  es.send(data);
}

void web_write(const char *data)
{
  ws.textAll(data);
  // es.send(data, NULL);
}

void web_cleanup()
{
  ws.cleanupClients();
}

index.htm

HTML
Download the required opencv.js from https://docs.opencv.org/4.6.0/opencv.js
Note: This will not fit on the ESP32 SPIFFS file system, so open from PC.
<!--
Download the required opencv.js from https://docs.opencv.org/4.6.0/opencv.js
Note: This will not fit on the ESP32 SPIFFS file system, so open from PC.
-->
<!DOCTYPE HTML>
<html>

<head>
  <title>Blu-Ray Laser Microscope</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="data:,">
  <style>
    html {
      font-family: Arial, Helvetica, sans-serif;
      text-align: center;
    }

    body {
      /*width: 900px;*/
    }

    h1 {
      font-size: 1.8rem;
      color: white;
    }

    h2 {
      font-size: 1.5rem;
      font-weight: bold;
    }

    .espcontrol {
      display: grid;
      grid-template-columns: auto auto;
      background-color: mediumblue;
      justify-content: center;
    }

    .header {
      grid-column: 1/4;
      background-color: darkviolet;
    }

    .left {
      background-color: #777;
    }

    .middle {
      background-color: black;
      align-items: center;
    }

    .right {
      background-color: #777
    }

    .footer {
      display: grid;
      gap: 10px;
      grid-column: 1/4;
      grid-template-columns: auto auto auto;
      background-color: darkslateblue;
      color: white;
      justify-content: left;
    }

    .slider_box {
      padding: 5px;
      display: grid;
      justify-content: space-between;
    }

    .slider {
      width: 150px;
    }

    .image {
      background-color: #143642;
      width: 64px;
      height: 64px;
    }

    .meters {
      grid-column: 3/4;
    }

    meter {
      width: 400px
    }

    div {
      border: #143642;
      border-style: dotted;
      border-width: 1px;
    }
  </style>

  <script script type="text/javascript">
    var gateway = "ws://lasermic.local/ws";
    var alfa = 1; //Contrast
    var beta = 0; //Brightness
    var act_channel = 0;
    var settings = { "speed": 2, "pwmbits": 6, "yres": 100, "focus": 0, "cmd": 0 };
    const xres = 2 * (1 << settings.pwmbits) - 1;
    const yres = settings.yres;
    const IDLE = 0;
    const SCAN = 1;
    const FOCUS = 2;
    var scanmat;
    var scanmat_vector;

    //var gateway = "ws://${window.location.hostname}/ws";
    var websocket;
    //var src; //OpenCv Mat
    window.addEventListener('load', onLoad);

    function onOpenCVReady() {
      scanmat = cv.Mat.zeros(yres, xres, cv.CV_16UC2);
      scanmat_vector = new cv.MatVector();
      var image = new cv.Mat();

      alfa = Number(document.getElementById("sld_cont").value);
      document.getElementById("sld_cont").oninput = function () {
        alfa = Number(this.value);
        scanmat_vector.get(act_channel).convertTo(image, cv.CV_16UC1, alfa, beta);
        cv.resize(image, image, new cv.Size(512, 512));
        cv.imshow("cvcanvas", image);
      }
      beta = Number(document.getElementById("sld_bright").value);
      document.getElementById("sld_bright").oninput = function () {
        beta = Number(this.value);
        scanmat_vector.get(act_channel).convertTo(image, cv.CV_16UC1, alfa, beta);
        cv.resize(image, image, new cv.Size(512, 512));
        cv.imshow("cvcanvas", image);
      }
      console.log(act_channel);
      var rad_c = document.getElementsByName("rad_channel");
      for (i = 0; i < rad_c.length; i++) {
        if (rad_c[i].checked) {
          act_channel = Number(rad_c[i].value);
          console.log(act_channel);
        }
        rad_c[i].onchange = function () {
          act_channel = Number(this.value);
          console.log(act_channel);
          scanmat_vector.get(act_channel).convertTo(image, cv.CV_16UC1, alfa, beta);
          cv.resize(image, image, new cv.Size(512, 512));
          cv.imshow("cvcanvas", image);
        }
      }
    }

    function dbgMat(mat) {
      console.log('image width: ' + mat.cols + '\n' +
        'image height: ' + mat.rows + '\n' +
        'image size: ' + mat.size().width + '*' + mat.size().height + '\n' +
        'image depth: ' + mat.depth() + '\n' +
        'image channels ' + mat.channels() + '\n' +
        'image type: ' + mat.type() + '\n');
      console.log(mat.data);
    }

    function onWsOpen(event) {
      console.log('Websocket open');
      websocket.send(JSON.stringify(settings));
      //Outgoing messages from user iteraction

      document.getElementById("sld_speed").oninput = function () {
        settings.speed = this.value;
        websocket.send(JSON.stringify(settings));
        //console.log(JSON.stringify(send));
      }

      document.getElementById("sld_focus").oninput = function () {
        settings.focus = this.value;
        websocket.send(JSON.stringify(settings));
        document.getElementById("sld_focus_fine").value = 0;
      }

      document.getElementById("sld_focus_fine").oninput = function () {
        settings.focus = Number(this.value) + Number(document.getElementById("sld_focus").value);
        websocket.send(JSON.stringify(settings));
      }

      document.getElementById("btn_scan").onclick = function () {
        settings.cmd = SCAN;
        websocket.send(JSON.stringify(settings));
        settings.cmd = 0;
        linenum = 0;
        this.setAttribute("disabled", "");
      }
      document.getElementById("h2_connected").innerHTML = "Connected";
      document.getElementById("btn_scan").removeAttribute("disabled");
    }
    function onWsClose(event) {
      console.log('Websocket closed');
      document.getElementById("h2_connected").innerHTML = "Not Connected";
      document.getElementById("btn_scan").setAttribute("disabled", "");
      setTimeout(initWebSocket, 2000);
    }
    var linenum = 0;
    function onMessage(event) {
      var arr = event.data.split(",");
      if (arr.length == 2) {
        document.getElementById("met_ch1").value = arr[0];
        document.getElementById("met_ch2").value = arr[1];
        return;
      }
      var scanline = cv.matFromArray(1, xres, cv.CV_16UC2, arr);
      var dstrow = scanmat.row(linenum++);
      scanline.copyTo(dstrow);
      if (linenum == yres - 1) //Finished scanning
      {
        //dbgMat(scanmat);
        document.getElementById("btn_scan").removeAttribute("disabled");
      }
      var image = new cv.Mat();
      cv.split(scanmat, scanmat_vector);
      scanmat_vector.push_back(new cv.Mat(yres, xres, cv.CV_16UC1)); //Add channel for FES

      scanmat_vector.get(act_channel).convertTo(image, cv.CV_16UC1, alfa, beta);
      cv.resize(image, image, new cv.Size(512, 512));
      cv.imshow("cvcanvas", image);
      image.delete();
    }
    function initWebSocket() {
      //console.log('Trying to open a WebSocket connection...');
      websocket = new WebSocket(gateway);
      websocket.onopen = onWsOpen;
      websocket.onclose = onWsClose;
      websocket.onmessage = onMessage; // <-- add this line
    }
    //Receiving bulk date
    function initEventSources() {
      var evtsource = new EventSource("http://lasermic.local/es");
      evtsource.onmessage = onMessage;
    }

    function onLoad(event) {
      console.log("onLoad()");
      document.getElementById("btn_scan").setAttribute("disabled", "");
      settings.speed = document.getElementById("sld_speed").value;
      settings.focus = document.getElementById("sld_focus").value;
      settings.focus += document.getElementById("sld_focus_fine").value;

      initWebSocket();
      initEventSources();
    }
    var Module = {
      // https://emscripten.org/docs/api_reference/module.html#Module.onRuntimeInitialized
      onRuntimeInitialized() {
        onOpenCVReady();
      }
    }
  </script>
  <script async src="opencv.js" type="text/javascript"></script>
  <!--<script async src="opencv_new.js" onload="onOpenCVReady()" type="text/javascript"></script>-->

</head>

<body>
  <div class="espcontrol">
    <div class="header">
      <h1>Blu-Ray Laser Scanning Microscope</h1>
    </div>
    <div class="left">
      <div class="slider_box">
        <label for="sld_cont">Contrast</label>
        <input class="slider" type="range" id="sld_cont" min="1" max="2" value="1" step="0.1">
      </div>
      <div class="slider_box">
        <label for="sld_bright">Brightness</label>
        <input class="slider" type="range" id="sld_bright" min="-65536" max="32768" value="0">
      </div>
      <div>
        <p><label for="rad_ch1">Sum</label>
          <input type="radio" id="rad_ch1" name="rad_channel" value="0" checked>
          <label for="rad_ch2">FES</label>
          <input type="radio" id="rad_ch2" name="rad_channel" value="1">
        </p>
      </div>
    </div>
    <div class="middle">
      <!--<div class="image">-->
      <canvas id="cvcanvas" width="512" height="512"></canvas>
      <!--</div>-->
    </div>
    <div class="right">
      <div class="slider_box">
        <label for="sld_speed">Scan Speed</label>
        <input class="slider" type="range" id="sld_speed" min="0" max="15">
      </div>
      <div class="slider_box">
        <label for="sld_focus">Focus (coarse)</label>
        <input class="slider" type="range" id="sld_focus" min="100" max="924">
      </div>
      <div class="slider_box">
        <label for="sld_focus_fine">Focus (fine)</label>
        <input class="slider" type="range" id="sld_focus_fine" min="-100" max="100">
      </div>
      <hr>
      <button type="button" id="btn_scan" style="width: 90%;">Scan</button>
    </div>
    <div class="footer">
      <div>
        <h2 id="h2_connected">Not Connected</h2>
      </div>
      <div class="meters">
        <p>
          <label for="met_ch1">Sum</label>
          <meter id="met_ch1" min="0" max="4096" low="1000"></meter>
        </p>
        <p><label for="met_ch2">FES</label>
          <meter id="met_ch2" min="0" max="4096" low="1000"></meter>
        </p>
      </div>
    </div>

</body>

</html>

Credits

Doctor Volt
19 projects • 127 followers

Comments