Creating a Live OSHWA Certification Map

Update 2/11/24: The original version of this map didn’t fully work because the OSHWA api paginates the results it provides. The original version did not take that into account, so only displayed the first 100 certifications. The newly updated version downloads all of the entries before creating the map, so it is actually complete. It also includes a count of the total entries and number of countries with certifications in the header. The new code still more or less works the same way, although there is now an initial step to loop through the api until everything is downloaded. You can find the updated version in the repo referenced below. Also, the API key expires every 100 days, so if you are reading this more than 100 days from 2/11/24 and the map is not loading, that may be the problem. There are instructions for getting your own API key in the repo.

With the recent release of the live OSHWA Open Source Hardware Certification API, I wanted to build a map that tracked the live distribution of certified open source hardware across the globe. You can see the map here. You can see the code here. This post explains how it all works.

The map I made is called a choropleth. It uses color intensity to compare the number of pieces of certified hardware between countries. I created it using leaflet.js. I chose leaflet mostly because I learned how to use it on a Coding Train tutorial. That tutorial also taught me how to access APIs, which is another piece of this project.

At a high level, creating the map involves a few steps:

  1. Download information from the OSHWA Open Source Hardware Certification API to get up to date information about all of the certified hardware.

  2. Add the number of pieces of certified hardware to the GeoJSON file. The GeoJSON file is the file that has the geometries for all of the countries so that they can be drawn on the map (and colored appropriately).

  3. Load the map.

  4. Add a layer on top of the map representing the number of pieces of certified hardware.

Downloading the Current Registration Information

The first thing you need to do for the map is get the current information about OSHWA certifications. Once you get an API key, you can look to the API documentation for example code that shows you how to access specific information in specific languages. For this map we are just going to get all of the OSHWA certified hardware projects.

In order to get the data, you can just copy the example code from the documentation. There are two things to keep in mind when you do this.

First, in order to actually get the data from the API into an object that is useful for javascript you need to parse it into a variable:

var data = JSON.parse(this.responseText);

Second, everything else in this code will happen in the place held by the console.log(this.responseText); in the example code. I think doing this causes everything to wait until the API data has been downloaded but I could be very wrong about that.

Unite the JSONs

Once you have the API data you need to combine it with the GeoJSON for the world. The first step is to create an object that contains the certified hardware count per country. That function is called in the var country_counter = countCountry(data); line. That calls the countCountry() function:

function countCountry(input_json) {
  //create a temporary dictionary in the function
  function_country_counter = {};
  //loop through all of the entries
  for (var i = 0; i < input_json.length; i++)
  {
    console.log(input_json[i]["country"]);
    var country = input_json[i]["country"];

    //this checks to see if the country is in the list
    let result = function_country_counter.hasOwnProperty(country);
    //console.log(result);

    //if the country is not in the dictionary
    if (function_country_counter.hasOwnProperty(country) == false) {
      //console.log('fffalse');
      //add the country with a count 1
      function_country_counter[country] = 1;
      //console.log(function_country_counter);
    }

    //if the country is in the dictionary
    if (function_country_counter.hasOwnProperty(country) == true) {
      //console.log('tttrue');
      //increment the counter
      function_country_counter[country] = function_country_counter[country] + 1;
      //console.log(function_country_counter);
    }
  }

  //return the tempoary dictionary
  return function_country_counter;

}

Once you have the countCountry() object, you can add it to the GeoJSON. The combineJSONs() function adds the HW_COUNT feature to the GeoJSON. It uses what seems like a comically inefficient process for doing this, but that’s what some person on stack overflow suggested and it worked, so…..

