Jacdac service updates (#1094)
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:
Родитель
e5202ac64c
Коммит
61a25f97c1
|
@ -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]))
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче