From 31bb2f02aad88ce0c563c4c8e4328c66387f6638 Mon Sep 17 00:00:00 2001
From: Jake <jake.read@cba.mit.edu>
Date: Wed, 15 Jun 2022 09:58:58 -0400
Subject: [PATCH] squish sub

---
 .../src/osape-d21/osape/osap/endpoint.cpp     | 253 +++++++++
 .../src/osape-d21/osape/osap/endpoint.h       |  79 +++
 .../src/osape-d21/osape/osap/osap.cpp         |  96 ++++
 .../fab-step/src/osape-d21/osape/osap/osap.h  |  45 ++
 .../src/osape-d21/osape/osap/osapLoop.cpp     | 304 +++++++++++
 .../src/osape-d21/osape/osap/osapLoop.h       |  24 +
 .../src/osape-d21/osape/osap/osapUtils.cpp    | 124 +++++
 .../src/osape-d21/osape/osap/osapUtils.h      |  23 +
 .../fab-step/src/osape-d21/osape/osap/ts.cpp  |  86 ++++
 .../fab-step/src/osape-d21/osape/osap/ts.h    | 127 +++++
 .../src/osape-d21/osape/osap/vertex.cpp       | 128 +++++
 .../src/osape-d21/osape/osap/vertex.h         |  92 ++++
 .../src/osape-d21/osape/utils/cobs.cpp        |  70 +++
 .../fab-step/src/osape-d21/osape/utils/cobs.h |  24 +
 .../fab-step/src/osape-d21/peripheralNums.h   |  18 +
 .../osape-d21/vertices/ucbus/ucBusDrop.cpp    | 484 ++++++++++++++++++
 .../src/osape-d21/vertices/ucbus/ucBusDrop.h  |  47 ++
 .../osape-d21/vertices/ucbus/ucBusHead.cpp    | 375 ++++++++++++++
 .../src/osape-d21/vertices/ucbus/ucBusHead.h  |  44 ++
 .../osape-d21/vertices/ucbus/ucBusMacros.h    | 125 +++++
 .../vertices/ucbus/ucbusDipConfig.cpp         |  61 +++
 .../osape-d21/vertices/ucbus/ucbusDipConfig.h |  36 ++
 .../osape-d21/vertices/ucbus/vt_ucBusDrop.cpp | 111 ++++
 .../osape-d21/vertices/ucbus/vt_ucBusDrop.h   |  33 ++
 .../osape-d21/vertices/ucbus/vt_ucBusHead.cpp |  82 +++
 .../osape-d21/vertices/ucbus/vt_ucBusHead.h   |  33 ++
 .../src/osape-d21/vertices/vertexConfig.h     |  10 +
 .../src/osape-d21/vertices/vt_usbSerial.cpp   | 112 ++++
 .../src/osape-d21/vertices/vt_usbSerial.h     |  37 ++
 29 files changed, 3083 insertions(+)
 create mode 100644 firmware/fab-step/src/osape-d21/osape/osap/endpoint.cpp
 create mode 100644 firmware/fab-step/src/osape-d21/osape/osap/endpoint.h
 create mode 100644 firmware/fab-step/src/osape-d21/osape/osap/osap.cpp
 create mode 100644 firmware/fab-step/src/osape-d21/osape/osap/osap.h
 create mode 100644 firmware/fab-step/src/osape-d21/osape/osap/osapLoop.cpp
 create mode 100644 firmware/fab-step/src/osape-d21/osape/osap/osapLoop.h
 create mode 100644 firmware/fab-step/src/osape-d21/osape/osap/osapUtils.cpp
 create mode 100644 firmware/fab-step/src/osape-d21/osape/osap/osapUtils.h
 create mode 100644 firmware/fab-step/src/osape-d21/osape/osap/ts.cpp
 create mode 100644 firmware/fab-step/src/osape-d21/osape/osap/ts.h
 create mode 100644 firmware/fab-step/src/osape-d21/osape/osap/vertex.cpp
 create mode 100644 firmware/fab-step/src/osape-d21/osape/osap/vertex.h
 create mode 100644 firmware/fab-step/src/osape-d21/osape/utils/cobs.cpp
 create mode 100644 firmware/fab-step/src/osape-d21/osape/utils/cobs.h
 create mode 100644 firmware/fab-step/src/osape-d21/peripheralNums.h
 create mode 100644 firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusDrop.cpp
 create mode 100644 firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusDrop.h
 create mode 100644 firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusHead.cpp
 create mode 100644 firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusHead.h
 create mode 100644 firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusMacros.h
 create mode 100644 firmware/fab-step/src/osape-d21/vertices/ucbus/ucbusDipConfig.cpp
 create mode 100644 firmware/fab-step/src/osape-d21/vertices/ucbus/ucbusDipConfig.h
 create mode 100644 firmware/fab-step/src/osape-d21/vertices/ucbus/vt_ucBusDrop.cpp
 create mode 100644 firmware/fab-step/src/osape-d21/vertices/ucbus/vt_ucBusDrop.h
 create mode 100644 firmware/fab-step/src/osape-d21/vertices/ucbus/vt_ucBusHead.cpp
 create mode 100644 firmware/fab-step/src/osape-d21/vertices/ucbus/vt_ucBusHead.h
 create mode 100644 firmware/fab-step/src/osape-d21/vertices/vertexConfig.h
 create mode 100644 firmware/fab-step/src/osape-d21/vertices/vt_usbSerial.cpp
 create mode 100644 firmware/fab-step/src/osape-d21/vertices/vt_usbSerial.h

