Lightsaber ignition pattern on a PB Pico

Recently went to the house of mouse (who now owns most of my childhood it seems) and built my own lightsaber along with my wife.

We’re wanting to display them as accent lights mounted on the wall, but didn’t want to modify them. I settled on building my own blade that stays mounted on the wall with external power, an external toggle button and the electronics completely self contained. The hilt just hangs off the twist lock base and has nothing to do with the operation.

I went with the PB pico because it fit in the tube perfectly and did not require recompiling it everytime to make changes to patterns as I will likely tweak it over time. It’s likely overpowered for my purpose but it ticked every box I had over WLED or other controllers.

I’ve got the blades built and looking beautiful (owning a small CnC is a wonderful thing) with an aluminum bar down the tube as a heatsink and mount for the LED’s and controller. The physical build is the easy part for me, I am not a coder however so I need some guidance.

Challenges I’m having;

  • Get a pattern to not loop. i.e. light pixels in order at a certain speed and then stay lit until the toggle is pressed again
  • Reversing the pattern. i.e ignition is all off and lit sequentially over X amount of time, then on toggle press, reverse that and turn them off in reverse order

I got some button code working for pad 0 on the back, so I can toggle a variable and I mangled the KITT example enough that I can turn it on and off with the toggle but can’t control the speed or direction yet.

I’m sure this is super easy for the experts here, but for me, it’s extremely daunting. Any snippets for having a single direction pattern that can be reversed on a button press would be super helpful.

Once I do final assembly, more than happy to share pics and designs.

Hey @renderman!

Sounds like a fun project. I wrote an example pattern for this. It’s copied below as well as uploaded to the Pattern Library.

Video

 

Code

/* 
SaberDeploy Tutorial

Deploy or retract a lightsaber across the length of a strip, toggled by a
momentary pushbutton.

Author: Jeff Vyduna (jeff@electromage.com)
v1 2023-09-17
License: MIT
*/
 
// vars here are exported so you can monitor them in the variable watcher
export var moving = 0     // 0 means it's paused at either end, 1 means animation is in progress
export var direction = -1 // 1 or -1. 1 means moving from index 0 to the end
export var fracLit = 0    // Fraction lit. 0 = beginning of animation, no pixels lit, 1 means finished, fully lit

export var lastButtonState = 0 // The toggle button's state last frame
export var buttonPushed = 0    // State of the button right now

// This UI trigger button will simulate a momentary hardware button. I use a toggle
// instead of a trigger because I want to show how you're only changing on the
// "press" transition (0 to 1), not the "release" 1 to 0 edge
export function toggleDeployOrRetract(pushed){
  buttonPushed = pushed
}

// `speed`` will be what percentage/fraction of the blade to progress 
// the animation each frame. e.g. if speed is .001, we progress .1% of 
// the overall length each frame. If the animation is running at 400 FPS, 
// 40% of the overall length is traveled in one second.
export var speed = .01 

// UI control to vary the speed of deploy/retract
export function sliderSpeed(_v) {
  speed = .001 + _v / 100 // Speeds between 0.001 and .011 
}

export function beforeRender(delta) {
  // Detect that the hardware button has just been pushed when it wasn't the frame before
  // For a real button, you may also want to implement a debounce delay - see forums
  // pushed = digitalRead(BUTTON_PIN) // Uncomment for use with a hardware button instead of UI toggle
  if (lastButtonState == 0 && buttonPushed) {
    // Flip the direction. 1 -> -1 -> 1 etc
    direction *= -1 
    
    // If it was done, start going. If it was already going one direction, just flip direction but keep moving
    moving = 1
  }
  lastButtonState = buttonPushed // Store this button state for the next frame's comparison
  
  // While in motion, we need to increase or decrease the fraction that should be lit up
  if (moving) fracLit += direction * speed
  
  // If we're past either end of the strip, stop moving, and set us right back at 0% or 100% lit
  if (fracLit > 1 || fracLit < 0) {
    moving = 0
    fracLit = clamp(fracLit, 0, 1)
  }
}

