JD service updates:
* add range limit to crank
* servo now only takes pulse length
* add arcade controls client
* add `jacdac.autoBind()` to bind services (randomly if need be)
* add JD packet forwarding to HF2
* add `jacdac.onRawPacket()`
* add register query infrastructure for Ctrl service (may need to move to general Client class)

Non-JD changes:
* add Buffer.toArray()
This commit is contained in:
Michał Moskal 2020-05-12 18:38:52 -07:00 коммит произвёл GitHub
Родитель e5202ac64c
Коммит 61a25f97c1
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
22 изменённых файлов: 494 добавлений и 435 удалений

Просмотреть файл

@ -183,6 +183,15 @@ namespace helpers {
return r
}
}
export function bufferToArray(buf: Buffer, format: NumberFormat) {
const sz = Buffer.sizeOfNumberFormat(format)
const len = buf.length - sz
const r: number[] = []
for (let i = 0; i <= len; i += sz)
r.push(buf.getNumber(format, i))
return r
}
}
interface Buffer {
@ -224,6 +233,13 @@ interface Buffer {
//% helper=bufferChunked
chunked(maxSize: number): Buffer[];
/**
* Read contents of buffer as an array in specified format
*/
//% helper=bufferToArray
toArray(format: NumberFormat): number[];
// rest defined in buffer.cpp
}

Просмотреть файл

@ -13,8 +13,6 @@
static void *stackCopy;
static uint32_t stackSize;
//#define LOG DMESG
#define LOG(...) ((void)0)
//#define LOG DMESG
#define LOG(...) ((void)0)
@ -54,7 +52,7 @@ static const HIDReportDescriptor reportDesc = {
};
static const InterfaceInfo ifaceInfoHID = {
&reportDesc,
&reportDesc,
sizeof(reportDesc),
1,
{
@ -102,21 +100,16 @@ static const InterfaceInfo ifaceInfoEP = {
{USB_EP_TYPE_BULK, 0},
};
int HF2::stdRequest(UsbEndpointIn &ctrl, USBSetup &setup)
{
int HF2::stdRequest(UsbEndpointIn &ctrl, USBSetup &setup) {
#ifdef HF2_HID
if (!useHID)
return DEVICE_NOT_SUPPORTED;
if (setup.bRequest == USB_REQ_GET_DESCRIPTOR)
{
if (setup.wValueH == 0x21)
{
if (setup.bRequest == USB_REQ_GET_DESCRIPTOR) {
if (setup.wValueH == 0x21) {
InterfaceDescriptor tmp;
fillInterfaceInfo(&tmp);
return ctrl.write(&tmp, sizeof(tmp));
}
else if (setup.wValueH == 0x22)
{
} else if (setup.wValueH == 0x22) {
return ctrl.write(hidDescriptor, sizeof(hidDescriptor));
}
}
@ -135,9 +128,9 @@ void HF2::prepBuffer(uint8_t *buf) {
target_disable_irq();
if (dataToSendLength) {
if (dataToSendPrepend) {
dataToSendPrepend = false;
buf[0] = HF2_FLAG_CMDPKT_BODY | 4;
memcpy(buf + 1, pkt.buf, 4);
memcpy(buf + 1, &dataToSendPrepend, 4);
dataToSendPrepend = 0;
} else {
int flag = dataToSendFlag;
int s = 63;
@ -158,7 +151,15 @@ void HF2::prepBuffer(uint8_t *buf) {
}
void HF2::pokeSend() {
if (!allocateEP || !CodalUSB::usbInstance->isInitialised())
if (!allocateEP) {
if (!lastExchange || current_time_ms() - lastExchange > 1000) {
lastExchange = 0;
dataToSendLength = 0;
}
return;
}
if (!CodalUSB::usbInstance->isInitialised())
return;
uint8_t buf[64];
@ -190,6 +191,7 @@ int HF2::classRequest(UsbEndpointIn &ctrl, USBSetup &setup) {
if (setup.wLength != 64)
return DEVICE_NOT_SUPPORTED;
lastExchange = current_time_ms();
uint8_t buf[64];
prepBuffer(buf);
ctrl.write(buf, sizeof(buf));
@ -206,10 +208,7 @@ const InterfaceInfo *HF2::getInterfaceInfo() {
return allocateEP ? &ifaceInfoEP : &ifaceInfo;
}
int HF2::sendSerial(const void *data, int size, int isError) {
if (!gotSomePacket)
return DEVICE_OK;
void HF2::sendCore(uint8_t flag, uint32_t prepend, const void *data, int size) {
for (;;) {
pokeSend();
@ -225,13 +224,25 @@ int HF2::sendSerial(const void *data, int size, int isError) {
// there could be a race
if (!dataToSendLength) {
dataToSend = (const uint8_t *)data;
dataToSendPrepend = false;
dataToSendFlag = isError ? HF2_FLAG_SERIAL_ERR : HF2_FLAG_SERIAL_OUT;
dataToSendPrepend = prepend;
dataToSendFlag = flag;
dataToSendLength = size;
size = -1;
}
target_enable_irq();
}
}
int HF2::sendEvent(uint32_t evId, const void *data, int size) {
sendCore(HF2_FLAG_CMDPKT_LAST, evId, data, size);
return 0;
}
int HF2::sendSerial(const void *data, int size, int isError) {
if (!gotSomePacket)
return DEVICE_OK;
sendCore(isError ? HF2_FLAG_SERIAL_ERR : HF2_FLAG_SERIAL_OUT, 0, data, size);
return 0;
}
@ -279,7 +290,7 @@ int HF2::recv() {
int HF2::sendResponse(int size) {
dataToSend = pkt.buf;
dataToSendPrepend = false;
dataToSendPrepend = 0;
dataToSendFlag = HF2_FLAG_CMDPKT_LAST;
dataToSendLength = 4 + size;
pokeSend();
@ -287,14 +298,13 @@ int HF2::sendResponse(int size) {
}
int HF2::sendResponseWithData(const void *data, int size) {
if (dataToSendLength)
oops(90);
// if there already is dataToSend, too bad, we'll just overwrite it
if (size <= (int)sizeof(pkt.buf) - 4) {
memcpy(pkt.resp.data8, data, size);
return sendResponse(size);
} else {
dataToSend = (const uint8_t *)data;
dataToSendPrepend = true;
dataToSendPrepend = pkt.resp.eventId;
dataToSendFlag = HF2_FLAG_CMDPKT_LAST;
dataToSendLength = size;
pokeSend();
@ -320,6 +330,11 @@ static void copy_words(void *dst0, const void *src0, uint32_t n_words) {
#define QUICK_BOOT(v) *DBL_TAP_PTR = v ? DBL_TAP_MAGIC_QUICK_BOOT : 0
#endif
static HF2 *jdLogger;
static void jdLog(const uint8_t *frame) {
jdLogger->sendEvent(HF2_EV_JDS_PACKET, frame, frame[2] + 12);
}
int HF2::endpointRequest() {
if (!allocateEP && !ctrlWaiting)
return 0;
@ -348,6 +363,7 @@ int HF2::endpointRequest() {
#define checkDataSize(str, add) usb_assert(sz == 8 + (int)sizeof(cmd->str) + (int)(add))
lastExchange = current_time_ms();
gotSomePacket = true;
switch (cmdId) {
@ -420,6 +436,24 @@ int HF2::endpointRequest() {
case HF2_DBG_GET_STACK:
return sendResponseWithData(stackCopy, stackSize);
case HF2_CMD_JDS_CONFIG:
if (cmd->data8[0]) {
jdLogger = this;
pxt::logJDFrame = jdLog;
} else {
pxt::logJDFrame = NULL;
}
return sendResponse(0);
case HF2_CMD_JDS_SEND:
if (pxt::sendJDFrame) {
pxt::sendJDFrame(cmd->data8);
return sendResponse(0);
} else {
resp->status16 = HF2_STATUS_INVALID_STATE;
return sendResponse(0);
}
default:
// command not understood
resp->status16 = HF2_STATUS_INVALID_CMD;
@ -429,8 +463,10 @@ int HF2::endpointRequest() {
return sendResponse(0);
}
HF2::HF2(HF2_Buffer &p) : gotSomePacket(false), ctrlWaiting(false), pkt(p), allocateEP(true), useHID(false) {}
HF2::HF2(HF2_Buffer &p)
: gotSomePacket(false), ctrlWaiting(false), pkt(p), allocateEP(true), useHID(false) {
lastExchange = 0;
}
static const InterfaceInfo dummyIfaceInfo = {
NULL,
@ -439,8 +475,8 @@ static const InterfaceInfo dummyIfaceInfo = {
{
0, // numEndpoints
0xff, /// class code - vendor-specific
0xff, // subclass
0xff, // protocol
0xff, // subclass
0xff, // protocol
0x00, // string
0x00, // alt
},
@ -448,7 +484,6 @@ static const InterfaceInfo dummyIfaceInfo = {
{0, 0},
};
const InterfaceInfo *DummyIface::getInterfaceInfo() {
return &dummyIfaceInfo;
}

Просмотреть файл

@ -6,7 +6,8 @@
#include "HID.h"
#include "uf2hid.h"
#define HF2_BUF_SIZE 256
// 260 bytes needed for biggest JD packets (with overheads)
#define HF2_BUF_SIZE 260
typedef struct {
uint16_t size;
@ -23,12 +24,15 @@ typedef struct {
class HF2 : public CodalUSBInterface {
void prepBuffer(uint8_t *buf);
void pokeSend();
void sendCore(uint8_t flag, uint32_t prepend, const void *data, int size);
const uint8_t *dataToSend;
volatile uint32_t dataToSendLength;
bool dataToSendPrepend;
uint32_t dataToSendPrepend;
uint8_t dataToSendFlag;
uint32_t lastExchange;
bool gotSomePacket;
bool ctrlWaiting;
@ -41,6 +45,7 @@ class HF2 : public CodalUSBInterface {
int sendResponse(int size);
int recv();
int sendResponseWithData(const void *data, int size);
int sendEvent(uint32_t evId, const void *data, int size);
HF2(HF2_Buffer &pkt);
virtual int endpointRequest();

Просмотреть файл

@ -39,9 +39,6 @@ using namespace codal;
#if CONFIG_ENABLED(DEVICE_JOYSTICK)
#include "HIDJoystick.h"
#endif
#if CONFIG_ENABLED(DEVICE_JACDAC_DEBUG)
#include "USBJACDAC.h"
#endif
#endif
namespace pxt {
@ -58,9 +55,6 @@ extern USBHIDKeyboard keyboard;
#if CONFIG_ENABLED(DEVICE_JOYSTICK)
extern USBHIDJoystick joystick;
#endif
#if CONFIG_ENABLED(DEVICE_JACDAC_DEBUG)
extern USBJACDAC *jacdacDebug;
#endif
#endif
// Utility functions
@ -70,6 +64,9 @@ extern MessageBus devMessageBus;
extern codal::CodalDevice device;
void set_usb_strings(const char *uf2_info);
extern void (*logJDFrame)(const uint8_t *data);
extern void (*sendJDFrame)(const uint8_t *data);
} // namespace pxt

Просмотреть файл

@ -63,6 +63,12 @@ struct HF2_WRITE_WORDS_Command {
// no arguments
// results is utf8 character array
#define HF2_EV_MASK 0x800000
#define HF2_CMD_JDS_CONFIG 0x0020
#define HF2_CMD_JDS_SEND 0x0021
#define HF2_EV_JDS_PACKET 0x800020
typedef struct {
uint32_t command_id;
uint16_t tag;
@ -111,5 +117,6 @@ typedef struct {
#define HF2_STATUS_OK 0x00
#define HF2_STATUS_INVALID_CMD 0x01
#define HF2_STATUS_INVALID_STATE 0x02
#endif

Просмотреть файл

@ -23,9 +23,6 @@ USBHIDKeyboard keyboard;
#if CONFIG_ENABLED(DEVICE_JOYSTICK)
USBHIDJoystick joystick;
#endif
#if CONFIG_ENABLED(DEVICE_JACDAC_DEBUG)
USBJACDAC *jacdacDebug;
#endif
static const DeviceDescriptor device_desc = {
0x12, // bLength
@ -47,7 +44,7 @@ static const DeviceDescriptor device_desc = {
static void start_usb() {
// start USB with a delay, so that user code can add new interfaces if needed
// (eg USB HID keyboard, or MSC)
fiber_sleep(100);
fiber_sleep(500);
usb.start();
}
@ -118,10 +115,6 @@ void usb_init() {
#if CONFIG_ENABLED(DEVICE_JOYSTICK)
usb.add(joystick);
#endif
// USBJACDAC ctor does a bunch of stuff, which we don't want in a static initializer
#if CONFIG_ENABLED(DEVICE_JACDAC_DEBUG)
usb.add(*(jacdacDebug = new USBJACDAC()));
#endif
create_fiber(start_usb);
}
@ -170,4 +163,8 @@ void dumpDmesg() {
sendSerial(codalLogStore.buffer, codalLogStore.ptr);
sendSerial("\n\n", 2);
}
void (*logJDFrame)(const uint8_t *data);
void (*sendJDFrame)(const uint8_t *data);
} // namespace pxt

Просмотреть файл

@ -0,0 +1,27 @@
namespace jacdac {
const INTERNAL_KEY_UP = 2050;
const INTERNAL_KEY_DOWN = 2051;
//% fixedInstances
export class ArcadeControlsClient extends Client {
constructor(requiredDevice: string = null) {
super("apad", jd_class.ARCADE_CONTROLS, requiredDevice);
}
handlePacket(pkt: JDPacket) {
if (pkt.service_command == CMD_EVENT) {
const [evid, key] = pkt.data.unpack("II")
let evsrc = 0
if (evid == 1)
evsrc = INTERNAL_KEY_DOWN
else if (evid == 2)
evsrc = INTERNAL_KEY_UP
if (!evsrc) return
control.raiseEvent(evsrc, key)
}
}
}
//% fixedInstance whenUsed block="arcade controls client"
export const arcadeControlsClient = new ArcadeControlsClient();
}

Просмотреть файл

@ -1,10 +1,21 @@
namespace jacdac {
const INTERNAL_KEY_UP = 2050;
const INTERNAL_KEY_DOWN = 2051;
//% fixedInstances
export class ButtonClient extends SensorClient {
constructor(requiredDevice: string = null) {
super("btn", jd_class.BUTTON, requiredDevice);
}
connectControllerButton(controllerButton: number) {
this.start()
control.internalOnEvent(this.eventId, JDButtonEvent.Down,
() => control.raiseEvent(INTERNAL_KEY_DOWN, controllerButton))
control.internalOnEvent(this.eventId, JDButtonEvent.Up,
() => control.raiseEvent(INTERNAL_KEY_UP, controllerButton))
}
/**
* Reads the current x value from the sensor
*/

Просмотреть файл

@ -1,13 +1,12 @@
namespace jacdac {
export class ConsoleClient extends Client {
minPriority = JDConsolePriority.Silent;
minPriority = JDConsolePriority.Silent; // drop all packets by default
onMessageReceived: (priority: number, dev: Device, message: string) => void;
constructor() {
super("conc", jd_class.LOGGER, null);
this.broadcast = true
this.minPriority = JDConsolePriority.Silent; // drop all packets by default
onAnnounce(() => {
// on every announce, if we're listening to anything, tell
// everyone to log

Просмотреть файл

@ -5,6 +5,71 @@ namespace jacdac {
const DNS_CMD_LIST_USED_NAMES = 0x83
const DNS_CMD_CLEAR_STORED_IDS = 0x84
export function autoBind() {
function log(msg: string) {
control.dmesg("autobind: " + msg)
}
function pending() {
return _allClients.filter(c => !!c.requiredDeviceName && !c.isConnected())
}
pauseUntil(() => pending().length == 0, 1000)
const plen = pending().length
log(`pending: ${plen}`)
if (plen == 0) return
pause(1000) // wait for everyone to enumerate
const requested: RemoteRequestedDevice[] = []
for (const client of _allClients) {
if (client.requiredDeviceName) {
const r = addRequested(requested, client.requiredDeviceName, client.serviceClass, null)
r.boundTo = client.device
}
}
if (!requested.length)
return
function nameFree(d: Device) {
return !d.name || requested.every(r => r.boundTo != d)
}
requested.sort((a, b) => a.name.compare(b.name))
let numSel = 0
recomputeCandidates(requested)
for (const r of requested) {
if (r.boundTo)
continue
const cand = r.candidates.filter(nameFree)
log(`name: ${r.name}, ${cand.length} candidate(s)`)
if (cand.length > 0) {
// take ones without existing names first
cand.sort((a, b) => (a.name || "").compare(b.name || "") || a.deviceId.compare(b.deviceId))
log(`setting to ${cand[0].toString()}`)
r.select(cand[0])
numSel++
}
}
}
export function clearAllNames() {
settings.list(devNameSettingPrefix).forEach(settings.remove)
}
function setDevName(id: string, name: string) {
const devid = devNameSettingPrefix + id
if (name.length == 0)
settings.remove(devid)
else
settings.writeString(devid, name)
Device.clearNameCache()
}
export class DeviceNameService extends Host {
constructor() {
super("dns", jd_class.DEVICE_NAME_SERVICE)
@ -19,15 +84,8 @@ namespace jacdac {
}
break
case DNS_CMD_SET_NAME:
if (packet.data.length >= 8) {
const devid = devNameSettingPrefix + packet.data.slice(0, 8).toHex()
const name = packet.data.slice(8)
if (name.length == 0)
settings.remove(devid)
else
settings.writeBuffer(devid, name)
Device.clearNameCache()
}
if (packet.data.length >= 8)
setDevName(packet.data.slice(0, 8).toHex(), packet.data.slice(8).toString())
break
case DNS_CMD_LIST_STORED_IDS:
this.sendChunkedReport(DNS_CMD_LIST_STORED_IDS,
@ -39,7 +97,7 @@ namespace jacdac {
this.sendChunkedReport(DNS_CMD_LIST_USED_NAMES, attachedClients.map(packName))
break
case DNS_CMD_CLEAR_STORED_IDS:
settings.list(devNameSettingPrefix).forEach(settings.remove)
clearAllNames()
break
}
@ -94,7 +152,7 @@ namespace jacdac {
}
}
export class RemoteNamedDevice {
export class RemoteRequestedDevice {
services: number[] = [];
boundTo: Device;
candidates: Device[] = [];
@ -111,22 +169,41 @@ namespace jacdac {
select(dev: Device) {
if (dev == this.boundTo)
return
if (this.boundTo)
this.parent.setName(this.boundTo, "")
this.parent.setName(dev, this.name)
if (this.parent == null) {
setDevName(dev.deviceId, this.name)
} else {
if (this.boundTo)
this.parent.setName(this.boundTo, "")
this.parent.setName(dev, this.name)
}
this.boundTo = dev
}
}
function recomputeCandidates(remotes: RemoteRequestedDevice[]) {
const localDevs = devices()
for (let dev of remotes)
dev.candidates = localDevs.filter(ldev => dev.isCandidate(ldev))
}
function addRequested(devs: RemoteRequestedDevice[], name: string, service_class: number, parent: DeviceNameClient) {
let r = devs.find(d => d.name == name)
if (!r)
devs.push(r = new RemoteRequestedDevice(parent, name))
r.services.push(service_class)
return r
}
export class DeviceNameClient extends Client {
public remoteNamedDevices: RemoteNamedDevice[] = []
public remoteRequestedDevices: RemoteRequestedDevice[] = []
private usedNames: Dechunker
constructor(requiredDevice: string = null) {
super("dnsc", jd_class.DEVICE_NAME_SERVICE, requiredDevice)
onNewDevice(() => {
this.recomputeCandidates()
recomputeCandidates(this.remoteRequestedDevices)
})
onAnnounce(() => {
@ -135,7 +212,7 @@ namespace jacdac {
this.usedNames = new Dechunker(DNS_CMD_LIST_USED_NAMES, buf => {
let off = 0
const devs: RemoteNamedDevice[] = []
const devs: RemoteRequestedDevice[] = []
const localDevs = devices()
while (off < buf.length) {
const devid = buf.slice(off, 8).toHex()
@ -145,11 +222,7 @@ namespace jacdac {
const name = buf.slice(off, nameBytes).toString()
off += nameBytes
let r = devs.find(d => d.name == name)
if (!r)
devs.push(r = new RemoteNamedDevice(this, name))
r.services.push(service_class)
const r = addRequested(devs, name, service_class, this)
const dev = localDevs.find(d => d.deviceId == devid)
if (dev)
r.boundTo = dev
@ -157,17 +230,11 @@ namespace jacdac {
devs.sort((a, b) => a.name.compare(b.name))
this.remoteNamedDevices = devs
this.recomputeCandidates()
this.remoteRequestedDevices = devs
recomputeCandidates(this.remoteRequestedDevices)
})
}
private recomputeCandidates() {
const localDevs = devices()
for (let dev of this.remoteNamedDevices)
dev.candidates = localDevs.filter(ldev => dev.isCandidate(ldev))
}
scan() {
pauseUntil(() => this.isConnected())
this.sendCommand(JDPacket.onlyHeader(DNS_CMD_LIST_USED_NAMES))

Просмотреть файл

@ -24,6 +24,7 @@
"proximityclient.ts",
"lightspectrumsensorclient.ts",
"rotaryencoderclient.ts",
"arcadecontrolsclient.ts",
"pwmlightclient.ts"
],
"testFiles": [

Просмотреть файл

@ -5,6 +5,20 @@ namespace jacdac {
super("crank", jd_class.ROTARY_ENCODER, requiredDevice);
}
scale = 1
private _min: number
private _max: number
private _offset: number
/**
* Always clamp the encoder position to given range.
*/
setRange(min: number, max: number) {
this._min = min
this._max = max
this._offset = 0
}
/**
* Gets the position of the rotary encoder
*/
@ -13,7 +27,18 @@ namespace jacdac {
get position(): number {
const st = this.state;
if (!st || st.length < 4) return 0;
return st.getNumber(NumberFormat.Int32LE, 0);
const curr = st.getNumber(NumberFormat.Int32LE, 0) * this.scale
if (this._offset != null) {
const p = curr + this._offset
if (p < this._min)
this._offset = this._min - curr
else if (p > this._max)
this._offset = this._max - curr
return curr + this._offset
} else {
return curr
}
}
/**

Просмотреть файл

@ -1,8 +1,42 @@
namespace jacdac {
//% fixedInstances
export class ServoClient extends ActuatorClient {
export class ServoClient extends Client {
constructor(requiredDevice: string = null) {
super("servo", jd_class.SERVO, 5, requiredDevice);
super("servo", jd_class.SERVO, requiredDevice);
}
private pulse: number
private autoOff: number
private lastSet: number
private sync(n: number) {
this.lastSet = control.millis()
if (n === this.pulse)
return
if (n == null) {
this.setRegInt(REG_INTENSITY, 0)
} else {
this.setRegInt(REG_VALUE, n | 0)
this.setRegInt(REG_INTENSITY, 1)
}
this.pulse = n
}
setAutoOff(ms: number) {
if (!ms) ms = 0
this.lastSet = control.millis()
if (this.autoOff === undefined)
jacdac.onAnnounce(() => {
if (this.pulse != null && this.autoOff && control.millis() - this.lastSet > this.autoOff) {
this.turnOff()
}
})
this.autoOff = ms
}
turnOff() {
this.sync(undefined)
}
/**
@ -17,11 +51,12 @@ namespace jacdac {
//% servo.fieldOptions.columns=2
//% blockGap=8
setAngle(degrees: number) {
if (!this.state[0] || this.state.getNumber(NumberFormat.Int16LE, 1) != degrees) {
this.state[0] = 1;
this.state.setNumber(NumberFormat.Int16LE, 1, degrees);
this.notifyChange();
}
// this isn't exactly what the internets say, but it's what codal does
const center = 1500
const range = 2000
const lower = center - (range >> 1) << 10;
const scaled = lower + (range * Math.idiv(degrees << 10, 180));
this.setPulse(scaled >> 10)
}
/**
@ -53,10 +88,7 @@ namespace jacdac {
setPulse(micros: number) {
micros = micros | 0;
micros = Math.clamp(500, 2500, micros);
if (this.state.getNumber(NumberFormat.UInt16LE, 3) != micros) {
this.state.setNumber(NumberFormat.UInt16LE, 3, micros);
this.notifyChange();
}
this.sync(micros)
}
}

Просмотреть файл

@ -171,6 +171,15 @@ void __physSendPacket(Buffer header, Buffer data) {
if (copyAndAppend(&txQ, frame, MAX_TX, data->data) < 0)
return;
if (pxt::logJDFrame) {
auto buf = (uint8_t *)malloc(JD_FRAME_SIZE(frame));
memcpy(buf, frame, JD_SERIAL_FULL_HEADER_SIZE);
memcpy(buf + JD_SERIAL_FULL_HEADER_SIZE, data->data,
JD_FRAME_SIZE(frame) - JD_SERIAL_FULL_HEADER_SIZE);
pxt::logJDFrame(buf);
free(buf);
}
jd_packet_ready();
}
@ -189,6 +198,8 @@ Buffer __physGetPacket() {
if ((superFrameRX = rxQ) != NULL)
rxQ = rxQ->next;
target_enable_irq();
if (pxt::logJDFrame)
pxt::logJDFrame((uint8_t *)&superFrameRX->frame);
}
if (!superFrameRX)
@ -214,12 +225,19 @@ bool __physIsRunning() {
return jd_is_running() != 0;
}
static void sendFrame(const uint8_t *data) {
jd_frame_t *frame = (jd_frame_t *)data;
copyAndAppend(&txQ, frame, MAX_TX);
jd_packet_ready();
}
/**
* Starts the JACDAC physical layer.
**/
//%
void __physStart() {
jd_init();
sendJDFrame = sendFrame;
}
/**

Просмотреть файл

@ -26,6 +26,8 @@ namespace jacdac {
export const CMD_GET_REG = 0x1000
export const CMD_SET_REG = 0x2000
export const CMD_TYPE_MASK = 0xf000
export const CMD_REG_MASK = 0x0fff
// Commands 0x000-0x07f - common to all services
// Commands 0x080-0xeff - defined per-service
@ -46,4 +48,13 @@ namespace jacdac {
export const CMD_CTRL_IDENTIFY = 0x81
// reset device
export const CMD_CTRL_RESET = 0x82
// identifies the type of hardware (eg., ACME Corp. Servo X-42 Rev C)
export const REG_CTRL_DEVICE_DESCRIPTION = 0x180
// a numeric code for the string above; used to mark firmware images
export const REG_CTRL_DEVICE_CLASS = 0x181
// MCU temperature in Celsius
export const REG_CTRL_TEMPERATURE = 0x182
// this is very approximate; ADC reading from backward-biasing the identification LED
export const REG_CTRL_LIGHT_LEVEL = 0x183
}

Просмотреть файл

@ -28,6 +28,9 @@ namespace jd_class {
export const ROTARY_ENCODER = 0x10fa29c9;
export const DEVICE_NAME_SERVICE = 0x117729bd;
export const PWM_LIGHT = 0x1fb57453;
export const BOOTLOADER = 0x1ffa9948
export const ARCADE_CONTROLS = 0x1deaa06e
// to generate a new class number, head to https://microsoft.github.io/uf2/patcher/
// click link at the bottom and replace first digit with '1'
}
@ -112,9 +115,13 @@ const enum JDMusicCommand {
PlayTone = 0x80,
}
const enum JDConsoleReg {
MinPriority = 0x80
}
const enum JDConsoleCommand {
MessageDbg = 0x80,
SetMinPriority = 0x90,
SetMinPriority = 0x2000 | JDConsoleReg.MinPriority,
}
const enum JDConsolePriority {

Просмотреть файл

@ -34,12 +34,12 @@ jd_diagnostics_t *jd_get_diagnostics(void) {
return &jd_diagnostics;
}
static void pulse1() {
static void pulse1(void) {
log_pin_set(1, 1);
log_pin_set(1, 0);
}
static void signal_error() {
static void signal_error(void) {
log_pin_set(2, 1);
log_pin_set(2, 0);
}
@ -51,9 +51,9 @@ static void signal_write(int v) {
static void signal_read(int v) {
// log_pin_set(0, v);
}
static void pulse_log_pin() {}
static void pulse_log_pin(void) {}
static void check_announce() {
static void check_announce(void) {
if (tim_get_micros() > nextAnnounce) {
// pulse_log_pin();
if (nextAnnounce)
@ -62,7 +62,7 @@ static void check_announce() {
}
}
void jd_init() {
void jd_init(void) {
DMESG("JD: init");
tim_init();
set_tick_timer(0);
@ -70,15 +70,15 @@ void jd_init() {
check_announce();
}
int jd_is_running() {
int jd_is_running(void) {
return nextAnnounce != 0;
}
int jd_is_busy() {
int jd_is_busy(void) {
return status != 0;
}
static void tx_done() {
static void tx_done(void) {
signal_write(0);
set_tick_timer(JD_STATUS_TX_ACTIVE);
}
@ -90,12 +90,12 @@ void jd_tx_completed(int errCode) {
tx_done();
}
static void tick() {
static void tick(void) {
check_announce();
set_tick_timer(0);
}
static void flush_tx_queue() {
static void flush_tx_queue(void) {
// pulse1();
if (annCounter++ == 0)
check_announce();
@ -110,8 +110,13 @@ static void flush_tx_queue() {
target_enable_irq();
txPending = 0;
if (!txFrame)
if (!txFrame) {
txFrame = app_pull_frame();
if (!txFrame) {
tx_done();
return;
}
}
signal_write(1);
if (uart_start_tx(txFrame, JD_FRAME_SIZE(txFrame)) < 0) {
@ -148,7 +153,7 @@ static void set_tick_timer(uint8_t statusClear) {
target_enable_irq();
}
static void rx_timeout() {
static void rx_timeout(void) {
target_disable_irq();
jd_diagnostics.bus_timeout_error++;
ERROR("RX timeout");
@ -159,7 +164,7 @@ static void rx_timeout() {
signal_error();
}
static void setup_rx_timeout() {
static void setup_rx_timeout(void) {
uint32_t *p = (uint32_t *)rxFrame;
if (p[0] == 0 && p[1] == 0)
rx_timeout(); // didn't get any data after lo-pulse
@ -247,7 +252,7 @@ void jd_rx_completed(int dataLeft) {
jd_diagnostics.packets_dropped++;
}
void jd_packet_ready() {
void jd_packet_ready(void) {
target_disable_irq();
txPending = 1;
if (status == 0)

Просмотреть файл

@ -75,6 +75,16 @@ extern "C" {
#define JD_CMD_CTRL_IDENTIFY 0x81
// reset device
#define JD_CMD_CTRL_RESET 0x82
// identifies the type of hardware (eg., ACME Corp. Servo X-42 Rev C)
#define JD_REG_CTRL_DEVICE_DESCRIPTION 0x180
// a numeric code for the string above; used to mark firmware images
#define JD_REG_CTRL_DEVICE_CLASS 0x181
// MCU temperature in Celsius
#define JD_REG_CTRL_TEMPERATURE 0x182
// this is very approximate; ADC reading from backward-biasing the identification LED
#define JD_REG_CTRL_LIGHT_LEVEL 0x183
// typically the same as JD_REG_CTRL_DEVICE_CLASS; the bootloader will respond to that code
#define JD_REG_CTRL_BL_DEVICE_CLASS 0x184
struct _jd_packet_t {
uint16_t crc;

Просмотреть файл

@ -123,21 +123,7 @@ namespace jacdac {
}
get intData() {
let fmt: NumberFormat
switch (this._data.length) {
case 0:
case 1:
fmt = NumberFormat.Int8LE
break
case 2:
case 3:
fmt = NumberFormat.Int16LE
break
default:
fmt = NumberFormat.Int32LE
break
}
return this._data.getNumber(fmt, 0)
return intOfBuffer(this._data)
}
compress(stripped: Buffer[]) {
@ -174,8 +160,12 @@ namespace jacdac {
return !!(this.packet_flags & JD_FRAME_FLAG_COMMAND)
}
get is_report() {
return !(this.packet_flags & JD_FRAME_FLAG_COMMAND)
}
toString(): string {
let msg = `${this.device_identifier}/${this.service_number}[${this.packet_flags}]: ${this.service_command} sz=${this.size}`
let msg = `${jacdac.shortDeviceId(this.device_identifier)}/${this.service_number}[${this.packet_flags}]: ${this.service_command} sz=${this.size}`
if (this.size < 20) msg += ": " + this.data.toHex()
else msg += ": " + this.data.slice(0, 20).toHex() + "..."
return msg
@ -283,4 +273,22 @@ namespace jacdac {
if (numNotify)
ackAwaiters = ackAwaiters.filter(a => a.added !== 0)
}
export function intOfBuffer(data: Buffer) {
let fmt: NumberFormat
switch (data.length) {
case 0:
case 1:
fmt = NumberFormat.Int8LE
break
case 2:
case 3:
fmt = NumberFormat.Int16LE
break
default:
fmt = NumberFormat.Int32LE
break
}
return data.getNumber(fmt, 0)
}
}

Просмотреть файл

@ -19,6 +19,7 @@ namespace jacdac {
//% whenUsed
let announceCallbacks: (() => void)[] = [];
let newDeviceCallbacks: (() => void)[];
let pktCallbacks: ((p: JDPacket) => void)[];
function log(msg: string) {
console.add(jacdac.consolePriority, msg);
@ -264,8 +265,11 @@ namespace jacdac {
handlePacketOuter(pkt: JDPacket) {
if (pkt.service_command == CMD_ADVERTISEMENT_DATA)
this.advertisementData = pkt.data
else
this.handlePacket(pkt)
if (pkt.service_command == CMD_EVENT)
control.raiseEvent(this.eventId, pkt.intData)
this.handlePacket(pkt)
}
handlePacket(pkt: JDPacket) { }
@ -350,6 +354,16 @@ namespace jacdac {
_allClients.push(this)
clearAttachCache()
}
destroy() {
if (this.device)
this.device.clients.removeElement(this)
_unattachedClients.removeElement(this)
_allClients.removeElement(this)
this.serviceNumber = null
this.device = null
clearAttachCache()
}
}
// 4 letter ID; 0.04%/0.01%/0.002% collision probability among 20/10/5 devices
@ -363,17 +377,28 @@ namespace jacdac {
String.fromCharCode(0x41 + Math.idiv(h, 26 * 26 * 26) % 26)
}
class RegQuery {
lastQuery = 0
value: Buffer
constructor(public reg: number) { }
}
export class Device {
services: Buffer
lastSeen: number
clients: Client[] = []
private _name: string
private _shortId: string
private queries: RegQuery[]
constructor(public deviceId: string) {
devices_.push(this)
}
get isConnected() {
return this.clients != null
}
get name() {
// TODO measure if caching is worth it
if (this._name === undefined)
@ -392,6 +417,59 @@ namespace jacdac {
return this.shortId + (this.name ? ` (${this.name})` : ``)
}
private lookupQuery(reg: number) {
if (!this.queries) this.queries = []
return this.queries.find(q => q.reg == reg)
}
queryInt(reg: number, refreshRate = 1000) {
const v = this.query(reg, refreshRate)
if (!v) return undefined
return intOfBuffer(v)
}
query(reg: number, refreshRate = 1000) {
let q = this.lookupQuery(reg)
if (!q)
this.queries.push(q = new RegQuery(reg))
const now = control.millis()
if (!q.lastQuery || (refreshRate != null && now - q.lastQuery > refreshRate)) {
q.lastQuery = now
this.sendCtrlCommand(CMD_GET_REG | reg)
}
return q.value
}
get classDescription() {
const v = this.query(REG_CTRL_DEVICE_DESCRIPTION, null)
if (v) return v.toString()
else {
const num = this.query(REG_CTRL_DEVICE_CLASS, null)
if (num)
return "0x" + num.toHex()
else
return ""
}
}
get temperature() {
return this.queryInt(REG_CTRL_TEMPERATURE)
}
get lightLevel() {
return this.queryInt(REG_CTRL_LIGHT_LEVEL)
}
handleCtrlReport(pkt: JDPacket) {
if ((pkt.service_command & CMD_TYPE_MASK) == CMD_GET_REG) {
const reg = pkt.service_command & CMD_REG_MASK
const q = this.lookupQuery(reg)
pkt.intData
q.value = pkt.data
}
}
hasService(service_class: number) {
for (let i = 4; i < this.services.length; i += 4)
if (this.services.getNumber(NumberFormat.UInt32LE, i) == service_class)
@ -464,6 +542,11 @@ namespace jacdac {
newDeviceCallbacks.push(cb)
}
export function onRawPacket(cb: (pkt: JDPacket) => void) {
if (!pktCallbacks) pktCallbacks = []
pktCallbacks.push(cb)
}
function queueAnnounce() {
const fmt = "<" + hostServices.length + "I"
const ids = hostServices.map(h => h.running ? h.serviceClass : -1)
@ -540,6 +623,10 @@ namespace jacdac {
}
}
if (pktCallbacks)
for (let f of pktCallbacks)
f(pkt)
if (multiCommandClass != null) {
if (!pkt.is_command)
return // only commands supported in multi-command
@ -574,8 +661,10 @@ namespace jacdac {
reattach(dev)
}
}
if (dev)
if (dev) {
dev.handleCtrlReport(pkt)
dev.lastSeen = control.millis()
}
return
} else if (pkt.service_number == JD_SERVICE_NUMBER_CRC_ACK) {
_gotAck(pkt)

Просмотреть файл

@ -2,7 +2,7 @@ namespace jacdac {
export class ServoService extends ActuatorService {
servo: servos.Servo;
constructor(name: string, servo: servos.Servo) {
super(name, jd_class.SERVO, 8);
super(name, jd_class.SERVO, 2);
this.servo = servo;
}
@ -10,10 +10,8 @@ namespace jacdac {
if (!this.intensity)
this.servo.stop();
else {
const [angle, pulse] = this.state.unpack("hh")
if (pulse)
this.servo.setPulse(pulse);
this.servo.setAngle(angle);
const pulse = this.state.unpack("h")[0]
this.servo.setPulse(pulse);
}
}
}

Просмотреть файл

@ -1,306 +0,0 @@
const fs = require("fs")
let frameBytes = []
let lastTime = 0
const dev_ids = {
"119c5abca9fd6070": "JDM3.0-ACC-burned",
"ffc91289c5dc5280": "JDM3.0-ACC",
"766ccc5755a22eb4": "JDM3.0-LIGHT",
"259ab02e98bc2752": "F840-0",
"69a9eaeb1a7d2bc0": "F840-1",
"08514ae8a1995a00": "KITTEN-0",
"XEOM": "DEMO-ACC-L",
"OEHM": "DEMO-ACC-M",
"MTYV": "DEMO-LIGHT",
"ZYQT": "DEMO-MONO",
"XMMW": "MB-BLUE",
"CJFN": "DEMO-CPB",
}
// Generic commands
const CMD_ADVERTISEMENT_DATA = 0x00
const JD_SERIAL_HEADER_SIZE = 16
const JD_SERIAL_MAX_PAYLOAD_SIZE = 236
const JD_SERVICE_NUMBER_MASK = 0x3f
const JD_SERVICE_NUMBER_INV_MASK = 0xc0
const JD_SERVICE_NUMBER_CRC_ACK = 0x3f
const JD_SERVICE_NUMBER_CTRL = 0x00
// the COMMAND flag signifies that the device_identifier is the recipent
// (i.e., it's a command for the peripheral); the bit clear means device_identifier is the source
// (i.e., it's a report from peripheral or a broadcast message)
const JD_FRAME_FLAG_COMMAND = 0x01
// an ACK should be issued with CRC of this package upon reception
const JD_FRAME_FLAG_ACK_REQUESTED = 0x02
// the device_identifier contains target service class number
const JD_FRAME_FLAG_IDENTIFIER_IS_SERVICE_CLASS = 0x04
const service_classes = {
"<disabled>": -1,
CTRL: 0,
LOGGER: 0x12dc1fca,
BATTERY: 0x1d2a2acd,
ACCELEROMETER: 0x1f140409,
BUTTON: 0x1473a263,
TOUCHBUTTON: 0x130cf5be,
LIGHT_SENSOR: 0x15e7a0ff,
MICROPHONE: 0x1a5c5866,
THERMOMETER: 0x1421bac7,
SWITCH: 0x14218172,
PIXEL: 0x1768fbbf,
HAPTIC: 0x116b14a3,
LIGHT: 0x126f00e0,
KEYBOARD: 0x1ae4812d,
MOUSE: 0x14bc97bf,
GAMEPAD: 0x100527e8,
MUSIC: 0x1b57b1d7,
SERVO: 0x12fc9103,
CONTROLLER: 0x188ae4b8,
LCD: 0x18d5284c,
MESSAGE_BUS: 0x115cabf5,
COLOR_SENSOR: 0x14d6dda2,
LIGHT_SPECTRUM_SENSOR: 0x16fa0c0d,
PROXIMITY: 0x14c1791b,
TOUCH_BUTTONS: 0x1acb49d5,
SERVOS: 0x182988d8,
ROTARY_ENCODER: 0x10fa29c9,
DNS: 0x117729bd,
PWM_LIGHT: 0x1fb57453,
}
const generic_commands = {
CMD_ADVERTISEMENT_DATA: 0x00,
CMD_EVENT: 0x01,
CMD_CALIBRATE: 0x02,
CMD_GET_DESCRIPTION: 0x03,
/*
CMD_CTRL_NOOP: 0x80,
CMD_CTRL_IDENTIFY: 0x81,
CMD_CTRL_RESET: 0x82,
*/
}
const generic_regs = {
REG_INTENSITY: 0x01,
REG_VALUE: 0x02,
REG_IS_STREAMING: 0x03,
REG_STREAMING_INTERVAL: 0x04,
REG_LOW_THRESHOLD: 0x05,
REG_HIGH_THRESHOLD: 0x06,
REG_MAX_POWER: 0x07,
REG_READING: 0x101
}
const CMD_TOP_MASK = 0xf000
const CMD_REG_MASK = 0x0fff
const CMD_SET_REG = 0x2000
const CMD_GET_REG = 0x1000
const devices = {}
function reverseLookup(map, n) {
for (let k of Object.keys(map)) {
if (map[k] == n)
return k
}
return toHex(n)
}
function serviceName(n) {
return reverseLookup(service_classes, n)
}
function commandName(n) {
let pref = ""
if ((n & CMD_TOP_MASK) == CMD_SET_REG) pref = "SET["
else if ((n & CMD_TOP_MASK) == CMD_GET_REG) pref = "GET["
if (pref) {
const reg = n & CMD_REG_MASK
return pref + reverseLookup(generic_regs, reg) + "]"
}
return reverseLookup(generic_commands, n)
}
function crc(p) {
let crc = 0xffff;
for (let i = 0; i < p.length; ++i) {
const data = p[i];
let x = (crc >> 8) ^ data;
x ^= x >> 4;
crc = (crc << 8) ^ (x << 12) ^ (x << 5) ^ x;
crc &= 0xffff;
}
return crc;
}
function toHex(n) {
return "0x" + n.toString(16)
}
function ALIGN(n) { return (n + 3) & ~3 }
function splitIntoPackets(frame) {
const res = []
if (frame.length != 12 + frame[2])
console.log("unexpected packet len: " + frame.length)
for (let ptr = 12; ptr < 12 + frame[2];) {
const psz = frame[ptr] + 4
const sz = ALIGN(psz)
const pkt = Buffer.concat([frame.slice(0, 12), frame.slice(ptr, ptr + psz)])
if (ptr + sz > 12 + frame[2]) {
console.log(`invalid frame compression, res len=${res.length}`)
res.push(pkt)
break
}
res.push(pkt)
ptr += sz
}
return res
}
function shortDeviceId(devid) {
function fnv1(data) {
let h = 0x811c9dc5
for (let i = 0; i < data.length; ++i) {
h = Math.imul(h, 0x1000193) ^ data[i]
}
return h
}
function hash(buf, bits) {
bits |= 0
if (bits < 1)
return 0
const h = fnv1(buf)
if (bits >= 32)
return h >>> 0
else
return ((h ^ (h >>> bits)) & ((1 << bits) - 1)) >>> 0
}
function idiv(x, y) { return ((x | 0) / (y | 0)) | 0 }
const h = hash(Buffer.from(devid, "hex"), 30)
return String.fromCharCode(0x41 + h % 26) +
String.fromCharCode(0x41 + idiv(h, 26) % 26) +
String.fromCharCode(0x41 + idiv(h, 26 * 26) % 26) +
String.fromCharCode(0x41 + idiv(h, 26 * 26 * 26) % 26)
}
function num2str(n) {
return n + " (0x" + n.toString(16) + ")"
}
function showPkt(pkt) {
const dev_id = pkt.slice(4, 12).toString("hex")
const size = pkt[12]
const service_number = pkt[13]
const service_cmd = pkt[14] | (pkt[15] << 8)
const frame_flags = pkt[3]
let dev = devices[dev_id]
if (!dev) {
dev = devices[dev_id] = { id: dev_id, service_names: ["CTRL"] }
}
const devname = dev_ids[dev_id] ||
dev_ids[shortDeviceId(dev_id)] ||
(dev_id + ":" + shortDeviceId(dev_id))
const service_name =
(service_number == JD_SERVICE_NUMBER_CRC_ACK ? "CRC-ACK" : (dev.service_names[service_number] || "")) +
` (${service_number})`
let pdesc = `${devname}/${service_name}: ${commandName(service_cmd)}; sz=${size}`
if (frame_flags & JD_FRAME_FLAG_COMMAND)
pdesc = 'to ' + pdesc
else
pdesc = 'from ' + pdesc
if (frame_flags & JD_FRAME_FLAG_ACK_REQUESTED)
pdesc = `[ack:0x${pkt.readUInt16LE(0).toString(16)}] ` + pdesc
if (frame_flags & JD_FRAME_FLAG_IDENTIFIER_IS_SERVICE_CLASS)
pdesc = "[mul] " + pdesc
const d = pkt.slice(16, 16 + size)
if (service_number == 0 && service_cmd == CMD_ADVERTISEMENT_DATA) {
if (dev.services && dev.services.equals(d)) {
pdesc = " ====== " + pdesc
// pdesc = ""
} else {
dev.services = d
const services = []
dev.service_names = services
for (let i = 0; i < d.length; i += 4) {
services.push(serviceName(d.readInt32LE(i)))
}
pdesc += "; " + "Announce services: " + services.join(", ")
}
} else {
let v0 = null, v1 = null
if (d.length == 1) {
v0 = d.readUInt8(0)
v1 = d.readInt8(0)
} else if (d.length == 2) {
v0 = d.readUInt16LE(0)
v1 = d.readInt16LE(0)
} else if (d.length == 4) {
v0 = d.readUInt16LE(0)
v1 = d.readInt16LE(0)
}
if (v0 != null) {
pdesc += "; " + num2str(v0)
if (v0 != v1)
pdesc += "; signed: " + num2str(v1)
} else if (d.length) {
pdesc += "; " + d.toString("hex")
}
}
if (pdesc)
console.log(Math.round(lastTime * 1000) + "ms: " + pdesc)
}
function displayPkt(msg) {
if (frameBytes.length == 0)
return
const frame = Buffer.from(frameBytes)
const size = frame[2] || 0
if (frame.length < size + 12) {
console.log(`got only ${frame.length} bytes; expecting ${size + 12}; end=${msg}`)
} else if (size < 4) {
console.log(`empty packet`)
} else {
const c = crc(frame.slice(2, size + 12))
if (frame.readUInt16LE(0) != c) {
console.log(`crc mismatch; msg=${msg} sz=${size} got:${frame.readUInt16LE(0)}, exp:${c}`)
} else {
if (msg) console.log(msg)
splitIntoPackets(frame).forEach(showPkt)
}
}
frameBytes = []
lastTime = 0
}
for (let ln of fs.readFileSync(process.argv[2], "utf8").split(/\r?\n/)) {
const m = /^([\d\.]+),Async Serial,.*(0x[A-F0-9][A-F0-9])/.exec(ln)
if (!m)
continue
const tm = parseFloat(m[1])
if (lastTime && tm - lastTime > 0.1) {
// timeout
displayPkt("timeout")
}
lastTime = tm
if (ln.indexOf("framing error") > 0) {
displayPkt("")
} else {
frameBytes.push(parseInt(m[2]))
}
}