diff --git a/firmware/fab-step/src/osape-d21/osape/osap/endpoint.cpp b/firmware/fab-step/src/osape-d21/osape/osap/endpoint.cpp
new file mode 100644
index 0000000..d713219
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/osape/osap/endpoint.cpp
@@ -0,0 +1,253 @@
+/*
+osap/endpoint.cpp
+
+network : software interface
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "endpoint.h"
+#include "osapUtils.h"
+#include "../../../indicators.h" 	// indicators are circuit specific, should live 3 levels up 
+#include "../../../syserror.h" 		// syserror is also circuit specific, ibid 
+
+uint8_t ack[VT_SLOTSIZE];
+
+// handle, return true to clear out. false to wait one turn 
+// item->data[ptr] == PK_DEST here 
+// item->data[ptr + 1, ptr + 2] = segsize !
+EP_ONDATA_RESPONSES endpointHandler(endpoint_t* ep, uint8_t od, stackItem* item, uint16_t ptr){
+	ptr += 3;
+	switch(item->data[ptr]){
+		case EP_SS_ACKLESS: // ah right, these were 'single segment' msgs... yikes 
+			{
+				// endpoints can REJECT, ACCEPT or ask for a WAIT for new data... 
+				// with an ackless write, WAIT is risky... but here we are 
+				// NOTE: previous code had *ackless* using 'ptr + 1' offsets, but should be the same 
+				// as *acked* code, which uses 'ptr + 2' offset ... swapped back here 2021-07-07, ? 
+				// was rarely using ackless... so I presume this is it 
+				EP_ONDATA_RESPONSES resp = ep->onData(&(item->data[ptr + 2]), item->len - ptr - 2);
+				switch(resp){
+					case EP_ONDATA_REJECT:
+						// nothing to do: msg will be deleted one level up 
+						break; 
+					case EP_ONDATA_ACCEPT:
+						// data OK, copy it in:
+						memcpy(ep->data, &(item->data[ptr + 2]), item->len - ptr - 2);
+						ep->dataLen = item->len - ptr - 2;
+						break;
+					case EP_ONDATA_WAIT:
+						// nothing to do, msg will be awaited one level up 
+						break;
+				} // end resp switch 
+				return resp; // return the response one level up... 
+			} // end ackless case closure 
+			break;
+		case EP_SS_ACKED:
+			{
+				// if there's not any space for an ack, we won't be able to ack, ask to wait 
+				if(!stackEmptySlot(ep->vt, VT_STACK_ORIGIN)) return EP_ONDATA_WAIT;
+				// otherwise carry on to the handler, 
+				EP_ONDATA_RESPONSES resp = ep->onData(&(item->data[ptr + 2]), item->len - ptr - 2);
+				switch(resp){
+					case EP_ONDATA_WAIT:
+						// again: will mirror this reponse up, wait will happen there 
+						break;
+					case EP_ONDATA_ACCEPT:
+						// this means we copy the data in, it's the new endpoint data:
+						memcpy(ep->data, &(item->data[ptr + 2]), item->len - ptr - 2);
+						ep->dataLen = item->len - ptr - 2;
+						// carry on to generate the response (no break)
+					case EP_ONDATA_REJECT:
+						{
+							// for reject *or* accept, we acknowledge that we got the data: 
+							// now generate are reply, 
+							uint16_t wptr = 0;
+							if(!reverseRoute(item->data, ptr - 4, ack, &wptr)){
+								// if we can't reverse it, bail, but issue same response to 
+								// osapLoop.cpp
+								return resp; 
+							}
+							// the ack, 
+							ack[wptr ++] = EP_SS_ACK;
+              // the ack ID is here in prv packet 
+							ack[wptr ++] = item->data[ptr + 1];
+							stackLoadSlot(ep->vt, VT_STACK_ORIGIN, ack, wptr);
+						}
+						break; // end accept / reject 
+					default:
+						break;
+				}
+				return resp; // mirror response to osapLoop.cpp 
+			} // end acked case closure 
+			break;
+		case EP_QUERY:
+			// if can generate new message, 
+			if(stackEmptySlot(ep->vt, VT_STACK_ORIGIN)){
+				// run the 'beforeQuery' call, which doesn't need to return anything: 
+				ep->beforeQuery();
+				uint16_t wptr = 0;
+				// if the route can't be reversed, trouble:
+				if(!reverseRoute(item->data, ptr - 4, ack, &wptr)) {
+					sysError("on a query, can't reverse a route, rming msg");
+					return EP_ONDATA_REJECT;
+				} else {
+					ack[wptr ++] = EP_QUERY_RESP;		// reply is response 
+					ack[wptr ++] = item->data[ptr + 1];	// has ID matched to request 
+					memcpy(&(ack[wptr]), ep->data, ep->dataLen);
+					wptr += ep->dataLen;
+					stackLoadSlot(ep->vt, VT_STACK_ORIGIN, ack, wptr);
+					return EP_ONDATA_ACCEPT;
+				}
+			} else {
+				// no space to ack w/ a query, pls wait 
+				return EP_ONDATA_WAIT;
+			}
+		case EP_SS_ACK:
+      // upd8 tx state on associated route 
+      {
+        for(uint8_t r = 0; r < ep->numRoutes; r ++){
+          // this is where the ackId is in the packet, we match to routes on that (for speed)
+          if(item->data[ptr + 1] == ep->routes[r].ackId){
+            switch(ep->routes[r].state){
+              case EP_TX_AWAITING_ACK:  // awaiting -> captured -> idle, 
+                ep->routes[r].state = EP_TX_IDLE;
+                break;
+              case EP_TX_AWAITING_AND_FRESH:  // awaiting -> captured -> fresh again 
+                ep->routes[r].state = EP_TX_FRESH;
+                break;
+              case EP_TX_FRESH:
+              case EP_TX_IDLE:
+              default:
+                // these are all nonsense states for receipt of an ack, 
+                break;
+            }
+          }
+        }
+      }
+      return EP_ONDATA_ACCEPT;
+		case EP_QUERY_RESP:
+			// not yet having embedded query function 
+		default:
+			// not recognized, bail city, get it outta here,
+			return EP_ONDATA_REJECT;
+	}
+}
+
+// add a route to an endpoint 
+boolean endpointAddRoute(endpoint_t* ep, uint8_t* path, uint16_t pathLen, EP_ROUTE_MODES mode){
+	// guard against more-than-allowed routes 
+	if(ep->numRoutes >= ENDPOINT_MAX_ROUTES) return false;
+	// handle for the route we're going to modify (and increment # of active routes)
+	endpoint_route_t* rt = &(ep->routes[ep->numRoutes ++]);
+	// load the path -> the path 
+	memcpy(rt->path, path, pathLen);
+	rt->pathLen = pathLen;
+	rt->ackMode = mode;
+	// done 
+	return true; 
+}
+
+void endpointWrite(endpoint_t* ep, uint8_t* data, uint16_t len){
+  // copy data in,
+  if(len > VT_SLOTSIZE) return; // no lol 
+  memcpy(ep->data, data, len);
+  ep->dataLen = len;
+  // set route freshness 
+  for(uint8_t r = 0; r < ep->numRoutes; r ++){
+    if(ep->routes[r].state == EP_TX_AWAITING_ACK){
+      ep->routes[r].state = EP_TX_AWAITING_AND_FRESH;
+    } else {
+      ep->routes[r].state = EP_TX_FRESH;
+    }
+  }
+}
+
+boolean endpointAllClear(endpoint_t* ep){
+  for(uint8_t r = 0; r < ep->numRoutes; r ++){
+    if(ep->routes[r].state != EP_TX_IDLE){
+      return false;
+    }
+  }
+  return true;
+}
+
+uint8_t EPOut[VT_SLOTSIZE];
+
+// check tx states, 
+void endpointLoop(endpoint_t* ep, unsigned long now){
+	// we want to pluck 'em out on round-robin...
+	uint8_t r = ep->lastRouteServiced;
+	for(uint8_t i = 0; i < ep->numRoutes; i ++){
+		r ++;
+		if(r >= ep->numRoutes) r = 0;
+		endpoint_route_t* rt = &(ep->routes[r]);
+		switch(rt->state){
+			case EP_TX_IDLE:
+				// no-op 
+				break;
+			case EP_TX_FRESH:
+				// foruml8 pck & tx it 
+				if(stackEmptySlot(ep->vt, VT_STACK_ORIGIN)){
+					// load it w/ data, 
+					#warning slow-load code, should write str8 to stack 
+					// write ptr in the head, 
+					uint16_t wptr = 0;
+					EPOut[wptr ++] = PK_PTR;
+					// the path next, 
+					memcpy(&(EPOut[wptr]), rt->path, rt->pathLen);
+					wptr += rt->pathLen;
+					// destination key, segment size 
+					EPOut[wptr ++] = PK_DEST;
+					ts_writeUint16(rt->segSize, EPOut, &wptr);
+					// mode-key, 
+					if(rt->ackMode == EP_ROUTE_ACKLESS){
+            EPOut[wptr ++] = EP_SS_ACKLESS;
+					} else if(rt->ackMode == EP_ROUTE_ACKED){
+            EPOut[wptr ++] = EP_SS_ACKED;
+            EPOut[wptr ++] = ep->nextAckId;
+            rt->ackId = ep->nextAckId;
+            ep->nextAckId ++; // increment and wrap: only one ID per endpoint per tx, for demux 
+          }
+					// check against write into stray memory 
+					if(ep->dataLen + wptr >= VT_SLOTSIZE){
+						ERROR(1, "write-to-endpoint exceeds slotsize");
+						return;
+					}
+					// the data, 
+					memcpy(&(EPOut[wptr]), ep->data, ep->dataLen);
+					wptr += ep->dataLen;
+					// that's a packet? we load it into stack, we're done 
+          rt->txTime = now;
+					stackLoadSlot(ep->vt, VT_STACK_ORIGIN, EPOut, wptr);
+					// transition state:
+					if(rt->ackMode == EP_ROUTE_ACKLESS) rt->state = EP_TX_IDLE;
+					if(rt->ackMode == EP_ROUTE_ACKED) rt->state = EP_TX_AWAITING_ACK;
+					// and track, so that we do *this recently serviced* thing *last* on next round 
+					ep->lastRouteServiced = r;
+				} else {
+					// no space... await... 
+				}
+			case EP_TX_AWAITING_ACK:
+				// check timeout & transition to idle state 
+        if(rt->txTime + rt->timeoutLength > now){
+          rt->state = EP_TX_IDLE;
+        }
+				break;
+      case EP_TX_AWAITING_AND_FRESH:
+        // check timeout & transition to fresh state 
+        if(rt->txTime + rt->timeoutLength > now){
+          rt->state = EP_TX_FRESH;
+        }
+        break;
+			default:
+				break;
+		}
+	}
+}
diff --git a/firmware/fab-step/src/osape-d21/osape/osap/endpoint.h b/firmware/fab-step/src/osape-d21/osape/osap/endpoint.h
new file mode 100644
index 0000000..0293e34
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/osape/osap/endpoint.h
@@ -0,0 +1,79 @@
+/*
+osap/endpoint.h
+
+network : software interface
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef ENDPOINT_H_
+#define ENDPOINT_H_
+
+#include "vertex.h"
+
+typedef struct vertex_t vertex_t;
+typedef struct stackItem stackItem;
+
+// endpoints have *routes* which they tx to... 
+
+#define ENDPOINT_MAX_ROUTES 4
+
+enum EP_ROUTE_MODES { EP_ROUTE_ACKLESS, EP_ROUTE_ACKED };
+enum EP_ROUTE_STATES { EP_TX_IDLE, EP_TX_FRESH, EP_TX_AWAITING_ACK, EP_TX_AWAITING_AND_FRESH };
+
+struct endpoint_route_t {
+  uint8_t path[64];
+  uint8_t pathLen = 0;
+  uint8_t ackId = 0;    // this'll limit the # of simultaneously tx'd-to routes to 255, considering each needs unique ackid 
+  EP_ROUTE_STATES state = EP_TX_IDLE;
+  unsigned long txTime = 0;
+  unsigned long timeoutLength = 1000; // ms 
+  // properties likely to be exposed to mvc 
+  EP_ROUTE_MODES ackMode = EP_ROUTE_ACKLESS;
+  uint16_t segSize = 256;
+};
+
+// endpoint handler responses must be one of these enum - 
+
+enum EP_ONDATA_RESPONSES { EP_ONDATA_REJECT, EP_ONDATA_ACCEPT, EP_ONDATA_WAIT };
+
+struct endpoint_t {
+  vertex_t* vt;
+  // local data store & length, 
+  uint8_t data[VT_SLOTSIZE];
+  uint16_t dataLen = 0; 
+  // callbacks: on new data & before a query is written out 
+  EP_ONDATA_RESPONSES (*onData)(uint8_t* data, uint16_t len) = nullptr;
+  boolean (*beforeQuery)(void) = nullptr;
+  // routes, for tx-ing to:
+  endpoint_route_t routes[ENDPOINT_MAX_ROUTES];
+  uint16_t numRoutes = 0;
+  uint16_t lastRouteServiced = 0;
+  uint8_t nextAckId = 77;
+};
+
+// route adder: 
+// vertex_t* ep is a mistake: osapBuildEndpoint is broken, and returns a vertex... 
+// we *should* have a better cpp API for this, but don't, that's next go-round 
+boolean endpointAddRoute(endpoint_t* ep, uint8_t* path, uint16_t pathLen, EP_ROUTE_MODES mode);
+
+// endpoint writer... 
+void endpointWrite(endpoint_t* ep, uint8_t* data, uint16_t len);
+
+// endpoint check-tx-state-machine 
+void endpointLoop(endpoint_t* ep, unsigned long now);
+
+// endpoint api-to-check-all-clear:
+
+boolean endpointAllClear(endpoint_t* ep);
+
+// a master handler: 
+EP_ONDATA_RESPONSES endpointHandler(endpoint_t* ep, uint8_t od, stackItem* item, uint16_t ptr);
+
+#endif 
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/osape/osap/osap.cpp b/firmware/fab-step/src/osape-d21/osape/osap/osap.cpp
new file mode 100644
index 0000000..ee04c13
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/osape/osap/osap.cpp
@@ -0,0 +1,96 @@
+/*
+osap/osap.cpp
+
+osap root / vertex factory
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "osap.h"
+#include "osapLoop.h"
+
+vertex_t _root;
+vertex_t* osap = &_root;
+
+void loopDefault(void){
+  // ... noop 
+}
+
+void osapSetup(String name){
+  _root.type = VT_TYPE_ROOT;
+  _root.name = name;
+  _root.indice = 0;
+  _root.loop = loopDefault;
+  stackReset(&_root);
+}
+
+void osapLoop(void){
+  osapRecursor(&_root);
+}
+
+boolean osapAddVertex(vertex_t* vt) {
+  if (_root.numChildren >= VT_MAXCHILDREN) {
+    return false;
+  } else {
+    stackReset(vt);
+    vt->indice = _root.numChildren;
+    vt->parent = &_root;
+    _root.children[_root.numChildren++] = vt;
+    return true;
+  }
+}
+
+boolean osapAddEndpoint(endpoint_t* ep){
+  return osapAddVertex(ep->vt);
+}
+
+EP_ONDATA_RESPONSES onDataDefault(uint8_t* data, uint16_t len){
+  return EP_ONDATA_ACCEPT;
+}
+
+boolean beforeQueryDefault(void){
+  return true;
+}
+
+endpoint_t* osapBuildEndpoint(String name, EP_ONDATA_RESPONSES (*onData)(uint8_t* data, uint16_t len), boolean (*beforeQuery)(void)){
+  vertex_t* vt = new vertex_t; // allocates new to heap someplace, 
+  vt->type = VT_TYPE_ENDPOINT;
+  vt->name = name;
+  stackReset(vt);
+  // add this to the system, 
+  if(osapAddVertex(vt)){
+    endpoint_t* ep = new endpoint_t;
+    if(onData != nullptr){
+      ep->onData = onData;
+    } else {
+      ep->onData = onDataDefault;
+    }
+    if(beforeQuery != nullptr){
+      ep->beforeQuery = beforeQuery;
+    } else {
+      ep->beforeQuery = beforeQueryDefault;
+    }
+    // endpoints don't get loop fns 
+    vt->loop = loopDefault;
+    vt->ep = ep;
+    ep->vt = vt;
+    return ep;
+  } else {
+    delete vt;
+    return nullptr;
+  }
+}
+
+endpoint_t* osapBuildEndpoint(String name){
+  return osapBuildEndpoint(name, nullptr, nullptr);
+}
+
+endpoint_t* osapBuildEndpoint(String name, EP_ONDATA_RESPONSES (*onData)(uint8_t* data, uint16_t len)){
+  return osapBuildEndpoint(name, onData, nullptr);
+}
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/osape/osap/osap.h b/firmware/fab-step/src/osape-d21/osape/osap/osap.h
new file mode 100644
index 0000000..9a5e702
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/osape/osap/osap.h
@@ -0,0 +1,45 @@
+/*
+osap/osap.h
+
+osap root / vertex factory 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_H_
+#define OSAP_H_
+
+#include "vertex.h"
+#include "endpoint.h"
+
+// vertex factory & root vertex 
+
+extern vertex_t* osap;
+
+void osapSetup(String name);
+void osapLoop(void);
+
+boolean osapAddVertex(vertex_t* vertex);
+boolean osapAddEndpoint(endpoint_t* endpoint);
+
+endpoint_t* osapBuildEndpoint(
+    String name, 
+    EP_ONDATA_RESPONSES (*onData)(uint8_t* data, uint16_t len), 
+    boolean (*beforeQuery)(void)
+);
+
+endpoint_t* osapBuildEndpoint(
+    String name
+);
+
+endpoint_t* osapBuildEndpoint(String name,
+    EP_ONDATA_RESPONSES (*onData)(uint8_t* data, uint16_t len)
+);
+
+#endif 
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/osape/osap/osapLoop.cpp b/firmware/fab-step/src/osape-d21/osape/osap/osapLoop.cpp
new file mode 100644
index 0000000..d891e0a
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/osape/osap/osapLoop.cpp
@@ -0,0 +1,304 @@
+/*
+osap/osapLoop.cpp
+
+main osap op: whips data vertex-to-vertex
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "osapLoop.h"
+#include "osapUtils.h"
+#include "../../../indicators.h"
+#include "../../../syserror.h"
+
+//#define LOOP_DEBUG
+
+// recurse down vertex's children, 
+// ... would be breadth-first, ideally 
+void osapRecursor(vertex_t* vt){
+  osapHandler(vt);
+  for(uint8_t child = 0; child < vt->numChildren; child ++){
+    osapRecursor(vt->children[child]);
+  };
+}
+
+void osapHandler(vertex_t* vt) {
+  //sysError("handler " + vt->name);
+  // time is now
+  unsigned long now = millis();
+  //unsigned long at0;
+  //unsigned long at1;
+
+  // run vertex's own loop code (with reference to self)
+  if(vt->loop != nullptr) vt->loop();
+  if(vt->ep != nullptr) endpointLoop(vt->ep, now);
+
+  // handle origin stack, destination stack, in same manner 
+  for(uint8_t od = 0; od < 2; od ++){
+    // try one handle per stack item, per loop:
+    stackItem* items[vt->stackSize];
+    uint8_t count = stackGetItems(vt, od, items, vt->stackSize);
+    // want to track out-of-order issues to the loop. 
+    // if(count) at0 = items[0]->arrivalTime; // pretty sure we can delete this as of 2021-12-31
+    for(uint8_t i = 0; i < count; i ++){
+      // the item, and ptr
+      stackItem* item = items[i];
+      uint16_t ptr = 0;
+      // check timeouts, 
+      if(item->arrivalTime + TIMES_STALE_MSG < now){
+        sysError("T/O indice " + String(vt->indice) + " " + String(item->data[ptr + 1]) + " " + String(item->arrivalTime));
+        stackClearSlot(vt, od, item);
+        continue;
+      }
+      // check for decent ptr walk, 
+      if(!ptrLoop(item->data, &ptr)){
+        sysError("main loop bad ptr walk, from vt->indice: " + String(vt->indice) + " o/d: " + String(od) + " len: " + String(item->len));
+        logPacket(item->data, item->len);
+        stackClearSlot(vt, od, item); // clears the msg 
+        continue; 
+      }
+      // check FIFO order, 
+      // at1 = item->arrivalTime; // pretty sure we can delete this as of 2021-12-31
+      // if(at1 - at0 < 0) sysError("out of order " + String(at1) + " " + String(at0));
+      // at1 = at0;
+      // handle it, 
+      osapSwitch(vt, od, item, ptr, now);
+    }
+  } // end lp over origin / destination stacks 
+}
+
+void osapSwitch(vertex_t* vt, uint8_t od, stackItem* item, uint16_t ptr, unsigned long now){
+  // switch at pck[ptr + 1]
+  ptr ++;
+  uint8_t* pck = item->data;
+  uint16_t len = item->len;
+  // do things, 
+  switch(pck[ptr]){
+    case PK_DEST: // instruction indicates pck is at vertex of destination, we try handler to rx data
+      switch(vt->type){
+        case VT_TYPE_ROOT:
+        case VT_TYPE_MODULE:
+        case VT_TYPE_VPORT:
+        case VT_TYPE_VBUS:
+          // all four: we are ignoring dms, wipe it:
+          stackClearSlot(vt, od, item);
+          break;
+        case VT_TYPE_ENDPOINT: 
+          {
+            EP_ONDATA_RESPONSES resp = endpointHandler(vt->ep, od, item, ptr);
+            switch(resp){
+              case EP_ONDATA_REJECT:
+              case EP_ONDATA_ACCEPT: // in either case, msg is handled / out of stack, we can clear it 
+                stackClearSlot(vt, od, item);
+                break;
+              case EP_ONDATA_WAIT: // not handled: want to wait in the stack, so update time & come back next 
+                item->arrivalTime = now;
+                break;
+              default:
+                sysError("on endpoint dest. handle, unknown ondata response");
+                stackClearSlot(vt, od, item);
+            } // end response switch 
+          }
+          break;
+        default:
+          // vertex has a 'type' we don't recognize, 
+          // best I can do is deletion 
+          sysError("unknown vertext type, when handling msg-at-destination");
+          stackClearSlot(vt, od, item);
+          break;
+      } // end vt typeswitch 
+      break; // end PK_DEST case 
+    case PK_SIB_KEY: {  // instruction to pass to this sibling, 
+      // need the indice, 
+      uint16_t si;
+      ptr ++;      
+      ts_readUint16(&si, pck, &ptr);
+      // can't do this if no parent, 
+      if(vt->parent == nullptr){
+        sysError("no parent for sib traverse");
+        stackClearSlot(vt, od, item);
+        break;
+      }
+      // nor if no target, 
+      if(si >= vt->parent->numChildren){
+        sysError("sib traverse oob");
+        stackClearSlot(vt, od, item);
+        break;
+      }
+      // now we can copy it in, only if there's space ahead to move it into 
+      if(stackEmptySlot(vt->parent->children[si], VT_STACK_DESTINATION)){
+        #ifdef LOOP_DEBUG
+        sysError("sib copy");
+        #endif 
+        ptr -= 4; // write in reversed instruction (reverse ptr to PK_PTR here)
+        pck[ptr ++] = PK_SIB_KEY; // overwrite with instruction that would return to us, 
+        ts_writeUint16(vt->indice, pck, &ptr);
+        pck[ptr] = PK_PTR;
+        // copy-in, set fullness, update time 
+        stackLoadSlot(vt->parent->children[si], VT_STACK_DESTINATION, pck, len);
+        // now og is clear 
+        stackClearSlot(vt, od, item);
+        // and we can finish here 
+        break;
+      } else {
+        // destination stack at target is full, msg stays in place, next loop checks 
+      }
+    } 
+    break; // end sib-fwd case, 
+    case PK_PARENT_KEY:
+      if(vt->parent == nullptr){
+        ERROR(1, "requests traverse to parent from top level");
+        stackClearSlot(vt, od, item);
+      } else if(stackEmptySlot(vt->parent, VT_STACK_DESTINATION)){
+        #ifdef LOOP_DEBUG
+        sysError("copy to parent");
+        #endif 
+        ptr -= 1; // write in reversed instruction 
+        pck[ptr ++] = PK_CHILD_KEY;
+        ts_writeUint16(vt->indice, pck, &ptr);
+        pck[ptr ++] = PK_PTR;
+        // copy-in, set fullness and update time 
+        stackLoadSlot(vt->parent, VT_STACK_DESTINATION, pck, len);
+        stackClearSlot(vt, od, item);
+      }
+      break;
+    case PK_CHILD_KEY:
+      // find child 
+      uint16_t ci;
+      ptr ++;
+      ts_readUint16(&ci, pck, &ptr);
+      // can't do it w/o the child, 
+      if(vt->numChildren <= ci){
+        ERROR(1, "no child at this indice " + String(ci));
+        stackClearSlot(vt, od, item);
+      } else if (stackEmptySlot(vt->children[ci], VT_STACK_DESTINATION)){
+        #ifdef LOOP_DEBUG
+        sysError("copy to child");
+        #endif 
+        ptr -= 4;
+        pck[ptr ++] = PK_PARENT_KEY;
+        ts_writeUint16(0, pck, &ptr); // parent 'index' used bc packet length should be symmetric 
+        pck[ptr ++] = PK_PTR;
+        // do the copy-in, set fullness, etc 
+        stackLoadSlot(vt->children[ci], VT_STACK_DESTINATION, pck, len);
+        stackClearSlot(vt, od, item);
+      }
+      break;
+    case PK_PFWD_KEY:
+      if(vt->type != VT_TYPE_VPORT || vt->cts == nullptr || vt->send == nullptr){
+        sysError("pfwd to non-vport vertex");
+        stackClearSlot(vt, od, item);
+      } else {
+        if(vt->cts(0)){ // walk ptr fwds, transmit, and clear the msg 
+          pck[ptr - 1] = PK_PFWD_KEY;
+          pck[ptr] = PK_PTR;
+          vt->send(pck, len, 0);
+          stackClearSlot(vt, od, item);
+        } else {
+          // failed to pfwd this turn, code will return here next go-round 
+        }
+      }
+      break;
+    case PK_BFWD_KEY:
+      if(vt->type != VT_TYPE_VBUS || vt->cts == nullptr || vt->send == nullptr){
+        sysError("bfwd to non-vbus vertex");
+        logPacket(item->data, item->len);
+        stackClearSlot(vt, od, item);
+      } else {
+        // need tx rxaddr, for which drop on bus to tx to, 
+        uint16_t rxAddr;
+        ptr ++;      
+        ts_readUint16(&rxAddr, pck, &ptr);
+        if(vt->cts(rxAddr)){  // walk ptr fwds, transmit, and clear the msg 
+          #ifdef LOOP_DEBUG 
+          sysError("busf " + String(rxAddr));
+          #endif
+          //sysError("be " + String(item->arrivalTime));
+          ptr -= 4;
+          pck[ptr ++] = PK_BFWD_KEY;
+          ts_writeUint16(vt->ownRxAddr, pck, &ptr);
+          pck[ptr] = PK_PTR;
+          //logPacket(pck, len);
+          vt->send(pck, len, rxAddr);
+          stackClearSlot(vt, od, item);
+        } else {
+          // failed to bfwd this turn, code will return here next go-round 
+        }
+      }
+      break;
+    case PK_SCOPE_REQ_KEY: 
+      {
+        // so we want to write a brief packet in here, and we should do it str8 into the same item, 
+        // we need to reverse the route into a temp object:
+        uint8_t route[VT_SLOTSIZE];
+        uint16_t wptr = 0;
+        if(!reverseRoute(item->data, ptr - 1, route, &wptr)){
+          ERROR(1, "reverse route badness at scope request");
+          stackClearSlot(vt, od, item);
+        }
+        // because reverse route assumes dest, segsize / checksum, we actually want...
+        wptr -= 3;
+        // now we can write in:
+        // recall that item->data is the stack-item's little msg block... 
+        memcpy(item->data, route, wptr);
+        // and write response key
+        item->data[wptr ++] = PK_SCOPE_RES_KEY;
+        // id from the REQ, should actually be untouched, right?
+        wptr ++;
+        // want 2 of these 
+        uint16_t rptr = wptr;
+        // collect new timeTag: vertex keeps 'last-time-scoped' state
+        // here we write-in to the response our *previous* time-scoped, and replace that w/ this requests' tag 
+        uint32_t newScopeTimeTag;
+        ts_readUint32(&newScopeTimeTag, item->data, &rptr);
+        ts_writeUint32(vt->scopeTimeTag, item->data, &wptr);
+        vt->scopeTimeTag = newScopeTimeTag;
+        // our type 
+        item->data[wptr ++] = vt->type;
+        // our own indice, our # of siblings, our # of children:
+        ts_writeUint16(vt->indice, item->data, &wptr);
+        if(vt->parent != nullptr){
+          ts_writeUint16(vt->parent->numChildren, item->data, &wptr);
+        } else {
+          ts_writeUint16(0, item->data, &wptr);
+        }
+        ts_writeUint16(vt->numChildren, item->data, &wptr);
+        // and our name... 
+        ts_writeString(vt->name, item->data, &wptr);
+        /*
+        switch(vt->type){
+          case VT_TYPE_ENDPOINT:
+            ts_writeString("embedded-endpoint", item->data, &wptr);
+            break;
+          case VT_TYPE_ROOT:
+            ts_writeString("embedded-root", item->data, &wptr);
+            break;
+          case VT_TYPE_VPORT:
+            ts_writeString("embedded-vport", item->data, &wptr);
+            break;
+          default:
+            ts_writeString("embedded-vertex", item->data, &wptr);
+            break;
+        }
+        */
+        // ok then, we can reset this item, basically:
+        item->len = wptr;
+        item->arrivalTime = now;
+        // osap will pick it up next loop, ship it back. 
+      }
+      break;
+    case PK_SCOPE_RES_KEY:
+    case PK_LLESCAPE_KEY:
+    default:
+      sysError("unrecognized ptr here at " + String(ptr) + ": " + String(pck[ptr]));
+      logPacket(pck, len);
+      stackClearSlot(vt, od, item);
+      break;
+  } // end main switch 
+}
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/osape/osap/osapLoop.h b/firmware/fab-step/src/osape-d21/osape/osap/osapLoop.h
new file mode 100644
index 0000000..099fbdb
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/osape/osap/osapLoop.h
@@ -0,0 +1,24 @@
+/*
+osap/osapLoop.h
+
+main osap op: whips data vertex-to-vertex 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_LOOP_H_
+#define OSAP_LOOP_H_ 
+
+#include "vertex.h"
+
+void osapRecursor(vertex_t* vt);
+void osapHandler(vertex_t* vt);
+void osapSwitch(vertex_t* vt, uint8_t od, stackItem* item, uint16_t ptr, unsigned long now);
+
+#endif 
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/osape/osap/osapUtils.cpp b/firmware/fab-step/src/osape-d21/osape/osap/osapUtils.cpp
new file mode 100644
index 0000000..ddc1266
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/osape/osap/osapUtils.cpp
@@ -0,0 +1,124 @@
+/*
+osap/osapUtils.cpp
+
+common routines 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "osapUtils.h"
+#include "ts.h"
+#include "../../../syserror.h"
+
+boolean ptrLoop(uint8_t* pck, uint16_t* pt){
+  uint16_t ptr = *pt;
+  for(uint8_t i = 0; i < 16; i ++){
+    //sysError(String(ptr));
+    switch(pck[ptr]){
+      case PK_PTR: // var is here 
+        *pt = ptr;
+        return true;
+      case PK_SIB_KEY:
+        ptr += PK_SIB_INC;
+        break;
+      case PK_PARENT_KEY:
+        ptr += PK_PARENT_INC;
+        break;
+      case PK_CHILD_KEY:
+        ptr += PK_CHILD_INC;
+        break;
+      case PK_PFWD_KEY:
+        ptr += PK_PFWD_INC;
+        break;
+      case PK_BFWD_KEY:
+        ptr += PK_BFWD_INC;
+        break;
+      default:
+        return false;
+    }
+  }
+  // case where no ptr after 16 hops, 
+  return false;
+}
+
+// pck: bytes, rptr: read ptr (should start at pck[rptr] = PTR / 88)
+// repl: reply bytes, replyPtr ... ptr after setup 
+boolean reverseRoute(uint8_t* pck, uint16_t rptr, uint8_t* repl, uint16_t* replyPtr){
+  // so we should have here that 
+  if(pck[rptr] != PK_PTR){
+    sysError("rr: pck[ptr] != pk_ptr");
+    return false;
+  }
+  // the tail is identical: dest & segsize following 
+  for(uint8_t i = 3; i > 0; i --){
+      repl[rptr + 4 - i] = pck[rptr + 4 - i];
+  }
+  // so we have a readptr (ptr) and writeptr (wptr)
+  // we write *from the tail back* and read *from the tip in*
+  uint16_t end = rptr;
+  uint16_t wptr = rptr + 1;
+  rptr = 0;
+  // sequentially, at most 16 ops 
+  for(uint8_t i = 0; i < 16; i ++){
+    // end case: 
+    if(rptr >= end){
+      if(rptr != end){
+        sysError("rr: rptr overruns end");
+        return false;
+      }
+      // start is pointer, 
+      repl[0] = PK_PTR;
+      // end is past ptr, + dest key, + checksum (2), + 1 so 
+      // first write repl[rptr] = nextByte 
+      *replyPtr = rptr + 4;
+      return true;
+    }
+    // switch each, 
+    switch(pck[rptr]){
+      case PK_PTR: // var is here 
+        sysError("rr: find pck_ptr during walk");
+        return false;
+      case PK_SIB_KEY:
+        wptr -= PK_SIB_INC;
+        for(uint8_t j = 0; j < PK_SIB_INC; j ++){
+          repl[wptr + j] = pck[rptr ++];
+        }
+        break;
+      case PK_PARENT_KEY:
+        wptr -= PK_PARENT_INC;
+        for(uint8_t j = 0; j < PK_PARENT_INC; j ++){
+          repl[wptr + j] = pck[rptr ++];
+        }
+        break;
+      case PK_CHILD_KEY:
+        wptr -= PK_CHILD_INC;
+        for(uint8_t j = 0; j < PK_CHILD_INC; j ++){
+          repl[wptr + j] = pck[rptr ++];
+        }
+        break;
+      case PK_PFWD_KEY:
+        wptr -= PK_PFWD_INC;
+        for(uint8_t j = 0; j < PK_PFWD_INC; j ++){
+          repl[wptr + j] = pck[rptr ++];
+        }
+        break;
+      case PK_BFWD_KEY:
+        wptr -= PK_BFWD_INC;
+        for(uint8_t j = 0; j < PK_BFWD_INC; j ++){
+          repl[wptr + j] = pck[rptr ++];
+        }
+        break;
+      default:
+        sysError("rr: default switch");
+        return false;
+    }
+  }
+  // if reach end of ptr walk but no exit, badness
+  return false;
+}
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/osape/osap/osapUtils.h b/firmware/fab-step/src/osape-d21/osape/osap/osapUtils.h
new file mode 100644
index 0000000..a8870bd
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/osape/osap/osapUtils.h
@@ -0,0 +1,23 @@
+/*
+osap/osapUtils.h
+
+common routines 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_UTILS_H_
+#define OSAP_UTILS_H_
+
+#include <Arduino.h>
+
+boolean ptrLoop(uint8_t* pck, uint16_t* ptr);
+boolean reverseRoute(uint8_t* pck, uint16_t rptr, uint8_t* repl, uint16_t* replyPtr);
+
+#endif 
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/osape/osap/ts.cpp b/firmware/fab-step/src/osape-d21/osape/osap/ts.cpp
new file mode 100644
index 0000000..ffa05e0
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/osape/osap/ts.cpp
@@ -0,0 +1,86 @@
+/*
+osap/ts.cpp
+
+typeset / keys / writing / reading
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "ts.h"
+
+void ts_writeBoolean(boolean val, unsigned char* buf, uint16_t* ptr){
+  if(val){
+    buf[(*ptr) ++] = 1;
+  } else {
+    buf[(*ptr) ++] = 0;
+  }
+}
+
+void ts_readUint16(uint16_t* val, unsigned char* buf, uint16_t* ptr){
+  *val = buf[(*ptr) + 1] << 8 | buf[(*ptr)];
+  *ptr += 2;
+}
+
+void ts_writeUint8(uint8_t val, unsigned char* buf, uint16_t* ptr){
+  buf[(*ptr) ++] = val;
+}
+
+void ts_writeUint16(uint16_t val, unsigned char* buf, uint16_t* ptr){
+  buf[(*ptr) ++] = val & 255;
+  buf[(*ptr) ++] = (val >> 8) & 255;
+}
+
+void ts_readUint32(uint32_t* val, unsigned char* buf, uint16_t* ptr){
+  *val = buf[(*ptr) + 3] << 24 | buf[(*ptr) + 2] << 16 | buf[(*ptr) + 1] << 8 | buf[(*ptr)];
+  *ptr += 4;
+}
+
+void ts_writeUint32(uint32_t val, unsigned char* buf, uint16_t* ptr){
+  buf[(*ptr) ++] = val & 255;
+  buf[(*ptr) ++] = (val >> 8) & 255;
+  buf[(*ptr) ++] = (val >> 16) & 255;
+  buf[(*ptr) ++] = (val >> 24) & 255;
+}
+
+void ts_writeFloat32(float val, volatile unsigned char* buf, uint16_t* ptr){
+  chunk_float32 chunk;
+  chunk.f = val;
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+  buf[(*ptr) ++] = chunk.bytes[2];
+  buf[(*ptr) ++] = chunk.bytes[3];
+}
+
+void ts_writeFloat64(double val, volatile unsigned char* buf, uint16_t* ptr){
+  chunk_float64 chunk;
+  chunk.f = val;
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+  buf[(*ptr) ++] = chunk.bytes[2];
+  buf[(*ptr) ++] = chunk.bytes[3];
+  buf[(*ptr) ++] = chunk.bytes[4];
+  buf[(*ptr) ++] = chunk.bytes[5];
+  buf[(*ptr) ++] = chunk.bytes[6];
+  buf[(*ptr) ++] = chunk.bytes[7];
+}
+
+void ts_writeString(String* val, unsigned char* buf, uint16_t* ptr){
+  uint32_t len = val->length();
+  buf[(*ptr) ++] = len & 255;
+  buf[(*ptr) ++] = (len >> 8) & 255;
+  buf[(*ptr) ++] = (len >> 16) & 255;
+  buf[(*ptr) ++] = (len >> 24) & 255;
+  val->getBytes(&buf[*ptr], len + 1);
+  *ptr += len;
+}
+
+void ts_writeString(String val, unsigned char* buf, uint16_t* ptr){
+  ts_writeString(&val, buf, ptr);
+}
+
diff --git a/firmware/fab-step/src/osape-d21/osape/osap/ts.h b/firmware/fab-step/src/osape-d21/osape/osap/ts.h
new file mode 100644
index 0000000..fefa860
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/osape/osap/ts.h
@@ -0,0 +1,127 @@
+/*
+osap/ts.h
+
+typeset / keys / writing / reading
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef TS_H_
+#define TS_H_
+
+#include <Arduino.h>
+
+// -------------------------------------------------------- Vertex Type Keys
+// will likely use these in the netrunner: 
+
+#define VT_TYPE_ROOT 22
+#define VT_TYPE_MODULE 23
+#define VT_TYPE_ENDPOINT 24 
+#define VT_TYPE_VPORT 44
+#define VT_TYPE_VBUS 45 // maybe bus-drop / bus-head / bus-cohost are differentiated 
+
+// -------------------------------------------------------- TIMES
+
+#define TIMES_STALE_MSG 800
+
+// -------------------------------------------------------- Routing (Packet) Keys
+
+#define PK_PTR 88
+#define PK_DEST 99
+#define PK_SIB_KEY 15 
+#define PK_SIB_INC 3 
+#define PK_PARENT_KEY 16 
+#define PK_PARENT_INC 3 
+#define PK_CHILD_KEY 14 
+#define PK_CHILD_INC 3 
+#define PK_PFWD_KEY 11 
+#define PK_PFWD_INC 1 
+#define PK_BFWD_KEY 12
+#define PK_BFWD_INC 3
+#define PK_SCOPE_REQ_KEY 21
+#define PK_SCOPE_REQ_INC 1
+#define PK_SCOPE_RES_KEY 22 
+#define PK_SCOPE_RES_INC 1
+#define PK_LLESCAPE_KEY 44
+#define PK_LLESCAPE_INC 1
+
+// -------------------------------------------------------- Endpoint Keys 
+
+#define EP_SS_ACK 101       // the ack 
+#define EP_SS_ACKLESS 121   // single segment, no ack 
+#define EP_SS_ACKED 122     // single segment, request ack 
+#define EP_QUERY 131        // query request 
+#define EP_QUERY_RESP 132   // reply to query request 
+
+// -------------------------------------------------------- BUS ACTION KEYS (outside OSAP scope)
+
+#define UB_AK_SETPOS 102
+#define UB_AK_GOTOPOS 105 
+
+// -------------------------------------------------------- Type Keys 
+
+#define TK_BOOL     2
+
+#define TK_UINT8    4
+#define TK_INT8     5
+#define TK_UINT16   6
+#define TK_INT16    7
+#define TK_UINT32   8
+#define TK_INT32    9
+#define TK_UINT64   10
+#define TK_INT64    11
+
+#define TK_FLOAT16  24
+#define TK_FLOAT32  26
+#define TK_FLOAT64  28
+
+// -------------------------------------------------------- Chunks
+
+union chunk_float32 {
+  uint8_t bytes[4];
+  float f;
+};
+
+union chunk_float64 {
+  uint8_t bytes[8];
+  double f;
+};
+
+union chunk_int32 {
+  uint8_t bytes[4];
+  int32_t i;
+};
+
+union chunk_uint32 {
+    uint8_t bytes[4];
+    uint32_t u;
+}; 
+
+// -------------------------------------------------------- Reading and Writing
+
+void ts_writeBoolean(boolean val, unsigned char* buf, uint16_t* ptr);
+
+void ts_readUint16(uint16_t* val, uint8_t* buf, uint16_t* ptr);
+
+void ts_writeUint8(uint8_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeUint16(uint16_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_readUint32(uint32_t* val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeUint32(uint32_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeFloat32(float val, volatile unsigned char* buf, uint16_t* ptr);
+
+void ts_writeFloat64(double val, volatile unsigned char* buf, uint16_t* ptr);
+
+void ts_writeString(String* val, unsigned char* buf, uint16_t* ptr);
+void ts_writeString(String val, unsigned char* buf, uint16_t* ptr);
+
+#endif 
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/osape/osap/vertex.cpp b/firmware/fab-step/src/osape-d21/osape/osap/vertex.cpp
new file mode 100644
index 0000000..987a03c
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/osape/osap/vertex.cpp
@@ -0,0 +1,128 @@
+/*
+osap/vertex.cpp
+
+graph vertex 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "vertex.h"
+#include "../../../syserror.h"
+
+void stackReset(vertex_t* vt){
+  // clear all elements & write next ptrs in linear order 
+  for(uint8_t od = 0; od < 2; od ++){
+    // set lengths, etc, 
+    for(uint8_t s = 0; s < vt->stackSize; s ++){
+      vt->stack[od][s].arrivalTime = 0;
+      vt->stack[od][s].len = 0;
+      vt->stack[od][s].indice = s;
+    }
+    // set next ptrs, 
+    for(uint8_t s = 0; s < vt->stackSize - 1; s ++){
+      vt->stack[od][s].next = &(vt->stack[od][s + 1]);
+    }
+    vt->stack[od][vt->stackSize - 1].next = &(vt->stack[od][0]);
+    // set previous ptrs, 
+    for(uint8_t s = 1; s < vt->stackSize; s ++){
+      vt->stack[od][s].previous = &(vt->stack[od][s - 1]);
+    }
+    vt->stack[od][0].previous = &(vt->stack[od][vt->stackSize - 1]);
+    // 1st element is 0th on startup, 
+    vt->queueStart[od] = &(vt->stack[od][0]); 
+    // first free = tail at init, 
+    vt->firstFree[od] = &(vt->stack[od][0]);
+  }
+}
+
+// -------------------------------------------------------- ORIGIN SIDE 
+// true if there's any space in the stack, 
+boolean stackEmptySlot(vertex_t* vt, uint8_t od){
+  if(od > 1) return false;
+  // if 1st free has ptr to next item, not full 
+  if(vt->firstFree[od]->next->len != 0){
+    return false;
+  } else {
+    return true;
+  }
+}
+
+// loads data into stack 
+void stackLoadSlot(vertex_t* vt, uint8_t od, uint8_t* data, uint16_t len){
+  if(od > 1) return; // bad od, lost data 
+  // copy into first free element, 
+  memcpy(vt->firstFree[od]->data, data, len);
+  vt->firstFree[od]->len = len;
+  vt->firstFree[od]->arrivalTime = millis();
+  //sysError("load " + String(vt->firstFree[od]->indice) + " " + String(vt->firstFree[od]->arrivalTime));
+  // now firstFree is next, 
+  vt->firstFree[od] = vt->firstFree[od]->next;
+}
+
+// -------------------------------------------------------- EXIT SIDE 
+// return count of items occupying stack, and list of ptrs to them, 
+uint8_t stackGetItems(vertex_t* vt, uint8_t od, stackItem** items, uint8_t maxItems){
+  if(od > 1) return 0;
+  // when queueStart == firstFree element, we have nothing for you 
+  if(vt->firstFree[od] == vt->queueStart[od]) return 0;
+  // starting at queue begin, 
+  uint8_t count = 0;
+  stackItem* item = vt->queueStart[od];
+  for(uint8_t s = 0; s < maxItems; s ++){
+    items[s] = item;
+    count ++;
+    if(item->next->len > 0){
+      item = item->next;
+    } else {
+      return count;
+    }
+  }
+  return count;
+}
+
+// clear the item, 
+void stackClearSlot(vertex_t* vt, uint8_t od, stackItem* item){
+  // item is 0-len, etc 
+  item->len = 0;
+  // is this
+  uint8_t indice = item->indice;
+  // if was queueStart, queueStart now at next,
+  if(vt->queueStart[od] == item){
+    vt->queueStart[od] = item->next;
+    // and wouldn't have to do any of the below? 
+  } else {
+    // pull from chain, now is free of associations, 
+    // these ops are *always two up*
+    item->previous->next = item->next;
+    item->next->previous = item->previous;
+
+    // now, insert this where old firstFree was 
+    vt->firstFree[od]->previous->next = item;
+    item->previous = vt->firstFree[od]->previous;
+    
+    item->next = vt->firstFree[od];
+    vt->firstFree[od]->previous = item;
+    // and the item is the new firstFree element, 
+    vt->firstFree[od] = item;
+    // and we can call, 
+  }
+
+  // clear flowcontrol conditions 
+  switch(od){
+    case VT_STACK_ORIGIN:
+      if(vt->onOriginStackClear != nullptr) vt->onOriginStackClear(indice);
+      break;
+    case VT_STACK_DESTINATION:
+      if(vt->onDestinationStackClear != nullptr) vt->onDestinationStackClear(indice);
+      break;
+    default:  // pretty unlikely 
+      sysError("stack clear slot, od > 1, que?");
+      break;
+  }
+}
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/osape/osap/vertex.h b/firmware/fab-step/src/osape-d21/osape/osap/vertex.h
new file mode 100644
index 0000000..bb18816
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/osape/osap/vertex.h
@@ -0,0 +1,92 @@
+/*
+osap/vertex.h
+
+graph vertex 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef VERTEX_H_
+#define VERTEX_H_
+
+#include <Arduino.h> 
+#include "ts.h"
+#include "../../vertices/vertexConfig.h" // vertex config is build dependent, define in <folder-containing-osape>/vertices/vertexConfig.h 
+#include "endpoint.h"
+
+#define VT_STACK_ORIGIN 0 
+#define VT_STACK_DESTINATION 1 
+
+// https://stackoverflow.com/questions/1813991/c-structure-with-pointer-to-self
+
+// linked ringbuffer item 
+struct stackItem {
+    uint8_t data[VT_SLOTSIZE];
+    uint16_t len = 0;
+    unsigned long arrivalTime = 0;
+    uint8_t indice;     // actual physical position in the stack 
+    stackItem* next = nullptr;
+    stackItem* previous = nullptr;
+};
+
+// we have the vertex type, 
+typedef struct vertex_t vertex_t;
+// which are typed by existence of some other object, 
+typedef struct endpoint_t endpoint_t;
+typedef struct vport_t vport_t;
+typedef struct vbus_t vbus_t;
+typedef struct root_t root_t;
+
+struct vertex_t {
+    // a loop code, run once per turn
+    void (*loop)() = nullptr;
+
+    // a type, a position, a name 
+    uint8_t type = 0;
+    uint16_t indice = 0;
+    String name; 
+    // a time tag, for when we git scoped (need for graph traversals)
+    uint32_t scopeTimeTag = 0;
+    // addnl' properties, can possess:
+    endpoint_t* ep = nullptr; 
+    vport_t* vp = nullptr;
+    vbus_t* vb = nullptr;
+    root_t* rt = nullptr; 
+    // stacks; 
+    // origin stack[0] destination stack[1]
+    // destination stack is for messages delivered to this vertex, 
+    stackItem stack[2][VT_STACKSIZE];
+    uint8_t stackSize = VT_STACKSIZE; // should be variable 
+    //uint8_t lastStackHandled[2] = { 0, 0 };
+    stackItem* queueStart[2] = { nullptr, nullptr };    // data is read from the tail  
+    stackItem* firstFree[2] = { nullptr, nullptr };     // data is loaded into the head 
+    // parent & children (other vertices)
+    vertex_t* parent = nullptr;
+    vertex_t* children[VT_MAXCHILDREN]; // I think this is OK on storage: just pointers 
+    uint16_t numChildren = 0;
+    // vertex-as-vport-interface 
+    boolean (*cts)(uint8_t drop) = nullptr;
+    void (*send)(uint8_t* data, uint16_t len, uint8_t rxAddr) = nullptr;
+    uint16_t ownRxAddr = 0;
+    // to notify for clear-out callbacks / flowcontrol etc 
+    void (*onOriginStackClear)(uint8_t slot) = nullptr;
+    void (*onDestinationStackClear)(uint8_t slot) = nullptr;
+};
+
+void stackReset(vertex_t* vt);
+
+// ORIGIN SIDE 
+boolean stackEmptySlot(vertex_t* vt, uint8_t od);
+void stackLoadSlot(vertex_t* vt, uint8_t od, uint8_t* data, uint16_t len);
+
+// EXIT SIDE 
+uint8_t stackGetItems(vertex_t* vt, uint8_t od, stackItem** items, uint8_t maxItems);
+void stackClearSlot(vertex_t* vt, uint8_t od, stackItem* item);
+
+#endif 
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/osape/utils/cobs.cpp b/firmware/fab-step/src/osape-d21/osape/utils/cobs.cpp
new file mode 100644
index 0000000..81cc05b
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/osape/utils/cobs.cpp
@@ -0,0 +1,70 @@
+/*
+utils/cobs.cpp
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "cobs.h"
+// str8 crib from
+// https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing
+
+/** COBS encode data to buffer
+	@param data Pointer to input data to encode
+	@param length Number of bytes to encode
+	@param buffer Pointer to encoded output buffer
+	@return Encoded buffer length in bytes
+	@note doesn't write stop delimiter 
+*/
+size_t cobsEncode(const void *data, size_t length, uint8_t *buffer){
+
+	uint8_t *encode = buffer; // Encoded byte pointer
+	uint8_t *codep = encode++; // Output code pointer
+	uint8_t code = 1; // Code value
+
+	for (const uint8_t *byte = (const uint8_t *)data; length--; ++byte){
+		if (*byte) // Byte not zero, write it
+			*encode++ = *byte, ++code;
+
+		if (!*byte || code == 0xff){ // Input is zero or block completed, restart
+			*codep = code, code = 1, codep = encode;
+			if (!*byte || length)
+				++encode;
+		}
+	}
+	*codep = code;  // Write final code value
+	return encode - buffer;
+}
+
+/** COBS decode data from buffer
+	@param buffer Pointer to encoded input bytes
+	@param length Number of bytes to decode
+	@param data Pointer to decoded output data
+	@return Number of bytes successfully decoded
+	@note Stops decoding if delimiter byte is found
+*/
+size_t cobsDecode(const uint8_t *buffer, size_t length, void *data){
+
+	const uint8_t *byte = buffer; // Encoded input byte pointer
+	uint8_t *decode = (uint8_t *)data; // Decoded output byte pointer
+
+	for (uint8_t code = 0xff, block = 0; byte < buffer + length; --block){
+		if (block) // Decode block byte
+			*decode++ = *byte++;
+		else
+		{
+			if (code != 0xff) // Encoded zero, write it
+				*decode++ = 0;
+			block = code = *byte++; // Next block length
+			if (code == 0x00) // Delimiter code found
+				break;
+		}
+	}
+
+	return decode - (uint8_t *)data;
+}
diff --git a/firmware/fab-step/src/osape-d21/osape/utils/cobs.h b/firmware/fab-step/src/osape-d21/osape/utils/cobs.h
new file mode 100644
index 0000000..b47070c
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/osape/utils/cobs.h
@@ -0,0 +1,24 @@
+/*
+utils/cobs.h
+
+consistent overhead byte stuffing implementation
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UTIL_COBS_H_
+#define UTIL_COBS_H_
+
+#include <Arduino.h>
+
+size_t cobsEncode(const void *data, size_t length, uint8_t *buffer);
+
+size_t cobsDecode(const uint8_t *buffer, size_t length, void *data);
+
+#endif
diff --git a/firmware/fab-step/src/osape-d21/peripheralNums.h b/firmware/fab-step/src/osape-d21/peripheralNums.h
new file mode 100644
index 0000000..eed9f18
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/peripheralNums.h
@@ -0,0 +1,18 @@
+#ifndef PERIPHERAL_NUMS_H_
+#define PERIPHERAL_NUMS_H_
+
+#define PERIPHERAL_A 0
+#define PERIPHERAL_B 1
+#define PERIPHERAL_C 2
+#define PERIPHERAL_D 3
+#define PERIPHERAL_E 4
+#define PERIPHERAL_F 5
+#define PERIPHERAL_G 6
+#define PERIPHERAL_H 7
+#define PERIPHERAL_I 8
+#define PERIPHERAL_K 9
+#define PERIPHERAL_L 10
+#define PERIPHERAL_M 11
+#define PERIPHERAL_N 12
+
+#endif 
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusDrop.cpp b/firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusDrop.cpp
new file mode 100644
index 0000000..ebb12b8
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusDrop.cpp
@@ -0,0 +1,484 @@
+/*
+osap/drivers/ucBusDrop.cpp
+
+beginnings of a uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "ucBusDrop.h"
+
+#ifdef UCBUS_IS_DROP
+
+#include "../../../indicators.h"
+#include "../../../syserror.h"
+#include "../../peripheralNums.h"
+#include "ucBusDipConfig.h"
+#include "../../osape/utils/cobs.h"
+
+// recieve buffers
+uint8_t recieveBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t recieveBufferWp[UB_CH_COUNT];
+// tracking did-last-msg have token,
+volatile boolean lastWordHadToken[UB_CH_COUNT];
+
+// stash buffers (have to ferry data from rx buffer -> here immediately on rx, else next word can overwrite)
+uint8_t inBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t inBufferLen[UB_CH_COUNT];
+
+// output buffer 
+uint8_t outBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t outBufferRp[UB_CH_COUNT];
+volatile uint16_t outBufferLen[UB_CH_COUNT];
+
+// receive word
+UCBUS_HEADER_Type inHeader = { .bytes = { 0,0 } };
+volatile uint8_t inWordWp = 0;
+uint8_t inWord[UB_HEAD_BYTES_PER_WORD];
+
+// outgoing word 
+UCBUS_HEADER_Type outHeader = { .bytes = { 0,0 } };
+uint8_t outWord[UB_DROP_BYTES_PER_WORD];
+volatile uint8_t outWordRp = 0;
+
+// reciprocal buffer space, for flowcontrol 
+volatile uint8_t rcrxb[UB_CH_COUNT];
+
+// our physical bus address, 
+volatile uint8_t id = 0;
+
+// available time count, in bus tick units 
+volatile uint16_t timeTick = 0;
+volatile uint64_t timeBlink = 0;
+uint16_t blinkTime = 1000;
+
+// baudrate 
+uint32_t ub_baud_val = 0;
+
+// we need to track interrupt states as well as setting the flags in the micro, 
+// since the D21 fires only one ISR for all of the flags;
+volatile boolean txcISR = false;
+volatile boolean dreISR = false;
+
+#define DRE_ISR_ON UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_DRE; dreISR = true
+#define DRE_ISR_OFF UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE; dreISR = false 
+#define TXC_ISR_ON UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_TXC; txcISR = true 
+#define TXC_ISR_OFF UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_TXC; txcISR = false 
+
+#ifdef UCBUS_IS_D51 
+// ------------------------------------ D51 SPECIFIC 
+// hardware init (file scoped)
+void setupBusDropUART(void){
+  // set driver output LO to start: tri-state 
+  UB_DE_PORT.DIRSET.reg = UB_DE_BM;
+  UB_DRIVER_DISABLE;
+  // set receiver output on, forever: LO to set on 
+  UB_RE_PORT.DIRSET.reg = UB_RE_BM;
+  UB_RE_PORT.OUTCLR.reg = UB_RE_BM;
+  // termination resistor should be set only on one drop, 
+  // or none and physically with a 'tail' cable, or something? 
+  UB_TE_PORT.DIRSET.reg = UB_TE_BM;
+  if(dip_readPin1()){
+    UB_TE_PORT.OUTCLR.reg = UB_TE_BM;
+  } else {
+    UB_TE_PORT.OUTSET.reg = UB_TE_BM;
+  }
+  // rx pin setup
+  UB_COMPORT.DIRCLR.reg = UB_RXBM;
+  UB_COMPORT.PINCFG[UB_RXPIN].bit.PMUXEN = 1;
+  if(UB_RXPIN % 2){
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_RXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_RXPERIPHERAL);
+  }
+  // tx
+  UB_COMPORT.DIRCLR.reg = UB_TXBM;
+  UB_COMPORT.PINCFG[UB_TXPIN].bit.PMUXEN = 1;
+  if(UB_TXPIN % 2){
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_TXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_TXPERIPHERAL);
+  }
+  // ok, clocks, first line au manuel
+  	// unmask clocks 
+	MCLK->APBAMASK.bit.SERCOM1_ = 1;
+  GCLK->GENCTRL[UB_GCLKNUM_PICK].reg = GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_DFLL) | GCLK_GENCTRL_GENEN;
+  while(GCLK->SYNCBUSY.reg & GCLK_SYNCBUSY_GENCTRL(UB_GCLKNUM_PICK));
+	GCLK->PCHCTRL[UB_SERCOM_CLK].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(UB_GCLKNUM_PICK);
+  // then, sercom
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 0;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  UB_SER_USART.CTRLA.bit.SWRST = 1;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST || UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  // ctrla 
+  UB_SER_USART.CTRLA.reg = SERCOM_USART_CTRLA_MODE(1) | SERCOM_USART_CTRLA_DORD;
+  UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_RXPO(UB_RXPO) | SERCOM_USART_CTRLA_TXPO(0);
+  //UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_FORM(1); // enable even parity 
+  // ctrlb 
+  while(UB_SER_USART.SYNCBUSY.bit.CTRLB);
+  UB_SER_USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN | SERCOM_USART_CTRLB_CHSIZE(0);
+	// enable interrupts 
+	NVIC_EnableIRQ(SERCOM1_2_IRQn); // rx interrupts 
+  NVIC_EnableIRQ(SERCOM1_1_IRQn); // transmit complete interrupt 
+	NVIC_EnableIRQ(SERCOM1_0_IRQn); // data register empty interrupts 
+	// set baud 
+  UB_SER_USART.BAUD.reg = ub_baud_val;
+  // and finally, a kickoff
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 1;
+  // enable rx interrupt, disable dre, txc 
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE | SERCOM_USART_INTENCLR_TXC;
+  // to enable tx complete, 
+  //UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_TXC; // now watch transmit complete
+}
+
+// DRE handler 
+void SERCOM1_0_Handler(void){
+  ucBusDrop_dreISR();
+}
+
+// TXC handler 
+void SERCOM1_1_Handler(void){
+  ucBusDrop_txcISR();
+}
+
+void SERCOM1_2_Handler(void){
+	ucBusDrop_rxISR();
+}
+// ------------------------------------ END D51 SPECIFIC 
+#endif 
+
+#ifdef UCBUS_IS_D21 
+// ------------------------------------ D21 SPECIFIC 
+void setupBusDropUART(void){
+  // ------------------------------------------ USART PIN CONFIG
+  // setup pins as output or inputs,
+  UB_PORT.DIRSET.reg = UB_TXBM;
+  UB_PORT.DIRCLR.reg = UB_RXBM;
+  // pincfg using wrconfig write, s/o
+  // https://community.atmel.com/forum/sam-d21-spi-interface-bare-code
+  PORT_WRCONFIG_Type wrconfig;  // make new write config object,
+  wrconfig.bit.WRPMUX = 1;      // it will write to pmux
+  wrconfig.bit.WRPINCFG = 1;    // it will write to pinconfig
+  wrconfig.bit.PMUX = MUX_PA16C_SERCOM1_PAD0;  // with this pmux setting
+                                                // (putting 16 on c, for ser1)
+  wrconfig.bit.PMUXEN = 1;                     // enabling pin muxing
+  wrconfig.bit.HWSEL = 1;  // writing to the upper half of the pins
+                            // and (below) writing these pins, masked and
+                            // shifted into the lower half
+  wrconfig.bit.PINMASK = (uint16_t)((UB_TXBM | UB_RXBM) >> 16);
+  UB_PORT.WRCONFIG.reg = wrconfig.reg;  // here's the one-shot write, using prep above
+  // ------------------------------------------ Transmit Driver / Recieve
+  // Driver Enable
+  UB_DE_SETUP;
+  UB_RE_SETUP;
+  // ------------------------------------------ SPI CONFIG
+  // now, lettuce unmask the peripheral SER1
+  PM->APBCMASK.reg |= PM_APBCMASK_SERCOM1;
+  // hook the peripheral up to our main CPU clock, which is running at 48mHz
+  // on the D21
+  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 |
+                      GCLK_CLKCTRL_ID_SERCOM1_CORE;
+  while (GCLK->STATUS.bit.SYNCBUSY);
+  // now we can setup the actual sercom, first do a reset for posterity and
+  // await complete
+  UB_SER_USART.CTRLA.bit.SWRST = 1;
+  while (UB_SER_USART.SYNCBUSY.bit.SWRST);
+  // pinout: TX on SERx-0, RX on SERx-2
+  UB_SER_USART.CTRLA.reg = SERCOM_USART_CTRLA_DORD |     // lsb first
+                            SERCOM_USART_CTRLA_MODE(1) |  // internal clock
+                            SERCOM_USART_CTRLA_TXPO(0) |  // tx on SERx-0
+                            SERCOM_USART_CTRLA_RXPO(UB_RXPO);  // rx on SERx-3
+  // enable reciever, transmit,
+  UB_SER_USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN;
+  // set BAUD:
+  UB_SER_USART.BAUD.reg = SERCOM_USART_BAUD_BAUD(ub_baud_val);
+  // we will use interrupts,
+  NVIC_EnableIRQ(SERCOM1_IRQn);
+  // rx interrupt always
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE | SERCOM_USART_INTENCLR_TXC;
+  // UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  // ok I think that's it?
+  UB_SER_USART.CTRLA.bit.ENABLE = 1;
+  while (UB_SER_USART.SYNCBUSY.bit.ENABLE);
+}
+
+void SERCOM1_Handler(void) {
+  if (UB_SER_USART.INTFLAG.reg & SERCOM_USART_INTFLAG_RXC) {
+    ucBusDrop_rxISR();
+  } 
+  if (UB_SER_USART.INTFLAG.reg & SERCOM_USART_INTFLAG_DRE && dreISR) {
+    ucBusDrop_dreISR();
+  } 
+  if (UB_SER_USART.INTFLAG.reg & SERCOM_USART_INTFLAG_TXC && txcISR){
+    ucBusDrop_txcISR();
+  } 
+} // ------------------------------------------------------ END SERCOM ISR
+// ------------------------------------ END D21 SPECIFIC 
+#endif 
+
+void ucBusDrop_setup(boolean useDipPick, uint8_t ID) {
+  #ifdef UCBUS_IS_D51
+  dip_setup();
+  if(useDipPick){
+    // set our id, 
+    id = dip_readLowerFive(); // should read lower 4, now that cha / chb 
+  } else {
+    id = ID;
+  }
+  #endif 
+  #ifdef UCBUS_IS_D21
+  id = ID;
+  #endif 
+  if(id > 31){ id = 31; }   // max 31 drops, logical addresses 1 - 31
+  if(id == 0){ id = 1; }    // 0 'tap' is the clk reset, bump up... maybe cause confusion: instead could flash err light 
+  // setup input / etc buffers 
+  for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+    recieveBufferWp[ch] = 0;
+    inBufferLen[ch] = 0;
+    outBufferRp[ch] = 0;
+    outBufferLen[ch] = 0;
+    rcrxb[ch] = 0;
+  }
+  // pick baud, via top level config.h 
+  // baud bb baud
+  // 63019 for a very safe 115200
+  // 54351 for a go-karting 512000
+  // 43690 for a trotting pace of 1MHz
+  // 21845 for the E30 2MHz
+  // 0 for max-speed 3MHz
+  switch(UCBUS_BAUD){
+    case 1:
+      ub_baud_val = 43690;
+      break;
+    case 2: 
+      ub_baud_val = 21845;
+      break;
+    case 3: 
+      ub_baud_val = 0;
+      break;
+    default:
+      ub_baud_val = 43690;
+  }
+  // start the hardware 
+  setupBusDropUART();
+}
+
+uint16_t ucBusDrop_getOwnID(void){
+  return id;
+}
+
+void ucBusDrop_rxISR(void){
+  // ------------------------------------------------------ DATA INGEST
+  // get the data 
+  uint8_t data = UB_SER_USART.DATA.reg;
+  inWord[inWordWp ++] = data;
+  // tracking delineation 
+  if(inWordWp >= UB_HEAD_BYTES_PER_WORD){
+    // always reset, never overwrite inWord[] tail
+    inWordWp = 0;
+    // is lastchar the rarechar ?
+    if(inWord[UB_HEAD_BYTES_PER_WORD - 1] == UCBUS_RARECHAR){
+      // carry on, 
+    } else {
+      // restart on appearance of rarechar 
+      for(uint8_t b = 0; b < UB_HEAD_BYTES_PER_WORD; b ++){
+        if(inWord[b] == UCBUS_RARECHAR){
+          inWordWp = UB_HEAD_BYTES_PER_WORD - 1 - b;
+          // in case the above ^ causes some wrapping case (?) don't think it does though 
+          if(inWordWp >= UB_HEAD_BYTES_PER_WORD) inWordWp = 0;
+          return;
+        }
+      }
+    }
+  } else {
+    // was just data byte, bail for now 
+    return;
+  }
+  // ------------------------------------------------------ TERMINAL BYTE CASE 
+  // blink on count-of-words:
+  timeTick ++;
+  timeBlink ++;
+  if(timeBlink >= blinkTime){
+    CLKLIGHT_TOGGLE; 
+    timeBlink = 0;
+  }
+  // extract the header, 
+  inHeader.bytes[0] = inWord[0];
+  inHeader.bytes[1] = inWord[1];
+  // now, check for our-rx:
+  if(inHeader.bits.DROPTAP == id){  // -------------------- OUR TAP, TX CASE 
+    // read-in fc states, 
+    rcrxb[0] = inHeader.bits.CH0FC;
+    rcrxb[1] = inHeader.bits.CH1FC;
+    // reset out header,
+    outHeader.bytes[0] = 0; 
+    outHeader.bytes[1] = 0;
+    // write outgoing flowcontrol terms: if we have unread buffers on these chs, zero space avail:
+    outHeader.bits.CH0FC = (inBufferLen[0] ?  0 : 1);
+    outHeader.bits.CH1FC = (inBufferLen[1] ?  0 : 1);
+    // write also our drop tap...
+    outHeader.bits.DROPTAP = id;
+    // check about tx state, 
+    for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+      if(outBufferLen[ch] && rcrxb[ch] > 0){
+        // can tx this ch, 
+        uint8_t numTx = outBufferLen[ch] - outBufferRp[ch];
+        if(numTx > UB_DATA_BYTES_PER_WORD) numTx = UB_DATA_BYTES_PER_WORD;
+        // can fill ch-output, 
+        outHeader.bits.CHSELECT = ch;
+        outHeader.bits.TOKENS = numTx;
+        // fill bytes,
+        uint8_t* outB = outBuffer[ch];
+        uint16_t outBRp = outBufferRp[ch];
+        for(uint8_t b = 0; b < numTx; b ++){
+          outWord[b + 2] = outB[outBRp + b];  // fill from ob[2], ob[0] and ob[1] are header 
+        }
+        outBufferRp[ch] += numTx;
+        // if numTx < data bytes / frame, packet terminates this word, we reset 
+        if(numTx < UB_DATA_BYTES_PER_WORD){
+          outBufferLen[ch] = 0;
+          outBufferRp[ch] = 0;
+        }
+        break; // don't check next ch, 
+      }
+    }
+    // stuff header -> word
+    outWord[0] = outHeader.bytes[0];
+    outWord[1] = outHeader.bytes[1];
+    // now setup the transmit action:
+    // set driver on, ship 1st byte, tx rest on DRE edges 
+    outWordRp = 1; // next is [1]
+    UB_DRIVER_ENABLE;
+    UB_SER_USART.DATA.reg = outWord[0];
+    DRE_ISR_ON;
+  } // ---------------------------------------------------- END TX CASE 
+
+  // ------------------------------------------------------ BEGIN RX TERMS 
+  // the ch that head tx'd to 
+  uint8_t rxCh = inHeader.bits.CHSELECT;
+  // and # bytes tx'd here 
+  uint8_t numToken = inHeader.bits.TOKENS;
+  // check for broken numToken count,
+  if(numToken > UB_DATA_BYTES_PER_WORD) { ERROR(1, "ucbus-drop outsize numToken rx"); return; }
+  // don't overfill recieve buffer: 
+  if(recieveBufferWp[rxCh] + numToken > UB_BUFSIZE){
+    recieveBufferWp[rxCh] = 0;
+    ERROR(1, "ucbus-drop rx overfull buffer");
+    return;
+  }
+  // so let's see, if we have any we write them in:
+  if(numToken > 0){
+    uint8_t* rxB = recieveBuffer[rxCh];
+    uint16_t rxBWp = recieveBufferWp[rxCh]; 
+    for(uint8_t i = 0; i < numToken; i ++){
+      rxB[rxBWp + i] = inWord[2 + i];
+    }
+    recieveBufferWp[rxCh] += numToken;
+    // set in-packet state,
+    lastWordHadToken[rxCh] = true;
+  }
+  // to find the edge, if we have numToken < numDataBytes and have at least one previous
+  // token in stream, we have pckt edge 
+  if((numToken < UB_DATA_BYTES_PER_WORD) && lastWordHadToken[rxCh]){
+    // reset token edge
+    lastWordHadToken[rxCh] = false;
+    // pckt edge on this ch, shift recieveBuffer -> inBuffer and reset write pointer 
+    // unfortunately we have to do this literal-swap thing (some memcpy coming up here), 
+    // but should be able to use a pointer-swapping approach later. here we check if the pck 
+    // is actually for us, then if we can accept it (fc not violated) and then swap it in:
+    if(recieveBuffer[rxCh][0] == id || recieveBuffer[rxCh][0] == 0){
+      // we should accept this, can we?
+      if(inBufferLen[rxCh] != 0){ // failed to clear before new arrival, FC has failed 
+        recieveBufferWp[rxCh] = 0;
+        ERROR(0, "ucbus-drop rx fc fails");
+        return;
+      } // end check-for-overwrite 
+      // copy from rxbuffer to inbuffer, it's ours... now FC will go lo, head should not tx *to us*
+      // before it is cleared with ucBusDrop_readB()
+      memcpy(inBuffer[rxCh], recieveBuffer[rxCh], recieveBufferWp[rxCh]);
+      inBufferLen[rxCh] = recieveBufferWp[rxCh];
+      recieveBufferWp[rxCh] = 0;
+      // if CH0, fire "RT" on-rx interrupt, this is where we should want RTOS in the future 
+      if(rxCh == 0){
+        ucBusDrop_onPacketARx(&(inBuffer[0][1]), inBufferLen[0] - 1);
+        // assuming the interrupt is the exit for time being,
+        inBufferLen[0] = 0;
+      }
+      //DEBUG1PIN_OFF;
+    } else {
+      // packet wasn't for us, ignore 
+      recieveBufferWp[rxCh] = 0;
+    }
+  } // ---------------------------------------------------- END RX TERMS
+
+  // finally (and a bit yikes) we call the onRxISR on *every* word, that's our 
+  // synced system clock: fair warning though, we're firing this pretty late
+  // esp. if we have also this time transmitted, read in a packet, etc... yikes 
+  ucBusDrop_onRxISR();
+} // end rx-isr 
+
+void ucBusDrop_dreISR(void){
+  UB_SER_USART.DATA.reg = outWord[outWordRp ++];
+  if(outWordRp >= UB_DROP_BYTES_PER_WORD){
+    DRE_ISR_OFF; // clear tx-empty int.
+    TXC_ISR_ON;  // set tx-complete int.
+  } 
+}
+
+void ucBusDrop_txcISR(void){
+  UB_SER_USART.INTFLAG.reg = SERCOM_USART_INTFLAG_TXC;   // clear flag (so interrupt not called again)
+  TXC_ISR_OFF;
+  UB_DRIVER_DISABLE;
+}
+
+// -------------------------------------------------------- ASYNC API
+
+boolean ucBusDrop_ctrB(void){
+  // clear to read a packet when this buffer occupied... 
+  return (inBufferLen[1] > 0);
+}
+
+size_t ucBusDrop_readB(uint8_t *dest){
+  if(!ucBusDrop_ctrB()) return 0;
+  // to read-out, we rm the 0th byte which is addr information
+  size_t len = inBufferLen[1] - 1;
+  memcpy(dest, &(inBuffer[1][1]), len);
+  inBufferLen[1] = 0; // now it's empty 
+  return len;
+}
+
+boolean ucBusDrop_ctsB(void){
+  if(outBufferLen[1] == 0 && rcrxb[1] > 0){
+    return true;
+  } else {
+    return false;
+  }
+}
+
+void ucBusDrop_transmitB(uint8_t *data, uint16_t len){
+  if(!ucBusDrop_ctsB()) return;
+  // we don't need to decriment our count of the remote rcrxb here
+  // because we get an update from the head on their actual rcrxb *each time we are tapped*
+  // however, we cannot tx more than the bufsize, bruh 
+  if(len > UB_BUFSIZE) return;
+  // copy it into the outBuffer, 
+  memcpy(&(outBuffer[1]), data, len);
+  // needs to be interrupt safe: transmit could start between these lines
+  __disable_irq();
+  outBufferLen[1] = len;
+  outBufferRp[1] = 0;
+  __enable_irq();
+}
+
+#endif 
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusDrop.h b/firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusDrop.h
new file mode 100644
index 0000000..5861b98
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusDrop.h
@@ -0,0 +1,47 @@
+/*
+osap/drivers/ucBusDrop.h
+
+beginnings of a uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UCBUS_DROP_H_
+#define UCBUS_DROP_H_
+
+#include "../../../config.h"
+
+#ifdef UCBUS_IS_DROP
+
+#include <Arduino.h>
+#include "ucBusMacros.h"
+
+// setup 
+void ucBusDrop_setup(boolean useDipPick, uint8_t ID);
+uint16_t ucBusDrop_getOwnID(void);
+
+// isrs 
+void ucBusDrop_rxISR(void);
+void ucBusDrop_dreISR(void);
+void ucBusDrop_txcISR(void);
+
+// handlers (define in main.cpp, these are application interfaces)
+void ucBusDrop_onRxISR(void);
+void ucBusDrop_onPacketARx(uint8_t* inBufferA, volatile uint16_t len);
+
+// the api, eh 
+boolean ucBusDrop_ctrB(void);
+size_t ucBusDrop_readB(uint8_t *dest);
+
+// drop cannot tx to channel A
+boolean ucBusDrop_ctsB(void); // true if tx buffer empty, 
+void ucBusDrop_transmitB(uint8_t *data, uint16_t len);
+
+#endif
+#endif 
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusHead.cpp b/firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusHead.cpp
new file mode 100644
index 0000000..f55b30d
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusHead.cpp
@@ -0,0 +1,375 @@
+/*
+osap/drivers/ucBusHead.cpp
+
+uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "ucBusHead.h"
+
+#ifdef UCBUS_IS_HEAD
+
+#include <Arduino.h>
+
+#include "../../../indicators.h"
+#include "../../../syserror.h"
+#include "../../peripheralNums.h"
+#include "../../d51ClockBoss.h"
+#include "../../osape/utils/cobs.h"
+
+// input buffers / space 
+uint8_t inBuffer[UB_CH_COUNT][UB_MAX_DROPS][UB_BUFSIZE];   // per-drop incoming bytes: 0 will be empty always, no drop here
+volatile uint16_t inBufferWp[UB_CH_COUNT][UB_MAX_DROPS];   // per-drop incoming write pointer
+volatile uint16_t inBufferLen[UB_CH_COUNT][UB_MAX_DROPS];  // per-drop incoming bytes, len of, set when EOP detected
+volatile boolean lastWordHadToken[UB_CH_COUNT][UB_MAX_DROPS];
+
+// transmit buffers 
+uint8_t outBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t outBufferRp[UB_CH_COUNT];
+volatile uint16_t outBufferLen[UB_CH_COUNT];
+
+// flow control, per ch per drop 
+volatile uint8_t rcrxb[UB_CH_COUNT][UB_MAX_DROPS];     // if 0 donot tx on this ch / this drop 
+
+// currently 'tapped' drop - we loop thru bus drops, 
+volatile uint8_t currentDropTap = 1; // drop we are currently 'txing' to / drop that will reply on this cycle
+volatile uint8_t lastDropTap = 1; 
+
+// outgoing word / stuff info 
+volatile UCBUS_HEADER_Type outHeader = { .bytes = { 0, 0 } };
+uint8_t outWord[UB_HEAD_BYTES_PER_WORD];                // this goes on-the-line, 
+volatile uint8_t outWordRp = 0;
+
+// incoming word 
+volatile UCBUS_HEADER_Type inHeader = { .bytes = { 0, 0 } };
+uint8_t inWord[UB_DROP_BYTES_PER_WORD];
+uint8_t inWordWp = 0;
+
+// baudrate 
+uint32_t ub_baud_val = 0;
+
+// uart init (file scoped)
+void setupBusHeadUART(void){
+  // driver output is always on at head, set HI to enable
+  UB_DE_PORT.DIRSET.reg = UB_DE_BM;
+  UB_DE_PORT.OUTSET.reg = UB_DE_BM;
+  // receive output is always on at head, set LO to enable
+  UB_RE_PORT.DIRSET.reg = UB_RE_BM;
+  UB_RE_PORT.OUTCLR.reg = UB_RE_BM;
+  // termination resistor for receipt on bus head is always on, set LO to enable 
+  UB_TE_PORT.DIRSET.reg = UB_TE_BM;
+  UB_TE_PORT.OUTCLR.reg = UB_TE_BM;
+  // rx pin setup
+  UB_COMPORT.DIRCLR.reg = UB_RXBM;
+  UB_COMPORT.PINCFG[UB_RXPIN].bit.PMUXEN = 1;
+  if(UB_RXPIN % 2){
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_RXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_RXPERIPHERAL);
+  }
+  // tx
+  UB_COMPORT.DIRCLR.reg = UB_TXBM;
+  UB_COMPORT.PINCFG[UB_TXPIN].bit.PMUXEN = 1;
+  if(UB_TXPIN % 2){
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_TXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_TXPERIPHERAL);
+  }
+  // ok, clocks, first line au manuel
+  // unmask clocks 
+	MCLK->APBAMASK.bit.SERCOM1_ = 1;
+  GCLK->GENCTRL[UB_GCLKNUM_PICK].reg = GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_DFLL) | GCLK_GENCTRL_GENEN;
+  while(GCLK->SYNCBUSY.reg & GCLK_SYNCBUSY_GENCTRL(UB_GCLKNUM_PICK));
+	GCLK->PCHCTRL[UB_SERCOM_CLK].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(UB_GCLKNUM_PICK);
+  // then, sercom: disable and then perform software reset
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 0;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  UB_SER_USART.CTRLA.bit.SWRST = 1;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST || UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  // ok, CTRLA:
+  UB_SER_USART.CTRLA.reg = SERCOM_USART_CTRLA_MODE(1) | SERCOM_USART_CTRLA_DORD; // data order (1: lsb first) and mode (?) 
+  UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_RXPO(UB_RXPO) | SERCOM_USART_CTRLA_TXPO(0); // rx and tx pinout options 
+  //UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_FORM(1); // turn on parity: parity is even by default (set in CTRLB), leave that 
+  // CTRLB has sync bit, 
+  while(UB_SER_USART.SYNCBUSY.bit.CTRLB);
+  // recieve enable, txenable, character size 8bit, 
+  UB_SER_USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN | SERCOM_USART_CTRLB_CHSIZE(0);
+  // CTRLC: setup 32 bit on read and write:
+  // UBH_SER_USART.CTRLC.reg = SERCOM_USART_CTRLC_DATA32B(3); 
+	// enable interrupts 
+	NVIC_EnableIRQ(SERCOM1_2_IRQn); // rx interrupts 
+  NVIC_EnableIRQ(SERCOM1_1_IRQn); // transmit complete interrupt 
+	NVIC_EnableIRQ(SERCOM1_0_IRQn); // data register empty interrupts 
+	// set baud 
+  UB_SER_USART.BAUD.reg = ub_baud_val;
+  // and finally, a kickoff
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 1;
+  // enable the RXC interrupt, disable TXC, DRE
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE | SERCOM_USART_INTENCLR_TXC;
+}
+
+// TX Handler, for second bytes initiated by timer, 
+// void SERCOM1_0_Handler(void){
+// 	ucBusHead_txISR();
+// }
+
+// startup, 
+void ucBusHead_setup(void){
+  // clear buffers to begin,
+  for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+    outBufferLen[ch] = 0;
+    outBufferRp[ch] = 0;
+    for(uint8_t d = 0; d < UB_MAX_DROPS; d ++){
+      inBufferLen[ch][d] = 0; // zero all input buffers, write-in pointers
+      inBufferWp[ch][d] = 0;
+      rcrxb[ch][d] = 0;       // assume zero space to tx to all drops until they report otherwise 
+      lastWordHadToken[ch][d] = false;
+    }
+  }
+  // pick baud, via top level config.h 
+  // baud bb baud
+  // 63019 for a very safe 115200
+  // 54351 for a go-karting 512000
+  // 43690 for a trotting pace of 1MHz
+  // 21845 for the E30 2MHz
+  // 0 for max-speed 3MHz
+  switch(UCBUS_BAUD){
+    case 1:
+      ub_baud_val = 43690;
+      break;
+    case 2: 
+      ub_baud_val = 21845;
+      break;
+    case 3: 
+      ub_baud_val = 0;
+      break;
+    default:
+      ub_baud_val = 43690;
+  }
+  // start the uart, 
+  setupBusHeadUART();
+  // ! alert ! need to setup timer in main.cpp 
+}
+
+void ucBusHead_timerISR(void){
+  // increment / wrap time division for drops  
+  currentDropTap ++;
+  if(currentDropTap > UB_MAX_DROPS){ // recall that tapping '0' should operate the clock reset, addr 0 doesn't exist 
+    currentDropTap = 1;
+  }
+  // reset the outgoing header, 
+  outHeader.bytes[0] = 0; 
+  outHeader.bytes[1] = 0;
+  // write in drop tap, flowcontrol rules 
+  outHeader.bits.CH0FC = (inBufferLen[0][currentDropTap] ?  0 : 1);
+  outHeader.bits.CH1FC = (inBufferLen[1][currentDropTap] ?  0 : 1);
+  outHeader.bits.DROPTAP = currentDropTap;                
+  // now we check if we can tx on either channel, 
+  for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+    // do we have ahn pck to be tx'ing, and is flowcontrol condition met 
+    // FC: outBuffer[x][0] is the 'addr' we are tx'ing to, so indexes relevant rcrxb as well
+    // ! and, when we broadcast (addr '0') we ignore FC rules, so: 
+    if(outBufferLen[ch] > 0 && (rcrxb[ch][outBuffer[ch][0]] | (outBuffer[ch][0] == 0))){
+      // ch has incomplete-tx of some packet 
+      // count them, max we will transmit is from word length: 
+      uint8_t numTx = outBufferLen[ch] - outBufferRp[ch];
+      if(numTx > UB_DATA_BYTES_PER_WORD) numTx = UB_DATA_BYTES_PER_WORD;
+      // we can write the 2nd header byte (ch select and # of words)
+      outHeader.bits.CHSELECT = ch;
+      outHeader.bits.TOKENS = numTx;
+      // fill bytes, 
+      uint8_t *outB = outBuffer[ch];
+      uint16_t outBRp = outBufferRp[ch];
+      for(uint8_t b = 0; b < numTx; b ++){ 
+        outWord[b + 2] = outB[outBRp + b];
+      }
+      outBufferRp[ch] += numTx;
+      // if numTx < data words per packet, packet will terminate this frame, we can reset 
+      // recipient uses the tailing '0' token-d byte to delineate packets (COBS for words)
+      if(numTx < UB_DATA_BYTES_PER_WORD) {
+        // flow control: we have tx'd to whichever drop... the head recieves updates from drops 
+        // for rcrxb, but they're potentially spaced 1/64 turns of this ISR, 
+        // so we need to update our accounting of their space-available-to-receive.
+        // recall also that rcrxb is parallel per channel *and* per drop 
+        rcrxb[ch][outBuffer[ch][0]] = 0; // 0 space available here now, 
+        outBufferLen[ch] = 0; // reset also the outgoing buffer,
+        outBufferRp[ch] = 0;  // and it's read-out ptr 
+      }
+      break; // don't check the next ch, outword occupied by this 
+    }
+  }
+  // stuff header -> outWord
+  outWord[0] = outHeader.bytes[0];
+  outWord[1] = outHeader.bytes[1];
+  // insert rarechar 
+  outWord[UB_HEAD_BYTES_PER_WORD - 1] = UCBUS_RARECHAR;
+  // now we transmit: 
+  UB_SER_USART.DATA.reg = outWord[0];
+  outWordRp = 1; // next up, 
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_DRE;
+}
+
+// data register empty: bang next byte in 
+void SERCOM1_0_Handler(void){
+  UB_SER_USART.DATA.reg = outWord[outWordRp ++];
+  if(outWordRp >= UB_HEAD_BYTES_PER_WORD){ // if we've transmitted them all, 
+    UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE; // clear tx-data-empty interrupt 
+    UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_TXC; // set tx-complete interrupt 
+  }
+}
+
+// transmit complete interrupt: delimit incoming words 
+void SERCOM1_1_Handler(void){
+  UB_SER_USART.INTFLAG.bit.TXC = 1;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_TXC;
+  // this means the latest word transmit is done, next byte on the line should be 1st in 
+  // upstream pckt 
+  lastDropTap = currentDropTap;
+  inWordWp = 0;
+}
+
+// rx handler, for incoming
+void SERCOM1_2_Handler(void){
+	ucBusHead_rxISR();
+}
+
+void ucBusHead_rxISR(void){
+	// shift the byte -> incoming, 
+  inWord[inWordWp ++] = UB_SER_USART.DATA.reg;
+  if(inWordWp >= UB_DROP_BYTES_PER_WORD){
+    // that's ^ word delineation, so our drop tap should be:
+    uint8_t rxDrop = lastDropTap; 
+    // check that, 
+    inHeader.bytes[0] = inWord[0];
+    inHeader.bytes[1] = inWord[1];
+    if(inHeader.bits.DROPTAP != rxDrop){ return; } // bail on mismatch, was a bad / misaligned word
+    // update our buffer states, 
+    rcrxb[0][rxDrop] = inHeader.bits.CH0FC;
+    rcrxb[1][rxDrop] = inHeader.bits.CH1FC; 
+    // the ch that drop tx'd on 
+    uint8_t rxCh = inHeader.bits.CHSELECT;
+    // has anything?
+    uint8_t numToken = inHeader.bits.TOKENS;
+    // check for broken numToken count,
+    if(numToken > UB_DATA_BYTES_PER_WORD) { ERROR(1, "ucbus-head outsize numToken rx"); return; }
+    // if we are filling this buffer, but it's already occupied, fc has failed and we
+    if(inBufferLen[rxCh][rxDrop] != 0){ ERROR(0, "ucbus-head rx FC broken"); return; }
+    // donot write past buffer size,
+    if(inBufferWp[rxCh][rxDrop] + numToken > UB_BUFSIZE){
+      inBufferWp[rxCh][rxDrop] = 0;
+      ERROR(0, "ucbus-head rx packet too-long");
+      return;
+    }
+    // shift bytes into rx buffer 
+    uint8_t * inB = inBuffer[rxCh][rxDrop];
+    uint16_t inBWp = inBufferWp[rxCh][rxDrop];
+    for(uint8_t i = 0; i < numToken; i ++){
+      inB[inBWp + i] = inWord[2 + i];
+    }
+    inBufferWp[rxCh][rxDrop] += numToken;
+    // to find packet edge, if we have numToken > numDataBytes and at least 
+    // one other in the stream, we have pckt edge
+    if(numToken > 0) lastWordHadToken[rxCh][rxDrop] = true;
+    if(numToken < UB_DATA_BYTES_PER_WORD && lastWordHadToken[rxCh][rxDrop]){
+      // packet edge, reset token edge
+      lastWordHadToken[rxCh][rxDrop] = false;
+      // pckt edge is here, set fullness, otherwise we're done, 
+      // application responsible for shifting it out and 
+      // inBufferLen is what we read to determine FC condition 
+      inBufferLen[rxCh][rxDrop] = inBufferWp[rxCh][rxDrop];
+      inBufferWp[rxCh][rxDrop] = 0;
+    }
+  }
+}
+
+// -------------------------------------------------------- API 
+
+// clear to read ? channel select ? 
+#warning TODO: bus head read per-ch: yep, should be a or b, 
+boolean ucBusHead_ctr(uint8_t drop){
+  // called once per loop, so here's where this debug goes:
+  //(rcrxb[1] > 0) ? DEBUG2PIN_OFF : DEBUG2PIN_ON; // for psu-breakout,
+  //(rcrxb[2] > 0) ? DEBUG3PIN_OFF : DEBUG3PIN_ON; // pin off is light on
+  if(drop >= UB_MAX_DROPS) return false;
+  if(inBufferLen[1][drop] > 0){
+    return true;
+  } else {
+    return false;
+  }
+}
+
+#warning TODO: bus head osap-read-in per-ch ? currently fixed to chb osap reads 
+size_t ucBusHead_read(uint8_t drop, uint8_t *dest){
+  if(!ucBusHead_ctr(drop)) return 0;
+  size_t len = inBufferLen[1][drop];
+  memcpy(dest, inBuffer[1][drop], len);
+  __disable_irq(); // again... do we need these ? big brain time 
+  inBufferLen[1][drop] = 0;
+  inBufferWp[1][drop] = 0;
+  __enable_irq();
+  return len;
+}
+
+boolean ucBusHead_ctsA(void){
+	if(outBufferLen[0] == 0){ 
+    // only condition is that our transmit buffer is zero, are not currently tx'ing on this channel 
+		return true;
+	} else {
+		return false;
+	}
+}
+
+boolean ucBusHead_ctsB(uint8_t drop){
+  // escape states 
+  if(outBufferLen[1] == 0 && rcrxb[1][drop] > 0){
+    return true; 
+  } else {
+    return false;
+  }
+}
+
+#warning TODO: we have this awkward +1 in the buffer / segsize, vs what the app. sees... 
+void ucBusHead_transmitA(uint8_t *data, uint16_t len){
+	if(!ucBusHead_ctsA()) return;
+  if(len > UB_BUFSIZE + 1) return; // none over buf size 
+  // 1st byte: broadcast identifier 
+  outBuffer[0][0] = 0;
+  // copy in @ 1th byte 
+  // we *shouldn't* have to guard against the memcpy, god bless, since 
+  // the bus shouldn't be touching this so long as our outBufferLen is 0,
+  // which - we are guarded against that w/ the flowcontrol check above 
+  memcpy(&(outBuffer[0][1]), data, len);
+  // len set 
+  __disable_irq();
+  outBufferLen[0] = len + 1;
+  outBufferRp[0] = 0;
+  __enable_irq();
+}
+
+void ucBusHead_transmitB(uint8_t *data, uint16_t len, uint8_t drop){
+  if(!ucBusHead_ctsB(drop)) return;
+  if(len > UB_BUFSIZE + 1) return; // same as above
+  __disable_irq();
+  // 1st byte: drop identifier 
+  outBuffer[1][0] = drop;
+  // copy in @ 1th byte 
+  memcpy(&(outBuffer[1][1]), data, len);
+  // length set 
+  outBufferLen[1] = len + 1; // + 1 for the addr... 
+  // read-out ptr reset 
+  outBufferRp[1] = 0;
+  __enable_irq();
+}
+
+#endif 
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusHead.h b/firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusHead.h
new file mode 100644
index 0000000..abc8e13
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusHead.h
@@ -0,0 +1,44 @@
+/*
+osap/drivers/ucBusHead.h
+
+beginnings of a uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UCBUS_HEAD_H_
+#define UCBUS_HEAD_H_
+
+#include "../../../config.h"
+
+#ifdef UCBUS_IS_HEAD
+
+#include <Arduino.h>
+#include "ucBusMacros.h"
+
+// setup, 
+void ucBusHead_setup(void);
+
+// need to call the main timer isr at some rate, 
+void ucBusHead_timerISR(void);
+void ucBusHead_rxISR(void);
+void ucBusHead_txISR(void);
+
+// ub interface, 
+boolean ucBusHead_ctr(uint8_t drop); // is there ahn packet to read at this drop 
+size_t ucBusHead_read(uint8_t drop, uint8_t *dest);  // get 'them bytes fam 
+//size_t ucBusHead_readPtr(uint8_t* drop, uint8_t** dest, unsigned long *pat); // vport interface, get next to handle... 
+//void ucBusHead_clearPtr(uint8_t drop);
+boolean ucBusHead_ctsA(void);  // return true if TX complete / buffer ready
+boolean ucBusHead_ctsB(uint8_t drop);
+void ucBusHead_transmitA(uint8_t *data, uint16_t len);  // ship bytes: broadcast to all 
+void ucBusHead_transmitB(uint8_t *data, uint16_t len, uint8_t drop);  // ship bytes: 0-14: individual drop, 15: broadcast
+
+#endif
+#endif 
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusMacros.h b/firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusMacros.h
new file mode 100644
index 0000000..64bc5be
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/vertices/ucbus/ucBusMacros.h
@@ -0,0 +1,125 @@
+/*
+ucBusMacros.h
+
+config / utes for the uart-clocked bus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+
+#ifndef UCBUS_MACROS_H_
+#define UCBUS_MACROS_H_
+
+#include "../../../config.h"
+#include <Arduino.h>
+
+// ---------------------------------------------- INFO 
+
+/*
+    assuming for now there is one bus PHY per micro, 
+    this is for shared hardware config *and* macros to operate 
+    / read / write on the bus 
+*/
+
+// ---------------------------------------------- BUFFER / DROP SIZES / RATES
+// the channel count: 2
+#define UB_CH_COUNT 2 
+// the size of each buffer: also the maximum segment size 
+#define UB_BUFSIZE 256
+// max. # of drops on the bus, just swapping from top level config.h 
+#define UB_MAX_DROPS UCBUS_MAX_DROPS
+// with a fixed 2-byte header, we can have some max # of data bytes, 
+// this is *probably* going to stay at 10, but might fluxuate a little 
+#define UB_DATA_BYTES_PER_WORD 12
+#define UB_HEAD_BYTES_PER_WORD (UB_DATA_BYTES_PER_WORD + 3)     // + 2 header, + 1 rare character
+#define UB_DROP_BYTES_PER_WORD (UB_DATA_BYTES_PER_WORD + 2)     // + 2 header
+
+// ---------------------------------------------- DATA WORDS -> INFO 
+
+typedef union {
+    struct {
+        uint8_t CH0FC:1;    // bit: channel 0 reported flowcontrol (1: full, 0: cts)
+        uint8_t CH1FC:1;    // bit: channel 1 reported flowcontrol 
+        uint8_t DROPTAP:6;  // 0-63: time division drop 
+        uint8_t CHSELECT:1; // bit: channel select: 1 for ch1, 0 ch0
+        uint8_t RESERVED:3; // not currently used, 
+        uint8_t TOKENS:4;   // 0-15: how many bytes in word are real data bytes 
+    } bits;
+    uint8_t bytes[2];
+} UCBUS_HEADER_Type;
+
+#define UCBUS_RARECHAR 0b10101010
+
+// ---------------------------------------------- PORT / PIN CONFIGS 
+#ifdef UCBUS_IS_D51
+// ------------------------------------ D51 HAL
+#define UB_SER_USART SERCOM1->USART
+#define UB_SERCOM_CLK SERCOM1_GCLK_ID_CORE
+#define UB_GCLKNUM_PICK 7
+#define UB_COMPORT PORT->Group[0]
+#define UB_TXPIN 16  // x-0
+#define UB_TXBM (uint32_t)(1 << UB_TXPIN)
+#define UB_RXPIN 18  // x-2
+#define UB_RXBM (uint32_t)(1 << UB_RXPIN)
+#define UB_RXPO 2 // RX on SER-2
+#define UB_TXPERIPHERAL PERIPHERAL_C
+#define UB_RXPERIPHERAL PERIPHERAL_C
+
+// the data enable / reciever enable pins were modified between module circuit 
+// revisions: the board w/ an SMT JTAG header is "the OG" module, 
+// these are from board-level config
+#ifdef IS_OG_MODULE 
+#define UB_DE_PIN 16 // driver output enable: set HI to enable, LO to tri-state the driver 
+#define UB_DE_PORT PORT->Group[1] 
+#define UB_RE_PIN 19 // receiver output enable, set LO to enable the RO, set HI to tri-state RO 
+#define UB_RE_PORT PORT->Group[0]
+#else 
+#define UB_DE_PIN 19 // driver output enable: set HI to enable, LO to tri-state the driver 
+#define UB_DE_PORT PORT->Group[0] 
+#define UB_RE_PIN 9 // receiver output enable, set LO to enable the RO, set HI to tri-state RO 
+#define UB_RE_PORT PORT->Group[1]
+#endif 
+
+#define UB_TE_PIN 17  // termination enable, drive LO to enable to internal termination resistor, HI to disable
+#define UB_TE_PORT PORT->Group[0]
+#define UB_TE_BM (uint32_t)(1 << UB_TE_PIN)
+#define UB_RE_BM (uint32_t)(1 << UB_RE_PIN)
+#define UB_DE_BM (uint32_t)(1 << UB_DE_PIN)
+
+#define UB_DRIVER_ENABLE UB_DE_PORT.OUTSET.reg = UB_DE_BM
+#define UB_DRIVER_DISABLE UB_DE_PORT.OUTCLR.reg = UB_DE_BM
+// ------------------------------------ END D51 HAL 
+#endif 
+
+#ifdef UCBUS_IS_D21
+// ------------------------------------ D21 HAL 
+#define UB_SER_USART SERCOM1->USART 
+#define UB_PORT PORT->Group[0]
+#define UB_TXPIN 16
+#define UB_TXBM (uint32_t)(1 << UB_TXPIN)
+#define UB_RXPIN 19
+#define UB_RXBM (uint32_t)(1 << UB_RXPIN)
+#define UB_RXPO 3 // RX is on SER1-3
+#define UB_TXPERIPHERAL PERIPHERAL_C
+#define UB_RXPERIPHERAL PERIPHERAL_C
+// data enable, recieve enable pins 
+#define UB_DEPIN 17
+#define UB_DEBM (uint32_t)(1 << UB_DEPIN)
+#define UB_REPIN 18
+#define UB_REBM (uint32_t)(1 << UB_REPIN)
+#define UB_DRIVER_ENABLE UB_PORT.OUTSET.reg = UB_DEBM
+#define UB_DRIVER_DISABLE UB_PORT.OUTCLR.reg = UB_DEBM
+#define UB_DE_SETUP UB_PORT.DIRSET.reg = UB_DEBM; UB_DRIVER_DISABLE
+#define UB_RECIEVE_ENABLE UB_PORT.OUTCLR.reg = UB_REBM
+#define UB_RECIEVE_DISABLE UB_PORT.OUTSET.reg = UB_REBM
+#define UB_RE_SETUP UB_PORT.DIRSET.reg = UB_REBM; UB_RECIEVE_ENABLE
+// ------------------------------------ END D21 HAL 
+#endif 
+
+#endif 
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/vertices/ucbus/ucbusDipConfig.cpp b/firmware/fab-step/src/osape-d21/vertices/ucbus/ucbusDipConfig.cpp
new file mode 100644
index 0000000..08742fd
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/vertices/ucbus/ucbusDipConfig.cpp
@@ -0,0 +1,61 @@
+// DIPs
+#include "ucBusDipConfig.h"
+
+#ifdef UCBUS_IS_D51 
+#ifdef UCBUS_IS_DROP
+
+void dip_setup(void){
+    // set direction in,
+    DIP_PORT.DIRCLR.reg = D_BM(D0_PIN) | D_BM(D1_PIN) | D_BM(D2_PIN) | D_BM(D3_PIN) | D_BM(D4_PIN) | D_BM(D5_PIN) | D_BM(D6_PIN) | D_BM(D7_PIN);
+    // enable in,
+    DIP_PORT.PINCFG[D0_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D1_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D2_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D3_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D4_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D5_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D6_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D7_PIN].bit.INEN = 1;
+    // enable pull,
+    DIP_PORT.PINCFG[D0_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D1_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D2_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D3_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D4_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D5_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D6_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D7_PIN].bit.PULLEN = 1;
+    // 'pull' references the value set in the 'out' register, so to pulldown:
+    DIP_PORT.OUTCLR.reg = D_BM(D0_PIN) | D_BM(D1_PIN) | D_BM(D2_PIN) | D_BM(D3_PIN) | D_BM(D4_PIN) | D_BM(D5_PIN) | D_BM(D6_PIN) | D_BM(D7_PIN);
+}
+
+uint8_t dip_readLowerFive(void){
+    uint32_t bits[5] = {0,0,0,0,0};
+    if(DIP_PORT.IN.reg & D_BM(D7_PIN)) { bits[0] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D6_PIN)) { bits[1] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D5_PIN)) { bits[2] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D4_PIN)) { bits[3] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D3_PIN)) { bits[4] = 1; }
+    /*
+    bits[0] = (DIP_PORT.IN.reg & D_BM(D7_PIN)) >> D7_PIN;
+    bits[1] = (DIP_PORT.IN.reg & D_BM(D6_PIN)) >> D6_PIN;
+    bits[2] = (DIP_PORT.IN.reg & D_BM(D5_PIN)) >> D5_PIN;
+    bits[3] = (DIP_PORT.IN.reg & D_BM(D4_PIN)) >> D4_PIN;
+    bits[4] = (DIP_PORT.IN.reg & D_BM(D3_PIN)) >> D3_PIN;
+    */
+    // not sure why I wrote this as uint32 (?) 
+    uint32_t word = 0;
+    word = word | (bits[4] << 4) | (bits[3] << 3) | (bits[2] << 2) | (bits[1] << 1) | (bits[0] << 0);
+    return (uint8_t)word;
+}
+
+boolean dip_readPin0(void){
+    return DIP_PORT.IN.reg & D_BM(D0_PIN);
+}
+
+boolean dip_readPin1(void){
+    return DIP_PORT.IN.reg & D_BM(D1_PIN);
+}
+
+#endif 
+#endif 
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/vertices/ucbus/ucbusDipConfig.h b/firmware/fab-step/src/osape-d21/vertices/ucbus/ucbusDipConfig.h
new file mode 100644
index 0000000..9632a03
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/vertices/ucbus/ucbusDipConfig.h
@@ -0,0 +1,36 @@
+// DIP switch HAL macros 
+// pardon the mis-labeling: on board, and in the schem, these are 1-8, 
+// here they will be 0-7 
+
+// note: these are 'on' hi by default, from the factory. 
+// to set low, need to turn the internal pulldown on 
+
+#ifndef UCBUS_DIP_CONFIG_H_
+#define UCBUS_DIP_CONFIG_H_
+
+#include "../../../config.h"
+
+#ifdef UCBUS_IS_D51 
+#ifdef UCBUS_IS_DROP
+
+#include <Arduino.h>
+
+#define D0_PIN 5
+#define D1_PIN 4
+#define D2_PIN 3
+#define D3_PIN 2
+#define D4_PIN 1 
+#define D5_PIN 0
+#define D6_PIN 31 
+#define D7_PIN 30
+#define DIP_PORT PORT->Group[1]
+#define D_BM(val) ((uint32_t)(1 << val))
+
+void dip_setup(void);
+uint8_t dip_readLowerFive(void);  // id, five bits, 0: clock reset, 1:31: drop ids, 
+boolean dip_readPin0(void); // bus-head (hi) or bus-drop (lo) (not used: firmware config drop or head) 
+boolean dip_readPin1(void); // if bus-drop, te-enable (hi) or no (lo)
+
+#endif 
+#endif
+#endif 
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/vertices/ucbus/vt_ucBusDrop.cpp b/firmware/fab-step/src/osape-d21/vertices/ucbus/vt_ucBusDrop.cpp
new file mode 100644
index 0000000..b6200bd
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/vertices/ucbus/vt_ucBusDrop.cpp
@@ -0,0 +1,111 @@
+/*
+osap/vport_ucbus_drop.cpp
+
+virtual port, bus drop, ucbus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "vt_ucBusDrop.h"
+
+#ifdef UCBUS_IS_DROP
+
+#include "../../../indicators.h"
+#include "../../../syserror.h"
+#include "ucBusDrop.h"
+
+vertex_t _vt_ucBusDrop;
+vertex_t* vt_ucBusDrop = &_vt_ucBusDrop;
+
+// badness, direct write in future 
+uint8_t _tempBuffer[1024];
+
+void vt_ucBusDrop_setup(boolean useDipPick, uint8_t ID){
+    _vt_ucBusDrop.type = VT_TYPE_VBUS;
+    _vt_ucBusDrop.name = "ucBusDrop";
+    _vt_ucBusDrop.loop = &vt_ucBusDrop_loop;
+    _vt_ucBusDrop.cts = &vt_ucBusDrop_cts;
+    _vt_ucBusDrop.send = &vt_ucBusDrop_send;
+    stackReset(&_vt_ucBusDrop);
+    // start it: use DIP 
+    ucBusDrop_setup(useDipPick, ID);
+    _vt_ucBusDrop.ownRxAddr = ucBusDrop_getOwnID();
+}
+
+void vt_ucBusDrop_loop(void){
+  // will want to shift(?) from ucbus inbuffer to vertex origin stack 
+  if(ucBusDrop_ctrB()){
+    // find a slot, 
+    uint8_t slot = 0;
+    if(stackEmptySlot(&_vt_ucBusDrop, VT_STACK_ORIGIN)){
+      // copy in to origin stack 
+      uint16_t len = ucBusDrop_readB(_tempBuffer);
+      stackLoadSlot(&_vt_ucBusDrop, VT_STACK_ORIGIN, _tempBuffer, len);
+    } else {
+      // no empty space, will wait in bus 
+    }
+  }
+}
+
+boolean vt_ucBusDrop_cts(uint8_t rxAddr){
+  // immediately clear? & transmit only to head 
+  if(rxAddr == 0 && ucBusDrop_ctsB()){
+    return true;
+  } else {
+    return false;
+  }
+}
+
+void vt_ucBusDrop_send(uint8_t* data, uint16_t len, uint8_t rxAddr){
+  // can't tx not-to-the-head, will drop pck 
+  if(rxAddr != 0) return;
+  // if the bus is ready, drop it,
+  if(ucBusDrop_ctsB()){
+    ucBusDrop_transmitB(data, len);
+  } else {
+    sysError("ubd tx while not clear"); // should be a check immediately beforehand ...  
+  }
+}
+
+/*
+void VPort_UCBus_Drop::read(uint8_t** pck, pckm_t* pckm){
+    unsigned long pat = 0;
+    pckm->vpa = this;
+    pckm->len = ucBusDrop->read_b_ptr(pck, &pat);
+    pckm->at = pat;
+    pckm->location = 0;
+    pckm->txAddr = 0; // the head transmitted to us, 
+    pckm->rxAddr = ucBusDrop->id + 1;
+    return;
+}
+*/
+
+/*
+// used to have a vertex local outgoing buffer, this can be the vertex's destination buffer afaiak
+void VPort_UCBus_Drop::init(void){
+    ucBusDrop->init(true, 0); 
+    for(uint8_t i = 0; i < UBD_OUTBUFFER_COUNT; i ++){
+        _outBufferLen[i] = 0;
+    }
+}
+
+void VPort_UCBus_Drop::loop(void){
+    // check our transmit buffer,
+    if(ucBusDrop->cts()){
+        for(uint8_t i = 0; i < UBD_OUTBUFFER_COUNT; i ++){
+            if(_outBufferLen[i] > 0){
+                ucBusDrop->transmit(_outBuffer[i], _outBufferLen[i]);
+                _outBufferLen[i] = 0;
+            }
+        }
+    }
+}
+*/
+
+#endif 
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/vertices/ucbus/vt_ucBusDrop.h b/firmware/fab-step/src/osape-d21/vertices/ucbus/vt_ucBusDrop.h
new file mode 100644
index 0000000..5ce6718
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/vertices/ucbus/vt_ucBusDrop.h
@@ -0,0 +1,33 @@
+/*
+osap/vport_ucbus_drop.h
+
+virtual port, bus drop, ucbus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef VPORT_UCBUS_HEAD_H_
+#define VPORT_UCBUS_HEAD_H_
+
+#include "../../../config.h"
+
+#ifdef UCBUS_IS_DROP
+
+#include <Arduino.h>
+#include "../../osape/osap/vertex.h"
+
+void vt_ucBusDrop_setup(boolean useDipPick, uint8_t ID);
+void vt_ucBusDrop_loop();
+boolean vt_ucBusDrop_cts(uint8_t rxAddr);
+void vt_ucBusDrop_send(uint8_t* data, uint16_t len, uint8_t rxAddr);
+
+extern vertex_t* vt_ucBusDrop;
+
+#endif 
+#endif 
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/vertices/ucbus/vt_ucBusHead.cpp b/firmware/fab-step/src/osape-d21/vertices/ucbus/vt_ucBusHead.cpp
new file mode 100644
index 0000000..09742a3
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/vertices/ucbus/vt_ucBusHead.cpp
@@ -0,0 +1,82 @@
+/*
+osap/vt_ucBusHead.cpp
+
+virtual port, bus head / host
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "vt_ucBusHead.h"
+
+#ifdef UCBUS_IS_HEAD
+
+#include "../../../indicators.h"
+#include "../../../syserror.h"
+#include "ucBusHead.h"
+
+// uuuuh, local thing and extern, not sure abt this
+// file scoped object, global ptr
+vertex_t _vt_ucBusHead;
+vertex_t* vt_ucBusHead = &_vt_ucBusHead;
+
+// locally, track which drop we shifted in a packet from last
+uint8_t _lastDropHandled = 0;
+
+// badness, should remove w/ direct copy in API eventually
+uint8_t _tempBuffer[1024];
+
+void vt_ucBusHead_setup(void) {
+  _vt_ucBusHead.type = VT_TYPE_VBUS;
+  _vt_ucBusHead.name = "ucBusHead";
+  _vt_ucBusHead.loop = &vt_ucBusHead_loop;
+  _vt_ucBusHead.cts = &vt_ucBusHead_cts;
+  _vt_ucBusHead.send = &vt_ucBusHead_send;
+  stackReset(&_vt_ucBusHead);
+  // start ucbus
+  ucBusHead_setup();  // todo: rewrite as c object, not class
+}
+
+void vt_ucBusHead_loop() {
+  // we need to shift items from the bus into the origin stack here
+  // we can shift multiple in per turn, if stack space exists
+  uint8_t drop = _lastDropHandled;
+  for (uint8_t i = 1; i < UB_MAX_DROPS; i++) {
+    drop++;
+    if (drop >= UB_MAX_DROPS) {
+      drop = 1;
+    }
+    if (ucBusHead_ctr(drop)) {
+      // find a stack slot,
+      uint8_t slot = 0;
+      if (stackEmptySlot(&_vt_ucBusHead, VT_STACK_ORIGIN)) {
+        // copy it in, 
+        uint16_t len = ucBusHead_read(drop, _tempBuffer);
+        stackLoadSlot(&_vt_ucBusHead, VT_STACK_ORIGIN, _tempBuffer, len);
+      } else {
+        // no more empty spaces this turn, continue 
+        return; 
+      }
+    }
+  }
+}
+
+boolean vt_ucBusHead_cts(uint8_t rxAddr) {
+  // mapping rxAddr in osap space (where 0 is head) to ucbus drop-id space...
+  return ucBusHead_ctsB(rxAddr);
+}
+
+void vt_ucBusHead_send(uint8_t* data, uint16_t len, uint8_t rxAddr) {
+  if (rxAddr == 0) {
+    sysError("attempt to busf from head to self");
+    return;
+  }
+  ucBusHead_transmitB(data, len, rxAddr);
+}
+
+#endif
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/vertices/ucbus/vt_ucBusHead.h b/firmware/fab-step/src/osape-d21/vertices/ucbus/vt_ucBusHead.h
new file mode 100644
index 0000000..4e88789
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/vertices/ucbus/vt_ucBusHead.h
@@ -0,0 +1,33 @@
+/*
+osap/vt_ucBusHead.h
+
+virtual port, bus head, ucbus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef VPORT_UCBUS_HEAD_H_
+#define VPORT_UCBUS_HEAD_H_ 
+
+#include "../../../config.h"
+
+#ifdef UCBUS_IS_HEAD
+
+#include <Arduino.h>
+#include "../../osape/osap/vertex.h"
+
+void vt_ucBusHead_setup(void);
+void vt_ucBusHead_loop();
+boolean vt_ucBusHead_cts(uint8_t rxAddr);
+void vt_ucBusHead_send(uint8_t* data, uint16_t len, uint8_t rxAddr);
+
+extern vertex_t* vt_ucBusHead;
+
+#endif
+#endif 
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/vertices/vertexConfig.h b/firmware/fab-step/src/osape-d21/vertices/vertexConfig.h
new file mode 100644
index 0000000..8345ebc
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/vertices/vertexConfig.h
@@ -0,0 +1,10 @@
+// vertex config for D21... 
+
+#ifndef VERTEX_CONFIG_H_
+#define VERTEX_CONFIG_H_
+
+#define VT_SLOTSIZE 256
+#define VT_STACKSIZE 2  // must be >= 2 for ringbuffer operation 
+#define VT_MAXCHILDREN 16
+
+#endif 
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/vertices/vt_usbSerial.cpp b/firmware/fab-step/src/osape-d21/vertices/vt_usbSerial.cpp
new file mode 100644
index 0000000..69c08ce
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/vertices/vt_usbSerial.cpp
@@ -0,0 +1,112 @@
+/*
+osap/vt_usbSerial.cpp
+
+serial port, virtualized
+does single-ended flowcontrol (from pc -> here) 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "vt_usbSerial.h"
+//#include "../osape/osap/ts.h"
+//#include "../../drivers/indicators.h"
+#include "../osape/utils/cobs.h"
+#include "../../syserror.h"
+
+vertex_t _vt_usbSerial;
+vertex_t* vt_usbSerial = &_vt_usbSerial;
+
+// incoming 
+uint8_t _inBuffer[VPUSB_SPACE_SIZE];
+uint8_t _pwp = 0; // packet to write into 
+uint16_t _bwp = 0; // byte to write at 
+
+// acks left to transmit 
+uint8_t _acksAwaiting = 0;
+
+// outgoing
+uint8_t _encodedOut[VPUSB_SPACE_SIZE];
+uint8_t _encodedIn[VPUSB_SPACE_SIZE];
+
+void vt_usbSerial_setup(void){
+  //vt_usbSerial = &vt;
+  // configure self, 
+  _vt_usbSerial.type = VT_TYPE_VPORT;
+  _vt_usbSerial.name = "usbSerial";
+  _vt_usbSerial.loop = &vt_usbSerial_loop;
+  _vt_usbSerial.cts = &vt_usbSerial_cts;
+  _vt_usbSerial.send = &vt_usbSerial_send;
+  _vt_usbSerial.onOriginStackClear = &vt_usbSerial_onOriginStackClear;
+  stackReset(&_vt_usbSerial);
+  // start arduino serial object 
+  Serial.begin(9600);
+}
+
+void vt_usbSerial_loop(){
+  // want to count through previous occupied-ness states, and on falling edge
+  // of stack education, ack... 
+  // ack if necessary (if didn't tx ack out on reciprocal send last)
+  if(_acksAwaiting){
+    vt_usbSerial_send(_encodedOut, 0, 0);
+  }
+  // then check about new messages: 
+  while(Serial.available()){
+    _inBuffer[_bwp] = Serial.read();
+    if(_inBuffer[_bwp] == 0){
+      // end of COBS-encoded frame, 
+      // decode into packet slot, record length (to mark fullness) and record arrival time 
+      // check if space in origin stack, 
+      uint8_t slot = 0;
+      if(stackEmptySlot(&_vt_usbSerial, VT_STACK_ORIGIN)){
+        // decode into decodebuf, load into stack 
+        // cobsDecode returns the length of the decoded packet
+        uint16_t len = cobsDecode(_inBuffer, _bwp, _encodedIn); 
+        stackLoadSlot(&_vt_usbSerial, VT_STACK_ORIGIN, _encodedIn, len);
+        // reset byte write pointer, and find the next empty packet write space 
+        _bwp = 0;
+      } else {
+        sysError("! serial no space !");
+      }
+      // reset for next write, 
+      _bwp = 0;     
+    } else {
+      _bwp ++;
+    }
+  }
+}
+
+// to clear packets out... for us to track flowcontrol
+void vt_usbSerial_onOriginStackClear(uint8_t slot){
+  // this is all, 
+  _acksAwaiting ++;
+}
+
+// there's at the moment no usb -> up flowcontrol 
+boolean vt_usbSerial_cts(uint8_t drop){
+  return true;
+}
+
+uint8_t _shift[VPUSB_SPACE_SIZE];
+
+void vt_usbSerial_send(uint8_t* data, uint16_t len, uint8_t rxAddr){
+  // damn, this is not fast: shifting one byte in for acks,
+  // probably faster than sending seperate packet though 
+  _shift[0] = _acksAwaiting;
+  _acksAwaiting = 0;
+  memcpy(&(_shift[1]), data, len);
+  // now encode out, 
+  size_t encLen = cobsEncode(_shift, len + 1, _encodedOut);
+  if(Serial.availableForWrite()){
+    _encodedOut[encLen] = 0; // write in final trailing zero 
+    Serial.write(_encodedOut, encLen + 1);
+    Serial.flush();
+  } else {
+    sysError("on write, serial not available");
+  }
+}
\ No newline at end of file
diff --git a/firmware/fab-step/src/osape-d21/vertices/vt_usbSerial.h b/firmware/fab-step/src/osape-d21/vertices/vt_usbSerial.h
new file mode 100644
index 0000000..e6fea5e
--- /dev/null
+++ b/firmware/fab-step/src/osape-d21/vertices/vt_usbSerial.h
@@ -0,0 +1,37 @@
+/*
+osap/vt_usbSerial.h
+
+virtual port, p2p
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef VPORT_USBSERIAL_H_
+#define VPORT_USBSERIAL_H_
+
+#include <Arduino.h>
+#include "../osape/osap/vertex.h" // this is a 'vertex' definition, extending osap for device specific COM 
+
+#define VPUSB_NUM_SPACES 8
+#define VPUSB_SPACE_SIZE 512
+
+// uuuuh classes are cancelled? 
+
+void vt_usbSerial_setup(void);
+void vt_usbSerial_loop(void);
+boolean vt_usbSerial_cts(uint8_t drop);
+void vt_usbSerial_send(uint8_t* data, uint16_t len, uint8_t rxAddr);
+void vt_usbSerial_onOriginStackClear(uint8_t slot);
+
+// tells linker that the thing exists & to... find it later?
+// is declared in this cpp file 
+
+extern vertex_t* vt_usbSerial;
+
+#endif
-- 
GitLab