Borg Cube for Scouts STEM Camp

Updated 30 November 2022

Introduction

What does one make for a Startrek-themed STEM camp for Scouts? Especially when given the Search and Rescue (SAR) activity base to run. 

Why this base you ask? Usually I run a radio direction-finding activity since I am an amateur radio operator. We hide a set of small transmitters operating in the amateur 2m band, and the youth search for these with small directional antennas (Yagis) and receivers.

So - first thought -  pick some Trekkie things that ‘could be searched for’, attach the transmitters, and voila - we have an activity!

What to search for ….. Some simple things first…. A lost Terran First Aid kit (pre-warp), … a rumour of a new archeological discovery – a cast of Baden Powell’s boot print!

Hmm … not very technical though yet. How about a ‘fallen to Earth’ BORG Cube?!? Now we are talking. Something semi-active but not actually able to assimilate our Scouts!

Ok - Some LEDs (Glowbit, Neopixels etc), a touch-sensitive bit maybe, something to sense as they approach it and to cause it to ‘become active’.

Thinks to self … How to make this work?... Ahah! A Raspberry Pico (my latest new toy to play with as I discover the differences between C and Python), a PiicoDev range sensor and a capacitive touch sensor, a selection of Glowbits/Neopixels, and something to manage the power. And how about an MP3 player? It’s not Borg unless it can produce “Resistance is futile”. Sounds like a workable project.

the borg cube in a tentthe borg cube in a tent

Table of Contents

Backstory

STARTREK: SURVIVAL is a STEM adventure experience at Mafeking Rover Park for all Scouting Members. This training program prepares Recruits for their first Missions as Starfleet Officers. Preparations provide skill development across areas including electrical system troubleshooting, diplomatic negotiations when meeting new cultures, geological surveys of hostile environments, and extended technical and interpersonal communications skills.

Welcome to Starfleet Academy
Due to a clumsy Klingon causing an incident at Starfleet Academy's primary Survival Training Facility and making it inoperable, the Survival Course has been relocated to Mafeking Rover Park.

The Park is a raw and untamed environment and is a most suitable location for Starfleet Recruits to have their resilience and resourcefulness tested.

The Design

Ok, without any more technical design at this early stage, it is now on to the practical side. How Big? What to make it out of? What to ‘decorate’ it with? What should it ‘do’?

  • How Big? - Depends on what materials I find that are in budget
    • What is the budget?
  • What to make it out of?
    • See the budget question above (sigh).
  • What to ‘decorate’ it with?
    • Luckily my son just bought a lot of IKEA stuff, and there are a LOT of chunky cardboard blocks on hand. And I have a friend with a great junk box - actually it’s his back shed. Decoration sorted!
  • What should it do? Add in the previous technical design ... not really, actually I should be into a bit more detailed planning now.
    • Run an ‘idling pattern’ on some LEDs,
    • Detect incoming Scouts
    • Become more active as they get closer
    • Play appropriate Borg statements if too close, or if touched 

The Build

The Physical Cube

After prowling around Bunnings, and keeping in mind the available budget, I found some sheets of 3mm MDF that were 1830mm x 915mm in size. Well that’s handy, just get the panels halved by the Bunnings woodwork yard team, and we can have a nominally 915mm x 915mm x 915mm cube. 

bunnings mdf sheet pricingbunnings mdf sheet pricing
mdf sheets used to build the borg cubemdf sheets used to build the borg cube

Add in some 64mm x 19mm dressed pine pieces for corner blocks etc. and it all goes together. 


With a good dollop of PVA glue, a dash of hot glue for instant grab, and (to be sure) some screws in stress points, we have a cube.

2 sides of the borg cube marked up and cut2 sides of the borg cube marked up and cut
Cutting the corner blocks to assemble the cubeCutting the corner blocks to assemble the cube
Blocks attached to the cubes side panelsBlocks attached to the cubes side panels
All side of the Borg cube laid outAll side of the Borg cube laid out
Attaching the corner blocksAttaching the corner blocks
All of the panels togetherAll of the panels together

And a slap of paint (matt black of course, but with some shiny stuff too), and the decorations.

Painting all of the decorative partsPainting all of the decorative parts
Assembling and decorating the cubeAssembling and decorating the cube
Painting the cube panelsPainting the cube panels
One of the decorated sides with cardboard and wires mountedOne of the decorated sides with cardboard and wires mounted

The Electronics

Concept testing

This was done using the PiicoDev Pico kit, a Raspberry Pi 3, Thonny,  and a lot of code bashing - especially as I am not a Python programmer! (Python, especially microPython, is oddly different to C).

Electronics test benchElectronics test bench

While testing the range sensor, I wondered if some way could be found to alter the brightness of the Glowbit Stick. The library only allowed setting the brightness value upon initialisation. 

The reason for this is that the design also includes a Neopixel string and a set of 3 watt high intensity LEDs, which I didn’t want to be too bright until the Scouts got in close. 


I poked about in the glowbit.py module and found these lines of code that allow subsequent alteration of the update rate values at any time after the initialisation, but there was not anything similar far dynamically changing the brightness.

## @brief Sets a new value for the GlowBit display's frames per second (FPS) limiter.
    #
    # \param rateLimitFPS An integer in units of frames per second.

    def updateRateLimitFPS(self, rateLimitFPS):
        self.rateLimit = rateLimitFPS