unction combineJSONs(country_list, geo_json) {
  console.log("hello");

  for (x in country_list) {

    //apparently the best way to add things to the geojson is to
    //loop over the entire thing every time to see if there is a match
    //and then add the entry when there is
    for (let i = 0; i < geo_json.features.length; i++) {
      // if the name of the country blob in the geo_json
      //being iterated upon equals x, which is the current country
      //from the country_list in the iteration
      if (geo_json.features[i].properties.ADMIN === x){
        //add a new elements that is HW_COUNT:<number of HW from the country_list>
        geo_json.features[i].properties["HW_COUNT"] = country_list[x]
        }
      }
    }

    //now loop over everything again and add a HW_COUNT of 0 to everything else
    for (let i = 0; i < geo_json.features.length; i++) {
      console.log(geo_json.features[i].properties.HW_COUNT);
      if (geo_json.features[i].properties.HW_COUNT === undefined){
        geo_json.features[i].properties["HW_COUNT"] = 0
      }
    }


    //prints the updated geojson
    console.log(geo_json);
    //returns the updated geojson
    return(geo_json);

  }

Now you have a combined_jsons object that has all of the geographic information for the countries and the information about how many pieces of hardware is certified in each country.

Make the map

At this point, everything basically follows the leaflet interactive choropleth tutoria. The only real changes I made were:

info.update = function (props) {
  		this._div.innerHTML = '<h4>OSHWA Open Source Hardware Certifications</h4>' +  (props ?
  			'<b>' + props.ADMIN + '</b><br />' + props.HW_COUNT + ' registrations'
  			: 'Hover over a country for registration count');
  	};

Updating this section so the title box talked about OSHWA

function getColor(d) {
    return d > 50 ? '#800026' :
        d > 20  ? '#BD0026' :
        d > 10  ? '#E31A1C' :
        d > 5  ? '#FC4E2A' :
        d > 1   ? '#FD8D3C' :
              '#FFFFFF';
  }

changed the thresholds and colors associated with those thresholds

function style(feature) {
  		return {
  			weight: 1,
  			opacity: 1,
  			color: '#d9d9d9',
  			dashArray: '3',
  			fillOpacity: 0.7,
  			fillColor: getColor(feature.properties.HW_COUNT)
  		};
  	}
layer.setStyle({
      weight: 1,
      color: '#666',
      dashArray: '',
      fillOpacity: 0.7
    });

slightly changed the weight and colors of the borders

map.attributionControl.addAttribution('Hardware Registrations from the <a href="https://certification.oshwa.org/">OSHWA Open Source Hardware Certification Program</a>');

changed the attribution light_number