export function render(index) {
  // Light in red all pixel indices between the start and fracLit * pixelCount
  hsv(0, 1, index < fracLit * pixelCount)
}

1 Like

Holy crap. I did not expect a full tutorial and working code. This is almost perfect for my project. Given the size of the pico, I’m surprised there’s not more lightsaber builds with it

Going to add a color picker and play with the brightness and speed a bit for effect, but this is exactly what I was aiming to create. Thank you.

The only issues I foresee are the heat from the PB pico. Hopefully I can use the aluminum bar in the tube as a heatsink, but if it’s all sealed, it may still build up. I’ve already dropped the clock to the lowest setting. At least the bar would help spread the heat and dissipate through the length of the tube. At worst I can move it to an external enclosure with better airflow.

I’ll certainly post pictures shortly. I’ve got the parts built, just need to be assembled and to find a good diffuser material.

2 Likes

I had to define my input pin and set it to a pull down. I added ‘pushed’ to the watcher. When I close the input with 3.3v I see the pushed variable to switch from 0 to 1 and back to zero when it’s removed.

However, buttonPressed doesn’t change state, so it’s not picking up on the change.

I know I’m missing something small but it’s new enough and complicated enough I’m having trouble following. Might also want to update the tutorial with the pin variables ready to uncomment.


export var moving = 0     // 0 means it's paused at either end, 1 means animation is in progress
export var direction = -1 // 1 or -1. 1 means moving from index 0 to the end
export var fracLit = 0    // Fraction lit. 0 = beginning of animation, no pixels lit, 1 means finished, fully lit
export var pushed
export var lastButtonState = 0 // The toggle button's state last frame
export var buttonPushed = 0    // State of the button right now

var BUTTON_PIN = 0
pinMode(BUTTON_PIN, INPUT_PULLDOWN)

// This UI trigger button will simulate a momentary hardware button. I use a toggle
// instead of a trigger because I want to show how you're only changing on the
// "press" transition (0 to 1), not the "release" 1 to 0 edge
export function toggleDeployOrRetract(pushed){
  buttonPushed = pushed
}

// `speed`` will be what percentage/fraction of the blade to progress 
// the animation each frame. e.g. if speed is .001, we progress .1% of 
// the overall length each frame. If the animation is running at 400 FPS, 
// 40% of the overall length is traveled in one second.
export var speed = .01

// UI control to vary the speed of deploy/retract
export function sliderSpeed(_v) {
  speed = .001 + _v / 100 // Speeds between 0.001 and .011 
}

export function sliderHue(v) {
  Hue = v
}


export function beforeRender(delta) {
  // Detect that the hardware button has just been pushed when it wasn't the frame before
  // For a real button, you may also want to implement a debounce delay - see forums
  pushed = digitalRead(BUTTON_PIN) // Uncomment for use with a hardware button instead of UI toggle
  if (lastButtonState == 0 && buttonPushed) {
    //buttonPushed = pushed
    // Flip the direction. 1 -> -1 -> 1 etc
    direction *= -1 
    
    // If it was done, start going. If it was already going one direction, just flip direction but keep moving
    moving = 1
  }
  lastButtonState = buttonPushed // Store this button state for the next frame's comparison
  
  // While in motion, we need to increase or decrease the fraction that should be lit up
  if (moving) fracLit += direction * speed
  
  // If we're past either end of the strip, stop moving, and set us right back at 0% or 100% lit
  if (fracLit > 1 || fracLit < 0) {
    moving = 0
    fracLit = clamp(fracLit, 0, 1)
  }
}

export function render(index) {
  // Light in red all pixel indices between the start and fracLit * pixelCount
  hsv(Hue, 1, index < fracLit * pixelCount)
}

Oops! My mistake. Looks like my:

pushed = digitalRead(BUTTON_PIN)

should be

buttonPushed = digitalRead(BUTTON_PIN) (inside beforeRender)