I had a chat on the forum and suggested a code function that might do the necessary dynamic change to the brightness value. Brenton came back very quickly with a ‘better’ function that does the brightness change and provided a link to the new library on Github. 


This is the new code function from Brenton.

## @brief Set a new brightness value
    #
    # \param brightness The relative brightness of the LEDs. Colours drawn to the internal buffer should be in the range [0,255] 
    # and the brightness parameter scales this value before drawing to the physical display. If brightness is an integer it 
    # should be in the range [0,255]. If brightness is floating point it is assumed to be in the range [0,1.0].
    def updateBrightness(self, brightness):
        if brightness <= 1.0 and isinstance(brightness, float):
            self.brightness = int(brightness*255)
        else:
            self.brightness = int(brightness)

(It was much better than my version as it addresses both integer values and float values of brightness. I only did integers in my first suggestion to ‘fix’ someone else's code)


While I was creating (and re-re-creating) the code for the Borg Cube during the concept test phase, the PiicoDev RGB Leds and OLED display were very useful to show the range values, and to indicate via LED selections and colours, what range setting was in effect, and what was/should be triggered. The Glowbit stick was moved from pin to pin to simulate the Neopixel string and the 3 watt LEDs, as well as the Glowbit effects. (This became an issue later as the various LED systems use different versions of the control chips)


This was also where I discovered the initialisation values to select different Pico PIO State Machines, to drive each of the separate LED arrays. You can’t use the same state machine for three different LED systems, the last one initialised runs, the other LEDs do nothing. It is obvious when you really think about it, but I interpreted the documentation for the Pico PIO systems to mean that the same PIO code could be re-used (but independently) through the three initialisations. Not so, if each initialisation uses the default state machine (sm0). 

Given that it is quite straightforward - and obvious -  to define a different pin for the LED data to appear on for each system, it should also be obvious to define and  use a different state machine to drive the separate arrays. After all they were doing different cycles and patterns. 


The correct way to drive three different LED systems. Each with their own state machine and output pin, but thanks to the Pico PIO magic, able to use the same instructions in the PIO code space.

default_rate = 40
# using modified glowbit library
# defaults pin 18, sm = 0
stick = glowbit2(numLEDS=stickLEDS, rateLimitFPS=default_rate)
# NB pin 19, and PIO sm 1
neo = glowbit2(numLEDS=neoLEDS, rateLimitFPS=default_rate, pin=19, sm=1)
# NB pin 20, and PIO sm 2
led3w = glowbit2(numLEDS=led3wLEDS, rateLimitFPS=default_rate, pin=20, sm=2)

The Actual Hardware Build

Regarding the Pico, RTFM, which as everyone knows is “Read The Fine Manual”! 


I realised early on that I would need a big PSU for the LEDs etc and would have an unfortunate set of voltage requirements, namely 12V, 5V, and 3V3.

Main power supply - a 12-volt Sealed Lead Acid (SLA) batteries (we are in the bush at a Scout camp - no mains power within cooee)

  • 12 volts - from battery
    • Power amplifier for the audio
    • Input to a 12v - 5v Buck converter
  • 5 volts - from the buck converter
    • Pico
    • Glowbits
    • Neopixels
    • 3-watt RGB LEDs
    • High voltage side of a level converter for the LEDs
  • 3.3 volts - from 3v3 output of Pico
    • MP3 player (so I can directly connect the UART data from the Pico)
    • Low voltage side of the level converter

Also, regarding the power, is the need to be able to connect the programmer Raspberry 3 to the Pico, while the big PSU is in use also. Without the smoke getting out of either one.


Solution: RTFM (https://datasheets.raspberrypi.com/pico/pico-datasheet.pdf - page Section 4.5 on page 19) and wire up the power to the Pico with a Schottky diode in line to the VSYS pin. And build in a good solid common earth point for everything to connect to.

Result: can safely power up from the battery, and can connect/disconnect the Raspberry Pi programmer, without disturbing the Pico (or killing the Raspberry Pi).

Putting it all together

The Pico board

This was based on the PiicoDev baseplate - seemed sensible since the major components are all from the same system. The Pico, the capacitive sensor and the MP3 player mounted easily, except the MP3 player has very small mounting holes. After checking I found that the copper on the PCB around the holes was not connected to anything, so I used some nylon washers under the pcb to provide necessary clearance, some M2 bolts, and more nylon washers on top. All good, and firm enough to allow plugging in a USB-C cable for uploading the Borgisms.


The tag strip is the foundation for the common 5V rail and the very necessary common Ground rail.  The tags on each side are linked with heavy copper wire.

Electronics used in the Borg cubeElectronics used in the Borg cube

Already connected is the PiicoDev cable to the Capacitive sensor, the UART tx/rx pair to the MP3 player and the command wires that will go to the LEDs. The orange wire is about to be cut and joined into a 3v3 power feed from the Pico to the MP3 player and to the Level shifter.

Assembling all of the electronic componentsAssembling all of the electronic components

 

Level shifter now mounted (with v. strong double sided tape) to the base, 3v3 wire done, and the important schottky diode protected 5v (and ground) connection made to the Pico. The 12v - 5v buck converter is now wired to the tag strip, and the LED level shifter power and command wires in place. The tag strip is also the start point for the power runs to the 3 LED systems

The Buck Converter

Perparing the heatsink for mounting the buck converterPerparing the heatsink for mounting the buck converter
The buck converter mounted to the heatsinkThe buck converter mounted to the heatsink

The heatsink / mount for the buck converter was actually a heatsink for a Raspberry Pi 4 !

Drill a few holes, carefully cut out the necessary part of the protective plastic on the sticky pad, and add a couple of cable ties (belt and braces).

Ziptied buck converter mounted to heatsinkZiptied buck converter mounted to heatsink
The front of the heatsinkThe front of the heatsink

The finished assembly - actually powered up, just to check that my meter hadn’t lied. There was no electrical contact between the converter and the heatsink.

The buck converter assembly mounted to the side of the Borg cubeThe buck converter assembly mounted to the side of the Borg cube

And the final mounting to the Borg Cube - a few long bolts, some stacks of washers for clearance, and it’s done. Noting that by being on the back of the main panel, there is a nice vertical airflow path over the fins.

And of course, add some locknuts.

The Rangefinder

As the rangefinder has to be somewhat exposed, and we were on a Scout camp where the weather is ‘interesting’ and where young people actively investigate EVERYTHING, I thought it necessary to devise a method of weather / touch proofing the sensor.

After another quick forum chat, I found that the sensor could be mounted very close to a clear window, without falsely sensing the window surface.

More nylon washers, M2 bolts (I had run out of my small stock of nylon bolts), and a scrap of perspex.

Parts required to mount the distance sensorParts required to mount the distance sensor
Distance sensor mounted to acrylicDistance sensor mounted to acrylic

Simple and beautiful! All ready to bolt into the Borg Cube.

The Cube

Template for soldering the Glowbit sticksTemplate for soldering the Glowbit sticks
The Glowbit sticks soldered togetherThe Glowbit sticks soldered together

Connect the Sticks  - power all linked in the middle, and data out to data in, data out to date in, data out to data in. These will be hot glued to the front panel.

 

 

Before twisting the Glowbit wireBefore twisting the Glowbit wire
Twisting Glowbit wireTwisting Glowbit wire

A quick twist to make a wiring loom … yes it is the Stick loom.

 

Sometimes it felt like I was living inside the Cube …

Murray living inside the cubeMurray living inside the cube
Another photo of all of the parts inside of the cubeAnother photo of all of the parts inside of the cube

Hmmm.. Wings as a heat sink on a 3 watt LED …. And yes it is untidy with all the external connections and nearby wiring chains from the neopixels, and cables off to the speakers and amplifier. But it is done! Quick, turn off the lights and close the Cube. (Hmmm .. Must have been fiddling with the program - USB cable still connected)

Heatsink for the 3W LEDsHeatsink for the 3W LEDs
All of the electronics and cables in an untidy bundleAll of the electronics and cables in an untidy bundle

Code

Code Experiments


Using the second core
After trying to run parts of the code in the second core as a form of co-processing, such that the Borg could appear to be doing two things at the same time, I discovered the Pico wasn’t  really able to do this. I ran into unexplainable lockups, odd crashes, and some actions that - while requested/scheduled - didn’t run.

It appears to be too early in the micro-python development cycle to obtain solid, long term twin core operations. I believe it is something to do with the garbage collection routines.

But it is possible to launch a short process on Core 1, such that it runs to completion, and terminates. This (so far) has been stable and repeatable. I have used this mode to run short ‘high energy’ Borg activities as needed.

The UART, a control ‘array’, and string concatenation
The UART on the Pico is used to control the DFRobot MP3 Player (DFR0768) for the ‘Borg-ish’ announcements. I have a trigger system based on the PiicoDev Capacitive Touch Sensor (CE07816), and a random audio effect, selected from the 7 available tracks. Each track has a unique duration, which has to be used to lock-out retriggering so that each track can play to completion.

The duration is stored in a python list, the choice of track number is random, and this number has to be concatenated into the control string – i.e. as a text character -- and also used in the lockout timer – as a numeric value that is the index for the duration entry.


This is what I developed (after a lot of Googling for the simplest integer-to-ascii conversion)

from machine import Pin, UART
from PiicoDev_Unified import sleep_ms
import random

uart - UART(0, baudrate=115200, tx=Pin(12), rx=Pin(13))

durations = [0, 17, 10, 2, 2, 6, 2, 3 ]    # runtime + 1 second, no track 0

uart.write("AT\r\n")        # wakeup
sleep_ms(100)
uart.write("AT+AMP=ON\r\n")
sleep_ms(100)
uart.write("AT+PLAYMODE=3\r\n")    # single track mode

# play 10 random noises
for i in range(10):
    play = random.randint(1,7)          # pick a track 1 - 7 
    print("playing track ", play)       # show me
    c_num = "% s" % play                #   / itoa()
    s = "AT+PLAYNUM=" + c_num + "\r\n"  #  / build DFRobot command
    uart.write(s)                       # / and send it
    sleep_ms(durations[play]*1000)      # zzzzz

Bottom line: works as planned.  (p.s. The DFRobot MP3 Player (DFR0768) has both its onboard amplifier which can drive 8 ohm speakers directly which is great for testing, and the actual DAC outputs can be fed straight into an external power amplifier.)

“Programming” Pins
During development I found it useful to be able to completely disable one or two of the LED systems to allow me to concentrate on creating the appropriate mode cycles and patterns. And I could also bypass the range sensor - sometimes it was removed while making the mounting for it, and sometimes it was easier to develop code with a stable ‘distance’ value.

#
# setup the 'programming' pins
# default 1, if grounded is 0
#
# allow up to 2 grounded (using jumpers from Ground in phys pin 3 and/or phys pin 8
#
#   P2  P3  P4  P5
#	1   1   1   1	normal operation
#	0   x   x   x	No Glowbit
#	x   0   x   x	No Neopixels (TBD)
#	x   x   0   x	No 3watt LEDs
#	x   x   x   0	Bypass range sensor
#
P2 = Pin(2, Pin.IN, Pin.PULL_UP)	 
P3 = Pin(3, Pin.IN, Pin.PULL_UP)
P4 = Pin(4, Pin.IN, Pin.PULL_UP)
P5 = Pin(5, Pin.IN, Pin.PULL_UP)

And lines like this to initialise / enable / disable functions appropriately throughout the code.

if P5.value() == 1:                      # using PiicoDev Ldistance sensor
	distSensor = PiicoDev_VL53L1X()    # initialise distance sensor
#
# lines omitted
#
 if P2.value() == 1:   # initialise Glowbit Stick
	stick = glowbit2.stick(numLEDs = stickLEDS, rateLimitFPS=default_rate)   
#
# lines omitted
#
	elif level == 3:
    	# < 2000mm range
    	    g_stop = False
    	    if P2.value() == 1:        # P2 not grounded
              stick_idle(level,100)  # call stick fn

The final code (snippets thereof …)
Full listing is on Github  https://github.com/mjtlx/BorgCube 


Note - the Github listing still includes the OLED display code and a few print statements - these were mostly removed in the final production code.

Start here - header and inclusions
Just the usual stuff to make sure that all the dots will join up.

Note the use of the glowbit2 library - I pulled down the new code  provided by Brenton, and renamed it to keep things separated in case I needed to fall back to the original.

##########################
#
# BORG CUBE
#
##########################
#
# STEM camp 1-3 July 2022
#
##########################
#
# Murray T
#
##########################

# PiicoDev Libs
from PiicoDev_Unified import sleep_ms # Cross-platform compatible sleep function
#from PiicoDev_RGB import PiicoDev_RGB, wheel   # RGB Leds
from PiicoDev_VL53L1X import PiicoDev_VL53L1X   # distance sensor
from PiicoDev_SSD1306 import *             	# OLED display
from PiicoDev_CAP1203 import PiicoDev_CAP1203   # touch sensor
# import glowbit                             	# Glowbit stick
import glowbit2                             	# Glowbit Stick with the new brightness command (and fixed rainbowDemo ;-)

# micropython libs
import _thread                             	# multi-core support
import random                              	# random fns
from machine import Pin, I2C, UART, Timer  	# Access to the GPIO pins, and the base i2c system
import sys                                 	# sys.exit()
import array

Globals
Probably NOT the right/best way to do this but I am still learning Python idioms. Anything with a g_ prefix is a GLOBAL flag value that is used to manage / prevent various bits of code overrunning each other - especially between Core 0 and Core 1 - but also in general operations.

#
# cross-core link variable
#
g_alert = False	# True if glowbits running on Core 1
g_timer = False	# true if timer running
g_stop = False

# odd variables
counter = 0
rand = 0
rand2 = 0

The main thread
This is the overall controller routine.

Check for distance sensor override on programming pin P5
Get a distance and a touch sensor value
Bump a counter and get some random values
Call an appropriate LED sequence based on distance - note these will check the g_alert flag and may return immediately if Core 1 is running a blocking sequence
Also checking the g_alert flag, touch[1] and touch[3] may actually launch the Core 1 process with one of the Glowbit Stick demo sequences (why re-invent the wheel?)
Checking the g_timer flag, touch[2] may launch a random Borg vocalisation from the MP3 player. The timer is needed to stop overrunning the player. (Here is where I wish for the C sprintf() command to make the UART control string)

#
# CORE 0 process
#
def main_thread():
	global g_alert, g_timer, counter, rand, rand2
   
	if P5.value() == 1:
    	    dist = distSensor.read() # read the distance in millimetres
	else:
    	    dist = 3000
   	 
	touch = touchSensor.read()
	counter += 1
	if counter == 100:
    	    rand = random.randint(0,15)
    	    counter = 0
	rand2 = random.randint(0,15)
    
	# distance sensor range 0 - 4000 mm (nominal)
	# 0 ---1000---1500---2000---2500---3000----inf
	if dist < 1000:
    	    mode(1)
	elif dist < 1500 :
    	    mode(2)
	elif dist < 2000:
    	    mode(3)
	elif dist < 2500:
    	    mode(4)
	elif dist < 3000:
    	    mode(5)
	else:
    	    mode(6)

	if touch[1] == 1:
    	    if g_alert == False:
        	  g_alert = True
        	  _thread.start_new_thread(pulse_thread, (2,))  # chaos flash Glowbits
	elif touch[2] == 1:
    	    if g_timer == False:
        	  g_timer = True
        	  play = random.randint(1,7)      	 # pick a track
                                            	 # build the play command
        	  c_num = "% s" % play            	 #   /  itoa()
        	  s = "AT+PLAYNUM=" + c_num + "\r\n" #  /  concatenate
        	  uart.write(s)                   	 # /	and send it
        	  # one shot firing after durations[play] seconds
              #   to allow MP3 player to finish
        	  tim.init(mode=Timer.ONE_SHOT, period=durations[play]*1000,    callback=timerdone)
	elif touch[3] == 1:
    	    if g_alert == False:
        	  g_alert = True
        	  _thread.start_new_thread(pulse_thread, (3,))

The mode controller
This is called by the main thread after determining the ‘victims’ distance from the Borg Cube.

Each level triggers various cycles and intensity settings on the LED systems. (c.f. the following Stick code)

The g_stop flag is used to block any execution of the neo_idle() or led3w_idle() functions.

There is a lot of testing of the programming pins here to lockout each of the LED systems independently.

This snip has all the programming pin code

def mode(level):
	global g_stop
	if level == 1:
    	    g_stop = False
    	    # they are really close < 1000mm
    	    if P4.value() == 1:
              led3w_cycle1()
    	    if P3.value() == 1:
              neo_idle(255)
	elif level == 2:
    	    g_stop = False
    	    # closer < 1500mm
    	    if P2.value() == 1:
              stick_two()
    	    if P3.value() == 1:
              neo_idle(127)
    	    if P4.value() == 1:
              led3w_cycle2()
    	    if P2.value() == 0 and P3.value() == 0 and P4.value() == 0:
              pass
# code omitted

This is what it looks like without the programming pin code - much simpler ;-)

Parameter values are brightness levels ( 0 - 255 ), noting that there is also a default value set in the function definitions. If a LED function is NOT called within a mode(level), that system does nothing when the Scout is in that distance range. The Glowbit Sticks also take the level parameter into the stick_idle() function.

#
# mode scripts
#
def mode(level):
	global g_stop
	if level == 1:
    	    g_stop = False
    	    # they are really close < 1000mm
    	    led3w_cycle1()
    	    neo_idle(255)
	elif level == 2:
    	    g_stop = False
    	    # closer < 1500mm
    	    stick_two()
    	    neo_idle(127)
    	    led3w_cycle2()
	elif level == 3:
    	# < 2000mm range
    	    g_stop = False
          stick_idle(level,100)
          neo_idle(127)
          led3w_idle(50)
          led3w_once = True
	elif level == 4:
    	# <  2500mm range
    	    g_stop = False
          stick_idle(level,100)
          neo_idle(50)
          led3w_idle()
          led3w_once = True
    	    g_timer = False     #force reset
	elif level == 5:
    	    # < 3000mm
    	    g_stop = False
    	    stick_idle(level,50)
     	    neo_idle()
     	    led3w_off()
     	    led3w_once = True
    	    g_timer = False 	#force reset
	elif level == 6:
    	    # > 3000mm range
    	    g_stop = True
     	    stick_idle()        # uses default_brightness
     	    neo_idle()
    	    led3w_off()
          led3w_once = True
    	    g_timer = False 	# force reset
	else:
    	    # default same as level 6
     	    g_stop = True
     	    stick_idle()        # uses default_brightness
    	    neo_idle()
    	    led3w_off()
          led3w_once = True
    	    g_timer = False 	# force reset

The Glowbit Stick functions
This is typical code for all 3 LED systems. Importantly, there is only one call to the library <object>.pixelShow() function within the update_stick() definition. The ‘action’ functions merely update the local array with the required values, then call the update_stick() function. This provides a quicker update to the LEDs rather than calling updates in an ad hoc manner within each lower function, especially as there may be several changes to the array before the actual update occurs..

Each of the LED systems have some ‘static’ variables. Static in that they are external to the function(s) that use them, and that hold value between function calls. This is a useful C programming concept, and there is probably a way to do this in Python that I haven't found yet.


The stick_two() function attempts to launch a specific Core 1 cycle if g_alert is NOT True, i.e. there is not already a Core 1 cycle executing.

 

In the stick_idle() function, the static counter value is tested for equality to 100, and a random colour sequence is added to the string of Sticks.


Programmer note:  even in the production code the Sticks provide a minimal real time ‘debugging’ capability, in that - while in the longer distance modes - the ‘bottom’ 4 LEDs act as a simple distance display by turning on 1, 2, or 4 LEDs as the distance reduces.

###################################
#
# STICK MODES
#
###################################
# "static" variables
stick_idle_counter = 0
stick_ar = array.array("I", [0 for _ in range(stickLEDS)])

def update_stick():
	global stick_ar
	for j in range(int(len(stick_ar))):
    	    stick.pixelSet(j, stick_ar[j])
	stick.pixelsShow()
   	 
def stick_two():
	global g_alert
	if g_alert == False:
    	    g_alert = True
    	_thread.start_new_thread(pulse_thread, (1,))   # pulse sweep Glowbits

def stick_idle(mm=6, bb=default_brightness):
	global rand, stick_idle_counter, stick_ar
	global g_alert
	if g_alert == True:
    	    return
	stick.updateBrightness(bb)
	stick_idle_counter += 1
	if stick_idle_counter == 100:
    	    stick_rand_string(rand)
    	    stick_idle_counter = 0
	if mm == 5:
    	    stick_ar[0] = stick.white()
    	    stick_ar[1] = stick.black()
	elif mm == 4:
    	    stick_ar[0] = stick.white()
    	    stick_ar[1] = stick.white()
    	    stick_ar[2] = stick.black()
	elif mm == 3:
    	    stick_ar[0] = stick.white()
    	    stick_ar[1] = stick.white()
    	    stick_ar[2] = stick.white()
    	    stick_ar[3] = stick.white()
    
	update_stick()

The stick_rand_string() function below generates the random colours for the Glowbit Sticks, and based on the received random parameter value, distributes the new colour across the stick_ar array with the selected positioning depending on various modulo division results, or finally a prime number test.

#
# this fn loads the stick_ar array, but does NOT update the actual LEDs
#
def stick_rand_string(n):
	global stick_ar
	new_colour = rotate_colours()
	if n == 0:          	# all same
    	    for j in range(int(len(stick_ar))):
        	  stick_ar[j] = new_colour
	elif (n % 2) == 0:  	# div by 2
    	    for j in range(int(len(stick_ar))):
        	  if (j % 2) == 0:
            	stick_ar[j] = new_colour
	elif (n % 3) == 0:  	# div by 3
    	    for j in range(int(len(stick_ar))):
        	  if (j % 3) == 0:
            	stick_ar[j] = new_colour
	elif (n % 4) == 0:  	# div by 5
    	     for j in range(int(len(stick_ar))):
        	  if (j % 4) == 0:
            	stick_ar[j] = new_colour
	elif n in {1,7,11,13}:  # prime numbers not otherwise handled ;-(
    	    for j in range(int(len(stick_ar))):
        	  if (j % 2) == 0:
            	stick_ar[n % (j+1)] = new_colour

And the rotate_colours() function defines a limited set of colours, and randomly selects one to be used. This function uses actual hex values for the colours instead of the library defined colours.


I was trying to generate colours based on ratio mixes of the available primaries, i.e. chartreuse is supposed to be 2 parts yellow and one part blue. But to generate yellow we need equal parts of red and green, so trying to determine the correct balance was ‘interesting’.

def rotate_colours():
 # this will step through a selection of colours and return the next in sequence
 # for the rand_string fn to use
	black = 0x000000
	red = 0xFF0000
	blue = 0x0000FF
	green = 0x00FF00
	# secondaries
	yellow_s = 0xFFFF00
	magenta_s = 0xFF00FF
	cyan_s = 0x00FFFF
	#proportions
	magenta = 0xFF0077   # 2R:1B
	violet = 0xFF00FF	# 1R:1B
	purple = 0x7700FF	# 1R:2B
	teal = 0x7777FF  	# 1Y:2B
	chartreuse = 0x33FF77  # 2Y:1B

	colours = [black, red, green, blue, yellow_s, magenta_s, cyan_s, magenta, violet, purple, teal, chartreuse, ]
# this selects from specific set defined above
	return colours[random.randint(0, (len(colours)-1))]

sSpecific LED functions
The other LED systems have similar basic functions to the Stick arrays, but also have some extra capabilities to cover the different nature of the devices. And in  the full code listing there are many developed, tested, but unused experiments for various colour transitions, random sparkles etc.


neo_off() - sets all the neopixels to black
led3w_off() - sets all the 3watt LEDs to black
neo_rand_string() - calls the neo_random_colour() function below with a 1 parameter, to limit the returned colour value to the blue-green range of the Glowbit library wheel() function.
led3w_cycle1() and led3w_cycle2() - attempt to create a slow bouncing colour sweep, controlled by static variables re the colour and direction of the actions. This appeared to work in test on the Glowbit Stick, but could not be shown in production due to the control issues discussed below.
(nu) slowroll() - fill an array with a slowly changing colour wheel cycle 
(nu) stick_slowroll2() - slowly shift a colour wheel value along a string, with occasional flashes 
(nu) stick_flash_restore() - flash the complete array white and restore it 
(nu) stick_flash2(): - white flash random pixels and restore 

# this just picks a random colour
# optionally limited to a subset of possibles
def neo_random_colour(ss=0):
	if ss == 0: 	# default any colour
    	    colour = neo.wheel(random.randint(0,255))
	elif ss == 1:   ## blue-green limited for neoLEDS
    	    colour = neo.wheel(random.randint(55,190))
	else:        	# default - anything else
    	    colour = neo.wheel(random.randint(0,255))
    
	if random.randint(0,63) == 42:   # occasionally set it black
    	    colour = neo.black()
#	print("neo_random_colour is ", hex(colour))
	return colour

The Core 1 process
This is the process that was launched on the second core, to execute a cycle of a selected Glowbit Library demo function, then terminate itself until next time.


Using Core 1 in this fashion avoided the issues with micro-python threading, and did not (as far as I could tell after days of operation) require any garbage collection, nor cause any system crash.


The g_alert flag is used to globally signal that Core 1 has control of the Glowbit Sticks, thus blocking any other access. The id parameter only selects the demo function to be run.


Developing this code is what caused me to ask if an updateBrightness() function could be added to the Glowbit library (thanks Brenton), and also is where I discovered that the rainbowDemo() library function for the Sticks was hardcoded to only use 8 LEDs. 

#
# CORE 1 process
# - started as needed by CORE 0 process
# - runs once and exits
#
def pulse_thread(id):
	global g_alert, rand2

	if id == 1:
    	    stick.updateRateLimitFPS(80)
    	    stick.updateBrightness(1.0)
    	    stick.pulseDemo()
            stick.pixelsFill(0)
            stick.pixelsShow()
    	    stick.updateRateLimitFPS(default_rate)
    	    stick.updateBrightness(default_brightness)
	elif id == 2:
    	    stick.updateRateLimitFPS(default_rate)
    	    stick.updateBrightness(1.0)
    	    stick.chaos()
    	    stick.updateBrightness(default_brightness)
	elif id == 3:
    	    stick.updateRateLimitFPS(default_rate)
    	    stick.rainbowDemo(2)
            stick.pixelsFill(0)
            stick.pixelsShow()
	else:
    	    # bad id - do nothing
    	    g_alert = False
	# cleanup and exit thread
	g_alert = False
	return

Here is the rainbowDemo() function from the Glowbit library with my modification to cover all LEDs in the Stick array

## @brief Uses the colourMapRainbow() colour map to display a colourful animation
def rainbowDemo(self, iters = 5):
	while iters > 0:
    	for offset in range(33):
#mjt        for i in range(8):
            for i in range(self.numLEDS):
            	self.pixelSet(i, self.colourMapRainbow(i,offset, offset+32))
        	self.pixelsShow()
    	iters -= 1

And Finally
Of course.

#####################################
#####################################
##  START HERE
#####################################
#####################################
#
# Launch Core 0 process
#
print("Launch!")
if P2.value() == 1:
	stick.pixelsFill(0)
        stick.pixelsShow()
if P3.value() == 1:
	neo.pixelsFill(0)
        sleep_ms(200)
        neo.pixelsShow()
if P4.value() == 1:
	led3w.pixelsFill(0)
        sleep_ms(200)
        led3w.pixelsShow()

sleep_ms(2000)

while True:
	main_thread()

The final guts of it all
The “As Built” photo - a picture is worth 1024 words at least!

All of the electronics mounted to a panel on the cubeAll of the electronics mounted to a panel on the cube

A comment from the event team blog: 

Nicola
  - The borg are held together with hot glue and duct tape? I KNEW it.
Murray
  - ???????? shhh the secret is ours
 
And a few words anyway: A design decision based on the planned use - specifically the approach path - and on the available budgets of time and money, was to mount all of the electronics and the visible effects on the ‘front’ panel. I was planning on extending the neopixel string to place pixels 31 - 40 on the left panel and 41 - 50 on the right panel, but due to the control issue this was cut. (The connection and the crossed out markup can be seen on the image above). Speakers were placed inside the Borg cube, directly driven from the MP3 player, and a small 12V amplifier connected to the MP3 DAC outputs drove the ‘surround sound’ speakers hidden in the bush.


Future directions - Resistance is Futile!
If the LED control issue is fixed, then the ‘grand scheme’ might be to clone the Pico / sensors / Glowbit Sticks / Neopixels / 3 Watt LEDs system onto the left, right, rear and top panels, and create a Master Borg - sorry Master Pico - to coordinate these and manage the MP3 player (players ?)

Parts used

Issues

  • A set of jumper wires that were (believe it or not) … RUSTY !
    • They did not push onto the header pins
    • They did not go easily into the sockets
    • Some of them were unable to pass 3V3 power
    • This was the cause of a wasted morning as I tried to get the MP3 player working. Finally solved by ripping out the wiring, discovering the offending wire (the 3V3 power wire of course) to be one of the rusty ones, replacing it, and Ahah - Things work!!
  • Time !!!
  • Postal and Courier deliveries … or not … or When?!?
  • When express post isn’t
  • Thinking in C, programming in Micropython – brain hertz ;-(
  • Discovering that the Capacitive sensor is affected by humidity (I think). Anyway what behaved nicely in the Scout Hall during the build and calibrate tests, was overly sensitive in the much damper, and colder, event site. So much so that I had to quickly make up a much smaller sense panel to prevent constant Borgian declarations!
  • Discovered in tests that the Laser distance sensor seems to need to ‘see’ something near its maximum range, otherwise it was generating very jittery values. Needs more definitive testing and a lot of controls in place.
  • Neopixels!!!!!!! Testing the code sequences with a Glowbit Stick worked, however attaching the neopixel string and the 3 watt neopixel LEDS resulted in seriously locked up pixels and / or pixels not responding to command updates. The Glowbits, neopixels and the 3 watt LEDs are all driven with a 5 volt supply voltage, and required a 3v3 - 5v level shifter for nominally correct drive. (refer to the Adafruit uberguide).  For details of the ongoing analysis see the Forum thread captured below.

Events

The Borg cube (and several other items) were made to support the Red Shirt Rescue activity base.

Below is the full description as provided to the Scouts about the activity. Once onsite at the activity base, they were shown how to use the Yagi ‘sniffers’ to find the transmitters, and coached with some strategies to use. I also set up some ‘red herrings’ in the search area, which added to the challenge (and provided me with some amusement).

Red Shirt Rescue

Practice navigation skills to locate 'lost' or 'abandoned' equipment in a search and rescue simulation. Using Radio Direction Finding technologies and mapping skills, Scouts will need to localise position indicating beacons, determine and plot the beacon positions, and navigate through unforgiving terrain to find the items.


Narrative

Welcome to Starfleet Academy's Search and Rescue (SAR) training module. Upon completion of this course you'll be ready to beam down to an alien planet, and use your skills to locate and possibly retrieve injured personnel, valuable artefacts, or dangerous materials. As you may need to operate within pre-warp societies, hence the need to develop the most basic skill levels as well as introducing higher level post-warp technologies. Usually the SAR component is completed in a state-of-the-art training facility, featuring anti-gravity modules, quantum computing terminals, immersive holodeck weapons training, and full-featured remote environment equipment support kits. However, it has been determined that the use of a more realistic and harsh environmental area with limited facilities operating at a reduced safety level provides better trained Starfleet team members.

Briefing

Starfleet officers and Crew are often sent of Missions of Recovery, due to equipment being lost on a previous mission, or due to new information received that provides a level of guidance to possible historical or useful discoveries. Various location methods can be used depending on the equipment or information being acted upon. In this training exercise it has been assumed that all items were fitted with Starfleet Mark 3 (post-warp) beacons. However, all the beacons have been found to be in a Level 1 civilisation area (pre-warp), so while you may use the Mark 3 equipment to assist in mapping the beacon locations, this equipment CANNOT be taken into the Level 1 zone. (Refer to the Prime Directive). It is assumed that Starfleet cadets have completed the required Level 1 mapping and navigation exercises before attempting this training module. 

Instructions

As a Patrol:

Plot a bearing to each of the 6 beacons from reference location A, move to reference location B, and plot a crossing bearing to each of the 6 beacons
 Using the generated map locations, enter the search zone, safely navigate to, and locate the objects
(Scan) record each of the object ID codes of the designated 6 items.
 Return to Mission Base to verify your finds.
 NB Some other objects in the search zone are dangerous or prohibited access - these will impact your final result

Activity Background

This activity is a scavenger hunt style wide game. Based on the clues determined (cross bearing locations), and actually finding the targets will provide points contributing to the success of each Patrol.

So what did they have to find?

  1. The Borg cube - approach carefully as it has been found by the pre-warp era Military, and is being studied in a restricted area. And somehow there are Klingons involved too - don’t ask questions! ( Luckily the cube could be disassembled to a somewhat “flatpack IKEA-ish bundle” for transport to the Scout camp - 144 km from Melbourne - all the rest of the stuff below, the Yagis and a command tent, and personal camping gear)
  2. A post-warp First Aid kit. Contains advanced equipment - carefully locked up so it could not be opened by the unwary.
    An illegal fuel cell dump. Caution Radioactive! (amazing what you can do with big blue plastic barrels, some borax slime, food colouring, and flickering blue/green camping headband lights)
  3. A ‘spatially relocated’ pallet of Chateau Picard wine delivery. Unfortunately this had been stolen by Quark, and pushed through the Bajoran Wormhole to the Gamma Quadrant. (Scouts had to cross a boundary via a slackline ‘bridge’ to reach the item)
  4. A Stasis transport container holding an alien body (alive but in stasis). Some more flickery LEDS and a status display (2 Glowbit 8x8 matrix arrays). The odd things that can be found in a Scout Group Q-Store - an ex-egyptian themed sarcophagus was repurposed into a Stasis transport container.
  5. A newly discovered archeological item - a cast of the boot print of Colonel Robert Baden Powell, founder of Scouting, and leader of the defence of Mafeking in 1900. 

 

What did the Scouts use to locate the items hidden in over a hectare or two of bush scrub? With wombat holes! Big wombat holes. And kangaroos, and echidnas. And multiple patrols in the same area.

The Best Bit

The final review comments from Scouts (S) and Leaders (L)


I liked looking for the different markers through the bush, I also liked using the technology to find them - S

I liked using the radio antenna to find places of interest around the area. - S

Great activity, repeat it again. - S + L

I loved using the metal thingo to find all of the different items, and all of their uniqueness and creativity was really impressive as well as cool - S

I liked how you had to find stuff, that was good  - S

Watching Scouts navigate around the scrub and wombat holes while tracking the transmitters. - L

Fooling Scouts with visible (but wrong) markers. - L

Watching a patrol 'straight-line' through all the scrub to the transmitters ( all we could see was a hand holding the tracker antenna high up overhead - no Scouts visible ). - L

Fun! - S + L

Have a question? Ask the Author of this guide today!

Please enter minimum 20 characters

Your comment will be posted (automatically) on our Support Forum which is publicly accessible. Don't enter private information, such as your phone number.

Expect a quick reply during business hours, many of us check-in over the weekend as well.

Comments


Loading...
Feedback

Please continue if you would like to leave feedback for any of these topics:

  • Website features/issues
  • Content errors/improvements
  • Missing products/categories
  • Product assignments to categories
  • Search results relevance

For all other inquiries (orders status, stock levels, etc), please contact our support team for quick assistance.

Note: click continue and a draft email will be opened to edit. If you don't have an email client on your device, then send a message via the chat icon on the bottom left of our website.

Makers love reviews as much as you do, please follow this link to review the products you have purchased.