var div = L.DomUtil.create("div", "legend"),
  			grades = [1, 5, 10, 20, 50],
  			labels = [],
  			from, to;

  		for (var i = 0; i < grades.length; i++) {
  			from = grades[i];
  			//to = grades[i + 1];
        to = (grades[i + 1]) - 1;

changed how the legend to match the other cutoffs.

That’s the long and short of it. I hope you take some time to play with the API and build a more interesting visualization than I put together.

Open Source Hardware Weather Report 2020

This post originally appeared on both the Engelberg Center and OSHWA blogs

Today the Engelberg Center, in collaboration with the Open Source Hardware Association (OSHWA) is thrilled to release the 2020 Open Source Hardware Weather Report. The report is a snapshot of the open source hardware community as it exists in 2020, ten years after the first Open Hardware Summit. It helps existing members of the open source hardware community take stock of where it is, and new members of the community understand the state of affairs today.

The open source hardware community has grown tremendously in the past decade. That growth is a testament to the viability of the idea of open source hardware. It can also create challenges when the community wants to talk to itself - let alone create welcoming pathways for new community members.

The 2020 report allows the open source hardware world to collectively identify what is working, share insights, and rally around shared challenges. It distills lessons learned and describes the collective understanding of the state of open source hardware. The report provides guidelines for how open source hardware can be a viable approach to hardware development, as well as identifies situations where open source hardware may not be the strongest approach. It also examines challenges that remain unresolved in 2020, along with opportunities for open source hardware in the future.

Like any weather report, this document is a snapshot of a moment in time. It was originally intended to flow from an in-person workshop held in connection with the tenth anniversary Open Hardware Summit here at the Engelberg Center. When the Summit went virtual, that workshop transformed into a series of interviews with a cross section of the open source hardware community.

Common themes, concerns, and challenges emerged during those discussions. The report provides an opportunity to summarize, distill, and universalize those insights. It makes it easier for the community to understand what is working in most places, and what challenges still demand our collective attention.

While this report is distilled from community input, it will also benefit from additional thoughts, concerns, and observations. That is why, in addition to the ‘stable release’ version captured in the PDF, we have also uploaded it to a github wiki. That is where we invite comments from the community, both on the substance of the report and on the form of the report itself. Let us know if a snapshot report is useful to you, and what we can do to make it more useful in the future.

Finally, thank you to everyone who took the time to contribute to this report. Some - but certainly not all - of them are listed in the acknowledgement section of the report. We also welcome outreach from other members of the community who did not participate this year, especially if they might be interested in participating in a future report.

This post originally appeared on the OSHWA blog

Today we are excited to announce the launch of a read/write API for our Open Source Hardware Certification program. This API will make it easier to apply for certification directly from where you already document your hardware, as well as empower research, visualizations, and explorations of currently certified hardware.

OSHWA’s Open Source Hardware Certification program has long been an easy way for creators and users alike to identify hardware that complies with the community definition of open source hardware. Since its creation in 2016, this free program has certified hardware from over 45 countries on every continent except Antarctica. Whenever you see the certification logo on hardware:

OSHWA certification logo

You know that it complies with the definition and that the documentation can be found using its unique identifier (UID).

What’s New?

The new API supports both read and write access to the certification process.

Write access means that you can submit certification applications directly instead of using the application form. If you already have all of the application information in a system, there is no need to retype them into a webform.

We hope that this will make it easier for entities that certify large amounts of hardware to build the certification process directly into their standard workflow. We are also working with popular platforms to integrate a ‘certify’ button directly into their systems.

Read access gives you access to information about hardware that has already been certified. This will make it easier to explore the data for research, create compelling visualizations of certified hardware, and build customized lenses to understand what is happening in open source hardware.

What Happens Now?

The first thing you can do is start exploring the API itself. The team at Objectively has created detailed documentation, code snippets, and sandboxes that make it easy to test out all of the features.

In the longer term, we hope that the community will build better ways to both submit applications for certification and present information about certified hardware. OSHWA expects to maintain our application form and certification list for the foreseeable future. That being said, we are also happy to share (and possibly cede) the stage to better ways to get information into and out of the system as they come along.

For now, let us know what you do with the API! You can tweet to us @OHSummit or send us an email at certification@oshwa.org.

Simulating Firefly Flashes with CircuitPython and Neopixels (Now with Classes!)

This is an updated version of an earlier post. The earlier post did not use classes. Here is the link to that post in case a slightly less functional non-class version is helpful.

This post is a walkthrough for having neopixels (individually addressable LEDs) flash in firefly patterns. The script uses circuitpython and uses three flash patterns from a National Park Service website. It should be very easy to add additional patterns as you see fit. The full script can be found here.

The current version of the script can easily accommodate an arbitrary number of lights, randomly assigning a firefly type to each one.

Here’s the full code:

#https://www.nps.gov/grsm/learn/nature/firefly-flash-patterns.htm

import board
import digitalio
import time
import neopixel
import random



#variables to hold the color that the LED will blink
neo_r = 255
neo_g = 255
neo_b = 0

# variable to hold the number of neopixels
number_of_lights = 10

#create the neopixel. auto_write=True avoids having to push changes (at the cost of speed, which probably doesn't matter here)
pixels = neopixel.NeoPixel(board.NEOPIXEL, number_of_lights, brightness = 0.1, auto_write=False)

# sets up the bug holder list, which holds all of the bug objects

bug_holder = []


# sets up the bug class

class Bug:
    def __init__(self, type, reset_time_input, light_number):
        self.type = type
        self.reset_time_input = reset_time_input
        self.light_number = light_number


#functions to turn light on and off
def on(light_num):
    pixels[light_num] = (neo_r, neo_g, neo_b)
    pixels.show()
def off(light_num):
    pixels[light_num] = (0, 0, 0)
    pixels.show()


#functions for the types of fireflies
def brimleyi(reset_time_input, light_number):
    #calculates how much time has passed since the new zero
    time_from_zero = time.monotonic() - reset_time_input
    # creates the carry over reset_time variable so that it can be returned even if it is not updated in the last if statement
    reset_time = reset_time_input

    # on flash
    if 5 <= time_from_zero <= 5.5:
        on(light_number)
    elif 15 <= time_from_zero <= 15.5:
        on(light_number)

    # reset (includes 10 seconds after second flash - 5 on the back end and 5 on the front end)
    elif time_from_zero > 20:
        off(light_number)
        reset_time = time.monotonic() + random.uniform(-3, 3)

    # all of the off times
    else:
        off(light_number)

    return reset_time

def macdermotti (reset_time_input, light_number):
    #calculates how much time has passed since the new zero
    time_from_zero = time.monotonic() - reset_time_input
    # creates the carry over reset_time variable so that it can be returned even if it is not updated in the last if statement
    reset_time = reset_time_input

    # on flash
    if 3 <= time_from_zero <= 3.5:
        on(light_number)
    elif 5 <= time_from_zero <= 5.5:
        on(light_number)
    elif 10 <= time_from_zero <= 10.5:
        on(light_number)
    elif 12 <= time_from_zero <= 12.5:
        on(light_number)

    elif time_from_zero > 14.5:
        off(light_number)
        reset_time = time.monotonic() + random.uniform(-3, 3)

    else:
        off(light_number)

    return reset_time

def carolinus(reset_time_input, light_number):
    time_from_zero = time.monotonic() - reset_time_input
    # creates the carry over reset_time variable so that it can be returned even if it is not updated in the last if statement
    reset_time = reset_time_input

    if 0 <= time_from_zero <= 0.5:
        on(light_number)
    elif 1 <= time_from_zero <= 1.5:
        on(light_number)
    elif 2 <= time_from_zero <= 2.5:
        on(light_number)
    elif 3 <= time_from_zero <= 3.5:
        on(light_number)
    elif 4 <= time_from_zero <= 4.5:
        on(light_number)
    elif 5 <= time_from_zero <= 5.5:
        on(light_number)
    elif 6 <= time_from_zero <= 6.5:
        on(light_number)

    elif time_from_zero >= 15:
        off(light_number)
        reset_time = time.monotonic()

    else:
        off(light_number)

    return reset_time


#create all of the light objects by appending a new light object to the list for each neopixel
#the first argument (random.randint(1, 3)) is used to assign a random number which corresponds to one of the ff functions
#if you start adding lots of those it might be worth using a universal variable

for i in range (0, number_of_lights):
    bug_holder.append(Bug(random.randint(1, 3), time.monotonic(), i))


while True:

    #iterates through all of the light objects in the bug_holder list
    #use the series of if statements to match the randomly assigned number to the types of fireflies

    for i in range (0, number_of_lights):
        if bug_holder[i].type == 1:
            bug_holder[i].reset_time_input = brimleyi(bug_holder[i].reset_time_input, i)
        elif bug_holder[i].type == 2:
            bug_holder[i].reset_time_input = macdermotti(bug_holder[i].reset_time_input, i)
        elif bug_holder[i].type == 3:
            bug_holder[i].reset_time_input = carolinus(bug_holder[i].reset_time_input, i)
        #this is just a catchall if there is some sort of error
        else:
            bug_holder[i].reset_time_input = brimleyi(bug_holder[i].reset_time_input, i)
            print("number error")


    #briefly pauses the loop to avoid crashing the USB bus. Also makes it easier to see what is happening.
    time.sleep(0.25)

At a high level, it creates a Bug class for each light, three functions (one for each type of firefly flash pattern) and then assigns that pattern to a light. The patterns are based on timing, so it uses the monotonic() function to keep track of time. There is not a real clock on microcontrollers, so monotonic() just counts up from the moment the board turns on.

#https://www.nps.gov/grsm/learn/nature/firefly-flash-patterns.htm

import board
import digitalio
import time
import neopixel
import random

The first part of the code imports the libraries used by the script.

#variables to hold the color that the LED will blink
neo_r = 255
neo_g = 255
neo_b = 0

The next part holds the color for the LED. The current color is yellow, although you could make it whatever you want. This script uses the same color for all of the lights, regardless of their pattern.

# variable to hold the number of neopixels
number_of_lights = 10

This variable holds the number of lights you are using. This makes it easy to change the number of lights you are controlling.

#create the neopixel. auto_write=True avoids having to push changes (at the cost of speed, which probably doesn't matter here)
pixels = neopixel.NeoPixel(board.NEOPIXEL, number_of_lights, brightness = 0.2, auto_write=False)

This line initializes the neopixels. I developed this on an Adafruit circuit playground board, so you may need to change this line depending on your setup. The other thing to point out here is that the brightness variable is set to 0.2. Neopixels are bright, so I toned things down during development. You might want to make them brighter for your final installation.

# sets up the bug holder list, which holds all of the bug objects

bug_holder = []

This creates the list to hold each individual instance of the bug light object. Holding them in a list makes it easy to address them as necessary.

# sets up the bug class

class Bug:
    def __init__(self, type, reset_time_input, light_number):
        self.type = type
        self.reset_time_input = reset_time_input
        self.light_number = light_number

This creates the Bug class. Each individual neopixel is an instantiation of a ‘Bug’. It has a type, which corresponds to the flash pattern it uses, a reset_time_input to keep track of time, and a light_number to assign it to a specific light. It is possible that the light_number is redundant because it also corresponds to its order in the list, but I’m still in baby step territory so I didn’t want to push it.

def on(light_num):
    pixels[light_num] = (neo_r, neo_g, neo_b)
    pixels.show()
def off(light_num):
    pixels[light_num] = (0, 0, 0)
    pixels.show()

These two little functions define the neopixel being on and being off. Each pattern function needs to turn lights on and off, so it was easier to define that behavior once and reuse it as a function.

def brimleyi(reset_time_input, light_number):
    #calculates how much time has passed since the new zero
    time_from_zero = time.monotonic() - reset_time_input
    # creates the carry over reset_time variable so that it can be returned even if it is not updated in the last if statement
    reset_time = reset_time_input

    # on flash
    if 5 <= time_from_zero <= 5.5:
        on(light_number)
    elif 15 <= time_from_zero <= 15.5:
        on(light_number)

    # reset (includes 10 seconds after second flash - 5 on the back end and 5 on the front end)
    elif time_from_zero > 20:
        off(light_number)
        reset_time = time.monotonic() + random.uniform(-3, 3)

    # all of the off times
    else:
        off(light_number)

    return reset_time

This is the first blinking function. It takes two arguments. The reset_time_input is the counter start time. The light_number is which neopixel it is controlling.

Without a real clock, all of the flash functions are controlled by a counter. You can think of the counter starting at 0 for the first loop (it doesn’t actually start a 0 the first time, but ignore that for a minute).

time_from_zero = time.monotonic() - reset_time_input figures out how long it has been since the start of the counter. In the example first loop, the reset_time_input would be 0. If it has been 2 seconds since the counter started counting, the time_from_zero would equal 2.

That value is then compared to a bunch of if statements that determine if the light is on or off. In this first function, the light goes on if the time_from_zero is between 5 and 5.5 seconds, and between 15 and 15.5 seconds. Because the default state of things is that the light is off, we only need if triggers for when the light needs to be on.

Once the time_from_zero exceeds 20 seconds, the counter resets. That reset is based on the current time (time.monotonic()) with a bit of random variation (random.uniform(-3, 3)) so that the different lights are not all in sync (the carolinus() function does not include this random variation because the carolinus bugs flash in unison).

As soon as the cycle is complete, it returns a new reset_time. Remember that there is only one counter on the board, and it just keeps counting up. The first time through the cycle, reset_time_input might be 0. The second time through, the cycle ‘starts’ closer to 20. Similarly, instead of being 2 the first time around, the time.monotonic() will be 22 the second time around. The time_from_zero function normalizes all of this, because 2-0, 22-20, and 82-80 are all the same value. That allows the function to keep working over time.

The macdermotti() and carolinus() functions work the same way. If you want to make a new function for a new pattern, just duplicate it, rename it, and change the if statements.

#create all of the light objects by appending a new light object to the list for each neopixel
#the first argument (random.randint(1, 3)) is used to assign a random number which corresponds to one of the ff functions
#if you start adding lots of those it might be worth using a universal variable

for i in range (0, number_of_lights):
    bug_holder.append(Bug(random.randint(1, 3), time.monotonic(), i))

Having done all of the setup work, this is where things start to actually happen. This loop creates as many Bug instances as necessary to match the number of lights you want to control. The first argument random.randint(1, 3) assigns an integer that corresponds to one of the three blink functions. The second argument time.monotonic() is the start time based on the board’s counter. The last argument i assigns the instance to a specific light.

while True:

    #iterates through all of the light objects in the bug_holder list
    #use the series of if statements to match the randomly assigned number to the types of fireflies

    for i in range (0, number_of_lights):
        if bug_holder[i].type == 1:
            bug_holder[i].reset_time_input = brimleyi(bug_holder[i].reset_time_input, i)
        elif bug_holder[i].type == 2:
            bug_holder[i].reset_time_input = macdermotti(bug_holder[i].reset_time_input, i)
        elif bug_holder[i].type == 3:
            bug_holder[i].reset_time_input = carolinus(bug_holder[i].reset_time_input, i)
        #this is just a catchall if there is some sort of error
        else:
            bug_holder[i].reset_time_input = brimleyi(bug_holder[i].reset_time_input, i)
            print("number error")

This is the loop that constantly checks each light to see if it should be on or off based on the pattern assigned to it. As it loops through each of the lights:

  for i in range (0, number_of_lights):

it looks to see which type of light it is. It then uses the type to decide which pattern function to use. The end of each function returns their ‘new’ reset time even if their state did not change, so these sections end by updating the reset time.

Now that all of the functions work, this while loop will just keep running them forever.

#briefly pauses the loop to avoid crashing the USB bus. Also makes it easier to see what is happening.
    time.sleep(0.25)

This last line just rests for 0.25 seconds. Before I added it, the looping was flooding the USB bus and creating all sorts of problems. Briefly pausing everything just makes it easier to work with.

header image: Case (Inrō) with Design of Fireflies in Flight and Climbing on Stone Baskets and Reeds at the Shore

Keep 3D Printers Unlocked (the petitions)

Today I filed a petition to expand the scope of the current (expiring) rule about unlocking 3D printers. The full petition is here. That follows a petition I filed in July to simply renew the current rule. That renewal petition is here. The petitions are short because the substantive discussion will be reserved for the hearing phase.

In June I wrote that the purpose of this process was to continue to clarify that 3D printer manufacturers cannot use copyright law to prevent people from using third party 3D printer material.

If no one objects to the renewal request, the Copyright Office will recommend that it be renewed without any additional process. If someone does object, they Copyright Office will hold hearings to consider the renewal. Regardless of the status of the renewal petition, the Copyright Office will hold a hearing about the expansion. We should find out reasonably soon if the hearings will include the renewal as well as the expansion, a well as when the hearings will be.

As I explained in my earlier post, the purpose of the expansion is to renew qualifying language from the current rule that brings additional confusion instead of clarity. I hope that no one objects to the expansion, but you never know.

Finally, thank you to everyone who submitted information about printers that:

requires you to purchase printing material (filament, powder, resin, etc.) from the printer manufacturer (or approved vendor)

AND

uses something besides a microchip to verify the source of the material.

These examples will be incorporated into the more substantive filing that is made as part of the hearing phase. If you have an example that fits this criteria and have not already sent it in, it is not too late! Please email me at hello@michaelweinberg.org or dm me on twitter @mweinberg2D. Feel free to send me this information anonymously if you prefer.