splits m:b simulator state
This commit is contained in:
parent
a984778dfd
commit
89e899cc79
711
sim/state.ts
711
sim/state.ts
@ -1,711 +0,0 @@
|
|||||||
namespace pxsim {
|
|
||||||
export interface RuntimeOptions {
|
|
||||||
theme: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum DisplayMode {
|
|
||||||
bw,
|
|
||||||
greyscale
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PinFlags {
|
|
||||||
Unused = 0,
|
|
||||||
Digital = 0x0001,
|
|
||||||
Analog = 0x0002,
|
|
||||||
Input = 0x0004,
|
|
||||||
Output = 0x0008,
|
|
||||||
Touch = 0x0010
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Pin {
|
|
||||||
constructor(public id: number) { }
|
|
||||||
touched = false;
|
|
||||||
value = 0;
|
|
||||||
period = 0;
|
|
||||||
mode = PinFlags.Unused;
|
|
||||||
pitch = false;
|
|
||||||
pull = 0; // PullDown
|
|
||||||
|
|
||||||
isTouched(): boolean {
|
|
||||||
this.mode = PinFlags.Touch;
|
|
||||||
return this.touched;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Button {
|
|
||||||
constructor(public id: number) { }
|
|
||||||
pressed: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EventBus {
|
|
||||||
private queues: Map<EventQueue<number>> = {};
|
|
||||||
|
|
||||||
constructor(private runtime: Runtime) { }
|
|
||||||
|
|
||||||
listen(id: number, evid: number, handler: RefAction) {
|
|
||||||
let k = id + ":" + evid;
|
|
||||||
let queue = this.queues[k];
|
|
||||||
if (!queue) queue = this.queues[k] = new EventQueue<number>(this.runtime);
|
|
||||||
queue.handler = handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
queue(id: number, evid: number, value: number = 0) {
|
|
||||||
let k = id + ":" + evid;
|
|
||||||
let queue = this.queues[k];
|
|
||||||
if (queue) queue.push(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PacketBuffer {
|
|
||||||
data: number[] | string;
|
|
||||||
rssi?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RadioDatagram {
|
|
||||||
datagram: PacketBuffer[] = [];
|
|
||||||
lastReceived: PacketBuffer = {
|
|
||||||
data: [0, 0, 0, 0],
|
|
||||||
rssi: -1
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(private runtime: Runtime) {
|
|
||||||
}
|
|
||||||
|
|
||||||
queue(packet: PacketBuffer) {
|
|
||||||
if (this.datagram.length < 4)
|
|
||||||
this.datagram.push(packet);
|
|
||||||
(<Board>runtime.board).bus.queue(DAL.MICROBIT_ID_RADIO, DAL.MICROBIT_RADIO_EVT_DATAGRAM);
|
|
||||||
}
|
|
||||||
|
|
||||||
send(buffer: number[] | string) {
|
|
||||||
if (buffer instanceof String) buffer = buffer.slice(0, 32);
|
|
||||||
else buffer = buffer.slice(0, 8);
|
|
||||||
|
|
||||||
Runtime.postMessage(<SimulatorRadioPacketMessage>{
|
|
||||||
type: "radiopacket",
|
|
||||||
data: buffer
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
recv(): PacketBuffer {
|
|
||||||
let r = this.datagram.shift();
|
|
||||||
if (!r) r = {
|
|
||||||
data: [0, 0, 0, 0],
|
|
||||||
rssi: -1
|
|
||||||
};
|
|
||||||
return this.lastReceived = r;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RadioBus {
|
|
||||||
// uint8_t radioDefaultGroup = MICROBIT_RADIO_DEFAULT_GROUP;
|
|
||||||
groupId = 0; // todo
|
|
||||||
power = 0;
|
|
||||||
transmitSerialNumber = false;
|
|
||||||
datagram: RadioDatagram;
|
|
||||||
|
|
||||||
constructor(private runtime: Runtime) {
|
|
||||||
this.datagram = new RadioDatagram(runtime);
|
|
||||||
}
|
|
||||||
|
|
||||||
setGroup(id: number) {
|
|
||||||
this.groupId = id & 0xff; // byte only
|
|
||||||
}
|
|
||||||
|
|
||||||
setTransmitPower(power: number) {
|
|
||||||
this.power = Math.max(0, Math.min(7, power));
|
|
||||||
}
|
|
||||||
|
|
||||||
setTransmitSerialNumber(sn: boolean) {
|
|
||||||
this.transmitSerialNumber = !!sn;
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcast(msg: number) {
|
|
||||||
Runtime.postMessage(<SimulatorEventBusMessage>{
|
|
||||||
type: "eventbus",
|
|
||||||
id: DAL.MES_BROADCAST_GENERAL_ID,
|
|
||||||
eventid: msg,
|
|
||||||
power: this.power,
|
|
||||||
group: this.groupId
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AccelerometerSample {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
z: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ShakeHistory {
|
|
||||||
x: boolean;
|
|
||||||
y: boolean;
|
|
||||||
z: boolean;
|
|
||||||
count: number;
|
|
||||||
shaken: number;
|
|
||||||
timer: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Co-ordinate systems that can be used.
|
|
||||||
* RAW: Unaltered data. Data will be returned directly from the accelerometer.
|
|
||||||
*
|
|
||||||
* SIMPLE_CARTESIAN: Data will be returned based on an easy to understand alignment, consistent with the cartesian system taught in schools.
|
|
||||||
* When held upright, facing the user:
|
|
||||||
*
|
|
||||||
* /
|
|
||||||
* +--------------------+ z
|
|
||||||
* | |
|
|
||||||
* | ..... |
|
|
||||||
* | * ..... * |
|
|
||||||
* ^ | ..... |
|
|
||||||
* | | |
|
|
||||||
* y +--------------------+ x-->
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* NORTH_EAST_DOWN: Data will be returned based on the industry convention of the North East Down (NED) system.
|
|
||||||
* When held upright, facing the user:
|
|
||||||
*
|
|
||||||
* z
|
|
||||||
* +--------------------+ /
|
|
||||||
* | |
|
|
||||||
* | ..... |
|
|
||||||
* | * ..... * |
|
|
||||||
* ^ | ..... |
|
|
||||||
* | | |
|
|
||||||
* x +--------------------+ y-->
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export enum MicroBitCoordinateSystem {
|
|
||||||
RAW,
|
|
||||||
SIMPLE_CARTESIAN,
|
|
||||||
NORTH_EAST_DOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Accelerometer {
|
|
||||||
private sigma: number = 0; // the number of ticks that the instantaneous gesture has been stable.
|
|
||||||
private lastGesture: number = 0; // the last, stable gesture recorded.
|
|
||||||
private currentGesture: number = 0 // the instantaneous, unfiltered gesture detected.
|
|
||||||
private sample: AccelerometerSample = { x: 0, y: 0, z: -1023 }
|
|
||||||
private shake: ShakeHistory = { x: false, y: false, z: false, count: 0, shaken: 0, timer: 0 }; // State information needed to detect shake events.
|
|
||||||
private pitch: number;
|
|
||||||
private roll: number;
|
|
||||||
private id: number;
|
|
||||||
public isActive = false;
|
|
||||||
public sampleRange = 2;
|
|
||||||
|
|
||||||
constructor(public runtime: Runtime) {
|
|
||||||
this.id = DAL.MICROBIT_ID_ACCELEROMETER;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setSampleRange(range: number) {
|
|
||||||
this.activate();
|
|
||||||
this.sampleRange = Math.max(1, Math.min(8, range));
|
|
||||||
}
|
|
||||||
|
|
||||||
public activate() {
|
|
||||||
if (!this.isActive) {
|
|
||||||
this.isActive = true;
|
|
||||||
this.runtime.queueDisplayUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the acceleration data from the accelerometer, and stores it in our buffer.
|
|
||||||
* This is called by the tick() member function, if the interrupt is set!
|
|
||||||
*/
|
|
||||||
public update(x: number, y: number, z: number) {
|
|
||||||
// read MSB values...
|
|
||||||
this.sample.x = Math.floor(x);
|
|
||||||
this.sample.y = Math.floor(y);
|
|
||||||
this.sample.z = Math.floor(z);
|
|
||||||
|
|
||||||
// Update gesture tracking
|
|
||||||
this.updateGesture();
|
|
||||||
|
|
||||||
// Indicate that a new sample is available
|
|
||||||
board().bus.queue(this.id, DAL.MICROBIT_ACCELEROMETER_EVT_DATA_UPDATE)
|
|
||||||
}
|
|
||||||
|
|
||||||
public instantaneousAccelerationSquared() {
|
|
||||||
// Use pythagoras theorem to determine the combined force acting on the device.
|
|
||||||
return this.sample.x * this.sample.x + this.sample.y * this.sample.y + this.sample.z * this.sample.z;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service function. Determines the best guess posture of the device based on instantaneous data.
|
|
||||||
* This makes no use of historic data (except for shake), and forms this input to the filter implemented in updateGesture().
|
|
||||||
*
|
|
||||||
* @return A best guess of the current posture of the device, based on instantaneous data.
|
|
||||||
*/
|
|
||||||
private instantaneousPosture(): number {
|
|
||||||
let force = this.instantaneousAccelerationSquared();
|
|
||||||
let shakeDetected = false;
|
|
||||||
|
|
||||||
// Test for shake events.
|
|
||||||
// We detect a shake by measuring zero crossings in each axis. In other words, if we see a strong acceleration to the left followed by
|
|
||||||
// a string acceleration to the right, then we can infer a shake. Similarly, we can do this for each acxis (left/right, up/down, in/out).
|
|
||||||
//
|
|
||||||
// If we see enough zero crossings in succession (MICROBIT_ACCELEROMETER_SHAKE_COUNT_THRESHOLD), then we decide that the device
|
|
||||||
// has been shaken.
|
|
||||||
if ((this.getX() < -DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && this.shake.x) || (this.getX() > DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && !this.shake.x)) {
|
|
||||||
shakeDetected = true;
|
|
||||||
this.shake.x = !this.shake.x;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((this.getY() < -DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && this.shake.y) || (this.getY() > DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && !this.shake.y)) {
|
|
||||||
shakeDetected = true;
|
|
||||||
this.shake.y = !this.shake.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((this.getZ() < -DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && this.shake.z) || (this.getZ() > DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && !this.shake.z)) {
|
|
||||||
shakeDetected = true;
|
|
||||||
this.shake.z = !this.shake.z;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shakeDetected && this.shake.count < DAL.MICROBIT_ACCELEROMETER_SHAKE_COUNT_THRESHOLD && ++this.shake.count == DAL.MICROBIT_ACCELEROMETER_SHAKE_COUNT_THRESHOLD)
|
|
||||||
this.shake.shaken = 1;
|
|
||||||
|
|
||||||
if (++this.shake.timer >= DAL.MICROBIT_ACCELEROMETER_SHAKE_DAMPING) {
|
|
||||||
this.shake.timer = 0;
|
|
||||||
if (this.shake.count > 0) {
|
|
||||||
if (--this.shake.count == 0)
|
|
||||||
this.shake.shaken = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.shake.shaken)
|
|
||||||
return DAL.MICROBIT_ACCELEROMETER_EVT_SHAKE;
|
|
||||||
|
|
||||||
let sq = (n: number) => n * n
|
|
||||||
|
|
||||||
if (force < sq(DAL.MICROBIT_ACCELEROMETER_FREEFALL_TOLERANCE))
|
|
||||||
return DAL.MICROBIT_ACCELEROMETER_EVT_FREEFALL;
|
|
||||||
|
|
||||||
if (force > sq(DAL.MICROBIT_ACCELEROMETER_3G_TOLERANCE))
|
|
||||||
return DAL.MICROBIT_ACCELEROMETER_EVT_3G;
|
|
||||||
|
|
||||||
if (force > sq(DAL.MICROBIT_ACCELEROMETER_6G_TOLERANCE))
|
|
||||||
return DAL.MICROBIT_ACCELEROMETER_EVT_6G;
|
|
||||||
|
|
||||||
if (force > sq(DAL.MICROBIT_ACCELEROMETER_8G_TOLERANCE))
|
|
||||||
return DAL.MICROBIT_ACCELEROMETER_EVT_8G;
|
|
||||||
|
|
||||||
// Determine our posture.
|
|
||||||
if (this.getX() < (-1000 + DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE))
|
|
||||||
return DAL.MICROBIT_ACCELEROMETER_EVT_TILT_LEFT;
|
|
||||||
|
|
||||||
if (this.getX() > (1000 - DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE))
|
|
||||||
return DAL.MICROBIT_ACCELEROMETER_EVT_TILT_RIGHT;
|
|
||||||
|
|
||||||
if (this.getY() < (-1000 + DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE))
|
|
||||||
return DAL.MICROBIT_ACCELEROMETER_EVT_TILT_DOWN;
|
|
||||||
|
|
||||||
if (this.getY() > (1000 - DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE))
|
|
||||||
return DAL.MICROBIT_ACCELEROMETER_EVT_TILT_UP;
|
|
||||||
|
|
||||||
if (this.getZ() < (-1000 + DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE))
|
|
||||||
return DAL.MICROBIT_ACCELEROMETER_EVT_FACE_UP;
|
|
||||||
|
|
||||||
if (this.getZ() > (1000 - DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE))
|
|
||||||
return DAL.MICROBIT_ACCELEROMETER_EVT_FACE_DOWN;
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateGesture() {
|
|
||||||
// Determine what it looks like we're doing based on the latest sample...
|
|
||||||
let g = this.instantaneousPosture();
|
|
||||||
|
|
||||||
// Perform some low pass filtering to reduce jitter from any detected effects
|
|
||||||
if (g == this.currentGesture) {
|
|
||||||
if (this.sigma < DAL.MICROBIT_ACCELEROMETER_GESTURE_DAMPING)
|
|
||||||
this.sigma++;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.currentGesture = g;
|
|
||||||
this.sigma = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we've reached threshold, update our record and raise the relevant event...
|
|
||||||
if (this.currentGesture != this.lastGesture && this.sigma >= DAL.MICROBIT_ACCELEROMETER_GESTURE_DAMPING) {
|
|
||||||
this.lastGesture = this.currentGesture;
|
|
||||||
board().bus.queue(DAL.MICROBIT_ID_GESTURE, this.lastGesture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the X axis value of the latest update from the accelerometer.
|
|
||||||
* @param system The coordinate system to use. By default, a simple cartesian system is provided.
|
|
||||||
* @return The force measured in the X axis, in milli-g.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* @code
|
|
||||||
* uBit.accelerometer.getX();
|
|
||||||
* uBit.accelerometer.getX(RAW);
|
|
||||||
* @endcode
|
|
||||||
*/
|
|
||||||
public getX(system: MicroBitCoordinateSystem = MicroBitCoordinateSystem.SIMPLE_CARTESIAN): number {
|
|
||||||
this.activate();
|
|
||||||
switch (system) {
|
|
||||||
case MicroBitCoordinateSystem.SIMPLE_CARTESIAN:
|
|
||||||
return -this.sample.x;
|
|
||||||
|
|
||||||
case MicroBitCoordinateSystem.NORTH_EAST_DOWN:
|
|
||||||
return this.sample.y;
|
|
||||||
//case MicroBitCoordinateSystem.SIMPLE_CARTESIAN.RAW:
|
|
||||||
default:
|
|
||||||
return this.sample.x;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the Y axis value of the latest update from the accelerometer.
|
|
||||||
* @param system The coordinate system to use. By default, a simple cartesian system is provided.
|
|
||||||
* @return The force measured in the Y axis, in milli-g.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* @code
|
|
||||||
* uBit.accelerometer.getY();
|
|
||||||
* uBit.accelerometer.getY(RAW);
|
|
||||||
* @endcode
|
|
||||||
*/
|
|
||||||
public getY(system: MicroBitCoordinateSystem = MicroBitCoordinateSystem.SIMPLE_CARTESIAN): number {
|
|
||||||
this.activate();
|
|
||||||
switch (system) {
|
|
||||||
case MicroBitCoordinateSystem.SIMPLE_CARTESIAN:
|
|
||||||
return -this.sample.y;
|
|
||||||
|
|
||||||
case MicroBitCoordinateSystem.NORTH_EAST_DOWN:
|
|
||||||
return -this.sample.x;
|
|
||||||
//case RAW:
|
|
||||||
default:
|
|
||||||
return this.sample.y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the Z axis value of the latest update from the accelerometer.
|
|
||||||
* @param system The coordinate system to use. By default, a simple cartesian system is provided.
|
|
||||||
* @return The force measured in the Z axis, in milli-g.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* @code
|
|
||||||
* uBit.accelerometer.getZ();
|
|
||||||
* uBit.accelerometer.getZ(RAW);
|
|
||||||
* @endcode
|
|
||||||
*/
|
|
||||||
public getZ(system: MicroBitCoordinateSystem = MicroBitCoordinateSystem.SIMPLE_CARTESIAN): number {
|
|
||||||
this.activate();
|
|
||||||
switch (system) {
|
|
||||||
case MicroBitCoordinateSystem.NORTH_EAST_DOWN:
|
|
||||||
return -this.sample.z;
|
|
||||||
//case MicroBitCoordinateSystem.SIMPLE_CARTESIAN:
|
|
||||||
//case MicroBitCoordinateSystem.RAW:
|
|
||||||
default:
|
|
||||||
return this.sample.z;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides a rotation compensated pitch of the device, based on the latest update from the accelerometer.
|
|
||||||
* @return The pitch of the device, in degrees.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* @code
|
|
||||||
* uBit.accelerometer.getPitch();
|
|
||||||
* @endcode
|
|
||||||
*/
|
|
||||||
public getPitch(): number {
|
|
||||||
this.activate();
|
|
||||||
return Math.floor((360 * this.getPitchRadians()) / (2 * Math.PI));
|
|
||||||
}
|
|
||||||
|
|
||||||
getPitchRadians(): number {
|
|
||||||
this.recalculatePitchRoll();
|
|
||||||
return this.pitch;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides a rotation compensated roll of the device, based on the latest update from the accelerometer.
|
|
||||||
* @return The roll of the device, in degrees.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* @code
|
|
||||||
* uBit.accelerometer.getRoll();
|
|
||||||
* @endcode
|
|
||||||
*/
|
|
||||||
public getRoll(): number {
|
|
||||||
this.activate();
|
|
||||||
return Math.floor((360 * this.getRollRadians()) / (2 * Math.PI));
|
|
||||||
}
|
|
||||||
|
|
||||||
getRollRadians(): number {
|
|
||||||
this.recalculatePitchRoll();
|
|
||||||
return this.roll;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recalculate roll and pitch values for the current sample.
|
|
||||||
* We only do this at most once per sample, as the necessary trigonemteric functions are rather
|
|
||||||
* heavyweight for a CPU without a floating point unit...
|
|
||||||
*/
|
|
||||||
recalculatePitchRoll() {
|
|
||||||
let x = this.getX(MicroBitCoordinateSystem.NORTH_EAST_DOWN);
|
|
||||||
let y = this.getY(MicroBitCoordinateSystem.NORTH_EAST_DOWN);
|
|
||||||
let z = this.getZ(MicroBitCoordinateSystem.NORTH_EAST_DOWN);
|
|
||||||
|
|
||||||
this.roll = Math.atan2(y, z);
|
|
||||||
this.pitch = Math.atan(-x / (y * Math.sin(this.roll) + z * Math.cos(this.roll)));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class Board extends BaseBoard {
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
// the bus
|
|
||||||
bus: EventBus;
|
|
||||||
radio: RadioBus;
|
|
||||||
|
|
||||||
// display
|
|
||||||
image = createInternalImage(5);
|
|
||||||
brigthness = 255;
|
|
||||||
displayMode = DisplayMode.bw;
|
|
||||||
font: Image = createFont();
|
|
||||||
|
|
||||||
// buttons
|
|
||||||
usesButtonAB: boolean = false;
|
|
||||||
buttons: Button[];
|
|
||||||
|
|
||||||
// pins
|
|
||||||
pins: Pin[];
|
|
||||||
|
|
||||||
// serial
|
|
||||||
serialIn: string[] = [];
|
|
||||||
|
|
||||||
// sensors
|
|
||||||
accelerometer: Accelerometer;
|
|
||||||
|
|
||||||
// gestures
|
|
||||||
useShake = false;
|
|
||||||
|
|
||||||
usesHeading = false;
|
|
||||||
heading = 90;
|
|
||||||
|
|
||||||
usesTemperature = false;
|
|
||||||
temperature = 21;
|
|
||||||
|
|
||||||
usesLightLevel = false;
|
|
||||||
lightLevel = 128;
|
|
||||||
|
|
||||||
animationQ: AnimationQueue;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
this.id = "b" + Math_.random(2147483647);
|
|
||||||
this.animationQ = new AnimationQueue(runtime);
|
|
||||||
this.bus = new EventBus(runtime);
|
|
||||||
this.radio = new RadioBus(runtime);
|
|
||||||
this.accelerometer = new Accelerometer(runtime);
|
|
||||||
this.buttons = [
|
|
||||||
new Button(DAL.MICROBIT_ID_BUTTON_A),
|
|
||||||
new Button(DAL.MICROBIT_ID_BUTTON_B),
|
|
||||||
new Button(DAL.MICROBIT_ID_BUTTON_AB)
|
|
||||||
];
|
|
||||||
this.pins = [
|
|
||||||
new Pin(DAL.MICROBIT_ID_IO_P0),
|
|
||||||
new Pin(DAL.MICROBIT_ID_IO_P1),
|
|
||||||
new Pin(DAL.MICROBIT_ID_IO_P2),
|
|
||||||
new Pin(DAL.MICROBIT_ID_IO_P3),
|
|
||||||
new Pin(DAL.MICROBIT_ID_IO_P4),
|
|
||||||
new Pin(DAL.MICROBIT_ID_IO_P5),
|
|
||||||
new Pin(DAL.MICROBIT_ID_IO_P6),
|
|
||||||
new Pin(DAL.MICROBIT_ID_IO_P7),
|
|
||||||
new Pin(DAL.MICROBIT_ID_IO_P8),
|
|
||||||
new Pin(DAL.MICROBIT_ID_IO_P9),
|
|
||||||
new Pin(DAL.MICROBIT_ID_IO_P10),
|
|
||||||
new Pin(DAL.MICROBIT_ID_IO_P11),
|
|
||||||
new Pin(DAL.MICROBIT_ID_IO_P12),
|
|
||||||
new Pin(DAL.MICROBIT_ID_IO_P13),
|
|
||||||
new Pin(DAL.MICROBIT_ID_IO_P14),
|
|
||||||
new Pin(DAL.MICROBIT_ID_IO_P15),
|
|
||||||
new Pin(DAL.MICROBIT_ID_IO_P16),
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
new Pin(DAL.MICROBIT_ID_IO_P19),
|
|
||||||
new Pin(DAL.MICROBIT_ID_IO_P20)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
initAsync(msg: SimulatorRunMessage): Promise<void> {
|
|
||||||
let options = (msg.options || {}) as RuntimeOptions;
|
|
||||||
let theme: micro_bit.IBoardTheme;
|
|
||||||
switch (options.theme) {
|
|
||||||
case 'blue': theme = micro_bit.themes[0]; break;
|
|
||||||
case 'yellow': theme = micro_bit.themes[1]; break;
|
|
||||||
case 'green': theme = micro_bit.themes[2]; break;
|
|
||||||
case 'red': theme = micro_bit.themes[3]; break;
|
|
||||||
default: theme = pxsim.micro_bit.randomTheme();
|
|
||||||
}
|
|
||||||
|
|
||||||
let view = new pxsim.micro_bit.MicrobitBoardSvg({
|
|
||||||
theme: theme,
|
|
||||||
runtime: runtime
|
|
||||||
})
|
|
||||||
document.body.innerHTML = ""; // clear children
|
|
||||||
document.body.appendChild(view.element);
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
receiveMessage(msg: SimulatorMessage) {
|
|
||||||
if (!runtime || runtime.dead) return;
|
|
||||||
|
|
||||||
switch (msg.type || "") {
|
|
||||||
case "eventbus":
|
|
||||||
let ev = <SimulatorEventBusMessage>msg;
|
|
||||||
this.bus.queue(ev.id, ev.eventid, ev.value);
|
|
||||||
break;
|
|
||||||
case "serial":
|
|
||||||
this.serialIn.push((<SimulatorSerialMessage>msg).data || "");
|
|
||||||
break;
|
|
||||||
case "radiopacket":
|
|
||||||
let packet = <SimulatorRadioPacketMessage>msg;
|
|
||||||
this.radio.datagram.queue({ data: packet.data, rssi: packet.rssi || 0 })
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readSerial() {
|
|
||||||
let v = this.serialIn.shift() || "";
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
kill() {
|
|
||||||
super.kill();
|
|
||||||
AudioContextManager.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
serialOutBuffer: string = "";
|
|
||||||
writeSerial(s: string) {
|
|
||||||
for (let i = 0; i < s.length; ++i) {
|
|
||||||
let c = s[i];
|
|
||||||
this.serialOutBuffer += c;
|
|
||||||
if (c == "\n") {
|
|
||||||
Runtime.postMessage(<SimulatorSerialMessage>{
|
|
||||||
type: "serial",
|
|
||||||
data: this.serialOutBuffer,
|
|
||||||
id: runtime.id,
|
|
||||||
sim: true
|
|
||||||
})
|
|
||||||
this.serialOutBuffer = ""
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Image extends RefObject {
|
|
||||||
public static height: number = 5;
|
|
||||||
public width: number;
|
|
||||||
public data: number[];
|
|
||||||
constructor(width: number, data: number[]) {
|
|
||||||
super()
|
|
||||||
this.width = width;
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public print() {
|
|
||||||
console.log(`Image id:${this.id} refs:${this.refcnt} size:${this.width}x${Image.height}`)
|
|
||||||
}
|
|
||||||
public get(x: number, y: number): number {
|
|
||||||
if (x < 0 || x >= this.width || y < 0 || y >= 5) return 0;
|
|
||||||
return this.data[y * this.width + x];
|
|
||||||
}
|
|
||||||
public set(x: number, y: number, v: number) {
|
|
||||||
if (x < 0 || x >= this.width || y < 0 || y >= 5) return;
|
|
||||||
this.data[y * this.width + x] = Math.max(0, Math.min(255, v));
|
|
||||||
}
|
|
||||||
public copyTo(xSrcIndex: number, length: number, target: Image, xTargetIndex: number): void {
|
|
||||||
for (let x = 0; x < length; x++) {
|
|
||||||
for (let y = 0; y < 5; y++) {
|
|
||||||
let value = this.get(xSrcIndex + x, y);
|
|
||||||
target.set(xTargetIndex + x, y, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public shiftLeft(cols: number) {
|
|
||||||
for (let x = 0; x < this.width; ++x)
|
|
||||||
for (let y = 0; y < 5; ++y)
|
|
||||||
this.set(x, y, x < this.width - cols ? this.get(x + cols, y) : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public shiftRight(cols: number) {
|
|
||||||
for (let x = this.width - 1; x <= 0; --x)
|
|
||||||
for (let y = 0; y < 5; ++y)
|
|
||||||
this.set(x, y, x > cols ? this.get(x - cols, y) : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public clear(): void {
|
|
||||||
for (let i = 0; i < this.data.length; ++i)
|
|
||||||
this.data[i] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createInternalImage(width: number): Image {
|
|
||||||
let img = createImage(width)
|
|
||||||
pxsim.noLeakTracking(img)
|
|
||||||
return img
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createImage(width: number): Image {
|
|
||||||
return new Image(width, new Array(width * 5));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createImageFromBuffer(data: number[]): Image {
|
|
||||||
return new Image(data.length / 5, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createImageFromString(text: string): Image {
|
|
||||||
let font = board().font;
|
|
||||||
let w = font.width;
|
|
||||||
let sprite = createInternalImage(6 * text.length - 1);
|
|
||||||
let k = 0;
|
|
||||||
for (let i = 0; i < text.length; i++) {
|
|
||||||
let charCode = text.charCodeAt(i);
|
|
||||||
let charStart = (charCode - 32) * 5;
|
|
||||||
if (charStart < 0 || charStart + 5 > w) {
|
|
||||||
charCode = " ".charCodeAt(0);
|
|
||||||
charStart = (charCode - 32) * 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
font.copyTo(charStart, 5, sprite, k);
|
|
||||||
k = k + 5;
|
|
||||||
if (i < text.length - 1) {
|
|
||||||
k = k + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sprite;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createFont(): Image {
|
|
||||||
const data = [0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x8, 0x8, 0x0, 0x8, 0xa, 0x4a, 0x40, 0x0, 0x0, 0xa, 0x5f, 0xea, 0x5f, 0xea, 0xe, 0xd9, 0x2e, 0xd3, 0x6e, 0x19, 0x32, 0x44, 0x89, 0x33, 0xc, 0x92, 0x4c, 0x92, 0x4d, 0x8, 0x8, 0x0, 0x0, 0x0, 0x4, 0x88, 0x8, 0x8, 0x4, 0x8, 0x4, 0x84, 0x84, 0x88, 0x0, 0xa, 0x44, 0x8a, 0x40, 0x0, 0x4, 0x8e, 0xc4, 0x80, 0x0, 0x0, 0x0, 0x4, 0x88, 0x0, 0x0, 0xe, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x1, 0x22, 0x44, 0x88, 0x10, 0xc, 0x92, 0x52, 0x52, 0x4c, 0x4, 0x8c, 0x84, 0x84, 0x8e, 0x1c, 0x82, 0x4c, 0x90, 0x1e, 0x1e, 0xc2, 0x44, 0x92, 0x4c, 0x6, 0xca, 0x52, 0x5f, 0xe2, 0x1f, 0xf0, 0x1e, 0xc1, 0x3e, 0x2, 0x44, 0x8e, 0xd1, 0x2e, 0x1f, 0xe2, 0x44, 0x88, 0x10, 0xe, 0xd1, 0x2e, 0xd1, 0x2e, 0xe, 0xd1, 0x2e, 0xc4, 0x88, 0x0, 0x8, 0x0, 0x8, 0x0, 0x0, 0x4, 0x80, 0x4, 0x88, 0x2, 0x44, 0x88, 0x4, 0x82, 0x0, 0xe, 0xc0, 0xe, 0xc0, 0x8, 0x4, 0x82, 0x44, 0x88, 0xe, 0xd1, 0x26, 0xc0, 0x4, 0xe, 0xd1, 0x35, 0xb3, 0x6c, 0xc, 0x92, 0x5e, 0xd2, 0x52, 0x1c, 0x92, 0x5c, 0x92, 0x5c, 0xe, 0xd0, 0x10, 0x10, 0xe, 0x1c, 0x92, 0x52, 0x52, 0x5c, 0x1e, 0xd0, 0x1c, 0x90, 0x1e, 0x1e, 0xd0, 0x1c, 0x90, 0x10, 0xe, 0xd0, 0x13, 0x71, 0x2e, 0x12, 0x52, 0x5e, 0xd2, 0x52, 0x1c, 0x88, 0x8, 0x8, 0x1c, 0x1f, 0xe2, 0x42, 0x52, 0x4c, 0x12, 0x54, 0x98, 0x14, 0x92, 0x10, 0x10, 0x10, 0x10, 0x1e, 0x11, 0x3b, 0x75, 0xb1, 0x31, 0x11, 0x39, 0x35, 0xb3, 0x71, 0xc, 0x92, 0x52, 0x52, 0x4c, 0x1c, 0x92, 0x5c, 0x90, 0x10, 0xc, 0x92, 0x52, 0x4c, 0x86, 0x1c, 0x92, 0x5c, 0x92, 0x51, 0xe, 0xd0, 0xc, 0x82, 0x5c, 0x1f, 0xe4, 0x84, 0x84, 0x84, 0x12, 0x52, 0x52, 0x52, 0x4c, 0x11, 0x31, 0x31, 0x2a, 0x44, 0x11, 0x31, 0x35, 0xbb, 0x71, 0x12, 0x52, 0x4c, 0x92, 0x52, 0x11, 0x2a, 0x44, 0x84, 0x84, 0x1e, 0xc4, 0x88, 0x10, 0x1e, 0xe, 0xc8, 0x8, 0x8, 0xe, 0x10, 0x8, 0x4, 0x82, 0x41, 0xe, 0xc2, 0x42, 0x42, 0x4e, 0x4, 0x8a, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1f, 0x8, 0x4, 0x80, 0x0, 0x0, 0x0, 0xe, 0xd2, 0x52, 0x4f, 0x10, 0x10, 0x1c, 0x92, 0x5c, 0x0, 0xe, 0xd0, 0x10, 0xe, 0x2, 0x42, 0x4e, 0xd2, 0x4e, 0xc, 0x92, 0x5c, 0x90, 0xe, 0x6, 0xc8, 0x1c, 0x88, 0x8, 0xe, 0xd2, 0x4e, 0xc2, 0x4c, 0x10, 0x10, 0x1c, 0x92, 0x52, 0x8, 0x0, 0x8, 0x8, 0x8, 0x2, 0x40, 0x2, 0x42, 0x4c, 0x10, 0x14, 0x98, 0x14, 0x92, 0x8, 0x8, 0x8, 0x8, 0x6, 0x0, 0x1b, 0x75, 0xb1, 0x31, 0x0, 0x1c, 0x92, 0x52, 0x52, 0x0, 0xc, 0x92, 0x52, 0x4c, 0x0, 0x1c, 0x92, 0x5c, 0x90, 0x0, 0xe, 0xd2, 0x4e, 0xc2, 0x0, 0xe, 0xd0, 0x10, 0x10, 0x0, 0x6, 0xc8, 0x4, 0x98, 0x8, 0x8, 0xe, 0xc8, 0x7, 0x0, 0x12, 0x52, 0x52, 0x4f, 0x0, 0x11, 0x31, 0x2a, 0x44, 0x0, 0x11, 0x31, 0x35, 0xbb, 0x0, 0x12, 0x4c, 0x8c, 0x92, 0x0, 0x11, 0x2a, 0x44, 0x98, 0x0, 0x1e, 0xc4, 0x88, 0x1e, 0x6, 0xc4, 0x8c, 0x84, 0x86, 0x8, 0x8, 0x8, 0x8, 0x8, 0x18, 0x8, 0xc, 0x88, 0x18, 0x0, 0x0, 0xc, 0x83, 0x60];
|
|
||||||
|
|
||||||
let nb = data.length;
|
|
||||||
let n = nb / 5;
|
|
||||||
let font = createInternalImage(nb);
|
|
||||||
for (let c = 0; c < n; c++) {
|
|
||||||
for (let row = 0; row < 5; row++) {
|
|
||||||
let char = data[c * 5 + row];
|
|
||||||
for (let col = 0; col < 5; col++) {
|
|
||||||
if ((char & (1 << col)) != 0)
|
|
||||||
font.set((c * 5 + 4) - col, row, 255);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return font;
|
|
||||||
}
|
|
||||||
}
|
|
389
sim/state/accelerometer.ts
Normal file
389
sim/state/accelerometer.ts
Normal file
@ -0,0 +1,389 @@
|
|||||||
|
namespace pxsim.input {
|
||||||
|
export function onGesture(gesture: number, handler: RefAction) {
|
||||||
|
let b = board().accelerometerState;
|
||||||
|
b.accelerometer.activate();
|
||||||
|
|
||||||
|
if (gesture == 11 && !b.useShake) { // SAKE
|
||||||
|
b.useShake = true;
|
||||||
|
runtime.queueDisplayUpdate();
|
||||||
|
}
|
||||||
|
pxt.registerWithDal(DAL.MICROBIT_ID_GESTURE, gesture, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function acceleration(dimension: number): number {
|
||||||
|
let b = board().accelerometerState;
|
||||||
|
let acc = b.accelerometer;
|
||||||
|
acc.activate();
|
||||||
|
switch (dimension) {
|
||||||
|
case 0: return acc.getX();
|
||||||
|
case 1: return acc.getY();
|
||||||
|
case 2: return acc.getZ();
|
||||||
|
default: return Math.floor(Math.sqrt(acc.instantaneousAccelerationSquared()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rotation(kind: number): number {
|
||||||
|
let b = board().accelerometerState;
|
||||||
|
let acc = b.accelerometer;
|
||||||
|
acc.activate();
|
||||||
|
let x = acc.getX(MicroBitCoordinateSystem.NORTH_EAST_DOWN);
|
||||||
|
let y = acc.getX(MicroBitCoordinateSystem.NORTH_EAST_DOWN);
|
||||||
|
let z = acc.getX(MicroBitCoordinateSystem.NORTH_EAST_DOWN);
|
||||||
|
|
||||||
|
let roll = Math.atan2(y, z);
|
||||||
|
let pitch = Math.atan(-x / (y * Math.sin(roll) + z * Math.cos(roll)));
|
||||||
|
|
||||||
|
let r = 0;
|
||||||
|
switch (kind) {
|
||||||
|
case 0: r = pitch; break;
|
||||||
|
case 1: r = roll; break;
|
||||||
|
}
|
||||||
|
return Math.floor(r / Math.PI * 180);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAccelerometerRange(range: number) {
|
||||||
|
let b = board().accelerometerState;
|
||||||
|
b.accelerometer.setSampleRange(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim {
|
||||||
|
interface AccelerometerSample {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShakeHistory {
|
||||||
|
x: boolean;
|
||||||
|
y: boolean;
|
||||||
|
z: boolean;
|
||||||
|
count: number;
|
||||||
|
shaken: number;
|
||||||
|
timer: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Co-ordinate systems that can be used.
|
||||||
|
* RAW: Unaltered data. Data will be returned directly from the accelerometer.
|
||||||
|
*
|
||||||
|
* SIMPLE_CARTESIAN: Data will be returned based on an easy to understand alignment, consistent with the cartesian system taught in schools.
|
||||||
|
* When held upright, facing the user:
|
||||||
|
*
|
||||||
|
* /
|
||||||
|
* +--------------------+ z
|
||||||
|
* | |
|
||||||
|
* | ..... |
|
||||||
|
* | * ..... * |
|
||||||
|
* ^ | ..... |
|
||||||
|
* | | |
|
||||||
|
* y +--------------------+ x-->
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* NORTH_EAST_DOWN: Data will be returned based on the industry convention of the North East Down (NED) system.
|
||||||
|
* When held upright, facing the user:
|
||||||
|
*
|
||||||
|
* z
|
||||||
|
* +--------------------+ /
|
||||||
|
* | |
|
||||||
|
* | ..... |
|
||||||
|
* | * ..... * |
|
||||||
|
* ^ | ..... |
|
||||||
|
* | | |
|
||||||
|
* x +--------------------+ y-->
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export enum MicroBitCoordinateSystem {
|
||||||
|
RAW,
|
||||||
|
SIMPLE_CARTESIAN,
|
||||||
|
NORTH_EAST_DOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Accelerometer {
|
||||||
|
private sigma: number = 0; // the number of ticks that the instantaneous gesture has been stable.
|
||||||
|
private lastGesture: number = 0; // the last, stable gesture recorded.
|
||||||
|
private currentGesture: number = 0 // the instantaneous, unfiltered gesture detected.
|
||||||
|
private sample: AccelerometerSample = { x: 0, y: 0, z: -1023 }
|
||||||
|
private shake: ShakeHistory = { x: false, y: false, z: false, count: 0, shaken: 0, timer: 0 }; // State information needed to detect shake events.
|
||||||
|
private pitch: number;
|
||||||
|
private roll: number;
|
||||||
|
private id: number;
|
||||||
|
public isActive = false;
|
||||||
|
public sampleRange = 2;
|
||||||
|
|
||||||
|
constructor(public runtime: Runtime) {
|
||||||
|
this.id = DAL.MICROBIT_ID_ACCELEROMETER;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSampleRange(range: number) {
|
||||||
|
this.activate();
|
||||||
|
this.sampleRange = Math.max(1, Math.min(8, range));
|
||||||
|
}
|
||||||
|
|
||||||
|
public activate() {
|
||||||
|
if (!this.isActive) {
|
||||||
|
this.isActive = true;
|
||||||
|
this.runtime.queueDisplayUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the acceleration data from the accelerometer, and stores it in our buffer.
|
||||||
|
* This is called by the tick() member function, if the interrupt is set!
|
||||||
|
*/
|
||||||
|
public update(x: number, y: number, z: number) {
|
||||||
|
// read MSB values...
|
||||||
|
this.sample.x = Math.floor(x);
|
||||||
|
this.sample.y = Math.floor(y);
|
||||||
|
this.sample.z = Math.floor(z);
|
||||||
|
|
||||||
|
// Update gesture tracking
|
||||||
|
this.updateGesture();
|
||||||
|
|
||||||
|
// Indicate that a new sample is available
|
||||||
|
board().bus.queue(this.id, DAL.MICROBIT_ACCELEROMETER_EVT_DATA_UPDATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
public instantaneousAccelerationSquared() {
|
||||||
|
// Use pythagoras theorem to determine the combined force acting on the device.
|
||||||
|
return this.sample.x * this.sample.x + this.sample.y * this.sample.y + this.sample.z * this.sample.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service function. Determines the best guess posture of the device based on instantaneous data.
|
||||||
|
* This makes no use of historic data (except for shake), and forms this input to the filter implemented in updateGesture().
|
||||||
|
*
|
||||||
|
* @return A best guess of the current posture of the device, based on instantaneous data.
|
||||||
|
*/
|
||||||
|
private instantaneousPosture(): number {
|
||||||
|
let force = this.instantaneousAccelerationSquared();
|
||||||
|
let shakeDetected = false;
|
||||||
|
|
||||||
|
// Test for shake events.
|
||||||
|
// We detect a shake by measuring zero crossings in each axis. In other words, if we see a strong acceleration to the left followed by
|
||||||
|
// a string acceleration to the right, then we can infer a shake. Similarly, we can do this for each acxis (left/right, up/down, in/out).
|
||||||
|
//
|
||||||
|
// If we see enough zero crossings in succession (MICROBIT_ACCELEROMETER_SHAKE_COUNT_THRESHOLD), then we decide that the device
|
||||||
|
// has been shaken.
|
||||||
|
if ((this.getX() < -DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && this.shake.x) || (this.getX() > DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && !this.shake.x)) {
|
||||||
|
shakeDetected = true;
|
||||||
|
this.shake.x = !this.shake.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((this.getY() < -DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && this.shake.y) || (this.getY() > DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && !this.shake.y)) {
|
||||||
|
shakeDetected = true;
|
||||||
|
this.shake.y = !this.shake.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((this.getZ() < -DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && this.shake.z) || (this.getZ() > DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && !this.shake.z)) {
|
||||||
|
shakeDetected = true;
|
||||||
|
this.shake.z = !this.shake.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shakeDetected && this.shake.count < DAL.MICROBIT_ACCELEROMETER_SHAKE_COUNT_THRESHOLD && ++this.shake.count == DAL.MICROBIT_ACCELEROMETER_SHAKE_COUNT_THRESHOLD)
|
||||||
|
this.shake.shaken = 1;
|
||||||
|
|
||||||
|
if (++this.shake.timer >= DAL.MICROBIT_ACCELEROMETER_SHAKE_DAMPING) {
|
||||||
|
this.shake.timer = 0;
|
||||||
|
if (this.shake.count > 0) {
|
||||||
|
if (--this.shake.count == 0)
|
||||||
|
this.shake.shaken = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.shake.shaken)
|
||||||
|
return DAL.MICROBIT_ACCELEROMETER_EVT_SHAKE;
|
||||||
|
|
||||||
|
let sq = (n: number) => n * n
|
||||||
|
|
||||||
|
if (force < sq(DAL.MICROBIT_ACCELEROMETER_FREEFALL_TOLERANCE))
|
||||||
|
return DAL.MICROBIT_ACCELEROMETER_EVT_FREEFALL;
|
||||||
|
|
||||||
|
if (force > sq(DAL.MICROBIT_ACCELEROMETER_3G_TOLERANCE))
|
||||||
|
return DAL.MICROBIT_ACCELEROMETER_EVT_3G;
|
||||||
|
|
||||||
|
if (force > sq(DAL.MICROBIT_ACCELEROMETER_6G_TOLERANCE))
|
||||||
|
return DAL.MICROBIT_ACCELEROMETER_EVT_6G;
|
||||||
|
|
||||||
|
if (force > sq(DAL.MICROBIT_ACCELEROMETER_8G_TOLERANCE))
|
||||||
|
return DAL.MICROBIT_ACCELEROMETER_EVT_8G;
|
||||||
|
|
||||||
|
// Determine our posture.
|
||||||
|
if (this.getX() < (-1000 + DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE))
|
||||||
|
return DAL.MICROBIT_ACCELEROMETER_EVT_TILT_LEFT;
|
||||||
|
|
||||||
|
if (this.getX() > (1000 - DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE))
|
||||||
|
return DAL.MICROBIT_ACCELEROMETER_EVT_TILT_RIGHT;
|
||||||
|
|
||||||
|
if (this.getY() < (-1000 + DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE))
|
||||||
|
return DAL.MICROBIT_ACCELEROMETER_EVT_TILT_DOWN;
|
||||||
|
|
||||||
|
if (this.getY() > (1000 - DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE))
|
||||||
|
return DAL.MICROBIT_ACCELEROMETER_EVT_TILT_UP;
|
||||||
|
|
||||||
|
if (this.getZ() < (-1000 + DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE))
|
||||||
|
return DAL.MICROBIT_ACCELEROMETER_EVT_FACE_UP;
|
||||||
|
|
||||||
|
if (this.getZ() > (1000 - DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE))
|
||||||
|
return DAL.MICROBIT_ACCELEROMETER_EVT_FACE_DOWN;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateGesture() {
|
||||||
|
// Determine what it looks like we're doing based on the latest sample...
|
||||||
|
let g = this.instantaneousPosture();
|
||||||
|
|
||||||
|
// Perform some low pass filtering to reduce jitter from any detected effects
|
||||||
|
if (g == this.currentGesture) {
|
||||||
|
if (this.sigma < DAL.MICROBIT_ACCELEROMETER_GESTURE_DAMPING)
|
||||||
|
this.sigma++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.currentGesture = g;
|
||||||
|
this.sigma = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've reached threshold, update our record and raise the relevant event...
|
||||||
|
if (this.currentGesture != this.lastGesture && this.sigma >= DAL.MICROBIT_ACCELEROMETER_GESTURE_DAMPING) {
|
||||||
|
this.lastGesture = this.currentGesture;
|
||||||
|
board().bus.queue(DAL.MICROBIT_ID_GESTURE, this.lastGesture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the X axis value of the latest update from the accelerometer.
|
||||||
|
* @param system The coordinate system to use. By default, a simple cartesian system is provided.
|
||||||
|
* @return The force measured in the X axis, in milli-g.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* @code
|
||||||
|
* uBit.accelerometer.getX();
|
||||||
|
* uBit.accelerometer.getX(RAW);
|
||||||
|
* @endcode
|
||||||
|
*/
|
||||||
|
public getX(system: MicroBitCoordinateSystem = MicroBitCoordinateSystem.SIMPLE_CARTESIAN): number {
|
||||||
|
this.activate();
|
||||||
|
switch (system) {
|
||||||
|
case MicroBitCoordinateSystem.SIMPLE_CARTESIAN:
|
||||||
|
return -this.sample.x;
|
||||||
|
|
||||||
|
case MicroBitCoordinateSystem.NORTH_EAST_DOWN:
|
||||||
|
return this.sample.y;
|
||||||
|
//case MicroBitCoordinateSystem.SIMPLE_CARTESIAN.RAW:
|
||||||
|
default:
|
||||||
|
return this.sample.x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the Y axis value of the latest update from the accelerometer.
|
||||||
|
* @param system The coordinate system to use. By default, a simple cartesian system is provided.
|
||||||
|
* @return The force measured in the Y axis, in milli-g.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* @code
|
||||||
|
* uBit.accelerometer.getY();
|
||||||
|
* uBit.accelerometer.getY(RAW);
|
||||||
|
* @endcode
|
||||||
|
*/
|
||||||
|
public getY(system: MicroBitCoordinateSystem = MicroBitCoordinateSystem.SIMPLE_CARTESIAN): number {
|
||||||
|
this.activate();
|
||||||
|
switch (system) {
|
||||||
|
case MicroBitCoordinateSystem.SIMPLE_CARTESIAN:
|
||||||
|
return -this.sample.y;
|
||||||
|
|
||||||
|
case MicroBitCoordinateSystem.NORTH_EAST_DOWN:
|
||||||
|
return -this.sample.x;
|
||||||
|
//case RAW:
|
||||||
|
default:
|
||||||
|
return this.sample.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the Z axis value of the latest update from the accelerometer.
|
||||||
|
* @param system The coordinate system to use. By default, a simple cartesian system is provided.
|
||||||
|
* @return The force measured in the Z axis, in milli-g.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* @code
|
||||||
|
* uBit.accelerometer.getZ();
|
||||||
|
* uBit.accelerometer.getZ(RAW);
|
||||||
|
* @endcode
|
||||||
|
*/
|
||||||
|
public getZ(system: MicroBitCoordinateSystem = MicroBitCoordinateSystem.SIMPLE_CARTESIAN): number {
|
||||||
|
this.activate();
|
||||||
|
switch (system) {
|
||||||
|
case MicroBitCoordinateSystem.NORTH_EAST_DOWN:
|
||||||
|
return -this.sample.z;
|
||||||
|
//case MicroBitCoordinateSystem.SIMPLE_CARTESIAN:
|
||||||
|
//case MicroBitCoordinateSystem.RAW:
|
||||||
|
default:
|
||||||
|
return this.sample.z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a rotation compensated pitch of the device, based on the latest update from the accelerometer.
|
||||||
|
* @return The pitch of the device, in degrees.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* @code
|
||||||
|
* uBit.accelerometer.getPitch();
|
||||||
|
* @endcode
|
||||||
|
*/
|
||||||
|
public getPitch(): number {
|
||||||
|
this.activate();
|
||||||
|
return Math.floor((360 * this.getPitchRadians()) / (2 * Math.PI));
|
||||||
|
}
|
||||||
|
|
||||||
|
getPitchRadians(): number {
|
||||||
|
this.recalculatePitchRoll();
|
||||||
|
return this.pitch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a rotation compensated roll of the device, based on the latest update from the accelerometer.
|
||||||
|
* @return The roll of the device, in degrees.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* @code
|
||||||
|
* uBit.accelerometer.getRoll();
|
||||||
|
* @endcode
|
||||||
|
*/
|
||||||
|
public getRoll(): number {
|
||||||
|
this.activate();
|
||||||
|
return Math.floor((360 * this.getRollRadians()) / (2 * Math.PI));
|
||||||
|
}
|
||||||
|
|
||||||
|
getRollRadians(): number {
|
||||||
|
this.recalculatePitchRoll();
|
||||||
|
return this.roll;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recalculate roll and pitch values for the current sample.
|
||||||
|
* We only do this at most once per sample, as the necessary trigonemteric functions are rather
|
||||||
|
* heavyweight for a CPU without a floating point unit...
|
||||||
|
*/
|
||||||
|
recalculatePitchRoll() {
|
||||||
|
let x = this.getX(MicroBitCoordinateSystem.NORTH_EAST_DOWN);
|
||||||
|
let y = this.getY(MicroBitCoordinateSystem.NORTH_EAST_DOWN);
|
||||||
|
let z = this.getZ(MicroBitCoordinateSystem.NORTH_EAST_DOWN);
|
||||||
|
|
||||||
|
this.roll = Math.atan2(y, z);
|
||||||
|
this.pitch = Math.atan(-x / (y * Math.sin(this.roll) + z * Math.cos(this.roll)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AccelerometerState {
|
||||||
|
accelerometer: Accelerometer;
|
||||||
|
useShake = false;
|
||||||
|
|
||||||
|
constructor(runtime: Runtime) {
|
||||||
|
this.accelerometer = new Accelerometer(runtime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
sim/state/buttonpair.ts
Normal file
41
sim/state/buttonpair.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
namespace pxsim.input {
|
||||||
|
export function onButtonPressed(button: number, handler: RefAction): void {
|
||||||
|
let b = board().buttonPairState;
|
||||||
|
if (button == DAL.MICROBIT_ID_BUTTON_AB && !b.usesButtonAB) {
|
||||||
|
b.usesButtonAB = true;
|
||||||
|
runtime.queueDisplayUpdate();
|
||||||
|
}
|
||||||
|
pxt.registerWithDal(button, DAL.MICROBIT_BUTTON_EVT_CLICK, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buttonIsPressed(button: number): boolean {
|
||||||
|
let b = board().buttonPairState;
|
||||||
|
if (button == DAL.MICROBIT_ID_BUTTON_AB && !b.usesButtonAB) {
|
||||||
|
b.usesButtonAB = true;
|
||||||
|
runtime.queueDisplayUpdate();
|
||||||
|
}
|
||||||
|
if (button == DAL.MICROBIT_ID_BUTTON_A) return b.aBtn.pressed;
|
||||||
|
if (button == DAL.MICROBIT_ID_BUTTON_B) return b.bBtn.pressed;
|
||||||
|
return b.abBtn.pressed || (b.aBtn.pressed && b.bBtn.pressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim {
|
||||||
|
export class Button {
|
||||||
|
constructor(public id: number) { }
|
||||||
|
pressed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ButtonPairState {
|
||||||
|
usesButtonAB: boolean = false;
|
||||||
|
aBtn: Button;
|
||||||
|
bBtn: Button;
|
||||||
|
abBtn: Button;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.aBtn = new Button(DAL.MICROBIT_ID_BUTTON_A);
|
||||||
|
this.bBtn = new Button(DAL.MICROBIT_ID_BUTTON_B);
|
||||||
|
this.abBtn = new Button(DAL.MICROBIT_ID_BUTTON_AB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
sim/state/compass.ts
Normal file
22
sim/state/compass.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
namespace pxsim.input {
|
||||||
|
export function compassHeading(): number {
|
||||||
|
let b = board().compassState;
|
||||||
|
if (!b.usesHeading) {
|
||||||
|
b.usesHeading = true;
|
||||||
|
runtime.queueDisplayUpdate();
|
||||||
|
}
|
||||||
|
return b.heading;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function magneticForce(): number {
|
||||||
|
// TODO
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim {
|
||||||
|
export class CompassState {
|
||||||
|
usesHeading = false;
|
||||||
|
heading = 90;
|
||||||
|
}
|
||||||
|
}
|
178
sim/state/edgeconnector.ts
Normal file
178
sim/state/edgeconnector.ts
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
namespace pxsim.input {
|
||||||
|
export function onPinPressed(pinId: number, handler: RefAction) {
|
||||||
|
let pin = getPin(pinId);
|
||||||
|
if (!pin) return;
|
||||||
|
pin.isTouched();
|
||||||
|
pxt.registerWithDal(pin.id, DAL.MICROBIT_BUTTON_EVT_CLICK, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onPinReleased(pinId: number, handler: RefAction) {
|
||||||
|
let pin = getPin(pinId);
|
||||||
|
if (!pin) return;
|
||||||
|
pin.isTouched();
|
||||||
|
pxt.registerWithDal(pin.id, DAL.MICROBIT_BUTTON_EVT_UP, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pinIsPressed(pinId: number): boolean {
|
||||||
|
let pin = getPin(pinId);
|
||||||
|
if (!pin) return false;
|
||||||
|
return pin.isTouched();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim {
|
||||||
|
export function getPin(id: number) {
|
||||||
|
return board().edgeConnectorState.getPin(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PinFlags {
|
||||||
|
Unused = 0,
|
||||||
|
Digital = 0x0001,
|
||||||
|
Analog = 0x0002,
|
||||||
|
Input = 0x0004,
|
||||||
|
Output = 0x0008,
|
||||||
|
Touch = 0x0010
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Pin {
|
||||||
|
constructor(public id: number) { }
|
||||||
|
touched = false;
|
||||||
|
value = 0;
|
||||||
|
period = 0;
|
||||||
|
mode = PinFlags.Unused;
|
||||||
|
pitch = false;
|
||||||
|
pull = 0; // PullDown
|
||||||
|
|
||||||
|
isTouched(): boolean {
|
||||||
|
this.mode = PinFlags.Touch;
|
||||||
|
return this.touched;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EdgeConnectorState {
|
||||||
|
pins: Pin[];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.pins = [
|
||||||
|
new Pin(DAL.MICROBIT_ID_IO_P0),
|
||||||
|
new Pin(DAL.MICROBIT_ID_IO_P1),
|
||||||
|
new Pin(DAL.MICROBIT_ID_IO_P2),
|
||||||
|
new Pin(DAL.MICROBIT_ID_IO_P3),
|
||||||
|
new Pin(DAL.MICROBIT_ID_IO_P4),
|
||||||
|
new Pin(DAL.MICROBIT_ID_IO_P5),
|
||||||
|
new Pin(DAL.MICROBIT_ID_IO_P6),
|
||||||
|
new Pin(DAL.MICROBIT_ID_IO_P7),
|
||||||
|
new Pin(DAL.MICROBIT_ID_IO_P8),
|
||||||
|
new Pin(DAL.MICROBIT_ID_IO_P9),
|
||||||
|
new Pin(DAL.MICROBIT_ID_IO_P10),
|
||||||
|
new Pin(DAL.MICROBIT_ID_IO_P11),
|
||||||
|
new Pin(DAL.MICROBIT_ID_IO_P12),
|
||||||
|
new Pin(DAL.MICROBIT_ID_IO_P13),
|
||||||
|
new Pin(DAL.MICROBIT_ID_IO_P14),
|
||||||
|
new Pin(DAL.MICROBIT_ID_IO_P15),
|
||||||
|
new Pin(DAL.MICROBIT_ID_IO_P16),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new Pin(DAL.MICROBIT_ID_IO_P19),
|
||||||
|
new Pin(DAL.MICROBIT_ID_IO_P20)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPin(id: number) {
|
||||||
|
return this.pins.filter(p => p && p.id == id)[0] || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim.pins {
|
||||||
|
export function digitalReadPin(pinId: number): number {
|
||||||
|
let pin = getPin(pinId);
|
||||||
|
if (!pin) return;
|
||||||
|
pin.mode = PinFlags.Digital | PinFlags.Input;
|
||||||
|
return pin.value > 100 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function digitalWritePin(pinId: number, value: number) {
|
||||||
|
let pin = getPin(pinId);
|
||||||
|
if (!pin) return;
|
||||||
|
pin.mode = PinFlags.Digital | PinFlags.Output;
|
||||||
|
pin.value = value > 0 ? 1023 : 0;
|
||||||
|
runtime.queueDisplayUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setPull(pinId: number, pull: number) {
|
||||||
|
let pin = getPin(pinId);
|
||||||
|
if (!pin) return;
|
||||||
|
pin.pull = pull;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function analogReadPin(pinId: number): number {
|
||||||
|
let pin = getPin(pinId);
|
||||||
|
if (!pin) return;
|
||||||
|
pin.mode = PinFlags.Analog | PinFlags.Input;
|
||||||
|
return pin.value || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function analogWritePin(pinId: number, value: number) {
|
||||||
|
let pin = getPin(pinId);
|
||||||
|
if (!pin) return;
|
||||||
|
pin.mode = PinFlags.Analog | PinFlags.Output;
|
||||||
|
pin.value = value ? 1 : 0;
|
||||||
|
runtime.queueDisplayUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function analogSetPeriod(pinId: number, micros: number) {
|
||||||
|
let pin = getPin(pinId);
|
||||||
|
if (!pin) return;
|
||||||
|
pin.mode = PinFlags.Analog | PinFlags.Output;
|
||||||
|
pin.period = micros;
|
||||||
|
runtime.queueDisplayUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function servoWritePin(pinId: number, value: number) {
|
||||||
|
analogSetPeriod(pinId, 20000);
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
export function servoSetPulse(pinId: number, micros: number) {
|
||||||
|
let pin = getPin(pinId);
|
||||||
|
if (!pin) return;
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
export function analogSetPitchPin(pinId: number) {
|
||||||
|
let pin = getPin(pinId);
|
||||||
|
if (!pin) return;
|
||||||
|
board().edgeConnectorState.pins.filter(p => !!p).forEach(p => p.pitch = false);
|
||||||
|
pin.pitch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function analogPitch(frequency: number, ms: number) {
|
||||||
|
// update analog output
|
||||||
|
let pins = board().edgeConnectorState.pins;
|
||||||
|
let pin = pins.filter(pin => !!pin && pin.pitch)[0] || pins[0];
|
||||||
|
pin.mode = PinFlags.Analog | PinFlags.Output;
|
||||||
|
if (frequency <= 0) {
|
||||||
|
pin.value = 0;
|
||||||
|
pin.period = 0;
|
||||||
|
} else {
|
||||||
|
pin.value = 512;
|
||||||
|
pin.period = 1000000 / frequency;
|
||||||
|
}
|
||||||
|
runtime.queueDisplayUpdate();
|
||||||
|
|
||||||
|
let cb = getResume();
|
||||||
|
AudioContextManager.tone(frequency, 1);
|
||||||
|
if (ms <= 0) cb();
|
||||||
|
else {
|
||||||
|
setTimeout(() => {
|
||||||
|
AudioContextManager.stop();
|
||||||
|
pin.value = 0;
|
||||||
|
pin.period = 0;
|
||||||
|
pin.mode = PinFlags.Unused;
|
||||||
|
runtime.queueDisplayUpdate();
|
||||||
|
cb()
|
||||||
|
}, ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
357
sim/state/ledmatrix.ts
Normal file
357
sim/state/ledmatrix.ts
Normal file
@ -0,0 +1,357 @@
|
|||||||
|
namespace pxsim {
|
||||||
|
export enum DisplayMode {
|
||||||
|
bw,
|
||||||
|
greyscale
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LedMatrixState {
|
||||||
|
image = createInternalImage(5);
|
||||||
|
brigthness = 255;
|
||||||
|
displayMode = DisplayMode.bw;
|
||||||
|
font: Image = createFont();
|
||||||
|
|
||||||
|
animationQ: AnimationQueue;
|
||||||
|
|
||||||
|
constructor(runtime: Runtime) {
|
||||||
|
this.animationQ = new AnimationQueue(runtime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Image extends RefObject {
|
||||||
|
public static height: number = 5;
|
||||||
|
public width: number;
|
||||||
|
public data: number[];
|
||||||
|
constructor(width: number, data: number[]) {
|
||||||
|
super();
|
||||||
|
this.width = width;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
public print() {
|
||||||
|
console.log(`Image id:${this.id} refs:${this.refcnt} size:${this.width}x${Image.height}`)
|
||||||
|
}
|
||||||
|
public get(x: number, y: number): number {
|
||||||
|
if (x < 0 || x >= this.width || y < 0 || y >= 5) return 0;
|
||||||
|
return this.data[y * this.width + x];
|
||||||
|
}
|
||||||
|
public set(x: number, y: number, v: number) {
|
||||||
|
if (x < 0 || x >= this.width || y < 0 || y >= 5) return;
|
||||||
|
this.data[y * this.width + x] = Math.max(0, Math.min(255, v));
|
||||||
|
}
|
||||||
|
public copyTo(xSrcIndex: number, length: number, target: Image, xTargetIndex: number): void {
|
||||||
|
for (let x = 0; x < length; x++) {
|
||||||
|
for (let y = 0; y < 5; y++) {
|
||||||
|
let value = this.get(xSrcIndex + x, y);
|
||||||
|
target.set(xTargetIndex + x, y, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public shiftLeft(cols: number) {
|
||||||
|
for (let x = 0; x < this.width; ++x)
|
||||||
|
for (let y = 0; y < 5; ++y)
|
||||||
|
this.set(x, y, x < this.width - cols ? this.get(x + cols, y) : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public shiftRight(cols: number) {
|
||||||
|
for (let x = this.width - 1; x <= 0; --x)
|
||||||
|
for (let y = 0; y < 5; ++y)
|
||||||
|
this.set(x, y, x > cols ? this.get(x - cols, y) : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(): void {
|
||||||
|
for (let i = 0; i < this.data.length; ++i)
|
||||||
|
this.data[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createInternalImage(width: number): Image {
|
||||||
|
let img = createImage(width)
|
||||||
|
pxsim.noLeakTracking(img)
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createImage(width: number): Image {
|
||||||
|
return new Image(width, new Array(width * 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createImageFromBuffer(data: number[]): Image {
|
||||||
|
return new Image(data.length / 5, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createImageFromString(text: string): Image {
|
||||||
|
let font = board().ledMatrixState.font;
|
||||||
|
let w = font.width;
|
||||||
|
let sprite = createInternalImage(6 * text.length - 1);
|
||||||
|
let k = 0;
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
let charCode = text.charCodeAt(i);
|
||||||
|
let charStart = (charCode - 32) * 5;
|
||||||
|
if (charStart < 0 || charStart + 5 > w) {
|
||||||
|
charCode = " ".charCodeAt(0);
|
||||||
|
charStart = (charCode - 32) * 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
font.copyTo(charStart, 5, sprite, k);
|
||||||
|
k = k + 5;
|
||||||
|
if (i < text.length - 1) {
|
||||||
|
k = k + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sprite;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createFont(): Image {
|
||||||
|
const data = [0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x8, 0x8, 0x0, 0x8, 0xa, 0x4a, 0x40, 0x0, 0x0, 0xa, 0x5f, 0xea, 0x5f, 0xea, 0xe, 0xd9, 0x2e, 0xd3, 0x6e, 0x19, 0x32, 0x44, 0x89, 0x33, 0xc, 0x92, 0x4c, 0x92, 0x4d, 0x8, 0x8, 0x0, 0x0, 0x0, 0x4, 0x88, 0x8, 0x8, 0x4, 0x8, 0x4, 0x84, 0x84, 0x88, 0x0, 0xa, 0x44, 0x8a, 0x40, 0x0, 0x4, 0x8e, 0xc4, 0x80, 0x0, 0x0, 0x0, 0x4, 0x88, 0x0, 0x0, 0xe, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x1, 0x22, 0x44, 0x88, 0x10, 0xc, 0x92, 0x52, 0x52, 0x4c, 0x4, 0x8c, 0x84, 0x84, 0x8e, 0x1c, 0x82, 0x4c, 0x90, 0x1e, 0x1e, 0xc2, 0x44, 0x92, 0x4c, 0x6, 0xca, 0x52, 0x5f, 0xe2, 0x1f, 0xf0, 0x1e, 0xc1, 0x3e, 0x2, 0x44, 0x8e, 0xd1, 0x2e, 0x1f, 0xe2, 0x44, 0x88, 0x10, 0xe, 0xd1, 0x2e, 0xd1, 0x2e, 0xe, 0xd1, 0x2e, 0xc4, 0x88, 0x0, 0x8, 0x0, 0x8, 0x0, 0x0, 0x4, 0x80, 0x4, 0x88, 0x2, 0x44, 0x88, 0x4, 0x82, 0x0, 0xe, 0xc0, 0xe, 0xc0, 0x8, 0x4, 0x82, 0x44, 0x88, 0xe, 0xd1, 0x26, 0xc0, 0x4, 0xe, 0xd1, 0x35, 0xb3, 0x6c, 0xc, 0x92, 0x5e, 0xd2, 0x52, 0x1c, 0x92, 0x5c, 0x92, 0x5c, 0xe, 0xd0, 0x10, 0x10, 0xe, 0x1c, 0x92, 0x52, 0x52, 0x5c, 0x1e, 0xd0, 0x1c, 0x90, 0x1e, 0x1e, 0xd0, 0x1c, 0x90, 0x10, 0xe, 0xd0, 0x13, 0x71, 0x2e, 0x12, 0x52, 0x5e, 0xd2, 0x52, 0x1c, 0x88, 0x8, 0x8, 0x1c, 0x1f, 0xe2, 0x42, 0x52, 0x4c, 0x12, 0x54, 0x98, 0x14, 0x92, 0x10, 0x10, 0x10, 0x10, 0x1e, 0x11, 0x3b, 0x75, 0xb1, 0x31, 0x11, 0x39, 0x35, 0xb3, 0x71, 0xc, 0x92, 0x52, 0x52, 0x4c, 0x1c, 0x92, 0x5c, 0x90, 0x10, 0xc, 0x92, 0x52, 0x4c, 0x86, 0x1c, 0x92, 0x5c, 0x92, 0x51, 0xe, 0xd0, 0xc, 0x82, 0x5c, 0x1f, 0xe4, 0x84, 0x84, 0x84, 0x12, 0x52, 0x52, 0x52, 0x4c, 0x11, 0x31, 0x31, 0x2a, 0x44, 0x11, 0x31, 0x35, 0xbb, 0x71, 0x12, 0x52, 0x4c, 0x92, 0x52, 0x11, 0x2a, 0x44, 0x84, 0x84, 0x1e, 0xc4, 0x88, 0x10, 0x1e, 0xe, 0xc8, 0x8, 0x8, 0xe, 0x10, 0x8, 0x4, 0x82, 0x41, 0xe, 0xc2, 0x42, 0x42, 0x4e, 0x4, 0x8a, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1f, 0x8, 0x4, 0x80, 0x0, 0x0, 0x0, 0xe, 0xd2, 0x52, 0x4f, 0x10, 0x10, 0x1c, 0x92, 0x5c, 0x0, 0xe, 0xd0, 0x10, 0xe, 0x2, 0x42, 0x4e, 0xd2, 0x4e, 0xc, 0x92, 0x5c, 0x90, 0xe, 0x6, 0xc8, 0x1c, 0x88, 0x8, 0xe, 0xd2, 0x4e, 0xc2, 0x4c, 0x10, 0x10, 0x1c, 0x92, 0x52, 0x8, 0x0, 0x8, 0x8, 0x8, 0x2, 0x40, 0x2, 0x42, 0x4c, 0x10, 0x14, 0x98, 0x14, 0x92, 0x8, 0x8, 0x8, 0x8, 0x6, 0x0, 0x1b, 0x75, 0xb1, 0x31, 0x0, 0x1c, 0x92, 0x52, 0x52, 0x0, 0xc, 0x92, 0x52, 0x4c, 0x0, 0x1c, 0x92, 0x5c, 0x90, 0x0, 0xe, 0xd2, 0x4e, 0xc2, 0x0, 0xe, 0xd0, 0x10, 0x10, 0x0, 0x6, 0xc8, 0x4, 0x98, 0x8, 0x8, 0xe, 0xc8, 0x7, 0x0, 0x12, 0x52, 0x52, 0x4f, 0x0, 0x11, 0x31, 0x2a, 0x44, 0x0, 0x11, 0x31, 0x35, 0xbb, 0x0, 0x12, 0x4c, 0x8c, 0x92, 0x0, 0x11, 0x2a, 0x44, 0x98, 0x0, 0x1e, 0xc4, 0x88, 0x1e, 0x6, 0xc4, 0x8c, 0x84, 0x86, 0x8, 0x8, 0x8, 0x8, 0x8, 0x18, 0x8, 0xc, 0x88, 0x18, 0x0, 0x0, 0xc, 0x83, 0x60];
|
||||||
|
|
||||||
|
let nb = data.length;
|
||||||
|
let n = nb / 5;
|
||||||
|
let font = createInternalImage(nb);
|
||||||
|
for (let c = 0; c < n; c++) {
|
||||||
|
for (let row = 0; row < 5; row++) {
|
||||||
|
let char = data[c * 5 + row];
|
||||||
|
for (let col = 0; col < 5; col++) {
|
||||||
|
if ((char & (1 << col)) != 0)
|
||||||
|
font.set((c * 5 + 4) - col, row, 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return font;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnimationOptions {
|
||||||
|
interval: number;
|
||||||
|
// false means last frame
|
||||||
|
frame: () => boolean;
|
||||||
|
whenDone?: (cancelled: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AnimationQueue {
|
||||||
|
private queue: AnimationOptions[] = [];
|
||||||
|
private process: () => void;
|
||||||
|
|
||||||
|
constructor(private runtime: Runtime) {
|
||||||
|
this.process = () => {
|
||||||
|
let top = this.queue[0]
|
||||||
|
if (!top) return
|
||||||
|
if (this.runtime.dead) return
|
||||||
|
runtime = this.runtime
|
||||||
|
let res = top.frame()
|
||||||
|
runtime.queueDisplayUpdate()
|
||||||
|
runtime.maybeUpdateDisplay()
|
||||||
|
if (res === false) {
|
||||||
|
this.queue.shift();
|
||||||
|
// if there is already something in the queue, start processing
|
||||||
|
if (this.queue[0])
|
||||||
|
setTimeout(this.process, this.queue[0].interval)
|
||||||
|
// this may push additional stuff
|
||||||
|
top.whenDone(false);
|
||||||
|
} else {
|
||||||
|
setTimeout(this.process, top.interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancelAll() {
|
||||||
|
let q = this.queue
|
||||||
|
this.queue = []
|
||||||
|
for (let a of q) {
|
||||||
|
a.whenDone(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancelCurrent() {
|
||||||
|
let top = this.queue[0]
|
||||||
|
if (top) {
|
||||||
|
this.queue.shift();
|
||||||
|
top.whenDone(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enqueue(anim: AnimationOptions) {
|
||||||
|
if (!anim.whenDone) anim.whenDone = () => { };
|
||||||
|
this.queue.push(anim)
|
||||||
|
// we start processing when the queue goes from 0 to 1
|
||||||
|
if (this.queue.length == 1)
|
||||||
|
this.process()
|
||||||
|
}
|
||||||
|
|
||||||
|
public executeAsync(anim: AnimationOptions) {
|
||||||
|
U.assert(!anim.whenDone)
|
||||||
|
return new Promise<boolean>((resolve, reject) => {
|
||||||
|
anim.whenDone = resolve
|
||||||
|
this.enqueue(anim)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim.images {
|
||||||
|
export function createImage(img: Image) {
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
export function createBigImage(img: Image) {
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim.ImageMethods {
|
||||||
|
export function showImage(leds: Image, offset: number) {
|
||||||
|
if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE);
|
||||||
|
|
||||||
|
leds.copyTo(offset, 5, board().ledMatrixState.image, 0)
|
||||||
|
runtime.queueDisplayUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function plotImage(leds: Image, offset: number): void {
|
||||||
|
if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE);
|
||||||
|
|
||||||
|
leds.copyTo(offset, 5, board().ledMatrixState.image, 0)
|
||||||
|
runtime.queueDisplayUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function height(leds: Image): number {
|
||||||
|
if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE);
|
||||||
|
return Image.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function width(leds: Image): number {
|
||||||
|
if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE);
|
||||||
|
return leds.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function plotFrame(leds: Image, frame: number) {
|
||||||
|
ImageMethods.plotImage(leds, frame * Image.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showFrame(leds: Image, frame: number) {
|
||||||
|
ImageMethods.showImage(leds, frame * Image.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pixel(leds: Image, x: number, y: number): number {
|
||||||
|
if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE);
|
||||||
|
return leds.get(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setPixel(leds: Image, x: number, y: number, v: number) {
|
||||||
|
if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE);
|
||||||
|
leds.set(x, y, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clear(leds: Image) {
|
||||||
|
if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE);
|
||||||
|
|
||||||
|
leds.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setPixelBrightness(i: Image, x: number, y: number, b: number) {
|
||||||
|
if (!i) panic(PanicCode.MICROBIT_NULL_DEREFERENCE);
|
||||||
|
|
||||||
|
i.set(x, y, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pixelBrightness(i: Image, x: number, y: number): number {
|
||||||
|
if (!i) panic(PanicCode.MICROBIT_NULL_DEREFERENCE);
|
||||||
|
|
||||||
|
return i.get(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scrollImage(leds: Image, stride: number, interval: number): void {
|
||||||
|
if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE);
|
||||||
|
if (stride == 0) stride = 1;
|
||||||
|
|
||||||
|
let cb = getResume();
|
||||||
|
let off = stride > 0 ? 0 : leds.width - 1;
|
||||||
|
let display = board().ledMatrixState.image;
|
||||||
|
|
||||||
|
board().ledMatrixState.animationQ.enqueue({
|
||||||
|
interval: interval,
|
||||||
|
frame: () => {
|
||||||
|
//TODO: support right to left.
|
||||||
|
if (off >= leds.width || off < 0) return false;
|
||||||
|
stride > 0 ? display.shiftLeft(stride) : display.shiftRight(-stride);
|
||||||
|
let c = Math.min(stride, leds.width - off);
|
||||||
|
leds.copyTo(off, c, display, 5 - stride)
|
||||||
|
off += stride;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
whenDone: cb
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim.basic {
|
||||||
|
export function showNumber(x: number, interval: number) {
|
||||||
|
if (interval < 0) return;
|
||||||
|
|
||||||
|
let leds = createImageFromString(x.toString());
|
||||||
|
if (x < 0 || x >= 10) ImageMethods.scrollImage(leds, 1, interval);
|
||||||
|
else showLeds(leds, interval * 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showString(s: string, interval: number) {
|
||||||
|
if (interval < 0) return;
|
||||||
|
if (s.length == 0) {
|
||||||
|
clearScreen();
|
||||||
|
pause(interval * 5);
|
||||||
|
} else {
|
||||||
|
if (s.length == 1) showLeds(createImageFromString(s + " "), interval * 5)
|
||||||
|
else ImageMethods.scrollImage(createImageFromString(s + " "), 1, interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showLeds(leds: Image, delay: number): void {
|
||||||
|
showAnimation(leds, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearScreen() {
|
||||||
|
board().ledMatrixState.image.clear();
|
||||||
|
runtime.queueDisplayUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showAnimation(leds: Image, interval: number): void {
|
||||||
|
ImageMethods.scrollImage(leds, 5, interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function plotLeds(leds: Image): void {
|
||||||
|
ImageMethods.plotImage(leds, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim.led {
|
||||||
|
export function plot(x: number, y: number) {
|
||||||
|
board().ledMatrixState.image.set(x, y, 255);
|
||||||
|
runtime.queueDisplayUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unplot(x: number, y: number) {
|
||||||
|
board().ledMatrixState.image.set(x, y, 0);
|
||||||
|
runtime.queueDisplayUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function point(x: number, y: number): boolean {
|
||||||
|
return !!board().ledMatrixState.image.get(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function brightness(): number {
|
||||||
|
return board().ledMatrixState.brigthness;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setBrightness(value: number): void {
|
||||||
|
board().ledMatrixState.brigthness = value;
|
||||||
|
runtime.queueDisplayUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopAnimation(): void {
|
||||||
|
board().ledMatrixState.animationQ.cancelAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setDisplayMode(mode: DisplayMode): void {
|
||||||
|
board().ledMatrixState.displayMode = mode;
|
||||||
|
runtime.queueDisplayUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function screenshot(): Image {
|
||||||
|
let img = createImage(5)
|
||||||
|
board().ledMatrixState.image.copyTo(0, 5, img, 0);
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
}
|
17
sim/state/lightsensor.ts
Normal file
17
sim/state/lightsensor.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace pxsim {
|
||||||
|
export class LightSensorState {
|
||||||
|
usesLightLevel = false;
|
||||||
|
lightLevel = 128;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim.input {
|
||||||
|
export function lightLevel(): number {
|
||||||
|
let b = board().lightSensorState;
|
||||||
|
if (!b.usesLightLevel) {
|
||||||
|
b.usesLightLevel = true;
|
||||||
|
runtime.queueDisplayUpdate();
|
||||||
|
}
|
||||||
|
return b.lightLevel;
|
||||||
|
}
|
||||||
|
}
|
228
sim/state/misc.ts
Normal file
228
sim/state/misc.ts
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
namespace pxsim {
|
||||||
|
/**
|
||||||
|
* Error codes used in the micro:bit runtime.
|
||||||
|
*/
|
||||||
|
export enum PanicCode {
|
||||||
|
// PANIC Codes. These are not return codes, but are terminal conditions.
|
||||||
|
// These induce a panic operation, where all code stops executing, and a panic state is
|
||||||
|
// entered where the panic code is diplayed.
|
||||||
|
|
||||||
|
// Out out memory error. Heap storage was requested, but is not available.
|
||||||
|
MICROBIT_OOM = 20,
|
||||||
|
|
||||||
|
// Corruption detected in the micro:bit heap space
|
||||||
|
MICROBIT_HEAP_ERROR = 30,
|
||||||
|
|
||||||
|
// Dereference of a NULL pointer through the ManagedType class,
|
||||||
|
MICROBIT_NULL_DEREFERENCE = 40,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function panic(code: number) {
|
||||||
|
console.log("PANIC:", code)
|
||||||
|
led.setBrightness(255);
|
||||||
|
let img = board().ledMatrixState.image;
|
||||||
|
img.clear();
|
||||||
|
img.set(0, 4, 255);
|
||||||
|
img.set(1, 3, 255);
|
||||||
|
img.set(2, 3, 255);
|
||||||
|
img.set(3, 3, 255);
|
||||||
|
img.set(4, 4, 255);
|
||||||
|
img.set(0, 0, 255);
|
||||||
|
img.set(1, 0, 255);
|
||||||
|
img.set(0, 1, 255);
|
||||||
|
img.set(1, 1, 255);
|
||||||
|
img.set(3, 0, 255);
|
||||||
|
img.set(4, 0, 255);
|
||||||
|
img.set(3, 1, 255);
|
||||||
|
img.set(4, 1, 255);
|
||||||
|
runtime.updateDisplay();
|
||||||
|
|
||||||
|
throw new Error("PANIC " + code)
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace AudioContextManager {
|
||||||
|
let _context: any; // AudioContext
|
||||||
|
let _vco: any; // OscillatorNode;
|
||||||
|
let _vca: any; // GainNode;
|
||||||
|
|
||||||
|
function context(): any {
|
||||||
|
if (!_context) _context = freshContext();
|
||||||
|
return _context;
|
||||||
|
}
|
||||||
|
|
||||||
|
function freshContext(): any {
|
||||||
|
(<any>window).AudioContext = (<any>window).AudioContext || (<any>window).webkitAudioContext;
|
||||||
|
if ((<any>window).AudioContext) {
|
||||||
|
try {
|
||||||
|
// this call my crash.
|
||||||
|
// SyntaxError: audio resources unavailable for AudioContext construction
|
||||||
|
return new (<any>window).AudioContext();
|
||||||
|
} catch (e) { }
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stop() {
|
||||||
|
if (_vca) _vca.gain.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tone(frequency: number, gain: number) {
|
||||||
|
if (frequency <= 0) return;
|
||||||
|
let ctx = context();
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
gain = Math.max(0, Math.min(1, gain));
|
||||||
|
if (!_vco) {
|
||||||
|
try {
|
||||||
|
_vco = ctx.createOscillator();
|
||||||
|
_vca = ctx.createGain();
|
||||||
|
_vco.connect(_vca);
|
||||||
|
_vca.connect(ctx.destination);
|
||||||
|
_vca.gain.value = gain;
|
||||||
|
_vco.start(0);
|
||||||
|
} catch (e) {
|
||||||
|
_vco = undefined;
|
||||||
|
_vca = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_vco.frequency.value = frequency;
|
||||||
|
_vca.gain.value = gain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuntimeOptions {
|
||||||
|
theme: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EventBus {
|
||||||
|
private queues: Map<EventQueue<number>> = {};
|
||||||
|
|
||||||
|
constructor(private runtime: Runtime) { }
|
||||||
|
|
||||||
|
listen(id: number, evid: number, handler: RefAction) {
|
||||||
|
let k = id + ":" + evid;
|
||||||
|
let queue = this.queues[k];
|
||||||
|
if (!queue) queue = this.queues[k] = new EventQueue<number>(this.runtime);
|
||||||
|
queue.handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
queue(id: number, evid: number, value: number = 0) {
|
||||||
|
let k = id + ":" + evid;
|
||||||
|
let queue = this.queues[k];
|
||||||
|
if (queue) queue.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim.basic {
|
||||||
|
export var pause = thread.pause;
|
||||||
|
export var forever = thread.forever;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim.control {
|
||||||
|
export var inBackground = thread.runInBackground;
|
||||||
|
|
||||||
|
export function reset() {
|
||||||
|
U.userError("reset not implemented in simulator yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function waitMicros(micros: number) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deviceName(): string {
|
||||||
|
let b = board();
|
||||||
|
return b && b.id
|
||||||
|
? b.id.slice(0, 4)
|
||||||
|
: "abcd";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deviceSerialNumber(): number {
|
||||||
|
let b = board();
|
||||||
|
return parseInt(b && b.id
|
||||||
|
? b.id.slice(1)
|
||||||
|
: "42");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onEvent(id: number, evid: number, handler: RefAction) {
|
||||||
|
pxt.registerWithDal(id, evid, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function raiseEvent(id: number, evid: number, mode: number) {
|
||||||
|
// TODO mode?
|
||||||
|
board().bus.queue(id, evid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim.pxt {
|
||||||
|
export function registerWithDal(id: number, evid: number, handler: RefAction) {
|
||||||
|
board().bus.listen(id, evid, handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim.input {
|
||||||
|
export function runningTime(): number {
|
||||||
|
return runtime.runningTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calibrate() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim.pins {
|
||||||
|
export function onPulsed(name: number, pulse: number, body: RefAction) {
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pulseDuration(): number {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBuffer(sz: number) {
|
||||||
|
return pxsim.BufferMethods.createBuffer(sz)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pulseIn(name: number, value: number, maxDuration: number): number {
|
||||||
|
let pin = getPin(name);
|
||||||
|
if (!pin) return 0;
|
||||||
|
|
||||||
|
return 5000;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function spiWrite(value: number): number {
|
||||||
|
// TODO
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function i2cReadBuffer(address: number, size: number, repeat?: boolean): RefBuffer {
|
||||||
|
// fake reading zeros
|
||||||
|
return createBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function i2cWriteBuffer(address: number, buf: RefBuffer, repeat?: boolean): void {
|
||||||
|
// fake - noop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim.bluetooth {
|
||||||
|
export function startIOPinService(): void {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
export function startLEDService(): void {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
export function startTemperatureService(): void {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
export function startMagnetometerService(): void {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
export function startAccelerometerService(): void {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
export function startButtonService(): void {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
57
sim/state/neopixel.ts
Normal file
57
sim/state/neopixel.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
namespace pxsim {
|
||||||
|
export function sendBufferAsm(buffer: Buffer, pin: DigitalPin) {
|
||||||
|
let b = board();
|
||||||
|
if (b) {
|
||||||
|
let np = b.neopixelState;
|
||||||
|
if (np) {
|
||||||
|
np.updateBuffer(buffer, pin);
|
||||||
|
runtime.queueDisplayUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim {
|
||||||
|
export enum NeoPixelMode {RGB, RGBW};
|
||||||
|
export type RGBW = [number, number, number, number];
|
||||||
|
|
||||||
|
function readNeoPixelBuffer(inBuffer: Uint8Array[], outColors: RGBW[], mode: NeoPixelMode) {
|
||||||
|
let buf = inBuffer;
|
||||||
|
let stride = mode === NeoPixelMode.RGBW ? 4 : 3;
|
||||||
|
let pixelCount = Math.floor(buf.length / stride);
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
// NOTE: for whatever reason, NeoPixels pack GRB not RGB
|
||||||
|
let r = buf[i * stride + 1] as any as number
|
||||||
|
let g = buf[i * stride + 0] as any as number
|
||||||
|
let b = buf[i * stride + 2] as any as number
|
||||||
|
let w = 0;
|
||||||
|
if (stride === 4)
|
||||||
|
w = buf[i * stride + 3] as any as number
|
||||||
|
outColors[i] = [r, g, b, w]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NeoPixelState {
|
||||||
|
private buffers: {[pin: number]: Uint8Array[]} = {};
|
||||||
|
private colors: {[pin: number]: RGBW[]} = {};
|
||||||
|
private dirty: {[pin: number]: boolean} = {};
|
||||||
|
|
||||||
|
public updateBuffer(buffer: Buffer, pin: DigitalPin) {
|
||||||
|
//update buffers
|
||||||
|
let buf = <Uint8Array[]>(<any>buffer).data;
|
||||||
|
this.buffers[pin] = buf;
|
||||||
|
this.dirty[pin] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getColors(pin: number, mode: NeoPixelMode): RGBW[] {
|
||||||
|
let outColors = this.colors[pin] || (this.colors[pin] = []);
|
||||||
|
if (this.dirty[pin]) {
|
||||||
|
let buf = this.buffers[pin] || (this.buffers[pin] = []);
|
||||||
|
readNeoPixelBuffer(buf, outColors, mode);
|
||||||
|
this.dirty[pin] = false;
|
||||||
|
}
|
||||||
|
return outColors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
158
sim/state/radio.ts
Normal file
158
sim/state/radio.ts
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
namespace pxsim {
|
||||||
|
export interface PacketBuffer {
|
||||||
|
data: number[] | string;
|
||||||
|
rssi?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RadioDatagram {
|
||||||
|
datagram: PacketBuffer[] = [];
|
||||||
|
lastReceived: PacketBuffer = {
|
||||||
|
data: [0, 0, 0, 0],
|
||||||
|
rssi: -1
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(private runtime: Runtime) {
|
||||||
|
}
|
||||||
|
|
||||||
|
queue(packet: PacketBuffer) {
|
||||||
|
if (this.datagram.length < 4) {
|
||||||
|
this.datagram.push(packet);
|
||||||
|
}
|
||||||
|
(<DalBoard>runtime.board).bus.queue(DAL.MICROBIT_ID_RADIO, DAL.MICROBIT_RADIO_EVT_DATAGRAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
send(buffer: number[] | string) {
|
||||||
|
if (buffer instanceof String) buffer = buffer.slice(0, 32);
|
||||||
|
else buffer = buffer.slice(0, 8);
|
||||||
|
|
||||||
|
Runtime.postMessage(<SimulatorRadioPacketMessage>{
|
||||||
|
type: "radiopacket",
|
||||||
|
data: buffer
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
recv(): PacketBuffer {
|
||||||
|
let r = this.datagram.shift();
|
||||||
|
if (!r) r = {
|
||||||
|
data: [0, 0, 0, 0],
|
||||||
|
rssi: -1
|
||||||
|
};
|
||||||
|
return this.lastReceived = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RadioBus {
|
||||||
|
// uint8_t radioDefaultGroup = MICROBIT_RADIO_DEFAULT_GROUP;
|
||||||
|
groupId = 0; // todo
|
||||||
|
power = 0;
|
||||||
|
transmitSerialNumber = false;
|
||||||
|
datagram: RadioDatagram;
|
||||||
|
|
||||||
|
constructor(private runtime: Runtime) {
|
||||||
|
this.datagram = new RadioDatagram(runtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
setGroup(id: number) {
|
||||||
|
this.groupId = id & 0xff; // byte only
|
||||||
|
}
|
||||||
|
|
||||||
|
setTransmitPower(power: number) {
|
||||||
|
this.power = Math.max(0, Math.min(7, power));
|
||||||
|
}
|
||||||
|
|
||||||
|
setTransmitSerialNumber(sn: boolean) {
|
||||||
|
this.transmitSerialNumber = !!sn;
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast(msg: number) {
|
||||||
|
Runtime.postMessage(<SimulatorEventBusMessage>{
|
||||||
|
type: "eventbus",
|
||||||
|
id: DAL.MES_BROADCAST_GENERAL_ID,
|
||||||
|
eventid: msg,
|
||||||
|
power: this.power,
|
||||||
|
group: this.groupId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RadioState {
|
||||||
|
bus: RadioBus;
|
||||||
|
|
||||||
|
constructor(runtime: Runtime) {
|
||||||
|
this.bus = new RadioBus(runtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public recievePacket(packet: SimulatorRadioPacketMessage) {
|
||||||
|
this.bus.datagram.queue({ data: packet.data, rssi: packet.rssi || 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim.radio {
|
||||||
|
export function broadcastMessage(msg: number): void {
|
||||||
|
board().radioState.bus.broadcast(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onBroadcastMessageReceived(msg: number, handler: RefAction): void {
|
||||||
|
pxt.registerWithDal(DAL.MES_BROADCAST_GENERAL_ID, msg, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setGroup(id: number): void {
|
||||||
|
board().radioState.bus.setGroup(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setTransmitPower(power: number): void {
|
||||||
|
board().radioState.bus.setTransmitPower(power);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setTransmitSerialNumber(transmit: boolean): void {
|
||||||
|
board().radioState.bus.setTransmitSerialNumber(transmit);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendNumber(value: number): void {
|
||||||
|
board().radioState.bus.datagram.send([value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendString(msg: string): void {
|
||||||
|
board().radioState.bus.datagram.send(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeValueToSerial(): void {
|
||||||
|
let b = board();
|
||||||
|
let v = b.radioState.bus.datagram.recv().data[0];
|
||||||
|
b.writeSerial(`{v:${v}}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendValue(name: string, value: number) {
|
||||||
|
board().radioState.bus.datagram.send([value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function receiveNumber(): number {
|
||||||
|
let buffer = board().radioState.bus.datagram.recv().data;
|
||||||
|
if (buffer instanceof Array) return buffer[0];
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function receiveString(): string {
|
||||||
|
let buffer = board().radioState.bus.datagram.recv().data;
|
||||||
|
if (typeof buffer === "string") return <string>buffer;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function receivedNumberAt(index: number): number {
|
||||||
|
let buffer = board().radioState.bus.datagram.recv().data;
|
||||||
|
if (buffer instanceof Array) return buffer[index] || 0;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function receivedSignalStrength(): number {
|
||||||
|
return board().radioState.bus.datagram.lastReceived.rssi;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onDataReceived(handler: RefAction): void {
|
||||||
|
pxt.registerWithDal(DAL.MICROBIT_ID_RADIO, DAL.MICROBIT_RADIO_EVT_DATAGRAM, handler);
|
||||||
|
radio.receiveNumber();
|
||||||
|
}
|
||||||
|
}
|
54
sim/state/serial.ts
Normal file
54
sim/state/serial.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
namespace pxsim {
|
||||||
|
export class SerialState {
|
||||||
|
serialIn: string[] = [];
|
||||||
|
|
||||||
|
public recieveData(data: string) {
|
||||||
|
this.serialIn.push();
|
||||||
|
}
|
||||||
|
|
||||||
|
readSerial() {
|
||||||
|
let v = this.serialIn.shift() || "";
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
serialOutBuffer: string = "";
|
||||||
|
writeSerial(s: string) {
|
||||||
|
for (let i = 0; i < s.length; ++i) {
|
||||||
|
let c = s[i];
|
||||||
|
this.serialOutBuffer += c;
|
||||||
|
if (c == "\n") {
|
||||||
|
Runtime.postMessage(<SimulatorSerialMessage>{
|
||||||
|
type: "serial",
|
||||||
|
data: this.serialOutBuffer,
|
||||||
|
id: runtime.id
|
||||||
|
})
|
||||||
|
this.serialOutBuffer = ""
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim.serial {
|
||||||
|
export function writeString(s: string) {
|
||||||
|
board().writeSerial(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readString(): string {
|
||||||
|
return board().serialState.readSerial();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readLine(): string {
|
||||||
|
return board().serialState.readSerial();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onDataReceived(delimiters: string, handler: RefAction) {
|
||||||
|
let b = board();
|
||||||
|
b.bus.listen(DAL.MICROBIT_ID_SERIAL, DAL.MICROBIT_SERIAL_EVT_DELIM_MATCH, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redirect(tx: number, rx: number, rate: number) {
|
||||||
|
// TODO?
|
||||||
|
}
|
||||||
|
}
|
18
sim/state/thermometer.ts
Normal file
18
sim/state/thermometer.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
namespace pxsim {
|
||||||
|
export class ThermometerState {
|
||||||
|
usesTemperature = false;
|
||||||
|
temperature = 21;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim.input {
|
||||||
|
export function temperature(): number {
|
||||||
|
let b = board();
|
||||||
|
if (!b.thermometerState.usesTemperature) {
|
||||||
|
b.thermometerState.usesTemperature = true;
|
||||||
|
runtime.queueDisplayUpdate();
|
||||||
|
}
|
||||||
|
return b.thermometerState.temperature;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user